.wasm 是什么?wasm 反编译简介

发布时间 · 标签: WebAssembly tooling

我们有越来越多的生成或操作 .wasm 文件的编译器和其他工具,有时你可能想看看里面。也许您是这种工具的开发人员,或更直接地,您是 Wasm 程序员,并且出于性能或其他原因,想知道生成的代码是什么样的。

问题是,Wasm相当底层,很像实际的汇编代码。特别是,与JVM不同,所有数据结构都编译成加载/存储操作,而不是方便地命名类和字段。诸如LLVM之类的编译器可以进行大量的转换,使生成的代码看起来像输入的代码一样。

反汇编还是..反编译? #

您可以使用 wasm2watWABT 工具包的一部分)之类的工具,将 .wasm 转换为 Wasm 的标准文本格式 .wat,是非常忠实但不是特别可读的表示形式。

例如,一个简单的计算点积的 C 函数:

typedef struct { float x, y, z; } vec3;

float dot(const vec3 *a, const vec3 *b) {
return a->x * b->x +
a->y * b->y +
a->z * b->z;
}

我们使用 clang dot.c -c -target wasm32 -O2,然后使用 wasm2wat -f dot.o 将其转换为下面这个 .wat

(func $dot (type 0) (param i32 i32) (result f32)
(f32.add
(f32.add
(f32.mul
(f32.load
(local.get 0))
(f32.load
(local.get 1)))
(f32.mul
(f32.load offset=4
(local.get 0))
(f32.load offset=4
(local.get 1))))
(f32.mul
(f32.load offset=8
(local.get 0))
(f32.load offset=8
(local.get 1))))))

那只是一小段代码,但是由于许多原因,阅读起来并不好。除了缺乏基于表达式的语法和冗长之外,还不容易让人理解的内存中的数据结构。现在想象一下如果是大型程序的输出,很容易让人崩溃。

如果替代 wasm2wat,运行 wasm-decompile dot.o,您将得到:

function dot(a:{ a:float, b:float, c:float },
b:{ a:float, b:float, c:float }):float {
return a.a * b.a + a.b * b.b + a.c * b.c
}

这看起来要熟悉得多。除了模仿你熟悉的基于表达式语法的编程语言的外,反编译器还会查看函数中的所有加载和存储的数据,并尝试推断其结构。然后,它给每个用作指针的变量添加“内联”的结构声明。它不会创建命名的结构体声明,因为它不一定知道3个浮点数的哪种用法代表相同的概念。

反编译成什么? #

wasm-decompile 的输出结果尽可能看起来像“非常普通的编程语言”,但仍十分接近 Wasm 的表达。

它的目标第一是可读性:尽可能用易于理解的代码帮助读者理解 .wasm 中的内容。其次是尽可能 1:1 表示 Wasm,以避免失去它作为反汇编程序的实用性。显然,这两个目标并不总是统一的。

这个输出并不意味着是一种实际的编程语言,并且目前无法将其编译回 Wasm。

加载和存储 #

如上所示,wasm-decompile 会查看特定指针上的所有加载和存储。如果它们形成一个连续的访问集,它将输出这些“内联”结构声明之一。

如果不是所有“字段”都被访问,则无法确定这是固定结构,还是其他无关的内存访问形式。在这种情况下,它会退回到更简单的类型,例如 float_ptr(如果类型相同),在最坏的情况下,会输出一个类似 o[2]:int 的数组访问,其中 o 指向 int 值,我们正在访问第三个值。

最后一种情况发生的频率比你想象的要多,因为 Wasm 局部变量的功能更像寄存器而不是变量,因此优化的代码可能会为不相关的对象共享同一个指针。

反编译器尝试在索引方面更加聪明,并检测诸如 (base + (index << 2))[0]:int 之类的模式,这些模式是由常规的 C 数组索引操作(如 base[index] 其中 base 指向4字节类型)导致的。这些在代码中非常常见,因为 Wasm 在加载和存储上只有恒定的偏移量。wasm-decompile 的输出结果会将它们转换回 base[index]:int

此外,它还知道绝对地址何时引用数据段。

控制流程 #

最常见的是Wasm的if-then结构,它翻译成一个熟悉的 if (cond) { A } else { B } 语法,另外在 Wasm 中它实际上可以返回一个值,所以它也可以表示成在某些语言中像这样的三元语法 cond ? A : B

Wasm 其余的控制流基于 blockloop 块,以及 brbr_ifbr_table 跳转。反编译器会适当地接近这样的结构,而不是试图推断它们可能来自 while/for/switch 的结构,因为这样可以更好地处理优化后的输出。例如,wasm-decompile 输出中典型的循环可能如下所示:

loop A {
// body of the loop here.
if (cond) continue A;
}

这里,A 是一个标签,允许嵌套多个。与 while 循环相比,使用 ifcontinue 来控制循环可能看起来有点陌生,但它直接对应于 Wasm 的 br_if

block 类似,但它们不是向后分支,而是向前分支:

block {
if (cond) break;
// body goes here.
}

这实际上实现了 if-then。如果可能的话,未来版本的反编译器可能会将这些代码转换为实际版本。

Wasm 最令人惊讶的控制结构是 br_table,它实现了类似 switch 的功能,但使用了嵌套的 block,这往往很难读取。反编译器会将这些 block 展平以使它们更容易理解,例如:

br_table[A, B, C, ..D](a);
label A:
return 0;
label B:
return 1;
label C:
return 2;
label D:

这类似于 switch(a) 默认返回 D

其他有趣的功能 #

反编译器:

  • 可以从调试或链接信息中提取名称,或生成名称本身。当使用现有名称时,它有特殊的代码来简化C++名称中混乱的符号。
  • 已经支持多值提案,这使得表达式和语句的转化有点困难。当返回多个值时,将使用其他变量。
  • 它甚至可以从数据段的 contents 生成名称
  • 输出所有 Wasm section 类型的漂亮声明,而不仅仅是代码。例如,通过文本输出,使其成为可能。
  • 支持运算符优先级(大多数类 C 语言通用)以减少公共表达式上的 ()

局限性 #

反编译 Wasm 比JVM字节码更难。

后者是未优化的,因此相对忠于原始代码的结构,即使名称可能丢失,也引用了唯一的类,而不仅仅是内存位置。

相比之下,大多数 .wasm 的输出都经过了 LLVM 的大量优化,因此常常会丢失其大部分原始结构。输出代码与程序员编写的代码非常不同。这使得 Wasm 反编译器将会是一个更大的挑战,但这并不意味着我们不应该尝试!

更多内容 #

当然,最好的方法是反编译您自己的 Wasm 项目!

此外,关于 wasm-decompile 的更深入的指南,链接。它的实现在源文件中以 decompiler 开头,链接(欢迎提PR,使它变得更好)。一些测试用例展示了 .wat 和反编译器之间差异的更多示例,链接.