为什么 GCC 分配的堆栈内存比需要的多?

Plu*_*uto 8 c gcc x86-64 compiler-optimization stack-memory

我正在阅读“计算机系统:程序员的视角,3/E”(CS:APP3e),以下代码是书中的示例:

long call_proc() {
    long  x1 = 1;
    int   x2 = 2;
    short x3 = 3;
    char  x4 = 4;
    proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
    return (x1+x2)*(x3-x4);
}
Run Code Online (Sandbox Code Playgroud)

书中给出了GCC生成的汇编代码:

long call_proc()
call_proc:
    ; Set up arguments to proc
    subq    $32, %rsp           ; Allocate 32-byte stack frame
    movq    $1, 24(%rsp)        ; Store 1 in &x1
    movl    $2, 20(%rsp)        ; Store 2 in &x2
    movw    $3, 18(%rsp)        ; Store 3 in &x3
    movb    $4, 17(%rsp)        ; Store 4 in &x4
    leaq    17(%rsp), %rax      ; Create &x4
    movq    %rax, 8(%rsp)       ; Store &x4 as argument 8
    movl    $4, (%rsp)          ; Store 4 as argument 7
    leaq    18(%rsp), %r9       ; Pass &x3 as argument 6
    movl    $3, %r8d            ; Pass 3 as argument 5
    leaq    20(%rsp), %rcx      ; Pass &x2 as argument 4
    movl    $2, %edx            ; Pass 2 as argument 3
    leaq    24(%rsp), %rsi      ; Pass &x1 as argument 2
    movl    $1, %edi            ; Pass 1 as argument 1
    ; Call proc
    call    proc
    ; Retrieve changes to memory
    movslq  20(%rsp), %rdx      ; Get x2 and convert to long
    addq    24(%rsp), %rdx      ; Compute x1+x2
    movswl  18(%rsp), %eax      ; Get x3 and convert to int
    movsbl  17(%rsp), %ecx      ; Get x4 and convert to int
    subl    %ecx, %eax          ; Compute x3-x4
    cltq                        ; Convert to long
    imulq   %rdx, %rax          ; Compute (x1+x2) * (x3-x4)
    addq    $32, %rsp           ; Deallocate stack frame
    ret                         ; Return
Run Code Online (Sandbox Code Playgroud)

我可以理解这段代码:编译器在堆栈上分配了32个字节的空间,其中前16个字节保存传递给的参数proc,最后16个字节保存4个局部变量。

然后我使用优化标志在 GCC 11.2 上测试了这段代码-Og,并得到了这个汇编代码:

call_proc():
        subq    $24, %rsp
        movq    $1, 8(%rsp)
        movl    $2, 4(%rsp)
        movw    $3, 2(%rsp)
        movb    $4, 1(%rsp)
        leaq    1(%rsp), %rax
        pushq   %rax
        pushq   $4
        leaq    18(%rsp), %r9
        movl    $3, %r8d
        leaq    20(%rsp), %rcx
        movl    $2, %edx
        leaq    24(%rsp), %rsi
        movl    $1, %edi
        call    proc(long, long*, int, int*, short, short*, char, char*)
        movslq  20(%rsp), %rax
        addq    24(%rsp), %rax
        movswl  18(%rsp), %edx
        movsbl  17(%rsp), %ecx
        subl    %ecx, %edx
        movslq  %edx, %rdx
        imulq   %rdx, %rax
        addq    $40, %rsp
        ret
Run Code Online (Sandbox Code Playgroud)

我注意到 gcc 首先为 4 个局部变量分配了 24 个字节。然后它用于pushq向堆栈添加 2 个参数,因此最终代码用于addq $40, %rsp释放堆栈空间。

相比书上的代码,GCC这里多分配了8个字节的空间,而且它似乎并没有使用多余的空间。为什么需要额外的空间?

Nat*_*dge 5

(此答案是 Antti Haapala、klutt 和 Peter Cordes 上面发表的评论的摘要。)

GCC 分配比“必要”更多的空间,以确保堆栈正确对齐以调用proc:堆栈指针必须调整为 16 的倍数加上 8(即 8 的奇数倍)。 为什么 x86-64 / AMD64 System V ABI 强制要求 16 字节堆栈对齐?

奇怪的是,书中的代码并没有这样做;所示的代码将违反 ABI,并且如果proc实际上依赖于正确的堆栈对齐(例如使用对齐的 SSE2 指令),它可能会崩溃。

因此,看来书中的代码是从编译器输出中错误地复制的,或者本书的作者使用了一些不寻常的编译器标志来改变 ABI。

现代 GCC 11.2 使用 发出几乎相同的 asm ( Godbolt ) -Og -mpreferred-stack-boundary=3 -maccumulate-outgoing-args,前者更改 ABI 以仅维护 2^3 字节堆栈对齐,从默认的 2^4 向下。(以这种方式编译的代码不能安全地调用任何正常编译的内容,甚至是标准库函数。) -maccumulate-outgoing-args曾经是旧版 GCC 中的默认设置,但现代 CPU 有一个“堆栈引擎”,可以使推送/弹出单微操作,因此该选项是不再是默认值;推送堆栈参数可以节省一些代码大小。

与本书的 asm 的一个区别是movl $0, %eax在调用之前,因为没有原型,所以调用者必须假设它可能是可变参数并传递 AL = XMM 寄存器中 FP 参数的数量。(与传递的参数相匹配的原型将阻止这种情况发生。)其他指令都是相同的,并且与本书使用的任何旧 GCC 版本的顺序相同,除了返回后寄存器的选择call proc:它最终使用movslq %edx, %rdx而不是cltq(使用 RAX 进行符号扩展)。


CS:APP 3e全球版因出版商(而非作者)引入的练习问题中的错误而臭名昭著,但显然该代码也存在于北美版中。所以这可能是作者的错误/选择使用带有奇怪选项的实际编译器输出。与一些糟糕的全局版本实践问题不同,此代码可能未经某些 GCC 版本的修改,但仅使用非标准选项。


相关:为什么 GCC 在堆栈上分配的空间超出了对齐所需的空间? - GCC 有一个错过优化的错误,它有时会保留实际上不需要的额外 16 个字节。然而,这并不是这里发生的事情。