Gcc 理解堆栈对齐强制
考虑以下C代码:Gcc 理解堆栈对齐强制,gcc,assembly,x86,memory-alignment,abi,Gcc,Assembly,X86,Memory Alignment,Abi,考虑以下C代码: #include <stdint.h> void func(void) { uint32_t var = 0; return; } 根据System V ABI的堆栈对齐要求,在每次调用指令之前,堆栈必须对齐16字节(如果未使用-mprefered stack boundary选项更改,则默认情况下堆栈边界为16字节)。因此,在函数调用之前,ESP模16的结果必须为零 考虑到这些堆栈对齐要求,我假设在执行leave指令之前,以下堆栈的状态表示是正确的
#include <stdint.h>
void func(void) {
uint32_t var = 0;
return;
}
根据System V ABI的堆栈对齐要求,在每次调用
指令之前,堆栈必须对齐16字节(如果未使用-mprefered stack boundary
选项更改,则默认情况下堆栈边界为16字节)。因此,在函数调用之前,ESP
模16的结果必须为零
考虑到这些堆栈对齐要求,我假设在执行leave
指令之前,以下堆栈的状态表示是正确的:
Size (bytes) Stack ESP mod 16 Description
-----------------------------------------------------------------------------------
| . . . |
------------------........0 at func call
4 | return address |
------------------.......12 at func entry
4 | saved EBP |
----> ------------------........8 EBP is pointing at this address
| 4 | var |
| ------------------........4
16 | | |
| 12 | |
| | |
----> ------------------........8 after allocating 16 bytes
考虑到堆栈的这种表示,有两点让我感到困惑:
var
在堆栈上显然没有对齐到16字节。这个问题似乎与我读到的内容相矛盾(重点是我自己):
-mprefered stack boundary=n
编译器尝试将堆栈上的项对齐到2^n
在我的例子中,没有提供-mpreferenced stack boundary
,因此根据(我得到的结果与-mpreferenced stack boundary=4
)默认设置为4(即:2^4=16字节边界)subl$16,%esp
指令)而不是只分配8个字节的目的是:分配16个字节后,堆栈不会按16个字节对齐,也不会留出任何内存空间。通过只分配8个字节,堆栈将对齐16个字节,并且不会浪费额外的8个字节这个回答的目的是进一步发展上面写的一些评论
首先,基于s,考虑最初发布的<代码>函数()/代码>函数的以下修改:
#include <stdint.h>
void bar(void);
void func(void) {
uint32_t var = 0;
bar(); // <--- function call
return;
}
请注意,指令subl$24,%esp
将堆栈对齐16个字节(原始func()
函数中的subl$16,%esp
指令没有对齐堆栈)
由于重新定义的func()
现在包含函数调用(即:调用栏
),因此在执行调用
指令之前,堆栈必须对齐16个字节。前面的func()
根本没有调用函数,因此堆栈不需要按16字节对齐
显然,必须在堆栈上为
var
变量分配至少4个字节。为了将堆栈对齐16个字节,需要额外分配4个字节
有人可能会问,为什么要分配24个字节来对齐堆栈,而只分配8个字节就可以了。好吧,通过解释部分的,这个问题也得到了回答:
还要记住,C编译器没有义务以任何形式生成最佳代码,包括堆栈空间使用。虽然它会努力尝试(在godbolt上玩GCC4.7.2看起来不错,垃圾空间只是对齐的结果),但如果它失败并分配比实际需要多16B的垃圾(特别是在未优化的代码中),则不会出现语言破坏问题
查看生成的机器代码通常是徒劳的。编译器将以最简单的方式发出任何有效的消息。这通常会导致奇怪的人工制品
堆栈对齐仅指堆栈框架的对齐。它与堆栈上对象的对齐没有直接关系。GCC将在堆栈对象上分配所需的对齐方式。如果GCC知道堆栈帧已经提供了足够的对齐,那么这会更简单,但如果GCC不知道,它将使用帧指针并执行显式对齐。这与C无关,与“x86”上的“System V ABI”有很大关系编译后的机器代码。@Sebivor您是否建议我编辑标记并选择?我被限制为5个标签。好吧,作为源代码,你提供了一些非常基本的东西,它可以移植到几乎任何语言,并生成相同的机器代码,所以。。。我建议您删除C标记,或者在n1570中找到一些引用,其中提到了“堆栈对齐”和“系统V ABI”…请参见:
-m-preferred-stack-boundary
不对齐单个变量。请参见第二点。同时请记住,C编译器没有义务以任何形式生成最佳代码,包括堆栈空间使用。虽然它会努力尝试(在godbolt上玩GCC4.7.2看起来不错,垃圾空间只是对齐的结果),但如果它失败并分配比实际需要多16B的垃圾(特别是在未优化的代码中),则不会出现语言破坏问题。它所遵循的(由于平台特定的选项)是在下一次调用指令时正确对齐esp
。从C语言的角度来看,甚至堆栈的存在也不是强制性的,也不是一些对齐方式。在你的最后一句话中,你是指典型的和l$-16,%esp
,以确保堆栈正确对齐到16字节?(通过ebp
保存原始的esp
)。是的,这是一种方法。但是默认情况下,GCC不会这样做,因为它假设堆栈已经对齐,您需要传递一个选项,如-mrealignstack
,GCC只会在需要时这样做。谢谢,我得到了它。属性force\u align\u arg\u指针也适用于单个函数
使编译器仍然使用-O3
进行存储。看-O0
代码是愚蠢的;它甚至都没有试图达到最佳状态。或者在没有volatile的情况下,强制编译器在函数调用中保存某些内容是另一种使用堆栈空间的方法。(对于寄存器args(如64位代码或regparm调用约定),在调用无法看到的函数后使用函数arg,就像您在这里使用bar()
)。实际的
#include <stdint.h>
void bar(void);
void func(void) {
uint32_t var = 0;
bar(); // <--- function call
return;
}
func:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
movl $0, -12(%ebp)
call bar
nop
leave
ret