为什么我可以访问寄存器中较低的 dword/word/byte 而不能访问较高的?
Why can I access lower dword/word/byte in a register but not higher?
我开始学习汇编,这在我看来不合逻辑。
为什么我不能在一个寄存器中使用多个高位字节?
明白了rax
->eax
->ax
的历史原因,所以重点说说new64位寄存器.例如,我可以使用 r8
和 r8d
,但为什么不能使用 r8dl
和 r8dh
? r8w
和 r8b
.
也是如此
我最初的想法是我可以同时使用 8 个 r8b
寄存器(就像我可以同时使用 al
和 ah
一样)。但我不能。并使用 r8b
生成完整的 r8
寄存器 "busy".
这提出了一个问题 - 为什么?如果不能同时使用其他部分,为什么只需要使用寄存器的一部分?为什么不只保留 r8
而忽略下面的部分呢?
why can't I use multiple higher bytes in a register
Every permutation of an instruction needs to be encoded in the instruction. The original 8086 processor supports the following options:
instruction encoding remarks
---------------------------------------------------------
mov ax,value b8 01 00 <-- whole register
mov al,value b4 01 <-- lower byte
mov ah,value b0 01 <-- upper byte
Because the 8086 is a 16 bit processor three different versions cover all options.
In the 80386 32-bit support was added. The designers had a choice, either add support for 3 additional sets of registers (x 8 registers = 24 new registers) and somehow find encodings for these, or leave things mostly as they were before.
Here's what the designers opted for:
instruction encoding remarks
---------------------------------------------------------
mov eax,value b8 01 00 00 00 (same encoding as mov ax,value!)
mov ax,value 66 b8 01 00 (prefix 66 + encoding for mov eax,value)
mov al,value (same as before)
mov ah,value (same as before)
They simply added a 0x66
prefix to change the register size from the (now) default 32 to 16 bit plus a 0x67
prefix to change the memory operand size. And left it at that.
To do otherwise would have meant doubling the number of instruction encodings or add three six new prefixes for each of your 'new' partial registers.
By the time the 80386 came out all instruction bytes were already taken, so there was no space for new prefixes. This opcode space had been eaten up by useless instructions like AAA
, AAD
, AAM
, AAS
, DAA
, DAS
SALC
. (These have been disabled in X64 mode to free up much needed encoding space).
If you want to change only the higher bytes of a register, simply do:
movzx eax,cl //mov al,cl, but faster
shl eax,24 //mov al to high byte.
But why not two (say r8dl and r8dh)
In the original 8086 there were 8 byte sized registers:
al,cl,dl,bl,ah,ch,dh,bh <-- in this order.
The index registers, base pointer and stack reg do not have byte registers.
In the x64 this was changed. If there is a REX
prefix (denoting x64 registers) then al..bh
(8 regs) encode al
..r15l
. 16 regs incl. 1 extra encoding bit from the rex prefix. This adds spl
, dil
, sil
, bpl
, but excludes any xh
reg. (you can still get the four xh
regs when not using a rex
prefix).
And using r8b makes the complete r8 "busy"
Yes, this is called a 'partial register write'. Because writing r8b
changes part, but not all of r8
, r8
is now split into two halves. One half has changed and one half has not. The CPU needs to join the two halves. It can either do this by using an extra CPU cycle to perform the work, or by adding more circuitry to the task to be able to do it in a single cycle.
The latter is expensive in terms of silicon and complex in terms of design, it also adds extra heat because of the extra work being done (more work per cycle = more heat produced). See for a 运行-down on how different x86 CPUs handle partial-register writes (and later reads of the full register).
if I use r8b I can't access upper 56 bits at the same time, they exist, but unaccessible
No they are not unaccessible
.
mov rax,bignumber //random value in eax
mov al,0 //clear al
xor r8d,r8d //r8=0
mov r8b,16 //set r8b
or r8,rax //change r8 upper without changing r8b
You use masks plus and
, or
, xor
and not and
to change parts of a register without affecting the rest of it.
There really was never a need for ah
, but it did lead to more compact code on 8086 (and effectively more usable registers). It's still sometimes useful to write EAX or RAX and then read AL and AH separately (e.g. movzx ecx, al
/ movzx edx, ah
) as part of unpacking bytes.
8086 之前的 8 位 8080 是历史上的又一步。尽管它是一个 8 位处理器,但您可以使用成对的 8 位寄存器来执行一些 16 位操作。
https://en.wikipedia.org/wiki/Intel_8080#Registers
因此,为了更轻松地将 8080 汇编代码转换为 8086 代码——这在当时似乎很重要(英特尔甚至提供了一个程序来自动执行此操作,几乎)——新的 16 位寄存器被设计为可选用作成对的 8 位寄存器。
然而,在 8086 中,没有使用成对的 16 位寄存器进行 32 位操作的功能,因此当 386 出现时,似乎没有必要将 32 位寄存器拆分为两个 16 位寄存器。
正如 Johan 所展示的,指令集仍然提供了一种从最低 16 位获取两个 8 位寄存器的方法。但是这个(错误的)特征没有扩展到更高的宽度。
同样,当移动到 64 位时,没有使用 32 位寄存器对进行 64 位操作的先例(除了一些奇数双移位)。并且没有人再尝试转换旧的汇编代码。反正从来没有这么好过。
一般的回答是,这种访问在某些方面成本很高,而且很少需要。
至少从 20 世纪 80 年代后半叶开始,从 1990 年代开始,指令集的建模主要是为了编译器的便利性,而不是人类的便利性。当编译器逻辑将具有定义大小(8、16、32、64 位)的一组变量投影到一组固定的寄存器上时,编译器逻辑会简单得多,并且每个寄存器一次只用于一个值。寄存器重叠对他们来说非常混乱。因此,编译器在内部知道一个寄存器 "A"(甚至 R0),它是 AL、AX、EAX 或 RAX,具体取决于操作数的大小。使用AH,需要注意AX由AH和AL组成,目前看不到。即使它生成带有 AH 的指令(例如 LAHF),它在内部也可能被视为 "operation that fills A with LowFlags*256"。 (实际上,有一些黑客抹黑了这张强大的图片,但它们非常本地化。)
这与其他编译器细节合并。例如,GCC 和 Clang 是基于 SSA 的。因此,您永远不会在它们的输出中看到 XCHG 指令;如果您在代码中的某处找到它,则它是 100% 手动编写的程序集插入。 RCL、RCR 也是如此,即使它们适用于某些特定情况(例如将 uint32 除以 7),也可能适用于 ROL、ROR。如果 AMD 从他们的 x86-64 设计中删除了 RCL、RCR,没有人会真正哀悼这些指令。
这不包括根据不同原则建模并与主要原则正交的矢量设施。当编译器决定对 XMM 寄存器执行 4 个并行 uint32 操作时,它可以使用 PINS* 指令替换此类寄存器的一部分或使用 PEXTR* 提取它,但在这种情况下,它会跟踪 2-4-8-16。 .. 值在一瞬间。但是这种矢量化不适用于主寄存器集,至少在主要的最先进的 ISA 中是这样。
编译器中的这一运动一直在硬件中不断加强。制作 16-32 个独立的架构寄存器并单独跟踪(参见 register renaming)它们(例如添加 2 个寄存器源并提供 1 个寄存器结果)比单独提供寄存器的每个部分并计算一条指令(对于相同的示例)获取 16 个单字节源并生成 8 个单字节结果。 (这就是为什么 x86-64 设计为 32 位寄存器写入清除 64 位寄存器的高 32 位;但是对于 8 位和 16 位操作不这样做,因为 CPU 已经需要由于遗留原因,与先前寄存器值的高位结合。)
在激进的 CPU 设计革命之前,有一些机会在未来看到这种改变,但我认为它们真的很少。
如果您当前需要访问部分寄存器,例如RAX 的第 40-47 位,这可以很容易地通过复制和旋转来实现。要提取它:
MOV RCX, RAX ; expect result in CL
SHR RCX, 40
MOVZX RCX, CL ; to clear all bits except 7-0
要替换值:
ROR RAX, 40
MOV AL, CL ; provided that CL is what to insert
ROL RAX, 40
这些代码块是线性的并且足够快。
我开始学习汇编,这在我看来不合逻辑。
为什么我不能在一个寄存器中使用多个高位字节?
明白了rax
->eax
->ax
的历史原因,所以重点说说new64位寄存器.例如,我可以使用 r8
和 r8d
,但为什么不能使用 r8dl
和 r8dh
? r8w
和 r8b
.
我最初的想法是我可以同时使用 8 个 r8b
寄存器(就像我可以同时使用 al
和 ah
一样)。但我不能。并使用 r8b
生成完整的 r8
寄存器 "busy".
这提出了一个问题 - 为什么?如果不能同时使用其他部分,为什么只需要使用寄存器的一部分?为什么不只保留 r8
而忽略下面的部分呢?
why can't I use multiple higher bytes in a register
Every permutation of an instruction needs to be encoded in the instruction. The original 8086 processor supports the following options:
instruction encoding remarks
---------------------------------------------------------
mov ax,value b8 01 00 <-- whole register
mov al,value b4 01 <-- lower byte
mov ah,value b0 01 <-- upper byte
Because the 8086 is a 16 bit processor three different versions cover all options.
In the 80386 32-bit support was added. The designers had a choice, either add support for 3 additional sets of registers (x 8 registers = 24 new registers) and somehow find encodings for these, or leave things mostly as they were before.
Here's what the designers opted for:
instruction encoding remarks
---------------------------------------------------------
mov eax,value b8 01 00 00 00 (same encoding as mov ax,value!)
mov ax,value 66 b8 01 00 (prefix 66 + encoding for mov eax,value)
mov al,value (same as before)
mov ah,value (same as before)
They simply added a 0x66
prefix to change the register size from the (now) default 32 to 16 bit plus a 0x67
prefix to change the memory operand size. And left it at that.
To do otherwise would have meant doubling the number of instruction encodings or add three six new prefixes for each of your 'new' partial registers.
By the time the 80386 came out all instruction bytes were already taken, so there was no space for new prefixes. This opcode space had been eaten up by useless instructions like AAA
, AAD
, AAM
, AAS
, DAA
, DAS
SALC
. (These have been disabled in X64 mode to free up much needed encoding space).
If you want to change only the higher bytes of a register, simply do:
movzx eax,cl //mov al,cl, but faster
shl eax,24 //mov al to high byte.
But why not two (say r8dl and r8dh)
In the original 8086 there were 8 byte sized registers:
al,cl,dl,bl,ah,ch,dh,bh <-- in this order.
The index registers, base pointer and stack reg do not have byte registers.
In the x64 this was changed. If there is a REX
prefix (denoting x64 registers) then al..bh
(8 regs) encode al
..r15l
. 16 regs incl. 1 extra encoding bit from the rex prefix. This adds spl
, dil
, sil
, bpl
, but excludes any xh
reg. (you can still get the four xh
regs when not using a rex
prefix).
And using r8b makes the complete r8 "busy"
Yes, this is called a 'partial register write'. Because writing r8b
changes part, but not all of r8
, r8
is now split into two halves. One half has changed and one half has not. The CPU needs to join the two halves. It can either do this by using an extra CPU cycle to perform the work, or by adding more circuitry to the task to be able to do it in a single cycle.
The latter is expensive in terms of silicon and complex in terms of design, it also adds extra heat because of the extra work being done (more work per cycle = more heat produced). See
if I use r8b I can't access upper 56 bits at the same time, they exist, but unaccessible
No they are not unaccessible
.
mov rax,bignumber //random value in eax
mov al,0 //clear al
xor r8d,r8d //r8=0
mov r8b,16 //set r8b
or r8,rax //change r8 upper without changing r8b
You use masks plus and
, or
, xor
and not and
to change parts of a register without affecting the rest of it.
There really was never a need for ah
, but it did lead to more compact code on 8086 (and effectively more usable registers). It's still sometimes useful to write EAX or RAX and then read AL and AH separately (e.g. movzx ecx, al
/ movzx edx, ah
) as part of unpacking bytes.
8086 之前的 8 位 8080 是历史上的又一步。尽管它是一个 8 位处理器,但您可以使用成对的 8 位寄存器来执行一些 16 位操作。
https://en.wikipedia.org/wiki/Intel_8080#Registers
因此,为了更轻松地将 8080 汇编代码转换为 8086 代码——这在当时似乎很重要(英特尔甚至提供了一个程序来自动执行此操作,几乎)——新的 16 位寄存器被设计为可选用作成对的 8 位寄存器。
然而,在 8086 中,没有使用成对的 16 位寄存器进行 32 位操作的功能,因此当 386 出现时,似乎没有必要将 32 位寄存器拆分为两个 16 位寄存器。
正如 Johan 所展示的,指令集仍然提供了一种从最低 16 位获取两个 8 位寄存器的方法。但是这个(错误的)特征没有扩展到更高的宽度。
同样,当移动到 64 位时,没有使用 32 位寄存器对进行 64 位操作的先例(除了一些奇数双移位)。并且没有人再尝试转换旧的汇编代码。反正从来没有这么好过。
一般的回答是,这种访问在某些方面成本很高,而且很少需要。
至少从 20 世纪 80 年代后半叶开始,从 1990 年代开始,指令集的建模主要是为了编译器的便利性,而不是人类的便利性。当编译器逻辑将具有定义大小(8、16、32、64 位)的一组变量投影到一组固定的寄存器上时,编译器逻辑会简单得多,并且每个寄存器一次只用于一个值。寄存器重叠对他们来说非常混乱。因此,编译器在内部知道一个寄存器 "A"(甚至 R0),它是 AL、AX、EAX 或 RAX,具体取决于操作数的大小。使用AH,需要注意AX由AH和AL组成,目前看不到。即使它生成带有 AH 的指令(例如 LAHF),它在内部也可能被视为 "operation that fills A with LowFlags*256"。 (实际上,有一些黑客抹黑了这张强大的图片,但它们非常本地化。)
这与其他编译器细节合并。例如,GCC 和 Clang 是基于 SSA 的。因此,您永远不会在它们的输出中看到 XCHG 指令;如果您在代码中的某处找到它,则它是 100% 手动编写的程序集插入。 RCL、RCR 也是如此,即使它们适用于某些特定情况(例如将 uint32 除以 7),也可能适用于 ROL、ROR。如果 AMD 从他们的 x86-64 设计中删除了 RCL、RCR,没有人会真正哀悼这些指令。
这不包括根据不同原则建模并与主要原则正交的矢量设施。当编译器决定对 XMM 寄存器执行 4 个并行 uint32 操作时,它可以使用 PINS* 指令替换此类寄存器的一部分或使用 PEXTR* 提取它,但在这种情况下,它会跟踪 2-4-8-16。 .. 值在一瞬间。但是这种矢量化不适用于主寄存器集,至少在主要的最先进的 ISA 中是这样。
编译器中的这一运动一直在硬件中不断加强。制作 16-32 个独立的架构寄存器并单独跟踪(参见 register renaming)它们(例如添加 2 个寄存器源并提供 1 个寄存器结果)比单独提供寄存器的每个部分并计算一条指令(对于相同的示例)获取 16 个单字节源并生成 8 个单字节结果。 (这就是为什么 x86-64 设计为 32 位寄存器写入清除 64 位寄存器的高 32 位;但是对于 8 位和 16 位操作不这样做,因为 CPU 已经需要由于遗留原因,与先前寄存器值的高位结合。)
在激进的 CPU 设计革命之前,有一些机会在未来看到这种改变,但我认为它们真的很少。
如果您当前需要访问部分寄存器,例如RAX 的第 40-47 位,这可以很容易地通过复制和旋转来实现。要提取它:
MOV RCX, RAX ; expect result in CL
SHR RCX, 40
MOVZX RCX, CL ; to clear all bits except 7-0
要替换值:
ROR RAX, 40
MOV AL, CL ; provided that CL is what to insert
ROL RAX, 40
这些代码块是线性的并且足够快。