61

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

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 源代码的检查表明,这可能是针对出现在“_main”例程中的 SIMD 指令,它只对对齐的地址进行操作。由于我们的例程不包含 SIMD 指令,因此这一行是不必要的。

我不明白这一点。有人可以解释一下将堆栈与下一个 16 字节边界对齐的含义以及为什么需要它吗?以及如何andl实现这一目标?

4

6 回答 6

72

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

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

%ebp,然后减去 8%esp为局部变量保留一些空间:

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

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

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

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

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

于 2010-11-13T23:58:06.470 回答
19

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

如果内存中有一个字节大小的项目,单位为 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 ...

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

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

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

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

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

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

many/most non-x86 architectures have alignment rules and requirements. x86 is overly flexible as far as the instruction set goes, but as far as execution goes you can/will pay a penalty for unaligned accesses on an x86, so even though you can do it you should strive to stay aligned as you would with any other architecture. Perhaps that is what this code was doing.

于 2010-11-14T06:07:11.893 回答
7

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

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

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

于 2010-11-13T23:46:01.630 回答
5

想象一下这幅“图画”

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

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

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

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

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


编辑 andl -16

-16 是 11111111111111111111111111110000 二进制

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

于 2010-11-13T23:47:22.147 回答
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将正确对齐,因此编译器不必通过填充来改变基地址。这就是为什么第二个的大小更小。

于 2010-11-13T23:50:46.253 回答
3

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

于 2010-11-13T23:32:23.463 回答