GCC - 如何重新对齐堆栈?

Posted

技术标签:

【中文标题】GCC - 如何重新对齐堆栈?【英文标题】:GCC - How to realign stack? 【发布时间】:2011-02-15 10:56:47 【问题描述】:

我尝试构建一个使用 pthreads 和 __m128 SSE 类型的应用程序。根据 GCC 手册,默认堆栈对齐是 16 个字节。为了使用__m128,要求是16字节对齐。

我的目标 CPU 支持 SSE。我使用不支持运行时堆栈重新对齐的 GCC 编译器(例如 -mstackrealign)。我不能使用任何其他 GCC 编译器版本。

我的测试应用程序如下所示:

#include <xmmintrin.h>
#include <pthread.h>
void *f(void *x)
   __m128 y;
   ...

int main(void)
  pthread_t p;
  pthread_create(&p, NULL, f, NULL);

应用程序产生异常并退出。经过简单的调试(printf "%p", &y),发现变量y不是16字节对齐的。

我的问题是:如何在不使用任何 GCC 标志和属性(它们没有帮助)的情况下正确地重新对齐堆栈(16 字节)?我应该在这个线程函数 f() 中使用 GCC 内联汇编器吗?

【问题讨论】:

如果您必须使用特定的 gcc 版本,请包括 gcc 版本(例如 gcc 4.3.2 i386)和主机/目标操作系统(例如 Debian 5.0 (lenny) Linux 2.6.26 i686)。知道是否建议 gcc 4.3 选项与 3.4 可能会有所不同。 【参考方案1】:

这不应该首先发生,但要解决此问题,您可以尝试:

void *f(void *x)

   __m128 y __attribute__ ((aligned (16)));
   ...

【讨论】:

不,它没有帮助。同样的问题。 我猜你是在 Windows 上而不是在正确的操作系统上做这个?这里有一些关于解决这个问题的好信息:sourceware.org/ml/pthreads-win32/2008/msg00056.html 看起来这是旧版本 gcc 中的一个错误 - 它似乎已在 2004 年左右修复 - 是否有某些原因您不能使用更新的工具链? 其实不行,我不能使用另一个 GCC 版本——我们有一个特定的硬件/软件环境。 我正在尝试使用内联汇编器实现显式堆栈调整。【参考方案2】:

在堆栈上分配一个比sizeof(__m128) 大 15 字节的数组,并使用该数组中第一个对齐的地址。如果需要多个,请将它们分配到一个数组中,并留出 15 字节的边距以进行对齐。

我不记得分配 unsigned char 数组是否可以让您免受编译器的严格别名优化,或者它是否只能反过来工作。

#include <stdint.h>

void *f(void *x)

   unsigned char y[sizeof(__m128)+15];
   __m128 *py = (__m128*) (((uintptr_t)&y) + 15) & ~(uintptr_t)15);
   ...

【讨论】:

您可能还想检查整个线程堆栈是否以 16 字节对齐方式分配。 谢谢,但是 ptr_t 是什么,为什么要使用 &~15 ? 不幸的是,无论潜在的编译器优化如何(例如将其保存在寄存器中),这都会强制变量位于堆栈上。 我猜它应该是uintptr_t,但不管怎样,它只是一个整数类型,大到可以容纳一个指针。 @Paul R 对,我正在寻找正确的头文件,但因为我记错了名称而找不到它。 @psihodelia &amp;~15 的意思是“向下舍入到 16 的倍数”。【参考方案3】:

我已经解决了这个问题。 这是我的解决方案:

void another_function()
   __m128 y;
   ...

void *f(void *x)
asm("pushl    %esp");
asm("subl    $16,%esp");
asm("andl    $-0x10,%esp");
another_function();
asm("popl %esp");

首先,我们将堆栈增加 16 个字节。其次,我们使最不重要的半字节等于 0x0。我们使用 push/pop 操作数保存堆栈指针。我们调用另一个函数,它有自己的所有局部变量 16 字节对齐。所有嵌套函数的局部变量也将 16 字节对齐。

而且它有效!

【讨论】:

说真的。更新您的编译器。不要为自己在代码中加入 rube goldberg 设备而自豪。 此代码似乎将 ESP 保存在堆栈上,然后将 ESP 移动到其他位置,然后弹出 ESP。这将导致随机值弹出到 ESP 中。这不会导致崩溃吗?或者您是否使用调用约定,其中 ESP 保存在其他地方,可能保存到 EBP 中,并在最后恢复,从而使 POP 变得多余? 1) 我无法更新 GCC -> 我有一个特定的运行时环境和一个特定的 x86 兼容 CPU。 2)不,为什么会导致崩溃?保存 ESP,然后恢复它不会导致任何崩溃或随机值。我已经在没有 pushl/popl 的情况下测试了上面的代码,它也可以。没有任何调用约定,ESP 也没有保存在其他地方。 就像 user9876 说的——你知道“pushl %esp”是做什么的吗?从概念上讲,它的工作原理如下: Memory[%esp] = %esp %esp -= 4; //取决于你的堆栈如何增长,它可能是 "+=4" 然后,一个 "popl %esp" 本质上是:%esp += 4; %esp = Memory[%esp] 现在,如果在“push”和“pop”之间修改了 esp - 第二次内存访问(“pop”)将从错误的地址读取。唯一合理的解释是编译器在函数 f() 的序言​​中也将 %esp 保存在其他地方(例如在 ebp 中?),然后在 f() 的尾声中恢复它。因此,它隐藏了您的错误。【参考方案4】:

另一种解决方案是,使用填充函数,它首先对齐堆栈,然后调用f。所以不是直接调用f,而是调用pad,它首先填充堆栈,然后调用foo,并使用对齐的堆栈。

代码如下所示:

#include <xmmintrin.h>
#include <pthread.h>

#define ALIGNMENT 16

void *f(void *x) 
    __m128 y;
    // other stuff


void * pad(void *val) 
    unsigned int x; // to get the current address from the stack
    unsigned char pad[ALIGNMENT - ((unsigned int) &x) % ALIGNMENT];
    return f(val);


int main(void)
    pthread_t p;
    pthread_create(&p, NULL, pad, NULL);

【讨论】:

【参考方案5】:

抱歉复活一个旧线程...

对于那些使用比 OP 更新的编译器的人,OP 提到了 -mstackrealign 选项,这导致我转到 __attribute__((force_align_arg_pointer))。如果您的函数正在优化以使用 SSE,但 %ebp 未对齐,如果您需要,这将透明地执行运行时修复。我还发现这只是i386 上的一个问题。 x86_64 ABI 保证参数对齐到 16 个字节。

__attribute__((force_align_arg_pointer)) void i_crash_when_not_aligned_to_16_bytes() ...

对于那些可能想了解更多信息的人来说很酷的文章:http://wiki.osdev.org/System_V_ABI

【讨论】:

谢谢。它帮助解决了 making .so files runnable as binaries 的 32 位 x86 问题。它还帮助我找到了讨论 confusion about this stuff 的错误。

以上是关于GCC - 如何重新对齐堆栈?的主要内容,如果未能解决你的问题,请参考以下文章

使用 GCC 但没有使用 Clang 的堆栈帧太大(过度对齐?)

gcc x86-32堆栈对齐并调用printf

即使没有对齐,GCC 分配的堆栈空间也比本地所需的更多。空间有啥用?

gcc数据对齐之: howto 2.

针对不同缓冲区大小的不同内存对齐

如何使用带有行号信息的 gcc 获取 C++ 的堆栈跟踪?