序言
出于对成为编译器工程师的向往,我开始深入挖掘各项编译技术的细节。作为一名前端工程师,我决定首先从 WebAssembly 技术开始学习。本系列文章记录了我阅读 WebAssembly 规范的重点笔记。
你可以在此链接查阅完整的 WebAssembly 规范:WebAssembly Spec 。
约定
在实例化模块或调用模块实例上的导出函数时,WebAssembly 代码执行。
执行通过抽象机器的形式定义,该机器模拟程序状态。它包括一个栈(stack) ,记录操作数和控制结构,还包含一个全局状态的抽象存储(store)。
对于每个指令,都有一条规则规定其执行对程序状态的影响。此外,还有描述模块实例化的规则。与验证一样,所有规则都以两种等效形式给出:
- 在描述性符号中,以直观的形式描述规则。
- 在形式化符号中,以数学形式描述规则。
描述性符号
执行由抽象语法的每条指令规则来定义。在描述这些规则时采用以下约定。
- 执行规则假设给定存储 <math xmlns="http://www.w3.org/1998/Math/MathML"> S S </math>S。
- 执行规则还假设存在一个栈,该栈通过值、标签和帧的出栈和入栈进行修改。
- 某些规则要求栈至少包含一个帧。最近的帧称为当前帧(current frame)。
- 存储和当前帧都可以通过替换它们的一些组件而被改变。这种替换被认为是全局应用的。
- 指令的执行可能会产生陷阱,此时整个计算将被中止,并且它不会对存储进行进一步的修改。(其他计算仍然可以在之后启动。)
- 指令的执行也可能以跳转到指定目标结束,该目标定义了要执行的下一条指令。
- 执行可以进入和退出成块的指令序列。
- 指令序列会按顺序执行,除非发生陷阱或跳转。
- 在各个地方,规则都包含了表达程序状态关键常量的断言。
形式化符号
形式化的执行规则使用一种标准方法来指定操作语义,将它们转化为简化规则。每个规则都有以下的一般形式:
configuration 是对程序状态的描述。每个规则指定了一个执行步骤。只要对给定的配置至多有一个可用的归约规则,归约------执行------就是确定的。WebAssembly 对此只有非常少的例外,这些例外在此规范中明确注明。
对于 WebAssembly,configuration 通常是一个由当前存储器 <math xmlns="http://www.w3.org/1998/Math/MathML"> S S </math>S、当前函数调用帧 <math xmlns="http://www.w3.org/1998/Math/MathML"> F F </math>F 和要执行的指令序列组成的元组( <math xmlns="http://www.w3.org/1998/Math/MathML"> S ; F ; i n s t r ∗ S;F;instr* </math>S;F;instr∗)。(稍后会给出更精确的定义。)
为了避免不必要的混乱,存储器 <math xmlns="http://www.w3.org/1998/Math/MathML"> S S </math>S 和帧 <math xmlns="http://www.w3.org/1998/Math/MathML"> F F </math>F 不会在归约规则中省略。
栈没有单独的表示。相反,它表示为 configuration 的指令序列的一部分。特别是,值被定义为与 <math xmlns="http://www.w3.org/1998/Math/MathML"> const \textsf{const} </math>const 指令相符,一系列的 <math xmlns="http://www.w3.org/1998/Math/MathML"> const \textsf{const} </math>const 指令可以被解释为一个向右增长的操作数"栈"。
注意
例如,可以按照以下方式给出 <math xmlns="http://www.w3.org/1998/Math/MathML"> i32.add \textsf{i32.add} </math>i32.add 指令的归约规则:
根据这个规则,两个 <math xmlns="http://www.w3.org/1998/Math/MathML"> const \textsf{const} </math>const 指令和 <math xmlns="http://www.w3.org/1998/Math/MathML"> add \textsf{add} </math>add 指令本身从指令流中移除,并替换为一个新的 <math xmlns="http://www.w3.org/1998/Math/MathML"> const \textsf{const} </math>const 指令。这可以解释为从栈中弹出两个值并推入结果。
当没有产生结果时,指令简化为空序列:
标签和帧也被定义为指令序列的一部分。
归约的顺序由适当的执行上下文的定义来确定。
当没有更多的归约规则适用时,归约终止。得益于 WebAssembly 类型系统的健全性保证,只有当原始指令序列已经被归约为一系列的 <math xmlns="http://www.w3.org/1998/Math/MathML"> const \textsf{const} </math>const 指令时,这会发生这种情况。这些归约后指令可以被解释为结果操作数栈的值,或者是产生的陷阱。
运行时结构
存储、栈和其他运行时结构形成 WebAssembly 抽象机器,例如值或模块实例,通过附加的辅助语法来精确描述。
值(Values)
WebAssembly 计算操作值可以是四种基本数字类型,即32位或64位的整数和浮点数,128位宽度的向量,引用类型。
在语义中,大多数地方,不同类型的值都可以出现。为了避免歧义,值用抽象语法表示,明确它们的类型。重用 <math xmlns="http://www.w3.org/1998/Math/MathML"> const \textsf{const} </math>const 指令和 <math xmlns="http://www.w3.org/1998/Math/MathML"> ref.null \textsf{ref.null} </math>ref.null 来构造它们。
除了 null 引用使用额外的管理指令来表示。它们要么是函数引用,指向一个特定的函数地址,要么是外部引用,指向一个由嵌入器定义的用于表示其自身对象的未解释的外部地址形式。
每种值类型都有一个关联的默认值。
结果(Results)
结果是计算的产物。它可以是一系列的值或者是一个陷阱。
存储(Store)
存储器代表了可以由 WebAssembly 程序操纵的所有全局状态。它由所有函数、表、内存、全局变量、元素段和数据段的运行时表示组成,这些都是在抽象机器的生命周期中分配的。
在语义上,没有任何元素或数据实例能被其他模块实例所访问。
在语法上,存储器被定义为一个记录,列出了每个类别的现有实例:
地址(Address)
存储器中的函数实例、表实例、内存实例、全局实例、元素实例和数据实例都使用抽象地址进行引用。这些只是对应存储组件的索引。此外,嵌入器可能会提供一组未解释的宿主地址。
嵌入器可以为与它们的地址相对应的导出存储对象分配身份,即使这种身份在 WebAssembly 代码本身内部并不能被观察到(例如对于函数实例或不可变的全局变量)。
模块实例(Module Instances)
模块实例是模块的运行时表示。它是通过实例化一个模块创建的,并收集了模块导入、定义或导出的所有实体的运行时表示。
函数实例(Function Instances)
函数实例是函数的运行时表示。它实际上是原始函数在其源模块的运行时模块实例上的闭包。该模块实例用于在执行函数时解析对其他定义的引用。
表实例(Table Instances)
全局实例(Global Instances)
元素实例(Element Instances)
数据实例(Data Instances)
外部实例(Export Instances)
外部值(External Values)
栈(Stack)
除了存储之外,大多数指令都与栈交互。栈包含三种类型的条目:
- 值(Values):指令的操作数。
- 标签(Labels):活动的结构化控制指令,作为分支目标。
- 栈帧(Activations):活动函数的调用帧。
这些条目可以在程序执行期间以任何顺序出现在栈上。栈条目由抽象语法描述如下。
值
表示值本身。
标签(Labels)
标签携带一个参数 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 和它们关联的分支目标,这在语法上表达为指令序列:
注意
例如,循环标签的形式为:
执行到该标签时,会执行该循环,从头开始重新启动。相反,一个简单的块标签的形式为:
当分支时,空续体会结束目标块,以便执行可以继续进行连续的指令。
栈帧(Activation Frames)
栈帧携带相应函数的返回元数,按照静态本地索引的顺序保存其本地变量的值(包括参数),以及对函数自身模块实例的引用。
管理指令
为了表达陷阱、调用和控制指令的减少,指令的语法被扩展以包括以下管理指令: