我的堆栈中 argv 和 argc 之间的空间是多少?

Jos*_*eph 4 c debugging x86 stack x86-64

我有一个非常简单的 C 程序,我将它与 GDB 一起使用以了解有关堆栈的更多信息:

#include<stdlib.h>
#include<stdio.h>

int main(int argc, char* argv[]){
  printf("argc is %d", argc);
  int i = 0;
  for(i; i<argc; i++){
    printf("argv at %d is %s", i, argv[i]);
  }
  return;
}
Run Code Online (Sandbox Code Playgroud)

我使用编译这个程序gcc foo.c -g,然后使用 gdb 运行它gdb ./a.out。在 gdb 中,我在 main 处设置了一个断点b main,然后显示堆栈指针和基指针:

Reading symbols from ./a.out...done.
(gdb) b main
Breakpoint 1 at 0x40053c: file foo.c, line 5.
(gdb) r
Starting program: /tmp/a.out 

Breakpoint 1, main (argc=1, argv=0x7fffffffdf48) at foo.c:5
5     printf("argc is %d", argc);
(gdb) p $sp
$1 = (void *) 0x7fffffffde40

(gdb) p $rbp
$2 = (void *) 0x7fffffffde60

(gdb) x/8x $sp
0x7fffffffde40: 0xffffdf48  0x00007fff  0x00400440  0x00000001
0x7fffffffde50: 0xffffdf40  0x00007fff  0x00000000  0x00000000

(gdb) p &argv
$3 = (char ***) 0x7fffffffde40
(gdb) p &argc
$4 = (int *) 0x7fffffffde4c
Run Code Online (Sandbox Code Playgroud)

所以我可以在这里看到 argv 指向与 $sp 相同的地址,栈顶,0x7fffffffde40。而且我还看到 argc 的地址不久之后就在0x7fffffffde4c.

但是,我不确定0x7fffffffde48through 中的数据0x7fffffffde4b是什么。它有什么重要的,还是只是垃圾?为什么 argv 不直接与堆栈中的 argc 相邻?

谢谢!

Pet*_*des 6

x86-64 System V ABI 中,函数 args 在寄存器中传递。(有关其他 ABI 文档的链接以及 ABI 是什么的解释,请参阅标签 wiki。)

它们只有地址,因为gcc -O0将它们溢出到堆栈中。这使得调试 C/C++ 更容易/更一致:一切都有一个地址,并且在每个 C 语句之后存储在那里的值总是最新的。然而,它使 asm 代码效率极低。 gcc -Og对始终存储到内存并不那么严格,因此有时您会得到“值优化”,但它仍然“针对调试进行了优化”。

gcc 的另一个目标-O0是快速编译,而不是编写好的代码。因此,不要对在堆栈上放置局部变量做出非最佳决定感到惊讶。例如,它可以只保留 16 个字节,并放置argv[rbp-16](8 字节对齐),argc 在[rbp-8](4 字节对齐),并将 4B 临时保留在[rbp-4]gcc5.3 的实际选择中。

它们的实际存储位置之间存在差距的唯一“原因”是在任何额外的优化通过之前,gcc 算法的内部工作原理。


要了解编译函数时到底发生了什么,请查看 asm 输出 ( -S) from-O3 -march=native -fverbose-asm或其他内容。(使用接受输入并返回值的函数而不是编译时常量输入来执行此操作,因此它们不会优化掉。)

这是 的开始main(),由Godbolt Compiler Explorer 上的 gcc 5.3 编译(带有-O0 -fverbose-asm

main:
    push    rbp     #
    mov     rbp, rsp  #,
    sub     rsp, 32   #,
    mov     DWORD PTR [rbp-20], edi   # argc, argc
    mov     QWORD PTR [rbp-32], rsi   # argv, argv
    mov     eax, DWORD PTR [rbp-20]   # tmp92, argc     # see how dumb gcc -O0 is: it reloads from memory instead of using the value in edi
    ...
Run Code Online (Sandbox Code Playgroud)

在函数入口,edi持有 argc,并rsi持有 argv。 main()的调用者(libc C 运行时启动代码)将它们放在那里。 mov QWORD PTR [rbp-32], rsi是将 argv 存储到保留空间底部的指令(带有sub rsp, 32)。 [rbp-32]恰好是与 相同的地址[rsp],但是由于 gcc 在制作堆栈帧时遇到了麻烦(-fomit-frame-pointer仅是默认值 at-O1或更高),因此它使用从rbp.


在 32 位 SysV ABI 中,这些 args 在函数入口时已经在内存中的堆栈中,因为不幸的是,该 ABI 不使用任何寄存器进行 arg 传递。传统 ABI 所需的额外存储转发往返所带来的额外指令和延迟是 32 位比 64 位慢的原因之一,即使不包括由于寄存器较少而导致的溢出/重新加载。一些 32 位 Windows ABI 使用 2 个 regs 进行 arg-passing,例如__vectorcallABI。这很好,因为许多 Windows 程序仍然以 32 位的形式分发。(64 位 Linux 系统通常不需要运行任何 32 位代码。)


顺便说一句,ABI 标准记录了如何将 argc/argv/envp 放置在新execve(2)进程的堆栈上,并且除了%rsp必须假定大多数寄存器包含垃圾。即 的进程启动环境_start,这与调用之前的 C 运行时代码设置的环境有很大不同main()。例如,在进入 时_start,栈顶不是返回地址,所以你不能ret。(您必须进行exit(2)系统调用,这是您从 返回后最终会发生的事情main()。)

有关文档/教程/初学者问题的更多链接,请参阅标签 wiki