更详细的讲解和代码调试演示过程,请参看视频
用java开发C语言编译器
上一节,我们实现了以数组的方式对指针指向的动态内存进行读写,本节,我们实现通过指针直接读写内存,完成本节代码后,我们的解释器能够解释执行下面的代码:
void main() {
char *p;
char *p1;
p = malloc(2);
p1 = malloc(1);
p1[0] = 0;
*(p+0) = 1;
*(p+1) = 2;
*(p+2) = 3;
printf("p[0] is : %d, p[1] is : %d, p1[0] is :%d", *(p+0), *(p+1), p1[0]);
}
代码中,*(p+1) 就是通过指针p对内存进行直接读写,本节的目的就是使得解释器能够解释执行这样的语句。
这种语句对应的语法表达式如下:
UNARY -> EXPR
UNARY -> STAR UNARY
括号以及里面的表达式对应的正是EXPR, *(p+1) 这一句代码对应的表达式就是UNARY -> STAR UNARY
当解释器读取到语句*(p+1)时,解释器先解释执行p+1, 它的做法是先把p对应的值读出来,由于p是指针变量,因此它的值是其对应的内存地址,然后计算p+1的值,把得到的结果当做内存地址,通过MemoryHeap对象获得该地址对应的字节数组,最后把对内存的读取或写入转换成对字节数值的读取或写入。
解释器实现对*(p+1)此类语句的解析是在类UnaryExecutor.java中实现的,我们看看对应代码:
public class UnaryNodeExecutor extends BaseExecutor{
@Override
public Object Execute(ICodeNode root) {
executeChildren(root);
int production = (Integer)root.getAttribute(ICodeKey.PRODUCTION);
String text ;
Symbol symbol;
Object value;
ICodeNode child;
switch (production) {
...
case CGrammarInitializer.Start_Unary_TO_Unary:
child = root.getChildren().get(0);
int addr = (Integer)child.getAttribute(ICodeKey.VALUE); //get mem addr
MemoryHeap memHeap = MemoryHeap.getInstance();
Map.Entry entry = memHeap.getMem(addr);
int offset = addr - entry.getKey();
if (entry != null) {
byte[] memByte = entry.getValue();
root.setAttribute(ICodeKey.VALUE, memByte[offset]);
}
DirectMemValueSetter directMemSetter = new DirectMemValueSetter(addr);
root.setAttribute(ICodeKey.SYMBOL, directMemSetter);
break;
...
}
上面代码中:
child = root.getChildren().get(0);
int addr = (Integer)child.getAttribute(ICodeKey.VALUE);
这两句的作用是获得p+1的结果,把该结果当做动态内存的读写地址, 然后通过MemoryHeap查找该地址对应的字节数组。
p+1所对应的地址,不是所分配内存的起始地址,变量p对应的值才是内存的起始地址,如果p的值是10000, *(p+1)表示读取内存地址为10001处的字节数据,那么解释器会通过MemoryHeap,得到10000所对应的字节数组,然后读出该数组下标为1处的字节数据,该逻辑正是由下面的代码片段实现的。
MemoryHeap memHeap = MemoryHeap.getInstance();
Map.Entry entry = memHeap.getMem(addr);
int offset = addr - entry.getKey();
if (entry != null) {
byte[] memByte = entry.getValue();
root.setAttribute(ICodeKey.VALUE, memByte[offset]);
}
*(p+1) = 1; 表示把1写入到内存地址10001处的字节,那么解释器通过MemoryHeap获得10000所对应的字节数组,然后把数值1写入到字节数组下标为1处的字节,这个写入逻辑是通过类DirectMemValueSetter来实现的,我们看看它的实现代码。
public class DirectMemValueSetter implements IValueSetter {
private int memAddr = 0;
public DirectMemValueSetter(int memAddr) {
this.memAddr = memAddr;
}
@Override
public void setValue(Object obj) throws Exception {
MemoryHeap memHeap = MemoryHeap.getInstance();
Map.Entry entry = memHeap.getMem(memAddr);
byte[] content = entry.getValue();
int offset = memAddr - entry.getKey();
Integer i = (Integer)obj;
content[offset] = (byte)(i & 0xFF);
}
}
该类的逻辑就是通过MemoryHeap对象找到对应的字节数组,然后把要写入的地址减去动态内存的入口地址,进而得到写入的地址偏移,这个偏移作为字节数组的下标,把数组写入到字节数组中。
通过上面的代码,解释器便能够解释执行*(p+1)=1;这种通过指针直接读写内存的语句。
我们给定的C程序中,存在一个故意设置的bug,就是内存越界读取,p指针对应的内存只有两个字节长,但*(p+2) = 3;是一种内存越界读写的情况,这条语句的作用是,把数值3写入到p1指针所对应的内存中,于是解释器执行上述代码后得到的输出结果是:
p[0] is : 1, p[1] is : 2, p1[0] is :3
内存越界读写是C程序的一大弊病,大多数C语言开发的程序所出现的那些难以复现,难以调试的bug,几乎都是由于内存越界读写导致的。
更加详细的代码讲解和调试演示过程,请参看视频。