1. 简介
1.1. 简介
WebAssembly(缩写为 Wasm [1])是一种为高效执行和紧凑表示而设计的安全、可移植、低级代码格式。它的主要目标是在 Web 上启用高性能应用程序,但它不做出任何特定于 Web 的假设或提供特定于 Web 的功能,因此它也可以在其他环境中使用。
WebAssembly 是一种由 W3C 社区小组 开发的开放标准。
本文档描述了 WebAssembly 核心标准的 2.0 版(2024-09-13 草案)。它旨在被未来的增量版本取代,这些版本将包含更多新功能。
1.1.1. 设计目标
WebAssembly 的设计目标如下
-
快速、安全、可移植的语义
-
快速:以接近原生代码的性能执行,利用所有当代硬件通用的功能。
-
安全:代码在内存安全的 [2]、沙盒环境中执行,以防止数据损坏或安全漏洞。
-
定义明确:以易于非正式和正式推理的方式完整准确地定义有效程序及其行为。
-
硬件无关:可以在所有现代架构上编译,包括台式机或移动设备和嵌入式系统。
-
语言无关:不偏袒任何特定语言、编程模型或对象模型。
-
平台无关:可以嵌入到浏览器中,作为独立的 VM 运行,或集成到其他环境中。
-
开放:程序可以通过简单通用的方式与其环境进行交互。
-
-
高效、可移植的表示
-
紧凑:具有二进制格式,该格式速度快,因为比典型的文本或原生代码格式更小。
-
模块化:程序可以分成更小的部分,这些部分可以分别传输、缓存和使用。
-
高效:可以在快速单次传递中进行解码、验证和编译,无论是即时 (JIT) 还是提前 (AOT) 编译。
-
可流式传输:允许在看到所有数据之前尽早开始解码、验证和编译。
-
可并行化:允许将解码、验证和编译分成许多独立的并行任务。
-
可移植:不做出现代硬件普遍支持的架构假设。
-
WebAssembly 代码还旨在易于检查和调试,尤其是在像 Web 浏览器这样的环境中,但这些功能超出了本规范的范围。
1.1.2. 范围
本质上,WebAssembly 是一种虚拟指令集架构(虚拟 ISA)。因此,它有许多用例,可以嵌入到许多不同的环境中。为了涵盖它们的各种用途并实现最大程度的重复使用,WebAssembly 规范被分成多个文档并分层。
本文档关注 WebAssembly 的核心 ISA 层。它定义了指令集、二进制编码、验证和执行语义,以及文本表示。但是,它没有定义 WebAssembly 程序如何与其执行的特定环境交互,也没有定义如何从这样的环境中调用它们。
相反,本规范由其他文档补充,这些文档定义了与特定嵌入环境(例如 Web)的接口。这些将分别定义适合给定环境的 WebAssembly 应用程序编程接口(API)。
1.1.3. 安全注意事项
WebAssembly 并没有提供访问代码执行环境的任何环境访问权限。任何与环境的交互,例如 I/O、访问资源或操作系统调用,只能通过调用由 嵌入器 提供并导入到 WebAssembly 模块 的 函数 来执行。嵌入器可以通过控制或限制其提供导入的函数功能来建立适合相应环境的安全策略。这些考虑因素是嵌入器的责任,也是特定环境 API 定义 的主题。
由于 WebAssembly 旨在被翻译成直接在主机硬件上运行的机器代码,因此它可能容易受到硬件级别侧信道攻击。在存在此问题的环境中,嵌入器可能需要采取适当的缓解措施来隔离 WebAssembly 计算。
1.1.4. 依赖项
WebAssembly 依赖于两个现有标准
但是,为了使本规范自包含,上述标准的相关方面在本规范中定义并形式化,例如 二进制表示 和 舍入 浮点值,以及 值范围 和 UTF-8 编码 Unicode 字符。
注意
上述标准是所有相应定义的权威来源。本规范中给出的形式化旨在与这些定义相匹配。描述的语法或语义中存在的任何差异应视为错误。
1.2. 概述
1.2.1. 概念
WebAssembly 编码了一种低级、类似汇编的编程语言。这种语言围绕以下概念构建。
- 值
-
WebAssembly 只提供四种基本数字类型。这些是整数和 [IEEE-754-2019] 数字,每种类型都具有 32 位和 64 位宽度。32 位整数也用作布尔值和内存地址。这些类型上的常见运算可用,包括它们之间所有转换的完整矩阵。没有区分有符号和无符号整数类型。相反,整数由各自的运算解释为二进制补码表示中的无符号或有符号。
除了这些基本数字类型之外,还有一个单一的 128 位宽向量类型,表示不同类型的打包数据。支持的表示形式是 4 个 32 位或 2 个 64 位 [IEEE-754-2019] 数字,或不同宽度的打包整数值,特别是 2 个 64 位整数、4 个 32 位整数、8 个 16 位整数或 16 个 8 位整数。
最后,值可以由表示指向不同类型实体的指针的不透明引用组成。与其他类型不同,它们的大小或表示形式不可观察。
- 指令
-
WebAssembly 的计算模型基于堆栈机。代码由按顺序执行的指令序列组成。指令操作隐式操作数堆栈 [1] 上的值,并分为两大类。简单指令对数据执行基本操作。它们从操作数堆栈中弹出参数,并将结果压回堆栈。控制指令改变控制流。控制流是结构化的,这意味着它用嵌套良好的结构(例如块、循环和条件语句)来表达。分支只能针对此类结构。
- 陷阱
-
在某些情况下,某些指令可能会产生陷阱,这会立即中止执行。陷阱无法由 WebAssembly 代码处理,而是报告给外部环境,在那里它们通常可以被捕获。
- 函数
-
代码被组织成单独的函数。每个函数都接受一系列值作为参数,并返回一系列值作为结果。函数可以相互调用,包括递归调用,从而产生一个无法直接访问的隐式调用堆栈。函数还可以声明可变的局部变量,这些变量可用作虚拟寄存器。
- 表
-
表是具有特定元素类型的不透明值的数组。它允许程序通过动态索引操作数间接选择这些值。目前,唯一可用的元素类型是无类型函数引用或对外部主机值的引用。因此,程序可以通过对表的动态索引间接调用函数。例如,这允许通过表索引模拟函数指针。
- 线性内存
-
线性内存是原始字节的连续可变数组。这种内存是在初始大小下创建的,但可以动态增长。程序可以在任何字节地址(包括未对齐的地址)从线性内存加载值或存储值。整数加载和存储可以指定一个比相应值类型大小更小的存储大小。如果访问超出当前内存大小的范围,则会发生陷阱。
- 模块
-
WebAssembly 二进制文件采用模块的形式,其中包含函数、表和线性内存的定义,以及可变或不可变的全局变量。定义也可以被导入,指定一个模块/名称对以及合适的类型。每个定义可以选择在多个名称下被导出。除了定义之外,模块还可以定义其内存或表的初始化数据,这些数据采用复制到给定偏移量的段的形式。它们还可以定义一个启动函数,该函数会自动执行。
- 嵌入器
-
WebAssembly 实现通常会嵌入到主机环境中。此环境定义了如何启动模块加载、如何提供导入(包括主机端定义)以及如何访问导出。但是,任何特定嵌入的细节超出了本规范的范围,而是由补充的、环境特定的 API 定义提供。
在实践中,实现不必维护实际的操作数堆栈。相反,可以将堆栈视为一组匿名寄存器,这些寄存器由指令隐式引用。类型系统 确保堆栈高度,以及任何引用的寄存器,始终是静态已知的。
1.2.2. 语义阶段
从概念上讲,WebAssembly 的语义分为三个阶段。对于语言的每个部分,规范都指定了每个阶段。
- 解码
-
WebAssembly 模块以二进制格式分发。解码处理该格式并将其转换为模块的内部表示。在本规范中,此表示形式由抽象语法建模,但实际实现可以改为直接编译为机器代码。
- 验证
-
已解码的模块必须是有效的。验证检查许多良好格式条件,以保证模块是有意义且安全的。特别是,它执行函数及其主体中指令序列的类型检查,确保例如操作数堆栈始终一致地使用。
- 执行
-
最后,有效的模块可以被执行。执行可以进一步分为两个阶段
实例化。模块实例是模块的动态表示,包含其自己的状态和执行堆栈。实例化执行模块主体本身,为其所有导入提供定义。它初始化全局变量、内存和表,并在定义的情况下调用模块的启动函数。它返回模块导出的实例。
调用。实例化后,可以通过调用模块实例上的导出函数来启动进一步的 WebAssembly 计算。给定必需的参数,这将执行相应函数并返回其结果。
实例化和调用是嵌入环境中的操作。
2. 结构
2.1. 约定
WebAssembly 是一种编程语言,具有多种具体表示形式(其 二进制格式 和 文本格式)。两者都映射到一个通用结构。为了简洁起见,这种结构以抽象语法的形式描述。本规范的所有部分都根据此抽象语法定义。
2.1.1. 语法表示法
在定义抽象语法的语法规则时,采用以下约定。
-
终结符(原子)以无衬线字体或符号形式编写: .
-
非终结符以斜体形式编写: .
-
是 次 的序列。
-
是一个可能是空的 迭代序列。 (这是 的简写,在 不相关的情况下使用。)
-
是一个非空的 迭代序列。 (这是 的简写,其中 。)
-
是 的可选出现。 (这是 的简写,其中 。)
-
产生式写作 .
-
大型产生式可以拆分为多个定义,用显式省略号 () 结束第一个定义,并用省略号 () 开始后续定义。
-
有些产生式用圆括号 (“”) 附加了边条件,用于简化将产生式组合扩展为多个独立情况的写法。
-
如果同一个元变量或非终结符在产生式中多次出现,则所有这些出现必须具有相同的实例化。 (这是对多个不同变量相等的要求的简写。)
2.1.2. 辅助符号
在处理语法结构时,还会使用以下符号
-
表示空序列。
-
表示序列 的长度。
-
表示序列 的第 个元素,从 开始。
-
表示序列 的子序列。
-
表示与 相同的序列,但将第 个元素替换为 。
-
表示与 相同的序列,除了子序列 被替换为 .
-
表示通过连接所有序列 (在 中)而形成的扁平序列。
此外,采用以下约定。
-
符号 ,其中 是一个非终结符,被视为一个元变量,它遍历 的相应序列(类似于 ,,)。
-
当给定一个序列 时,则假设序列 中的 的出现与 呈逐点对应关系(类似于 ,,)。这隐式地表达了一种在序列上映射语法结构的形式。
以下形式的产生式被解释为记录,它们将一组固定的字段 分别映射到“值” .
采用以下符号来操作这些记录。
-
表示 组件在 中的内容。
-
表示与 相同的记录,只是 组件的内容被替换为 .
-
表示两个具有相同字段序列的记录的组合,通过逐点追加每个序列来完成。
-
表示对一个记录序列进行组合,按顺序进行;如果序列为空,则结果记录的所有字段都为空。
序列和记录的更新符号递归地推广到通过“路径”访问的嵌套组件
-
是 的简写形式,
-
是 的简写形式,
其中 简化为 。
2.1.3. 向量
向量 是形式为 (或 )的有界序列,其中 可以是值或复杂结构。一个向量最多可以有 个元素。
2.2. 值
WebAssembly 程序操作原始数值值。此外,在程序的定义中,值的不变序列用来表示更复杂的数据,例如文本字符串或其他向量。
2.2.1. 字节
最简单的值形式是原始的未解释字节。在抽象语法中,它们用十六进制文字表示。
2.2.1.1. 约定
-
元变量 代表字节。
-
字节有时被解释为自然数 。
2.2.2. 整数
不同类别的整数具有不同的值范围,它们由它们的位宽 以及它们是无符号还是有符号来区分。
类 定义了未解释的整数,其有符号解释可能根据上下文而有所不同。在抽象语法中,它们表示为无符号值。但是,某些操作会 转换 它们为基于二进制补码解释的有符号值。
注意
本规范中出现的主要整数类型是 、、、、、、、。但是,其他大小作为辅助结构出现,例如,在 浮点数 的定义中。
2.2.2.1. 约定
-
元变量 表示整数。
-
数字可以用简单的算术表示,如上面的语法所示。为了区分 这样的算术运算和 这样的序列,后者用括号来区分。
2.2.3. 浮点数
浮点数数据表示与 [IEEE-754-2019] 标准(第 3.3 节)中相应的二进制格式相对应的 32 位或 64 位值。
每个值都有一个符号和一个大小。 大小可以表示为形式为 , 其中 是指数, 是尾数,其最高有效位 是 ,或者表示为非规格化数,其中指数固定为最小可能值, 是 ; 非规格化数中包括正零和负零值。 由于尾数是二进制值,因此规格化数表示为 , 其中 是 的位宽; 非规格化数也是如此。
可能的大小还包括特殊值 (无穷大)和 (NaN,非数字)。 NaN 值具有一个有效载荷,它描述了底层 二进制表示 中的尾数位。 信号 NaN 和安静 NaN 之间没有区别。
其中 且 ,
规范 NaN 是一个浮点值 ,其中 是一个有效载荷,其最高有效位为 ,而其他所有位都为 。
算术 NaN 是一个浮点值 ,其中 ,使得最高有效位为 ,而其他所有位都为任意值。
注意
在抽象语法中,次正规数通过尾数的开头 0 来区分。次正规数的指数与正规数的最小可能指数相同。仅在 二进制表示 中,次正规数的指数以与任何正规数的指数不同的方式编码。
这里定义的规范 NaN 概念与 [IEEE-754-2019] 标准(第 3.5.2 节)为十进制交换格式定义的规范 NaN 概念无关。
2.2.3.1. 约定
-
元变量 涵盖浮点值,其含义从上下文中清楚。
2.2.4. 向量
数值向量 是 128 位值,由向量指令(也称为 SIMD 指令,单指令多数据)处理。它们在抽象语法中使用 表示。通道类型(整数 或 浮点数)和通道大小的解释由对它们进行操作的特定指令确定。
2.2.5. 名称
名称 是字符的序列,字符是 [UNICODE](第 2.4 节)中定义的标量值。
由于 二进制格式 的限制,名称的长度受其 UTF-8 编码长度的限制。
2.2.5.1. 约定
-
字符(Unicode 标量值)有时与自然数 可互换使用。
2.3. 类型
WebAssembly 中的各种实体按类型分类。类型在 验证、实例化 以及可能 执行 期间进行检查。
2.3.1. 数值类型
数值类型 用于对数值进行分类。
类型 和 分别对应于 32 位和 64 位整数。整数本身并非有符号或无符号,它们的解释由各个操作决定。
类型 和 分别对应于 32 位和 64 位浮点数。它们分别对应于二进制浮点数表示,也称为单精度和双精度,如 [IEEE-754-2019] 标准(第 3.3 节)中所定义。
数值类型是透明的,这意味着它们的位模式是可以观察到的。数值类型的值可以存储在 内存 中。
2.3.1.1. 约定
-
符号 表示数值类型 的位宽。也就是说, 和 .
2.3.2. 向量类型
向量类型 用于对由向量指令处理的 数值 向量进行分类(也称为SIMD 指令,单指令多数据)。
类型 对应一个包含128位整型或浮点型数据的打包向量。打包数据可以解释为有符号或无符号整型,单精度或双精度浮点型值,或单个128位类型。解释方式由各个操作确定。
向量类型,如同 数字类型,是透明的,这意味着它们的位模式是可以观察到的。向量类型的值可以存储在 内存 中。
2.3.2.1. 约定
-
符号 用于表示 位宽,它也适用于向量类型,也就是说,.
2.3.3. 引用类型
引用类型对运行时 存储 中对象的的一级引用进行分类。
类型 表示所有对 函数 的引用的无限并集,无论它们的 函数类型 是什么。
类型 表示所有由 嵌入器 拥有并可以以这种类型传递到 WebAssembly 中的引用的无限并集。
引用类型是不透明的,这意味着它们的尺寸或位模式都无法观察到。引用类型的值可以存储在 表格 中。
2.3.4. 值类型
值类型对 WebAssembly 代码可以计算的值和变量接受的值进行分类。它们要么是 数字类型,要么是 向量类型,要么是 引用类型。
2.3.4.1. 约定
-
元变量 在上下文中明确的情况下,表示值类型或其子类。
2.3.5. 结果类型
结果类型对 执行 指令 或 函数 的结果进行分类,结果是一个值序列,用方括号表示。
2.3.6. 函数类型
函数类型对 函数 的签名进行分类,将参数向量映射到结果向量。它们也用于对 指令 的输入和输出进行分类。
2.3.7. 限制
限制 用于对与 内存类型 和 表格类型 相关的可调整大小的存储的大小范围进行分类。
如果没有指定最大值,则相应的存储可以增长到任何大小。
2.3.8. 内存类型
内存类型 用于对线性 内存 及其大小范围进行分类。
这些限制会约束内存的最小大小以及可选的最大大小。限制以 页面大小 为单位。
2.3.9. 表格类型
表格类型 用于对在给定大小范围内包含 引用类型 元素的 表格 进行分类。
与内存类似,表格也会受到限制,用于约束其最小大小和可选的最大大小。限制以条目数为单位。
注意
在 WebAssembly 的未来版本中,可能会引入其他元素类型。
2.3.10. 全局类型
全局类型 用于对 全局变量 进行分类,这些变量会保存一个值,并且可以是可变的,也可以是不可变的。
2.3.11. 外部类型
外部类型 用于对 导入 和 外部值 及其相应的类型进行分类。
2.3.11.1. 约定
以下辅助符号用于外部类型序列。它以保留顺序的方式筛选出特定类型的条目。
2.4. 指令
WebAssembly 代码由一系列指令组成。其计算模型基于堆栈机,指令在隐式操作数堆栈上操作值,消耗(弹出)参数值并生成或返回(压入)结果值。
除了来自堆栈的动态操作数之外,一些指令还具有静态立即参数,通常是索引或类型注释,它们是指令本身的一部分。
一些指令在结构上是结构化的,因为它们对嵌套的指令序列进行括号。
以下部分将指令分为多个不同的类别。
2.4.1. 数值指令
数值指令提供对特定类型的数值值的基本操作。这些操作与硬件中可用的相应操作密切匹配。
数值指令按数值类型划分。对于每种类型,可以区分几个子类别
-
常量:返回一个静态常量。
-
一元运算:消耗一个操作数并生成一个相应类型的结果。
-
二元运算:消耗两个操作数并生成一个相应类型的结果。
-
测试:消耗一个相应类型的操作数并生成一个布尔整型结果。
-
比较:消耗两个相应类型的操作数并生成一个布尔整型结果。
-
转换:消耗一个类型的值并生成另一个类型的结果(转换的源类型是“”之后的那个)。
一些整型指令有两种形式,其中一个符号位注释区分操作数是作为解释为无符号还是有符号整数。对于其他整型指令,使用二进制补码进行有符号解释意味着它们的行为相同,无论符号位如何。
2.4.1.1. 约定
有时,根据以下语法简写将运算符分组在一起会很方便。
2.4.2. 向量指令
向量指令(也称为SIMD 指令,即单指令多数据)提供了对值 的基本操作向量类型.
向量指令的命名约定包括一个前缀,该前缀决定了操作数的解释方式。该前缀描述了操作数的 *形状*,写成 ,由一个打包的 数值类型 和该类型的 *通道* 数 组成。操作在每个通道的值上逐点执行。
注意
例如,形状 将操作数解释为四个 值,打包成一个 。数值类型 的位宽乘以 始终为 128。
以 为前缀的指令不涉及特定解释,并将 视为 值或 128 个独立位的向量。
向量指令可以分组到几个子类别中
-
常量:返回一个静态常量。
-
一元运算:消耗一个 操作数并生成一个 结果。
-
二元运算:消耗两个 操作数并生成一个 结果。
-
三元运算:消耗三个 操作数并生成一个 结果。
-
测试:消耗一个 操作数并生成一个布尔整型结果。
-
移位:消耗一个 操作数和一个 操作数,生成一个 结果。
-
扩展:消耗一个数值类型的值并生成一个指定形状的 结果。
-
提取通道:消耗一个 操作数并返回给定通道中的数值。
-
替换通道:消耗一个 操作数和给定通道的数值,并生成一个 结果。
一些向量指令有一个有符号性注释 ,它区分操作数中的元素是 解释 为 无符号 还是 有符号 整数。对于其他向量指令,使用二进制补码进行有符号解释意味着它们的行为相同,无论有符号性如何。
2.4.2.1. 约定
有时,根据以下语法简写将运算符分组在一起会很方便。
2.4.3. 引用指令
此组指令与访问引用相关。
这些指令分别生成空值、检查空值或生成对给定函数的引用。
2.4.4. 参数指令
此组指令可以在任何值类型的操作数上进行操作。
The instruction simply throws away a single operand.
The instruction selects one of its first two operands based on whether its third operand is zero or not. It may include a value type determining the type of these operands. If missing, the operands must be of numeric type.
注意
In future versions of WebAssembly, the type annotation on may allow for more than a single value being selected at the same time.
2.4.5. 变量指令
这些指令分别获取或设置变量的值。The instruction is like but also returns its argument.
2.4.6. 表指令
这组指令与表 table 相关。
指令 和 分别用于加载或存储表中的元素。
指令 返回表的当前大小。指令 按给定的增量扩展表,并返回之前的大小,如果无法分配足够的空间,则返回 。它还接受新分配的条目的初始化值。
指令 将指定范围内所有条目设置为给定值。
指令 将元素从源表区域复制到可能重叠的目标区域;第一个索引表示目标。指令 将元素从 被动元素段 复制到表中。指令 防止进一步使用被动元素段。此指令旨在用作优化提示。在元素段被丢弃后,其元素将无法再被检索,因此此段使用的内存可能会被释放。
另一个访问表的指令是 控制指令 。
2.4.7. 内存指令
这组指令涉及线性 内存。
使用 和 指令访问内存,这些指令对应不同的 数字类型。它们都接受一个 *内存立即数* ,其中包含地址 *偏移量* 和预期 *对齐方式*(用 2 的幂的指数表示)。整数加载和存储可以选择指定一个比相应值类型 位宽 更小的 *存储大小*。在加载的情况下,需要一个符号扩展模式 来选择适当的行为。
向量加载可以指定一个大小为 位宽 一半的形状。每个车道的大小是其正常大小的一半,符号扩展模式 随后指定如何将较小的车道扩展到较大的车道。或者,向量加载可以执行 *splat*,这样只加载指定存储大小的单个车道,并将结果复制到所有车道。
静态地址偏移量加到动态地址操作数,产生一个 33 位 *有效地址*,该地址是访问内存的零基索引。所有值都以 小端 字节顺序读取和写入。如果任何被访问的内存字节位于内存当前大小暗示的地址范围之外,则会导致 陷阱。
注意
WebAssembly 的未来版本可能会提供具有 64 位地址范围的内存指令。
指令 返回内存的当前大小。指令 通过给定的增量扩展内存并返回之前的大小,如果无法分配足够的内存,则返回 。这两个指令都以 页大小 为单位。
指令 将区域中的所有值设置为给定的字节。指令 将数据从源内存区域复制到可能重叠的目标区域。指令 将数据从 被动数据段 复制到内存中。指令 阻止进一步使用被动数据段。此指令旨在用作优化提示。在数据段被丢弃后,其数据将无法再检索,因此此段使用的内存可能会被释放。
2.4.8. 控制指令
此组中的指令影响控制流。
指令 什么也不做。
指令会导致无条件的 陷阱。
、 和 指令是结构化指令。它们将嵌套的指令序列(称为块)括起来,并以 或 伪指令终止或分隔。根据语法规定,它们必须是嵌套良好的。
结构化指令可以根据其注释的块类型在操作数栈上消耗输入并产生输出。它可以作为一个 类型索引,该索引引用一个合适的 函数类型,或者作为一个可选的 值类型 内联,这是一种函数类型 的简写。
每个结构化控制指令都引入一个隐式的标签。标签是分支指令的目标,这些分支指令使用 标签索引 引用它们。与其他 索引空间 不同,标签的索引是按嵌套深度相对的,也就是说,标签 指的是包围引用分支指令的最内层结构化控制指令,而增加的索引指的是更外层的那些指令。因此,标签只能从内部关联的结构化控制指令中引用。这也意味着分支只能被定向到外部,“中断”它们所针对的控制结构的块。确切的效果取决于该控制结构。在 或 的情况下,它是一个向前跳转,在匹配的 之后恢复执行。在 的情况下,它是一个向后跳转到循环的开头。
注意
这强制执行结构化控制流。直观地,一个针对 或 的分支的行为类似于大多数类似 C 的语言中的 语句,而一个针对 的分支的行为类似于 语句。
分支指令有几种形式: 执行无条件分支, 执行条件分支, 通过操作数索引到标签向量执行间接分支,该标签向量是指令的立即数,或者如果操作数越界,则索引到默认目标。 指令是针对最外层块的无条件分支的快捷方式,该块隐式地是当前函数的主体。执行分支会展开操作数栈,直到到达目标结构化控制指令被输入的高度。但是,分支本身可能会额外消耗操作数,它们会在展开后将操作数压回操作数栈。向前分支需要根据目标块类型的输出来提供操作数,即代表已终止块所产生的值。向后分支需要根据目标块类型的输入来提供操作数,即代表已重新启动块所消耗的值。
指令调用另一个 函数,从栈中消耗必要的参数,并返回调用的结果值。 指令通过操作数索引到一个 表格 间接调用函数,该表格由一个 表格索引 表示,并且必须具有类型 。由于它可能包含不同类型的函数,因此被调用者会动态地与指令第二个立即数所索引的 函数类型 进行检查,如果它们不匹配,则调用会被中止并产生一个 陷阱。
2.4.9. 表达式
函数 主体、全局变量 的初始化值、元素 段的元素和偏移量以及 数据 段的偏移量都以表达式给出,表达式是 指令 的序列,以一个 标记终止。
在某些情况下,验证限制表达式为常量,这限制了允许指令集。
2.5. 模块
WebAssembly 程序被组织成模块,它们是部署、加载和编译的单元。模块收集类型、函数、表、内存和全局变量的定义。此外,它可以声明导入和导出,并以数据和元素段的形式提供初始化,或一个开始函数。
每个向量(以及整个模块)都可能是空的。
2.5.1. 索引
定义使用从零开始的索引进行引用。每类定义都有自己的索引空间,由以下类别区分。
函数、表格、内存和全局变量的索引空间包括同一模块中声明的相应导入。这些导入的索引位于同一索引空间中其他定义的索引之前。
元素索引引用元素段,数据索引引用数据段。
局部变量的索引空间仅在函数内部可访问,并且包括该函数的参数,参数位于局部变量之前。
标签索引引用指令序列内的结构化控制指令。
2.5.1.1. 约定
-
元变量 指代标签索引。
-
元变量 指代任何其他索引空间中的索引。
-
符号 表示在 索引空间中自由出现的索引集。有时该集合被重新解释为其元素的向量。
注意
例如,如果 是 , 那么 , 或等效地,向量 .
2.5.2. 类型
模块的 组件定义了一个函数类型的向量。
模块中使用的所有函数类型都必须在此组件中定义。它们由类型索引引用。
注意
WebAssembly 的未来版本可能会添加其他形式的类型定义。
2.5.3. 函数
模块的 组件定义了一个具有以下结构的函数向量
函数的 通过引用模块中定义的 类型 来声明其签名。 函数的参数通过函数体中以 0 为基的 局部索引 来引用;它们是可变的。
The 声明一个可变局部变量及其类型的向量。 这些变量通过函数体中的 局部索引 来引用。 第一个局部的索引是未引用参数的最小索引。
The 是一个 指令 序列,该序列在终止时必须生成与函数类型的 结果类型 匹配的堆栈。
Functions are referenced through function indices, starting with the smallest index not referencing a function import.
2.5.4. 表
模块的 组件定义了一个由其 表类型 描述的表向量。
表是特定 引用类型 的不透明值的向量。 表类型中的 大小指定了该表的初始大小,而其 (如果存在)限制了其以后可以增长的尺寸。
Tables can be initialized through element segments.
Tables are referenced through table indices, starting with the smallest index not referencing a table import. Most constructs implicitly reference table index .
2.5.5. 内存
模块的 组件定义了一个由其 内存类型 描述的线性内存(或简称内存)向量。
内存是原始未解释字节的向量。 内存类型中的 大小指定了该内存的初始大小,而其 (如果存在)限制了其以后可以增长的尺寸。 两者都以 页面大小 为单位。
Memories can be initialized through data segments.
Memories are referenced through memory indices, starting with the smallest index not referencing a memory import. Most constructs implicitly reference memory index .
注意
在 WebAssembly 的当前版本中,单个模块中最多可以定义或导入一个内存,并且所有结构都隐式地引用了这个内存 。 此限制可能会在将来的版本中解除。
2.5.6. 全局变量
模块的 组件定义了一个全局变量(或简称全局变量)向量。
每个全局变量存储给定全局类型的单个值。其还指定全局变量是不可变还是可变的。此外,每个全局变量都使用值初始化,该值由常量初始化器表达式给出。
2.5.7. 元素段
表的初始内容未初始化。元素段可用于从静态向量元素中初始化表的子范围。
模块的组件定义元素段向量。每个元素段定义一个引用类型以及相应的常量元素表达式列表。
元素段具有一个模式,用于将它们标识为被动、主动或声明式。被动元素段的元素可以使用指令复制到表中。主动元素段在实例化期间将元素复制到表中,如表索引和定义表中偏移量的常量表达式指定。声明式元素段在运行时不可用,但仅用于向前声明在代码中使用等指令形成的引用。
元素段通过元素索引引用。
2.5.8. 数据段
内存的初始内容为零字节。数据段可用于从静态向量字节中初始化内存范围。
模块的组件定义数据段向量。
与元素段类似,数据段具有一个模式,用于将它们标识为被动或主动。被动数据段的内容可以使用指令复制到内存中。主动数据段在实例化期间将内容复制到内存中,如内存索引和定义内存中偏移量的常量表达式指定。
数据段通过 数据索引 引用。
注意
在当前版本的 WebAssembly 中,模块中最多允许一个内存。因此,唯一有效的 是 。
2.5.9. 开始函数
模块的 组件声明一个 开始函数 的 函数索引,该函数在模块被 实例化 时自动调用,在 表格 和 内存 初始化之后。
注意
开始函数用于初始化模块的状态。在此初始化完成之前,模块及其导出无法从外部访问。
2.5.10. 导出
模块的 组件定义了一组 导出,这些导出在模块被 实例化 后对主机环境可用。
每个导出都由一个唯一的 名称 标记。可导出定义是 函数、表格、内存 和 全局变量,它们通过相应的描述符引用。
2.5.10.1. 约定
以下辅助符号定义了导出序列,以按顺序保留的方式筛选出特定类型的索引
2.5.11. 导入
模块的组件定义了一组用于实例化的导入。
每个导入都由一个两级名称空间标记,包括一个名称和一个,用于该模块中的一个实体。可导入的定义是函数、表、内存和全局变量。每个导入都由一个描述符指定,该描述符具有一个相应的类型,实例化期间提供的定义需要与该类型匹配。
每个导入都在相应的索引空间中定义一个索引。在每个索引空间中,导入的索引都位于模块本身包含的任何定义的第一个索引之前。
注意
与导出名称不同,导入名称不一定是唯一的。可以多次导入相同的 / 对;这些导入甚至可能具有不同的类型描述,包括不同类型的实体。具有此类导入的模块仍然可以实例化,具体取决于 嵌入器 如何允许解析和提供导入。但是,嵌入器不需要支持这种重载,并且 WebAssembly 模块本身不能实现重载的名称。
3. 验证
3.1. 约定
验证检查 WebAssembly 模块是否格式正确。只有有效的模块才能 实例化。
有效性由 抽象语法 上的类型系统定义,该系统定义了 模块 及其内容。对于每个抽象语法,都有一个类型规则,指定适用于它的约束。所有规则都以两种等效形式给出
-
以散文形式描述直观的含义。
-
以正式符号形式描述规则,以数学形式表示。 [1]
注意
散文和正式规则是等效的,因此不需要理解正式符号就可以阅读本规范。形式主义提供了一种更简洁的描述,它使用的是编程语言语义中广泛使用的符号,并且很容易进行数学证明。
在这两种情况下,规则都是以声明式方式制定的。也就是说,它们只制定约束,并不定义算法。在 附录 中提供了根据本规范对指令序列进行类型检查的健全且完整的算法的框架。
3.1.1. 上下文
单个定义的有效性是相对于上下文指定的,上下文收集了有关周围 模块 和作用域内的定义的相关信息
-
类型:当前模块中定义的类型列表。
-
函数:当前模块中声明的函数列表,用它们的函数类型表示。
-
表:当前模块中声明的表列表,用它们的表类型表示。
-
内存:当前模块中声明的内存列表,用它们的内存类型表示。
-
全局变量:当前模块中声明的全局变量列表,用它们的全局变量类型表示。
-
元素段:当前模块中声明的元素段列表,用它们的元素类型表示。
-
数据段:当前模块中声明的数据段列表,每个数据段都用 条目表示。
-
局部变量:当前函数(包括参数)中声明的局部变量列表,用它们的值类型表示。
-
标签:从当前位置可以访问的标签堆栈,用它们的返回值类型表示。
-
返回值:当前函数的返回值类型,表示为可选的返回值类型,当不允许返回值时(如在独立表达式中)不存在。
-
引用:模块中函数外部出现的 函数索引 列表,因此可以在函数内部使用它们来形成引用。
换句话说,上下文包含一系列适合的 类型,用于每个 索引空间,描述该空间中每个定义的条目。局部变量、标签和返回值类型仅用于验证 指令 在 函数体 中,在其他地方为空。标签堆栈是上下文唯一在验证指令序列时发生变化的部分。
更具体地说,上下文被定义为 记录 ,其抽象语法为
除了以 形式编写的字段访问之外,以下符号用于操作上下文。
-
当拼写出上下文时,空字段会被省略。
-
表示与 相同的上下文,但元素 被预先添加到其 组件序列中。
3.1.2. 散文符号
验证由针对 抽象语法 的每个相关部分的程式化规则指定。这些规则不仅陈述了定义何时短语有效的约束条件,还将其归类为一种类型。在陈述这些规则时,采用以下约定。
-
如果且仅当某个词组 满足了相应规则所表达的所有约束,那么该词组就被称为“在类型 下有效”。 的形式取决于 是什么。
-
这些规则隐含地假设了给定的 上下文 。
-
在某些地方,该上下文被局部扩展为上下文 ,其中包含了额外的条目。 “在上下文 下,… 语句 …” 这种表述用于表达以下语句必须在扩展上下文所蕴含的假设下适用。
3.1.3. 形式符号
注意
本节简要介绍了用于正式指定类型规则的符号。 对于感兴趣的读者,可以在相应的教科书中找到更全面的介绍。 [2]
一个词组 具有相应的类型 的命题写成 。 然而,一般来说,类型取决于上下文 。 为了明确表达这一点,完整形式是 判断 ,它表明 在 中编码的假设下成立。
形式类型规则采用了一种标准方法来指定类型系统,将其呈现为 推导规则。 每个规则都有以下一般形式
这种规则被解读为一个大的蕴涵:如果所有前提都成立,那么结论就成立。 有些规则没有前提;它们是结论无条件成立的 公理。 结论始终是一个判断 ,并且每个相关的抽象语法结构 都对应一个规则。
注意
例如, 指令的类型规则可以作为一个公理给出
该指令始终在类型 下有效(表明它消耗两个 值并生成一个),与任何辅助条件无关。
像 这样的指令可以这样类型化
这里,前提要求立即的 局部索引 存在于上下文中。指令会产生一个与其类型 相对应的值(并且不会消耗任何值)。如果 不存在,则前提不成立,指令类型错误。
最后,一个 结构化 指令需要一个递归规则,其中前提本身就是一个类型判断。
一个 指令只有在它主体中的指令序列有效时才有效。此外,结果类型必须与块的注释 匹配。如果是这样,则 指令具有与主体相同的类型。在主体内部,有一个额外的对应结果类型的标签可用,这可以通过将上下文 扩展到前提中包含额外的标签信息来表达。
语义来自以下文章:Andreas Haas, Andreas Rossberg, Derek Schuff, Ben Titzer, Dan Gohman, Luke Wagner, Alon Zakai, JF Bastien, Michael Holman. Bringing the Web up to Speed with WebAssembly. Proceedings of the 38th ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI 2017). ACM 2017.
例如:Benjamin Pierce. Types and Programming Languages. The MIT Press 2002
3.2. 类型
大多数 类型 都是普遍有效的。但是,对 限制 的限制适用,这些限制必须在验证期间进行检查。此外, 块类型 被转换为普通 函数类型 以便于处理。
3.2.1. 限制
限制 必须具有有意义的边界,这些边界必须在给定范围内。
3.2.1.1.
-
的值不能大于 .
-
如果最大值 非空,则
-
它的值不能大于 .
-
它的值不能小于 .
-
-
那么这个限制在 范围内是有效的。
3.2.2. 块类型
块类型 可以用两种形式表示,这两种形式都将被以下规则转换为普通的 函数类型。
3.2.2.1.
-
类型 必须在上下文中定义。
-
那么块类型在 函数类型 中是有效的。
3.2.2.2.
-
块类型在 函数类型 中是有效的。
3.2.3. 函数类型
函数类型 始终有效。
3.2.3.1.
-
函数类型是有效的。
3.2.4. 表类型
3.2.4.1.
-
限制 必须在范围 内 有效。
-
然后表类型有效。
3.2.5. 内存类型
3.2.5.1.
-
限制 必须在范围 内 有效。
-
然后内存类型有效。
3.2.6. 全局类型
3.2.6.1.
-
全局类型有效。
3.2.7. 外部类型
3.2.7.1.
-
函数类型 必须 有效。
-
然后外部类型有效。
3.2.7.2.
-
表类型 必须 有效。
-
然后外部类型有效。
3.2.7.3.
-
内存类型 必须 有效。
-
然后外部类型有效。
3.2.7.4.
-
The global type must be valid.
-
然后外部类型有效。
3.2.8. 导入子类型
当 实例化 模块时,外部值 必须提供,其 类型 与每个导入所分类的相应 外部类型 相匹配。 在某些情况下,这允许使用简单的子类型形式(正式写为“”),如这里所定义。
3.2.8.1. 限制
限制 匹配限制 当且仅当
-
大于或等于 .
-
要么
-
为空。
-
-
或者
-
两者 和 均不为空。
-
小于或等于 .
-
3.2.8.2. 函数
一个 外部类型 匹配 当且仅当
-
两者 和 相同。
3.2.8.3. 表
一个外部类型 与 匹配,当且仅当
-
限制 匹配 .
-
两个 和 相同。
3.2.8.4. 内存
一个外部类型 与 匹配,当且仅当
-
限制 匹配 .
3.2.8.5. 全局变量
一个 外部类型 与 相匹配当且仅当
-
和 相同。
3.3. 指令
指令 通过堆栈类型 进行分类,这些分类描述了指令如何操作 操作数堆栈.
这些类型描述了指令弹出所需的输入栈,其中包含操作数类型 ,以及提供的输出栈,其中包含类型为 的结果值,它会将这些值压回栈中。栈类型类似于 函数类型,但它们允许将单个操作数分类为 (底部),表示该类型不受约束。作为辅助概念,操作数类型 与另一个操作数类型 匹配,如果 是 或等于 。这以逐点方式扩展到栈类型。
注意
例如,指令 的类型为 ,它消耗两个 值并产生一个值。
输入扩展到 指令序列 . 这样的序列具有 栈类型 ,如果执行指令的累积效果是从操作数栈中消耗类型为 的值,并将类型为 的新值压入栈。
对于某些指令,类型规则并不能完全约束类型,因此允许多种类型。这些指令被称为多态。可以区分两种程度的多态性
在这两种情况下,只要满足程序周围部分施加的约束,就可以任意选择不受约束的类型或类型序列。
注意
例如, 指令在类型 下有效,对于任何可能的 数字类型 。因此,这两个指令序列
和
是有效的,其中 在 的类型中分别实例化为 或 .
指令 在任何可能的 操作数类型 序列 和 下都是有效的,类型为 。因此,
假设指令 的类型为 是有效的。相反,
是无效的,因为没有可以为指令 选择的类型可以使序列类型正确。
在附录中描述了一种类型检查 算法,它可以有效地实现这里给出的规则所规定的指令序列验证。
3.3.1. 数值指令
3.3.1.1.
-
该指令的类型为 是有效的。
3.3.1.2.
-
该指令的类型为 是有效的。
3.3.1.3.
-
该指令的类型为 是有效的。
3.3.1.4.
-
该指令的类型为 是有效的。
3.3.1.5.
-
该指令在类型 下有效。
3.3.1.6.
-
该指令在类型 下有效。
3.3.2. 引用指令
3.3.2.1.
-
该指令的类型为 是有效的。
注意
在未来的 WebAssembly 版本中,可能存在不允许空引用的引用类型。
3.3.2.2.
-
该指令在类型 下有效,对于任何 引用类型 。
3.3.2.3.
-
函数 必须在上下文中定义。
-
函数索引 必须包含在 中。
-
该指令的类型有效为 .
3.3.3. 向量指令
向量指令可以带有前缀来描述操作数的 形状。打包数值类型, 和 , 不是 值类型。辅助函数将这种打包类型形状映射到值类型
以下辅助函数表示向量形状的通道数,即其 *维度*
3.3.3.1.
-
该指令的类型有效为 .
3.3.3.2.
-
该指令的类型为 。
3.3.3.3.
-
该指令的类型为 。
3.3.3.4.
-
该指令的类型为 。
3.3.3.5.
-
该指令的类型为 。
3.3.3.6.
-
该指令的类型为 。
3.3.3.7.
-
对于所有 , 在 , 必须小于 .
-
该指令的类型为 。
3.3.3.8.
-
令 为 .
-
该指令在类型 下有效。
3.3.3.9.
-
车道索引 必须小于 .
-
该指令在类型 下有效。
3.3.3.10.
-
车道索引 必须小于 .
-
令 为 .
-
该指令在类型 下有效。
3.3.3.11.
-
该指令的类型为 。
3.3.3.12.
-
该指令的类型为 。
3.3.3.13.
-
该指令的类型为 。
3.3.3.14.
-
该指令类型有效:.
3.3.3.15.
-
该指令的类型为 。
3.3.3.16.
-
该指令的类型为 。
3.3.3.17.
-
该指令的类型为 。