32 位 x86 汇编中堆栈对齐的职责
- 2024-11-04 08:43:00
- admin 原创
- 66
问题描述:
我试图弄清楚谁(调用者或被调用者)负责堆栈对齐。64 位汇编的情况相当清楚,它由调用者负责。
参照 System V AMD64 ABI,第 3.2.2 节“堆栈框架”:
输入参数区域的末尾应与 16(如果 __m256 在堆栈上传递,则为 32)字节边界对齐。
换句话说,可以安全地假设,对于被调用函数的每个入口点:
16 | (%rsp + 8)
保持(额外的八个是因为call
隐式地将返回地址推送到堆栈上)。
在 32 位世界中看起来如何(假设是 cdecl)?我注意到gcc
将对齐放在调用函数内部,其结构如下:
and esp, -16
这似乎表明,这是被调用者的责任。
为了更清楚起见,请考虑以下 NASM 代码:
global main
extern printf
extern scanf
section .rodata
s_fmt db "%d %d", 0
s_res db `%d with remainder %d
`, 0
section .text
main:
start 0, 0
sub esp, 8
mov DWORD [ebp-4], 0 ; dividend
mov DWORD [ebp-8], 0 ; divisor
lea eax, [ebp-8]
push eax
lea eax, [ebp-4]
push eax
push s_fmt
call scanf
add esp, 12
mov eax, [ebp-4]
cdq
idiv DWORD [ebp-8]
push edx
push eax
push s_res
call printf
xor eax, eax
leave
ret
在调用之前是否需要对齐堆栈?如果是这样,那么在将这两个参数推送到之前,scanf
需要减少四个字节:%esp
`scanf`
4 bytes (return address)
4 bytes (%ebp of previous stack frame)
8 bytes (for two variables)
12 bytes (three arguments for scanf)
= 28
解决方案 1:
GCC仅在 中进行额外的堆栈对齐main
;该函数很特殊。 如果您查看任何其他函数的代码生成,您将看不到它,除非您有本地的 withalignas(32)
或类似的东西。
GCC 只是采取了一种防御性的方法-m32
,不假设main
使用正确 16B 对齐的堆栈调用。或者这种特殊处理是从-mpreferred-stack-boundary=4
一个好主意而不是法律1遗留下来的。
多年来,i386 System V ABI 一直保证/要求 ESP+4 在进入函数时是 16B 对齐的。(即,ESP在 CALL 指令之前必须是 16B 对齐的,因此堆栈上的参数从 16B 边界开始。这与 x86-64 System V 相同。) ESP % 16 == 0
在调用之前、ESP % 16 == 12
在函数进入时、在调用之后。
ABI 还保证新的 32 位进程以在 16B 边界上对齐的 ESP 启动(例如_start
,在 ELF 入口点,其中 ESP 指向 argc,而不是返回地址),并且 glibc CRT 代码保持该对齐。
就调用约定而言,EBP 只是另一个调用保留寄存器。但是,是的,编译器输出-fno-omit-frame-pointer
确实会在其他调用保留寄存器(如 EBX)之前进行处理push ebp
,因此保存的 EBP 值会形成一个链接列表。(因为它还负责mov ebp, esp
在推送之后设置帧指针。)
也许 gcc 是防御性的,因为非常古老的 Linux 内核(从 i386 ABI 修订版之前开始,当时所需的对齐只有 4B)可能会违反该假设,并且它只是在进程的生命周期中运行一次的额外几个指令(假设程序不递归main
调用)。
与 gcc 不同,clang 假定堆栈在进入主函数时正确对齐。(clang 还假定窄参数已被符号或零扩展为 32 位,即使当前 ABI 修订版尚未指定该行为。gcc 和 clang 都发出在调用方执行的代码,但只有 clang 在被调用方依赖于它。这发生在 64 位代码中,但我没有检查 32 位。)
如果您好奇的话,请查看http://gcc.godbolt.org/上的 main 函数和除 main 之外的其他函数的编译器输出。
我刚刚更新了x86标签 wiki。http: //x86-64.org/仍然处于死机状态并且似乎不会回来,因此我更新了 System V 链接以指向 HJ Lu 的 github repo 中当前修订版的 PDF,以及他的带有链接的页面。
请注意,SCO 网站上的最新版本不是当前修订版,并且不包括 16B 堆栈对齐要求。
ABI 从 4 字节对齐变为 16 字节对齐的历史
脚注 1:向 i386 SysV ABI 添加 16 字节对齐要求纯属意外;GCC 出于性能原因维持了 16 字节对齐(例如,8 字节double
永远不会跨越缓存行边界)。
另请参阅我的答案底部关于为什么 x86-64 / AMD64 System V ABI 要求 16 字节堆栈对齐?的部分以获取更多详细信息。
在某些 GCC 版本中,SSE/SSE2 代码生成开始使用movaps
溢出/重新加载__m128
变量到堆栈,而无需手动对齐传入的 ESP。这使调整选择成为一项要求,但直到带有此类代码的库在一些长期稳定的 Linux 发行版中广泛部署时才被发现。
面对这一选择,GCC 开发人员/ABI 维护人员选择了最不坏的路径,将其作为官方要求。这破坏了调用其他函数的现有手写 asm。
请参阅https://sourceforge.net/p/fbc/bugs/659/了解一些历史记录,以及我在https://gcc.gnu.org/bugzilla/show_bug.cgi?id=40838#c91上的评论,尝试总结 i386 GNU/Linux + GCC 意外陷入这样一种境地的不幸历史:对 i386 System V ABI 进行向后不兼容的更改是两害相权取其轻。
大多数 BSD 版本和 i386 MacOS 未采用此 ABI 更改,并且仍然不需要16字节堆栈对齐。GCC 可能默认为-mpreferred-stack-boundary=4
这些目标,但代码生成alignas(16) char buf[16];
(或__m128
从 regs 溢出的本地变量)需要手动对齐函数内的 ESP,以防它一开始就不是。
因此,从 4 字节对齐到 16 字节对齐的改变实际上针对的是 Linux,而不是其他操作系统。这可能是简化 GCC 源代码并始终为main
32 位目标包含额外堆栈对齐代码的另一个原因。目前,Linux 的 32 位 x86 已经过时,现在不值得改变。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理必备:盘点2024年13款好用的项目管理软件