【问题标题】:Why gcc sometimes allocate extra space for local but sometimes not?为什么 gcc 有时会为本地分配额外的空间,但有时不会?
【发布时间】:2017-08-25 23:45:49
【问题描述】:

这是我的代码

#include <stdio.h>                                                              

char func_with_ret()
{
    return 1;
}
void func_1()
{
    char buf[16];
    func_with_ret();
}
void func_2()
{
    char buf[16];
    getchar();
}
int main()
{
    func_1();
    func_2();
}
  1. 我声明了 16 字节的本地缓冲区以保持堆栈指针对齐(对于 x86)。
  2. 我写了两个函数“func_1”,“func_2”,它们看起来几乎一样——分配16字节的本地缓冲区并调用一个char返回值且没有参数的函数,但一个是自定义的,另一个是getchar ()。
  3. 使用 gcc 参数“-fno-stack-protector”(因此堆栈上没有金丝雀)和“-O0”进行编译以避免意外的优化行为。

这是 gdb 对 func_1 和 func_2 的反汇编代码。

Dump of assembler code for function func_1:
   0x08048427 <+0>:     push   ebp
   0x08048428 <+1>:     mov    ebp,esp
   0x0804842a <+3>:     sub    esp,0x10
   0x0804842d <+6>:     call   0x804841d <func_with_ret>
   0x08048432 <+11>:    leave  
   0x08048433 <+12>:    ret 

Dump of assembler code for function func_2:
   0x08048434 <+0>:     push   ebp
   0x08048435 <+1>:     mov    ebp,esp
   0x08048437 <+3>:     sub    esp,0x18
   0x0804843a <+6>:     call   0x80482f0 <getchar@plt>
   0x0804843f <+11>:    leave  
   0x08048440 <+12>:    ret

在 func_1 中,为 0x10(16) 字节分配缓冲区, 但是在 func_2 中,它被分配了 0x18(24) 个字节,为什么?


编辑: @Attie 发现两者的缓冲区大小实际上是相同的,但是有 func_2 中奇怪的 8 字节堆栈空间不知道它来自哪里。

【问题讨论】:

  • 也许这与getchar()返回int,而func_with_ret()返回char有关。但归根结底,你为什么要关心?
  • 我在这两种情况下都得到 16 here。你的编译器是什么?
  • 对齐通常不是您在 C 中关心的事情...编译器会为您处理它(如果有必要)。
  • 你告诉编译器你想要 16 个元素的空间,它给了你 16 个元素的空间。没有什么可以阻止它在堆栈上留出额外的空间出于自己的目的,无论它们可能是什么(也许是为了getchar 的使用,也许不是)。我不能告诉你为什么它预留了额外的 8 个字节,只是那些额外的 8 个字节不是缓冲区的一部分。

标签: c gcc


【解决方案1】:

我刚刚尝试重现这个,见下文:

x86-64 编译(不高兴):

$ gcc p.c -g -o p -O0 -fno-stack-protector
$ objdump -d p

p:     file format elf64-x86-64

[...]

0000000000400538 <func_1>:
  400538:       55                      push   %rbp
  400539:       48 89 e5                mov    %rsp,%rbp
  40053c:       48 83 ec 10             sub    $0x10,%rsp
  400540:       b8 00 00 00 00          mov    $0x0,%eax
  400545:       e8 e3 ff ff ff          callq  40052d <func_with_ret>
  40054a:       c9                      leaveq
  40054b:       c3                      retq

000000000040054c <func_2>:
  40054c:       55                      push   %rbp
  40054d:       48 89 e5                mov    %rsp,%rbp
  400550:       48 83 ec 10             sub    $0x10,%rsp
  400554:       e8 c7 fe ff ff          callq  400420 <getchar@plt>
  400559:       c9                      leaveq
  40055a:       c3                      retq

i386 编译(成功):

$ gcc p.c -g -o p -O0 -fno-stack-protector -m32
$ objdump -d p

p:     file format elf32-i386

[...]

08048427 <func_1>:
 8048427:       55                      push   %ebp
 8048428:       89 e5                   mov    %esp,%ebp
 804842a:       83 ec 10                sub    $0x10,%esp
 804842d:       e8 eb ff ff ff          call   804841d <func_with_ret>
 8048432:       c9                      leave
 8048433:       c3                      ret

08048434 <func_2>:
 8048434:       55                      push   %ebp
 8048435:       89 e5                   mov    %esp,%ebp
 8048437:       83 ec 18                sub    $0x18,%esp
 804843a:       e8 b1 fe ff ff          call   80482f0 <getchar@plt>
 804843f:       c9                      leave
 8048440:       c3                      ret

这似乎与以下任何一项无关:

  • getchar() 是一个库函数,因此我们正在调用 PLT(过程链接表)。
  • 与函数的返回类型有关
  • main() 中的调用顺序
  • 二进制中函数的顺序
  • 动态/静态编译

但是,如果您将缓冲区的大小增加一到17,则堆栈使用量将分别增加到 32 和 40 字节(从 16 字节和 24 字节)。差别是 16 个字节,用于对齐,如回答 here

我无法回答为什么在输入 func_2() 时堆栈对齐似乎偏离了 8 个字节。


如果您将func_1()func_2() 更新为拥有一个15 字节的缓冲区和一个单字节变量,并向它们写入数据,那么您可以看到这些项目在堆栈帧中的位置:

void func_1(void) {
    char buf[15];
    char x;
    buf[0] = 0xaa;
    x = 0x55;
    func_with_ret();
}

void func_2(void) {
    char buf[15];
    char x;
    buf[0] = 0xaa;
    x = 0x55;
    getchar();
}
08048434 <func_1>:
 8048434:       55                      push   %ebp
 8048435:       89 e5                   mov    %esp,%ebp
 8048437:       83 ec 10                sub    $0x10,%esp
 804843a:       c6 45 f0 aa             movb   $0xaa,-0x10(%ebp)
 804843e:       c6 45 ff 55             movb   $0x55,-0x1(%ebp)
 8048442:       e8 d6 ff ff ff          call   804841d <func_with_ret>
 8048447:       c9                      leave
 8048448:       c3                      ret

08048449 <func_2>:
 8048449:       55                      push   %ebp
 804844a:       89 e5                   mov    %esp,%ebp
 804844c:       83 ec 18                sub    $0x18,%esp
 804844f:       c6 45 e8 aa             movb   $0xaa,-0x18(%ebp)
 8048453:       c6 45 f7 55             movb   $0x55,-0x9(%ebp)
 8048457:       e8 94 fe ff ff          call   80482f0 <getchar@plt>
 804845c:       c9                      leave
 804845d:       c3                      ret

【讨论】: