【问题标题】:Responsibility of stack alignment in 32-bit x86 assembly32 位 x86 汇编中堆栈对齐的责任
【发布时间】:2016-10-28 14:22:39
【问题描述】:

我试图清楚地了解谁(调用者或被调用者)负责堆栈对齐。 64 位汇编的情况相当清楚,它是由 caller 完成的。

参考 System V AMD64 ABI,第 3.2.2 节 堆栈框架

输入参数区域的结尾应对齐在 16(32,如果 __m256 在堆栈上传递)字节边界。

换句话说,应该安全地假设,对于被调用函数的每个入口点:

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\n`, 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 之前是否需要对齐堆栈?如果是这样,那么在将这两个参数推送到scanf 之前,这需要将%esp 减少四个字节:

4 bytes (return address)
4 bytes (%ebp of previous stack frame)
8 bytes (for two variables)
12 bytes (three arguments for scanf)
= 28

【问题讨论】:

  • 看起来像 and esp, 0xfffffff0 的 16 字节对齐代码通常会添加到 main 的模板代码中。但是对于每个其他函数,对齐都是由调用函数维护的。我应该指出,对于 32 位代码,您应该遵循 System V i386 ABI。是的,您需要在调用 scanf 等函数之前保持堆栈 16 字节对齐
  • x86 堆栈必须仅对齐 4 字节(通用寄存器大小)。所以通常不需要为堆栈对齐执行特殊任务。在 x64 中 - 这是调用者在调用前对 16*x 上的堆栈对齐负责
  • @RbMm:据我了解,Linux 上 x86 堆栈的对齐要求有所提高。当前要求为 16 个字节(如果一个通过 _m256 参数,则为 32 个字节)。 ABI 的第 2.2.2 节现在包含这个短语:The end of the input argument area shall be aligned on a 16 (32, if __m256 is passed on stack) byte boundary.
  • @GrzegorzSzpetkowski - 我对 Linux 一无所知,但对于处理器视图和 Windows 操作系统 - 32 位堆栈 enouhg 的 4 字节对齐
  • @RbMm:您对 Windows 的看法是正确的。我应该指定有问题的操作系统。

标签: linux gcc assembly x86 memory-alignment


【解决方案1】:

GCC onlymain 中进行这种额外的堆栈对齐;该功能很特殊。如果您查看任何其他功能的代码生成,您将看不到它,除非您有一个带有alignas(32) 或其他东西的本地。

GCC 只是对-m32 采取了一种防御方法,而不是假设main 是使用正确的16B 对齐堆栈调用的。或者这种特殊待遇是在 -mpreferred-stack-boundary=4 只是一个好主意而不是法律时遗留下来的。

多年来,i386 System V ABI 一直保证/要求 ESP+4 在进入函数时与 16B 对齐。 (即 ESP 必须在 CALL 指令之前 对齐 16B,因此堆栈上的 args 从 16B 边界开始。这与 x86-64 System V 相同。)

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 假定堆栈在进入 main 时正确对齐。 (clang 也 assumes that narrow args have been sign or zero-extended to 32 bits,尽管当前的 ABI 版本没有指定该行为(还)。gcc 和 clang 都发出在调用方执行的代码,但只有 clang 在被调用方中依赖它。这发生在 64 -bit 代码,但我没有检查 32 位。)

如果您好奇,请查看 http://gcc.godbolt.org/ 上的编译器输出,了解 main 和 main 以外的函数。


前几天我刚刚更新了 标签 wiki 中的 ABI 链接。 http://x86-64.org/ 仍然死了,似乎不会回来,所以我更新了 System V 链接以指向 HJ Lu 的 github 存储库中当前修订版的 PDF 和 his page with links

请注意,last version on SCO's site 不是当前版本,并且不包括 16B 堆栈对齐要求。

我认为一些 BSD 版本仍然不需要/维护 16 字节堆栈对齐。

【讨论】:

  • IDK 为什么这被否决了。更新以防仅回答问题的前半部分>.
猜你喜欢
  • 1970-01-01
  • 2011-11-04
  • 2014-03-11
  • 2017-01-04
  • 2020-12-04
  • 2023-01-03
  • 2014-08-04
  • 1970-01-01
  • 2017-10-20
相关资源
最近更新 更多