我正在学习反向工程师。因此,我正在编写一些程序并尝试理解它们的程序集。
我偶然发现了一个奇怪的案例,我想我不能一个人解决它。 />
 #include <stdio.h>

int main(){

char *texto = "O numero e %d\n";
int i = 10;

while(i){
    printf(texto, i--);
}

return 0;
}


IDA生产的组件如下:

mov     eax, [esp+28]
lea     edx, [eax-1] ; The part i don't understand
mov     [esp+28], edx
mov     [esp+4], eax
mov     eax, [esp+18h]
mov     [esp], eax      ; char *
call    _printf


我能理解的是它存储了eax中的旧值并推送到堆栈(我故意没有打开优化),然后推送格式。
虽然发生在中间,但它执行i--,但是我不明白它是如何工作的。因此它得到的是eax-1的地址并存储在edx中,然后将其存储在i中,但是eax并不保存地址,而是一个值。

评论

lea edx,[blah-1]本质上是mov edx,blah-1(后者显然不是有效的命令)

#1 楼

您所看到的是编译器喜欢使用的一种效率技巧。

内部,CPU在数字和地址之间没有区别-32位整数和指针是同一回事。 (或者,如果使用的是较新的体系结构,则为64位,但是由于您的寄存器名称以e开头,因此您使用的是32位)。操作数本身。用C术语,您可以将[eax-1]视为*(eax-1),并且lea向其添加了lea运算符,因此&类似于lea edx, [eax-1]。当然,这与edx = &(*(eax-1))相同。

使用指令序列eax-1mov edx, eax; sub edx, 1,编译器可以完成完全相同的操作。那么,为什么它使用mov edx, eax; dec edx指令呢?同样,当在后续操作中两次使用同一寄存器时,流水线也会出现问题。这意味着,在较旧的处理器上,使用lea比其他处理器快几个周期,并且在编译器中实现起来并不难,因此这是编译器传统上所做的。
“(需要)lea使用单独的硬件”已经不再存在了,流水线比以前更加智能,因此我怀疑这些天是否会有所作为。但是它仍然在编译器中,不会被从编译器中删除,因为没有充分的理由。

评论


了解&(*(eax-1))可以解决此问题。感谢您提供完整而深入的答案!

–krystalgamer
16-3-29在14:12

即使在今天,它也减小了代码大小。那不是胜利吗?

– John Dvorak
16 Mar 29 '16 at 14:23

@JanDvorak:不会将EAX存储到[ESP + 4],然后使用“ dec eax”并将EAX存储到[ESP + 28]保存更多代码吗?我知道现代CPU的“ inc”和“ dec”比“ add”要慢,但是我认为它们仍然是单字节指令,不是吗?

–超级猫
16 Mar 29 '16 at 15:46

@Jan:现代编译器倾向于在最小的挑衅下积极地进行内联,因此,在超级膨胀中,这里和那里的微不足道的节省将被忽略。而且,CPU现在有太多的ALU,几乎没有人会注意到lea不使用任何ALU。但是,lea不用理会这些标志,它可以减少寄存器压力(在x86模式下总是有问题!),并且可以缩短依赖链。不喜欢什么;-)

– DarthGizka
16-3-29在15:47

实际上,它仍然有很大的不同。在这种情况下,生成是愚蠢的,因为常量为1,但是在大多数情况下,它比mov / add对甚至移动/添加/添加三元组要好得多。

–约书亚
16 Mar 29 '16 at 20:06

#2 楼

根据Intel,LEA指令为:


该指令计算第二个操作数的有效地址
(源操作数)并将其存储在第一个操作数中(目标
/>操作数)。源操作数是用一种处理器寻址模式指定的存储器地址(偏移量部分)。 destination
操作数是一个通用寄存器。因此,lea edx, [eax-1]计算[eax-1]的地址,即eax-1,因为[]表示操作数应为作为地址处理。该地址之后将存储在edx中。

评论


我一直在搞乱寄存器的地址。.现在我知道它减去1。谢谢!

–krystalgamer
16 Mar 29 '16 at 12:06