惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
U
Unit 42
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
The GitHub Blog
The GitHub Blog
Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
Stack Overflow Blog
Stack Overflow Blog
爱范儿
爱范儿
博客园 - 司徒正美
Vercel News
Vercel News
I
InfoQ
GbyAI
GbyAI
C
Check Point Blog
B
Blog RSS Feed
Martin Fowler
Martin Fowler
B
Blog
MyScale Blog
MyScale Blog
腾讯CDC
博客园 - Franky
Blog — PlanetScale
Blog — PlanetScale
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 三生石上(FineUI控件)

h-jr.me

Tinymist 的 Typst 无用声明检测 一路顺风 经验贴|末九17%上岸南大计院 Frama-C WP Tutorial | 函数契约 SOP 和 POS Verilog 打砖块游戏 Verilog 基础语法 我和 Typst 的 2024 计算机网络 TCP 大实验 2024 年冬天吃的第一个亏从换电脑开始 我学编程原神 Rust 记三天数模国赛 C 编译体系
Wasm 二进制结构
2025-06-02 · via h-jr.me

使用一个示例的 add.wat

(module

(func (export "add") (param $a i32) (param $b i32) (result i32)

(local.get $a)

(local.get $b)

i32.add

)

)

执行以下命令,-v 可以展示详细的编译信息,这里我们主要看编译码。

wat2wasm -v add.wat

0000000: 0061 736d ; WASM_BINARY_MAGIC

0000004: 0100 0000 ; WASM_BINARY_VERSION

; section "Type" (1)

0000008: 01 ; section code

0000009: 00 ; section size (guess)

000000a: 01 ; num types

; func type 0

000000b: 60 ; func

000000c: 02 ; num params

000000d: 7f ; i32

000000e: 7f ; i32

000000f: 01 ; num results

0000010: 7f ; i32

0000009: 07 ; FIXUP section size

; section "Function" (3)

0000011: 03 ; section code

0000012: 00 ; section size (guess)

0000013: 01 ; num functions

0000014: 00 ; function 0 signature index

0000012: 02 ; FIXUP section size

; section "Export" (7)

0000015: 07 ; section code

0000016: 00 ; section size (guess)

0000017: 01 ; num exports

0000018: 03 ; string length

0000019: 6164 64 add ; export name

000001c: 00 ; export kind

000001d: 00 ; export func index

0000016: 07 ; FIXUP section size

; section "Code" (10)

000001e: 0a ; section code

000001f: 00 ; section size (guess)

0000020: 01 ; num functions

; function body 0

0000021: 00 ; func body size (guess)

0000022: 00 ; local decl count

0000023: 20 ; local.get

0000024: 00 ; local index

0000025: 20 ; local.get

0000026: 01 ; local index

0000027: 6a ; i32.add

0000028: 0b ; end

0000021: 07 ; FIXUP func body size

000001f: 09 ; FIXUP section size

首先第一部分是 magic number,代表 \0asm 和 wasm 标准的版本号。

0000000: 0061 736d ; WASM_BINARY_MAGIC

0000004: 0100 0000 ; WASM_BINARY_VERSION

后面紧跟着的就是不同的 section。根据https://www.w3.org/TR/wasm-core-1/,一个 wasm 文件可能会有以下这些 section:

IdSection描述
0custom section
1type section包含函数签名的信息
2import section定义从其他模块导入的信息
3function section提供函数签名的引用信息
4table section
5memory section包含线性内存的信息
6global section
7export section定义导出到其他模块的信息
8start section
9element section
10code section包含每个函数的指令信息
11data section定义初始化时要放入内存的数据

Type Section

这里的 Type 指函数类型,也就是它的签名。

typesec::=ft∗:section1(vec(functype))⇒ft∗\mathtt{typesec} ::= ft^* : \mathtt{section}_1(\mathtt{vec}(\mathtt{functype})) \Rightarrow ft^*

以上面这个例子中的 Type Section 为例:

; section "Type" (1)

0000008: 01 ; section code

0000009: 00 ; section size (guess)

000000a: 01 ; num types

; func type 0

000000b: 60 ; func

000000c: 02 ; num params

000000d: 7f ; i32

000000e: 7f ; i32

000000f: 01 ; num results

0000010: 7f ; i32

0000009: 07 ; FIXUP section size

这里开头两个字节的 section codesection size 在每个 section 都会出现,其中 section size 并不是必须的,但如果有的话,它的值会是这个 section 除了前两个字节的字节数,可以用来跳过这个 section。

可以看到后面 functype 的容器类型是 vector,紧跟着的 num types 表示函数签名的数量,然后就是各个签名。

functype::=0x60t1∗:vec(valtype)t2∗:vec(valtype)⇒[t1∗]→[t2∗]\mathtt{functype} ::= \mathtt{0x60} \quad t_1^* : \mathtt{vec}(\mathtt{valtype}) \quad t_2^* : \mathtt{vec}(\mathtt{valtype}) \Rightarrow [t_1^*] \to [t_2^*]

每个签名以 0x60 开头,然后是参数数量、参数类型和返回值数量、返回值类型。

0000009: 07 ; FIXUP section size

这里这行并不是标准中提及的,而是因为 wat2wasm 是一个单遍编译器,所以它不会先计算大小(size 为 0)而是在最后再计算大小,用计算出的大小替换原来的 0。

Code Section

这个 section 存储函数的指令信息。

codesec::=code∗:section10(vec(code))⇒code∗code::=size:u32code:func⇒code(if size=∣∣func∣∣)func::=(t∗)∗:vec(locals)e:expr⇒concat((t∗)∗),e∗(if ∣concat((t∗)∗)∣<232)locals::=n:u32t:valtype⇒tn\mathtt{codesec} ::= code^* : \mathtt{section}_{10}(\mathtt{vec}(\mathtt{code})) \Rightarrow code^* \\ \mathtt{code} ::= size:\mathtt{u32} \quad code:\mathtt{func} \Rightarrow code \quad (\text{if } size = ||\mathtt{func}||) \\ \mathtt{func} ::= (t^*)^* : \mathtt{vec}(\mathtt{locals}) \quad e:\mathtt{expr} \Rightarrow \mathrm{concat}((t^*)^*), e^* \quad (\text{if } |\mathrm{concat}((t^*)^*)| < 2^{32}) \\ \mathtt{locals} ::= n:\mathtt{u32} \quad t:\mathtt{valtype} \Rightarrow t^n

; section "Code" (10)

000001e: 0a ; section code

000001f: 00 ; section size (guess)

0000020: 01 ; num functions

; function body 0

0000021: 00 ; func body size (guess)

0000022: 00 ; local decl count

0000023: 20 ; local.get

0000024: 00 ; local index

0000025: 20 ; local.get

0000026: 01 ; local index

0000027: 6a ; i32.add

0000028: 0b ; end

0000021: 07 ; FIXUP func body size

000001f: 09 ; FIXUP section size

num functions 表示功能的数量,并根据该数量对功能进行解码。其余部分包括每个函数的局部变量定义和指令信息,需要反复解码。func body size 表示函数体的字节数。local decl count 表示局部变量的数量。如果是 0 则不采取任何行动,但如果大于 1,随后的字节序列定义了局部变量的类型。

Function Section

将函数体 (Code Section) 与类型信息 (Type Section) 关联。

; section "Function" (3)

0000011: 03 ; section code

0000012: 00 ; section size (guess)

0000013: 01 ; num functions

0000014: 00 ; function 0 signature index

0000012: 02 ; FIXUP section size

开头是函数数量,然后跟着的是每个函数下标对应的类型下标。下面这张图中间就是 function section。

image.png

Memory Section

存储有关为 Runtime 提供线性内存的信息。内存可以以页为单位进行扩展,1 个页为 64KiB。内存格式为 (memory $initial $max) 。max 是可选项,如果未指定,则没有上限。

比如下面这个例子:

会被编译为

; section "Memory" (5)

0000008: 05 ; section code

0000009: 04 ; section size

000000a: 01 ; num memories

; memory 0

000000b: 01 ; limits: flags

000000c: 02 ; limits: initial

000000d: 03 ; limits: max

num memories 表示存储器的数量,但在规范的第 1 版中,每个模块只能定义一个存储器,因此该值实际上固定为 1。limits: flags 是一个值,用于确定 max 是否存在,也就是说,如果 0 只有 initial 存在;如果 1 则 initial 和 max 都存在。

Data Section

这个地方定义了内存的初始数据。

datasec::=seg∗:section11(vec(data))⇒segdata::=x:memidxe:exprb∗:vec(byte)⇒{data x,offset e,init b∗}\mathtt{datasec} ::= seg^* : \mathtt{section}_{11}(\mathtt{vec}(\mathtt{data})) \Rightarrow seg \\ \mathtt{data} ::= x:\mathtt{memidx} \quad e:\mathtt{expr} \quad b^*:\mathtt{vec}(\mathtt{byte}) \Rightarrow \{\mathsf{data}~x, \mathsf{offset}~e, \mathsf{init}~b^*\}

数据格式为 (data $memory $offset $data) 并由以下要素组成:

  • $memory 是放置数据的内存的索引
  • $offset 是计算内存偏移量以放置数据的指令序列
  • $data 是要存入内存的实际数据

比如下面这个例子:

(module

(memory 1)

(data 0 (i32.const 0) "Hello, World!\n")

)

对应的内存段:

; section "Data" (11)

000000d: 0b ; section code

000000e: 14 ; section size

000000f: 01 ; num data segments

; data segment header 0

0000010: 00 ; segment flags

0000011: 41 ; i32.const

0000012: 00 ; i32 literal

0000013: 0b ; end

0000014: 0e ; data segment size

; data segment data 0

0000015: 4865 6c6c 6f2c 2057 6f72 6c64 210a ; data segment data

数据被组织成称为 segments 的结构,可能有多个 segments。一个 segment 包括 header 和 data 两部分,其中 header 包含计算偏移和 data 保存实际数据。

data segment header 是保存元数据的区域,例如数据放置的内存和偏移量。每个 segment 都有一个 header.

segment flags 表示放置数据的内存的索引,在版本 1 固定为 0。

从 i32.const 至 end 是偏移量。data segment size 是实际放置数据的长度,而 data segment data 是要放入内存的实际数据。

Export Section

这里定义导出的存储器和函数。

exportsec::=ex∗:section7(vec(export))⇒ex∗export::=nm:named:exportdesc⇒{name nm,desc d}exportdesc::=0x00x:funcidx⇒func x∣0x01x:tableidx⇒table x∣0x02x:memidx⇒mem x∣0x03x:globalidx⇒global x\mathtt{exportsec} ::= ex^* : \mathtt{section}_7(\mathtt{vec}(\mathtt{export})) \Rightarrow ex^* \\ \mathtt{export} ::= nm:\mathtt{name} \quad d:\mathtt{exportdesc} \Rightarrow \{\mathsf{name}~nm, \mathsf{desc}~d\} \\ \mathtt{exportdesc} ::= \mathtt{0x00} \quad x:\mathtt{funcidx} \Rightarrow \mathsf{func}~x \\ \mid \mathtt{0x01} \quad x:\mathtt{tableidx} \Rightarrow \mathsf{table}~x \\ \mid \mathtt{0x02} \quad x:\mathtt{memidx} \Rightarrow \mathsf{mem}~x \\ \mid \mathtt{0x03} \quad x:\mathtt{globalidx} \Rightarrow \mathsf{global}~x

比如我们上面的例子:

; section "Export" (7)

0000015: 07 ; section code

0000016: 00 ; section size (guess)

0000017: 01 ; num exports

0000018: 03 ; string length

0000019: 6164 64 add ; export name

000001c: 00 ; export kind

000001d: 00 ; export func index

0000016: 07 ; FIXUP section size

定义导出数量之后,每个导出的元素需要提供导出的名字、导出的类型和对应的下标。

Import Section

对应的也有导入模块外的元素的区域。

import::={module name,name name,desc importdesc}importdesc::=func typeidx∣table tabletype∣mem memtype∣global globaltype\mathtt{import} ::= \{\mathsf{module}~name, \mathsf{name}~name, \mathsf{desc}~importdesc\} \\ \mathtt{importdesc} ::= \mathsf{func}~typeidx \\ \mid \mathsf{table}~tabletype \\ \mid \mathsf{mem}~memtype \\ \mid \mathsf{global}~globaltype

对于导入,和 export 对应,我们只需要额外知道一个模块名。导入格式为 (import $module $name $type)$module 是模块名称、$name 是要导入的函数或内存的名称,而 $type 包含类型定义信息。对于函数,它包含函数的签名信息;对于内存,它定义了 min 和 max 内存信息。

比如下面这个例子,从 adder 导入了一个 add 函数。

(module

(import "adder" "add" (func (param i32 i32) (result i32)))

)

对应:

; section "Type" (1)

0000008: 01 ; section code

0000009: 07 ; section size

000000a: 01 ; num types

; func type 0

000000b: 60 ; func

000000c: 02 ; num params

000000d: 7f ; i32

000000e: 7f ; i32

000000f: 01 ; num results

0000010: 7f ; i32

; section "Import" (2)

0000011: 02 ; section code

0000012: 0d ; section size

0000013: 01 ; num imports

; import header 0

0000014: 05 ; string length

0000015: 6164 6465 72 ; import module name (adder)

000001a: 03 ; string length

000001b: 6164 64 ; import field name (add)

000001e: 00 ; import kind

000001f: 00 ; import signature index

string length 表示字符字节序列的长度、import module name 表示实际模块名称的字节序列,而 import field name 表示要导入的函数或存储器名称的字节序列。import kind 表示导入类型,其中 0 用于功能。import signature index 指向函数签名信息的索引,指向 func type 0 在 Type Section