直接跳到内容

WASM 内部模块

模块结构概述

WebAssembly 模块是一个结构化的二进制单元,采用自定义的二进制格式设计,旨在实现紧凑的编码和快速的解码。每个模块都遵循严格的格式规范,由一系列有序的段 (section) 组成。

基础模块结构示意图:

wasm 二进制文件
├── 魔数头 (0x00 0x61 0x73 0x6D)
├── 版本号 (0x01 0x00 0x00 0x00)
└── 段序列
    ├── 类型段
    ├── 导入段
    ├── 函数段
    ├── 表段
    ├── 内存段
    ├── 全局段
    ├── 导出段
    ├── 起始段
    ├── 元素段
    ├── 代码段
    └── 数据段

核心段结构详解

类型段 (Type Section)

类型段定义了模块中所有函数的签名,采用函数类型形式化描述。每个函数类型由参数类型列表和返回类型列表组成。

函数类型编码示意图:

函数类型结构:
0x60 → 函数类型前缀
参数数量 → 参数类型序列
返回数量 → 返回类型序列

类型段二进制布局:

类型段头部: [段ID: 0x01] [段长度]
类型数量: N
重复N次:
    0x60 (函数类型标识)
    参数个数 → 参数类型序列 (i32=0x7F, i64=0x7E, f32=0x7D, f64=0x7C)
    返回个数 → 返回类型序列

函数段 (Function Section)

函数段声明了模块中所有函数的类型索引,将函数体与类型签名关联起来。

函数声明流程:

类型段: [签名1, 签名2, ...]
    ↓ 类型索引引用
函数段: [函数1:类型索引, 函数2:类型索引, ...]
    ↓ 对应函数体
代码段: [函数1体, 函数2体, ...]

代码段 (Code Section)

代码段包含所有函数的实际指令代码,采用局部变量声明后接指令序列的格式。

函数体结构:

函数体大小 (u32)
局部变量声明数量
重复局部变量声明:
    局部变量数量 → 值类型
指令序列:
    操作码 → 立即数 (可选) → ...
结束符: 0x0B (end)

内存段 (Memory Section)

内存段定义了模块的线性内存配置,采用页为单位 (64KiB) 进行管理。

内存类型编码:

内存标志: 
  0x00 → 无最大值
  0x01 → 有最大值
初始页数
最大页数 (如果标志为0x01)

内存布局示例:

线性内存地址空间:
0x00000000 ┌─────────────────┐
          │   代码和数据段   │
0x00001000 ├─────────────────┤
          │     堆区域      │ → 可动态增长
0x00002000 ├─────────────────┤
          │     栈区域      │
          └─────────────────┘

执行模型与指令集

基于栈的虚拟机

WebAssembly 采用基于栈的执行模型,所有操作都通过操作数栈进行。

栈执行示例:

指令序列: i32.const 5 → i32.const 3 → i32.add → end
栈状态变化:
初始: []
i32.const 5: [5]
i32.const 3: [5, 3] 
i32.add:     [8]
end:         []

控制流指令

控制流指令采用结构化编程范式,确保代码的安全性和可验证性。

块结构示意图:

block $label → 结果类型
    ... 指令序列 ...
end

loop $label → 结果类型
    ... 循环体 ...
end

if $label → 结果类型
    ... 条件为真时的指令 ...
else
    ... 条件为假时的指令 ...
end

内存指令

内存指令提供对线性内存的精细控制,支持多种访问模式和地址计算。

内存访问模式:

加载指令: i32.load [offset=imm] [align=imm]
存储指令: i32.store [offset=imm] [align=imm]

地址计算: base_address + offset
对齐要求: 2^align 字节边界

高级模块特性

表段与间接调用

表段实现了函数指针机制,支持动态函数调用。

间接调用流程:

表段: [函数引用1, 函数引用2, ...]

call_indirect 类型索引 → 表索引

运行时类型检查 → 函数调用

表段结构示例:

表类型: 0x70 (anyfunc) → 初始大小 → 最大大小
元素段: 初始化表内容
    [表索引] → [偏移量] → [函数索引列表]

全局段 (Global Section)

全局段定义了模块的全局变量,支持可变和不可变两种类型。

全局变量编码:

全局数量: N
重复N次:
    值类型
    可变性 (0x00=不可变, 0x01=可变)
    初始化表达式

初始化表达式示例:

i32.const 42 → end  ; 常量42
global.get 0 → i32.add → end  ; 基于其他全局的计算

元素段 (Data Section)

数据段用于初始化线性内存的内容,支持复杂的初始化模式。

数据段结构:

数据段数量: N
重复N次:
    内存索引 (通常为0)
    偏移量表达式
    数据大小 → 原始字节数据

模块链接与实例化

导入与导出机制

导入导出系统实现了模块间的依赖管理和接口暴露。

模块链接示意图:

模块A
├── 导出: [func1, memory1, global1]
└── 依赖: [模块B的funcX]

模块B
├── 导入: [env.memory, 模块A.func1]
└── 导出: [funcX]

实例化流程:
解析导入 → 初始化内存/表 → 运行起始函数

起始段 (Start Section)

起始段指定了模块实例化后自动执行的函数,用于初始化逻辑。

起始函数特征:

  • 无参数无返回值
  • 在实例化完成后自动调用
  • 主要用于模块初始化

自定义段与元数据

名称段 (Name Section)

名称段存储调试信息,包含函数名、局部变量名等符号信息。

名称段结构:

模块名称子段
函数名称子段
局部变量名称子段

源码映射段

源码映射段将 WASM 指令映射回原始源代码位置,支持源码级调试。

验证与安全特性

模块验证流程

WebAssembly 模块在加载时必须通过严格的验证过程。

验证阶段:

结构验证 → 类型检查 → 指令验证 → 内存安全验证

可安全执行的模块实例

类型安全保证

类型系统确保所有操作在编译时类型正确,防止运行时类型错误。

类型检查规则:

  • 操作数栈类型必须匹配指令期望
  • 控制流必须保持栈高度平衡
  • 函数调用必须匹配类型签名

内存安全机制

线性内存模型提供强大的内存安全保证。

内存保护特性:

边界检查: 所有内存访问验证在有效范围内
隔离性: 模块无法访问外部内存
初始化: 数据段确保内存正确初始化

二进制编码细节

LEB128 变长编码

WebAssembly 广泛使用 LEB128 编码实现紧凑的整数表示。

编码示例:

值: 127 → 编码: 0x7F
值: 128 → 编码: 0x80 0x01
值: 624485 → 编码: 0xE5 0x8E 0x26

段序列化格式

每个段都遵循统一的序列化格式:

段ID (1字节) → 段长度 (u32) → 段内容

高级模块模式

动态链接模块

通过共享内存和表实现模块间的动态链接。

动态链接架构:

主模块
├── 导出共享内存
├── 导出函数表
└── 导入外部函数

侧模块
├── 导入共享内存
├── 导入函数表
└── 导出新增函数

组件模型

基于接口类型的组件模型支持高级的跨模块交互。

组件结构:

组件包装器
├── 实例化参数
├── 导入适配器
└── 导出适配器

通过深入理解 WebAssembly 模块的内部结构,开发者能够更好地优化模块性能、调试复杂问题,并充分利用 WASM 的安全和可移植特性。

WASM 内部模块已经加载完毕