我正在尝试更多地了解x86中的程序集。我在这里有一个神秘的功能,我知道返回一个int
,并采用int
参数。
所以它看起来像int mystery(int n){}
。但我无法弄清楚C中的功能。大会是:
mov %edi, %eax
lea 0x0(,%rdi, 8), %edi
sub %eax, %edi
add $0x4, %edi
callq < mystery _util >
repz retq
< mystery _util >
mov %edi, %eax
shr %eax
and $0x1, %edi
and %edi, %eax
retq
我不明白lea在这里做了什么以及它可能是什么样的功能。
答案 0 :(得分:8)
汇编代码似乎是由计算机生成的,并且可能是由GCC编译的,因为在无条件分支(repz retq
)之后有call
。还有一个迹象表明,因为在转到jmp
代码是使用call
编译时,没有尾调用(mystery_util
)而不是-O1
(更高的优化水平可能会内联这里没有发生的功能)。缺少框架指针和额外的加载/存储表明它没有用-O0
将x
乘以7与将x
乘以8并减去x
相同。这就是以下代码所做的事情:
lea 0x0(,%rdi, 8), %edi
sub %eax, %edi
LEA可以计算地址,但也可以用于简单算术。内存操作数的语法是位移(base,index,scale)。比例可以是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<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 $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上询问的related question,似乎int mystery(int n)
作为分配的一部分给你作为原型。这很重要,因为这意味着必须进行修改。
需要进行的更改与mystery_util
有关。在要反向工程的代码中有以下几行:
mov %edi, %eax
shr %eax
EDI 是第一个参数。 SHR 是合乎逻辑的右移。如果 EDI 是unsigned int
(或等效的),编译器只会生成此项。 int
是一个签名类型,会生成 SAR (算术右移)。这意味着mystery_util
的参数必须为unsigned int
(并且后面的返回值很可能是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 -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
答案 1 :(得分:2)
LEA只是左移3,并将结果截断为32位(即将零延伸EDI转换为RDI隐含)。 x86-64 System V传递RDI中的第一个整数arg,因此所有这些都与一个int
arg一致。 LEA使用内存操作数语法和机器编码but it's really just a shift-and-add instruction。将它作为乘以常数is a common compiler optimization for 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
但相反,编译器在n*7
中生成%edi
时遇到困难,可能是因为它为常量乘法应用了一个窥孔优化,以至于重做寄存器分配的时间太晚了。
mystery_util
在低位返回其arg的低2位的按位AND,因此为0或1整数值,也可以是bool
。
(shr
没有计数意味着计数为1;请记住,x86对于隐含计数为1的班次有一个特殊的操作码.8086只有1或cl
的计数;立即计数是稍后作为扩展添加,隐式表单操作码仍然更短。)
答案 2 :(得分:1)
LEA
执行地址计算,但不是解除引用地址,而是将计算出的地址存储到目标寄存器中。
在AT&amp; T语法中,lea C(b,c,d), reg
表示reg = C + b + c*d
其中C
是常量,b
,c
是寄存器,d
是标量来自{1,2,4,8}。因此,您可以看到为什么LEA在简单的数学运算中很受欢迎:它在单个指令中有相当多的作用。 (*包括下面prl评论的更正)
此汇编代码有一些奇怪的特性:repz
前缀仅在应用于某些指令时被严格定义,而retq
不是其中之一(尽管处理器的一般行为是忽略它)。请参阅下面的Michael Petch的评论以及更多信息的链接。使用lea (,rdi,8), edi
后跟sub eax, edi
来计算arg1 * 7
也似乎很奇怪,但是一旦prl注意到标量d
必须是2的恒定幂,这是有道理的。 case,这是我阅读片段的方式:
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.