课程的 WebAssembly 工作原理一章已经详细介绍了基于栈结构的指令执行原理和过程,然而,了解 WebAssembly 原理最有效的方式仍然是自己动手实现一个执行引擎;本文将从零开始实现一个简单的 WebAssembly 解释器 WAInterp (WebAssembly Interpreter),通过这一过程来进一步掌握和实践 WebAssembly 虚拟机技术。
本文将介绍最基本的虚拟机实现技术,高级优化虚拟机相关技术请阅读参考文献 [2~5] 中列出的相关专业书籍和文档。
从语义上讲,一个 WebAssembly 模块的生命周期可以分为"编译"、"解码"、"验证"、"实例化"、"指令执行" 几个阶段,而各个阶段分别创建和使用了 WebAssembly 模块的不同变体:
WebAssembly 模块生命周期流程如下图 1 所示。
图 1. WebAssembly 模块生命周期流程图
本文将从零开始实现一个简单的基于 IR (Intermediate Representation) 树遍历的 WebAssembly 二进制解释器 "WAInterp";该解析器的执行主要包括二进制模块的"解码"、"实例化"、"执行"等几个阶段。接下来各个小节将逐一介绍每个阶段的详细设计和实现,以便读者可以循序渐进地构建 WAInterp 解析器。由于本文的主要目的是更好的理解 WebAssembly 底层原理、实践 WebAssembly 虚拟机相关技术,因此,我们采用相对简单和便于发布、共享的 Typescript 作为解析器的开发语言,并以 NPM 包的形式发布至 NPM Registery[6],以便开发者能够方便的集成。
WAInterp 解释器的输入是 WebAssembly 二进制格式文件,实现二进制模块的加载和解码是模块实例化和指令执行的前提和必要条件;因此,本节先简要回顾下 WebAssembly 二进制格式相关内容,然后,实现一个二进制文件解码器;从而完成 WebAssembly 文件加载并转换为运行环境中对应的数据结构。
与其他很多二进制格式 (比如 Java 类文件、ELF二进制文件) 类似,WebAssembly 二进制文件也是以魔数 (Magic Number )和版本号开头。在魔数和版本号之后是模块的主体内容,这些内容以段 (Section) 的形式进行组织。WebAssembly MVP 规范一共定义了12种段,并给每种段分配了 ID (从0到11),除了自定义段以外,其他所有的段都最多只能出现一次,且必须按照段 ID 递增的顺序出现 (本文中暂不实现自定义段相关功能)。WebAssembly 模块结构如下图 2 所示。
图 2. WebAssembly 模块文件结构
根据 WebAssembly 模块的规范和格式定义 (如上图2所示),我们为二进制模块定义如下的模块数据类型。
type BinaryModule = {
Magic : uint32,
Version : uint32,
TypeSec : Array,
ImportSec : Array,
FuncSec : Array,
TableSec : Array ,
MemSec : Array,
GlobalSec : Array,
ExportSec : Array,
StartFunc : Funcidx,
ElemSec : Array,
CodeSec : Array>,
DataSec : Array
}
在详细描述 BinaryModule 各段 (Segment) 的表示和解码之前,我们先为 WebAssembly 内置的值类型定义对应的数据结构。WebAssembly 规范中定义了 4 种基本的值类型:32 位整数 (简称 i32)、64 位整数 (简称 i64)、32 位浮点数 (简称 f32) 和 64 位浮点数 (简称 f64),与高级语言中的整数类型有所不同,WebAssembly 底层的整数类型是不区分符号的。高级语言所支持的一切类型 (比如布 尔值、数值、指针、数组、结构体等),都必须由编译器翻译成这 4 种基本类型或者组合。按照二进制规范,我们定义如下的 valtypes 来表示模块中定义的基本类型编码。
const valtypes = {
0x7f: "i32",
0x7e: "i64",
0x7d: "f32",
0x7c: "f64",
0x7b: "v128",
};
在 BinaryModule 中的大部分段都包含结构化的项目,运行环境中这些结构化项目的容器被称呼为 "索引空间"。函数签名、函数、表、内存、全局变量等类型在模块内有各自的索引空间;局部变量和跳转标签在函数内有各自的索引空间。模块的各类索引空间及其作用可以总结如下:
- 类型索引空间: 不管是外部导入的函数还是内部定义的函数,其签名全都存储在类型段中,因此类型段的有效索引范围就是类型索引空间。
- 函数索引空间: 函数索引空间由外部函数和内部函数共同构成;当调用某函数时,需要给定该函数的索引。
- 全局变量索引空间: 和函数一样,全局变量索引空间也是由外部全局变量和内部全局变量共同构成的;当读写某全局变量时,需要给定该全局变量的索引。
- 表和内存索引: 表和内存也可以从外部导入,所以索引空间的情况和函数索引空间类似;由于 WebAssembly MVP 规范的限制,最多只能导入或定义一个表和内存,所以索引空间内的唯一有效索引只能是 0。
- 局部变量索引: 函数的局部变量索引空间由函数的参数和局部变量构成;当读写某参数或局部变量时,需要给定该参数或局部变量的索引。
- 跳转标签索引: 和局部变量索引一样,每个函数有自己的跳转标签索引空间;结构化控制指令和跳转指令需要指定跳转标签索引。
为了提高代码的可读性,我们给这些索引分别定义了类型别名,如下代码所示。
type U32Literal = NumberLiteral;
type Typeidx = U32Literal;
type Funcidx = U32Literal;
type Tableidx = U32Literal;
type Memidx = U32Literal;
type Globalidx = U32Literal;
type Localidx = U32Literal;
type Labelidx = U32Literal;
type Index =
| Typeidx
| Funcidx
| Tableidx
| Memidx
| Globalidx
| Localidx
| Labelidx
3.2 WAInterp 段解码
模块解码是将 WebAssembly 二进制文件中各个段的编码数据解析为运行时环境中的数据结构的过程。为了便于理解,我们对 WebAssembly 二进制模块的的形式化表示[7] 做了简化,其中,id 表示各个段的识别号,byte_count 表示对应段的字节数,vec表示元素类型为 T 的向量,leb(T) 用于表示对应类型 T 的 LEB128 编码值,竖线 "|" 作为各个语义域的分隔符;采用简化的形式化描述,WebAssembly 中二进制格式中段编码可以见如下的描述。
sec: id | byte_count | vec
byte_count: leb(u32) #LEB128编码的32位无符号整数
上面我们已经简要地介绍了 WebAssembly 的内置类型及段结构描述,接下来,我们将逐一实现各个段的二进制数据编码,将它们转换为运行环境中的对象"内存格式"数据结构。
类型段 (ID = 1)
类型段记录了模块中使用到的所有函数类型,函数类型包括参数数量和类型,以及返回值数量和类型。在 WebAssembly 二进制格式里,函数类型以 0x60 开头,后跟参数数量、参数类型、返回值数量和返回值类型,函数类型的二进制编码格式如下所示。
type_section : id | byte_count | vector
func_type : 0x60 | vec | vec
针对类型段及函数类型编码,我们定义如下的数据结构;其中 FuncSig 表示函数类型签名,其中 params 描述函数的参数数量和类型,result 描述返回值数量和类型。
const typesInModule: Array
type TypeInstruction = {
type: "TypeInstruction";
id: Index;
functype: FuncSig;
};
type FuncSig = {
params: Array,
result: Array,
};
type FuncParam = {
id ?: string,
valtype: Valtype,
};
基于类型段和函数类型的定义,WAInterp 定义 parseTypeSection 函数实现类型段的二进制解析;其中,typesInModule 为模块中函数类型集合。
function parseTypeSection(numberOfTypes: number) {
const typesInModule: Array = [];
for (let i = 0; i < numberOfTypes; i++) {
// 0x60
const type = readByte();
skipBytes(1);
if (type == constants.types.func) {
const paramValtypes: Array = parseVec(
(b) => constants.valtypes[b]
);
const params = paramValtypes.map((v) =>
t.funcParam(/*Valtype*/ v, undefined)
);
const result: Array = parseVec((b) => constants.valtypes[b]);
typesInModule.push(
t.typeInstruction(undefined, t.signature(params, result)),
);
} else {
throw new Error("Unsupported type: " + toHex(type));
}
}
return typesInModule;
}
导入段 (ID = 2)
一个模块可以导入函数、表、内存、全局变量 4 种类型的外部对象;这些导入对象通过模块名、成员名,以及具体描述信息在模块的导入段进行声明,导入段的二进制编码格式如下所示。
import_sec : 0x02 | byte_count | vec
import : module_name | member_name | import_desc
import_desc: tag | [type_idx, table_type, mem_type, global_type]
针对导入段及导入项的结构,我们定义如下的数据结构;其中 ModuleImport 为导入项,module 和 name 分别表示导入的模块名和符号名;ImportDescr 用于表示实际导入的函数、表、内存、 全局变量的详细信息。为了表示 4 种不同的导入项, ImportDescr 数据类型为 GlobalType,Table,Memory,FuncImportDescr 的组合类型;其中 GlobalType 用于描述全局变量类型信息;Table 用于描述表类型信息,元素类型可以为 funcref 和 externref;Memory 用于描述内存类型信息;FuncImportDescr 用户描述导入的外部函数签名信息,如下代码所示。
const importsInModule: Array
type ModuleImport = {
type: "ModuleImport";
module: string;
name: string;
descr: ImportDescr;
};
type ImportDescr = GlobalType | Table | Memory | FuncImportDescr;
type GlobalType = {
type: "GlobalType";
valtype: Valtype;
mutability: Mutability;
};
type Table = {
type: "Table";
elementType: TableElementType;
limits: Limit;
name?: Identifier;
elements?: Array;
};
type TableElementType = "funcref" | "externref"
type Memory = {
type: "Memory";
limits: Limit;
id?: Index;
};
type FuncImportDescr = {
type: "FuncImportDescr";
id: Identifier;
signature: Signature;
};
基于导入段二进制格式和数据结构定义,WAInterp 定义 parseImportSection 函数实现导入段的二进制解析,其中, importsInModule 为模块中的导入项集合。
function parseImportSection(numberOfImports: number) {
const importsInModule: Array = [];
for (let i = 0; i < numberOfImports; i++) {
const moduleName = readUTF8String();
skipBytes(moduleName.nextIndex);
const name = readUTF8String();
skipBytes(name.nextIndex);
const descrTypeTag = readByte(); // import kind
skipBytes(1);
let importDescr : ImportDescr;
const descrType = constants.importTypes[descrTypeTag];
if (descrType === "func") {
const indexU32 = readU32();
const typeindex = indexU32.value;
skipBytes(indexU32.nextIndex);
const signature = state.typesInModule[typeindex];
const id = getUniqueName("func");
importDescr = t.funcImportDescr(
t.identifier(id),
t.signature(signature.params, signature.result),
true
);
} else if (descrType === "global") {
importDescr = parseGlobalType();
} else if (descrType === "table") {
importDescr = parseTableType(i);
} else if (descrType === "memory") {
const importDescr = parseMemoryType(0);
} else {
throw new CompileError("Unsupported import of type: " + descrType);
}
importsInModule.push(
t.moduleImport(moduleName.value, name.value, importDescr),
);
}
return importsInModule;
}
函数段 (ID = 3)
函数段相对比较简单,它列出了内部函数的签名在类型段中的索引,函数段的编码格式如下所示。
func_sec: 0x03 | byte_count | vec
函数段实际声明了模块内部定义的函数类型,我们定义如下的数据结构来表示模块中的函数类型。
type FuncType = {
id: Identifier,
signature: Signature,
isExternal: boolean,
};
type FuncSig = {
params: Array,
result: Array,
};
基于函数段的二进制格式及数据结构,WAInterp 定义 parseFuncSection 函数实现导入段的二进制解析;其中, functionsInModule 为模块中函数集合。
function parseFuncSection(numberOfFunctions: number) {
let functionsInModule: Array = [];
for (let i = 0; i < numberOfFunctions; i++) {
const indexU32 = readU32(); // type index
const typeindex = indexU32.value;
skipBytes(indexU32.nextIndex);
const signature = state.typesInModule[typeindex];
const id = t.withRaw(t.identifier(getUniqueName("func")), "") as Identifier;
functionsInModule.push({
id,
signature,
isExternal: false,
});
return functionsInModule;
}
}
Table段 (ID = 4)
WebAssembly MVP 规定了一个模块最多只能定义一张表,且元素类型必须为函数引用。除了元素类型,表类型还需要指定元素数量的限制,包括元素数量的最小值和最大值;由于元素数量的最大值是可选域,因此二进制编码中通过 tag 域来识别,如果 tag 是0,表示只指定下限;否则,tag 必须为1,表示既指定下限,又指定上限。Table 段的二进制编码格式如下所示。
table_sec : 0x04 | byte_count | vec # MVP vec长度只能是1
table_type: 0x70 | limits
limits : tag | min | max?
针对 Table 段的编码,我们定义如下的数据结构,其中 TableElementType 描述表中元素类型,当前只能为 funcref;Limit 用于描述表元素大小的限制。
const tablesInModule: Array
type Table = {
type: "Table";
elementType: TableElementType;
limits: Limit;
name?: Identifier;
elements?: Array;
};
type TableElementType = "funcref" | "externref"
type Limit = {
type: "Limit";
min: number;
max?: number;
};
基于表段的二进制格式和数据结构定义,WAInterp 定义 parseTableSection 函数实现表段的二进制解析;其中,tablesInModule 为模块中表的集合。
function parseTableSection(numberOfElements: number) {
const tablesInModule: Array = [];
for (let i = 0; i < numberOfElements; i++) {
const tablesDescr: Table = parseTableType(i);
tablesInModule.push(tablesDescr);
}
return tablesInModule;
}
内存段 (ID = 5)
和 Table 一样,WebAssembly MVP 规定一个模块最多只能定义一块内存;与 Table 不同的是内存类型只须指定内存页数限制,而不需要指定类型。内存段和内存类型的二进制编码格式如下所示。
mem_sec : 0x05 | byte_count | vec # MVP vec长度只能是1
mem_type: limits
针对内存段和内存类型编码,我们定义如下的数据结构,其中 Limits 用于描述指定内存页数限制。
const memoriesInModule: Array
type Memory = {
type: "Memory";
limits: Limit;
id?: Index;
};
type Limit = {
type: "Limit";
min: number;
max?: number;
};
基于内存段的二进制格式和数据结构定义,WAInterp 定义 parseTableSection 函数实现内存段的二进制解析;其中,memoriesInModule 为模块中的内存集合。
function parseMemorySection(numberOfElements: number) {
const memoriesInModule: Array = [];
for (let i = 0; i < numberOfElements; i++) {
const memoryDescr: Memory = parseMemoryType(i);
state.memoriesInModule.push(memoryDescr);
memories.push(memoryDescr);
}
return memoriesInModule;
}
全局段 (ID = 6)
全局段列出模块内定义的所有全局变量,全局项需要指定全局变量类型和初始值;其中,全局变量类型需要描述全局变量的类型以及可变性。全局段和全局变量的二进制编码格式如下所示。
global_sec : 0x06 | byte_count | vec
global : global_type | init_expr
global_type: val_type | mut
init_expr : vec | 0x0B
针对全局段和全局变量类型编码,我们定义如下的数据结构,其中 GlobalType 描述全局变量类型及其可变性;init 是初始化表达式,用指令序列 Array 来表示。
const globalsInModule: Array;
type Global = {
type: "Global";
globalType: GlobalType;
init: Array;
name?: Identifier;
};
type GlobalType = {
type: "GlobalType";
valtype: Valtype;
mutability: Mutability;
};
type Valtype = "i32" | "i64" | "f32" | "f64";
type Mutability = "const" | "var";
基于全局段的二进制格式和数据结构定义,WAInterp 定义 parseTableSection 函数实现全局段的二进制解析;其中,globalsInModule 为模块中全局变量集合。
function parseGlobalSection(numberOfGlobals: number) {
const globalsInModule: Array = [];
for (let i = 0; i < numberOfGlobals; i++) {
const globalType = parseGlobalType();
const init: Array = [];
parseInstructionBlock(init);
const globalDescr: Global = t.global(globalType, init, undefined);
globalsInModule.push(globalDescr);
}
return globalsInModule;
}
导出段 (ID = 7)
导出段列出模块所有导出成员,只有被导出的成员才能被外界访问,其他成员被很好地“封装”在模块内部。与模块导入类似,一个模块也可以导出 "函数"、"表"、"内存"、 "全局变量" 4 种类型的成员;导出项除了指定导出的符号名,还需要指定实际导出的元素内容,而元素的内容仅指定对应索引空间中成员索引即可,因为通过索引即可从对应的索引空间中获取需要的数据。
导出段及其导出项二进制编码格式如下所示。
export_sec : 0x07 | byte_count | vec
export : name | export_desc
export_desc: tag | [func_idx, table_idx, mem_idx, global_idx]
针对导出段编码,我们定义如下的数据结构,其中 name 为导出的符号名,ModuleExportDescr 用于描述导出的类型以及导出项在各自所以空间中的索引值。
const exportsInModule: Array
type ModuleExport = {
type: "ModuleExport";
name: string;
descr: ModuleExportDescr;
};
type ModuleExportDescr = {
type: "ModuleExportDescr";
exportType: ExportDescrType;
id: Index;
};
type ExportDescrType = "Func" | "Table" | "Memory" | "Global";
基于导出段的二进制格式和数据结构定义,WAInterp 定义 parseTableSection 函数实现全局段的二进制解析;其中,exportsInModule 为导出对象集合。
function parseExportSection(numberOfExport: number) {
const exportsInModule: Array;
for (let i = 0; i < numberOfExport; i++) {
const name = readUTF8String(); // export name
skipBytes(name.nextIndex);
const typeIndex = readByte(); // export kind
skipBytes(1);
const indexu32 = readU32(); // export index
const index = indexu32.value;
skipBytes(indexu32.nextIndex);
let id: Index;
let signature = undefined;
if (constants.exportTypes[typeIndex] === "Func") {
const func = state.functionsInModule[index];
id = t.numberLiteralFromRaw(index, String(index) as any) as NumberLiteral;
signature = func.signature;
} else if (constants.exportTypes[typeIndex] === "Table") {
const table = state.tablesInModule[index];
id = t.numberLiteralFromRaw(index, String(index) as any) as NumberLiteral;
} else if (constants.exportTypes[typeIndex] === "Memory") {
const memory = state.memoriesInModule[index];
id = t.numberLiteralFromRaw(index, String(index) as any) as NumberLiteral;
} else if (constants.exportTypes[typeIndex] === "Global") {
const global = state.globalsInModule[index];
id = t.numberLiteralFromRaw(index, String(index) as any) as NumberLiteral;
} else {
console.warn("Unsupported export type: " + toHex(typeIndex));
return;
}
exportsInModule.push(
t.moduleExport(
name.value,
t.moduleExportDescr(
constants.exportTypes[typeIndex],
t.numberLiteral(typeIndex, String(typeIndex)
)
)
);
}
}
起始段 (ID = 8)
起始段只需要记录一个起始函数索引,该函数是一个可选的模块入口函数,类似于高级编程语言中的 "main" 函数;起始段二进制编码格式如下所示。
start_sec: 0x08 | func_idx
针对起始段编码,我们定义如下的数据结构,其中 index 为函数名字空间的索引值。
type Start = {
type: "Start";
index: Index;
};
基于起始段的二进制格式和数据结构定义,WAInterp 定义 parseTableSection 函数实现全局段的二进制解析。
function parseStartSection() : Start {
const u32 = readU32(); // start func index
const startFuncIndex = u32.value;
skipBytes(u32.nextIndex);
return t.start(t.indexLiteral(startFuncIndex));
}
export function start(index: Index): Start {
const node: Start = {
type: "Start",
index,
};
return node;
}
元素段 (ID = 9)
元素段存放 Table 的初始化数据,每个元素项由表索引、表内偏移量、函数索引列表三部分组成;其中,表索引用于指定初始化哪张表,表内偏移量用于指定初始元素填充的起始偏移,函数索引列表指定了用于初始化 Table 的函数索引值。和全局变量初始值类似,表内偏移量也用初始化表达式指定;元素段二进制编码格式如下所示。
elem_sec : 0x09 | byte_count | vec
elem : table_idx | offset_expr | vec
offset_expr: vec | 0x0B
针对元素段编码格式,我们定义如下的数据结构;其中,offset 为初始化表达式描述的表初始化偏移量,funcs 为函数索引列表。
const elemsInModule: Array;
type Elem = {
type: "Elem";
table: Index; // table index
offset: Array; // offset in table
funcs: Array; // func indice
};
基于元素段的二进制格式和数据结构定义,WAInterp 定义 parseElemSection 函数实现元素段的二进制解析。
function parseElemSection(numberOfElements: number) {
const elemsInTable: Array = [];
for (let i = 0; i < numberOfElements; i++) {
const tableindexu32 = readU32(); // table index
const tableindex = tableindexu32.value;
skipBytes(tableindexu32.nextIndex);
const instr: Array = [];
parseInstructionBlock(instr); // offset expression
const indicesu32 = readU32(); // func index number
const indices = indicesu32.value;
skipBytes(indicesu32.nextIndex);
const indexValues : Arrany = [];
for (let i = 0; i < indices; i++) {
const indexu32 = readU32();
const index = indexu32.value;
skipBytes(indexu32.nextIndex);
indexValues.push(t.indexLiteral(index));
}
const elemNode = t.elem(t.indexLiteral(tableindex), instr, indexValues);
elemsInTable.push(elemNode);
}
return elemsInTable;
}
代码段 (ID = 10)
代码段描述了函数的局部变量信息和函数的指令序列,而函数的类型和索引信息在类型段和函数段中定义。和其它段相比,代码段中每个项都以所占字节数开头,虽然有所冗余,但方便 WebAssembly 实现验证、分析、编译的并行处理;此外,为了节约空间,连续多个相同类型的局部变量会被分为一组,统一记录变量数量和类型,从而实现局部变量信息的压缩存储;代码段二进制编码格式如下所示。
code_sec: 0x0A | byte_count | vec
code : byte_count | vec | expr
locals : local_count | val_type
针对代码段的编码格式,我们定义如下的数据结构,其中 code 为函数体的指令序列,locals 为局部变量列表。
const funcBodiesInModule : Array;
type FuncBody = {
code: Array,
locals: Array,
bodySize: number,
}
基于代码段的二进制格式和数据结构定义,WAInterp 定义 parseCodeSection 函数实现元素段的二进制解析,其中 funcBodiesInModule 为模块中的函数体集合。
function parseCodeSection(numberOfFuncs: number) {
for (let i = 0; i < numberOfFuncs; i++) { // Parse vector of function
const bodySizeU32 = readU32(); // size of the function code in bytes
skipBytes(bodySizeU32.nextIndex);
const code: Array = [];
const funcLocalNumU32 = readU32(); // local group count
const funcLocalNum = funcLocalNumU32.value;
skipBytes(funcLocalNumU32.nextIndex);
const locals: Array = [];
const localsTypes: Array = [];
for (let i = 0; i < funcLocalNum; i++) {
const localCountU32 = readU32(); // local count
const localCount = localCountU32.value;
skipBytes(localCountU32.nextIndex);
const valtypeByte = readByte(); // local value type
skipBytes(1);
const type = constants.valtypes[valtypeByte];
const args: Array = [];
for (let i = 0; i < localCount; i++) {
args.push(t.valtypeLiteral(type));
localsTypes.push(type);
}
const localNode: Instr = t.instruction("local", args) as Instr;
locals.push(localNode);
}
code.push(...locals);
parseInstructionBlock(code); // decode instrs until the "end"
funcBodiesInModule.push({
code,
locals: localsTypes,
bodySize: bodySizeU32.value,
});
}
}
数据段 (ID = 11)
数据段用于存放内存的初始化数据;与元素段中的元素项类似,数据项包含内存索引、内存偏移量、初始化数据三部分信息,内存索引用于指定初始化哪块内存,内存偏移量用于指定从哪里开始填充初始化数据,初始化数据为用于初始化内存的字节数组。数据段和数据项的二进制编码格式如下所示。
data_sec: 0x0B | byte_count | vec
data : mem_idx | offset_expr | vec
针对数据段的编码规范,我们定义如下的数据结构;其中,memoryIndex 描述需要初始化的内存块;offset 描述内存中的数据区域的起始偏移量;initData 指定内存区域的初始化数据。
const dataInModule: Array;
type Data = {
type: "Data";
memoryIndex: Memidx;
offset: Array;
initData: Array;
};
基于数据段的二进制格式和数据结构定义,WAInterp 定义 parseDataSection 函数实现元素段的二进制解析。
function parseDataSection(numberOfElements: number) {
const dataInModule: Array = [];
for (let i = 0; i < numberOfElements; i++) {
const memoryIndexu32 = readU32(); // memory index
const memoryIndex = memoryIndexu32.value;
skipBytes(memoryIndexu32.nextIndex);
const instrs: Array = []; // offset expr
parseInstructionBlock(instrs);
const bytes: Array = parseVec((b) => b); // init data
dataInModule.push(
t.data(t.memIndexLiteral(memoryIndex), instrs, t.byteArray(bytes))
);
}
return dataInModule;
}
自定义段 (ID = 0)
自定义段用于存放自定义功能数据,当前阶段主要用于保存调试符号信息。自定义段与其他段有如下两方面的差异,首先,自定义段不参与模块语义,自定义段存放的都是额外信息 (比如,函数名和局部变量名等调试信息,第三方扩展信息等),即使完全忽略这些信息也不影响模块的执行;其次,自定义段可以出现在任何一个非自定义段前后,而且出现的次数不受限制。由于自定义段为定制化的需求服务,所以对于 WebAssembly 规范来说它的数据是非结构化的,即,对于自定义段中字节数组的结构化解析由对应的自定义功能模块负责。
自定义段二进制编码格式如下所示。
custom_sec: 0x00 | byte_count | name | vec
针对自定义段的编码规范,我们定义如下的数据结构来表示;其中,Bytes 用于保存原始的字节数据,提供给自定义功能解析和使用。
type CustomSec = {
Name : string
Bytes : Array
}
WAInterp 暂不考虑实现自定义段功能,因此,后续不再展开讨论自定义段,相关功能和详细的规范请参阅WebAssembly 核心规范[8]。
3.3 WAInterp 模块解码
在上一小节中,我们对模块中各个段的二进制编码格式做了详细分析,并为各段定义了对应的数据结构和解析函数。基于各段的解析函数和数据结构,WAInterp 定义 decode 函数来实现模块的二进制格式解析并转换为运行时环境中的对象实例;在 decode 函数中的,参数 buffer 是原始的模块二进制字节数组,返回值 Program 是解码后的运行时对象表示。解码的主要过程分为三个部分,第一部分是模块的文件头解析,包括模数和版本号;第二部分是各个段的解析及各个索引空间的创建;第三部分是运行时对象实例 Program 的创建和初始化。
type Program = {
type: "Program";
body: Array;
};
export function decode(buffer: ArrayBuffer, opts: DecoderOpts): Program
魔数和版本号
WebAssembly 模块二进制格式中魔数占 4 个字节,内容是