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

我试图更多地了解x86中的汇编。 我在这里有一个神秘的function,我知道返回一个int并接受一个int参数。 所以它看起来像int mystery(int n){} 。 但我无法弄清楚C中的function。 该组件是:

 mov %edi, %eax lea 0x0(,%rdi, 8), %edi sub %eax, %edi add $0x4, %edi callq  repz retq  mov %edi, %eax shr %eax and $0x1, %edi and %edi, %eax retq 

我不明白lea在这里做了什么以及它可能是什么样的function。

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

x乘以7与将x乘以8并减去x 。 这就是以下代码正在做的事情:

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

LEA可以计算地址,但也可以用于简单算术。 内存操作数的语法是位移(基数,索引,比例)。 比例可以是1,2,4,8。计算是位移+基数+指数*比例。 在你的情况下, 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的函数,该函数返回n &= (n>>1) & 1

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

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

 #include 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 $1, %edi andl %edi, %eax ret mystery: movl %edi, %eax leal 0(,%rdi,8), %edi subl %eax, %edi addl $4, %edi call mystery_util rep ret 

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


重要更新 – 没有布尔的版本

我在解释这个问题时显然错了。 我假设这个问题的人自己确定mystery的原型是int mystery(int n) 。 我以为我可以改变它。 根据一天后在Stackoverflow上提出的相关问题 ,似乎int mystery(int n)作为原型作为赋值的一部分给你。 这很重要,因为这意味着必须进行修改。

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

 mov %edi, %eax shr %eax 

EDI是第一个参数。 SHR是合乎逻辑的右移。 如果EDIunsigned int (或等效的),编译器只会生成这个。 int是一个有符号的类型,它会生成SAR (算术右移)。 这意味着mystery_util的参数必须是unsigned int (并且mystery_util的返回值可能是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作为参数并返回mystery_util类型。 为了使用GCC 4.9.x生成此代码,我发现您需要使用-O1 -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 $1, %edi andl %edi, %eax ret 

LEA只是左移3,并将结果截断为32位(即将零扩展EDI转换为RDI隐含)。 x86-64 System V传递RDI中的第一个整数arg,因此所有这些都与一个int arg一致。 LEA使用内存操作数语法和机器编码, 但它实际上只是一个移位和添加指令 。 使用它作为乘以常量的一部分是x86的常见编译器优化 。

生成此函数的编译器在此处错过了优化; 可以避免使用第一个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_utilmystery_util返回其arg的低2位的按位AND,因此为0或1整数值,也可能是bool

(没有计数的shr意味着计数为1;记住x86对于隐含计数为1的移位有一个特殊的操作码.8086只有1或cl计数;立即计数后来作为扩展和隐式forms的操作码添加还是更短。)

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不是其中之一(尽管处理器的一般行为是忽略它)。 请参阅下面的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 $0x4, %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 $0x1, %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。