.wasm文件内是什么?引入wasm反编译

我们拥有许多可用于创建和使用.wasm文件的编译器和其他工具。这些工具的数量正在不断增长。有时您需要查看.wasm文件并弄清楚其中的内容。也许您是Wasm工具之一的开发人员,或者也许您是一位编写旨在转换为Wasm的代码的程序员,并且对如何将其转换为代码感兴趣。这样的兴趣可以例如由性能考虑触发。



问题是.wasm文件包含非常低级的代码,看起来很像真实的汇编代码。特别是,与JVM不同,所有数据结构都被编译为装入/存储操作的集合,而不是具有明确的类名和字段名的东西。像LLVM一样,编译器可以以某种方式更改输入代码,以使所获得的内容看起来不那么接近。 

想要一个.wasm文件来找出其中发生了什么的人呢?

拆卸还是反编译?


您可以使用wasm2wat之类的工具将.wasm文件转换为包含Wasm代码的标准文本表示形式的.wat文件(这是WABT工具箱的一部分)。转换的结果非常准确,但是读取结果代码并不是特别方便。

例如,这里是一个用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;
}

该代码存储在文件中dot.c

我们使用以下命令:

clang dot.c -c -target wasm32 -O2

接下来,要将发生的事情转换为.wat文件,我们应用以下命令:

wasm2wat -f dot.o

这将给我们带来什么:

(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个float值的结构之间是否存在共同点。

如您所见,反汇编的结果比反汇编的结果更容易理解。

反编译器写的代码是哪种语言?


wasm反编译工具输出代码,试图使该代码看起来像某种“平均”编程语言。同时,此工具尽量不要离Wasm太远。

wasm-decompiler的第一个目标是创建可读代码。就是这样的代码,它的读者可以轻松了解反编译的.wasm文件中正在发生的事情。该工具的第二个目的是通过生成完全表示源文件中发生的事情的代码来提供.wasm文件的最准确表示。显然,这些目标彼此之间并非始终具有良好的一致性。

wasm反编译器的输出最初并不被认为是代表某种实际编程语言的代码。当前无法在Wasm中编译此代码。

加载和保存数据的命令


如上所示,wasm-decompile查找与特定指针关联的加载和保存命令。如果这些命令形成一个连续序列,则反编译器将显示“内置”数据结构声明之一。

如果未访问所有“字段”,则反编译器无法可靠地将结构与使用内存进行的某些操作序列区分开。在这种情况下,wasm-decompile使用fallback选项,使用更简单的类型,例如float_ptr(如果类型相同),或者在最坏的情况下,生成说明如何使用数组的代码,例如o[2]:int这样的代码告诉我们,它o指向类型的元素int,我们转向第三个这样的元素。

由于本地Wasm函数更着重于使用寄存器而不是变量,因此后一种情况比您想象的要频繁得多。结果,在优化的代码中,可以使用同一指针处理完全不相关的对象。

反编译器寻求一种智能的索引方法,并且能够识别类似的模式(base + (index << 2))[0]:int。这种模式的来源是C的常规索引操作,例如base[index]base指向4字节类型的地方。这在代码中很常见,因为Wasm在加载和保存数据命令中仅支持定义为常量的偏移量。在wasm-decompile生成的代码中,此类构造转换为type base[index]:int

另外,反编译器知道绝对地址何时指向数据段。

程序流程控制


如果我们谈论控制构造,其中最著名的是if-then Wasm构造,它变成if (cond) { A } else { B },加上Wasm中的这种构造可以返回值的事实,因此它也可以表示一个三元运算符,例如cond ? A : B,语言。

其他WASM控制结构是基于块blockloop,以及过渡brbr_ifbr_table反编译器试图保持尽可能靠近这些结构。他不寻求在创建/ for / switch构造时可以重新构造它们的基础。事实是,这种方法在处理优化的代码时表现出更好的效果。例如,常规设计loop 可能会在wasm-decompile返回的代码中看起来像这样:

loop A {
  //    .
  if (cond) continue A;
}

A是一个允许您相互构建嵌套结构的标签loop。有命令ifcontinue用于控制周期的事实可能看起来与while循环有些不同,但它们与Wasm构造相对应br_if

块以类似的方式绘制,但是这里的条件是开始而不是结尾:

block {
  if (cond) break;
  //    .
}

这里显示了反编译if-then构造的结果。在将来的反编译器版本中,可能的话,可能会形成更熟悉的if-then构造,而不是这样的代码。

用于控制程序流程的最不常见的Wasm工具是br_table该工具是一种switch语句,但它使用嵌入式块。所有这些使代码的读取变得复杂。反编译器简化了此类结构的结构,力求使它们的感知更容易一些:

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

当默认选项 为时,这让人想起switch分析aD

其他有趣的功能


这是wasm反编译的更多功能:

  • , . C++-.
  • , , , . . , .
  • .
  • Wasm-, . , wasm-decompile , , , .
  • , ( , C- ).

 


反编译Wasm代码是比反编译JVM字节代码复杂得多的任务。

字节码没有经过优化,也就是说,它相当准确地再现了源代码的结构。同时,尽管这样的代码可能不包含原始名称,但字节码仍使用对唯一类的引用,而不是对内存区域的引用。

与JVM字节码不同,LLVM对进入.wasm文件的代码进行了高度优化。结果,此类代码通常会丢失大部分原始结构。输出代码与程序员编写的代码完全不同。这极大地复杂化了反汇编Wasm代码和输出结果的工作,而这些结果可以为程序员带来真正的好处。但是,这并不意味着我们不应该努力解决这个问题!

摘要


如果您对Wasm代码反编译主题感兴趣,那么理解此主题的最好方法就是采用并反编译您自己的.wasm项目!此外,您可以此处找到有关wasm-decompile的更多详细指南。可以在存储库的文件中找到反编译器代码,该文件的名称开头decompile(如果需要,请加入反编译器上的工作)。在这里,您可以找到测试,这些测试显示了.wat文件和反编译结果之间差异的其他示例。

您使用什么工具研究.wasm文件?

, , iPhone. , .


All Articles