【问题标题】:Understanding stack alignment enforcement了解堆栈对齐强制
【发布时间】:2017-11-21 10:45:56
【问题描述】:

考虑以下 C 代码:

#include <stdint.h>

void func(void) {
   uint32_t var = 0;
   return;
}

GCC 4.7.2 为上述代码生成的未优化(即:-O0 选项)汇编代码为:

func:
    pushl %ebp
    movl %esp, %ebp
    subl $16, %esp
    movl $0, -4(%ebp)
    nop
    leave
    ret

根据System V ABI堆栈对齐要求,堆栈必须在每条call指令之前对齐16个字节(堆栈边界 默认为 16 个字节,当不使用选项 -mpreferred-stack-boundary 更改时)。因此,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

考虑到堆栈的这种表示,有两点让我感到困惑:

  1. var 显然没有在堆栈上对齐到 16 个字节。这个问题似乎与我从in this answerthis question 阅读的内容相矛盾(重点是我自己的):

    -mpreferred-stack-boundary=n,其中编译器尝试将堆栈上的项目保持对齐到 2^n

    在我的情况下,-mpreferred-stack-boundary 没有提供,因此根据this section of GCC's documentation,它默认设置为 4(即:2^4=16 字节边界)(我确实得到了与-mpreferred-stack-boundary=4 相同的结果) .

  2. 在堆栈上分配 16 个字节(即:subl $16, %esp 指令)而不是仅分配 8 个字节的目的:在分配 16 个字节后,堆栈既没有按 16 个字节对齐,也没有节省任何内存空间。通过仅分配 8 个字节,堆栈将对齐 16 个字节,不会浪费额外的 8 个字节。

【问题讨论】:

  • 这与 C 关系不大,与“x86”后编译为机器码的“System V ABI”关系很大。
  • @Sebivor 您是否建议我编辑标签并选择abi 而不是c?我仅限于 5 个标签。
  • 好吧,作为源代码,您提供了一些非常基本的东西,它可以移植到几乎任何语言并生成相同的机器代码,所以...我建议您删除 C标签,或者在 n1570 中找到一些引用,其中提到了诸如“堆栈对齐”和“System V ABI”之类的东西......
  • 参见this-m-preferred-stack-boundary 不对齐单个变量。第二点见this
  • 另外请记住,C 编译器没有义务以任何指标生成最佳代码,包括堆栈空间使用情况。虽然它会努力(并且在 godbolt 上使用 gcc 4.7.2 看起来不错,垃圾空间只是对齐的结果),但如果它失败并分配比实际需要多 16B 的垃圾,则没有语言破坏问题(尤其是在未优化的代码中)。它遵循的(由于 platform specific 选项)是在下一个call 指令时正确对齐esp。从 C 语言的角度来看,即使堆栈存在也不是强制性的,也不是必须对齐的。

标签: gcc assembly x86 memory-alignment abi


【解决方案1】:

查看-O0 生成的机器代码通常是徒劳的。编译器将以最简单的方式发出任何工作。这通常会导致奇怪的伪影。

堆栈对齐仅指堆栈帧的对齐。它与堆栈上对象的对齐方式没有直接关系。 GCC 将分配具有所需对齐的堆栈对象。如果 GCC 知道堆栈帧已经提供了足够的对齐,这会更简单,但如果没有,GCC 将使用帧指针并执行显式对齐。

【讨论】:

  • 在你的最后一句中,你的意思是典型的andl $-16, %esp 以确保堆栈正确对齐到 16 个字节吗? (通过ebp 保留原来的esp)。
  • 是的,这是一种方法。但是 GCC 默认不会这样做,因为它假定堆栈已经对齐,你需要传递一个像 -mrealignstack 这样的选项,而 GCC 只会在需要时这样做。
  • 谢谢,我知道了。属性force_align_arg_pointer 也适用于个别功能。
【解决方案2】:

这个答案旨在进一步发展上面写的一些cmet。


首先,基于Margaret Bloomcomment,考虑对原贴的func()函数进行如下修改:

#include <stdint.h>

void bar(void);    

void func(void) {
   uint32_t var = 0;
   bar(); // <--- function call
   return;
}

与原来的func() 函数不同,重新定义的函数包含对bar()函数调用

这次生成的汇编代码是:

func:
    pushl %ebp
    movl %esp, %ebp
    subl $24, %esp
    movl $0, -12(%ebp)
    call bar
    nop
    leave
    ret

请注意,指令 subl $24, %esp 确实将堆栈对齐 16 个字节(原始 func() 函数中的 subl $16, %esp 指令没有)。

由于重新定义的 func() 现在包含一个函数调用(即:call bar),因此在执行 call 指令之前,堆栈必须对齐 16 个字节。之前的func()根本没有调用任何函数,因此栈不需要16字节对齐。


很明显,至少必须在堆栈上为var 变量分配4 个字节。需要分配 4 个额外字节才能将堆栈对齐 16 个字节。

有人可能会问为什么要分配 24 字节为了对齐堆栈,而只分配 8 字节 就可以了。嗯,通过解释Ped7gcomment的一部分,这个问题也得到了回答:

另外请记住,C 编译器没有义务以任何指标生成最佳代码,包括堆栈空间使用情况。虽然它会努力(并且在 godbolt 上使用 gcc 4.7.2 看起来不错,垃圾空间只是对齐的结果),但如果它失败并分配比实际需要多 16B 的垃圾,则没有语言破坏问题(尤其是在未优化的代码中)。

【讨论】:

  • 使用volatile int var = 1; 让编译器仍然使用-O3 进行存储。看-O0 代码很傻;它甚至没有试图达到最佳状态。或者没有volatile,强制编译器在函数调用中保存一些东西是另一种让它使用堆栈空间的方法。 (使用寄存器 args(如在 64 位代码中,或使用 regparm 调用约定),在调用它看不到的函数后使用函数 arg,就像您在此处使用 bar() 所做的那样)。实际上,nvm 将推送/弹出 ebxrbx 并将值保留在那里。我在想void foo(int a) { bar(); return a+1; },但是 NVM。
  • 对齐到32。不要小看返回地址,保存的ebp,栈canary。现代 C 编译器倾向于对齐到 16 或 32,因此它们可以使用 SSE2 或 AVX 生成最佳 SIMD 代码。
  • @HansPassant AFAIK,在执行调用时,堆栈与 16 对齐。然后,返回地址和 ebp 寄存器都被压入堆栈。之后,在堆栈上分配 24 个字节。总的来说,esp 减少了 32。但是,从 16 字节对齐的地址中减去 32 并不一定会导致地址与 32 字节对齐。你怎么知道它是对齐到 32 字节的?
  • @HansPassant 当然,如果对齐到 32 也会对齐到 16,因为后者要求较弱。
猜你喜欢
  • 2018-07-18
  • 2011-06-16
  • 2015-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-14
  • 2015-01-08
相关资源
最近更新 更多