当前位置: 首页 > 文档资料 > larva 中文文档 >

字节码解释执行

优质
小牛编辑
124浏览
2023-12-01

字节码的解释执行和AST的解释执行有类似之处,而且更简单,因为树形结构已经展开成顺序了,以栈虚拟机为例,为方便起见,假设所有的指令都在一个指令数组里,每个元素是一个指令对象,有code和arg两个属性,解释器入口:

Object execute(Inst[] inst_list, Object[] func_arg);

由于continue和break已经被jmp指令代替了,这里我们认为execute的执行对应于源语言的一个函数调用,因此返回值就是源语言函数调用的返回,我们可以用null(或一个特殊的对象)来表示异常,func_arg表示对应函数的输入参数

既然是栈虚拟机,就需要一个栈,假设用Stack对象,pop和push两个操作,代码依然用动态类型形式,类型统一为Object:

int idx = 0;   
Stack<Object> stk;   
for (;;)   
{   
    Inst inst = inst_list[idx]; //如果这里越界,肯定是编译器bug   
    ++ idx;   
    switch(inst.code)   
    {   
        case CODE_LOAD:   
        {   
            stk.push(env.get(inst.arg)); //env是运行时环境的抽象   
            continue;   
        }   
        case CODE_STORE:   
        {   
            env.set(inst.arg, stk.pop());   
            continue;   
        }   
        ...   
        case CODE_ADD:   
        {   
            Object b = stk.pop();   
            Object a = stk.pop();   
            stk.push(a.add(b)); //其实这是不好的   
            continue;   
        }   
        ...   
        case CODE_DIV:   
        {   
            Object b = stk.pop();   
            Object a = stk.pop();   
            Object c = a.div(b);   
            if (c == null)   
            {   
                return null;   
            }   
            stk.push(c);   
        }   
        ...   
        case CODE_JMP: //假设采用绝对地址跳转   
        {   
            idx = inst.arg; //假设arg是个int类型,否则根据实际情况转换   
            continue;   
        }   
        ...   
        case CODE_CALL:   
        {   
            Object[] func_arg = new Object[inst.arg]; //call ARG_COUNT这种形式   
            for (int i = inst.arg - 1; i >= 0; -- i) //弹出参数   
            {   
                func_arg[i] = stk.pop();   
            }   
            Object func = stk.pop();   
            Object ret = execute(func.inst_list, func_arg); //递归调用解释器解释要call的函数   
            if (ret == null)   
            {   
                //异常了   
                return null;   
            }   
            stk.push(ret); //ret是函数调用的运算结果,压栈继续计算   
            continue;   
        }   
        ...   
        case CODE_RETURN:   
        {   
            //栈顶是需要返回的值   
            return stk.pop();   
        }   
        ...   
        default:   
        {   
            //字节码非法,严重错误,直接退出整个解释器   
            show_fatal_error("Invalid instruction");   
            exit(1);   
        }   
    }   
//这里不需要返回值,因为上面是死循环   
}

上面这个大结构已经能表示几乎所有机制了,像pop_jmp_if_false之类的代码就没有列出来,但很容易也能想到怎么实现的,单个字节码的操作都是非常简单的

需要注意上面的ADD和DIV两个过程,ADD那个实际是错的实现,DIV的是对的,因为动态类型语言的运算可能是会有异常的,当然就伪代码来说,ADD也表述清楚意思了,DIV判断了div过程是否有异常,比如整数对象的div过程的实现可能是:

Object div(Object x)   
{   
    if (!(x instanceof IntObj))   
    {   
        env.set_exception("Int div by non-Int");   
        return null;   
    }   
    IntObj i = (IntObj)x;   
    if (i.is_zero())   
    {   
        //除数为0异常   
        env.set_exception("Zero division");   
        return null;   
    }   
    return new IntObj(this.value / i.value);   
}

函数调用方便起见直接使用了解释器本身实现语言的递归,也可以实现为先压栈再jmp代码,这样只需要一个execute就可以执行一个很复杂的程序,当然需要实现数据栈的压栈操作,就像汇编一样,两种做法差别不算很大,后面只讨论上面的递归做法

最后是异常机制的实现,这个上面只体现了抛出异常,而没有捕获的代码,相关字节码可能是这样:

setup_try ON_EXC   
...   
@ON_EXC   
...

实现的时候,需要一个try_stk保持当前函数的try栈(因为try可以嵌套),然后在出现异常的时候:

if (c == null)   
{   
    if (try_stk.size() > 0)   
    {   
        //有try,跳到捕获错误的代码位置   
        idx = try_stk.pop();   
        continue;   
    }   
    return null; //当前函数没有try,抛给上一层   
}

当然,如果宿主语言本身支持异常机制,如java,则可以利用throw来抛异常,不过,这个异常体系最好不要和宿主的混起来,自己设计一套比较好