Wasm 中的“栈机”(Stack Machine),这正是其核心执行模型之一。Wasm 是一种基于栈的虚拟机,这意味着它的所有操作都通过从一个操作数栈中弹出值、执行操作并将结果压回栈中来完成。它没有传统的“寄存器”概念。

什么是栈机?#

在计算机科学中,栈机是一种计算模型,其中指令操作数被隐含地从一个被称为“操作数栈”的内存区域中获取,并且结果被隐含地压回这个栈。这种模型与基于寄存器或基于累加器的模型形成对比。

Wasm 栈机的工作原理#

Wasm 模块中的函数是由一系列指令组成的。这些指令会操作一个中央的操作数栈。

  1. 操作数栈(Operand Stack)

    • 这是 Wasm 执行函数时最核心的数据结构。
    • 所有的操作数(如整数、浮点数)和操作结果都临时存储在这个栈上。
    • 指令不会像在注册机中那样直接指定操作数的位置(如“将 R1 的值加到 R2”)。相反,它们会假定操作数已经在栈的顶部。
  2. 局部变量(Local Variables)

    • 除了操作数栈,每个函数调用还有一个独立的“局部变量”区域。
    • 局部变量是命名的存储位置,可以在函数的整个执行过程中被访问和修改。
    • 虽然局部变量不是栈的一部分,但有很多指令允许你将局部变量的值压入栈中,或者将栈顶部的值存储到局部变量中。
  3. 参数(Parameters)

    • 函数的参数在函数被调用时,会被初始化为局部变量的一部分(通常是前几个局部变量)。
    • 它们也可以被认为是函数执行上下文的一部分。
  4. 指令的操作

    • 压栈(Push):很多指令会将值压入栈中。例如:
      • i32.const 42:将整数常量 42 压入栈。
      • local.get <idx>:获取索引为 <idx> 的局部变量的值并压入栈。
    • 弹栈(Pop):大多数操作指令会从栈顶弹出所需数量的操作数。例如:
      • i32.add:弹出栈顶的两个 i32 整数,将它们相加,然后将结果压回栈。
      • if/else/loop 等控制流指令的条件值也会从栈中弹出。
    • 复合操作:一些指令可能弹出一个值,执行一些副作用(如内存写入),而不压入任何新值。例如:
      • i32.store:弹出内存地址和要存储的值,将值写入内存。

栈机模型的优势与特点#

  1. 紧凑性(Compactness)

    • 指令更短,因为它们不需要编码操作数的位置。例如,一个加法操作,在寄存器机中可能需要指定两个源寄存器和一个目标寄存器;在栈机中,它只是一个简单的 add 指令。
    • 这有助于生成更小的二进制代码,对于 Web 环境中的快速下载和解析非常有利。
  2. 简化编译器后端(Simplified Compiler Backends)

    • IR(中间表示)到指令的映射通常更直接。许多高级语言的语义本身就可以很容易地映射到栈操作。
    • 这使得将 C/C++/Rust 等语言编译到 Wasm 变得相对容易。
  3. 易于验证(Easy to Validate)

    • Wasm 在加载时会进行严格的类型检查和结构验证。栈机模型使得验证其类型安全变得相对容易。例如,当检查 i32.add 指令时,验证器只需确保栈顶有两个 i32 类型的值。
    • 这对于沙盒环境中的安全性至关重要。
  4. 独立于目标架构(Architecture-Independent)

    • Wasm 字节码是平台无关的。它可以在任何支持 Wasm 运行时的 CPU 架构上运行。栈机模型是这种可移植性的一部分。
  5. 性能(Performance)

    • 虽然是栈机,但现代 Wasm 运行时(如 V8、SpiderMonkey)会进行即时编译(JIT),将 Wasm 字节码编译成本地机器码。
    • JIT 编译器能很好地优化栈操作,例如通过将栈顶的某些值映射到 CPU 寄存器,从而避免实际的内存栈访问。因此,栈机在运行时层面并不意味着性能低下。

示例代码(Wasm Text Format - WAT)#

以下是一个简单的 Wasm 函数(WAT 格式),演示了栈机的工作原理:

(module
  (func $add_two_numbers_and_store (param $x i32) (param $y i32) (result i32)
    (local $temp_sum i32)  ;; 声明一个局部变量

    ;; 将参数压入栈
    get_local $x         ;; 栈: [x]
    get_local $y         ;; 栈: [x, y]

    ;; 执行加法操作
    i32.add              ;; 弹出 x 和 y,计算 x + y,将结果压回栈
                         ;; 栈: [x+y]

    ;; 将栈顶结果存储到局部变量
    set_local $temp_sum  ;; 栈: [] (弹出了 x+y 并存储到 $temp_sum)

    ;; 将局部变量的值压入栈作为函数结果
    get_local $temp_sum  ;; 栈: [temp_sum]
  )
)

栈状态变化跟踪:

  1. get_local $x: 参数 $x (类型 i32) 的值被压入操作数栈。
    • 栈: [val_of_x]
  2. get_local $y: 参数 $y (类型 i32) 的值被压入操作数栈。
    • 栈: [val_of_x, val_of_y]
  3. i32.add: 从栈顶弹出 val_of_yval_of_x。它们被相加。结果 (val_of_x + val_of_y) 被压回栈。
    • 栈: [sum]
  4. set_local $temp_sum: 从栈顶弹出 sum,并将其值存储到局部变量 $temp_sum 中。
    • 栈: []
  5. get_local $temp_sum: 局部变量 $temp_sum 的值被压入操作数栈。
    • 栈: [final_result]

由于这是函数的最后一条指令,并且函数声明了 (result i32),栈顶的 [final_result] 就是函数的返回值。

Reference#