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 相邻?
谢谢!
在x86-64 System V ABI 中,函数 args 在寄存器中传递。(有关其他 ABI 文档的链接以及 ABI 是什么的解释,请参阅x86标签 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()。)