试图对一个函数进行逆向工程

Trying to reverse engineer a function

我想更多地了解 x86 中的汇编。我在这里有一个神秘的函数,我知道 returns 一个 int 并接受一个 int 参数。 所以它看起来像 int mystery(int n){}。但是我无法弄清楚C中的功能。程序集是:

mov  %edi, %eax
lea  0x0(,%rdi, 8), %edi
sub  %eax, %edi
add  [=10=]x4, %edi
callq < mystery _util >
repz retq

< mystery _util >
mov  %edi, %eax
shr  %eax
and  [=10=]x1, %edi
and  %edi, %eax
retq

我不明白 lea 在这里做什么,它可能是什么功能。

LEA 只是一个 left-shift 乘以 3,并将结果截断为 32 位(即 zero-extending EDI 隐含为 RDI)。 x86-64 System V 在 RDI 中传递了第一个整数 arg,因此所有这些都与一个 int arg 一致。 LEA 使用 memory-operand 语法和机器编码,. Using it as part of a multiply by a constant .

生成此函数的编译器在这里错过了优化;第一个 mov 可以用

避免
lea  0x0(,%rdi, 8), %eax     # n << 3 = n*8
sub  %edi, %eax              # eax = n*7
lea  4(%rax), %edi           # rdi = 4 + n*7

但是,编译器却卡在了在 %edi 中生成 n*7,可能是因为它对常量乘法应用了窥孔优化,来不及重做寄存器分配。


mystery_util returns 其 arg 的低 2 位的按位与,在低位,因此是 0 或 1 整数值,也可以是 bool .

(shr 没有计数意味着计数为 1;请记住 x86 有一个特殊的移位操作码,隐式计数为 1。8086 只有计数 1 或 cl;立即数后来添加了计数作为扩展,implicit-form 操作码仍然更短。)

LEA 执行地址计算,但不是取消引用地址,而是将计算出的地址存储到目标寄存器中。 在 AT&T 语法中,lea C(b,c,d), reg 表示 reg = C + b + c*d,其中 C 是常数,bc 是寄存器,d 是来自 { 1,2,4,8}。因此,您可以了解为什么 LEA 在简单的数学运算中很受欢迎:它在一条指令中完成了相当多的工作。 (*包括以下 prl 评论的更正)

此汇编代码有一些奇怪的特征:repz 前缀仅在应用于某些指令时才严格定义,而 retq 不是其中之一(尽管一般行为处理器将忽略它)。有关详细信息,请参阅下面带有 link 的 Michael Petch 评论。使用 lea (,rdi,8), edi 后跟 sub eax, edi 来计算 arg1 * 7 似乎也很奇怪,但是一旦 prl 注意到标量 d 必须是 2 的常数次幂就有意义了。在任何情况下案例,这是我阅读片段的方式:

mov  %edi, %eax          ; eax = arg1
lea  0x0(,%rdi, 8), %edi ; edi = arg1 * 8
sub  %eax, %edi          ; edi = (arg1 * 8) - arg1 = arg1 * 7
add  [=10=]x4, %edi          ; edi = (arg1 * 7) + 4
callq < mystery _util >  ; call mystery_util(arg1 * 7 + 4)
repz retq                ; repz prefix on return is de facto nop.


< mystery _util >
mov  %edi, %eax          ; eax = arg1
shr  %eax                ; eax = arg1 >> 1
and  [=10=]x1, %edi          ; edi = 1 iff arg1 was odd, else 0
and  %edi, %eax          ; eax = 1 iff smallest 2 bits of arg1 were both 1.
retq

注意第 4 行的 +4 完全是虚假的。它不能影响 mystery_util 的结果。

所以,总的来说,这个 ASM 片段计算了布尔值 (arg1 * 7) % 4 == 3。

汇编代码似乎是计算机生成的,并且可能是由 GCC 编译的,因为在无条件分支 (call) 之后有一个 repz retq。还有一个迹象表明,因为在转到 mystery_util 时没有尾调用 (jmp) 而不是 call,所以代码是用 -O1 编译的(更高优化级别可能会内联这里没有发生的函数)。缺少帧指针和额外的 load/stores 表明它不是用 -O0

编译的

x 乘以 7 等同于 x 乘以 8 再减去 x。这就是以下代码所做的:

lea  0x0(,%rdi, 8), %edi
sub  %eax, %edi

LEA 可以计算地址,但它也可以用于简单的算术运算。内存操作数的语法是位移(基数、索引、比例)。 scale 可以是 1, 2, 4, 8。计算是 displacement + base + index * scale。在你的情况下 lea 0x0(,%rdi, 8), %edi 实际上是 EDI = 0x0 + RDI * 8 或 EDI = RDI * 8。完整的计算是 n * 7 - 4;

mystery_util 的计算似乎只是

n &= (n>>1) & 1;

如果我将所有这些因素综合考虑,我们得到一个函数 mystery,它将 n * 7 - 4 传递给一个名为 mystery_util 的函数,该函数 return 为 n &= (n>>1) & 1

由于 mystery_util return 是单个位值(0 或 1),因此 bool 是 return 类型是合理的。

我很好奇是否可以获得具有优化级别 1 (-O1) 的特定版本的 GCC 来重现此汇编代码。我发现 GCC 4.9.x 将为给定的 C 程序生成此 精确的汇编代码

#include<stdbool.h>

bool mystery_util(unsigned int n)
{
    n &= (n>>1) & 1;
    return n;
}

bool mystery(unsigned int n)
{
    return mystery_util (7*n+4);
}

汇编输出为:

mystery_util:
        movl    %edi, %eax
        shrl    %eax
        andl    , %edi
        andl    %edi, %eax
        ret
mystery:
        movl    %edi, %eax
        leal    0(,%rdi,8), %edi
        subl    %eax, %edi
        addl    , %edi
        call    mystery_util
        rep ret

您可以在 godbolt 上玩这个代码。


重要更新 - 没有布尔值的版本

我显然在解释这个问题时犯了错误。我假设问这个问题的人自己确定 mystery 的原型是 int mystery(int n)。我以为我可以改变它。根据一天后在 S​​tackoverflow 上询问的 related question,似乎 int mystery(int n) 作为原型作为作业的一部分提供给您。这很重要,因为这意味着必须进行修改。

需要进行的更改与mystery_util有关。在要进行逆向工程的代码中有以下几行:

mov  %edi, %eax
shr  %eax

EDI是第一个参数。 SHR是逻辑右移。如果 EDIunsigned int(或等价物),编译器只会生成它。 int 是有符号类型,会生成 SAR(算术右移)。这意味着 mystery_util 的参数必须是 unsigned int(因此 return 值可能是 unsigned int。这意味着代码将如下所示:

unsigned int mystery_util(unsigned int n)
{
    n &= (n>>1) & 1;
    return n;
}

int mystery(int n)
{
    return mystery_util (7*n+4);
}

mystery 现在有你的教授给出的原型(bool 被删除)我们使用 unsigned int 作为参数和 return 类型的 mystery_util.为了使用 GCC 4.9.x 生成此代码,我发现您需要使用 -O1 -fno-inline。可以在 godbolt 上找到此代码。程序集输出与使用 bool 的版本相同。

如果你使用unsigned int mystery_util(int n)你会发现它并没有完全输出我们想要的:

mystery_util:
        movl    %edi, %eax
        sarl    %eax          ; <------- SAR (arithmetic shift right) is not SHR
        andl    , %edi
        andl    %edi, %eax
        ret