浅谈WASM栈式虚拟机模型
栈
栈是一种先入后出的数据结构,只能在一端进行插入和删除操作,这一端一般被习惯陈为栈顶,而对应的另一端为栈底。对于栈来说,有两种基本操作:
- 入栈push:也称压入。栈顶添加一个元素,栈中元素个数+1
- 出栈pop:也称弹出。栈顶元素删除,栈中元素个数-1
栈式虚拟机
WebAssembly在官方的定义上就是为栈式虚拟机设计一门编程语言,对应栈式虚拟机体系的结构规范。
什么是栈式虚拟机?首先需要对比普通的x86机器汇编语言。我们知道x86汇编语言是有通用和专用寄存器,CPU指令一般会使用这些寄存器存放操作数进行操作,或者是在指定内存地址存放操作数。而在WebAssembly体系中,没有这些寄存器,也不允许直接通过内存地址访问,所有操作数都放在运行时的栈上,因此是一种栈式虚拟机。
栈式指令
x86的指令一般需要我们指定操作数位置,比如存在某个寄存器里,或者在栈里,或者在某个内存地址里。
而因为栈式虚拟机的特点,绝大多数WebAssembly指令都是在栈上进行操作,不用指定操作数位置,也不能指定立即数。
i32.add
:从栈中出栈2个32位整型相加,计算后将结果入栈。i32.const n
:压入一个值为n的32位整型常量。
栈式函数调用
WebAssembly函数也是通过栈进行参数传递。
- 调用方首先将参数压入栈中;
- 进入函数后,初始化参数(根据函数参数声明进行出栈);
- 函数内部通过
get_local
指令(压栈)操作去将参数压入自己的栈帧进行使用; - 执行其他指令,并产生返回值压入栈中;
- 调用方从栈中获取返回值;
函数调用经常是嵌套的。在逻辑上,每个未返回的函数都占用一段栈,不会越过自己的栈去访问其他栈位置,这一段栈被称为栈帧,可以视为函数拥有一个独立的栈(实际物理上栈还是连续的)。
WebAssembly是一门强类型语言,在函数调用出入栈时会发生类型检查。例如:
- 函数声明了一个
i32
返回值,函数结束时栈帧中有且只能有一个i32
类型的值。 - 函数声明了
i32
和f32
,函数调用时栈顶上必须也是一个i32
和一个f32
。
(func $add (param $a i32)(param $b i32)(result i32)
get_local $a ;;栈帧: [$a]
)
辩证
一个架构型式的选择必然有其好处也有坏处。栈式虚拟机也是如此。
相对于普通x86汇编,WebAssembly舍弃了寄存器、地址访问等概念,放弃了其可以带来的速度提升和优化,以及贴近机器的特性。但禁止地址访问大大提高了程序安全性,杜绝了例如越界访问的情况。舍弃寄存器概念也提高了程序可移植性。
同时相比普通x86汇编,WebAssembly加入了更加强大的类型检查机制。在函数的任意位置,栈的布局都是可以准确预估的,就可以进行更加有效的非运行时的静态检查,也提高了程序健壮性。