2017-02-27 84 views
1

我不知道我的标题是否合适,导致我的问题是:我知道有时候(当我想使用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

我不认为打开优化检查生成的代码是没有任何价值的。 – DeiDei

+0

可能$ 32意味着ASCII'2',它只是硬编码这个值而不是计算它?你已经启用了哪些优化? – Lundin

+1

“为什么不出现在第一个示例中,没有printf语句?”它不必,因为它是(堆栈式)最深的功能。调用printf的版本必须保存堆栈,因为printf本身会禁止它。但DeiDei的未经优化的代码没有经过优化,在 – Tommylee2k

回答

3

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

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

但为什么编译器会在这里放置一条subq $32, %rsp行?为什么不出现在第一个例子中,没有printf声明?

可能因为没有任何电话给printf您的main已成为叶子程序。 因此,编译器不需要更新%rsp以符合ABI(在中调用printf调用帧)。

+0

输出几乎相同,只有几条注释出现在行尾 – Frynio

2

rsp用来传递帧指向内部调用,如果一个函数没有调用其他函数,它不需要调整rsp偏移量。

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

+0

好的,但后来我只需要2个printf参数的空间,所以这将是2x8字节= 16 – Frynio

+1

这是非常不准确的。 rsp是堆栈指针,用于保留内存。可能是在OP的环境中,你可以在不降低rsp的情况下写入堆栈,但在其他环境中,这会具有破坏性。 – linuxfan

+0

@Frynio数组本身5 * 4 = 20B也存在,它是局部变量,所以它驻留在堆栈内存中。从'main'返回时,堆栈帧被抛弃,因此任何本地POD变量都会自动“释放”(尽管对象需要先析构函数调用,因此在释放堆栈帧和“ret”之前会有更多的汇编代码)。实际上,printf不会为参数提供空间,您正在使用64b ABI,其中参数在寄存器中发送(如果可能,请在此处)。 – Ped7g

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 
4

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

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