java开发C解释器:实现动态内存的分配和读写

更详细的讲解和代码调试演示过程,请参看视频
用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 接口,得到一个虚拟的内存起始地址,然后把该地址返回即可。

本节内容有点复杂,请参看视频获得更详细的讲解和调试演示,这样才好加深理解。更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值