"aligning the stack" 在汇编中是什么意思?

What does "aligning the stack" mean in assembly?

堆栈对齐在 ASMx64 中如何工作?函数调用前什么时候需要对齐栈,需要减去多少?

我不明白这样做的目的是什么。我知道还有其他关于此的帖子,但对我来说还不够清楚。例如:

extern foo
global bar

section .text
bar:
  ;some code...
  sub  rsp, 8     ; Why 8 (I saw this on some posts) ? Can it be another value ? Why do we need to substract?
  call foo        ; Do we need to align stack everytime we call a function?
  add  rsp, 8
  ;some code...
  ret

寻址一般是byte-based。唯一地址指向一个字节(可以是字或双字等中的第一个字节,但引用该地址)。

对于任何编号系统,最低有效位都保留值基数的 0 次方(数字 1)。 1 次方的下一个最小基数,2 次方的下一个基数。在十进制中,这是个位列,十位列,百位列。在二进制中,一个,两个,四个...对齐意味着可以被整除,这也意味着最低有效数字为零。

您总是在字节边界上“对齐”,但二进制中的 16 位边界意味着最低有效位为零,32 位边界对齐两个零等等。

0x1234 在 16 位和 32 位边界上对齐,但不是 64 位边界
0x1235 未对齐(字节对齐真的不是问题)
0x1236 在 16 位边界上对齐
0x1230 四个零,所以 16、32、64、128 位不是字节。 2,4,8,16 字节。

原因是出于性能原因,所有内存和数据总线都有固定宽度,一旦实现,您就无法在逻辑中神奇地添加或删除线路,存在物理限制,您可以选择不使用所有这些都是设计的一部分,但您不能添加任何内容。

因此,虽然 x86 总线更宽,但假设您有一个 32 位宽的数据总线和一个 32 位宽的内存(想想缓存和 dram,但我们通常不直接访问 dram)。

如果我想将 16 位 0xAABB 保存到小端机器中的地址 0x1001,则 0x1001 将获得 0xBB,0x1002 将获得 0xAA。如果我有一个 32 位数据总线和一个 32 位内存在它的远端,那么如果我为此设计总线,我可以移动这 16 位,通过将 0xXXAABBXX 写入地址 0x1000,字节通道掩码 0b0110 告诉内存控制器使用与基于 BYTE 的地址 0x1000 关联的 32 位内存,总线上的字节通道掩码告诉控制器只保存中间两个字节,外面两个不关心。

内存通常是固定宽度的,所以所有的事务都必须是全宽度的,它会读取32位,用0xAABB修改中间的16位,然后写回32位。这当然是低效的。更糟糕的是将 0xAABB 写入 0x1003,这将是两个总线事务,一个用于地址 0x1000 的 0xBBXXXXXX,另一个用于地址 0x1004 的 0xXXXXXXAA。这是总线上和内存上的 read-modify-writes 上的很多额外周期。

现在堆栈对齐规则不会阻止 read-modify-writes 写入。对于发生较大传输的情况,有机会获得性能提升,例如,如果总线是 32 位,而内存和您对地址 0x1000 进行了 64 位传输,基于总线设计,这看起来像是具有两个的长度。总线握手发生然后两个背靠背时钟数据移动,而不是握手和数据总线的一个宽度用于较小的传输。因此,如果内存是 32 位宽,那么您会在那里获得收益,那么它是两次写入而没有 read-modify-write 到缓存中的 sram。挺干净的,想避开read-modify-writes.

随着事情的发展,硬件和工具需要堆栈对齐,现在做一段时间。

根据指令集,很明显你在这里问的是 x86,但作为程序员,你有时可以选择将一个字节压入堆栈,然后调整它使其对齐。或者,如果您要为局部变量腾出空间,则取决于指令集(如果堆栈指针的通用性足以对其进行数学运算),您可以简单地减去,因此 sub sp,#8 与压入两个相同将 32 位项目放入堆栈只是为了为两个 32 位项目腾出空间。

如果规则是 32 位对齐并且你压入一个字节,那么你需要将堆栈指针调整 3 以使堆栈指针的总变化是 4 字节(32 位)的倍数。

你怎么知道有多少,你简单地数一下就知道了。如果是16字节对齐,你push 4那么你需要再push 12或者把stack pointer再调整12.

这里的关键是,如果每个人都同意保持堆栈对齐,那么你实际上不必查看堆栈指针的低位,你只需在调用之前跟踪你正在推送和弹出的内容否则。

如果堆栈与中断处理程序共享(在您当前的 x86 运行 操作系统中不是真的,但在通用处理器的许多其他用例中仍然可能并且可能)我还没有看到这条规则在那里适用,因为您会看到编译器执行小于对齐大小的推送或弹出,然后使用其他推送或弹出或减法或加法进行调整。如果在它们之间发生中断,处理程序将看到未对齐的堆栈。

一些架构会在未对齐访问时出错,这是保持堆栈对齐的另一个原因。

如果您的代码没有混淆 stck 然后你不需要弄乱堆栈(指针)。仅当您通过在堆栈上分配 space 来在代码中使用堆栈(堆栈指针上的推送或数学运算)时,您才需要关心并且需要知道链接此代码的编译器的约定符合并符合这一点。如果这都是汇编语言而没有编译器,那么你自己决定约定,基本上在处理器本身的限制内做任何你想做的事。

从你的标题问题来看,它与汇编完全无关,也与机器代码无关。它与您的代码及其作用有关。汇编语言只是一种语言,您可以在其中传达您想要调整堆栈指针的程度,指令不关心或不知道任何此类事情,它采用提供的常量并将其用于寄存器。汇编是为数不多的允许您在堆栈指针寄存器上进行数学运算的语言之一,因此存在这种联系。但是对齐和组装是没有关系的。

When do you need to align the stack before a function call and ....?

当您调用的函数需要对齐堆栈时,您需要对齐堆栈。

用其他语言(例如 C)编写的函数,以及用汇编编写但设计为从其他语言调用的函数,将遵守某种调用约定(其中包括的不仅仅是堆栈对齐——如何传递参数,参数在哪里,诸如“红区”之类的东西);对于 64 位 80x86,2 个通用调用约定期望堆栈与 16 字节边界对齐。

在“纯程序集”项目中,您调用的函数是为程序集调用者编写的程序集;程序员可以自由地做任何他们喜欢的事情(例如,任何对性能最好的事情),而不用关心其他语言的 limitations/restrictions 会降低性能(调用约定)。在这种情况下,您可能根本不需要对齐堆栈(但如果您正在处理 AVX-512,函数可能希望堆栈对齐到 64 字节,如果您正在处理 AVX2,函数可能希望堆栈对齐到 32 字节,以及 ..).

... and how much do you need to substract?

如果您不知道堆栈是否足够对齐;然后通常使用 AND 来对齐堆栈(例如,可能 and rsp,0xFFFFFFFFFFFFFFF0 将堆栈对齐到 16 字节边界)。这也意味着您需要将旧的堆栈指针存储在某个地方,以便您可以恢复它;这通常意味着还有 4 条指令(push rbpmov rbp,rsp 在对齐之前,然后 mov rsp,rbppop rbp 稍后恢复)。

但是;如果您知道您的调用者为您对齐堆栈(并且您调用的函数需要相同或更少的对齐方式),那么您可以通过跟踪您压入堆栈的量来计算要减去的额外数量。例如,如果调用者将堆栈对齐为 32 字节,然后将四个 64 位(8 字节)值压入堆栈,call 指令将压入另一个 64 位值(return 地址);那么它总共是 5*8 = 40 个字节;所以你知道如果你想对齐到 16 个字节,你需要再减去 8 个字节来使总数为 48 个字节,或者如果你想对齐到 32 个字节,你需要再减去 24 个字节来使总数为 64 个字节。这也避免了保存原始堆栈指针的需要(你可以添加你以后减去的任何东西)所以它可以节省 4 条指令。

当然(对于“纯汇编”)你会查看你调用的所有函数的要求并选择最坏的情况并将堆栈对齐一次(并避免多次不同地对齐堆栈,一次对于您调用的每个函数);你可能会说“我的函数要求堆栈与我调用的函数的最坏情况对齐”以确保你可以计算要减去多少(并避免更昂贵的“AND with ...”方法) .但是(对于“纯组装”)这会给您的调用者带来负担(谁可能会给他们的调用者带来负担,谁可能......)所以它会使性能变差(调用链中的所有祖先都必须做额外的工作,这样你就可以避免更少的工作)。换一种说法;对于“纯组装”;达到最高 efficiency/performance 需要大量工作(确定 if/when 堆栈应该对齐多少,并尽量减少确保堆栈在必要时对齐的费用)。

这也是编译器将对齐放在调用约定中的部分原因 - 所需的“大部分时间不太可能是最佳的”标准对齐使编译器更容易。