为什么 rbp 和 rsp 被称为通用寄存器?
Why are rbp and rsp called general purpose registers?
根据 Intel 在 x64 中的说法,以下寄存器称为通用寄存器(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 根本称为通用目的?
感谢您的帮助。
通用意味着所有这些寄存器都可以与使用通用寄存器进行计算的任何指令一起使用,而例如,您不能对指令指针 (RIP) 或标志寄存器 (RFLAGS) 做任何您想做的事情。
其中一些寄存器被设想用于特定用途,并且通常是。最关键的是RSP和RBP。
如果您需要将它们用于您自己的目的,您应该在存储其他内容之前保存它们的内容,并在完成后将它们恢复为原始值。
如果寄存器可以作为 add
的操作数,或用于寻址模式,则它是“通用”,与 FS
段寄存器,或RIP。 GP 寄存器也称为“整数寄存器”,尽管其他类型的寄存器也可以保存整数。
在计算机体系结构中,CPU 在内部处理整数寄存器/指令与 FP/SIMD 寄存器/指令分开是很常见的。例如Intel Sandybridge-family CPUs 具有单独的物理寄存器文件,用于重命名 GP 整数与 FP/vector 寄存器。这些简称为整数与 FP 寄存器文件。 (其中 FP 是内核不需要 save/restore 来使用 GP 寄存器,同时保持用户 space 的 FPU/SIMD 状态不变的所有内容的缩写。) FP 寄存器文件为 256 位 wide(用于保存 AVX ymm 向量),但整数寄存器文件条目只需为 64 位 wide.
在重命名段寄存器 () 的 CPU 上,我猜这将是整数状态的一部分,RFLAGS + RIP 也是如此。但是当我们说“整数寄存器”时,我们通常特指通用寄存器。
“通用”在此用法中的意思是“数据或地址”,而不是像 m68k 这样的 ISA,其中你有 d0..7 数据寄存器和 a0..7 地址寄存器,所有 16 个都是整数寄存器.不管寄存器如何正常使用,通用是关于它如何可以使用。
除了x86-64添加的一些全新的寄存器:R8-R15之外,每个寄存器对于某些指令都有一些特殊性。这些并没有取消它们作为通用目的的资格原始 8 的(低 16 个)可以追溯到 8086,即使在原始 8086 中也隐含地使用了它们中的每一个。
对于 RSP,它对于 push/pop/call/ret 是特殊的,因此大多数代码从不将它用于任何其他用途。 (在内核模式下,异步用于中断,所以你真的不能把它藏在某个地方以获得额外的 GP 寄存器,就像在用户 space 代码中那样:)
但是在受控条件下(比如没有信号处理程序),您不必将 RSP 用于堆栈指针。例如你可以用它在循环中读取数组,如 in this code-golf answer。 (我实际上在 32 位代码中使用 esp
,但相同的区别:pop
在 Skylake 上比 lodsd
快,而两者都是 1 个字节。)
每个寄存器的隐含用途和特殊性:
另请参阅 x86 Assembly - Why is [e]bx preserved in calling conventions? 以获取部分列表。
我主要将此限制为用户-space 指令,尤其是现代编译器实际上可能从 C 或 C++ 代码发出的指令。对于有很多隐含用途的规则,我并不想详尽无遗。
rax
:单操作数[i]mul / [i]div / cdq / cdqe,字符串指令(stos),cmpxchg
等. 等等 以及许多立即指令的特殊较短编码,如 2 字节 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,因此不能使用 0x90
编码。但是 xchg rax,rax
可以 仍然 assemble 到 REX.W= 1 0x90.)
rcx
:班次计数,rep
-string counts, the slow loop
instruction
rdx
:rdx:rax
被 divide 和 widening-multiply(单操作数形式)使用) 和 cwd
/ cdq
/ cqo
来设置 idiv
。还有 rdtsc
和 BMI2 mulx
.
rbx
: 8086 xlatb
. cpuid
use all four of EAX..EDX. 486 cmpxchg8b
, 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
)。在内核模式下(不是用户 space)硬件异步使用来保存中断上下文。这就是内核代码不能有红区的原因。
r11
: syscall
/sysret
将其用于 save/restore 用户-space 的 RFLAGS。 (连同 RCX 到 save/restore 用户-space 的 RIP)。
寻址模式编码特殊情况:
(另请参阅 ,它只是关于寻址模式,我在其中复制了此答案的这一部分。)
rbp
/r13
不能是没有位移的基址寄存器:编码相反意味着:(在 ModRM 中)rel32
(相对于 RIP),或(在SIB) disp32
没有基址寄存器。 (r13
在 ModRM/SIB 中使用相同的 3 位,所以这个选择通过不让指令长度解码器查看 the REX.B bit 来获得第 4 个基址寄存器位来简化解码。 [r13]
assemble 到 [r13 + disp8=0]
。 [r13+rdx]
assembles 到 [rdx+r13]
(当这是一个选项时,通过交换 base/index 来避免问题)。
rsp
/r12
作为基址寄存器总是需要一个 SIB 字节。 (base=RSP 的 ModR/M 编码是用于发送 SIB 字节信号的转义码,同样,如果 r12
的处理方式不同,更多的解码器将不得不关心 REX 前缀。
rsp
不能是变址寄存器。这使得编码 [rsp]
成为可能,这比 [rsp + rsp]
更有用。 (英特尔本可以为 32 位寻址模式(386 中的新功能)设计 ModRM/SIB 编码,因此 SIB-with-no-index 只有在 base=ESP 时才有可能。这将使 [eax + esp*4]
成为可能且仅exclude [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
编译器喜欢所有寄存器 可以 用于任何事情,只限制少数特殊情况操作的寄存器分配。这就是寄存器正交性的意思。
取消引用 rbp 可能会导致 #SS(堆栈段)错误。
最近,我遇到了 linux 内核崩溃 'stack segment fault'。
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 — Pointer to data on the stack (in the SS segment)
根据 Intel 在 x64 中的说法,以下寄存器称为通用寄存器(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 根本称为通用目的?
感谢您的帮助。
通用意味着所有这些寄存器都可以与使用通用寄存器进行计算的任何指令一起使用,而例如,您不能对指令指针 (RIP) 或标志寄存器 (RFLAGS) 做任何您想做的事情。
其中一些寄存器被设想用于特定用途,并且通常是。最关键的是RSP和RBP。
如果您需要将它们用于您自己的目的,您应该在存储其他内容之前保存它们的内容,并在完成后将它们恢复为原始值。
如果寄存器可以作为 add
的操作数,或用于寻址模式,则它是“通用”,与 FS
段寄存器,或RIP。 GP 寄存器也称为“整数寄存器”,尽管其他类型的寄存器也可以保存整数。
在计算机体系结构中,CPU 在内部处理整数寄存器/指令与 FP/SIMD 寄存器/指令分开是很常见的。例如Intel Sandybridge-family CPUs 具有单独的物理寄存器文件,用于重命名 GP 整数与 FP/vector 寄存器。这些简称为整数与 FP 寄存器文件。 (其中 FP 是内核不需要 save/restore 来使用 GP 寄存器,同时保持用户 space 的 FPU/SIMD 状态不变的所有内容的缩写。) FP 寄存器文件为 256 位 wide(用于保存 AVX ymm 向量),但整数寄存器文件条目只需为 64 位 wide.
在重命名段寄存器 (
“通用”在此用法中的意思是“数据或地址”,而不是像 m68k 这样的 ISA,其中你有 d0..7 数据寄存器和 a0..7 地址寄存器,所有 16 个都是整数寄存器.不管寄存器如何正常使用,通用是关于它如何可以使用。
除了x86-64添加的一些全新的寄存器:R8-R15之外,每个寄存器对于某些指令都有一些特殊性。这些并没有取消它们作为通用目的的资格原始 8 的(低 16 个)可以追溯到 8086,即使在原始 8086 中也隐含地使用了它们中的每一个。
对于 RSP,它对于 push/pop/call/ret 是特殊的,因此大多数代码从不将它用于任何其他用途。 (在内核模式下,异步用于中断,所以你真的不能把它藏在某个地方以获得额外的 GP 寄存器,就像在用户 space 代码中那样:
但是在受控条件下(比如没有信号处理程序),您不必将 RSP 用于堆栈指针。例如你可以用它在循环中读取数组,如 in this code-golf answer。 (我实际上在 32 位代码中使用 esp
,但相同的区别:pop
在 Skylake 上比 lodsd
快,而两者都是 1 个字节。)
每个寄存器的隐含用途和特殊性:
另请参阅 x86 Assembly - Why is [e]bx preserved in calling conventions? 以获取部分列表。
我主要将此限制为用户-space 指令,尤其是现代编译器实际上可能从 C 或 C++ 代码发出的指令。对于有很多隐含用途的规则,我并不想详尽无遗。
rax
:单操作数[i]mul / [i]div / cdq / cdqe,字符串指令(stos),cmpxchg
等. 等等 以及许多立即指令的特殊较短编码,如 2 字节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,因此不能使用0x90
编码。但是xchg rax,rax
可以 仍然 assemble 到 REX.W= 1 0x90.)rcx
:班次计数,rep
-string counts, the slowloop
instructionrdx
:rdx:rax
被 divide 和 widening-multiply(单操作数形式)使用) 和cwd
/cdq
/cqo
来设置idiv
。还有rdtsc
和 BMI2mulx
.rbx
: 8086xlatb
.cpuid
use all four of EAX..EDX. 486cmpxchg8b
, x86-64cmpxchg16b
。大多数 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
)。在内核模式下(不是用户 space)硬件异步使用来保存中断上下文。这就是内核代码不能有红区的原因。r11
:syscall
/sysret
将其用于 save/restore 用户-space 的 RFLAGS。 (连同 RCX 到 save/restore 用户-space 的 RIP)。
寻址模式编码特殊情况:
(另请参阅
rbp
/r13
不能是没有位移的基址寄存器:编码相反意味着:(在 ModRM 中)rel32
(相对于 RIP),或(在SIB) disp32
没有基址寄存器。 (r13
在 ModRM/SIB 中使用相同的 3 位,所以这个选择通过不让指令长度解码器查看 the REX.B bit 来获得第 4 个基址寄存器位来简化解码。 [r13]
assemble 到 [r13 + disp8=0]
。 [r13+rdx]
assembles 到 [rdx+r13]
(当这是一个选项时,通过交换 base/index 来避免问题)。
rsp
/r12
作为基址寄存器总是需要一个 SIB 字节。 (base=RSP 的 ModR/M 编码是用于发送 SIB 字节信号的转义码,同样,如果 r12
的处理方式不同,更多的解码器将不得不关心 REX 前缀。
rsp
不能是变址寄存器。这使得编码 [rsp]
成为可能,这比 [rsp + rsp]
更有用。 (英特尔本可以为 32 位寻址模式(386 中的新功能)设计 ModRM/SIB 编码,因此 SIB-with-no-index 只有在 base=ESP 时才有可能。这将使 [eax + esp*4]
成为可能且仅exclude [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
编译器喜欢所有寄存器 可以 用于任何事情,只限制少数特殊情况操作的寄存器分配。这就是寄存器正交性的意思。
取消引用 rbp 可能会导致 #SS(堆栈段)错误。
最近,我遇到了 linux 内核崩溃 'stack segment fault'。
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 — Pointer to data on the stack (in the SS segment)