为什么在 inc 指令期间使用 ecx 寄存器

逆向工程 部件 x86
2021-06-10 16:46:46

这是执行一些算术的代码。

int main(void) {
    int i = 3;
    i++;
    i+= 2;
    return 0;
}

我使用 32 位tcc和以下命令编译它

tcc -o hello.exe hello.c

然后,我使用IDA 免费版对其进行了反汇编,在盯着start说明看了一段时间后,我意识到我正在寻找的主要功能是在一个子程序中,去那里我看到了这个:

sub_401000 proc near

var_4= dword ptr -4

push    ebp               
mov     ebp, esp           
sub     esp, 4            // allocate 4 bytes on the stack for var i
nop

mov     eax, 3            // i = 3 instructions
mov     [ebp+var_4], eax  // 

mov     eax, [ebp+var_4]  // i++ instructions
mov     ecx, eax          // 
inc     eax               // 
mov     [ebp+var_4], eax  //

mov     eax, [ebp+var_4]  // i+=2 instructions
add     eax, 2            //   
mov     [ebp+var_4], eax  //  

mov     eax, 0           
jmp     $+5              
leave
retn
sub_401000 endp

我已经在我感兴趣的方法主体右侧的评论中添加了我对正在发生的事情的理解。

例如,增加变量将涉及将一个值移动到一个寄存器上,然后对其进行操作。我希望i++看起来像

mov     eax, [ebp+var_4]
inc     eax             

但实际指令涉及一个额外的动作

mov     eax, [ebp+var_4]  
mov     ecx, eax         // <----- ?
inc     eax               

在添加指令中,没有额外的移动。当我使用递减操作修改代码时,我也看到了额外的移动。

eaxto移动有什么目的ecx吗?

更新:

我仍在阅读有关寄存器的内容,并且从我读过的ecx内容来看,它被用作计数器,但是从这段代码中,它的用途并不明显,如果有的话。

2个回答

Guntram Blohm 的评论是正确的。的原始值i存储在其中ecx以备后用。

ISO/IEC 9899:TC3 (C99) --- 6.5.2.4 后缀自增和自减运算符:

后缀++运算符的结果是操作数的值。得到结果后,将操作数的值递增。(也就是说,将适当类型的值 1 添加到其中。)有关约束、类型和转换以及操作对指针的影响的信息,请参阅加法运算符和复合赋值的讨论。更新操作数的存储值的副作用将发生在前一个和下一个序列点之间。

序列点在文件的附录 C 中描述。

TCC 只执行很少的优化,代码是一次性生成的,每个 C 语句都是单独编译的。似乎编译器几乎尽可能简单,因此如果不需要,它不会在编译期间存储任何状态。

当TCC在++不知道++操作之前遇到序列点时++在评估操作后稍后执行操作符的副作用++将意味着存储状态信息。似乎 TCC 的作者选择了最简单的方法 - 将副作用与++算子的评估一起执行

i变量的原始值(递增之前)可能会在同一表达式中再次使用,因此将其保存到ecx寄存器中,并将后缀递增操作的结果立即存储到堆栈中的变量中 ( mov [ebp+var_4], eax)。ecx后面不使用in 的值也没有关系由于 TCC 的简单性,这是一个缺点。例如,您可以注意到代码i从堆栈加载值,eax即使该值eax已经存在。

额外使用 的原始值的代码示例i

j = i++ + i * 3;

汇编代码:

13: 8b 45 fc        mov eax, DWORD PTR [rbp-0x4]    // load i to eax
16: 48 89 c1        mov ecx, eax                    // store the original value of i for later use
19: 83 c0 01        add eax, 0x1                    // increment the value (the side effect of ++)
1c: 89 45 fc        mov DWORD PTR [rbp-0x4], eax    // store it to i - everything between "=" and "+" is done at this point including the side-effect of i++
1f: 8b 45 fc        mov eax, DWORD PTR [rbp-0x4]    // load i to eax (Now it is the incremented value.)
22: ba 03 00 00 00  mov edx, 0x3                    // load value 3 for the multiplication
27: 0f af c2        imul    eax, edx                // multiply
2a: 01 c1           add ecx, eax                    // add the original value of i (the result of i++)
2c: 89 4d f8        mov DWORD PTR [rbp-0x8], ecx    // store the result to j

1f使用增量值时,i但也可以使用i那里的原始值并且仍然遵守 C99,因为i++尚未遇到之后的序列点在这种情况下,序列点就在语句之前和之后。

TCC 被编写为快速而简单,无需大多数优化。它只编译一次程序并使用非常有限数量的寄存器(我认为是 eax、ecx 和 edx)。所以不要以为它是高效的,也不要在它“愚蠢”地做某事时感到惊讶。

在 x86 上,使用了三个临时寄存器。当需要更多寄存器时,将一个寄存器溢出到堆栈上的一个新临时变量中。

http://bellard.org/tcc/tcc-doc.html#SEC31

TCC 一次性生成代码,并且不会执行其他编译器(例如 GCC)执行的大部分优化。TCC 自己编译每条语句,并且在每条语句结束时寄存器值被写回堆栈并且必须重新读取,即使下一行使用寄存器中的值(在语句之间创建无关的保存/加载对)。TCC 只使用一些可用的寄存器(例如,在 x86 上它从不使用 ebx、esi 或 edi,因为它们需要在函数调用之间保留)。[4]

https://en.wikipedia.org/wiki/Tiny_C_Compiler#Compiled_program_performance