浅谈WASM栈式虚拟机模型

2019-09-09

栈是一种先入后出的数据结构,只能在一端进行插入和删除操作,这一端一般被习惯陈为栈顶,而对应的另一端为栈底。对于栈来说,有两种基本操作:

  • 入栈push:也称压入。栈顶添加一个元素,栈中元素个数+1
  • 出栈pop:也称弹出。栈顶元素删除,栈中元素个数-1

栈式虚拟机

WebAssembly在官方的定义上就是为栈式虚拟机设计一门编程语言,对应栈式虚拟机体系的结构规范。

什么是栈式虚拟机?首先需要对比普通的x86机器汇编语言。我们知道x86汇编语言是有通用和专用寄存器,CPU指令一般会使用这些寄存器存放操作数进行操作,或者是在指定内存地址存放操作数。而在WebAssembly体系中,没有这些寄存器,也不允许直接通过内存地址访问,所有操作数都放在运行时的栈上,因此是一种栈式虚拟机

栈式指令

x86的指令一般需要我们指定操作数位置,比如存在某个寄存器里,或者在栈里,或者在某个内存地址里。

而因为栈式虚拟机的特点,绝大多数WebAssembly指令都是在栈上进行操作,不用指定操作数位置,也不能指定立即数。

  • i32.add:从栈中出栈2个32位整型相加,计算后将结果入栈。
  • i32.const n:压入一个值为n的32位整型常量。

栈式函数调用

WebAssembly函数也是通过栈进行参数传递。

  1. 调用方首先将参数压入栈中;
  2. 进入函数后,初始化参数(根据函数参数声明进行出栈);
  3. 函数内部通过get_local指令(压栈)操作去将参数压入自己的栈帧进行使用;
  4. 执行其他指令,并产生返回值压入栈中;
  5. 调用方从栈中获取返回值;

函数调用经常是嵌套的。在逻辑上,每个未返回的函数都占用一段栈,不会越过自己的栈去访问其他栈位置,这一段栈被称为栈帧,可以视为函数拥有一个独立的栈(实际物理上栈还是连续的)。

WebAssembly是一门强类型语言,在函数调用出入栈时会发生类型检查。例如:

  • 函数声明了一个i32返回值,函数结束时栈帧中有且只能有一个i32类型的值。
  • 函数声明了i32f32,函数调用时栈顶上必须也是一个i32和一个f32
(func $add (param $a i32)(param $b i32)(result i32)
	get_local $a ;;栈帧: [$a]
)

辩证

一个架构型式的选择必然有其好处也有坏处。栈式虚拟机也是如此。

相对于普通x86汇编,WebAssembly舍弃了寄存器、地址访问等概念,放弃了其可以带来的速度提升和优化,以及贴近机器的特性。但禁止地址访问大大提高了程序安全性,杜绝了例如越界访问的情况。舍弃寄存器概念也提高了程序可移植性。

同时相比普通x86汇编,WebAssembly加入了更加强大的类型检查机制。在函数的任意位置,栈的布局都是可以准确预估的,就可以进行更加有效的非运行时的静态检查,也提高了程序健壮性。