序言
出于对成为编译器工程师的向往,我开始深入挖掘各项编译技术的细节。作为一名前端工程师,我决定首先从 WebAssembly 技术开始学习。本系列文章记录了我阅读 WebAssembly 规范的重点笔记。
你可以在此链接查阅完整的 WebAssembly 规范:WebAssembly Spec 。
约定
WebAssembly 是一种编程语言,具有二进制格式和文本格式两种表示形式。两者都映射到同一个结构。为了简洁起见,这个结构以抽象语法的形式描述。
语法符号
在定义抽象语法的规则时采用以下约定。
- 终结符使用无衬线字体或符号形式表示:i32、end、→、[,]。
- 非终结符号以斜体表示:valtype、instr。
- An 是 A 迭代了 n≥0 次的序列。
- A∗ 是 A 的可为空的迭代序列。(这是 An 的简写。)
- A+ 是 A 的非空迭代序列。(这是 An 在 n≥1 时的简写。)
- A? 是 A 是可选的。(这是 An 在 n≤1 时的简写。)
- 产生式表示为 sym::=A1∣...∣An。
- 长产生式可以分为多个,以省略号 sym::=A1∣... 结束第一个定义,并以省略号 sym::=...∣A2 开始来延续。
- 一些产生式在括号中增加附加条件,“(if 表达式)”,提供一种简写方式,用于将产生式组合扩展为许多单独的情况。
- 如果在一个产生式中多次出现相同的元变量或非终端符号,则所有这些出现必须具有相同的实例化。(这是一个简写,要求多个不同的变量相等需要一个附加条件。)
辅助符号
在处理语法结构时,还使用以下符号:
- ϵ 表示空序列。
- ∣s∣ 表示序列 s 的长度。
- s[i] 表示序列 s 的第 i 个元素,索引从0开始。
- s[i:n] 表示序列 s 的子序列 s[i]...s[i+n−1]。
- swith[i]=A 表示与 s 相同的序列,只是第 i 个元素被替换为 A。
- swith[i:n]=A 表示与 s 相同的序列,只是子序列 s[i:n] 个被替换为 An。
- concat(s∗) 表示通过连接所有序列 si 形成序列 s∗。
向量
向量是 An(或 A∗)形式的序列,其中 A 可以是值或复杂结构。一个向量最多可以有232−1个元素。
vec(A)::=An(if n<232)
值
WebAssembly 程序处理原始的数字值。此外,在程序定义中,用不可变的值序列表示复杂数据,例如文本字符串或其他向量。
字节(Bytes)
最简单的值形式是字节。在抽象语法中,它们表示为十六进制字面量。
byte::=0x00∣...∣0xFF
整数(Integers)
不同类别的整数根据位宽和是无符号还是有符号来区分其值的范围。
uN::=0∣1∣...∣2N−1
sN::=−2N−1∣...∣−1∣0∣1∣...∣2N−1−1
iN 表示整数,它们的符号依赖上下文。在抽象语法中,它们表示为无符号值。但是,一些操作会根据二进制补码将它们转换为有符号值。
浮点数(Floating-Point)
浮点数表示32位或64位值,对应于 IEEE 754标准(第3.3节)的二进制格式。
向量(Vectors)
向量是由向量指令(也称为 SIMD 指令,单指令多数据)处理的128位值。它们在抽象语法中使用 i128 表示。向量的类型(整数或浮点数)和向量的大小取决于特定指令对它们的操作。
字符串(Names)
字符串是字符序列,这些字符是由 Unicode(第2.4节)定义的常量值。
name:=char∗(if ∣utf8(char∗)∣<232∣
char:=U+00∣...∣U+D7FF∣U+E00∣...∣U+10FFFF
类型
WebAssembly 中的值会被指定类型。在验证、实例化和执行期间进行类型检查。
数字类型
数字类型表示数字值。
numtype::=I32∣I64∣F32∣F64
向量类型
向量类型表示向量指令(也称为 SIMD 指令、单指令多数据)处理的数值。
vectype::=v128
引用类型
引用类型表示运行时存储中对象的一级引用。
reftype::=funcref∣externref
值类型
结果类型
函数类型
Limits
Limits 表示内存类型和表类型这种可变存储的大小范围。
limits::={min u32,max u32?}
如果没有给出最大值,则相应的存储可以增长到任何大小。
内存类型
内存类型表示线性内存及其大小范围。
memtype::=limits
这些限制限制内存的最小大小和最大大小。这些限制以内存页为单位给出。
表类型
全局类型
外部类型
外部类型表示导入和外部值,以及它们的类型。
externtype::=func functype
指令
WebAssembly 代码由指令序列组成。它的计算模型基于栈式虚拟机,即指令隐式操作栈上的操作数,消费(popping)参数值并产生或返回(pushing)结果值。
除了来自栈中的动态操作数之外,一些指令还具有静态参数,通常是索引或类型,它们是指令本身的一部分。
一些指令是按照嵌套指令序列的方式进行结构化的。
下面将指令分为多个不同的类别。
数值指令
数值指令提供对特定类型数值的基本操作。这些操作与硬件中可用的相应操作密切匹配。
数值指令按数值类型划分。对于每种类型,可以区分几个子类别:
- 常量(Constants):返回静态常量
- 一元运算(Unary Operations):消费一个操作数并产生一个相应类型的结果。
- 二元运算(Binary Operations):消费两个操作数并产生一个相应类型的结果。
- 测验(Tests):消费一个相应类型的操作数并产生一个布尔整数结果。
- 比较(Comparisons):消费两个相应类型的操作数并产生一个布尔整数结果。
- 转换(Conversions):消耗一种类型的值并产生另一种类型的结果。
向量指令
向量指令(也称为 SIMD 指令、单指令多数据)提供对向量类型值的基本操作。
向量指令有一个命名约定,涉及一个前缀,确定它们的操作数将被如何解析。该前缀描述了操作数的形状,写成 txN,由打包的数值类型和该类型的通道数组成。操作在每个通道的值上逐点进行。
备注:
例如,32x4表示4个 i32 值,打包成一个 i128。数字类型 t 乘 N 的位宽必须为128。
引用指令
这部分的指令与引用类型相关。
instr::=...∣ ref.null reftype∣ ref.is_null∣ ref.func funcidx
这些指令分别为生成空值、检查空值或生成给定函数的引用。
参数化指令
这部分的指令可以对任何值类型的操作数进行操作。
instr::=...∣ drop∣ select(valtype∗)?
drop 指令用于丢弃单个操作数。
select 指令根据其第三个操作数是否为零来选择其前两个操作数中的一个。
变量指令
变量指令用于访问本地和全局变量。
表指令
这部分的指令与表类型相关。
内存指令
这部分的指令与线性内存相关。
控制指令
这部分的指令会影响控制流。
表达式
模块
WebAssembly 程序被组织成模块,这是部署、加载和编译的基本单元。一个模块收集类型、函数、表、内存和全局变量的定义。此外,它可以声明导入和导出,并提供数据和元素段的初始化,或者一个启动函数。
module::={types vec(functype),funcs vec(func),tables vec(table),mems vec(mem),globals vec(global),elems vec(elem),datas vec(data),start start,imports vec(import),exports vec(export)}
每个向量——以及整个模块——都可能为空。
索引
定义由从零开始的索引引用。每个类型都有自己的索引空间,由以下类型区分。
typeidx::=u32
funcidx::=u32
tableidx::=u32
memidx::=u32
globalidx::=u32
elemidx::=u32
dataidx::=u32
localidx::=u32
labelidx::=u32
类型
模块中使用的所有函数类型都必须在 types 中定义。它们由类型索引引用。
函数
func::=type typeidx,locals vec(valtype),body expr
表
内存
全局
元素段
数据段
启动函数
导出
导入
模块中的导入组件定义了一组在实例化时所需的导入。

每个导入都通过两级命名空间标识,模块名和该模块内实体名。可导入的定义包括函数、表、内存和全局变量。每个导入都通过描述符表示,该描述符具有相应的类型,要求在实例化期间提供的定义必须匹配。