在 x86-64 中,当我们想要将某些东西压入堆栈时,我们总是执行 pushq 吗?

In x86-64 do we always do pushq when we want to push something on the stack?

因为在 x86-64 中所有 16 个寄存器都可以是 8 个字节,所以在函数调用开始时函数(被调用方)必须推送被调用方保存的寄存器(%rbx、%rbp 和 %r12-15)它想使用,它无法知道调用者是否在这些寄存器中存储了 64 位或 32 位或 16 位或 8 位值,所以他们总是必须调用 pushq 来推送将这些寄存器的整个 8 个字节压入堆栈,而不是 pushl?换句话说,pushlpushw 曾经在 x86-64 中使用过吗?

The entire register is call-preserved,不仅仅是低位双字或单词。 普通函数总是save/restore整个 qword 寄存器因为这是唯一安全的做法,而且它的效率也足够高,因此没有理由为函数创建一种机制来知道它们何时运行可以做任何其他事情。

在写入 32 位低半部分后读取完整寄存器总是有效的,因为 32-bit register writes implicitly zero-extend to 64-bit. Reading a 64-bit register after the caller wrote the low 8 or 16-bits could cause a partial-register stall on Intel P6-family microarchitectures, if the caller was careless about how it used the register before making a call. On modern uarches (not Intel P6), the 8/16-bit operand size register write already paid 。 (我掩盖了一些细节,比如部分 AH 重命名仍然是现代英特尔的事情,包括 Skylake)


虽然您可以使用sub , %rsp移动堆栈指针并使用movlmovb来存储32位或8位某些寄存器的低位部分,只有当您了解调用者如何使用寄存器并希望相应地进行优化时,这才是安全的。 (使您的函数依赖于调用者的内部结构,而不仅仅是 ABI)。即使这是某些辅助函数的一个选项,通常也不值得将堆栈帧的占用空间减少几个字节。

(函数很少使用 16 位数据,但 8 位数据并不少见。boolchar 很常见。编译器通常使用 movzx aka movzbl 从内存加载到零扩展到完整寄存器,并且通常可以使用 32 位操作数大小来避免实际处理部分寄存器恶作剧。但是他们不会关心你是否 saved/restored 只有低带有 mov store / movzbl reload 的 8 位,用于编译保持零扩展 bool 或 char 的寄存器。)

Are pushl and pushw ever used in x86-64?

pushl 在 64 位模式下根本不存在push 的 32 位操作数大小是 not encodeable even with a REX.W=0 prefix.

pushw 可编码但从未在 32 位或 64 位模式下被编译器使用。 (并且通常对人类没有用或不推荐,除了奇怪的角落案例或 hack,比如 shellcode。我在代码高尔夫(优化代码大小)合并时确实使用过一次 two 16-bit values into one register for adler-32)。

如果编译器确实想要进行字或双字存储(例如,在未优化的构建中溢出传入的寄存器参数),它只会使用 movwmovl.

您通常希望使堆栈按 16 对齐,以便您准备好进行另一个函数调用;这就是我在上面建议 sub , %rsp 的原因。 (在函数入口处,RSP 指向调用者推送的 return 地址。RSP+8 和 RSP-8 是 16 字节对齐的。)


pushq %reg 在现代 CPU 上非常高效:当它只需要将堆栈指针移动 8 个字节时,使用 outside the OoO exec back-end. It's so efficient that 而不是 sub , %rsp 在 CPU 上解码为单个 uop,例如在另一个调用之前重新对齐堆栈。

pushq %reg 是 1 字节指令(或 2 字节 r8..r15,包括 REX 前缀)