assembly - 为什么gcc会产生额外的寄信人地址?

标签 assembly gcc x86 memory-alignment callstack

我目前正在学习汇编的基础知识,并且在查看gcc(6.1.1)生成的指令时遇到了一些奇怪的事情。

来源如下:

#include <stdio.h>

int foo(int x, int y){
    return x*y;
}

int main(){
    int a = 5;
    int b = foo(a, 0xF00D);
    printf("0x%X\n", b);
    return 0;
}


用于编译的命令:gcc -m32 -g test.c -o test

在检查gdb中的功能时,我得到了:

(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
   0x080483f7 <+0>:     lea    ecx,[esp+0x4]
   0x080483fb <+4>:     and    esp,0xfffffff0
   0x080483fe <+7>:     push   DWORD PTR [ecx-0x4]
   0x08048401 <+10>:    push   ebp
   0x08048402 <+11>:    mov    ebp,esp
   0x08048404 <+13>:    push   ecx
   0x08048405 <+14>:    sub    esp,0x14
   0x08048408 <+17>:    mov    DWORD PTR [ebp-0xc],0x5
   0x0804840f <+24>:    push   0xf00d
   0x08048414 <+29>:    push   DWORD PTR [ebp-0xc]
   0x08048417 <+32>:    call   0x80483eb <foo>
   0x0804841c <+37>:    add    esp,0x8
   0x0804841f <+40>:    mov    DWORD PTR [ebp-0x10],eax
   0x08048422 <+43>:    sub    esp,0x8
   0x08048425 <+46>:    push   DWORD PTR [ebp-0x10]
   0x08048428 <+49>:    push   0x80484d0
   0x0804842d <+54>:    call   0x80482c0 <printf@plt>
   0x08048432 <+59>:    add    esp,0x10
   0x08048435 <+62>:    mov    eax,0x0
   0x0804843a <+67>:    mov    ecx,DWORD PTR [ebp-0x4]
   0x0804843d <+70>:    leave  
   0x0804843e <+71>:    lea    esp,[ecx-0x4]
   0x08048441 <+74>:    ret    
End of assembler dump.
(gdb) disas foo
Dump of assembler code for function foo:
   0x080483eb <+0>:     push   ebp
   0x080483ec <+1>:     mov    ebp,esp
   0x080483ee <+3>:     mov    eax,DWORD PTR [ebp+0x8]
   0x080483f1 <+6>:     imul   eax,DWORD PTR [ebp+0xc]
   0x080483f5 <+10>:    pop    ebp
   0x080483f6 <+11>:    ret    
End of assembler dump.


令我感到困惑的部分是它正在尝试使用堆栈。
据我了解,这是做什么的:

首先,它引用了堆栈中高出4个字节的某个内存地址,据我所知应该是传递给main的变量,因为esp当前指向内存中的返回地址。

其次,出于性能原因,它将堆栈对齐到0边界。

第三,它压入新的堆栈区域ecx + 4,这应该转换为将我们假定要返回的地址压入堆栈。

第四,它将旧的框架指针推入堆栈并设置新的框架指针。

第五,它将ecx(仍指向它应该是main的参数)推入堆栈。

程序将执行应做的事情,并开始返回过程。

首先,它通过在ebp上使用-0x4偏移来恢复ecx,该偏移应访问第一个局部变量。

其次,它执行离开指令,该指令实际上只是将esp设置为ebp,然后从堆栈中弹出ebp。

因此,堆栈上的下一个内容是返回地址,并且esp和ebp寄存器应该返回到返回所需的状态了吗?

显然不是因为接下来要做的是用ecx-0x4加载esp,因为ecx仍然指向传递给main的变量,所以应该将其放在堆栈上的返回地址地址。

这工作得很好,但是引起了一个问题,为什么它会在第3步中麻烦将返回地址放到堆栈上,因为它在实际上从函数返回之前将堆栈返回到末尾的原始位置。

最佳答案

更新:gcc8至少在正常用例(-fomit-frame-pointer,并且没有alloca或需要可变大小分配的C99 VLA)中简化了此操作。可能是由于AVX使用量的增加导致更多功能需要32字节对齐的本地或数组而引起的。

另外,可能是What's up with gcc weird stack manipulation when it wants extra stack alignment?的副本



如果仅运行几次(例如在32位代码中的main开头),则此复杂的序言就可以了,但是它看起来越多,就越有必要对其进行优化。 GCC有时仍会在函数中对堆栈进行过度对齐,在这些函数中,所有> 16字节对齐的对象都已优化到寄存器中,这已经错过了优化,但是当堆栈对齐更便宜时,这种情况就不那么糟糕了。



即使在启用优化的情况下,对齐函数中的堆栈时,gcc也会生成一些笨拙的代码。我有一个可能的理论(见下文),为什么gcc可能将返回地址复制到刚好保存ebp的上面,以形成堆栈框架(是的,我同意这就是gcc在做的事情)。在此函数中看起来没有必要,而clang并没有做任何类似的事情。

除此之外,ecx的废话可能只是gcc并没有优化掉其对齐堆栈样板中不需要的部分。 (需要esp的预对齐值来引用堆栈上的args,因此将第一个可能为arg的地址放入寄存器中是有意义的)。



通过32位代码的优化,您会看到相同的结果(即使当前版本的ABI要求在进程启动时使用gcc生成的main也不假定16B堆栈对齐,而调用< cc>要么对齐堆栈本身,要么保留内核提供的初始对齐(我忘记了)。您还会在将堆栈对齐到大于16B的函数中看到这一点(例如,使用main类型的函数,有时即使它们从未溢出到堆栈中。或者带有C ++ 11 __m256声明的数组的函数,或任何其他要求对齐的方式。)在64位代码中,gcc似乎总是为此使用alignas(32)而不是r10

gcc的执行方式不需要ABI合规性,因为clang的功能要简单得多。

我添加了一个对齐变量(使用rcx作为强制编译器实际上在其堆栈上为其保留对齐空间而不是对其进行优化的一种简单方法)。我将您的代码on the Godbolt compiler explorer放入volatile的汇编中。我在gcc 4.9、5.3和6.1中看到了相同的行为,但是在clang中却看到了不同的行为。

int main(){
    __attribute__((aligned(32))) volatile int v = 1;
    return 0;
}


Clang3.8的-O3输出在功能上与其-O3 -m32输出相同。请注意,-m64启用-O3,但是某些功能仍然会生成堆栈帧。

    push    ebp
    mov     ebp, esp                # make a stack frame *before* aligning, so ebp-relative addressing can only access stack args, not aligned locals.
    and     esp, -32
    sub     esp, 32                 # esp is 32B aligned with 32 or 48B above esp reserved (depending on incoming alignment)
    mov     dword ptr [esp], 1      # store v
    xor     eax, eax                # return 0
    mov     esp, ebp                # leave
    pop     ebp
    ret


gcc的输出在-fomit-frame-pointer-m32之间几乎相同,但是它将-m64v放在中,因此-m64输出有两个额外的指令:

    # gcc 6.1 -m32 -O3 -fverbose-asm.  Most of gcc's comment lines are empty.  I guess that means it has no idea why it's emitting those insns :P
    lea     ecx, [esp+4]      #,   get a pointer to where the first arg would be
    and     esp, -32  #,          align
    xor     eax, eax  #           return 0
    push    DWORD PTR [ecx-4]       #  No clue WTF this is for; this looks batshit insane, but happens even in 64bit mode.
    push    ebp     #             make a stackframe, even though -fomit-frame-pointer is on by default and we can already restore the original esp from ecx (unlike clang)
    mov     ebp, esp  #,
    push    ecx     #             save the old esp value (even though this function doesn't clobber ecx...)
    sub     esp, 52   #,          reserve space for v  (not present with -m64)
    mov     DWORD PTR [ebp-56], 1     # v,
    add     esp, 52   #,          unreserve (not present with -m64)
    pop     ecx       #           restore ecx (even though nothing clobbered it)
    pop     ebp       #           at least it knows it can just pop instead of `leave`
    lea     esp, [ecx-4]      #,  restore pre-alignment esp
    ret


似乎gcc想要在对齐堆栈后制作其堆栈框架(使用-m32)。我想这很有意义,因此它可以引用相对于push ebp的本地语言。否则,如果要对齐本地,则必须使用ebp相对寻址。

我关于gcc为什么这样做的理论:

对齐后但按esp之前返回地址的额外副本意味着返回地址相对于保存的ebp值(以及调用子函数时将在ebp中的值)复制到预期位置)。因此,通过遵循堆栈框架的链接列表并查看返回地址以找出涉及的功能,这确实可以帮助希望放松堆栈的代码。

我不确定这是否与现代堆栈展开信息有关,该信息允许通过ebp展开堆栈(回溯/异常处理)。 (这是-fomit-frame-pointer部分中的元数据。这是围绕.eh_frame进行每次修改的.cfi_*指令的目的。)我应该看看clang在非叶函数中必须对齐堆栈时所执行的操作。



函数内部需要esp的原始值,以引用堆栈上的函数args。我认为gcc不知道如何优化其align-the-stack方法中不需要的部分。 (例如out esp不会查看其args(并且声明不接受任何参数))

这种代码生成是您在需要对齐堆栈的函数中看到的典型代码。这并不奇怪,因为使用了带有自动存储功能的main

关于assembly - 为什么gcc会产生额外的寄信人地址?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38781118/

相关文章:

assembly - 从内存中添加一个字节到 AX 寄存器

c++ - GCC/CLang 不同意模板模板参数的部分特化

c - 从头开始为 Linux 构建 Glibc-2.11.1 时出错

c - 为什么 memcpy 在 32 位模式下使用 gcc -march=native 在 Ryzen 上对于大缓冲区很慢?

c - 高效的整数比较函数

assembly - 将多个变量连接成一个字符串

c - 有没有办法忽略C预处理器输出顶部的定义(行标记)?

assembly - 无堆栈函数的跳转/跳转编译策略。 (手动使用链接注册而不是调用/返回)

assembly - 只有一个操作数的 SHR

assembly - EMU8086显示屏(黑屏)