简体   繁体   English

对齐堆栈是什么意思?

[英]What does it mean to align the stack?

I have been a high-level coder, and architectures are pretty new to me, so I decided to read the tutorial on Assembly here:我一直是一名高级编码员,架构对我来说还很陌生,所以我决定在这里阅读有关 Assembly 的教程:

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

Far down the tutorial, instructions on how to convert the Hello World!在教程的后面,说明如何转换 Hello World! program程序

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}

into equivalent assembly code was given and the following was generated:给出了等效的汇编代码,并生成了以下内容:

        .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

For one of the lines,对于其中一行,

andl    $-16, %esp

the explanation was:解释是:

This code "and"s ESP with 0xFFFFFFF0, aligning the stack with the next lowest 16-byte boundary.此代码“与”ESP 与 0xFFFFFFF0,将堆栈与下一个最低的 16 字节边界对齐。 An examination of Mingw's source code reveals that this may be for SIMD instructions appearing in the "_main" routine, which operate only on aligned addresses.对 Mingw 源代码的检查表明,这可能是针对出现在“_main”例程中的 SIMD 指令,这些指令仅对对齐的地址进行操作。 Since our routine doesn't contain SIMD instructions, this line is unnecessary.由于我们的例程不包含 SIMD 指令,因此该行是不必要的。

I do not understand this point.我不明白这一点。 Can someone give me an explanation of what it means to align the stack with the next 16-byte boundary and why it is required?有人可以解释一下将堆栈与下一个 16 字节边界对齐的含义以及为什么需要这样做吗? And how is the andl achieving this? andl是如何实现这一目标的?

Assume the stack looks like this on entry to _main (the address of the stack pointer is just an example):假设堆栈在进入_main看起来像这样(堆栈指针的地址只是一个例子):

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

Push %ebp , and subtract 8 from %esp to reserve some space for local variables:按下%ebp ,然后从%esp减去 8 为局部变量保留一些空间:

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

Now, the andl instruction zeroes the low 4 bits of %esp , which may decrease it;现在, andl指令将%esp的低 4 位%esp ,这可能会减少它; in this particular example, it has the effect of reserving an additional 4 bytes:在此特定示例中,它具有保留额外 4 个字节的效果:

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

The point of this is that there are some "SIMD" (Single Instruction, Multiple Data) instructions (also known in x86-land as "SSE" for "Streaming SIMD Extensions") which can perform parallel operations on multiple words in memory, but require those multiple words to be a block starting at an address which is a multiple of 16 bytes.重点是有一些“SIMD”(单指令多数据)指令(在 x86 领域也称为“SSE”,表示“流式 SIMD 扩展”)可以对内存中的多个字执行并行操作,但是要求这些多个字是从 16 字节倍数的地址开始的块。

In general, the compiler can't assume that particular offsets from %esp will result in a suitable address (because the state of %esp on entry to the function depends on the calling code).通常,编译器不能假设%esp特定偏移量会产生合适的地址(因为%esp在进入函数时的状态取决于调用代码)。 But, by deliberately aligning the stack pointer in this way, the compiler knows that adding any multiple of 16 bytes to the stack pointer will result in a 16-byte aligned address, which is safe for use with these SIMD instructions.但是,通过以这种方式故意对齐堆栈指针,编译器知道将 16 字节的任意倍数添加到堆栈指针将导致 16 字节对齐的地址,这对于这些 SIMD 指令是安全的。

This does not sound to be stack specific, but alignment in general.这听起来不是特定于堆栈的,而是一般对齐的。 Perhaps think of the term integer multiple.也许想想整数倍这个词。

If you have items in memory that are a byte in size, units of 1, then lets just say they are all aligned.如果内存中的项目大小为一个字节,单位为 1,那么就可以说它们都是对齐的。 Things that are two bytes in size, then integers times 2 will be aligned, 0, 2, 4, 6, 8, etc. And non-integer multiples, 1, 3, 5, 7 will not be aligned.大小为两个字节的东西,那么整数乘以 2 将对齐,0, 2, 4, 6, 8 等。非整数倍数,1, 3, 5, 7 将不会对齐。 Items that are 4 bytes in size, integer multiples 0, 4, 8, 12, etc are aligned, 1,2,3,5,6,7, etc are not.大小为 4 字节、整数倍 0、4、8、12 等的项目对齐,1、2、3、5、6、7 等不对齐。 Same goes for 8, 0,8,16,24 and 16 16,32,48,64, and so on. 8、0、8、16、24 和 16 16、32、48、64 等也是如此。

What this means is you can look at the base address for the item and determine if it is aligned.这意味着您可以查看项目的基地址并确定它是否对齐。

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

In the case of a compiler mixing in data with instructions in the .text segment it is fairly straightforward to align data as needed (well, depends on the architecture).在编译器将数据与 .text 段中的指令混合的情况下,根据需要对齐数据相当简单(好吧,取决于体系结构)。 But the stack is a runtime thing, the compiler cannot normally determine where the stack will be at run time.但是堆栈是运行时的东西,编译器通常无法确定堆栈在运行时的位置。 So at runtime if you have local variables that need to be aligned you would need to have the code adjust the stack programmatically.因此,在运行时,如果您有需要对齐的局部变量,则需要让代码以编程方式调整堆栈。

Say for example you have two 8 byte items on the stack, 16 total bytes, and you really want them aligned (on 8 byte boundaries).例如,假设您在堆栈上有两个 8 字节的项目,总共 16 个字节,并且您确实希望它们对齐(在 8 字节边界上)。 On entry the function would subtract 16 from the stack pointer as usual to make room for these two items.在进入时,函数会像往常一样从堆栈指针中减去 16,为这两个项目腾出空间。 But to align them there would need to be more code.但是为了对齐它们,需要更多的代码。 If we wanted these two 8 byte items aligned on 8 byte boundaries and the stack pointer after subtracting 16 was 0xFF82, well the lower 3 bits are not 0 so it is not aligned.如果我们希望这两个 8 字节项目在 8 字节边界上对齐,并且减去 16 后的堆栈指针是 0xFF82,那么低 3 位不是 0,所以它没有对齐。 The lower three bits are 0b010.低三位是 0b010。 In a generic sense we want to subtract 2 from the 0xFF82 to get 0xFF80.一般而言,我们希望从 0xFF82 中减去 2 以获得 0xFF80。 How we determine it is a 2 would be by anding with 0b111 (0x7) and subtracting that amount.我们如何确定它是 2 将通过与 0b111 (0x7) 和并减去该数量。 That means to alu operations an and and a subtract.这意味着铝运算和和减法。 But we can take a shortcut if we and with the ones complement value of 0x7 (~0x7 = 0xFFFF...FFF8) we get 0xFF80 using one alu operation (so long as the compiler and processor have a single opcode way to do that, if not it may cost you more than the and and subtract).但是我们可以走捷径,如果我们使用 0x7 (~0x7 = 0xFFFF...FFF8) 的补码,我们可以使用一个 alu 操作得到 0xFF80(只要编译器和处理器有一个单一的操作码方式来做到这一点,如果不是,它可能比和和减法花费更多)。

This appears to be what your program was doing.这似乎是您的程序正在执行的操作。 Anding with -16 is the same as anding with 0xFFFF....FFF0, resulting in an address that is aligned on a 16 byte boundary.与 -16 与与与 0xFFFF....FFF0 相同,导致在 16 字节边界上对齐的地址。

So to wrap this up, if you have something like a typical stack pointer that works its way down memory from higher addresses to lower addresses, then you want to所以总结一下,如果你有一个典型的堆栈指针之类的东西,它可以从较高的地址到较低的地址在内存中工作,那么你想要

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

where n is the number of bytes to align (must be powers but that is okay most alignment usually involves powers of two).其中 n 是要对齐的字节数(必须是幂,但没关系,大多数对齐通常涉及 2 的幂)。 If you have say done a malloc (addresses increase from low to high) and want to align the address of something (remember to malloc more than you need by at least the alignment size) then如果你说做了一个 malloc(地址从低到高增加)并且想要对齐某物的地址(记住 malloc 超过你需要的至少对齐大小)然后

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

Or if you want just take the if out there and perform the add and mask every time.或者,如果您只想将 if 放在那里并每次都执行添加和掩码。

many/most non-x86 architectures have alignment rules and requirements.许多/大多数非 x86 架构都有对齐规则和要求。 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.就指令集而言,x86 过于灵活,但就执行而言,您可以/将会为 x86 上的未对齐访问付出代价,因此即使您可以做到,您也应该努力保持对齐,就像您对任何其他架构。 Perhaps that is what this code was doing.也许这就是这段代码所做的。

This has to do with byte alignment .这与字节对齐有关 Certain architectures require addresses used for a specific set of operations be aligned to specific bit boundaries.某些架构要求用于特定操作集的地址与特定位边界对齐。

That is, if you wanted 64-bit alignment for a pointer, for example, then you could conceptually divide the entire addressable memory into 64-bit chunks starting at zero.也就是说,例如,如果您想要指针的 64 位对齐,那么您可以在概念上将整个可寻址内存划分为从零开始的 64 位块。 An address would be "aligned" if it fit exactly into one of these chunks, and not aligned if it took part of one chunk and part of another.如果地址恰好适合这些块中的一个,则该地址将被“对齐”,如果它占用一个块的一部分和另一个块的一部分,则不对齐。

A significant feature of byte alignment (assuming the number is a power of 2) is that the least-significant X bits of the address are always zero.字节对齐的一个重要特征(假设数字是 2 的幂)是地址的最低有效X位始终为零。 This allows the processor to represent more addresses with fewer bits by simply not using the bottom X bits.这允许处理器通过简单地不使用底部X位来用更少的位来表示更多的地址。

Imagine this "drawing"想象一下这张“画”

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

Values at addresses multiple of 8 "slide" easily into (64-bit) registers 8 的倍数地址处的值很容易“滑动”到(64 位)寄存器中

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

Of course registers "walk" in steps of 8 bytes当然以 8 个字节的步长注册“walk”

Now if you want to put the value at address xxx5 into a register is much more difficult :-)现在,如果要将地址 xxx5 处的值放入寄存器中要困难得多:-)


Edit andl -16编辑和-16

-16 is 11111111111111111111111111110000 in binary -16 是 11111111111111111111111111110000 二进制

when you "and" anything with -16 you get a value with the last 4 bits set to 0 ... or a multitple of 16.当你用 -16 “和”任何东西时,你会得到一个最后 4 位设置为 0 的值......或 16 的倍数。

When the processor loads data from memory into a register, it needs to access by a base address and a size.当处理器将内存中的数据加载到寄存器中时,它需要通过基地址和大小进行访问。 For example, it will fetch 4 bytes from address 10100100. Notice that there are two zeros at the end of that example.例如,它将从地址 10100100 获取 4 个字节。请注意,在该示例的末尾有两个零。 That's because the four bytes are stored so that the 101001 leading bits are significant.那是因为存储了四个字节,因此前导位 101001 很重要。 (The processor really accesses these through a "don't care" by fetching 101001XX.) (处理器通过获取 101001XX 真正通过“不关心”访问这些。)

So to align something in memory means to rearrange data (usually through padding) so that the desired item's address will have enough zero bytes.因此,对齐内存中的某些内容意味着重新排列数据(通常通过填充),以便所需项目的地址有足够的零字节。 Continuing the above example, we can't fetch 4 bytes from 10100101 since the last two bits aren't zero;继续上面的例子,我们不能从 10100101 中取出 4 个字节,因为最后两位不为零; that would cause a bus error.这会导致总线错误。 So we must bump the address up to 10101000 (and waste three address locations in the process).所以我们必须将地址提升到 10101000(并在此过程中浪费三个地址位置)。

The compiler does this for you automatically and is represented in the assembly code.编译器会自动为您执行此操作并在汇编代码中表示。

Note that this is manifest as an optimization in C/C++:请注意,这在 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;
}

The output is输出是

Size of first: 12
Size of second: 8

Rearranging the two char 's means that the int will be aligned properly, and so the compiler doesn't have to bump the base address via padding.重新排列两个char意味着int将正确对齐,因此编译器不必通过填充来增加基地址。 That's why the size of the second is smaller.这就是为什么第二个的大小更小。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM