04 - WAT 文本格式
04 - WAT 文本格式
WAT(WebAssembly Text Format)是 Wasm 的人类可读表示。掌握它,你就能直接"看到"Wasm 在做什么。
4.1 WAT 语法基础
WAT 使用 S-expression(S-表达式)语法,类似于 Lisp:
(module ;; 模块定义(最外层)
(func $greet ;; 函数定义
(param $name i32) ;; 参数
(result i32) ;; 返回值
local.get $name ;; 指令
)
)
基本规则
| 规则 | 说明 |
|---|---|
使用 () 括号 | 所有结构都由括号包裹 |
注释用 ;; | 单行注释 |
块注释用 (; ... ;) | 多行注释 |
$name 命名 | 以 $ 开头的标识符 |
| 缩进无意义 | 仅为可读性,不影响语义 |
命名与索引
;; 两种引用函数的方式:
;; 1. 使用命名(可读性好)
call $my_function
;; 2. 使用数字索引(底层表示)
call 0
4.2 值类型
i32 ;; 32 位整数
i64 ;; 64 位整数
f32 ;; 32 位浮点数(IEEE 754 单精度)
f64 ;; 64 位浮点数(IEEE 754 双精度)
v128 ;; 128 位 SIMD 向量(需要 SIMD 提案)
funcref ;; 函数引用
externref ;; 外部引用
多返回值
;; 函数可以返回多个值
(func $divmod (param $a i32) (param $b i32) (result i32 i32)
local.get $a
local.get $b
i32.div_u ;; 商
local.get $a
local.get $b
i32.rem_u ;; 余数
)
;; 结果: 返回 [商, 余数] 两个 i32
4.3 函数
基本函数
(module
;; 简单的加法函数
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a ;; 将参数 $a 压栈
local.get $b ;; 将参数 $b 压栈
i32.add ;; 弹出两个值,相加,压入结果
)
(export "add" (func $add))
)
函数中的局部变量
(func $example (param $x i32) (param $y i32) (result i32)
(local $temp i32) ;; 声明局部变量
(local $flag i32) ;; 可以声明多个
;; local.set — 赋值
local.get $x
local.get $y
i32.add
local.set $temp ;; temp = x + y
;; local.tee — 赋值并保持栈上值
i32.const 42
local.tee $flag ;; flag = 42,同时 42 保留在栈上
drop ;; 丢弃栈顶
local.get $temp ;; 返回 temp
)
类型复用
(module
;; 定义函数类型
(type $binop (func (param i32 i32) (result i32)))
;; 复用类型定义
(func $add (type $binop)
local.get 0
local.get 1
i32.add
)
(func $mul (type $binop)
local.get 0
local.get 1
i32.mul
)
)
4.4 控制流
Wasm 的控制流基于结构化控制,有三个核心指令:block、loop、if。
block — 代码块
(block $label
;; ... 指令 ...
(br $label) ;; 跳出 block
)
;; br $label 跳到这里
loop — 循环
;; 循环 10 次
(func $count_to_10
(local $i i32)
(local.set $i (i32.const 0))
(block $break
(loop $continue
;; i++
local.get $i
i32.const 1
i32.add
local.set $i
;; 如果 i < 10,继续循环
(br_if $break (i32.ge_u (local.get $i) (i32.const 10)))
(br $continue)
)
)
)
💡 关键理解:
block的br跳到块之后,loop的br跳到循环开始。这是 Wasm 控制流最重要的一点。
if — 条件分支
(func $abs (param $x i32) (result i32)
local.get $x
i32.const 0
i32.lt_s ;; x < 0 ?
(if (result i32)
(then
i32.const 0
local.get $x
i32.sub ;; 0 - x = -x
)
(else
local.get $x ;; 直接返回 x
)
)
)
if-else 嵌套
(func $sign (param $x i32) (result i32)
(if (i32.lt_s (local.get $x) (i32.const 0))
(then
(i32.const -1)
)
(else
(if (i32.gt_s (local.get $x) (i32.const 0))
(then (i32.const 1))
(else (i32.const 0))
)
)
)
)
br_table — 多路跳转
(func $switch (param $case i32) (result i32)
(block $default
(block $case2
(block $case1
(block $case0
local.get $case
(br_table $case0 $case1 $case2 $default)
;; case 0 → 跳到 $case0
;; case 1 → 跳到 $case1
;; case 2 → 跳到 $case2
;; 其他 → 跳到 $default
)
;; $case0:
(return (i32.const 100))
)
;; $case1:
(return (i32.const 200))
)
;; $case2:
(return (i32.const 300))
)
;; $default:
(i32.const -1)
)
4.5 数值运算指令
整数运算
| 指令 | 说明 | 栈操作 |
|---|---|---|
i32.add | 加法 | [a, b] → [a+b] |
i32.sub | 减法 | [a, b] → [a-b] |
i32.mul | 乘法 | [a, b] → [a*b] |
i32.div_s | 有符号除法 | [a, b] → [a/b] |
i32.div_u | 无符号除法 | [a, b] → [a/b] |
i32.rem_s | 有符号取余 | [a, b] → [a%b] |
i32.rem_u | 无符号取余 | [a, b] → [a%b] |
i32.and | 按位与 | [a, b] → [a&b] |
i32.or | 按位或 | [a, b] → [a|b] |
i32.xor | 按位异或 | [a, b] → [a^b] |
i32.shl | 左移 | [a, b] → [a<<b] |
i32.shr_s | 有符号右移 | [a, b] → [a>>b] |
i32.shr_u | 无符号右移 | [a, b] → [a>>>b] |
i32.rotl | 循环左移 | [a, b] → [rotl(a,b)] |
i32.rotr | 循环右移 | [a, b] → [rotr(a,b)] |
i32.clz | 前导零计数 | [a] → [clz(a)] |
i32.ctz | 尾部零计数 | [a] → [ctz(a)] |
i32.popcnt | 置位计数 | [a] → [popcnt(a)] |
浮点运算
| 指令 | 说明 |
|---|---|
f64.add / f64.sub / f64.mul / f64.div | 基本四则运算 |
f64.sqrt | 平方根 |
f64.min / f64.max | 最小值/最大值 |
f64.ceil / f64.floor / f64.trunc / f64.nearest | 取整 |
f64.abs / f64.neg | 绝对值/取反 |
f64.copysign | 符号复制 |
比较指令
;; 整数比较(返回 i32,0 = false,1 = true)
i32.eq ;; 相等
i32.ne ;; 不等
i32.lt_s ;; 有符号小于
i32.lt_u ;; 无符号小于
i32.gt_s ;; 有符号大于
i32.gt_u ;; 无符号大于
i32.le_s ;; 有符号小于等于
i32.ge_s ;; 有符号大于等于
;; 浮点比较
f64.eq ;; 相等
f64.ne ;; 不等
f64.lt ;; 小于
f64.gt ;; 大于
f64.le ;; 小于等于
f64.ge ;; 大于等于
类型转换
;; i32 → f64(有符号整数转浮点)
i32.const 42
f64.convert_i32_s ;; → 42.0
;; f64 → i32(截断浮点为整数)
f64.const 3.14
i32.trunc_f64_s ;; → 3
;; i32 → i64(扩展)
i32.const -1
i64.extend_i32_s ;; → -1(符号扩展)
i64.extend_i32_u ;; → 4294967295(零扩展)
;; i64 → i32(截断)
i64.const 123456789
i32.wrap_i64 ;; → 低 32 位
;; f32 → f64(提升)
f32.const 1.5
f64.promote_f32 ;; → 1.5
;; f64 → f32(截断)
f64.const 1.5
f32.demote_f64 ;; → 1.5
4.6 内存操作指令
加载(Load)
;; 基本加载
i32.load ;; 从内存加载 i32(4 字节)
i64.load ;; 从内存加载 i64(8 字节)
f32.load ;; 从内存加载 f32(4 字节)
f64.load ;; 从内存加载 f64(8 字节)
;; 窄加载
i32.load8_s ;; 加载 1 字节,符号扩展为 i32
i32.load8_u ;; 加载 1 字节,零扩展为 i32
i32.load16_s ;; 加载 2 字节,符号扩展为 i32
i32.load16_u ;; 加载 2 字节,零扩展为 i32
i64.load8_s ;; 加载 1 字节,符号扩展为 i64
i64.load16_s ;; 等等...
i64.load32_s
;; 带偏移量
i32.load offset=8 ;; 从 (addr + 8) 处加载
;; 带对齐提示
i32.load align=4 ;; 4 字节对齐
存储(Store)
;; 基本存储
i32.store ;; 存储 i32(4 字节)
i64.store ;; 存储 i64(8 字节)
f32.store ;; 存储 f32
f64.store ;; 存储 f64
;; 窄存储(只写低 8/16 位)
i32.store8 ;; 存储低 8 位
i32.store16 ;; 存储低 16 位
;; 带偏移量和对齐
i32.store offset=16 align=2
内存管理
memory.size ;; 返回当前内存页数
memory.grow ;; 增长内存(参数: 要增加的页数,返回: 增长前的页数)
memory.fill ;; 填充内存区域(类似 memset)
memory.copy ;; 复制内存区域(类似 memcpy)
memory.init $seg ;; 从数据段初始化内存
data.drop $seg ;; 释放数据段
4.7 逐行分析示例
示例 1:数组求和
(module
(memory (export "memory") 1)
;; 假设数组起始地址为 0,每个元素 i32(4 字节)
;; 数组布局: [count][elem0][elem1][elem2]...
;; 0-3 4-7 8-11 12-15
(func $array_sum (param $ptr i32) (result i32)
(local $sum i32) ;; 累加器
(local $end i32) ;; 结束地址
(local $count i32) ;; 元素数量
;; 读取数组长度
(local.set $count (i32.load (local.get $ptr)))
;; 计算结束地址: ptr + 4 + count * 4
(local.set $end
(i32.add
(i32.add (local.get $ptr) (i32.const 4))
(i32.mul (local.get $count) (i32.const 4))
)
)
;; 累加器初始化
(local.set $sum (i32.const 0))
;; 遍历数组
(local.set $ptr (i32.add (local.get $ptr) (i32.const 4))) ;; 跳过 count
(block $break
(loop $loop
(br_if $break (i32.ge_u (local.get $ptr) (local.get $end)))
;; sum += *ptr
(local.set $sum
(i32.add (local.get $sum) (i32.load (local.get $ptr)))
)
;; ptr += 4
(local.set $ptr (i32.add (local.get $ptr) (i32.const 4)))
(br $loop)
)
)
(local.get $sum)
)
(export "arraySum" (func $array_sum))
)
示例 2:字符串反转
(module
(memory (export "memory") 1)
(func $reverse_string (param $start i32) (param $len i32)
(local $left i32)
(local $right i32)
(local $tmp i32)
(local.set $left (local.get $start))
(local.set $right
(i32.sub
(i32.add (local.get $start) (local.get $len))
(i32.const 1)
)
)
(block $done
(loop $swap
(br_if $done (i32.ge_u (local.get $left) (local.get $right)))
;; 交换 left 和 right 处的字节
(local.set $tmp (i32.load8_u (local.get $left)))
(i32.store8 (local.get $left) (i32.load8_u (local.get $right)))
(i32.store8 (local.get $right) (local.get $tmp))
;; left++, right--
(local.set $left (i32.add (local.get $left) (i32.const 1)))
(local.set $right (i32.sub (local.get $right) (i32.const 1)))
(br $swap)
)
)
)
(export "reverseString" (func $reverse_string))
)
4.8 不可达指令与陷阱
;; unreachable — 标记不可达代码,执行时触发 trap
(func $check_positive (param $x i32) (result i32)
(if (i32.lt_s (local.get $x) (i32.const 0))
(then (unreachable)) ;; 负数会触发 trap
)
(local.get $x)
)
;; nop — 空操作(什么都不做)
nop
常见 trap 原因:
| Trap 原因 | 说明 |
|---|---|
unreachable 执行 | 手动触发的 trap |
| 内存越界访问 | 访问超出内存边界 |
| 除以零 | 整数除以零 |
| 栈溢出 | 调用栈过深 |
| 间接调用类型不匹配 | call_indirect 的类型检查失败 |
| 表访问越界 | 访问超出表边界 |
4.9 常见模式速查
条件赋值
;; result = (condition) ? a : b
(if (result i32) (local.get $condition)
(then (local.get $a))
(else (local.get $b))
)
最小值 / 最大值
;; min(a, b)
(local.get $a)
(local.get $b)
i32.lt_s
(if (result i32) (then (local.get $a)) (else (local.get $b)))
;; 使用内置指令(浮点)
f64.min
f64.max
循环累加
(local $acc i32)
(local $i i32)
(block $break
(loop $continue
;; ... 使用 $i, 更新 $acc ...
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $break (i32.ge_u (local.get $i) (i32.const N)))
(br $continue)
)
)
4.10 注意事项
⚠️
blockvsloop的区别:block中的br跳到块后,loop中的br跳回循环头部。初学者最常犯的错误是混淆二者。
⚠️ 类型安全:Wasm 是强类型的。你不能把 i32 和 f32 混用,WAT 文本格式和二进制格式都会在编译/验证时检查类型。
⚠️ 栈平衡:每个代码块在入口和出口处的栈状态必须与类型签名匹配。违反会导致验证失败。
4.11 扩展阅读
- WebAssembly Instruction Set Reference
- MDN: WebAssembly Reference
- WAT by Example
- WebAssembly Explorer — 在线 WAT 编辑器
下一章:05 - C/C++ 编译到 Wasm — 使用 Emscripten 将 C/C++ 代码编译为 WebAssembly。