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

栈虚拟机字节码

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

虽然AST可以直接解释执行,实现也不复杂,但大部分语言,比如java,python,ruby(1.9版本之后)使用虚拟机解释字节码执行。字节码和AST的执行有很强的一致性,但字节码执行机制可以实现一些更细粒度的控制

这里的虚拟机是指执行一种低级语言字节码的虚拟机,这个限定可能强了些,比方说,前面说的一个AST解释器,也可以看做是一种虚拟机,因为理论上是可以有一个机器解释AST执行,但这里我们说的虚拟机是和真机相对的,确切说就是和现在广泛使用的冯诺依曼结构对应,因此狭义地,只有执行一个个指令的实现才成为虚拟机VM。具体的实现方式分为栈虚拟机和寄存器虚拟机,下面主要讨论栈虚拟机

在语法分析部分,主要是讨论了stmt_list和expr树,其中stmt_list已经很清楚了,这里先看expr,对expr的分析,很多资料上会讲到逆波兰表达式,也译作后缀表达式,这个表达式也可以通过同样的算法生成,只是输出的操作不同而已,但是考虑到语义分析(类型检查,常量静态计算等),还是expr树比较方便。后缀表达式和expr树可以很方便地互相转换,而后缀表达式中的各元素都直接对应栈虚拟机中表达式计算的字节码,因此从expr树就可以直接生成字节码

例如expr:a+b-c*d/e,根据运算符优先级,最后生成的expr树大概是这样(Python类结构表示,假设统一类Expr,类型用运算符属性op表示,二元运算两个属性为a和b):

Expr(op = "-",   
     a = Expr(op = "+",   
              a = Expr(op = "load", name = "a"),   
              b = Expr(op = "load", name = "b")),   
     b = Expr(op = "/",   
              a = Expr(op = "*",   
                       a = Expr(op = "load", name = "c"),   
                       b = Expr(op = "load", name = "d")),   
              b = Expr(op = "load", name = "e")))

这个expr树转字节码,和其解释执行的流程一样,只不过并不是真的执行,而是将执行的步骤记录下来,中间数据记录在栈上,比如加法的to_byte_code(伪代码):

this.a.to_byte_code();   
this.b.to_byte_code();   
output_byte_code(BYTE_CODE_ADD);

因此上面的树最终的结果是这么个样子:

load a   
load b   
add   
load c   
load d   
mul   
load e   
div   
sub

其中add,sub,mul,div是加减乘除,运算字节码没有参数,是对当前栈顶两个元素弹出运算,结果写回栈顶,手工模拟下就能验证,这个操作序列和AST的执行是一致的,最后的结果存在栈顶

函数调用的expr,区别在于函数的传入参数可能不同,如果将函数设计为对象,则做call指令的时候需要带参数,而且要运行时检查,比如:

f(a,b,c)   
ff(d,e)

结果是:

load f   
load a   
load b   
load c   
call 3 //3个参数   
load ff   
load d   
load e   
call 2 //2个参数

一般来说都这么实现,不过如果把函数本身看做一个操作,则可以:

load a   
load b   
load c   
call_f   
load d   
load e   
call_ff

很容易看出,这样做一个明显的问题是字节码必须跟着函数走,因为函数大都是自定义的,为减少耦合,几乎没有这样实现语言的,因为字节码基本上都是固定的。但并非说这种方式行不通,后面实现语言的时候我会用到类似的技术对代码进行特化

有了expr的字节码,stmt的字节码就很容易写了,比如expr形式的stmt,算完后用pop指令丢弃栈顶结果即可

赋值形式stmt和上面的区别在于,栈顶结果要存储到某个地方,这样就需要根据左值不同采用不同的store指令,如果左值是一个变量,简单用store就可以,如果左值是一个较复杂的表达式,则一般需要先计算,比如:

a[b + c] = d * e

左值和右值分别计算expr树,在转换字节码的时候,区别在于左值的最后一步运算转换成相反的存储指令,假设左值先执行:

load a   
load b   
load c   
add //左值算完,最后一步load_item不写,栈上留a和b+c的值   
load d   
load e   
mul   
store_item //执行时栈里有三个元素,容器、下标和值

当然,反过来先输出赋值语句右边的表达式计算代码也可以,这个看设计了,会造成语言中表达式的计算顺序问题

上面说的字节码在执行是都是顺序解释执行,除了expr和赋值两种stmt外,其他大多数语句都和跳转有关,即字节码的jmp指令,广泛用于循环、分支等。函数调用广义上说也算是一种跳转,但具体实现上,函数调用有不同的做法,后面再讨论

比如while语句的实现:

@START   
... //计算condition   
pop_jmp_if_false OUT   
... //执行内容   
jmp START   
@OUT

这里我们用@来表示label,具体实现中应该是一个字节码的索引位置,就好像汇编语言的label会变成地址一样,当然跳转可以做绝对跳转和相对跳转,这个比较随意,视具体情况而定 如果上述循环中出现break和continue语句,则直接jmp至OUT或START即可。另外由于任何循环形式都可以归约至while循环,道理都是一样的,就不赘述了

if语句,假设是Python的elif这种并列形式:

... //计算条件1   
pop_jmp_if_false IF_2   
... //执行内容   
jmp OUT   
@IF_2   
... //计算条件2   
pop_jmp_if_false IF_3   
... //执行内容   
jmp OUT   
@IF_3   
... //多个if并列的类似代码   
@IF_N   
... //计算条件N   
pop_jmp_if_false ELSE   
... //执行内容   
jmp OUT   
@ELSE   
... //执行内容   
@OUT

如果是C和java的if...else形式,则实现更简单,因为这里把AST树展开了,再深也没关系