为什么编译器会在堆栈上创建空间

时间:2017-02-27 09:31:27

标签: c linux assembly x86-64

我不知道我的标题是否合适,因为我的问题是:我知道有时候(当我想使用argv []时)编译器必须在堆栈上为命令行参数安排空间。现在我编写了一个简单的程序,只是为了看看C编译器如何处理简单的C数组(实际上它以与std::array相同的方式处理它们)。我正在使用Manjaro Linux 64位。 C代码如下所示:

#include <stdio.h>

int main(){
    int a[5] = {1,2,3,4,5};
    //printf("%d", a[1]);
    return 0;
}

程序集生成输出(来自gcc main.c -fno-asynchronous-unwind-tables -o XD.s -S):

    .file   "main.c"
    .text
    .globl  main
    .type   main, @function
main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $1, -32(%rbp)
    movl    $2, -28(%rbp)
    movl    $3, -24(%rbp)
    movl    $4, -20(%rbp)
    movl    $5, -16(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret
    .size   main, .-main
    .ident  "GCC: (GNU) 6.3.1 20170109"
    .section    .note.GNU-stack,"",@progbits

现在,当我取消注释printf语句时,代码如下所示:

    .file   "main.c"
    .section    .rodata
.LC0:
    .string "%d"
    .text
    .globl  main
    .type   main, @function
main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $32, %rsp
    movl    $1, -32(%rbp)
    movl    $2, -28(%rbp)
    movl    $3, -24(%rbp)
    movl    $4, -20(%rbp)
    movl    $5, -16(%rbp)
    movl    -28(%rbp), %eax
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (GNU) 6.3.1 20170109"
    .section    .note.GNU-stack,"",@progbits

中间部分很明显,只是调用printf。但是为什么编译器在这里添加subq $32, %rsp行?为什么它没有出现在第一个例子中,没有printf语句?

4 个答案:

答案 0 :(得分:5)

这是编译器的优化。它在第一种情况下实现main是叶函数,因此它知道数组在堆栈上是安全的。而在第二种情况下,对printf的调用将覆盖堆栈帧,因此它通过递增%rsp来保护它。

实际上编译器有很多这样的优化。例如,ABI指定在调用之前堆栈必须是16字节对齐,并且以这样的方式创建帧,即%rsp是16字节对齐的。但是,如果函数没有调用任何其他函数或者没有使用任何SSE指令(需要对齐堆栈帧的指令的一个示例),则它会破坏ABI要求。 这些只是为了节省每个字节而进行的微优化。

答案 1 :(得分:4)

如果要查看生成的gcc -S -fverbose-asm -O汇编程序文件,则应使用.s编译(实际)代码。

请注意,最近的ABIcalling conventions要求堆栈指针至少对齐16字节(特别是为了与AVX或SSE兼容)。另请阅读Red Zone(建议by Zang Ming Jie)。

  

但是为什么编译器会在这里添加subq $32, %rsp行?如果没有printf语句,为什么它不会出现在第一个示例中?

可能因为没有对printf的任何调用,您的main已成为一个例行程序。 因此,编译器不需要将%rsp更新为ABI兼容(在称为 printf调用帧中)。

答案 2 :(得分:4)

我在每个装配线上添加了注释。

main:
    pushq   %rbp            ; save old stack frame
    movq    %rsp, %rbp      ; rbp = stack frame of this function
    subq    $32, %rsp       ; 32 bytes reserved on stack for local variable(s)
    movl    $1, -32(%rbp)   ; a[0] = 1 (at rbp-32 == rsp)
    movl    $2, -28(%rbp)   ; a[1] = 2
    movl    $3, -24(%rbp)   ; a[2] = 3
    movl    $4, -20(%rbp)   ; a[3] = 4
    movl    $5, -16(%rbp)   ; a[4] = 5 (rbp-16 == rsp+16)
        ; remaining 12B from rbp-12 to rbp-1 is not used ("wasted")
        ; but it works as "padding" to have correctly aligned rsp for printf
    movl    -28(%rbp), %eax ; eax = a[1]
    movl    %eax, %esi      ; esi = a[1] (argument for printf)
    movl    $.LC0, %edi     ; edi = format string pointer
    movl    $0, %eax        ; eax = 0 (zero FP/SSE arguments)
    call    printf
    movl    $0, %eax        ; return value of main
    leave                   ; restore stack frame and exit main
    ret

答案 3 :(得分:2)

rsp用于将帧点传递给内部调用,如果函数不调用其他函数,则不需要调整rsp偏移量。

注意:%rsp所指的位置之外有一个128字节的区域,称为红区。即使该功能没有调整rsp,其红色区域仍然受到保护。