编译器为进程运行时堆栈上的函数堆栈帧分配了多少空间涉及多个因素:
- 在堆栈帧中保存函数的参数副本所需的空间
- 在堆栈帧中存储局部变量所需的空间
- 堆栈对齐到 16 字节边界(i386 架构的 GCC 默认值)
背景
i386 ABI
堆栈帧规范
x86 机器上堆栈帧的规范在System V 应用程序二进制接口 Intel386 架构处理器增补第四版的第 3 章:“低级系统信息”中给出,标题为“函数调用序列”部分。
笔记:
在本规范中,术语半字指的是 16 位对象,术语字指的是 32 位对象,术语双字指的是 64 位对象。
以下是相关摘录:
堆栈是字对齐的。尽管该体系结构不需要任何堆栈对齐,但软件约定和操作系统要求堆栈在字边界上对齐。
如有必要,增加参数的大小以使其成为单词的倍数。这可能需要尾部填充,具体取决于参数的大小。
其他方面取决于编译器和正在编译的代码。标准调用序列不定义最大堆栈帧大小,也不限制语言系统如何使用标准堆栈帧的“未指定”区域。
堆栈帧中的“未指定”区域是为局部变量创建的空间以及函数参数被复制到的空间。这个空间由编译器管理。
这是来自 ABI 的图表:
结盟
管理堆栈帧的是编译器,为了对齐堆栈帧,还必须知道堆栈帧内变量的对齐方式。
变量的对齐方式取决于它们的类型和 CPU 的架构。这也在 ABI 中指定:
有一些约定特别适用于数组、结构和联合的对齐:
聚合(结构和数组)和联合假定其最严格对齐的组件对齐。任何对象(包括聚合和联合)的大小始终是对象对齐的倍数。数组使用与其元素相同的对齐方式。结构和联合对象可能需要填充以满足大小和对齐约束。任何填充的内容都是未定义的。
但是,在 i386 架构系统上,GCC 默认将堆栈对齐到 16 字节的边界:
-mpreferred-stack-boundary=num
尝试将堆栈边界与 2 提升到 num 字节边界对齐。如果-mpreferred-stack-boundary
未指定,则默认值为 4(16 字节或 128 位)。
这意味着编译器会在堆栈帧上为类型大小小于 16 字节的变量分配 16 字节的空间。例如,即使int
i386 系统上的an是 4 个字节,编译器仍会在堆栈帧上为其分配 16 个字节的空间。
易受攻击的函数()的堆栈帧
让我们用两个简单的例子来分析编译器如何在函数的堆栈帧上分配空间:一个带有char
指针局部变量的函数和一个带有 100 字节char
数组的函数。
pointer_test
使用char
指针局部变量调用的函数:
void pointer_test(void) {
char *i = "test";
}
gcc
+生成的汇编代码as
:
Dump of assembler code for function pointer_test:
0x080483db <+0>: push %ebp
0x080483dc <+1>: mov %esp,%ebp
0x080483de <+3>: sub $0x10,%esp <-- 16 bytes of space created for 4-byte pointer
0x080483e1 <+6>: movl $0x8048480,-0x4(%ebp)
0x080483e8 <+13>: nop
0x080483e9 <+14>: leave
0x080483ea <+15>: ret
这里我们看到为 4 字节指针分配了 16 字节的空间。
char_array_test
使用 char 数组局部变量调用的函数:
void char_array_test(void) {
char buffer[100];
}
gcc
+生成的汇编代码as
:
Dump of assembler code for function char_array_test:
0x0804844b <+0>: push %ebp
0x0804844c <+1>: mov %esp,%ebp
0x0804844e <+3>: sub $0x78,%esp <-- 120 bytes of space created for 100-byte array
0x08048451 <+6>: mov %gs:0x14,%eax
0x08048457 <+12>: mov %eax,-0xc(%ebp)
0x0804845a <+15>: xor %eax,%eax
0x0804845c <+17>: nop
0x0804845d <+18>: mov -0xc(%ebp),%eax
0x08048460 <+21>: xor %gs:0x14,%eax
0x08048467 <+28>: je 0x804846e <char_array_test+35>
0x08048469 <+30>: call 0x8048310 <__stack_chk_fail@plt>
0x0804846e <+35>: leave
0x0804846f <+36>: ret
这里我们看到为 100 字节的数组分配了 120 字节的空间。
在 的情况下void vulnerable_function(char *string)
,必须gcc
为 4 字节指针和 100 字节数组分配堆栈帧中的空间。
- 正如我们在上面 中观察到的
pointer_test()
,由于gcc
默认情况下将分配的空间与 16 字节边界对齐,因此还在堆栈帧上为 4 字节指针char *string
(函数的参数)分配了 16 字节的空间。
- 我们在上面观察到
char_array_test()
的是gcc
分配120个字节的空间为100字节阵列(120不是16的倍数,所以这不是一个16字节边界对齐。我不知道为什么编译器这一点)。同样,编译器为char buffer[100]
in分配了 120 字节的空间vulnerable_function()
。
0x10 字节string
+ 0x78 字节buffer[100]
= 0x88
资源
Compiler Explorer是一个在浏览器中运行的交互式编译器。使用它比不断地重新编译和反汇编代码要快得多。
System V 应用二进制接口 Intel386 架构处理器增补,第四版
GCC 的 Intel 386 和 AMD x86-64 选项
cdecl
和 x86 调用约定讨论了 x86 编译器中的调用约定
Poke-a-hole and Friends是一篇文章,讨论了如何填充结构以保持对齐以及这如何跨架构发生变化。
相关的问题
堆栈分配、填充和对齐
什么是“堆栈对齐”?