我是一名高级程序员,架构对我来说很新,所以我决定在这里阅读有关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
如何实现这一目标?
答案 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)
它应该只在偶数地址,而不是在奇数地址,因为存在性能缺陷访问它们。