Chapter 02

WAT:WebAssembly 文本格式

掌握 WAT 模块结构、类型系统、控制流指令与 wat2wasm 编译工具

WAT(WebAssembly Text Format)

WAT 是什么?

WAT 是 WebAssembly 的人类可读文本格式,使用 S 表达式(Lisp 风格的括号嵌套语法)来表示 WebAssembly 模块的结构和指令。它与 .wasm 二进制格式是等价的——任何 WAT 代码都可以编译为 .wasm,任何 .wasm 都可以反编译为 WAT。

最小的 WAT 模块

;; 注释以双分号开始
(module)  ;; 空模块:最小的合法 Wasm 程序

模块结构详解

类型段(Type Section)

(module
  ;; 类型定义:函数签名
  (type $add_type (func (param i32 i32) (result i32)))

  ;; 函数定义,引用上面的类型
  (func $add (type $add_type)
    local.get 0    ;; 获取第一个参数
    local.get 1    ;; 获取第二个参数
    i32.add        ;; 栈上弹出两个值相加,结果压回栈
  )

  (export "add" (func $add))
)

WebAssembly 类型系统

i32
32 位整数。WebAssembly 中最常用的类型,布尔值也用 i32 表示(0 为 false,非 0 为 true)。
i64
64 位整数。用于需要大整数范围的场景,注意 JS 中 i64 和 BigInt 互操作需要特殊处理。
f32
32 位浮点数(单精度)。遵循 IEEE 754 标准。适用于图形计算(顶点坐标、颜色值)。
f64
64 位浮点数(双精度)。与 JS 的 Number 类型精度相同。适用于科学计算。
v128
128 位 SIMD 向量类型(Wasm SIMD 提案)。可以同时处理 4 个 f32 或 2 个 f64,大幅加速数值计算。
funcref / externref
引用类型(Reference Types 提案)。funcref 是函数引用,externref 是对 JS 对象的不透明引用。

控制流指令

(module
  ;; if-else
  (func $max (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.gt_s      ;; a > b(有符号比较)
    (if (result i32)  ;; if 可以有返回值
      (then local.get $a)
      (else local.get $b)
    )
  )

  ;; loop:斐波那契数列
  (func $fib (param $n i32) (result i32)
    (local $a i32) (local $b i32) (local $i i32)
    i32.const 0   local.set $a
    i32.const 1   local.set $b
    i32.const 0   local.set $i
    (block $break
      (loop $continue
        local.get $i  local.get $n  i32.ge_s
        br_if $break          ;; if i >= n, break
        local.get $a  local.get $b  i32.add
        local.get $b  local.set $a
        local.set $b
        local.get $i  i32.const 1  i32.add  local.set $i
        br $continue
      )
    )
    local.get $a
  )

  (export "max" (func $max))
  (export "fib" (func $fib))
)

内存操作

(module
  (memory 1)  ;; 1 个内存页 = 64 KB
  (export "memory" (memory 0))

  (func $store_and_load
    i32.const 0    ;; 内存地址
    i32.const 42   ;; 值
    i32.store      ;; 将 42 存储到地址 0

    i32.const 0    ;; 内存地址
    i32.load       ;; 从地址 0 加载(结果在栈上:42)
  )
)
WAT 中 S 表达式的嵌套易错点

WAT 使用 S 表达式(以 ( 开始、) 结束),每一层括号的层级必须正确匹配。最常见的错误是:(1) 将指令嵌套在 (func ...) 外面;(2) 函数签名写成 (param i32 i32) 正确,但写成 (param i32)(param i32) 也正确(拆开声明);(3) 局部变量 (local ...) 必须在所有指令之前声明,不能夹在指令中间。

用 wat2wasm 编译

# 安装 WebAssembly Binary Toolkit
npm install -g wabt

# 编译 WAT 到 Wasm
wat2wasm hello.wat -o hello.wasm

# 反编译 Wasm 到 WAT
wasm2wat hello.wasm -o hello.wat

# 验证 Wasm 模块
wasm-validate hello.wasm

WebAssembly 的栈式虚拟机模型

理解栈机(Stack Machine)

WebAssembly 是一种栈式虚拟机(Stack Machine),所有操作都基于一个隐式的值栈进行。这与寄存器机(Register Machine,如 x86/ARM)不同。理解栈机对于理解 WAT 指令的执行顺序至关重要。

操作数栈(Operand Stack)
每个函数有自己的操作数栈。指令从栈上弹出操作数,计算结果压回栈上。函数结束时,栈上剩余的值就是函数的返回值。
local.get / local.set
local.get 将局部变量的值压入栈顶;local.set 从栈顶弹出值并存入局部变量。这是访问函数参数和局部变量的主要方式。
global.get / global.set
全局变量的读写指令,类似 local.get/set,但作用域是整个模块而非单个函数。可变全局变量(mut)对多线程不安全,需要用 Atomics 指令。
br / br_if / br_table
分支跳转指令。Wasm 没有 goto,只能跳转到封闭的 block/loop/if 块的出口(br)或开头(对 loop 块用 br)。br_if 是条件跳转,br_table 是跳转表。

栈机执行过程图解

执行 local.get $a; local.get $b; i32.add 的栈变化 初始状态: local.get $a: local.get $b: i32.add: [] [a] [a, b] [a+b] 每条指令完成后栈的状态(a=3, b=4): [] [3] [3, 4] [7] 函数结束时栈上应剩下恰好 1 个值(函数的返回值)

完整模块结构示例

包含所有常用 Section 的完整模块

(module
  ;; ========== 导入(Import Section)==========
  ;; 从 JS 导入函数,第一个参数是模块名,第二个是函数名
  (import "console" "log" (func $console_log (param i32)))

  ;; 从 JS 导入共享内存
  (import "js" "mem" (memory 1))

  ;; ========== 全局变量(Global Section)==========
  (global $counter (mut i32) (i32.const 0))  ;; 可变全局变量
  (global $PI f64 (f64.const 3.14159265358979))  ;; 常量

  ;; ========== 函数定义 ==========
  (func $increment (result i32)
    (local $old i32)           ;; 局部变量声明
    global.get $counter        ;; 读取全局变量,压栈
    local.set $old             ;; 保存旧值
    global.get $counter
    i32.const 1
    i32.add
    global.set $counter        ;; 更新全局变量
    local.get $old             ;; 返回旧值
  )

  (func $circle_area (param $r f64) (result f64)
    local.get $r
    local.get $r
    f64.mul                    ;; r * r
    global.get $PI
    f64.mul                    ;; r² * PI
  )

  ;; ========== 表(Table Section)==========
  ;; 函数引用表,用于间接调用(实现函数指针)
  (table 2 funcref)
  (elem (0) $increment $circle_area)  ;; 初始化表

  ;; ========== 数据段(Data Section)==========
  ;; 在内存偏移 0 处写入字符串 "Hello"
  (data (i32.const 0) "Hello\00")

  ;; ========== 导出 ==========
  (export "increment" (func $increment))
  (export "circleArea" (func $circle_area))
  (export "counter" (global $counter))
)

WAT 中的间接函数调用

call_indirect:函数指针的 Wasm 实现

Wasm 没有像 C 那样的函数指针,但通过 Table(函数引用表)和 call_indirect 指令可以实现同等功能。这是实现多态、回调和动态分发的关键机制。

(module
  (type $int_to_int (func (param i32) (result i32)))

  (func $double (param i32) (result i32)
    local.get 0
    i32.const 2
    i32.mul
  )
  (func $square (param i32) (result i32)
    local.get 0
    local.get 0
    i32.mul
  )

  (table 2 funcref)
  (elem (0) $double $square)  ;; 索引0=double, 索引1=square

  ;; 根据索引动态调用不同函数
  (func $apply (param $func_idx i32) (param $value i32) (result i32)
    local.get $value
    local.get $func_idx
    call_indirect (type $int_to_int)  ;; 类型必须匹配!
  )

  (export "apply" (func $apply))
)
// JavaScript 使用示例
const { instance } = await WebAssembly.instantiateStreaming(fetch('./module.wasm'));
const { apply } = instance.exports;

apply(0, 5);  // 调用 double:10
apply(1, 5);  // 调用 square:25
i32 有符号 vs 无符号比较

WAT 的 i32 本身不区分有符号/无符号——这是指令的职责。i32.gt_s 是有符号比较(把 i32 视为 -2^31 到 2^31-1),i32.gt_u 是无符号比较(把 i32 视为 0 到 2^32-1)。对于数组索引、内存地址等通常是非负数的值,应该用 _u 后缀的无符号指令,避免把大无符号整数(如 0xFFFFFFFF)错误解释为 -1。

WAT 工具集与调试技巧

WebAssembly Binary Toolkit(WABT)核心工具

wat2wasm
将 .wat 文本格式编译为 .wasm 二进制。--debug-names 保留变量/函数名称,--validate 只验证不输出。
wasm2wat
将 .wasm 反编译为可读的 .wat 文本。这是分析编译器输出、理解 Wasm 底层工作方式的利器。
wasm-objdump
类似 objdump,查看 .wasm 文件的 Section 结构、导入导出表、函数列表等元信息,不反编译字节码。
wasm-validate
验证 .wasm 文件是否符合 Wasm 规范。在调试编译器或手写 Wasm 时非常有用。
wasm-strip
删除调试信息和名称 Section,减小 .wasm 文件体积。发布前使用可减小 10-30%。
# 查看 Wasm 文件的 Section 结构
wasm-objdump -h module.wasm

# 查看所有函数及其签名
wasm-objdump -j Function module.wasm

# 完整反汇编(WAT + 字节码偏移)
wasm-objdump -d module.wasm

# 反编译为干净的 WAT
wasm2wat --no-debug-names module.wasm -o module.wat

# 在线工具:WebAssembly Studio(在线 WAT 编辑/编译)
# https://webassembly.studio/

本章小结

本章核心要点