为什么GCC会分配比所需更多的栈内存?

8
我正在阅读《计算机系统:程序员的视角,第三版》(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);
}

本书提供了由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

我能理解这段代码:编译器在栈上分配了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

我注意到gcc先为4个本地变量分配了24字节。然后它使用pushq将2个参数添加到堆栈中,因此最终的代码使用addq $40, %rsp来释放堆栈空间。

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


1
看起来Compiler Explorer使用的GCC版本在所有版本中都非常一致,但是我的Ubuntu 20.04 GCC 9.3.0却执行pushq %rbx; ...; addq $32, %rsp; pop %rbx - Antti Haapala -- Слава Україні
@klutt 实际上,我想探究编译器为什么会分配额外的8字节空间。 - Pluto
9
你的书中的代码违反了ABI,在call之前没有保持16字节的堆栈对齐。为什么x86-64 / AMD64 System V ABI要求16字节的堆栈对齐? 如果这是来自CS:APP全球版,那是因为出版商雇佣了一些无能的人编写了糟糕的练习题,其中包括虚假地声称他们的垃圾是实际的编译器输出,而它甚至无法汇编。书中其他部分仍然不错。 - Peter Cordes
1
@PeterCordes,我刚刚根据你提供的链接阅读了北美版书中相应的代码,它与我书中的代码完全相同。我还查阅了勘误表,但没有关于这段代码的信息。不过,书中的代码确实违反了16字节对齐要求。感谢你的回答。 - Pluto
重新打开,因为对于重复代码无法解决原始代码的混淆。 - Nate Eldredge
显示剩余2条评论
1个回答

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使用-Og -mpreferred-stack-boundary=3 -maccumulate-outgoing-args编译时,所生成的汇编代码(Godbolt)几乎相同。其中前者将ABI更改为仅保持2^3字节的堆栈对齐,而不是默认的2^4。(使用此方式编译的代码无法安全地调用任何正常编译的内容,甚至标准库函数也不行。)-maccumulate-outgoing-args曾经是旧版GCC的默认选项,但现代CPU具有“堆栈引擎”,使得push/pop成为单个uop,因此该选项不再是默认选项;堆栈参数的push可以节省一些代码大小。

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


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


相关文章: 为什么GCC在栈上分配比对齐所需更多的空间? - GCC存在一个错过优化的bug,有时会额外保留16个字节,但这不是本文讨论的情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接