我开始学习汇编程序,这对我来说看起来不合逻辑。
为什么我不能在寄存器中使用多个更高的字节?
我理解rax
- > eax
- > ax
的历史原因,所以让我们关注新的 64位寄存器。例如,我可以使用r8
和r8d
,但为什么不r8dl
和r8dh
? r8w
和r8b
也是如此。
我最初的想法是,我可以同时使用8个r8b
个寄存器(就像我可以同时使用al
和ah
一样)。但我不能。使用r8b
会使完整的r8
注册“忙碌”。
提出了一个问题 - 为什么?如果您不能同时使用其他部件,为什么还只需要使用寄存器的一部分?为什么不只保留r8
并忘记下半部分?
答案 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出现所有指令字节已经被采用时,所以没有新前缀的空间。此操作码空间被无用的说明(例如AAA
,AAD
,AAM
,AAS
,DAA
,DAS
{{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个额外编码位。这会添加spl
,dil
,sil
,bpl
,但不包括任何xh
注册。 (如果不使用xh
前缀,您仍然可以获得四个rex
个注册表。
使用r8b使完整的r8“忙”
是的,这被称为'部分寄存器写'。由于编写r8b
更改了部分,但并非全部r8
,r8
现在分为两部分。一半已经改变,一半没有改变。 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
您使用掩码加and
,or
,xor
和not 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位操作(除了一些奇数双移)。没有人试图再转换旧的汇编代码。无论如何,从来没有那么好过。