对齐堆栈意味着什么?

Leg*_*end 45 c assembly gcc

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

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

在教程的下方,有关如何转换Hello World的说明!程序

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

进入等效的汇编代码并生成以下内容:

        .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
Run Code Online (Sandbox Code Playgroud)

对于其中一条线,

andl    $-16, %esp
Run Code Online (Sandbox Code Playgroud)

解释是:

这段代码"和"ESP与0xFFFFFFF0,使堆栈与下一个最低的16字节边界对齐.对Mingw源代码的检查表明,这可能是出现在"_main"例程中的SIMD指令,它只在对齐的地址上运行.由于我们的例程不包含SIMD指令,因此不需要此行.

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

Mat*_*ery 56

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

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230
Run Code Online (Sandbox Code Playgroud)

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

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230
|      %ebp       |
+-----------------+  <--- 0xbfff122c
:    reserved     :
:     space       :
+-----------------+  <--- 0xbfff1224
Run Code Online (Sandbox Code Playgroud)

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

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230
|      %ebp       |
+-----------------+  <--- 0xbfff122c
:    reserved     :
:     space       :
+ - - - - - - - - +  <--- 0xbfff1224
:   extra space   :
+-----------------+  <--- 0xbfff1220
Run Code Online (Sandbox Code Playgroud)

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

通常,编译器不能假设特定的偏移量%esp将导致合适的地址(因为%esp函数的进入状态取决于调用代码).但是,通过以这种方式故意对齐堆栈指针,编译器知道向堆栈指针添加任意16个字节的多个将导致16字节对齐的地址,这对于使用这些SIMD指令是安全的.

  • @secmask:推送原始`%ebp`之后的'%esp`的值已经存储在`%ebp`中,所以它不需要知道,因为`%ebp`指向保留空间的顶部.`%esp`通过显示的代码中的`leave`指令恢复 - "leave"相当于`movl%ebp,%esp; popl%ebp`. (3认同)

old*_*mer 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 ...

这似乎是您的计划正在做的事情.使用-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上的未对齐访问支付罚款,所以即使你可以做到这一点,你应该努力保持对齐,就像你对任何其他建筑.也许这就是这段代码所做的.


tyl*_*erl 7

这与字节对齐有关.某些体系结构要求用于特定操作集的地址与特定位边界对齐.

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

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


pmg*_*pmg 5

想象一下这个"画"

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

地址为8的地址值"轻松"滑入(64位)寄存器

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

当然以8个字节的步长记录"walk"

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


编辑和-16

-16是二进制的11111111111111111111111111110000

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


chr*_*ock 5

当处理器将数据从内存加载到寄存器时,需要通过基地址和大小进行访问。例如,它将从地址 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;
}
Run Code Online (Sandbox Code Playgroud)

输出是

Size of first: 12
Size of second: 8
Run Code Online (Sandbox Code Playgroud)

重新排列两个char' 意味着 ' 'int将正确对齐,因此编译器不必通过填充来碰撞基地址。这就是第二个尺寸较小的原因。