gcc版本的这种改进的原因和好处> = 4.9.0 vs gcc version< 4.9?

时间:2016-12-23 13:47:15

标签: c gcc disassembly

我最近利用了一个危险的程序,发现了x86-64架构上gcc版本之间差异的一些有趣内容。

注意

  1. 错误地使用gets 此问题。

  2. 如果我们将gets替换为任何其他功能,则问题不会改变。

  3. 这是我使用的源代码:

    #include <stdio.h>
    int main()
    {
        char buf[16];
        gets(buf);
        return 0;
    }
    

    我使用gcc.godbolt.org以标记-m32 -fno-stack-protector -z execstack -g反汇编程序。

    在反汇编代码中,gcc版本&gt; = 4.9.0

    lea     ecx, [esp+4]            # begin of main
    and     esp, -16
    push    DWORD PTR [ecx-4]       # push esp
    push    ebp
    mov     ebp, esp
    /* between these comment is not related to the question
    push    ecx
    sub     esp, 20
    sub     esp, 12
    lea     eax, [ebp-24]
    push    eax
    call    gets
    add     esp, 16
    mov     eax, 0
    */
    mov     ebp, esp            
    mov     ecx, DWORD PTR [ebp-4]  # ecx = saved esp
    leave
    lea     esp, [ecx-4]
    ret                             # end of main
    

    但gcc版本&lt; 4.9.0 只是:

    push    ebp                     # begin of main
    mov     ebp, esp
    /* between these comment is not related to the question
    and     esp, -16
    sub     esp, 32
    lea     eax, [esp+16]
    mov     DWORD PTR [esp], eax
    call    gets
    mov     eax, 0
    */
    leave
    ret                             # end of main
    

    我的问题是:反汇编代码及其好处的差异是什么原因造成的?它有这个技术的名称吗?

2 个答案:

答案 0 :(得分:1)

如果没有实际值,我无法肯定地说:

and     esp, 0xXX               # XX is a number

但这看起来很像额外的代码,可以将堆栈对齐到比ABI要求的更大的值。

编辑:值为-16,即32位0xFFFFFFF0或64位0xFFFFFFFFFFFFFFF0,因此这确实是堆栈对齐到16字节,可能意味着使用SSE指令。正如评论中所提到的,&gt; = 4.9.0版本中有更多代码,因为它也会对齐帧指针而不是堆栈指针。

答案 1 :(得分:1)

用于32位程序的i386 ABI强制一个进程在加载后立即必须使堆栈在32位值上对齐:

  

%esp执行通常的工作,堆栈指针保存地址   堆栈的底部,保证字对齐

使用用于64位程序的x86_64 ABI 1 来对付它:

  

%rsp堆栈指针保存具有最低地址的字节的地址   是堆栈的一部分。保证在进程条目

16字节对齐

新AMD的64位技术改写旧i386 ABI的机会允许进行大量由于向后兼容而缺乏的优化,其中包括更大(更严格的)堆栈对齐。
我不会详细讨论堆栈对齐的好处,但只要说4字节对齐好,那么16字节对齐就好了。
非常值得花费一些指令来对齐堆栈。

这就是GCC 4.9.0+的作用,它将堆栈对齐为16字节 这解释了and esp, -16而不是其他说明。

当编译器只知道堆栈是4字节对齐时(因为and esp, -16可以是0,4,8或12),将堆栈与esp MOD 16对齐是最快的方法。
但是,它是破坏性方法,编译器会丢失原始esp值。

但是现在它出现了鸡或蛋的问题:如果我们在对齐堆栈之前将原始esp保存在堆栈上,我们会失去它,因为我们不知道有多远通过对齐降低堆栈指针。如果我们在对齐之后保存它,那么我们就不能。我们在路线上失去了它 因此,唯一可行的解​​决方案是将其保存在寄存器中,对齐堆栈,然后将所述寄存器保存在堆栈中。

;Save the stack pointer in ECX, actually is ESP+4 but still does
lea     ecx, [esp+4]            #ECX = ESP+4

;Align the stack
and     esp, -16                #This lowers ESP by 0, 4, 8 or 12

;IGNORE THIS FOR NOW
push    DWORD PTR [ecx-4]  

;Usual prolog
push    ebp
mov     ebp, esp

;Save the original ESP (before alignment), actually is ESP+4 but OK
push    ecx

GCC将esp+4保存在ecx中,我不知道为什么 2 但这个值仍然可以解决问题。

唯一的谜团是push DWORD PTR [ecx-4] 但事实证明这是一个简单的谜团:出于调试目的,GCC会在旧帧指针之前(push ebp之前)推送返回地址,这是32位工具所期望的。 自ecx=esp_o+4起,其中esp_o是原始堆栈指针预对齐,[ecx-4] = [esp_o] = return address

注意,现在堆栈的模数为12个12字节,因此局部变量区域的大小必须为16 * k + 4,以使堆栈再次以16字节对齐。
在您的示例中, k 为1,区域大小为20字节。

后续sub esp, 12用于对齐gets函数的堆栈(要求是在函数调用时对齐堆栈)。

最后,代码

mov ebp,esp
mov ecx,DWORD PTR [ebp-4] #ecx = saved esp 离开 lea esp,[ecx-4] 转发

第一条指令是复制粘贴错误 人们可以check it out或者只是推理 如果它在那里,[ebp-4]下面堆栈指针(并且i386 ABI没有red zone)。

其余的只是撤消prolog中的内容:

;Get the original stack pointer
mov     ecx, DWORD PTR [ebp-4]          ;ecx = esp_o+4

;Standard epilog
leave                                   ;mov esp, ebp / pop ebp
                                        ;The stack pointer points to the copied return address                

;Restore the original stack pointer
lea     esp, [ecx-4]                    ;esp = esp_o
ret   

GCC必须先获取保存在堆栈中的原始堆栈指针(+4),然后恢复旧的帧指针(ebp),最后恢复原始堆栈指针。
执行lea esp, [ecx-4]时,返回地址位于堆栈的顶部,因此理论上GCC可以返回,但它必须恢复原始esp,因为main 不是< / em>要在C程序中执行的第一个函数,因此它不能使堆栈不平衡。

1 这是不是最新版本,但引用的文字在后续版本中没有变化。
2 这已在此处讨论,但我不记得是在某些评论或答案中。