理解堆栈对齐

时间:2018-02-08 11:03:35

标签: assembly x86-64

我正在阅读有关Stack Frames的Intel manual。有人指出

  

输入参数区域的末尾应对齐16(32,如果   __m256在堆栈上传递。字节边界。

我不太明白这意味着什么。这是否意味着rsp应该指向始终在16上对齐的地址?

我试着用它来试验并编写了一个非常简单的程序:

section .text
    global _start

_start:
    push byte 0xFF

    ;SYS_exit syscall

我使用gdb运行它并注意到在执行push指令rsp = 0x7fffffffdcf0之前。它实际上是在16 x/1xg $rsp返回0x0000000000000001

现在,在推送rsp的内容成为0x7fffffffdce8之后。这违反了对齐要求吗?

我还注意到x/1xg $rsp返回了0xffffffffffffffff。这意味着我们将1设置为接下来的8个字节,而不仅仅是push指令中指定的字节。为什么?在推到x/1xg $rsp之后我期望0x00000000000000FF的输出(我们只推了一个字节)。

1 个答案:

答案 0 :(得分:4)

<{> 1}} rsp - 操作系统的入口点,它实际上违反了ABI,因为堆栈应该在_start之前对齐,因此{{ 1}}本身将添加8B的返回地址,并且您可以预期call在进入时未对齐-8。

在应用程序输入时,请确保在调用符合ABI的任何其他代码之前手动对齐堆栈(或者如果您计划使用C运行时库,则应用程序代码的入口点应为call,并且让crtlib让它自己的初始代码在rsp运行。

  

现在,在推送main的内容成为_start之后。这违反了对齐要求吗?

是的,如果你在那一点rsp有一些更复杂的函数,例如0x7fffffffdce8有非平凡的参数(所以它会使用SSE指令来实现),它很可能是段错误。 / p>

关于call

这在64b模式下是不合法的指令(即使在16位和32位模式下也是如此)(printf操作数目标大小意义上不合法,push byte 0xFF立即作为源值是合法,但operand size can be only 16, 32 or 64 bits),因此NASM将猜测目标大小(任何来自合法的大小,在64b模式下自然选择byte),并使用猜测的目标大小与byte来自源。

在这种情况下,BTW使用qword选项使NASM发出(有点奇怪,但至少可以调查)警告:

imm8

例如,合法-w+all只会将两个字节推送到字符串warning: signed byte value exceeds bounds 的堆栈。

如何对齐堆栈:如果您已经知道初始对齐,只需在调用某些需要子程序的ABI之前根据需要进行调整(通常64b代码通常就像不推送任何东西一样简单,或者执行一次多余的推送,如push word 0xFF)。

如果您不确定对齐方式,请使用一些备用寄存器来存储原始0x00FF(通常使用push rbp,因此它也可用作堆栈帧指针),然后rsp清除底部的位。

请记住,在创建自己的ABI符合子例程时,该堆栈在rbp之前对齐,因此在进入时为-8B。同样简单的and rsp,-16通常足以同时解决多个问题,保留call值(因此push rbp可以&#34;免费&#34;)并对齐堆栈以供休息子程序。

编辑:关于编码,源大小和即时大小...

不幸的是,我并不是100%确定在NASM中应该如何定义它,但我认为实际上rbp定义是如此复杂,以至于它破坏了NASM语法(令人筋疲力尽当前语法指向您无法指定操作数大小或源即时大小的点,因此默认情况下,大小说明符主要是操作数大小,在某些情况下会影响立即数。)

通过使用mov rbp, rsp,NASM将push部分作为&#34;操作数大小&#34;,而不仅仅是立即大小。并且push byte 0xFF不是推送的合法操作数大小,因此NASM将在64b模式下默认选择byte。然后,它还会将byte视为即时大小,并将qword签名扩展为byte。即这对我来说是一种未定义的行为。 NASM创建者可能不希望你指定即时大小,因为NASM会对大小进行优化,因此当你执行0xFF时,它会将其组合为&#34;推送操作数imm8&#34;。你可以用另一种方式覆盖它,以确保你qword获得imm16。

查看由各种组合产生的机器代码(在64b模式下)(其中一些严格说话至少值得警告,甚至错误,例如&#34;严格qword&#34;仅产生imm32,而不是imm64 (因为当然不存在imm64操作码)...甚至没有提到push word -1变体实际上是push strict word -1操作数大小,你不能在64b模式下使用32b操作数大小):< / p>

dword

无论如何,我想没有太多人为此烦恼,因为在64b模式下你通常需要qword push(qword)并以最短的方式立即编码,所以你只需要写 6 00000000 6AFF push -1 7 00000002 6AFF push strict byte 0xFF 8 ****************** warning: signed byte value exceeds bounds 9 00000004 6AFF push byte 0xFF 10 ****************** warning: signed byte value exceeds bounds 11 00000006 6AFF push strict byte -1 12 00000008 6AFF push byte -1 13 0000000A 6668FF00 push strict word 0xFF 14 0000000E 6668FF00 push word 0xFF 15 00000012 6668FFFF push strict word -1 16 00000016 666AFF push word -1 17 00000019 68FF000000 push strict dword 0xFF 18 0000001E 68FF000000 push dword 0xFF 19 00000023 68FFFFFFFF push strict dword -1 20 00000028 6AFF push dword -1 21 0000002A 68FF000000 push strict qword 0xFF 22 0000002F 68FF000000 push qword 0xFF 23 00000034 68FFFFFFFF push strict qword -1 24 00000039 6AFF push qword -1 和让NASM处理rsp -= 8优化本身,期望push -1当然要改变-8。在其他情况下,他们可能希望您了解合法的操作数大小,而不是使用imm8

如果您认为这是不可接受的,我会在NASM论坛/ bugzilla /某处提出这个问题,它应该如何正常工作。就我个人而言,目前的行为已经足够好了#34;对我来说(有道理,加上我不时快速查看列表文件,以验证机器代码字节中没有令人讨厌的惊喜,并按预期落地)。也就是说,我主要是代码大小的介绍,所以我知道生成的每个字节及其目的。如果NASM突然产生rsp而不是预期的byte,我会在二进制大小上看到它并进行调查。