3.2 控制流指令


3.2 控制流指令

本节摘要:Wasm 没有任意跳转,控制流全部住在 block、loop、if 三种结构块里,由 br 系列指令在块间穿梭。这不是审美偏好,而是可验证性的硬前提。本节从历史动机讲到三件套的组合用法,用阶乘与斐波那契示范循环写法,并把数值运算指令的家族谱系一并归拢——旧版"数值与操作指令"的内容并入本节。

最初设计的字节码格式里,goto 曾是不可撼动的基本盘——直到验证器登场。要让静态检查在每个程序点都算得出栈形状,任意跳转就是天然的敌人:跳进块中间、跳到栈深不一的位置,验证立刻无解。这一节讲的就是 Wasm 用什么替代 goto,以及替代之后溢出来的表达力去哪了。

一、结构化控制流:验证器赢下的历史妥协

Wasm 的控制流原语只有三个结构块与一组受控分支。block 是顺序块,出口在末尾;loop 是循环块,入口在开头;if/else 是分支块,二选一。br 指令能做的只有"跳出 N 层块"或"跳到某层 loop 的开头",跳转目标必须是语法上可见的块标签,绝不能落到块的缝隙里。

这个约束给验证器送了两份大礼。其一,每个块的入口与出口的栈形状可以静态声明:块的参数与返回值在语法里写明,验证变成局部的形状核对。其二,引擎编译时可以把结构块一对一映射到原生标签与跳转——没有控制流图重构,没有不可达代码的意外,JIT 与 AOT 都省了大力气。高级语言编译器(LLVM 后端)负责把源码里任意复杂的控制流图重写成结构化形式,代价由编译期承担,运行期净赚。

分支深度用数字标注:br 0 跳出最内层块,br 1 跳出向外一层,以此类推。也可以给块起名,用名字代替数字,编译器生成的代码常这么干,手写时更不容易数错层数。

二、循环与分支的实战写法

用阶乘把 loop 与 br_if 的配合走一遍:

(func $fact (param $n i64) (result i64) (local $acc i64) ;; acc 起始为 1 (local.set $acc (i64.const 1)) (block $exit (loop $again ;; n 等于零则跳出整个 block (br_if $exit (i64.eqz (local.get $n))) (local.set $acc (i64.mul (local.get $acc) (local.get $n))) (local.set $n (i64.sub (local.get $n) (i64.const 1))) ;; 无条件跳回 loop 开头 (br $again))) (local.get $acc))

读法要点:br_if 带一个条件操作数(从栈顶弹出),条件为真才跳;br 是无条件跳;loop 的 br 跳到块开头(形成循环),block 的 br 跳到块之后(形成 break)。这个"block 套 loop、出口放 br_if"的骨架是编译器产出的标准形态,见到即应条件反射地翻译回 while 循环。斐波那契可以写成双变量版本,留作练手——结构完全相同,只是每轮同时滚动两个变量。

多路分支用 br_table:一条指令在一张跳转目标表里按索引选路,超出表长则走默认目标。解释器的 opcode 分派、状态机的状态转移,编译后就是它,比一长串 if 高效得多。顺带一提 if 的栈语义:条件操作数弹出后,then 与 else 两条支路的栈形必须与块声明一致——分支块的参数在进入时复制到两条支路,出口处汇合。

br_table 的手写形态一睹为快——按操作码分派到处理例程:

(func $dispatch (param $op i32) (result i32) (block $done (block $add (block $sub (br_table $add $sub $done (local.get $op)))) ;; 索引零落进 $add 出口、索引一落进 $sub 出口 (i32.add (i32.const 1) (i32.const 2))))

索引零命中表首项、索引一命中次项、越界命中默认项($done)——编译器产出的分派代码正是这个形状的工业级版本,理解了这段,反汇编里成片的 br_table 就不再面目可憎。

图 3-B:阶乘函数的结构化控制流

三、数值指令家族:一张谱系图

控制流之外,把并入本节的数值指令版图收拢成谱系。整数与浮点两大族,各按"算术、比较、位运算、转换"四条线归拢:

家族 代表指令 使用纪律
整数算术 i32.add、i32.sub、i32.mul 回绕语义,溢出不陷阱、不报错
整数除法 i32.div_s、i32.div_u、i32.rem_s 除零陷阱;有符号最小值除以 -1 也陷阱
整数比较 i32.lt_s、i32.lt_u、i32.eqz 有无符号看后缀,选错即语义 bug
位运算 i32.and、i32.or、i32.shl、i32.shr_s、i32.shr_u 移位量对位宽取模;逻辑右移与算术右移分家
浮点算术 f64.add、f64.sqrt、f64.min 遵循 IEEE 语义,NaN 与无穷大按标准传播
浮点转换 i32.trunc_f64_s、f64.convert_i32_u 截断越界陷阱;这是少数会陷阱的数值指令
位重解释 i32.reinterpret_f32 位模式原样搬家,密码学与图形常用

三条最值得刻进肌肉记忆的纪律:整数溢出是回绕而不是异常,需要防溢出的代码自己写检查;除法与截断是仅有的会陷阱的数值操作,边界值(零、最小值)必须先行防御;比较指令后缀选错(有符号当无符号)不报错、只在结果上撒谎,是真实世界里最难排查的一类数值事故。

本节要点回顾

  • 结构化是硬约束:br 只能在语法可见的块标签间跳转,任意 goto 被"验证器算得出栈形"这一要求淘汰;
  • block 套 loop 是标准循环骨架:出口条件放 br_if,回头路走 br,读到即翻译回 while;
  • br_table 是多路分派:解释器与状态机编译后的标准形态;
  • 数值指令四条线:算术、比较、位运算、转换,有无符号语义由指令后缀决定;
  • 陷阱边界清单:除零、最小值除负一、截断越界——数值指令里仅有的几处当场熄火点。

指令在栈上的推演已经完整,下一节把推演伸向内存:加载与存储如何安全地触碰那片字节平原。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U