assembly 组装中“对齐堆叠”是什么意思?

6ss1mwsb  于 2023-01-26  发布在  其他
关注(0)|答案(3)|浏览(165)

堆栈对齐在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
koaltpgm

koaltpgm1#

寻址通常是基于字节的。一个唯一的地址指向一个字节(可以是一个字或双字等的第一个字节,但引用该地址)。
在任何一种编号系统中,最低有效位都是0次幂(数字1)的值基数。次低的1次幂基数,次低的2次幂基数。在十进制中,这是个位列、十位列和百位列。在二进制中,一、二、四...对齐意味着可被整除,也意味着最低有效位是零。
您总是在字节边界上“对齐”,但二进制中的16位边界意味着最低有效位为零,32位对齐两个零,依此类推。
0x 1234在16位和32位边界上对齐,但在64位边界上不对齐
0x 1235未对齐(字节对齐实际上并不重要)
0x 1236在16位边界对齐
0x 1230四个零,所以16、32、64、128位不是字节。2、4、8、16字节。
原因在于,出于性能原因,所有存储器都有固定的宽度和数据总线,一旦实现,就不能神奇地添加或删除逻辑中的导线,这是一个物理限制,您可以选择不将所有导线用作设计的一部分,但不能添加任何导线。
因此,虽然x86总线更宽,让我们假设你有一个32位宽的数据总线以及一个32位宽的内存(想想缓存,也想想dram,但我们一般不直接访问dram)。
如果我想将16位0xAABB保存到小字节序机器中的地址0x 1001,则0x 1001将获得0xBB,0x 1002将获得0xAA。如果我有一个32位数据总线,并且在其远端有一个32位存储器,那么我可以移动这16位,如果我为此设计了总线,通过将0xXXAABBXX写入地址0x 1000,其中字节通道掩码0 b 0110告知存储器控制器使用与基于字节的地址0x 1000相关联的32位存储器,且总线上的字节通道掩码告知控制器仅保存中间两个字节,外面的两个是无关紧要的。
存储器通常是固定宽度的,因此所有事务必须是全宽度的,它将读取32位,用0xAABB修改中间的16位,并将32位写回。这当然是低效的。更糟糕的是将0xAABB写入0x 1003,这将是两个总线事务,一个用于地址0x 1000处的0xBBXXXXXX,一个用于地址0x 1004处的0xXXXXXXAA。这意味着总线和存储器上的读取-修改-写入都需要大量额外的周期。
现在堆栈对齐规则不会阻止写操作的读-修改-写操作,对于较大传输量的情况,有可能获得性能提升,例如,如果总线为32位,而内存为64位,则传输地址为0x 1000,基于总线设计,看起来像是长度为2的单次传输。总线握手发生,然后两个背靠背时钟数据移动,而不是握手和一个宽度的数据总线为较小的传输,所以你得到了一个增益,如果内存是32位宽,那么它是两个写入没有读修改写入该高速缓存中的sram,非常干净,希望避免读修改写入。
现在,随着事情的发展以及硬件和工具需要堆栈对齐,请执行此操作一段时间。
根据指令集的不同,很明显这里你问的是x86,但是作为一个程序员,你有时可以选择说push a byte on the stack,然后调整它以对齐它,或者如果你要为局部变量腾出空间,这取决于指令集(如果堆栈指针足够通用,能够在其上进行数学运算)您可以简单地进行减法,因此sub sp,#8等同于将两个32位项目压入堆栈,只是为了给两个32位项目腾出空间。
如果规则是32位对齐,并且您压入了一个字节,那么您需要将堆栈指针调整3,以使堆栈指针中的总更改为4字节(32位)的倍数。
你怎么知道是多少,你只需把它加起来。如果是16字节对齐,你压入4,那么你需要再压入12,或者再调整堆栈指针12。
这里的关键是如果每个人都同意保持堆栈对齐,那么你实际上不必查看堆栈指针的低位,你只需在调用其他东西之前跟踪你正在推送和弹出的东西。
如果堆栈与中断处理程序共享(不是真的在你当前运行操作系统的x86中,但在通用处理器的许多其他用例中仍然是可能的)我还没有看到这个规则适用于那里,因为你会看到编译器做一个小于对齐大小的推送或弹出,然后用其他推送或弹出或者减法或加法进行调整。如果中断发生在这些处理程序之间,则处理程序将看到未对齐的堆栈。
有些架构会在未对齐访问时出错,这是保持堆栈对齐的另一个原因。

如果你的代码没有扰乱堆栈,那么你就不需要扰乱堆栈(指针)。仅当通过在堆栈上分配空间来在代码中使用堆栈时(堆栈指针上的压入或数学运算),你是否需要关心,你是否需要知道,你链接这些代码的编译器的约定,并遵守它。如果这都是汇编语言,没有编译器,那么您可以自己决定约定,基本上可以在处理器本身的限制范围内做任何您想做的事情。
从你的标题问题来看,它与汇编完全无关,也与机器码无关。它与你的代码和它做什么有关。汇编语言只是一种语言,你用它来传达你想要调整堆栈指针多少,该指令不关心或不知道任何这样的事情,它获取所提供的常数并针对寄存器使用它。汇编是少数几个允许你在栈指针寄存器上做数学运算的程序之一,所以有这种联系,但是对齐和汇编是不相关的。

41zrol4v

41zrol4v2#

什么时候需要在函数调用之前对齐堆栈,然后......?
当调用的函数需要对齐的堆栈时,需要对齐堆栈。
用其他语言(例如C)编写的函数,以及用汇编语言编写但设计为从其他语言调用的函数,将遵守某种调用约定(包括的不仅仅是堆栈对齐--参数如何传递,参数在哪里,像“红区”之类的东西,等等);且对于64位80 × 86,2个共同调用惯例期望堆栈对准到16字节边界。
在“纯程序集”项目中,您调用的函数是为程序集调用方编写的;程序员可以自由地做他们想做任何事情(例如,无论什么对性能最好),而不关心降低性能的其他语言的限制/约束(调用约定)。在这种情况下,您可能根本不需要对齐堆栈(但如果处理AVX-512,函数可能希望堆栈对齐到64字节,如果你在处理AVX 2,函数可能希望堆栈对齐到32字节,并且..)。
......你需要减去多少?
如果不知道堆栈是否足够对齐;那么对齐堆栈通常是用AND完成的(例如and rsp,0xFFFFFFFFFFFFFFF0可以将堆栈对齐到16字节的边界)。这也意味着你需要将旧的堆栈指针存储在某个地方以便你可以恢复它;这通常意味着另外4个指令(对准之前的push rbpmov rbp,rsp,然后是mov rsp,rbppop rbp,以便稍后恢复)。
然而;如果你知道你的调用者已经帮你对齐了堆栈(并且您调用的函数需要相同或更少的对齐方式),那么您可以通过跟踪您在堆栈上压入了多少来计算要减去多少额外的内容。例如,如果调用方将堆栈对齐为32字节,将四个64位(8字节)值压入堆栈,call指令将压入另一个64位值(返回地址);那么它的总长度就是5*8 = 40字节这样你就知道如果你想对齐16个字节,你需要再减去8个字节,使总的48个字节,或者如果你想对齐32个字节,你需要再减去24个字节,使总的64个字节。这也避免了保存原始堆栈指针的需要(你可以添加你后来减去的任何东西),这样就可以节省4条指令。
当然(对于“纯汇编”),您需要查看所调用的所有函数的需求,选择最坏的情况,并将堆栈与之对齐一次(避免多次对齐堆栈,每次调用一个函数);并且您可能会说“我的函数要求堆栈与我调用的函数的任何最坏情况对齐”以确保您可以计算要减去多少(并且避免更昂贵的“与...”方法)。然而(对于“纯汇编”)这将负担加到您的调用者身上(调用者可能将负担加到其调用者身上,谁可能......),因此它会使性能变差(调用链中的所有祖先都必须做额外的工作,以便您可以避免更少的工作)。用于“纯组装”;实现最高效率/性能需要大量的工作(以确定是否/何时堆叠应当对准多少,并最小化确保堆叠在必要的地方对准的费用)。
这也是编译器将对齐放在调用约定中的部分原因--所需的“大多数时间不太可能是最优的”标准对齐使编译器更容易。

ruyhziif

ruyhziif3#

我想我知道为什么行sub rsp,8出现在调用之前了(但我不是Maven)-我以完成干墙为生。所以调用指令实际上将执行2条指令。首先它将把返回地址推到堆栈上,然后第二它将jmp到函数。返回地址是8个字节,所以这将导致堆栈失去对齐。因此额外的sub rsp,8将在函数执行之前修复未对齐。
然后-为了从函数返回,RET指令将从堆栈中弹出返回地址,然后将jmp添加到该地址。因此,从函数返回时,堆栈将再次未对齐,因此调用后的行将添加rsp,8以再次修复堆栈对齐。

相关问题