[关闭]
@a06062125 2017-06-10T17:29:29.000000Z 字数 31184 阅读 353

Semantics - 语义

This document explains the high-level design of WebAssembly code: its types, constructs, and semantics. WebAssembly code can be considered a structured stack machine; a machine where most computations use a stack of values, but control flow is expressed in structured constructs such as blocks, ifs, and loops. In practice, implementations need not maintain an actual value stack, nor actual data structures for control; they need only behave as if they did so. For full details consult the formal Specification, for file-level encoding details consult Binary Encoding, and for the human-readable text representation consult Text Format.

本文档主要介绍WebAssembly代码的高级设计:类型,结构和语义。WebAssembly代码可以被认为是一个结构化堆栈机器; 大多数计算仅使用栈中存储的数值内容,但是控制流使用结构化构造表示,如区块,条件判断ifs 和循环等。 实践中, 具体实现无须为控制流维护实际值栈和数据结构;它们只须遵循as if规则 。 全部详情请参阅the formal Specification, 想要了解文件级编码详情,请阅读Binary Encoding, 人类可读的文本表示请参阅文本格式请阅读Text Format.

[注]

as-if rule 只要不改变程序的 可观察行为(observable behavior),就可以任意改变程序代码。在as-if规则下,编译器可以对代码做最大限度的优化,尽可能提升应用程序的执行效率。

Each function body consists of a list of instructions which forms an implicit block. Execution of instructions proceeds by way of a traditional program counter that advances through the instructions. Instructions fall into two categories: control instructions that form control constructs and simple instructions. Control instructions pop their argument value(s) off the stack, may change the program counter, and push result value(s) onto the stack. Simple instructions pop their argument value(s) from the stack, apply an operator to the values, and then push the result value(s) onto the stack, followed by an implicit advancement of the program counter.

函数体由指令序列组成, 也要以把它看作一个隐式块。 同传统程序计数器(PC)一样,指令执行过程通过向前移动的方式处理指令序列。 指令分为两类:控制指令和简单指令。控制指令组成了控制结构和简单指令, 它从栈顶弹出参数值,根据情况决定是否更改PC的内容,然后将结果值压入堆栈。 简单指令从堆栈中弹出一个或多个参数值,批量对这些值应用一个运算符操作后,将结果值压入堆栈,随后自动递增 PC 的内容。

All instructions and operators in WebAssembly are explicitly typed, with no overloading rules. Verification of WebAssembly code requires only a single pass with constant-time type checking and well-formedness checking.

WebAssembly中所有指令和运算符都是显式类型的,不支持重载。 WebAssembly代码验证需要在一个常数时间内同时通过类型检查和合法性检查。

WebAssembly offers a set of language-independent operators that closely match operators in many programming languages and are efficiently implementable on all modern computers. Each operator has a corresponding simple instruction.

WebAssembly提供了一组语言无关的运算符,它们与许多编程语言中的运算符紧密匹配,并且可在所有现代计算机上高效地实现。 每个运算符都有相应的简单指令。

The rationale document details why WebAssembly is designed as detailed in this document.

:unicorn: = Planned future feature

理论基础 文档详细解释了WebAssembly被设计成这样的原因。

:unicorn:

= 未来功能 计划

Traps - 陷阱

Some operators may trap under some conditions, as noted below. In the MVP, trapping means that execution in the WebAssembly instance is terminated and abnormal termination is reported to the outside environment. In a JavaScript environment such as a browser, a trap results in throwing a JavaScript exception. If developer tools are active, attaching a debugger before the termination would be sensible.

一些运算符在某些情况下可能会引起陷入,下面会提到具体情况。 在MVP中,陷阱意味着WebAssembly实例中的执行将被终止, 将异常终止报告给外部环境。 在JavaScript环境中,比如浏览器,陷阱会导致JavaScript异常。如果找开了开发者工具,在终止前添加一个debugger调试会是非常明智的。

Stack Overflow - 栈溢出

Call stack space is limited by unspecified and dynamically varying constraints and is a source of nondeterminism. If program call stack usage exceeds the available call stack space at any time, the execution in the WebAssembly instance is terminated and abnormal termination is reported to the outside environment.

调用堆栈空间受到未指定和动态变化约束的限制,它们也带来了 不确定性.。 任何时候程序调用堆栈使用的空间超出了可用的调用堆栈空间,都会终止WebAssembly实例中的执行,异常终止将被报告给外部环境。

Implementations must have an internal maximum call stack size, and every call must take up some resources toward exhausting that size (of course, dynamic resources may be exhausted much earlier). This rule exists to avoid differences in observable behavior; if some implementations have this property and others don’t, the same program which runs successfully on some implementations may consume unbounded resources and fail on others. Also, in the future, it is expected that WebAssembly will add some form of stack-introspection functionality, in which case such optimizations would be directly observable.

实现必须设定一个内部的最大调用堆栈空间尺寸,每次调用必须占用一些资源, 来消耗该值(当然,动态资源可能会更快耗尽)。 这个规则是为了避免观察行为间的差异; 如果某些实现设定了内部最大调用堆栈空间尺寸,有些实则没有,那么在这些没设定最大调用堆栈空间尺寸的实现上成功运行的程序可能会无限制地消耗资源,将这些程序放在其他实现中运行将会失败。 此外,预期WebAssembly未来将添加某种形式的堆栈内省功能,在这种情况下,优化会是直接可见的。

Support for explicit tail calls is planned in the future :unicorn:, which would add an explicit tail-call operator with well-defined effects on stack introspection.

显式尾调用计划在未来 :unicorn: 支持。 介时会提供一个显式的尾调用操作符, 它具有明确的栈内省功能定义。

Types - 类型

WebAssembly has the following value types:

WebAssembly 具有以下 值类型:

Each parameter and local variable has exactly one value type. Function signatures consist of a sequence of zero or more parameter types and a sequence of zero or more return types. (Note: in the MVP, a function can have at most one return type).

每个参数和局部变量都必须是以上四种值类型之一 . 函数签名由0或多个参数的类型序列及0或多个返回值的类型序列组成。 (注意:在MVP中,一个函数最多可以有一个返回类型)。

Note that the value types i32 and i64 are not inherently signed or unsigned. The interpretation of these types is determined by individual operators.

需要注意的是, 值类型i32i64不是固有有符号或无符号的。 这些类型的解释取决于某个具体的操作符。

Linear Memory - 线性内存

A linear memory is a contiguous, byte-addressable range of memory spanning from offset 0 and extending up to a varying memory size. This size is always a multiple of the WebAssembly page size, which is fixed to 64KiB (though large page support may be added in an opt-in manner in thefuture). The initial state of a linear memory is defined by the module’s linear memory and datasections. The memory size can be dynamically increased by the grow_memory operator.

线性内存 是一段 按字节寻址的连续存储区间, 范围从0 到一个可变 内存容量 。 这个容量通常是WebAssembly页容量的倍数, WebAssembly的页容量被固定为64KiB(尽量未来可能添加可选的更大页容量支持)。模块文档的线性内存数据部分中给出了线性内存的初始状态定义。可以用 grow_memory 操作符动态增加线性内存容量。

A linear memory can be considered to be an untyped array of bytes, and it is unspecified how embedders map this array into their process’ own virtual memory. Linear memory is sandboxed; it does not alias other linear memories, the execution engine’s internal data structures, the execution stack, local variables, or other process memory.

线性内存可以被认为是一个无类型的字节数组,嵌入器是如何把这个数组映射到进程自己的 虚拟内存 中并没给出明确说明。线性内存是沙盒隔离的,它不依赖其它的线性内存、 执行引擎的内部数据结构 , 执行堆栈、局部变量、其它进程的内存。

Every WebAssembly instance has one specially-designated default linear memory which is the linear memory accessed by all the memory operators below. In the MVP, there are only default linear memories but new memory operators :unicorn: may be added after the MVP which can also access non-default memories.

每个WebAssembly 实例都有特别指定的 默认 线性内存, 这段内存可被所有的内存操作符访问。在MVP中,只有默认的线性内存,但在MVP之后可能会添加可以访问非默认内存的 新存储器操作符

Linear memories (default or otherwise) can either be imported or defined inside the module. After import or definition, there is no difference when accessing a linear memory whether it was imported or defined internally.

线性内存(默认或其它)的分配有两种方式, 要么是被 导入 , 要么是 模块内定义的。这两种方式对后续的线性内存访问没有区别。

In the MVP, linear memory cannot be shared between threads of execution. The addition of threads :unicorn: will allow this.

在MVP中,线性内存不能在执行线程间共享。未来将会允许线程间共享, 见线程 .

Linear Memory Accesses - 线性内存访问

Linear memory access is accomplished with explicit load and store operators. All load and store operators use little-endian byte order when translating between values and bytes. Integer loads can specify a storage size which is smaller than the result type as well as a signedness which determines whether the bytes are sign- or zero- extended into the result type.

线性存储器访问通过显式调用 loadstore 操作符完成。 对值和字节进行转换时, 所有的loadstore 操作符都采用小端字节序。加载整数可以指定存储大小和符号, 存储大要小小于结果类型的大小, 符号决定结果类型中的字节是符号扩展的还是零扩展的。

注:

符号扩展: 二进制中的有符号数,符号位总是位于数的第一位,如果向方位较大的数据类型进行扩展,符号位也应该位于第一位才对,所以当一个负数被扩展时,其扩展的高位全被置位为1;对于整数,因为符号位是0,所以其扩展的位仍然是0

零扩展: 不管要转换成什么整型类型,不要最初值的符号位是什么,扩展的高位都被置位0.

Stores have an additional input operand which is the value to store to memory. Like loads, integer stores may specify a smaller storage size than the operand size in which case integer wrapping is implied.

Store operators do not produce a value.

The above operators operate on the default linear memory.

存储有一个额外的输入操作数,这个操作数就是要存储到内存中的value, 同加载一样,整数存储可以指定比操作数更小的存储大小, 这种情况下隐含了整数包装。

存储操作符不产生value

以上操作符都在默认线性内存上进行操作。

Addressing - 寻址

Each linear memory access operator has an address operand and an unsigned integer byte offset immediate. The infinite-precision unsigned sum of the address operand’s value with the offset’s value is called the effective address, which is interpreted as an unsigned byte index into the linear memory.

每个线性存储器访问操作符都有一个地址操作数和无符号的整数字节偏移立即数。 对地址操作数值与偏移值进行无限精度无符号求和得到的值, 称为有效地址,这被解释为线性内存中的无符号字节索引。

Linear memory operators access the bytes starting at the effective address and extend for the number of bytes implied by the storage size. If any of the accessed bytes are beyond the current memory size, the access is considered out-of-bounds.

线性内存操作符从有效地址开始访问字节,一直访问到存储大小指定的字节数。 如果任何访问的字节超出了当前的内存大小,则访问被认为越界

The use of infinite-precision in the effective address computation means that the addition of the offset to the address never causes wrapping, so if the address for an access is out-of-bounds, the effective address will always also be out-of-bounds.

在有效地址计算中使用无限精度意味着向地址添加偏移量绝对不会导致包装,因此,如果访问地址超出范围,则有效地址将始终为越界。

In wasm32, address operands and offset attributes have type i32, and linear memory sizes are limited to 4 GiB (of course, actual sizes are further limited by available resources). In wasm64, address operands and offsets have type i64. The MVP only includes wasm32; subsequent versions will add support for wasm64 and thus >4 GiB linear memory :unicorn:.

在wasm32中, 地址操作数和偏移量属性的数据类型为i32, 线性内存大小限制为4 GiB(当然,实际大小进一步受 可用资源的限制)。 在wasm64中,地址操作数和偏移量的数据类型为i64。 MVP只包括wasm32; 后续版本将增加对wasm64的支持,从而支持 >4 GiB 的线性内存 .。

Alignment - 对齐

Each linear memory access operator also has an immediate positive integer power of 2 alignment attribute which must be no greater than the memory access’ size. An alignment value which is the same as the memory access’ size is considered to be a natural alignment. The alignment applies to the effective address and not merely the address operand, i.e. the immediate offset is taken into account when considering alignment.

The alignment has same type (determined by wasm32/wasm64, as described above) as the address and offset operands.

If the effective address of a memory access is a multiple of the alignment attribute value of the memory access, the memory access is considered aligned, otherwise it is considered misaligned. Aligned and misaligned accesses have the same behavior.

Alignment affects performance as follows:

Thus, it is recommend that WebAssembly producers align frequently-used data to permit the use of natural alignment access, and use loads and stores with the greatest alignment values practical, while always avoiding misaligned accesses.

Out of Bounds

Out of bounds accesses trap. If the access is a store, if any of the accessed bytes are out of bounds, none of the bytes are modified.

Resizing

In the MVP, linear memory can be resized by a grow_memory operator. The operand to this operator is in units of the WebAssembly page size, which is defined to be 64KiB (though large page support may be added in the future :unicorn:).

When a linear memory has a declared maximum memory size, grow_memory must fail if it would grow past the maximum. However, grow_memory may still fail before the maximum if it was not possible to reserve the space up front or if enabling the reserved memory fails. When there is no maximum memory size declared, grow_memory is expected to perform a system allocation which may fail.

The current size of the linear memory can be queried by the following operator:

As stated above, linear memory is contiguous, meaning there are no “holes” in the linear address space. After the MVP, there are future features :unicorn: proposed to allow setting protection and creating mappings within the contiguous linear memory.

In the MVP, memory can only be grown. After the MVP, a memory shrinking operator may be added. However, due to normal fragmentation, applications are instead expected release unused physical pages from the working set using the discard :unicorn: future feature.

The above operators operate on the default linear memory.

Table

A table is similar to a linear memory whose elements, instead of being bytes, are opaque values of a particular table element type. This allows the table to contain values—like GC references, raw OS handles, or native pointers—that are accessed by WebAssembly code indirectly through an integer index. This feature bridges the gap between low-level, untrusted linear memory and high-level opaque handles/references at the cost of a bounds-checked table indirection.

The table’s element type constrains the type of elements stored in the table and allows engines to avoid some type checks on table use. When a WebAssembly value is stored in a table, the value’s type must precisely match the element type. Depending on the operator/API used to store the value, this check may be static or dynamic. Just like linear memory, updates to a table are observed immediately by all instances that reference the table. Host environments may also allow storing non-WebAssembly values in tables in which case, as with imports, the meaning of using the value is defined by the host environment.

Every WebAssembly instance has one specially-designated default table which is indexed by call_indirect and other future table operators. Tables can either be imported or defined inside the module. After import or definition, there is no difference when calling into a table whether it was imported or defined internally.

In the MVP, the primary purpose of tables is to implement indirect function calls in C/C++ using an integer index as the pointer-to-function and the table to hold the array of indirectly-callable functions. Thus, in the MVP:

These restrictions may be relaxed in the future :unicorn:.

Local variables

Each function has a fixed, pre-declared number of local variables which occupy a single index space local to the function. Parameters are addressed as local variables. Local variables do not have addresses and are not aliased by linear memory. Local variables have value types and are initialized to the appropriate zero value for their type (0 for integers, +0. for floating-point) at the beginning of the function, except parameters which are initialized to the values of the arguments passed to the function.

The details of index space for local variables and their types will be further clarified, e.g. whether locals with type i32 and i64 must be contiguous and separate from others, etc.

Global variables

A global variable stores a single value of a fixed value type and may be declared either mutable or immutable. This provides WebAssembly with memory locations that are disjoint from any linear memory and thus cannot be arbitrarily aliased as bits.

Global variables are accessed via an integer index into the module-defined global index space. Global variables can either be imported or defined inside the module. After import or definition, there is no difference when accessing a global.

It is a validation error for a set_global to index an immutable global variable.

In the MVP, the primary use case of global variables is to represent instantiation-time immutable values as a useful building block for dynamic linking.

After the MVP, when reference types are added to the set of value types, global variables will be necessary to allow sharing reference types between threads :unicorn: since shared linear memory cannot load or store references.

Control constructs and instructions

WebAssembly offers basic structured control flow constructs such as blocks, loops, and ifs. All constructs are formed out of the following control instructions:

Blocks are composed of matched pairs of blockend instructions, loops with matched pairs ofloopend instructions, and ifs with either ifend or ifelseend sequences. For each of these constructs the instructions in the ellipsis are said to be enclosed in the construct.

Branches and nesting

The br, br_if, and br_table instructions express low-level branching and are hereafter refered to simply as branches. Branches may only reference labels defined by a construct in which they are enclosed. For example, references to a block’s label can only occur within the block’s body.

In practice, outer blocks can be used to place labels for any given branching pattern, except that the nesting restriction makes it impossible to branch into the middle of a loop from outside the loop. This limitation ensures by construction that all control flow graphs are well-structured as in high-level languages like Java, JavaScript and Go. Notice that a branch to a block’s label is equivalent to a labeled break in high-level languages; branches simply break out of a block, and branches to a loop correspond to a “continue” statement.

Execution semantics of control instructions

Executing a return pops return value(s) off the stack and returns from the current function.

Executing a block or loop instruction has no effect on the value stack.

Executing the end of a block or loop (including implicit blocks such as in if or for a function body) has no effect on the value stack.

Executing the end of the implicit block for a function body pops the return value(s) (if any) off the stack and returns from the function.

Executing the if instruction pops an i32 condition off the stack and either falls through to the next instruction or sets the program counter to after the else or end of the if.

Executing the else instruction of an if sets the program counter to after the corresponding endof the if.

Branches that exit a block or if may yield value(s) for that construct. Branches pop result value(s) off the stack which must be the same type as the declared type of the construct which they target. If a conditional or unconditional branch is taken, the values pushed onto the stack between the beginning of the construct and the branch are discarded, the result value(s) are pushed back onto the stack, and the program counter is updated to the end of the construct.

Branches that target a loop do not yield a value; they pop any values pushed onto the stack since the start of the loop and set the program counter to the start of the loop.

The drop operator can be used to explicitly pop a value from the stack.

The implicit popping associated with explicit branches makes compiling expression languages straightforward, even non-local control-flow transfer, requiring fewer drops.

Note that in the MVP, all control constructs and control instructions, including return are restricted to at most one value.

br_table

A br_table consists of a zero-based array of labels, a default label, and an index operand. A br_table jumps to the label indexed in the array or the default label if the index is out of bounds.

Calls

Each function has a signature, which consists of:

WebAssembly doesn’t support variable-length argument lists (aka varargs). Compilers targeting WebAssembly can instead support them through explicit accesses to linear memory.

In the MVP, the length of the return types sequence may only be 0 or 1. This restriction may be lifted in the future.

Direct calls to a function specify the callee by an index into the function index space.

A direct call to a function with a mismatched signature is a module verification error.

Indirect calls to a function indicate the callee with an i32 index into a table. The *expected*signature of the target function (specified by its index in the types section) is given as a second immediate.

Unlike call, which checks that the caller and callee signatures match statically as part of validation, call_indirect checks for signature match dynamically, comparing the caller’s expected signature with the callee function’s signature and and trapping if there is a mismatch. Since the callee may be in a different module which necessarily has a separate types section, and thus index space of types, the signature match must compare the underlying func_type. As noted above, table elements may also be host-environment-defined values in which case the meaning of a call (and how the signature is checked) is defined by the host-environment, much like calling an import.

In the MVP, the single call_indirect operator accesses the default table.

Multiple return value calls will be possible, though possibly not in the MVP. The details of multiple-return-value calls needs clarification. Calling a function that returns multiple values will likely have to be a statement that specifies multiple local variables to which to assign the corresponding return values.

Constants

These operators have an immediate operand of their associated type which is produced as their result value. All possible values of all types are supported (including NaN values of all possible bit patterns).

32-bit Integer operators

Integer operators are signed, unsigned, or sign-agnostic. Signed operators use two’s complement signed integer representation.

Signed and unsigned operators trap whenever the result cannot be represented in the result type. This includes division and remainder by zero, and signed division overflow (INT32_MIN / -1). Signed remainder with a non-zero denominator always returns the correct value, even when the corresponding division would trap. Sign-agnostic operators silently wrap overflowing results into the result type.

Shifts counts are wrapped to be less than the log-base-2 of the number of bits in the value to be shifted, as an unsigned quantity. For example, in a 32-bit shift, only the least 5 significant bits of the count affect the result. In a 64-bit shift, only the least 6 significant bits of the count affect the result.

Rotate counts are treated as unsigned. A count value greater than or equal to the number of bits in the value to be rotated yields the same result as if the count was wrapped to its least significant N bits, where N is 5 for an i32 value or 6 for an i64 value.

All comparison operators yield 32-bit integer results with 1 representing true and 0representing false.

64-bit integer operators

The same operators are available on 64-bit integers as the those available for 32-bit integers.

Floating point operators

Floating point arithmetic follows the IEEE 754-2008 standard, except that:

In the future, these limitations may be lifted, enabling full IEEE 754-2008 support :unicorn:.

Note that not all operators required by IEEE 754-2008 are provided directly. However, WebAssembly includes enough functionality to support reasonable library implementations of the remaining required operators.

When the result of any arithmetic operation other than neg, abs, or copysign is a NaN, the sign bit and the fraction field (which does not include the implicit leading digit of the significand) of the NaN are computed as follows:

32-bit floating point operations are as follows:

64-bit floating point operators:

min and max operators treat -0.0 as being effectively less than 0.0.

In floating point comparisons, the operands are unordered if either operand is NaN, and *ordered*otherwise.

Datatype conversions, truncations, reinterpretations, promotions, and demotions

Wrapping and extension of integer values always succeed. Promotion and demotion of floating point values always succeed. Demotion of floating point values uses round-to-nearest ties-to-even rounding, and may overflow to infinity or negative infinity as specified by IEEE 754-2008.

If the operand of promotion or demotion is a NaN, the result is a NaN with the following sign bit and fraction field (which does not include the implicit leading digit of the significand):

Reinterpretations always succeed.

Conversions from integer to floating point always succeed, and use round-to-nearest ties-to-even rounding.

Truncation from floating point to integer where IEEE 754-2008 would specify an invalid operator exception (e.g. when the floating point value is NaN or outside the range which rounds to an integer in range) traps.

Type-parametric operators

Unreachable

Validation

A module binary must be validated before it is compiled. Validation ensures that the module is well-defined and that its code cannot exhibit any undefined behavior. In particular, along with some runtime checks, this ensures that no program can access or corrupt memory it does not own.

Validation of code is mostly defined in terms of type-checking the use of the operand stack. It sequentially checks for each instruction that the expected operands can be popped from the stack and tracks which new operands are pushed onto it. At the start of a function the stack is empty; at its end it must match the return type of the function. In addition, instructions inside a block (or loop or if) cannot consume operands pushed outside. At the end of the block the remaining inner operands must match the block signature.

A special case is unconditional control transfers (br, br_table, return, unreachable), because execution never proceeds after them. The stack after such an instruction is unconstrained, and thus said to be polymorphic. The following instructions still must type-check, but conceptually, values of any type can be popped off a polymorphic stack for the sake of checking consecutive instructions. A polymorphic stack also matches any possible signature at the end of a block or function. After the end of a block, the stack is determined by the block signature and the stack before the block.

The details of validation are currently defined by the spec interpreter.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注