为啥 rbp 和 rsp 被称为通用寄存器?

Posted

技术标签:

【中文标题】为啥 rbp 和 rsp 被称为通用寄存器?【英文标题】:Why are rbp and rsp called general purpose registers?为什么 rbp 和 rsp 被称为通用寄存器? 【发布时间】:2016-07-31 11:29:58 【问题描述】:

根据 x64 中的 Intel,以下寄存器称为通用寄存器(RAX、RBX、RCX、RDX、RBP、RSI、RDI、RSP 和 R8-R15)https://software.intel.com/en-us/articles/introduction-to-x64-assembly。

在下面的文章中,写到 RBP 和 RSP 是特殊用途的寄存器(RBP 指向当前堆栈帧的底部,RSP 指向当前堆栈帧的顶部)。 https://www.recurse.com/blog/7-understanding-c-by-learning-assembly

现在我有两个相互矛盾的陈述。英特尔声明应该是值得信赖的,但什么是正确的,为什么 RBP 和 RSP 被称为通用?

感谢您的帮助。

【问题讨论】:

您可以将两者用作通用寄存器,这意味着通常的算术和逻辑指令可以很好地使用它们。 rbp 非常通用,帧指针只是惯例。 对于某些指令,每个寄存器都有一些特殊性(R8-R15 除外)。对于 RSP,它对于 push/pop/call/ret 是特殊的,因此大多数代码从不将它用于其他任何事情。但是在受控条件(如无信号处理程序)中,您没有必须将它用于堆栈指针。例如您可以使用它来读取带有pop 的循环中的数组,例如in this code-golf answer。 (我实际上在 32 位代码中使用了 esp,但相同的区别)。 我想如果你将“特殊性”的定义扩展到编码,即使r13 也有点特别,尽管它并不是真正的功能,因为你仍然可以有效地使用每种寻址模式(甚至如果组件有时会为您设置隐藏的零位移)。 RBP 可与-fomit-frame-pointer 一起用于一般用途。虽然 RSP 更难[ @PeterCordes R11 对系统调用有特殊作用 【参考方案1】:

如果一个寄存器可以是add 的操作数,或者用于寻址模式,那么它就是“通用”,而不是像FS 段寄存器或RIP 这样的寄存器。 GP 寄存器也称为“整数寄存器”,尽管其他类型的寄存器也可以保存整数。

在计算机架构中,CPU 在内部处理整数寄存器/指令与 FP/SIMD 寄存器/指令是很常见的。例如Intel Sandybridge-family CPUs 具有单独的物理寄存器文件,用于重命名 GP 整数与 FP/向量寄存器。这些被简单地称为整数与 FP 寄存器文件。 (其中 FP 是内核不需要保存/恢复以使用 GP 寄存器同时保持用户空间的 FPU/SIMD 状态不变的所有内容的简写。)FP 寄存器文件中的每个条目都是 256 位宽(到保存一个 AVX ymm 向量),但整数寄存器文件条目只需 64 位宽。

在重命名段寄存器 (Skylake does not) 的 CPU 上,我猜这将是整数状态的一部分,RFLAGS + RIP 也是如此。但是当我们说“整数寄存器”时,我们通常指的是一个通用寄存器。


“通用”在此用法中的意思是“数据或地址”,而不是像 m68k 这样的 ISA,其中您有 d0..7 数据寄存器和 a0..7 地址寄存器,其中 16 个都是整数寄存器。不管寄存器是如何正常使用的,通用是关于它如何可以使用。


每个寄存器对某些指令都有一些特殊性,除了一些使用 x86-64 添加的全新寄存器:R8-R15。这些并不能取消它们作为通用用途的资格 原始 8 的(低 16 位)可以追溯到 8086,即使在原始 8086 中也隐含使用它们。

对于 RSP,它对 push/pop/call/ret 是特殊的,因此大多数代码从不将它用于其他任何事情。 (在内核模式下,异步用于中断,所以你真的不能像在用户空间代码中那样将它存储在某个地方以获得额外的 GP 寄存器:Is ESP as general-purpose as EAX?)

但在受控条件(如无信号处理程序)中,您不必将 RSP 用于堆栈指针。例如您可以使用它在带有 pop 的循环中读取数组,例如in this code-golf answer。 (我实际上在 32 位代码中使用了 esp,但相同的区别:pop 在 Skylake 上比 lodsd 快,而两者都是 1 字节。)


每个寄存器的隐式使用和特殊性:

有关部分列表,另请参阅x86 Assembly - Why is [e]bx preserved in calling conventions?。

我主要将此限制为用户空间指令,尤其是现代编译器实际上可能从 C 或 C++ 代码发出的指令。对于有很多隐含用途的 reg,我并不想详尽无遗。

rax: 单操作数 [i]mul / [i]div / cdq / cdqe、字符串指令 (stos)、cmpxchg 等。以及用于许多立即指令(如 2)的特殊较短编码-byte cmp al, 1 或 5 字节 add eax, 12345(无 ModRM 字节)。另见codegolf.SE Tips for golfing in x86/x64 machine code。

还有xchg-with-eax,这是0x90 nop的来源(在nop成为x86-64中单独记录的指令之前,因为xchg eax,eax将eax零扩展为RAX,因此可以' t 使用0x90 编码。但xchg rax,rax 可以仍然组装成 REX.W=1 0x90。)

rcx:移位计数,rep-string 计数,the slow loop instruction

rdxrdx:rax 用于除法运算,cwd / cdq / cqo 为它们设置。 rdtsc。 BMI2 mulx.

rbx: 8086 xlatbcpuid 使用所有四个 EAX..EDX。 486cmpxchg8b,x86-64 cmpxchg16b。大多数 32 位编译器将为 std::atomic<long long>::compare_exchange_weak 生成 cmpxchg8。 (纯加载/纯存储可以使用 SSE MOVQ 或 x87 fild/fistp,不过,如果针对 Pentium 或更高版本。)64 位编译器将使用 64 位 lock cmpxchg,而不是 cmpxchg8b。

一些 64 位编译器会为 atomic<struct_16_bytes> 生成 cmpxchg16b。 RBX 对原始 8 的隐式使用最少,但 lock cmpxchg16b 是少数几个会实际使用的编译器之一。

rsi/rdi:字符串操作,包括一些编译器有时内联的rep movsb。 (在某些情况下,gcc 还为字符串文字内联 rep cmpsb,但这可能不是最佳的)。

rbp: leave(仅比mov rsp, rbp / pop rbp 慢1 uop。gcc 实际上在带有帧指针的函数中使用它,而它不能只是pop rbp)。还有一个非常慢的enter,没人用过。

rsp:堆栈操作:push/pop/call/ret 和leave。 (和enter)。并且在内核模式(不是用户空间)中,硬件使用异步来保存中断上下文。这就是内核代码不能有红区的原因。

r11: syscall/sysret 使用它来保存/恢复用户空间的 RFLAGS。 (与 RCX 一起保存/恢复用户空间的 RIP)。

寻址模式编码特殊情况:

(另请参阅rbp not allowed as SIB base?,这只是关于寻址模式,我复制了这个答案的这一部分。)

rbp/r13 不能是没有位移的基址寄存器:该编码意味着:(在 ModRM 中)rel32(RIP 相对),或(在 SIB 中)disp32 没有基址登记。 (r13 在 ModRM/SIB 中使用相同的 3 位,因此该选择通过不让指令长度解码器查看the REX.B bit 来获得第 4 个基寄存器位来简化解码)。 [r13] 组装成 [r13 + disp8=0][r13+rdx] 组装成 [rdx+r13](通过交换基数/索引来避免问题)。

rsp/r12 作为基址寄存器总是需要一个 SIB 字节。 (base=RSP 的 ModR/M 编码是用于发送 SIB 字节信号的转义码,同样,如果 r12 的处理方式不同,则更多的解码器将不得不关心 REX 前缀。

rsp 不能是索引寄存器。这使得编码[rsp] 成为可能,这比[rsp + rsp] 更有用。 (英特尔本可以为 32 位寻址模式设计 ModRM/SIB 编码(386 中的新功能),因此只有 base=ESP 才有可能使用无索引的 SIB。这将使[eax + esp*4] 成为可能,并且只排除[esp + esp*1/2/4/8] . 但这没有用,因此他们通过使 index=ESP 成为无索引的代码来简化硬件,无论基数如何。这允许两种冗余方式来编码任何基数或基数 + disp 寻址模式:有或没有 SIB。)

r12 可以是索引寄存器。与其他情况不同,这不会影响指令长度解码。此外,它不能像其他情况一样使用更长的编码来解决。 AMD 希望 AMD64 的寄存器集尽可能正交,因此他们会花费一些额外的晶体管来检查 REX.X 作为索引/无索引解码的一部分是有道理的。例如,[rsp + r12*4] 需要 index=r12,因此 r12 不完全通用会使 AMD64 成为更糟糕的编译器目标。

   0:   41 8b 03                mov    eax,DWORD PTR [r11]
   3:   41 8b 04 24             mov    eax,DWORD PTR [r12]      # needs a SIB like RSP
   7:   41 8b 45 00             mov    eax,DWORD PTR [r13+0x0]  # needs a disp8 like RBP
   b:   41 8b 06                mov    eax,DWORD PTR [r14]
   e:   41 8b 07                mov    eax,DWORD PTR [r15]
  11:   43 8b 04 e3             mov    eax,DWORD PTR [r11+r12*8] # *can* be an index

当所有寄存器可以用于任何事情时,编译器喜欢它,只限制少数特殊情况操作的寄存器分配。这就是寄存器正交性的含义。

【讨论】:

另外寄存器DX在IN、OUT、INS、OUTS指令中是特殊的。 @vitsoft:正如我所说,我并不想详尽,只是为了涵盖实际上仍然相关的用途,尤其是对于编译器生成的代码。仅在没有其他内容的情况下提及晦涩的用途。【参考方案2】:

取消引用 rbp 可能会导致 #SS(堆栈段)错误。

最近,我遇到了一个带有“堆栈段错误”的 linux 内核崩溃。

crash> dmesg
[...]
stack segment: 0000 [#1] SMP
[...]
RIP: 0010:[<ffffffff8125fa8b>]  lock_get_status+0x9b/0x3b0
RSP: 0018:ffff89954a317d90  EFLAGS: 00010282
[...]
RBP: 800000fa8c251867 R08: 0000000000001000 R09: 000000000000ffff
[...]
crash> dis lock_get_status+0x9b
0xffffffff8125fa8b <lock_get_status+0x9b>:      mov    0x28(%rbp),%rax

rbp中的内存地址是非规范地址。这就是这次崩溃的原因。 我从这次崩溃中学到的是,即使通过 rbp 访问 rbp 隐式访问 ss 段寄存器也不会用作堆栈帧基指针。

根据 Intel SDMv1 3.4.1 通用寄存器:

EBP — 指向栈上数据的指针(在 SS 段中)

【讨论】:

取消引用任何其他寄存器中的非规范地址仍然会出错,只是出现#GP 而不是#SS 异常。这并没有降低寄存器的通用性,尤其是在 64 位模式下,段基数和限制固定在 0 并且对于 SS 和 DS 为“无限制”。唯一的区别是您将在非规范地址上获得哪个异常。这在另一个答案上讨论了in comments。 @PeterCordes 感谢您指出这一点。我需要删除这个答案吗? IDK,我正在考虑是否投反对票或建议您删除它。它并没有降低您(或编译器)如何使用它的通用性,但在这种极端情况下它确实会产生不同的行为。如果您将答案修改为解释这不会使其不那么普遍,只是某些错误的调试结果有所不同,那么可能将它放在这里可以帮助某人解开谜团。【参考方案3】:

通用意味着所有这些寄存器都可以与任何使用通用寄存器进行计算的指令一起使用,例如,您无法使用指令指针 (RIP) 或标志寄存器 (RFLAGS) 执行任何操作。

其中一些寄存器被设想用于特定用途,并且通常如此。最关键的是 RSP 和 RBP。

如果您需要将它们用于自己的目的,您应该先保存它们的内容,然后再将其他东西存储在其中,并在完成后将它们恢复为原始值。

【讨论】:

一些编译器可以选择不使用帧指针,在这种情况下,RBP 成为通用计算机。 值得注意的是,使用rpb 作为帧指针本质上完全是约定,并且实际上并没有任何 CPU 支持(事实上,Windows 64 ABI 允许您使用任何寄存器作为帧指针并且不喜欢rbp)。这与rsp 非常不同,后者在硬件级别与其功能紧密绑定,因为它被pushpop 和朋友隐式使用。 @BeeOnRope LEAVE 和 ENTER 指令特别支持使用 RBP 作为帧指针。 RBP 用作基础时也像 RSP 一样是 SS 相对的,而不是像其他的那样是 DS 相对的。现代 x86 代码中不使用 ENTER 指令以及单独的数据和堆栈段,但编译器仍会生成 LEAVE 指令。 RBP 不能用作没有位移的基数这一事实也意味着它通常是用作帧指针的最佳寄存器。它不像 RSP 那样紧密绑定,但是 x86 指令集倾向于使用 RBP 作为帧指针。 @BeeOnRope:是的,它仍然是正确的,但几乎无关紧要。 IIRC,x86-64 要求 SS 和 DS(以及 ES 和 CS)具有 base=0。 IDK 在某些假设的操作系统中,您可以在段描述符中放入哪些其他内容很重要。只有 FS 和 GS 或多或少具有完整的功能。 @BeeOnRope 正如 Peter Cordes 所说,在使用 RBP 和 RSP 作为基础时仍然使用 SS 段,但在实践中几乎没有什么区别。我知道的唯一区别是,如果使用 SS 段而不是其他段寄存器之一,非规范地址将生成堆栈错误而不是一般保护错误。

以上是关于为啥 rbp 和 rsp 被称为通用寄存器?的主要内容,如果未能解决你的问题,请参考以下文章

rsp rbp 寄存器用途

在C ++内联asm中使用基指针寄存器

在 C++ 内联 asm 中使用基指针寄存器

rdi 和 rsi 调用者保存还是被调用者保存的寄存器?

将较小大小的值移动到寄存器中

处理器体系结构