结合xxd命令和WABT工具提供的wasm-objdump命令分析,深入细节观察Wasm二进制格式。
魔数和版本号
Wasm二进制格式的魔数占4个字节,内容\0asm。Wasm二进制格式的版本号也占4个字节,当前版本是1,可以把魔数和版本号定义为常量,代码如下:
const (
MagicNumber = 0x6D736100 // \0asm
Version = 0x00000001
)
前8个字节是魔数.asm和版本号1。
注意,由于Wasm二进制格式采用小端(Little-Endian)方式编码数值,所以魔数\0asm和版本号1都是倒着排列的。
每一个段都以1字节的段ID开始,除了自定义段其他段的结构都完全定义好了,由于段的内容可能是未知的,所以段ID后面存储了段的实际内容字节数,Wasm可以根据字节数跳过不认识的自定义段,自定义段的内容字节数并非解码必须的,使用一种类似正则表达式的简单语法来描述Wasm二进制格式,下面给出段的同一编码格式。
sec: id | byte_count | byte+
byte_count : u32 #LEB编码的32位无符号整数
“|”仅在描述时起分隔作用,在实际二进制格式中不存在,byte后面的“+”号表示出现至少一次,“*”表示出现任意次,“?”表示出现0次或1次,为了让二进制格式尽可能紧凑,二进制模块中是按LEB128格式编码后存储的。
以项目数量开头后面跟相应数量项目的结构,我们把这种结构叫作向量用vec
type_sec: 0x01 | byte_count | type_count | func_type+
type_sec: 0x01 | byte_count | vec
类型段
红色段表示段ID为1占用了25(0x19)个字节,共有5条类型数据
黄色段表示0x60为函数类型开头,有1个参数,类型为7F(i32),无返回值
蓝色段表示函数类型、无参数、无返回值
绿色段表示函数类型、有2个参数、参数类型1为7F(i32)、参数类型2为7F(i32)有一个返回值、返回值类型为7F(i32),后面几个字节以此类推。
导入段
可导入4种类型的成员以供其他模块使用,分别是:函数、表、内存、全局变量,导入和导出项通过名字链接,这些名字也被称为符号(Symbol)
Wasm在导入描述前安排了一个单字节tag 用于区分:0表示函数、1表示表、2表示内存、3表示全局变量
绿色表示ID为2占用了18(0x12)个字节,只有一条导入数据
红色表示导入的成员名为“env.print_char”,函数tag(函数标识)为0,函数签名的索引为0
函数段
蓝色部分表示ID为3占用了12(0x0C)个字节,一共存储了11(0x0B)个函数类型索引
后面表示每个函数对应的函数类型索引下标。
表段
Wasm规范规定最多只能定义一张表,且元素类型必须为函数引用(编码为0x70),表类型还需要指定元素数量的限制,其中必须指定下限,上限则可选。限制编码后以1字节tag开头。如果tag是0,表示只指定下限。否则tag必须为1,表示即指定下限,又指定上限。
蓝色表示ID为4占用了5个字节,记录了1个表类型
红色表示为元素类型为函数引用(编码为:0x70),限制的tag为1,上限为1,下限为1
内存段
和表一样Wasm规范规定模块最多只能定义一块内存,内存类型只须指定内存页数限制。
红色表示ID为5占用3个字节
蓝色表示限制的tag是0,内存页数的下限是17(0x11),没有指定上限。
全局段
列出模块内定义的所有全局变量,全局变量需要指定全局变量的类型(包括值类型和可变性)以及初始值
红色表示ID为6占用25(0x19)个字节,共记录了3个全局变量
蓝色表示第一个全局变量类型为i32(0x7F),第二个0表示不可变,1表示可变
蓝色表示初始值由常量指令i32.const(操作码0x41,立即数0x100000)给出,以0x0B结尾
导出段
导出段列出模块所有导出成员,只有被导出的成员才能被外界访问,其他成员被很好的封装在模块内部,和导入段一样,导出段也可以包含4种导出项:函数、表、内存、全局变量。
导出项要更简单一些,一只要指定成员名即可不需要指定模块名,模块名并没有写在Wasm二进制格式里,而是在链接时由链接器指定,第二导出项只要指定成员索引即可,不需要指定具体类型。
Wasm在导出描述前安排了一个单字节tag 用于区分:0表示函数、1表示表、2表示内存、3表示全局变量
红色表示ID为7,占用44(0x2C)个字节,共有4个导出项
黄色表示06表示名称有6个字节第一个导出项名称是memory,
蓝色描述的是tag为2
绿色表述的是类型索引为0
起始段
起始段值需要记录一个起始函数索引。
元素段
元素段存放表初始化数据,每个元素项包含3部分信息:表索引(初始化哪张表)、表内编译量(从哪里开始初始化),目前模块最多只能导入或定义一张表,所以表索引暂时只起占位作用,值必须为0。
代码段
这是Wasm二进制模块的核心,除了字节码,方法的局部变量也在代码段中,是压缩后存储的,连续多个相同类型的局部变量会被分为一组,统一记录变量数量和类型。
代码段有个特殊之处:每个代码项都以该项所占字节数开头,目的是方便Wasm实现并行处理(比如验证、分析、编译等)函数字节码。
绿色表示ID为10(0x0A)
蓝色表示内容占据1110个字节(0xD6、0x08)因为超过了127所以LEB128编码后占据了2个字节
红色表示一共有一共有11个代码项(0x0B)
红色表示代码项内容占据234(0xEA、0x01)个字节,
黄色表示有一个局部变量组
蓝色和绿色表示共计22(0x16)个i32类型(0x7F)的局部变量
数据段
数据段和元素段相似:
1、元素段存放表初始化数据,数据段则存放内存初始化数据。
2、数据项也包含三部分信息:内存索引(初始化哪块内存)、内存偏移量(从哪里开始初始化)、初始数据
3、目前模块最多只能导入或定义一个内存、所以内存索引暂时也只起占位符作用必须为0
4、内存偏移量也是由表达式指定的
绿色表示ID11(0x0B)占用23(0x17)个字节,只有1个数据项
红色表示数据项内存索引为0
黄色表示由常量指令i32.const(操作码0x41,立即数0x100000)给出,以0x0B结尾
红色表示共14个字节
蓝色表示Hello, World!字符的16进制
自定义段
用于存放自定义数据,和其他段相比自定义段主要有亮点不同
一、也是最重要的一点,自定义段不参与模块语义,自定义段存放的都是额外信息(比如函数名和局部变量名等调试信息或第三方扩展信息),即使完全忽略这些信息也不影响模块的执行。
二、自定义段可以出现在任何一个非自定义段前后,而且出现的次数不受限制。
Wasm规范要求自定义段的内容必须以一个字符串为开头这个字符串作为自定义段的名称起到标识作用。Wasm中定义了一个标准的自定义段,名字是name,专门用来存放模块名、内部函数名和局部变量名。
蓝色表示ID为0 占用892(0xFC、0x06)个字节
绿色表示自定义名称长4个字节
红色表示name字符的16进制码