为什么我可以在寄存器中访问较低的双字/字/字节但不能更高?

时间:2017-08-04 07:13:21

标签: assembly x86 64-bit x86-64 cpu-registers

我开始学习汇编程序,这对我来说看起来不合逻辑。

为什么我不能在寄存器中使用多个更高的字节?

我理解rax - > eax - > ax的历史原因,所以让我们关注新的 64位寄存器。例如,我可以使用r8r8d,但为什么不r8dlr8dhr8wr8b也是如此。

我最初的想法是,我可以同时使用8个r8b个寄存器(就像我可以同时使用alah一样)。但我不能。使用r8b会使完整的r8注册“忙碌”。

提出了一个问题 - 为什么?如果您不能同时使用其他部件,为什么还只需要使用寄存器的一部分?为什么不只保留r8并忘记下半部分?

3 个答案:

答案 0 :(得分:12)

  

为什么我不能在寄存器中使用多个更高的字节

指令的每个排列都需要在指令中编码。原始8086处理器支持以下选项:

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

由于8086是16位处理器,因此有三个不同版本涵盖所有选项 在80386中添加了32位支持。设计人员可以选择,或者增加对3组寄存器的支持(x 8个寄存器= 24个新寄存器),并以某种方式找到这些寄存器的编码,或者保留大部分内容,就像以前一样。

以下是设计师选择的内容:

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)

他们只需添加0x66前缀即可将寄存器大小从(现在)默认32位更改为16位加上0x67前缀来更改内存操作数大小。并留在那。

否则意味着加倍指令编码的数量或为每个“新”部分寄存器添加三个六个新前缀。
当80386出现所有指令字节已经被采用时,所以没有新前缀的空间。此操作码空间被无用的说明(例如AAAAADAAMAASDAADAS {{3} }。 (这些已在X64模式下禁用,以释放急需的编码空间)。

如果只想更改寄存器的高字节,只需执行:

movzx eax,cl     //mov al,cl, but faster   
shl eax,24       //mov al to high byte.
  

但为什么不是两个(比如r8dl和r8dh)

在最初的8086中,有8个字节大小的寄存器:

al,cl,dl,bl,ah,ch,dh,bh  <-- in this order.

索引寄存器,基址指针和堆栈寄存器没有字节寄存器。

在x64中,这已经改变了。如果有REX前缀(表示x64寄存器),则al..bh(8 regs)编码al .. r15l。 16 regs incl。来自rex前缀的1个额外编码位。这会添加spldilsilbpl,但不包括任何xh注册。 (如果不使用xh前缀,您仍然可以获得四个rex个注册表。

  

使用r8b使完整的r8“忙”

是的,这被称为'部分寄存器写'。由于编写r8b更改了部分,但并非全部r8r8现在分为两部分。一半已经改变,一半没有改变。 CPU需要加入两半。它可以通过使用额外的CPU周期来执行此操作,或者通过向任务添加更多电路以便能够在单个周期中执行此操作。
后者在硅方面很昂贵并且在设计方面很复杂,由于进行了额外的工作(每个周期更多的工作=更多的热量产生),它还增加了额外的热量。有关不同x86 CPU如何处理部分寄存器写入(以及稍后读取完整寄存器)的详细信息,请参见SALC

  

如果我使用r8b我无法同时访问高56位,它们存在但无法访问

不,他们不是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  

您使用掩码加andorxornot and来更改寄存器的某些部分,而不会影响其余部分。

ah确实没有需要,但它确实导致了8086上更紧凑的代码(实际上更有用的寄存器)。编写EAX或RAX然后分别读取AL和AH(例如movzx ecx, al / movzx edx, ah)作为解包字节的一部分仍然有用。

答案 1 :(得分:4)

一般的答案是,这种访问在很少的意义上是昂贵的,很少需要。

自20世纪80年代后半期以来,以及自20世纪90年代以来,指令集主要是为了编译方便,而不是人类的便利。当一组变量以其定义的大小(8,16,32,64位)投影到一组固定的寄存器上时,编译器逻辑就会简单得多,并且每个寄存器一次只能用于一个值。寄存器重叠对他们来说非常困惑。因此,编译器内部知道单个寄存器“A”(或甚至R0),即AL,AX,EAX或RAX,具体取决于操作数大小。要使用AH,应注意AX由AH和AL组成,这是当前不可见的。即使它用AH(例如LAHF)生成指令,在内部它也可能被视为“用LowFlags * 256填充A的操作”。 (实际上,有一些黑客可以玷污这张强大的画面,但它们非常本地化。)

这与其他编译器细节合并。例如,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

这些代码块是线性且足够快的。

答案 2 :(得分:3)

历史上还有另一个步骤,即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位操作(除了一些奇数双移)。没有人试图再转换旧的汇编代码。无论如何,从来没有那么好过。