我有一个非常简单的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;
}
我使用gcc foo.c -g
编译此程序,然后使用gdb ./a.out
运行gdb。在gdb里面,我使用b main
在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
所以我在这里可以看到argv指向与$ sp相同的地址,堆栈的顶部0x7fffffffde40
。我还看到argc的地址不久之后在0x7fffffffde4c
。
但是,我不确定0x7fffffffde48
到0x7fffffffde4b
的数据是什么。它有什么重要的,还是垃圾?为什么argv不直接与堆栈上的argc相邻?
谢谢!
答案 0 :(得分:3)
在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暂时保留在{{1}像gcc5.3的实际选择。
实际存储位置之间差距的唯一“原因”是gcc用于布局本地的算法的内部工作,在任何额外的优化过程之前。
要查看编译函数时到底发生了什么,请查看来自[rbp-4]
的asm输出(-S
)或其他内容。 (对于接受输入并返回值的函数执行此操作,而不是编译时常量输入,因此它们不会进行优化。)
这是由gcc 5.3 on the Godbolt Compiler Explorer (with -O0 -fverbose-asm
)编译的-O3 -march=native -fverbose-asm
的开头:
main()
在函数输入上,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
...
保存argc,edi
保存argv。 rsi
的调用者(libc C运行时启动代码)将它们放在那里。 main()
是将argv存储到保留空间底部的指令(使用mov QWORD PTR [rbp-32], rsi
)。 sub rsp, 32
恰好与[rbp-32]
的地址相同,但由于gcc遇到了构建堆栈帧的麻烦([rsp]
只是默认值-fomit-frame-pointer
或更高),它针对偏离-O1
的本地人。
在32位SysV ABI中,那些args在函数入口处已经在堆栈的内存中,因为不幸的是,ABI不使用任何寄存器进行arg传递。传统ABI所需的那些额外的存储转发往返所带来的额外指令和延迟是32位比64位慢的原因之一,甚至除了由于具有更少的寄存器而导致的溢出/重新加载之外。一些32位Windows ABI使用2个reg进行arg传递,例如: rbp
ABI。这很好,因为许多Windows程序仍然以32位分发。 (64位Linux系统通常不必运行任何32位代码。)
__vectorcall
ed进程的堆栈中,并且必须假定除execve(2)
以外的大多数寄存器都包含垃圾。即%rsp
的进程启动环境,它与调用_start
之前C运行时代码设置的显着不同。例如在进入main()
时,堆栈顶部不是返回地址,因此您不能_start
。 (您必须进行ret
系统调用,这是从exit(2)
返回后最终发生的事情。)
答案 1 :(得分:2)
这是关于地址对齐的。处理器更喜欢某些类型在某些边界上对齐。有时它是硬件性能问题,有时根本不起作用,引发某种中断。 (有时会添加软件陷阱来处理/隐藏这些异常,但会影响性能)
你会在结构中看到类似的东西。除非您手动打包,否则不会紧紧打包结构。
int main() {
struct {
char c;
void *p;
int i;
} a;
printf("%d\n", sizeof(char));
printf("%d\n", sizeof(void *));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof(a));
}
您会发现1+8+4
不等于24
。这是因为int
和void *
已对齐:
$ ./a.out
1
8
4
24
x86和x86_64列于此处:https://en.wikipedia.org/wiki/Data_structure_alignment#Typical_alignment_of_C_structs_on_x86
这也是英特尔的一张很好的表格:https://software.intel.com/en-us/articles/coding-for-performance-data-alignment-and-structures
+----------+---------+--------+
| DATATYPE | 32-BIT | 64-BIT |
+----------+---------+--------+
| | | |
| char | 1 | 1 |
| | | |
| short | 2 | 2 |
| | | |
| int | 4 | 4 |
| | | |
| long | 8 | 8 |
| | | |
| float | 4 | 4 |
| | | |
| double | 8 | 8 |
| | | |
| long | long | 8 |
| | | |
| long | double | 4 |
| | | |
| Any | pointer | 4 |
+----------+---------+--------+