WebAssembly 模块

WebAssembly 字节码从前导开始,前导指明这个模块是一个 WebAssembly 模块,前导之后是段。WebAssembly 有两种类型的段:

  • 已知段
  • 自定义段

这些段会在 WebAssembly 模块初始化时验证。

已知段有专门的用途和定义,自定义段可以放在已知段的前面、后面或者之间。已知段和自定义段都是可选的,而且已知段最多只能出现一次,从技术上来说,可以创建一个没有任何段的空模块。

图1

结构描述

已知段

类型段(Type(ID=1))

该段列出 WebAssembly 模块用到的所有函数类型(或函数签名、函数原型)。图1中列举了三个函数签名:

  • 第一个有两个32位整型(i32)参数,一个32位整型(i32)返回值;
  • 第二个有两个64位整型(i64)参数,没有返回值;
  • 第三个不接受任何参数,也没有返回值。

导入段和导出段(Import(ID=2) 和 Export(ID=7))

这两个段分别列出模块所有的导入项和导出项,多个模块可以通过导入和导出项链接在一起。

函数段和代码段(Function(ID=3) 和 Code(ID=10))

内部函数信息分别被分开存储在两个段中,其中函数段实际上是一个索引表,列出内部函数所对应的签名索引。代码段存储内部函数的局部变量信息和字节码。函数和代码段中的项目数量必须一致,且一一对应。

表段和元素段(Table(ID=4) 和 Element(ID=9))

表段列出模块内定义的所有表,元素段列出表初始化数据。WebAssembly规范规定模块最多只能导入或定义一张表,所以即使模块有表段,里面也只能有一个项目。表主要和间接函数调用有关。

内存段和数据段(Memory(ID=5) 和 Data(ID=11))

内存段列出模块内定义的所有内存,数据段列出内存初始化数据。WebAssembly 规范规定模块最多只能导入或定义一块内存,所以即使模块有内存段,里面也是能有一个项目。

全局段(Global(ID=6))

该段列出模块内定义的所有全局变量信息, 包括值类型、可变性和初始值。

起始段(Start(ID=8))

该段给出模块的起始函数索引。和其它段有所不同,起始段只能有一个项目。起始函数主要起到两个作用,一个是在模块加载后进行一些初始化工作;另一个是把模块编程可执行程序。如果模块有起始段,那么 WebAssembly 实现在加载模块后会自动执行起始函数(一般是有 main 函数的到代码被编译后就会有起始段,里面放着 main 函数的索引)

内存段

内存段是 WebAssembly 核心安全性的一部分,因为 WebAssembly 不能直接访问设备内存。模块实例化时外部环境传入一个ArrayBuffer, 模块实例会以这个 ArrayBuffer 实例当作线性内存使用。这个线性内存就像 C++ 的堆,并且每次请求访问内存时,框架都会验证请求是否在这个 ArrayBuffer 的范围内。

模块的内存被定义为 WebAssembly 页, 每页 64KB。外部可以指定模块内存的初始页数和最大页数。如果指定了最大页数,那么框架会防止内存增长超过这个点,如果没有,则内存可以无限增长。

多个 WebAssembly 模块可以共享同一个线性内存,动态连接模块时比较有用。

C++ 中, 执行栈和线性内存位于同一内存中。尽管C++代码不应该修改执行栈,但可以使用指针进行该操作。WebAssembly 的安全性不仅在于代码不能访问设备内存,而且它还分隔了执行栈和线性内存。

自定义段(ID=0)

自定义段可以出现在模块的任意位置,任意多次,多个自定义段可以复用同一个名称。自定义段是给编译器等工具使用的,里面可以存放函数名等调试信息,或者其他任何附加信息。自定义段不参与 WebAssembly 语义,所以即便是完全忽略自定义段也不影响模块的执行。自定义段即使布局错误,也不会触发验证错误。框架可以惰性加载自定义段。

结构关系说明

除了自定义段,其它段必须按ID递增的顺序出现。这是因为很多段之间存在信息的依赖关系,例如导入段、函数段、代码段都需要知道函数类型信息,所以类型段必须在这个三个段之前出现; 由于导入的函数、表、内存、全局变量在各自索引空空间的最前面,所以导入段必须在这4个段之前出现;由于导出函数、表、内存、全局变量时需要知道其索引,所以导出段必须在这4个段之后出现。

WebAssembly 二进制格式的设计原则之一是可以一遍完成模块的解析、验证、和编译(AOT 或 JIT)。WebAssembly 可以在下载模块同时进行解码、验证和编译。可流式处理是 WebAssembly 特点之一,二进制模块中各个段的排列方式一定程度上是为了满足这一点。另外之所以要把函数的签名信息和其他信息分别存放在这两个段里,也是为了满足这一特点。

类型段、函数段和代码段配合示例

类型段、函数段和代码段配合示例

图2

调用表段中某个条目的示例

调用表段中某个条目的示例

图3

表段会被给定一个初始长度, 还有最大长度限制。对于表来说,长度就是表中元素的个数。可以通过指定元素的个数来请求增加表的长度。如果指定了元素的最大个数,系统就会阻止表表增长超过最大个数。但如果没有指定最大长度,那么这个表就可以无限增长。

posted on 2022-05-17 23:02  y1j2x34  阅读(98)  评论(0编辑  收藏  举报