gcc x86 Windows堆栈对齐

gcc x86 Windows stack alignment

本文关键字:对齐 堆栈 Windows x86 gcc      更新时间:2023-10-16

我正在编写一个纯粹作为学习经验的编译器。我目前正在通过编译简单的c++代码来学习堆栈框架,然后研究gcc 4.9.2为Windows x86生成的输出asm。

我的简单c++代码是

#include <iostream>
using namespace std;
int globalVar;
void testStackStuff(void);
void testPassingOneInt32(int v);
void forceStackFrameCreation(int v);
int main()
{
  globalVar = 0;
  testStackStuff();
  std::cout << globalVar << std::endl;
}
void testStackStuff(void)
{
  testPassingOneInt32(666);
}
void testPassingOneInt32(int v)
{
  globalVar = globalVar + v;
  forceStackFrameCreation(v);
}
void forceStackFrameCreation(int v)
{
  globalVar = globalVar + v;
}

好的,当用-mreferred堆栈边界=4编译时,我希望看到一个对齐到16字节的堆栈(从技术上讲,它对齐到16个字节,但有额外的16个字节的未使用堆栈空间(。gcc生成的main的序言是:

22                      .loc 1 12 0
23                      .cfi_startproc
24 0000 8D4C2404        lea ecx, [esp+4]
25                      .cfi_def_cfa 1, 0
26 0004 83E4F0          and esp, -16
27 0007 FF71FC          push    DWORD PTR [ecx-4]
28 000a 55              push    ebp
29                      .cfi_escape 0x10,0x5,0x2,0x75,0
30 000b 89E5            mov ebp, esp
31 000d 51              push    ecx
32                      .cfi_escape 0xf,0x3,0x75,0x7c,0x6
33 000e 83EC14          sub esp, 20
34                      .loc 1 12 0
35 0011 E8000000        call    ___main
35      00
36                      .loc 1 13 0
37 0016 C7050000        mov DWORD PTR _globalVar, 0
38                      .loc 1 15 0
39 0020 E8330000        call    __Z14testStackStuffv

第26行将esp向下舍入到最近的16字节边界。

第27、28和31行将总共12个字节推送到堆栈上,然后是

第33行从esp中再减去20个字节,总共得到32个字节!

为什么?

第39行调用testStackStuff。

注意-此调用推送返回地址(4个字节(。

现在,让我们看一下testStackStuff的序言,记住堆栈现在离下一个16字节的边界更近了4个字节。

67 0058 55              push    ebp
68                      .cfi_def_cfa_offset 8
69                      .cfi_offset 5, -8
70 0059 89E5            mov ebp, esp
71                      .cfi_def_cfa_register 5
72 005b 83EC18          sub esp, 24
73                      .loc 1 22 0
74 005e C704249A        mov DWORD PTR [esp], 666

第67行将另外4个字节(现在是8个字节(推向边界。

第72行减去另外24个字节(总共32个字节(。

此时,堆栈已在16字节边界上正确对齐。但是为什么是2的倍数呢?

如果我将编译器标志更改为-mreferred stack boundary=5,我希望堆栈对齐到32字节,但gcc似乎再次生成对齐到64字节的堆栈帧,是我预期的两倍。

主要的序言

23                      .cfi_startproc
24 0000 8D4C2404        lea ecx, [esp+4]
25                      .cfi_def_cfa 1, 0
26 0004 83E4E0          and esp, -32
27 0007 FF71FC          push    DWORD PTR [ecx-4]
28 000a 55              push    ebp
29                      .cfi_escape 0x10,0x5,0x2,0x75,0
30 000b 89E5            mov ebp, esp
31 000d 51              push    ecx
32                      .cfi_escape 0xf,0x3,0x75,0x7c,0x6
33 000e 83EC34          sub esp, 52
34                      .loc 1 12 0
35 0011 E8000000        call    ___main
35      00
36                      .loc 1 13 0
37 0016 C7050000        mov DWORD PTR _globalVar, 0
37      00000000 
37      0000
38                      .loc 1 15 0
39 0020 E8330000        call    __Z14testStackStuffv

第26行将esp向下舍入到最近的32字节边界

第27、28和31行将总共12个字节推送到堆栈上,然后是

第33行从esp中再减去52个字节,总共得到64个字节!

testStackStuff的序言是

66                      .cfi_startproc
67 0058 55              push    ebp
68                      .cfi_def_cfa_offset 8
69                      .cfi_offset 5, -8
70 0059 89E5            mov ebp, esp
71                      .cfi_def_cfa_register 5
72 005b 83EC38          sub esp, 56
73                      .loc 1 22 0

(堆栈上的4个字节来自(调用__Z14testStackStuff

(堆栈上的4个字节来自(推送ebp

(堆栈上的56字节来自(sub-esp,56

总共64个字节。

有人知道gcc为什么要创建这个额外的堆栈空间吗?或者我忽略了一些显而易见的东西吗?

谢谢你能提供的任何帮助。

为了解开这个谜团,你需要查看gcc的文档,以确定它使用的是哪种风格的应用程序二进制接口(ABI(,然后找到ABI的规范并阅读它。如果你"正在编写纯粹作为学习体验的编译器",你肯定需要它。

简而言之,从广义上讲,ABI要求当前函数保留此额外空间,以便将参数传递给当前函数调用的函数。决定保留多少空间主要取决于函数打算进行的参数传递量,但它比这更微妙,ABI是详细解释的文件

在旧样式的堆栈帧中,我们将PUSH参数添加到堆栈中,然后调用一个函数。

在新样式的堆栈帧中,不再使用EBP,(不确定为什么它被保留并从ESP复制,(参数被放置在堆栈中相对于ESP的特定偏移处,然后调用函数。mov DWORD PTR [esp], 666用于将666参数传递给调用testPassingOneInt32(666);,这一事实证明了这一点。

要了解为什么要使用push DWORD PTR [ecx-4]来复制返回地址,请参阅此部分副本。IIRC,它正在构建返回地址/保存的ebp对的完整副本。


但是gcc似乎再次产生与64字节对齐的堆栈帧

不,它使用了and esp, -32。堆栈帧大小看起来像64个字节,但其对齐方式只有32B。

我不知道为什么它会在堆栈框架中留下这么多额外的空间。猜测gcc -O0为什么会这样做并不是很有趣,因为它甚至没有试图达到最优。

你显然是在没有优化的情况下编译的,这让整个事情变得不那么有趣。这篇文章告诉您更多关于gcc内部的信息,以及什么对gcc方便,而不是它发出的代码是必要的或做任何有用的事情。此外,使用http://gcc.godbolt.org/以在没有CFI指令和其他噪声的情况下获得良好的asm输出。(请整理问题中的asm代码块并输出。所有的噪音都会使它们更难阅读。(