对齐堆栈意味着什么?

时间:2010-11-13 23:29:14

标签: c gcc assembly

我是一名高级程序员,架构对我来说很新,所以我决定在这里阅读有关Assembly的教程:

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

解释是:

  

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

我不明白这一点。有人能给我一个解释,说明将堆栈与下一个16字节边界对齐是什么意思以及为什么需要它? andl如何实现这一目标?

6 个答案:

答案 0 :(得分:55)

假设在_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指令是安全的。

答案 1 :(得分:16)

这听起来不是特定于堆栈的,但通常是对齐。也许想到术语整数倍。

如果内存中的项目大小为字节,单位为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,依此类推。

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

size in bytes, address in the form of 
1, xxxxxxx
2, xxxxxx0
4, xxxxx00
8, xxxx000
16,xxx0000
32,xx00000
64,x000000
and so on

如果编译器将数据与.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(只要编译器和处理器有一个操作码方式来做到这一点,如果不是,它可能会花费你超过和减去)。

这似乎是您的计划正在做的事情。使用-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 :(得分:7)

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

也就是说,例如,如果你想要一个指针的64位对齐,那么你可以在概念上将整个可寻址存储器分成从0开始的64位块。如果一个地址完全适合这些块中的一个,那么它将被“对齐”,如果它占用了一个块的一部分并且是另一个块的一部分,则不会对齐。

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

答案 3 :(得分:5)

想象一下这个“绘画”

addresses
 xxx0123456789abcdef01234567 ...
    [------][------][------] ...
registers

地址的多个值8“滑动”到(64位)寄存器

addresses
         56789abc ...
    [------][------][------] ...
registers

当然以8字节

的步长注册“walk”

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


编辑andl -16

-16是二进制的11111111111111111111111111110000

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

答案 4 :(得分: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 :(得分:3)

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

相关问题