理解堆栈对齐

St.*_*rio 5 assembly x86-64

我正在阅读有关堆栈帧的英特尔手册。注意到

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

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

我试着用它做实验并编写了非常简单的程序:

section .text
    global _start

_start:
    push byte 0xFF

    ;SYS_exit syscall
Run Code Online (Sandbox Code Playgroud)

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

现在,在推送内容后rsp变成了0x7fffffffdce8. 是否违反对齐要求?

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

Ped*_*d7g 6

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

在应用程序进入时,请确保在调用任何其他符合 ABI 的代码之前手动对齐堆栈(或者如果您打算使用 C 运行时库,那么您的应用程序代码的入口点应该是main,并让 crtlib 拥有它自己的 init代码在_start) 处运行。


现在,在推送内容后rsp变成了0x7fffffffdce8. 是否违反对齐要求?

是的,如果此时您想要call一些更复杂的函数,例如printf带有非平凡参数(因此它将使用 SSE 指令进行实现),则很可能会出现段错误。


关于push byte 0xFF

这不是 64b 模式下的合法指令(甚至在 16 位和 32 位模式下也不合法)(在byte操作数目标大小的意义上不合法,byte立即作为源值是合法的,但操作数大小只能是 16、32 或 64 位),所以NASM 将猜测目标大小(任何来自合法的,自然选择qword64b 模式),并将猜测的目标大小与imm8源一起使用。

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

warning: signed byte value exceeds bounds
Run Code Online (Sandbox Code Playgroud)

例如,legitpush word 0xFF只会将两个字节推入堆栈,字值0x00FF


如何对齐堆栈:如果您已经知道初始对齐,只需在调用一些需要 ABI 的子例程之前根据需要进行调整(在常见的 64b 代码中,通常很简单,要么不推送任何内容,要么执行更多冗余推送,例如push rbp)。

如果不确定对齐方式,可以使用一些空闲寄存器来存储原始寄存器rsp(经常rbp使用,因此它也用作堆栈帧指针),然后and rsp,-16清除底部位。

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


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

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

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

查看由各种组合(在 64b 模式下)产生的机器代码(其中一些严格地说至少值得警告,甚至错误,例如“严格的 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
Run Code Online (Sandbox Code Playgroud)

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

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

  • _“所以 NASM 会猜测目标大小(任何合法的,自然选择 64b 模式下的 qword),并对值进行符号扩展。”_ 为什么 NASM 会在这里进行任何符号扩展?编码`6A ib`(`push imm8`)在64位模式下完全有效。会有符号扩展,但_assembler_ 不需要这样做。 (2认同)
  • @Ped7g:您不需要在进程入口处手动对齐堆栈:`rsp` 在 `_start` 处是 16 字节对齐的,ABI 保证了这一点。(听起来你的第一段是说你希望 `rsp+8` 在 `_start` 处对齐,但是 `_start` 不是一个被任何东西 `call`ed` 的函数。进程入口状态不同于函数调用状态。因此,如果内核遵循 ABI(它确实遵循 ABI),那么如果在通过执行 `push` 使堆栈未对齐后调用任何(非私有)函数,则 OP 的代码将违反 ABI。) (2认同)
  • @St.Antario 嗯.. 也许其他方式.. 64b 模式下的 `68` `push` 操作码默认读取 4 个字节 (imm32),并将符号扩展的 8 个字节 (64b) 推送到堆栈,并调整`rsp` 为 -8。如果你给它加上 `66` 前缀,它只会读取两个字节,并将两个字节存储到堆栈中,将 `rsp` 调整为 -2。64b 中没有 `push imm` 操作码将 `rsp` 调整为 -4,或者使用 `imm64` 作为源值,即你可以使用作为源值 `imm8/16/32` (`6A`/`66 68 `/`68`) 和操作数大小 16 或 64(`66` 前缀或无前缀)。 (2认同)