2010-11-13 97 views
38

我一直是高层次的编码器,以及架构是相当新的给我,所以我决定在这里阅读关于大会教程:对齐堆栈是什么意思?

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

远了教程,如何转换的说明你好,世界!程序

#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的源代码 的检查揭示,这可能是针对出现在“_main” 例程中的SIMD 指令,其仅在对齐的 地址上操作。由于我们的例程不包含 包含SIMD指令,此行 是不必要的。

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

+3

http://en.wikipedia.org/wiki/Data_structure_alignment – chrisaycock 2010-11-13 23:33:57

+1

在没有启用优化器的情况下查看机器代码没什么意义。 – 2010-11-14 01:23:31

回答

51

假设栈看起来像这样在进入_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的土地也称为作为“流式SIMD扩展”的“SSE”),它可以对存储器中的多个字执行并行操作,但要求这些多个字是从16字节的倍数的地址开始的块。

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

+0

现在,andl指令清零%esp的低4位,这可能会减少。那么编译器怎么知道有多少字节被减少来平衡堆栈呢? – secmask 2010-11-14 05:49:50

+3

@secmask:在推送原始'%ebp'之后'%esp'的值已经存储在'%ebp'中,所以它不需要知道,因为'%ebp'指向保留的顶部空间。 '%esp'通过所示代码中的'leave'指令得到恢复 - 'leave'等同于'movl%ebp,%esp; popl%ebp'。 – 2010-11-14 13:54:52

3

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

+0

这与性能无关。 CPU根本无法从未对齐的地址获取数据,因为这会是一个总线错误。 – chrisaycock 2010-11-14 00:08:52

+0

总线错误与否,它不会失败。 – 2010-11-14 04:54:04

+0

@chrisaycock现代处理器可能会有小的性能损失。 – YoYoYonnY 2017-11-21 14:52:34

7

这和byte alignment有关。某些体系结构要求将用于特定操作集的地址与特定的位边界对齐。例如,如果你想要一个指针的64位对齐,那么你可以在概念上将整个可寻址的存储器划分为从零开始的64位组块。如果一个地址与其中一个块完全匹配,则该地址将“对齐”,并且如果它将一个块和另一个块的一部分组合,则该地址将不对齐。

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

+1

从我身边也+1!感谢您的解释。 – Legend 2010-11-14 05:43:05

5

设想这样在8 “滑动” 地址的多个 “绘图”

 
addresses 
xxxabcdef... 
    [------][------][------] ... 
registers 

值容易进(64位)寄存器

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

当然在步骤寄存器 “走” 8字节

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


编辑和L -16

-16是二进制

当你 “和” 任何与你得到的值设置为0的最后4位-16 ...或11111111111111111111111111110000多16个。

3

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

因此,对齐内存中的内容意味着重新排列数据(通常通过填充)以便所需项目的地址将具有足够的零字节。继续上面的例子,我们不能从10100101中获取4个字节,因为最后两位不是0;这会导致总线错误。所以我们必须将地址碰撞到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将被正确地对准,并所以编译器不必通过填充来冲突基地址。这就是为什么第二个规模较小。

13

这听起来并不是特定的堆栈,而是一般的对齐。也许想到整数倍这个词。

如果您的内存中的项目大小为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等等也是如此。

这是什么意思是你可以看看该项目的基地址,并确定它是否对齐。

 
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)和减去该数量。这意味着一个和一个和一个减法操作。但是,如果我们和0x7(〜0x7 = 0xFFFF ... FFF8)的补码值使用一个alu操作(只要编译器和处理器有一个单一的操作码方式来实现这个操作,如果没有,它可能比你更多和减去)。

这似乎是你的程序在做什么。使用-16与和0xFFFF ....和FFF0相同,导致在16字节边界上对齐的地址。

所以包装这件事,如果你碰到这样一个典型的堆栈指针的作品其一路下跌,从高地址内存到低地址,那么你要

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

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

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

或者,如果你想只要拿出如果在那里,每次执行添加和掩码。

许多/大部分非x86体系结构都有对齐规则和要求。 x86就指令集而言过于灵活,但就执行情况而言,您可能会为x86上的未对齐访问付出代价,因此尽管您可以这样做,但您应该努力保持对齐状态其他架构。也许这就是这个代码所做的。

+1

非常棒的答案,它为什么在页面的底部? – jwbensley 2016-06-08 16:59:43

相关问题