栈是用来存放数据的,不是用来存放代码的
我想到了。看起来 sub 语句正在为整个程序分配空间,四舍五入到最近的 qword。程序 1 只需要 0x17 字节 23 字节的代码,而程序 2 需要 0x28 或 40 字节的空间。
这是不正确的:堆栈上的空间没有分配给函数的代码(或任何其他类型的代码)。相反,堆栈用作函数使用的变量的存储空间。当一个函数被调用时,在栈上创建一个称为栈帧的空间来容纳传递给该函数的参数以及在该函数中声明的局部变量。这个空间是通过递减堆栈指针来创建的%rsp。这是 2 个堆栈帧的视觉描述:

这里描绘了 2 个框架:
- 的呼叫者帧,这对于函数创建的堆栈帧已经调用的当前功能
- 的被叫方框架,这是当前正在执行的函数创建的帧
请注意,代码驻留在虚拟内存地址空间中与堆栈不同的区域(图片是 Win32 进程,但也适用于PE32+ 进程地址空间,除了地址空间要大得多):

堆栈帧应与 16 字节边界对齐
为了充分了解这种情况,应该包括对所讨论的两个功能的完全拆卸,特别是因为在这种情况下拆卸很方便。
main()从 prog1完全反汇编:
00000000004015b0 <main>:
4015b0: 55 push %rbp // save caller's frame base address
4015b1: 48 89 e5 mov %rsp,%rbp // current top of stack becomes base of caller's frame
4015b4: 48 83 ec 20 sub $0x20,%rsp // allocate space for new stack frame
4015b8: e8 93 01 00 00 callq 401750 <__main>
4015bd: b8 00 00 00 00 mov $0x0,%eax
4015c2: 48 83 c4 20 add $0x20,%rsp // callee stack frame now out of scope
4015c6: 5d pop %rbp // restore caller's base frame pointer
4015c7: c3 retq
4015c8: 90 nop
4015c9: 90 nop
4015ca: 90 nop
4015cb: 90 nop
4015cc: 90 nop
4015cd: 90 nop
4015ce: 90 nop
4015cf: 90 nop
main()从 prog2完全反汇编:
00000000004015b0 <main>:
4015b0: 55 push %rbp
4015b1: 48 89 e5 mov %rsp,%rbp
4015b4: 48 83 ec 30 sub $0x30,%rsp
4015b8: e8 a3 01 00 00 callq 401760 <__main>
4015bd: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp)
4015c4: c7 45 f8 06 00 00 00 movl $0x6,-0x8(%rbp)
4015cb: 8b 55 fc mov -0x4(%rbp),%edx
4015ce: 8b 45 f8 mov -0x8(%rbp),%eax
4015d1: 01 d0 add %edx,%eax
4015d3: 48 83 c4 30 add $0x30,%rsp
4015d7: 5d pop %rbp
4015d8: c3 retq
4015d9: 90 nop
4015da: 90 nop
4015db: 90 nop
4015dc: 90 nop
4015dd: 90 nop
4015de: 90 nop
4015df: 90 nop
这段代码未经优化,这使得解释变得更加容易,并使我们能够清楚地辨别所遵循的调用约定。
根据微软关于函数类型的文档,
基本上有两种类型的功能。需要堆栈帧的函数称为帧函数。不需要堆栈帧的函数称为叶函数。
帧函数是分配堆栈空间、调用其他函数、保存非易失性寄存器或使用异常处理的函数。它还需要一个函数表条目。一个框架函数需要一个序言和一个尾声。帧函数可以动态分配堆栈空间并可以使用帧指针。一个框架函数拥有这个调用标准的全部功能。
如果框架函数不调用另一个函数,则不需要对齐堆栈。
上面的函数都调用__main,所以它们会被认为是框架函数。因此,main正如我们从上面的反汇编中看到的那样,在这两种情况下都分配了一个堆栈帧。
根据微软关于堆栈分配的文档,
堆栈将始终保持 16 字节对齐,除了在 prolog 内(例如,在返回地址被压入之后),并且除了在函数类型中为特定类别的帧函数指明的地方。
这让我们回到最初的问题:
为什么这个程序分配了 48 个字节而不是第一个程序的 32 个字节?
直接回答这个问题并不容易。简短的回答是堆栈帧对齐 + 编译器的变幻莫测。
编译器负责堆栈内存分配(除非alloca在运行时用于动态分配堆栈上的内存,但这不适用于有问题的反汇编)。需要注意的是,main()在程序 1的反汇编中,创建的堆栈帧中没有保存任何内容(除了在调用时%rip被压入堆栈的 32 字节堆栈帧__main),但无论如何分配了 32 字节。
在程序 2 中,__main也被调用,另外 2 个 4 字节整数值被写入为main().
这种“额外”内存的分配通常由 GCC 完成——堆栈帧在 64 位和 32 位运行时环境中没有尽可能紧凑地分配。System V ABI AMD64 Architecture Processor Supplement Draft Version 0.99.7第 3.2.1 节“寄存器和堆栈帧”中提供了有关为什么会出现这种情况的线索:

具体来说,“线索”是当前堆栈帧中的“未指定”区域。这个区域是不是在AMD64 ABI进一步讨论,但它是在讨论System V的ABI,英特尔386架构处理器补充部分3“低层系统信息”,第37页:
其他方面取决于编译器和正在编译的代码。标准调用序列不定义最大堆栈帧大小,也不限制语言系统如何使用标准堆栈帧的“未指定”区域。
如果这也扩展到 AMD64 架构,则意味着编译器可以在函数堆栈帧基指针%rbp和“红色区域”之间的函数堆栈帧区域中以编译器本身确定的方式分配内存,因此只要堆栈正确对齐到 16 字节边界。换句话说,这个区域的内存分配是依赖于编译器的。例如,MSVC 可能会为堆栈帧的这个区域以不同的方式分配内存。更具体地说,不同的编译器可能不会main()像 GCC 那样为程序 2 的函数分配 48 个字节。
更新(感谢 Igor Skochinksy 指出这一点):
MS 文档并不完全正确;叶函数是一个不调用其他函数的函数(它是函数调用图上的叶);它可能会或可能不会使用堆栈帧(在 MS ABI 中它没有,但在其他地方不一定如此)。
除此之外,AMD64 ABI 和 Windows x64 ABI 之间还有一些重要的区别:
x64 上的 Windows 实现了自己的 ABI,这与 AMD64 ABI 有点不同……主要区别如下:
- 只有 4 个整数/指针参数在寄存器(rcx、rdx、r8、r9)中传递。
- 没有任何“红区”的概念。事实上,ABI 明确指出 rsp 以外的区域被认为是不稳定的,使用起来不安全。操作系统、调试器或中断处理程序可能会覆盖该区域。
- 相反,调用者在每个堆栈帧中提供了一个“寄存器参数区域”(有时也称为“主空间”)。当一个函数被调用时,在返回地址之前栈上分配的最后一个东西是至少有 4 个寄存器的空间(每个 8 字节)。该区域可供被调用者使用,无需明确分配。它对于可变参数函数以及调试(为参数提供已知位置,而寄存器可重用于其他目的)很有用。尽管该区域最初是为了溢出寄存器中传递的 4 个参数而设计的,但如今编译器也将其用于其他优化目的(例如,如果函数需要少于 32 字节的堆栈空间用于其局部变量,则该区域可能在不接触 rsp 的情况下使用)。
所有这些点都很重要,但我强调第 3 点的第一句话的原因是,它特别与我们讨论的空间如何由编译器分配给堆栈帧的讨论有关。
main() 在程序 1 中没有传递给它的局部变量或参数,但它确实有一个 32 字节(4 个寄存器 * 8 个字节)的寄存器参数区域。
main()在程序 2 中,局部变量使用了 8 字节的空间,因此为其堆栈帧加上 32 字节的寄存器参数区域分配了 16 字节的空间。为局部变量分配了 16 个字节,以保持与 16 字节边界的对齐。
这说明了main()在程序 1 和程序 2 中创建的堆栈帧之间的大小差异。