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

时间:2016-06-09 22:57:44

标签: 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;
}

我使用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

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

谢谢!

2 个答案:

答案 0 :(得分:3)

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暂时保留在{{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位代码。)

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

有关docs / tutorials / beginner-questions

的更多链接,请参阅标记wiki

答案 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。这是因为intvoid *已对齐:

$ ./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      |
+----------+---------+--------+