WebAssembly规范精读(第二篇)——结构

301 阅读7分钟

序言

出于对成为编译器工程师的向往,我开始深入挖掘各项编译技术的细节。作为一名前端工程师,我决定首先从 WebAssembly 技术开始学习。本系列文章记录了我阅读 WebAssembly 规范的重点笔记。

你可以在此链接查阅完整的 WebAssembly 规范:WebAssembly Spec 。

约定

WebAssembly 是一种编程语言,具有二进制格式和文本格式两种表示形式。两者都映射到同一个结构。为了简洁起见,这个结构以抽象语法的形式描述。

语法符号

在定义抽象语法的规则时采用以下约定。

  • 终结符使用无衬线字体或符号形式表示:i32end、→、[,]。
  • 非终结符号以斜体表示:valtypeinstr
  • AnA^n 是 A 迭代了 n0n\geq0 次的序列。
  • AA^* 是 A 的可为空的迭代序列。(这是 AnA^n 的简写。)
  • A+A^+ 是 A 的非空迭代序列。(这是 AnA^nn1n\geq1 时的简写。)
  • A?A^? 是 A 是可选的。(这是 AnA^nn1n\leq1 时的简写。)
  • 产生式表示为 sym::=A1...Ansym::=A_1|...|A_n
  • 长产生式可以分为多个,以省略号 sym::=A1...sym::=A_1|... 结束第一个定义,并以省略号 sym::=...A2sym::=...|A_2 开始来延续。
  • 一些产生式在括号中增加附加条件,“(if 表达式)”,提供一种简写方式,用于将产生式组合扩展为许多单独的情况。
  • 如果在一个产生式中多次出现相同的元变量或非终端符号,则所有这些出现必须具有相同的实例化。(这是一个简写,要求多个不同的变量相等需要一个附加条件。)

辅助符号

在处理语法结构时,还使用以下符号:

  • ϵ\epsilon 表示空序列。
  • s|s| 表示序列 ss 的长度。
  • s[i]s[i] 表示序列 ss 的第 ii 个元素,索引从0开始。
  • s[i:n]s[i:n] 表示序列 ss 的子序列 s[i]...s[i+n1]s[i]...s[i+n-1]
  • swith[i]=As with [i]=A 表示与 ss 相同的序列,只是第 ii 个元素被替换为 AA
  • swith[i:n]=As with [i:n]=A 表示与 ss 相同的序列,只是子序列 s[i:n]s[i:n] 个被替换为 AnA^n
  • concat(s)concat(s*) 表示通过连接所有序列 sis_i 形成序列 ss_*

向量

向量是 AnA^n(或 AA^*)形式的序列,其中 AA 可以是值或复杂结构。一个向量最多可以有23212^{32}-1个元素。

vec(A)::=An(if n<232)vec(A) ::= A^n (if\ n < 2^{32})

WebAssembly 程序处理原始的数字值。此外,在程序定义中,用不可变的值序列表示复杂数据,例如文本字符串或其他向量。

字节(Bytes)

最简单的值形式是字节。在抽象语法中,它们表示为十六进制字面量。

byte::=0x00...0xFFbyte ::= \text{0x00}|...|\text{0xFF}

整数(Integers)

不同类别的整数根据位宽和是无符号还是有符号来区分其值的范围。

uN::=01...2N1uN ::= 0|1|...|2^N-1
sN::=2N1...101...2N11sN ::= -2^{N-1}|...|-1|0|1|...|2^{N-1}-1
iN=uNiN = uN

iNiN 表示整数,它们的符号依赖上下文。在抽象语法中,它们表示为无符号值。但是,一些操作会根据二进制补码将它们转换为有符号值。

浮点数(Floating-Point)

浮点数表示32位或64位值,对应于 IEEE 754标准(第3.3节)的二进制格式。

向量(Vectors)

向量是由向量指令(也称为 SIMD 指令,单指令多数据)处理的128位值。它们在抽象语法中使用 i128 表示。向量的类型(整数或浮点数)和向量的大小取决于特定指令对它们的操作。

字符串(Names)

字符串是字符序列,这些字符是由 Unicode(第2.4节)定义的常量值。

name:=char(if utf8(char)<232name := char^* (if\ |utf8(char^*)| < 2^{32}|
char:=U+00...U+D7FFU+E00...U+10FFFFchar := \text{U+00}|...|\text{U+D7FF}|\text{U+E00}|...|\text{U+10FFFF}

类型

WebAssembly 中的值会被指定类型。在验证、实例化和执行期间进行类型检查。

数字类型

数字类型表示数字值。

numtype::=I32I64F32F64numtype ::= I32|I64|F32|F64

向量类型

向量类型表示向量指令(也称为 SIMD 指令、单指令多数据)处理的数值。

vectype::=v128vectype ::= v128

引用类型

引用类型表示运行时存储中对象的一级引用。

reftype::=funcrefexternrefreftype ::= funcref|externref

值类型

结果类型

函数类型

Limits

Limits 表示内存类型和表类型这种可变存储的大小范围。

limits::={min u32,max u32?}limits ::= \{min\ u32, max\ u32^?\}

如果没有给出最大值,则相应的存储可以增长到任何大小。

内存类型

内存类型表示线性内存及其大小范围。

memtype::=limitsmemtype ::= limits

这些限制限制内存的最小大小和最大大小。这些限制以内存页为单位给出。

表类型

全局类型

外部类型

外部类型表示导入和外部值,以及它们的类型。

externtype=func functypeexterntype \Coloneqq \textsf{func}\ functype

指令

WebAssembly 代码由指令序列组成。它的计算模型基于栈式虚拟机,即指令隐式操作栈上的操作数,消费(popping)参数值并产生或返回(pushing)结果值。

除了来自栈中的动态操作数之外,一些指令还具有静态参数,通常是索引或类型,它们是指令本身的一部分。

一些指令是按照嵌套指令序列的方式进行结构化的。

下面将指令分为多个不同的类别。

数值指令

数值指令提供对特定类型数值的基本操作。这些操作与硬件中可用的相应操作密切匹配。

数值指令按数值类型划分。对于每种类型,可以区分几个子类别:

  • 常量(Constants):返回静态常量
  • 一元运算(Unary Operations):消费一个操作数并产生一个相应类型的结果。
  • 二元运算(Binary Operations):消费两个操作数并产生一个相应类型的结果。
  • 测验(Tests):消费一个相应类型的操作数并产生一个布尔整数结果。
  • 比较(Comparisons):消费两个相应类型的操作数并产生一个布尔整数结果。
  • 转换(Conversions):消耗一种类型的值并产生另一种类型的结果。

向量指令

向量指令(也称为 SIMD 指令、单指令多数据)提供对向量类型值的基本操作。

向量指令有一个命名约定,涉及一个前缀,确定它们的操作数将被如何解析。该前缀描述了操作数的形状,写成 txNt\textsf{x}N,由打包的数值类型和该类型的通道数组成。操作在每个通道的值上逐点进行。

备注:

例如,32x432\textsf{x}4表示4个 i32 值,打包成一个 i128。数字类型 t 乘 N 的位宽必须为128。

引用指令

这部分的指令与引用类型相关。

instr=... ref.null reftype ref.is_null ref.func funcidxinstr \Coloneqq ...\\ |~\textsf{ref.null}~reftype\\ |~\textsf{ref.is\_null}\\ |~\textsf{ref.func}~funcidx

这些指令分别为生成空值、检查空值或生成给定函数的引用。

参数化指令

这部分的指令可以对任何值类型的操作数进行操作。

instr=... drop select(valtype)?instr \Coloneqq ...\\ |~\textsf{drop}\\ |~\textsf{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)}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::=u32typeidx ::= u32

funcidx::=u32funcidx ::= u32

tableidx::=u32tableidx ::= u32

memidx::=u32memidx ::= u32

globalidx::=u32globalidx ::= u32

elemidx::=u32elemidx ::= u32

dataidx::=u32dataidx ::= u32

localidx::=u32localidx ::= u32

labelidx::=u32labelidx ::= u32

类型

模块中使用的所有函数类型都必须在 types 中定义。它们由类型索引引用。

函数

func::=type typeidx,locals vec(valtype),body exprfunc ::= {type\ typeidx,locals\ vec(valtype),body\ expr}

内存

全局

元素段

数据段

启动函数

导出

导入

模块中的导入组件定义了一组在实例化时所需的导入。

Import Type

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