【问题标题】:What does it mean to align the stack?对齐堆栈是什么意思?
【发布时间】:2011-05-09 16:25:41
【问题描述】:

我一直是一名高级编码员,架构对我来说相当新,所以我决定在这里阅读关于组装的教程:

http://en.wikibooks.org/wiki/X86_Assembly/Print_Version

在教程的最后,关于如何转换 Hello World 的说明!程序

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}

给出了等效的汇编代码并生成了以下内容:

        .text
LC0:
        .ascii "Hello, world!\12\0"
.globl _main
_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        andl    $-16, %esp
        movl    $0, %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        call    __alloca
        call    ___main
        movl    $LC0, (%esp)
        call    _printf
        movl    $0, %eax
        leave
        ret

对于其中一行,

andl    $-16, %esp

解释是:

这段代码“和”s ESP 与 0xFFFFFFF0, 将堆栈与下一个对齐 最低 16 字节边界。一个 Mingw的源代码检查 表明这可能适用于 SIMD “_main”中出现的说明 例程,仅在对齐时运行 地址。因为我们的例程没有 包含 SIMD 指令,这一行 是不必要的。

我不明白这一点。有人可以解释一下将堆栈与下一个 16 字节边界对齐意味着什么以及为什么需要它吗? andl 是如何做到这一点的?

【问题讨论】:

    标签: c assembly gcc x86 memory-alignment


    【解决方案1】:

    这听起来不是特定于堆栈,而是总体上对齐。也许想想整数倍这个词。

    如果内存中的项目大小为一个字节,单位为 1,那么可以说它们都是对齐的。大小为两个字节的东西,然后整数乘以 2 将对齐,0、2、4、6、8 等。非整数倍数,1、3、5、7 将不对齐。大小为 4 字节、整数倍数 0、4、8、12 等的项目对齐,1、2、3、5、6、7 等不对齐。 8、0、8、16、24 和 16 16、32、48、64 等也是如此。

    这意味着您可以查看项目的基地址并确定它是否对齐。

    大小以字节为单位,地址形式为 1、xxxxxx 2、xxxxxx0 4、xxxx00 8、xxxx000 16,xxx0000 32,xx00000 64,x000000 等等

    在编译器将数据与 .text 段中的指令混合的情况下,根据需要对齐数据是相当简单的(嗯,取决于架构)。但是堆栈是运行时的东西,编译器通常无法确定堆栈在运行时的位置。因此,在运行时,如果您有需要对齐的局部变量,则需要让代码以编程方式调整堆栈。

    例如,您在堆栈上有两个 8 字节项目,总共 16 个字节,并且您真的希望它们对齐(在 8 字节边界上)。在进入时,该函数会像往常一样从堆栈指针中减去 16,以便为这两项腾出空间。但是要对齐它们,就需要更多的代码。如果我们希望这两个 8 字节的项目在 8 字节边界上对齐,并且减去 16 后的堆栈指针是 0xFF82,那么低 3 位不是 0,因此它没有对齐。低三位是 0b010。在一般意义上,我们希望从 0xFF82 中减去 2 以获得 0xFF80。我们如何确定它是 2 将通过与 0b111 (0x7) 进行与运算并减去该数量。这意味着对 alu 操作一个和和一个减法。但是我们可以走捷径,如果我们使用 0x7 (~0x7 = 0xFFFF...FFF8) 的补码值,我们使用一个 alu 操作得到 0xFF80(只要编译器和处理器有一个单一的操作码方法来做到这一点,如果不是,它可能比 and 和减法花费更多)。

    这似乎是您的程序正在执行的操作。与 -16 的与与与 0xFFFF....FFF0 的与与相同,导致在 16 字节边界上对齐的地址。

    所以总结一下,如果你有一个像典型的堆栈指针这样的东西,它从内存的高地址向下工作到低地址,那么你想要

    sp = sp & (~(n-1))

    其中 n 是要对齐的字节数(必须是幂,但没关系,大多数对齐通常涉及 2 的幂)。如果你说做了一个 malloc(地址从低到高增加)并且想要对齐某些东西的地址(记住 malloc 至少比你需要的对齐大小更多)然后

    if(ptr&(~(n-)) { ptr = (ptr+n)&(~(n-1)); }

    或者,如果你想把 if 拿出来,每次都执行 add 和 mask。

    许多/大多数非 x86 架构都有对齐规则和要求。 x86 就指令集而言过于灵活,但就执行而言,您可以/将为 x86 上的未对齐访问付出代价,因此即使您可以做到,您也应该努力保持对齐,就像任何其他架构。也许这就是这段代码所做的。

    【讨论】:

      【解决方案2】:

      这与byte alignment 有关。某些架构要求用于特定操作集的地址与特定位边界对齐。

      也就是说,例如,如果您想要一个指针的 64 位对齐,那么您可以从概念上将整个可寻址内存划分为从零开始的 64 位块。如果地址完全适合其中一个块,则该地址将被“对齐”,而如果它既属于一个块的一部分又属于另一个块的一部分,则该地址将不对齐。

      字节对齐的一个重要特征(假设数字是 2 的幂)是地址的最低有效位 X 位始终为零。这允许处理器通过简单地不使用底部 X 位来用更少的位表示更多地址。

      【讨论】:

        【解决方案3】:

        想象一下这幅“图画”

        地址 xxx0123456789abcdef01234567 ... [------][------][------] ... 寄存器

        地址的值是 8 的倍数,“滑动”到(64 位)寄存器中

        地址 56789abc ... [------][------][------] ... 寄存器

        当然是以 8 个字节为步长注册“walk”

        现在如果你想把地址 xxx5 的值存入寄存器要困难得多:-)


        编辑和l -16

        -16 是二进制的 1111111111111111111111111110000

        当你用 -16 “和”任何东西时,你会得到一个最后 4 位设置为 0 ... 或 16 的倍数的值。

        【讨论】:

          【解决方案4】:

          当处理器将数据从内存加载到寄存器中时,它需要通过基地址和大小来访问。例如,它将从地址 10100100 获取 4 个字节。请注意,该示例的末尾有两个零。这是因为存储了四个字节,因此 101001 前导位很重要。 (处理器通过获取 101001XX 真正通过“无关”访问这些。)

          因此,对齐内存中的某些内容意味着重新排列数据(通常通过填充),以便所需项目的地址具有足够的零字节。继续上面的例子,我们不能从 10100101 中获取 4 个字节,因为最后两位不为零;这会导致总线错误。所以我们必须将地址增加到 10101000(并在此过程中浪费三个地址位置)。

          编译器会自动为您执行此操作,并在汇编代码中表示。

          请注意,这表现为 C/C++ 中的优化:

          struct first {
              char letter1;
              int number;
              char letter2;
          };
          
          struct second {
              int number;
              char letter1;
              char letter2;
          };
          
          int main ()
          {
              cout << "Size of first: " << sizeof(first) << endl;
              cout << "Size of second: " << sizeof(second) << endl;
              return 0;
          }
          

          输出是

          Size of first: 12
          Size of second: 8
          

          重新排列两个char 意味着int 将正确对齐,因此编译器不必通过填充来改变基地址。这就是为什么秒的大小更小。

          【讨论】:

          • 这对我来说是一个新知识。我在哪里可以详细了解此内容?
          【解决方案5】:

          假设堆栈在进入_main 时看起来像这样(堆栈指针的地址只是一个示例):

          |    existing     |
          |  stack content  |
          +-----------------+  <--- 0xbfff1230
          

          推%ebp,并从%esp中减去8,为局部变量保留一些空间:

          |    existing     |
          |  stack content  |
          +-----------------+  <--- 0xbfff1230
          |      %ebp       |
          +-----------------+  <--- 0xbfff122c
          :    reserved     :
          :     space       :
          +-----------------+  <--- 0xbfff1224
          

          现在,andl 指令将%esp 的低 4 位归零,这可能会减少它;在这个特定的例子中,它具有额外保留 4 个字节的效果:

          |    existing     |
          |  stack content  |
          +-----------------+  <--- 0xbfff1230
          |      %ebp       |
          +-----------------+  <--- 0xbfff122c
          :    reserved     :
          :     space       :
          + - - - - - - - - +  <--- 0xbfff1224
          :   extra space   :
          +-----------------+  <--- 0xbfff1220
          

          这里的重点是有一些“SIMD”(单指令,多数据)指令(在 x86-land 中也称为“SSE”,表示“流式 SIMD 扩展”)可以对多个字执行并行操作内存,但要求这些多个字是一个从 16 字节的倍数地址开始的块。

          一般来说,编译器不能假设与%esp 的特定偏移量会产生合适的地址(因为进入函数时%esp 的状态取决于调用代码)。但是,通过故意以这种方式对齐堆栈指针,编译器知道将 16 字节的任意倍数添加到堆栈指针将导致 16 字节对齐的地址,这对于这些 SIMD 指令是安全的。

          【讨论】:

          • 现在,andl 指令将 %esp 的低 4 位归零,这可能会减少它。那么编译器是如何知道后来为了平衡堆栈减少了多少字节呢?
          • @secmask:%esp的值是推入后的原始%ebp已经存入%ebp,所以不需要知道,因为%ebp指向顶部的预留空间。 %esp 由所示代码中的 leave 指令恢复 - leave 等效于 movl %ebp, %esp ; popl %ebp。
          【解决方案6】:

          它应该只在偶数地址上,而不是在奇数地址上,因为访问它们时存在性能缺陷。

          【讨论】:

          • 这与性能无关。 CPU 根本无法从未对齐的地址获取数据,因为那将是总线错误。
          • @chrisaycock 现代处理器可以,但性能损失很小。
          猜你喜欢
          • 1970-01-01
          • 2023-03-09
          • 2010-10-14
          • 2010-09-07
          • 2010-09-15
          • 2016-06-11
          • 2011-05-30
          • 2015-05-14
          相关资源
          最近更新 更多