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/
本章小结
本章核心要点
- WAT 是栈式虚拟机汇编:所有操作通过操作数栈完成;local.get 压栈,local.set 出栈存变量;函数结束时栈上剩余的值是返回值
- 四种基础数值类型:i32(最常用)、i64(大整数)、f32(单精度浮点)、f64(双精度浮点);外加 SIMD v128 和引用类型
- 控制流:Wasm 使用结构化控制流(block/loop/if-else),没有 goto;br 跳到 block 出口,loop 中的 br 跳回循环头
- 模块结构:Import → Type → Function → Table → Memory → Global → Export → Start → Element → Code → Data
- call_indirect:通过 Table 实现函数指针,调用时会进行类型检查(运行时安全保证)
- 实践建议:通常不需要手写 WAT;WAT 主要用于学习原理、调试和编写极小的工具函数