地址通常是以字节为单位。唯一的地址指向一个字节(可以是一个字或双字中的第一个字节等,但与该地址相关联)。
在任何进位制中,最不重要的数字保持基数的幂为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),如果我想要将16位0xAABB保存到小端机器的地址0x1001,那么0x1001将获得0xBB,0x1002将获得0xAA。如果我在总线上设计了这个功能,那么如果在远端有一个32位数据总线和一个32位存储器,我可以通过向地址0x1000写入0xXXAABBXX并设置字节掩码为0b0110来移动这些16位,告诉内存控制器使用与基于字节的地址0x1000相关联的32位存储器,总线上的字节掩码告诉控制器只保存中间的两个字节,外面的两个字节是无关紧要的。
存储器通常是固定宽度的,所以所有事务都必须是全宽度的,它会读取32位,将其中间的16位修改为0xAABB,然后将32位写回。这当然是低效的。更糟糕的是,将0xAABB写入0x1003将需要两个总线事务,一个是地址0x1000处的0xBBXXXXXX,另一个是地址0x1004处的0xXXXXXXAA。这将在总线和内存上产生大量额外的周期。
现在,堆栈对齐规则不会阻止写操作中的读-修改-写。 对于大块数据传输,例如如果总线是32位,内存也是32位,并且您将64位数据传输到地址0x1000,则可以根据总线设计将其视为长度为2的单个传输。 总线握手发生,然后两个相邻的时钟数据移动,而不是对于较小的数据传输进行握手和总线数据宽度为一次写入两次。 因此,如果内存宽度为32位,则是两个写入,而不是带有读-修改-写的SRAM缓存。要避免读-修改-写。
现在,随着事物的发展和硬件和工具的需求,需要堆栈对齐。
根据指令集,显然这里问的是x86,但作为程序员,您有时可以选择将一个字节推送到堆栈上,然后调整它以使其对齐。 或者,如果您要为本地变量腾出空间,则可以根据指令集(如果堆栈指针足够通用,可以在其上进行数学计算),只需减去sub sp,#8,即推送两个32位项到堆栈上,仅为了腾出两个32位项的空间。
如果规则是32位对齐,而您推送了一个字节,则需要将堆栈指针调整3以使堆栈指针的总变化为4字节(32位)的倍数。
如何知道需要多少,请简单地计数。 如果它是16字节对齐,并且您推送了4个,则需要再推送12个或将堆栈指针进一步调整12个。
关键在于,如果所有人都同意保持堆栈对齐,则实际上无需查看堆栈指针的较低位,您只需在调用其他内容之前跟踪推送和弹出的内容即可。
如果堆栈与中断处理程序共享(当前的x86运行操作系统可能不会,但在许多其他通用处理器的用例中仍然可能),我没有看到该规则适用于其中,因为编译器会执行小于对齐大小的推送或弹出,然后使用其他推送或弹出或减法或加法进行调整。 如果在它们之间发生中断,则处理程序将看到不对齐的堆栈。
某些体系结构会在未对齐访问时出错,这是保持堆栈对齐的另一个原因。
如果您的代码不涉及堆栈,则不需要涉及堆栈(指针)。 只有在代码中使用堆栈通过在堆栈上分配空间(推送或堆栈指针上的数学运算)时,您需要关心并了解链接此代码的编译器的约定,并符合该约定。 如果这全部是汇编语言而没有编译器,则自己决定约定,并在处理器本身的限制范围内进行任何操作。
从你的标题问题来看,这与汇编语言或机器码没有任何关系。它涉及到你的代码和它的功能。汇编语言只是一种传达调整堆栈指针所需的数量的语言,指令并不知道或关心任何这样的事情,它使用提供的常量对寄存器进行操作。汇编语言是为数不多,如果不是唯一允许你对堆栈指针寄存器进行数学运算的语言,所以有这个联系。但是对齐和汇编语言之间没有关联。
call
指令将返回地址压入栈中,栈会被错位8字节。为了使栈重新对齐到16字节边界,可以通过从RSP(栈指针)减去8或将一个64位寄存器推入栈中来实现。 - Michael Petch