WebAssembly规范精读(第一篇)——简介

577 阅读7分钟

序言

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

你可以在此链接查阅完整的 WebAssembly 规范:webassembly.github.io/spec/core/i… 。

WebAssembly 是什么?

WebAssembly(缩写为 wasm)是一种安全的、可移植的低级代码格式,旨在实现高效的执行和紧凑的表示。它的主要目标是在可以 Web 上执行高性能的应用程序,但它没有做出任何特定于 Web 的假设或提供特定于 Web 的功能,因此它也可以在其他环境中使用。

设计目标

WebAssembly 的设计目标如下:

  • 快速、安全、可移植的语义:

    • 快速:以接近原生代码的性能执行,利用所有当代硬件共有的功能。
    • 安全:代码在内存安全的沙盒环境中进行验证和执行,防止数据损坏或安全漏洞。
    • 定义良好:完全且精确地定义了有效程序及其行为,使得我们能够以非正式和正式的方式轻易地对其进行推理。
    • 硬件无关:可以在所有现代架构、桌面或移动设备以及嵌入式系统上编译。
    • 语言无关:不受限于任何特定语言、编程模型或对象模型。
    • 平台无关:可以嵌入浏览器、作为独立 VM 运行或集成到其他环境中。
    • 开放:程序可以以简单和通用的方式与其环境进行互操作。
  • 高效且可移植的表示:

    • 紧凑:具有二进制格式,比一般的文本或本机代码格式更小,因此传输速度更快。
    • 模块化:程序可以分成较小的部分进行传输、缓存和单独使用。
    • 高效:可以快速单次解码、验证和编译,无论是即时编译(JIT)还是预编译(AOT)。
    • 可流式传输:允许在看到所有数据之前尽快开始解码、验证和编译。
    • 可并行化:允许将解码、验证和编译分成许多独立的并行任务。
    • 可移植:不做不受现代硬件广泛支持的架构假设。

安全考虑

WebAssembly 不提供对执行代码的计算环境的本机调用。与环境的任何交互,例如 I/O、对资源的访问或操作系统调用,只能通过调用嵌入器提供的函数并导入 WebAssembly 模块来执行。嵌入器可以通过控制或限制它提供的可用于导入的功能来建立适合相应环境的安全策略。这些考虑是嵌入器的责任,也是特定环境的 API 定义的主题。

WebAssembly 中的重要概念

值(Values)

WebAssembly 仅提供了四种基本数字类型。有32位和64位的整数和 IEEE 754数字。32位整数也用作布尔值和内存地址。整数类型没有有符号和无符号的区分。相反,整数是通过相应的操作被解释为二进制补码,来表示无符号或有符号。

除了这些基本数字类型外,还有一种128位的向量类型,表示不同类型的封装数据。支持的表示形式是4个32位或2个64位 IEEE 754数字,或不同宽度的整数值,具体而言是2个64位整数,4个32位整数,8个16位整数或16个8位整数。

值还可以由表示指向不同类型实体的指针的不透明引用组成。与其他类型不同,它们的大小或表示是不可观察的。

指令(Instructions)

WebAssembly 的计算模型基于栈虚拟机。代码由按顺序执行的指令序列组成。指令隐式处理栈上的操作值,分为两个主要类别。简单指令对数据执行基本操作。它们从栈中弹出参数并将结果推回到栈中。控制指令改变控制流。控制流是结构化的,这意味着它用嵌套结构(如块、循环和条件)来表达。程序只能使用这些结构进行分支。

陷阱(Traps)

在某些情况下,某些指令可能会产生陷阱,立即中止执行。陷阱不能由 WebAssembly 代码处理,会报告给外部环境,通常可以在其中捕获它们。

函数(functions)

代码被组织成单独的函数。每个函数都将一系列值作为参数,并返回一系列值作为结果。函数可以相互调用,包括递归调用,导致无法直接访问的隐式调用栈。函数还可以声明可用作虚拟寄存器的可变局部变量。

表(Tables)

表是特定元素类型的不透明值数组。它允许程序通过动态索引操作数间接选择这些值。目前,唯一可用的元素类型是无类型函数引用或对外部主机值的引用。因此,程序可以通过动态索引间接调用函数到表中。例如,这允许通过表索引来模拟函数指针。

线性内存(Linear Memory)

线性内存是一个连续的、可变的原始字节数组。这样的内存是以初始大小创建的,但可以动态增长。程序可以在任何字节地址(包括不对齐的地址)上加载和存储值到线性内存中。整数加载和存储可以指定一个比相应值类型的大小小的存储空间。如果访问不在当前内存空间的范围内,就会发生陷阱。

模块(Modules)

WebAssembly 二进制文件采用模块的形式,其中包含函数、表和线性内存的定义,以及可变或不可变的全局变量。还可以导入定义,指定模块/名称对和合适的类型。每个定义都可以选择以一个或多个名称导出。除了定义之外,模块还可以为其存储器或表定义初始化数据,这些数据采用复制到给定偏移量的段的形式。它们还可以定义自动执行的启动函数。

嵌入器(Embedder)

一个 WebAssembly 实现通常会被嵌入到一个宿主环境中。这个环境定义了如何启动模块的加载,如何提供导入(包括宿主端定义),以及如何访问导出。

语义阶段

解码(Decoding)

WebAssembly 模块以二进制格式分发。解码过程将该格式转换为模块的内部表示形式。在本规范中,这种表示形式是通过抽象语法建模的,但实际实现可以直接编译成机器代码。

校验(Validation)

解码后的模块必须是有效的。通过多种检查,以确保模块是有效和安全的。特别是,对函数和它们内部的指令序列进行类型检查。

执行(Execution)

最终,一个有效的模块可以被执行。执行可以进一步分为两个阶段:

实例化。模块实例是模块的动态表示,包括其自身状态和执行堆栈。实例化阶段执行模块本身的主体,获得所有导入的定义。它初始化全局变量、内存和表,并调用模块的启动函数(如果定义了)。它返回模块导出的实例。

调用。一旦实例化,可以通过在模块实例上调用导出函数来启动进一步的 WebAssembly 计算。给定所需的参数,执行相应的函数并返回其结果。

实例化和调用是在嵌入环境中的操作。