如何将汇编代码翻译回 C?

How To Translate Assembly Code Back Into C?

我看到了下面的汇编代码(注意是我自己加的注释):

00000000004005f0 <check_password>:
  4005f0:   31 c0                   xor    %eax,%eax          # i=0
  4005f2:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)   # ignore
  4005f8:   0f b6 90 c0 1b 40 00    movzbl 0x401bc0(%rax),%edx# edx=foo[i]
  4005ff:   83 f2 5f                xor    [=10=]x5f,%edx
  400602:   38 14 07                cmp    %dl,(%rdi,%rax,1)  # cmp prev result with s[i]
  400605:   75 14                   jne    40061b <check_password+0x2b> # failed return 0
  400607:   48 83 c0 01             add    [=10=]x1,%rax          # i++
  40060b:   48 83 f8 0a             cmp    [=10=]xa,%rax          # if (i==10)
  40060f:   75 e7                   jne    4005f8 <check_password+0x8> # run again
  400611:   31 c0                   xor    %eax,%eax
  400613:   80 7f 0a 00             cmpb   [=10=]x0,0xa(%rdi) 
  400617:   0f 94 c0                sete   %al
  40061a:   c3                      retq   
  40061b:   31 c0                   xor    %eax,%eax
  40061d:   c3                      retq    

并想编写等效的 C 代码来填充此内容:

编辑 1:

int check_password (char *s)
{
    for (int i=0; i!=10 ; i++)
    {
        if (foo[i] ^ 0x5f != s[i])
            return 0;
    }
    return 0==s[10]; // Isn't this strange? input is of size 10...
}

编辑 2:

int check_password (char *s)
{
    for (int i=0; i!=10 ; i++)
    {
        if (foo[i] ^ 0x5f != s[i])
            return 0==s[10]; // Isn't this strange? input is of size 10...
    }
    return 1;
}

我觉得太接近了,但有点卡住了,版本 1 是正确的还是版本 2 或其中的 none。 另外,这里正确的翻译应该是什么?

一些重要的事情要知道:

我有以下名为 foo 的字符数组,它位于内存中的地址 401bc0

char foo[10] = { 0x33, 0x3d, ...... }; // Not all values are shown

从过去做这种事情的经验来看,一个循序渐进的过程是有序的。所以首先像 fortran 一样写它:

check_password(char *rdi) {

     int  eax = 0;
     char *str = (char *)0x401bc0;
loop:
     char dl = str[eax];
     dl ^= 0x5f;
     if (dl == rdi[eax]) {
         eax++;
         if (eax == 10) {
              return rdi[10] == 0;
         }
         goto loop;
      }
      return 0;
}

在进一步学习 C 语言之前:

char *str = (char *)0x401bc0;
int check_password(char *rdi) {
     int i;
     for (i = 0; i < 10 && rdi[i] == str[i]^0x5f; i++) {
     }
     return i == 10 && rdi[i] == 0;
}

这不是非常重要,但是随着 asm 的分支越来越多,C-goto 是你的朋友;减少它后,检测结构化模式通常很简单。 在一个例子中,我将一个 900 行没人理解的汇编块提炼成 80 行 C。然后它传播到 6 个架构,每个架构都有 10 - 30% 的减速,并且所有这些都在最长的中断关闭时间内内核。

您是否查看了实际的编译器输出以查看 GCC 是否使用您的任一版本重建了相同的 asm? https://godbolt.org/z/ed86jG9rf 显示几乎正确和错误的版本。 (我故意不在这里说哪个是正确的,所以你必须自己去寻找,尽管 cmp/sete 在失败路径或不匹配路径中的区别对我来说似乎很明显。另外,你看到任何无条件 returns 1 的汇编吗?0 怎么样?)

请注意,在您修复运算符优先级错误后,其中一个会使用 GCC5.4 编译回您的 asm:foo[i] ^ 0x5f != s[i] 表示 foo[i] ^ (0x5f != s[i]),即与布尔异或,因此 if根据 XOR 结果的低位,将是 true/false。你居然(pass[i] ^ 0xf5) != s[i]比较异或结果

我只是从 asm 中注意到这一点很奇怪(循环中有 cmp / setne ),所以我打开了 -Wall 并且 GCC 告诉我发生了什么上。

<source>:19:28: warning: suggest parentheses around comparison in operand of '^' [-Wparentheses]
         if (pass[i] ^ 0x5f != s[i])
                            ^

我们有很好的C编译工具;在出于任何原因编写 C 时利用它们,包括从 asm 进行逆向工程。特别是对于您知道是编译器生成的代码,尤其是当您可以猜出编译器时。具有 System V 调用约定(RDI 中的 arg)的 x86-64 通常是 GCC 或 clang。在这种情况下,如果您将 -fno-unroll-loops 与 clang 一起使用,它们都会产生非常相似的 asm。


return 0==s[10]; // Isn't this strange? input is of size 10...

对我来说似乎很正常(但效率低下):显然静态 const char pass[] 没有以 0 ^ 0x5f 终止符结尾。否则,他们可以让循环 运行 再进行一次迭代,以检查输入的隐式长度 C 字符串函数 arg 在其 10 个匹配字节后是否具有 0 终止符。

此最终检查将拒绝在前 10 个字节后有尾随垃圾但匹配到该点的密码。

所以这意味着它期望 strlen(s) == 10,即 char [11] 包含 10 个字节加上终止符 0。