了解堆栈对齐

Understanding stack alignment

我正在阅读 Intel manual 有关堆栈框架的内容。有人指出

The end of the input argument area shall be aligned on a 16 (32, if __m256 is passed on stack) byte boundary.

不太明白什么意思。是不是说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(我们只推送了一个字节)。

rsp % 16 == 0_start - 那是 OS 入口点。它不是函数(堆栈上没有 return 地址,而是 RSP 指向 argc)。 与函数不同,RSP 在进入 _start 时按 16 对齐,如 x86-64 System V ABI 所指定。

_start开始,您可以立即调用一个函数,而无需调整堆栈,因为堆栈应该在call之前对齐。 call 本身将添加 8B 的 return 地址,您可以在输入时期待 rsp % 16 == 8,从 16 字节对齐中再推一次。这在进入任何函数时得到保证1.

进入应用程序后,您可以信任内核为您提供 16 字节 RSP 对齐,或者您可以在调用任何其他符合 ABI 的代码之前手动将堆栈与 and rsp, -16 对齐。 (或者如果你打算使用C运行time lib,那么你app代码的入口应该是main,让libc的crt启动代码代码运行为_start . main 和其他任何函数一样是一个普通函数,因此 RSP & 0xF == 0x8 在它最终被调用时进入它。)

脚注 1:除非您使用更改 ABI 的特殊选项进行构建,例如 -mpreferred-stack-boundary=3 而不是默认值 4。但这会使在没有它的情况下编译的任何代码中调用函数变得不安全。例如glibc scanf Segmentation faults when called from a function that doesn't align RSP


Now, after pushing the content of rsp became 0x7fffffffdce8. Is it a violation of the alignment requirements?

是的,如果你在那个时候 call 一些更复杂的函数,例如 printf 带有非平凡的参数(所以它会使用 SSE 指令来实现),它很可能会出现段错误.


关于push byte 0xFF

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

顺便说一句,在这种情况下使用 -w+all 选项使 NASM 发出(有点奇怪,但至少你可以调查)警告:

warning: signed byte value exceeds bounds

例如合法 push word 0xFF 只会将两个字节压入堆栈,字值为 0x00FF


如何对齐堆栈:如果您已经知道初始对齐,只需在调用一些需要子例程的 ABI 之前根据需要进行调整(在常见的 64b 代码中,通常很简单,要么不压入任何东西,要么再做一次冗余压入, 比如 push rbp).

如果不确定对齐,用一些空余的寄存器存放原来的rsp(经常用到rbp,所以也起到栈帧指针的作用),然后and rsp,-16 清除底部位。

请记住,在创建您自己的符合 ABI 的子例程时,堆栈在 call 之前对齐,因此在输入时为 -8B。同样,简单的 push rbp 通常足以同时解决多个问题,保留 rbp 值(因此 mov rbp, rsp 可以“免费”)并为子程序的其余部分对齐堆栈。


编辑:关于编码、源大小和直接大小...

不幸的是,我不是 100% 确定在 NASM 中应该如何定义它,但我认为实际上 push 定义是如此复杂,以至于它破坏了 [=112] =] 语法(将当前语法用尽到无法指定是指操作数大小还是源立即数大小的程度,因此默认大小说明符主要是操作数大小并在某些情况下影响立即数)。

通过使用 push byte 0xFF,NASM 会将 byte 部分也作为“操作数大小”,而不仅仅是直接大小。并且 byte 不是推送的合法操作数大小,因此 NASM 将改为选择 qword 作为 64b 模式的默认值。然后它也会将 byte 视为直接大小,并将 0xFF 符号扩展为 qword。 IE。在我看来,这有点未定义的行为。 NASM 创作者可能不希望您指定直接大小,因为 NASM 针对大小进行了优化,因此当您执行 push word -1 时,它会 assemble 作为“推词”操作数 imm8”。您可以用另一种方式覆盖它,以确保您通过 push strict word -1.

获得 imm16

查看各种组合产生的机器码(在 64b 模式下)(其中一些严格来说至少值得警告,甚至是错误,比如“strict qword”只产生 imm32,而不是 imm64(如 imm64操作码当然不存在)...甚至没有提到 dword 变体实际上是 qword 操作数大小,你不能在 64b 模式下使用 32b 操作数大小):

 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

无论如何,我想不会有太多人对此感到困扰,因为在 64b 模式下,您通常希望 qword push (rsp -= 8) 以尽可能短的方式立即编码,所以您只需编写 push -1 并让 NASM 自己处理 imm8 优化,当然期望 rsp 改变 -8。在其他情况下,他们可能希望您知道合法的操作数大小,并且根本不使用 byte

如果您认为这是不可接受的,我会在 NASM forum/bugzilla/somewhere 上提出这个问题,它应该如何工作。就我个人而言,当前的行为对我来说“足够好”(这两者都有道理,而且我不时快速查看列表文件以验证机器代码字节中没有令人讨厌的惊喜并且它着陆了正如预期的那样)。也就是说,我主要是代码大小介绍,所以我知道生成的每个字节及其用途。如果 NASM 突然产生 imm16 而不是预期的 imm8,我会在二进制大小上看到它并进行调查。