You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 06 | WAT如何让一个 WebAssembly 二进制模块的内容易于解读?
你好,我是于航。
在前面的两节课中,我们分别讲解了 Wasm 模块在二进制层面的基本组成结构与数据编码方式。在 04 的结尾,我们还通过一个简单的例子,逐个字节地分析了定义在 C/C++ 源代码中的函数,在被编译到 Wasm 之后所对应的字节码组成结构。
比如字节码 “0x60 0x2 0x7f 0x7f 0x1 0x7f” ,便表示了 Type Section 中定义的一个函数类型(签名)。而该函数类型为 “接受两个 i32 类型参数,并返回一个 i32 类型值”。
我相信,无论你对 Wasm 的字节码组成结构、V-ISA 指令集中的各种指令使用方式有多么熟悉,在仅通过二进制字节码来分析一个 Wasm 模块时,都会觉得无从入手。那感觉仿佛是在上古时期时,直接面对着机器码来调试应用程序。那么,有没有一种更为简单、更具有可读性的方式来解读一个 Wasm 模块的内容呢?答案,就在 WAT。
## WATWebAssembly Text Format
首先,我们来直观地感受一下 WAT 的“样貌”。假设我们有如下这样一段 C/C++ 源代码,在这段代码中,我们定义了一个函数 factorial该函数接受一个 int 类型的整数 n然后返回该整数所对应的阶乘。现在我们来将它编译成对应的 WAT 代码。
```
int factorial(int n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n-1);
}
}
```
经过编译和转换后,该函数对应的 WAT 文本代码如下所示。
```
(func $factorial (; 0 ;) (param $0 i32) (result i32)
(local $1 i32)
(local $2 i32)
(block $label$0
(br_if $label$0
(i32.eqz
(get_local $0)
)
)
(set_local $2
(i32.const 1)
)
(loop $label$1
(set_local $2
(i32.mul
(get_local $0)
(get_local $2)
)
)
(set_local $0
(tee_local $1
(i32.add
(get_local $0)
(i32.const -1)
)
)
)
(br_if $label$1
(get_local $1)
)
)
(return
(get_local $2)
)
)
(i32.const 1)
)
```
WAT 的全称 “WebAssembly Text Format”我们一般称其为 “WebAssembly 可读文本格式”。它是一种与 Wasm 字节码格式完全等价,可用于编码 Wasm 模块及其相关定义的文本格式。
这种格式使用 “S-表达式” 的形式来表达 Wasm 模块及其定义,将组成模块各部分的字节码用一种更加线性的、可读的方式进行表达。
这种文本格式可以被 Wasm 相关的编译工具直接使用,比如 WAVM 虚拟机、Binaryen 调试工具等。不仅如此Web 浏览器还会在 Wasm 模块没有与之对应的 source-map 数据时(即无法显示模块对应的源语言代码,比如 C/C++ 代码),使用对应的 WAT 可读文本格式代码来作为代替,以方便开发者进行调试。
OK既然我们之前提到WAT 使用了 “S-表达式” 的形式来表达 Wasm 模块及其相关定义,那么接下来,我们就来看看这个 “S-表达式” 究竟是什么?
### S-表达式S-Expression
“S-表达式”,又被称为 “S-Expression”或者简写为 “sexpr”它是一种用于表达树形结构化数据的记号方式。最初S-表达式被用于 Lisp 语言,表达其源代码以及所使用到的字面量数据。比如,在 Common Lisp 这个 Lisp 方言中,我们可以有如下形式的一段代码。
```
(print
(* 2 (+ 3 4))
)
```
不知道你有没有感受到,这段 Lisp 代码与之前我们生成的函数 factorial 所对应 WAT 可读文本代码,在结构上有着些许的相似。在这段代码中,我们调用了名为 print 的方法,将一个简单数学表达式 “2 \* (3 + 4)” 的计算结果值打印到了系统的标准输出流stdout中。
在 “S-表达式” 中,我们使用一对小括号 “()” 来定义每一个表达式的结构。而表达式之间的相互嵌套关系则表达了一定的语义规则。比如在上面的 Lisp 代码中,子表达式 “(\* 2 (+ 3 4))” 的值直接作为了 print 函数的输入参数。而对于这个子表达式本身,也通过内部嵌套的括号表达式及运算符,规定了求值的具体顺序和规则。
不仅如此,每一个表达式在求值时,都会将该表达式将要执行的“操作”,作为括号结构的第一个元素,而对应该操作的具体操作“内容”则紧跟其后。
这里我将“操作”和“内容”都加上了引号,因为 “S-表达式” 可以被应用于多种不同的场景中,所以这里的操作可能是指一个函数、一个 V-ISA 中的指令,甚至是标识一个结构的标识符。而所对应的“内容”也可以是不同类型的元素或结构。因此,这里你只要了解这种通过括号划分出的所属关系就可以了。
对一个 “S-表达式” 的求值会从最内层的括号表达式开始。比如对于上述的 Lisp 代码,我们会首先计算其最内层表达式 “(+ 3 4)” 的值。计算完毕后,该括号表达式的位置会由该表达式的计算结果进行替换。以此类推,从内到外,最后计算出整个表达式的值。当然,除了求值,对于诸如 print 函数来说,也会产生一些如“与操作系统 IO 进行交互”之类的副作用Side Effect
你可以参考下面这张图来理解 “S-表达式” 的组成结构与求值方式(以上述 Lisp 代码为例)。
![](https://static001.geekbang.org/resource/image/cf/3e/cf30453b05873f51ecb9559bb31a563e.png)
我们再把目光移回到 WAT 身上。既然我们说WAT 具有与 Wasm 字节码完全等价的表达能力,可以完全表达通过 Wasm 字节码定义的 Wasm 模块内容。那么从高级语言源代码,到 Wasm 模块字节码、再到对应的 WAT 可读文本代码,这三者是如何做到一一对应的呢?
### 源码、字节码与 Flat-WAT
为了能够让你更加直观地看清楚从源代码、Wasm 字节码再到 WAT 三者之间的对应关系,首先我们要做的第一件事就是将对应的 WAT 代码 “拍平flatten将其变成 “Flat-WAT”。这里还是以“factorial” 函数对应生成的 WAT 可读文本代码为例。
“拍平”的过程十分简单。正常在通过 “S-表达式” 形式表达的 WAT 代码中,我们通过“嵌套”与“小括号”的方式指定了各个表达式的求值顺序。而 “拍平” 的过程就是将这些嵌套以及括号结构去掉,以“从上到下”的先后顺序,来表达整个程序的执行流程。
上述 WAT 代码在被“拍平”之后,我们可以得到如下所示的 Flat-WAT 代码(这里我们只列出函数体所对应的部分)。
```
(func $factorial (param $0 i32) (result i32)
block $label$0
local.get $0
i32.eqz
br_if $label$0
local.get $0
i32.const 255
i32.add
i32.const 255
i32.and
call $factorial
local.get $0
i32.mul
i32.const 255
i32.and
return
end
i32.const 1)
```
然后我们再将对应 “factorial” 函数的 C/C++ 源代码、Wasm 字节码以及上述 WAT 经过转换生成的 Flat-WAT 代码放到一起,相信你会有一个更加直观的感受。如下图所示,你可以看到 Flat-WAT 代码与 Wasm 字节码会有着直观的“一对一”关系。
![](https://static001.geekbang.org/resource/image/d2/ff/d2764cbb73d17fe8afb8bddbbd229dff.png)
### 模块结构与 WAT
除了我们前面看到的WAT 可以通过“S-表达式”的形式,来描述一个定义在 Wasm 模块内的函数定义以外WAT 还可以描述与 Wasm 模块定义相关的其他部分,比如模块中各个 Section 的具体结构。如下所示,这是用于构成一个完整 Wasm 模块定义的其他字节码组成部分,所对应的 WAT 可读文本代码。
```
(module
(table 0 anyfunc)
(memory $0 1)
(export "memory" (memory $0))
(export "factorial" (func $factorial))
...
)
```
在这里,我们仍然使用 “S-表达式” 的形式,通过为子表达式指定不同的“操作”关键字,进而赋予每个表达式不同的含义。
比如带有 “table” 关键字的子表达式,定义了 Table Section 的结构。其中的 “0” 表示该 Section 的初始大小为 0随后紧跟的 “anyfunc” 表示该 Section 可以容纳的元素类型为函数指针类型。其他的诸如 “memory” 表达式定义了 Memory Section“export” 表达式定义了 Export Section以此类推。
### WAT 与 WAST
在 Wasm 的发展初期,曾出现过一种以 “.wast” 为后缀的文本文件格式,这种文本文件经常被用来存放类似 WAT 的代码内容。
但实际上,以 “.wast” 为后缀的文本文件通常表示着 “.wat” 的一个超集。也就是说,在该文件中可能会包含有一些,基于 WAT 可读文本格式代码标准扩展而来的其他语法结构。比如一些与“断言”和“测试”有关的代码,而这部分语法结构并不属于 Wasm 标准的一部分。
相反的,以 “.wat” 为后缀结尾的文本文件,通常只能够包含有 Wasm 标准语法所对应的 WAT 可读文本代码。并且在一个文本文件中,我们也只能够定义单一的 Wasm 模块结构。
因此,在日常的 Wasm 学习、开发和调试过程中,我更推荐你使用 “.wat” 这个后缀,来作为包含有 WAT 代码的文本文件扩展名。这样可以保障该文件能够具有足够高的兼容性,能够适配大多数的编译工具,甚至是浏览器来进行识别和解析。
## WAT 相关工具
在这节课的最后,我们来看看与 WAT 相关的编译工具。为了使用下面这些工具,你需要安装名为 WABTThe WebAssembly Binary Toolkit的 Wasm 工具集。关于如何进行安装,你可以在[这里](https://github.com/WebAssembly/wabt#building-using-cmake-directly-linux-and-macos)找到答案。安装完毕后,我们便可以使用如下这些工具来进行 WAT 代码的相关处理。
* **wasm2wat**:该工具主要用于将指定文件内的 Wasm 二进制代码转译为对应的 WAT 可读文本代码。
* **wat2wasm**:该工具的作用恰好与 wasm2wat 相反。它可以将输入文件内的 WAT 可读文本代码转译为对应的 Wasm 二进制代码。
* **wat-desugar**:该工具主要用于将输入文件内的,基于 “S-表达式” 形式表达的 WAT 可读文本代码“拍平”成对应的 Flat-WAT 代码。
上述这三个工具的用法十分简单,默认情况下,转译生成的目标代码将被输出到操作系统的标准输出流中。当然,你也可以通过 “-o” 参数来指定输出结果的保存文件。更详细的信息,你可以直接参考该项目在 Github 上的帮助文档。
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
本节课我们主要讲解了 WAT这是一种可以将 Wasm 二进制字节码基于 “S-表达式” 的结构,用“人类可读”的方式展现出来的文本代码格式。
WAT 使用嵌套的“括号表达式”结构来表达 Wasm 字节码的内容表达式由“操作”关键字与相应的“内容”两部分组成。Wasm 字节码与 WAT 可读文本代码两者之间是完全等价的。
WAT 还有与之相对应的 Flat-WAT 形式的代码。在这个类型的代码中WAT 内部嵌套的表达式结构(主要是指函数定义部分)将由按顺序平铺开的,由上至下的指令执行结构作为代替。
除此之外,我们还讲解了 “.wast” 与 “.wat” 两种文本文件格式之间的区别。其中,前者为后者的超集,其内部可能会含有与“测试”和“断言”相关的扩展性语法结构;而后者仅包含有与 Wasm 标准相关的可读文本代码结构。因此,在日常编写 WAT 的过程中,建议你以 “.wat” 作为保存 WAT 代码的文本文件后缀。
最后,我们还介绍了几个可以用来与 WAT 格式打交道的工具。这几个工具均来自于名为 WABT 的 Wasm 二进制格式工具集,它们的用法都十分简单,相信你可以快速上手。
## **课后练习**
最后,我们来做一个小练习吧。
尝试使用 C/C++ 编写一个“计算第 n 项斐波那契数列值”的函数 fibonacci然后在 [WasmFiddle](https://wasdk.github.io/WasmFiddle/) 上编译你的函数,并查看对应生成的 WAT 可读文本代码。
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。