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,先分配两个字节,接下来分别对分配的内存进行读写和赋值,然后再把赋值内容打印出来。我们先看看在解释器上实现动态内存分配的机制。
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是数组,那么Declarator的类型是ARRAY,如果是指针,那么类型为Pointer。
如果p是指针的话,那么if(pointer!=null)里面的代码就会执行,首先通过setPonterValue把指定内存的内容读取出来,对应指针p[0]就是把p指向的内存读取偏移量为0的内存数据。
setPointerValue的逻辑是先得到内存地址,这个地址的数值就是allocMem返回的,通过这个地址,MemoryHeap的哈希表中找到对应的字节数值,这个字节数组就是用来模拟动态内存的,他的输入参数index对应于地址偏移,symbol.getBytes()用来获得指针变量的数据类型, 如果变量类型是char,那么我们一次读取一字节shuju7,若不然我们一次读取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);
copy Child(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 接口,得到一个虚拟的内存起始地址,然后把该地址返回即可。