更详细的讲解和代码调试演示过程,请参看视频
用java开发C语言编译器
C语言有一个强大的功能,就是通过指针直接操作内存,正是因为C语言含有直接读写内存的机制,使得C语言在系统开发,底层开发等方面,占据了难以撼动的地位,同时也正是这个原因,C语言开发的程序常常出现内存泄漏和野指针等及其令人头疼的问题。
本节,我们为解释器添加动态内存的分配和读写机制,完成本节内容后,解释器能准确解释执行下面代码:
void main() {
char *p;
p = malloc(2);
printf("addr of p is : %d\n", p);
p[0] = 1;
p[1] = 2;
printf("p[0] is : %d, p[1] is : %d", p[0], p[1]);
}
上面代码中,通过库函数调用malloc,先分配2字节的内存,接下来分别对分配的内存进行读写赋值,然后再把赋值后的内存内容打印出来。我们先看看怎么在解释器上实现动态内存的分配机制。
malloc 调用后,会返回一个整形值,这个数值的内容无关紧要,只要知道以这个数值开始的地址,连续若干个字节的内存是可以提供给程序任意读写的就可以了。也就是说,这个数值相当于一把钥匙,通过这把钥匙,我们就能打开能用于打开存储东西的抽屉。我们看看如何在解释器中模拟这个机制,该机制的实现在MemoryHeap.java中:
package backend;
import java.util.HashMap;
import java.util.Map;
public class MemoryHeap {
private static int initAddr = 10000;
private static MemoryHeap instance = null;
private static HashMap<Integer, byte[]> memMap = new HashMap<Integer, byte[]>();
public static MemoryHeap getInstance() {
if (instance == null) {
instance = new MemoryHeap();
}
return instance;
}
public static int allocMem(int size) {
byte[] mem = new byte[size];
memMap.put(initAddr, mem);
int allocAddr = initAddr;
initAddr += size;
return allocAddr;
}
public static Map.Entry<Integer, byte[]> getMem(int addr) {
int initAddr = 0;
for (Map.Entry<Integer, byte[]> entry : memMap.entrySet()) {
if (entry.getKey() <= addr && entry.getKey() > initAddr) {
initAddr = entry.getKey();
byte[] mems = entry.getValue();
if (initAddr + mems.length > addr) {
return entry;
}
}
}
return null;
}
private MemoryHeap() {
}
}
allocMem用来生成动态内存,调用该函数是,传入的参数就是要申请的内存大小。该类用一个HashMap来表示动态内存,map的key用来模拟动态内存的地址,value则是byte[] 数据类型,用来模拟分配的动态内存。当这个函数调用时,它使用一个整形数值来表示内存的虚拟起始地址,然后构造一个给定长度的字节数组,把整形数组和分配的字节数组结合起来,放入到map 中,以后程序可以通过对应的整形数来获得字节数组。
有了虚拟起始地址后,通过这个地址,调用getMem,就可以获得对应的字节数组,程序对该数组的读取,就相当于对动态内存的读取,getMem返回的是一个Entry对象,这个对象包含了虚拟起始地址和byte类型数组。
p[0] 表示读取分配的动态内存的第一个字节,它相当于把一组连续的内存当做数组来访问,我们以前讲解过,读取数组元素是由UnaryNodeExecutor来实现的,因此对应的内存读取机制其实现代码如下:
public class UnaryNodeExecutor extends BaseExecutor{
@Override
public Object Execute(ICodeNode root) {
executeChildren(root);
....
switch (production) {
....
case CGrammarInitializer.Unary_LB_Expr_RB_TO_Unary:
child = root.getChildren().get(0);
symbol = (Symbol)child.getAttribute(ICodeKey.SYMBOL);
child = root.getChildren().get(1);
int index = (Integer)child.getAttribute(ICodeKey.VALUE);
try {
Declarator declarator = symbol.getDeclarator(Declarator.ARRAY);
if (declarator != null) {
Object val = declarator.getElement(index);
root.setAttribute(ICodeKey.VALUE, val);
ArrayValueSetter setter = new ArrayValueSetter(symbol, index);
root.setAttribute(ICodeKey.SYMBOL, setter);
root.setAttribute(ICodeKey.TEXT, symbol.getName());
}
Declarator pointer = symbol.getDeclarator(Declarator.POINTER);
if (pointer != null) {
setPointerValue(root, symbol, index);
//create a PointerSetter
PointerValueSetter pv = new PointerValueSetter(symbol, index);
root.setAttribute(ICodeKey.SYMBOL, pv);
root.setAttribute(ICodeKey.TEXT, symbol.getName());
}
}catch (Exception e) {
System.err.println(e.getMessage());
System.exit(1);
}
break;
}
}
}
private void setPointerValue(ICodeNode root, Symbol symbol, int index) {
MemoryHeap memHeap = MemoryHeap.getInstance();
int addr = (Integer)symbol.getValue();
Map.Entry<Integer, byte[]> entry = memHeap.getMem(addr);
byte[] content = entry.getValue();
if (symbol.getByteSize() == 1) {
root.setAttribute(ICodeKey.VALUE, content[index]);
} else {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.put(content, index, 4);
buffer.flip();
root.setAttribute(ICodeKey.VALUE, buffer.getLong());
}
}
当解释器解析到语句p[0]时,p[0]可能表示读取数组下标为0的元素,也可以表示读取动态内存从起始地址开始,偏移为0出的内存数据,怎么判断到底是哪一种情况呢。我们在以前实现类型系统时,在解析过程中,如果变量定义成数组或指针,那么我们会在它的Symbol 对象中添加一个成员,称之为Declarator,用这个类来对变量进行描述,如果变量p是数组,那么对应的Delcarator 类型是ARRAY, 如果是指针,那么对应的类型是POINTER.
如果p是指针的话,那么if(pointer != null) 里面的代码就会执行,首先它通过调用setPointerValue, 把给定内存的内容读取出来,对应于p[0],就是把指针p 指向的内存,读取偏移为0出的内存数据。
setPointerValue的逻辑是,先得到内存的起始地址,这个地址的数值就是allocMem返回的,通过这个地址,在MemoryHeap的哈希表中找到对应的字节数值,这个字节数组就是用来模拟动态内存的。它的输入参数index对应于地址偏移,symbol.getByteSize() 用来获得指针变量的数据类型,如果变量类型是char, 那么我们一次读取一字节数据,若不然,我们一次读取4字节的数据。
当解释器解析到语句 p[0] = 1 ; 时,着表明程序想对分配的内存进行写入,我们用一个类PointerValueSetter,把对内存的写入逻辑封装起来,该类代码如下:
public class PointerValueSetter implements IValueSetter {
private Symbol symbol;
private int index = 0;
@Override
public void setValue(Object obj) throws Exception {
int addr = (Integer)symbol.getValue();
MemoryHeap memHeap = MemoryHeap.getInstance();
Map.Entry<Integer, byte[]> entry = memHeap.getMem(addr);
byte[] content = entry.getValue();
Integer i = (Integer)obj;
try {
if (symbol.getByteSize() == 4) {
content[index] = (byte)((i>>24) & 0xFF);
content[index + 1] = (byte)((i>>16) & 0xFF);
content[index + 2] = (byte)((i>>8) & 0xFF);
content[index + 3] = (byte)(i & 0xFF);
} else {
content[index] = (byte)(i & 0xFF);
}
} catch (Exception e) {
System.err.println(e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
public PointerValueSetter(Symbol symbol, int index) {
this.symbol = symbol;
this.index = index;
}
}
它的逻辑是,先从变量对应的Symbol对象中,获得变量的值,在指针变量的情况下,这个值代表的是内存的起始地址,根据这个地址,通过MemoryHeap获得对应的字节数组对象,然后根据偏移,把数据写入到字节数值中,在此,我们暂时默认写入的数据要不是4字节的int, 要不就是但自己的byte, 以后要读写更复杂的数据内容时,我们再做相应修改。
对应变量赋值语句 p[0] = 1; 它的实现是在NoCommaExprExecutor这个类中的,我们看看对应实现:
public class NoCommaExprExecutor extends BaseExecutor{
ExecutorFactory factory = ExecutorFactory.getExecutorFactory();
@Override
public Object Execute(ICodeNode root) {
executeChildren(root);
....
switch (production) {
....
case CGrammarInitializer.NoCommaExpr_Equal_NoCommaExpr_TO_NoCommaExpr:
child = root.getChildren().get(0);
String t = (String)child.getAttribute(ICodeKey.TEXT);
IValueSetter setter;
setter = (IValueSetter)child.getAttribute(ICodeKey.SYMBOL);
child = root.getChildren().get(1);
value = child.getAttribute(ICodeKey.VALUE);
try {
setter.setValue(value);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.err.println("Runtime Error: Assign Value Error");
}
child = root.getChildren().get(0);
child.setAttribute(ICodeKey.VALUE, value);
copyChild(root, root.getChildren().get(0));
break;
}
这段代码跟以前我们讲解对数组元素的赋值时所实现的一模一样,这主要得益于,我们一开始就把赋值机制通过接口IValueSetter封装起来,在这里,setter所对应的类就是前面提到的PointerValueSetter,解释器此处不需要知道到底当前是对数组元素赋值,还是对内存赋值,只要调用接口就可以了,具体的赋值逻辑由具体的接口类负责实现。
最后我们再看看库函数malloc的实现,代码如下:
public class ClibCall {
....
private Object handleMallocCall() {
ArrayList<Object> argsList = FunctionArgumentList.getFunctionArgumentList().getFuncArgList(false);
int size = (Integer)argsList.get(0);
int addr = 0;
if (size > 0) {
MemoryHeap memHeap = MemoryHeap.getInstance();
addr = memHeap.allocMem(size);
}
return addr;
}
....
}
它的逻辑比较简单,就是通过MemoryHeap的allocMem 接口,得到一个虚拟的内存起始地址,然后把该地址返回即可。
本节内容有点复杂,请参看视频获得更详细的讲解和调试演示,这样才好加深理解。更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号: