【问题标题】:Why can I access lower dword/word/byte in a register but not higher?为什么我可以访问寄存器中较低的 dword/word/byte 而不能访问更高的 dword/word/byte?
【发布时间】:2018-01-12 00:20:43
【问题描述】:

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

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

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

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

这引发了问题 - 为什么?如果您不能同时使用其他部分,为什么只需要使用寄存器的一部分?为什么不只保留r8 而忘记下部?

【问题讨论】:

  • 如何使用“仅r8”将单个字节写入内存?除了r8b 不会使r8 “忙”之外,高56 位仍然存在,不知道你让你怎么想。它只是不能作为单个 8b 寄存器直接访问,仅此而已。以及为什么没有更高位的寄存器别名:有没有想过指令是如何编码成机器代码的?现在添加足够的位来编码所有新的变体,每条指令都像 +1B = 太贵了。这是相关的dsasmblr.com/accessing-and-modifying-upper-half-of-registers
  • 允许写入 AH 或部分寄存器会导致性能下降。这就是why most x64 instructions zero the upper part of a 32 bit registerwhy sometimes modern compilers use add instead of inc 的原因
  • 我会把答案再扩展一点......通常你不需要直接访问寄存器的高位。如果您有两个 8 位变量,则只需使用两个寄存器(例如al, cl)。使用al, ah 是对原始 8086 设计的充分利用,当然有时在创建 256B 介绍时很方便,但对于一般编译器(大约 95% 以上的软件是由编译器产生的),这没什么价值,无论如何,他们必须有管理备用寄存器短缺的机制,这样他们就可以忍受只能从底部通过特定大小访问的寄存器。
  • 还有一件事(tm):mov BYTE PTR result, r8 ...好吧,我们可以讨论这种助记符的有效性(英特尔的原始语法不会这样,它是英特尔的mov [address],r8b,但是一些智能汇编器可以处理你的),但最终归结为指令编码,即 CPU 已知哪些指令。 x86/x64 CPU 可以进行直接寻址或rip-相对寻址(目标参数),但受影响内存的大小不是其中的一部分,也不是mov [mem],r 指令操作码的一部分,它被编码在源操作数中,当 r8 使用时,这意味着要写入 8 个字节。

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


【解决方案1】:

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

指令的每个排列都需要在指令中进行编码。原 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 出现时,所有指令字节都已被占用,因此没有空间用于新前缀。该操作码空间已被AAAAADAAMAASDAADASSALC 等无用指令占用。 (这些在 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 条规则,包括。来自 rex 前缀的 1 个额外编码位。这增加了spldilsilbpl,但不包括任何xh reg。 (不使用 rex 前缀时,您仍然可以获得四个 xh reg)。

而使用 r8b 会使整个 r8 “忙”

是的,这称为“部分寄存器写入”。因为写r8b 改变了一部分,但不是所有的r8r8 现在被分成两半。一半变了,一半没变。 CPU 需要加入两半。它既可以通过使用额外的 CPU 周期来执行工作,也可以通过向任务添加更多电路以便能够在单个周期内完成。
后者在硅方面是昂贵的并且在设计方面是复杂的,它还增加了额外的热量,因为要完成额外的工作(每个循环更多的工作=产生更多的热量)。请参阅Why doesn't GCC use partial registers?,了解不同 x86 CPU 如何处理部分寄存器写入(以及以后读取完整寄存器)。

如果我使用 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)有时仍然很有用。

【讨论】:

  • 请详细说明为什么 14nm CPU 在这种情况下是特定的。以及为什么不关注 shuf* 和 pins* 指令,例如它在 SIMD 子集中是如何完成的?
  • 实际上,在 Intel Skylake 上,mov al, 123 依赖于之前的值 rax。 R8b 没有与 R8 的其余部分分开重命名。我怀疑自 IvyBridge 以来就是这种情况,当时 Agner Fog 说没有更多的合并 uops 用于使用低 8 寄存器。 mov al, 123 的吞吐量为每时钟 1,除非您包含 dep-break 指令。英特尔确实将 AH 与 RAX 的其余部分分开重命名,但奇怪的是 mov ah, 123setne ah 仍然是每个时钟 1 个的瓶颈,而 mov ah, bl 每个时钟可以运行 4 个。 (不过,这些仍然独立于inc al。)
  • 好的,我有一个问题,因为我刚刚开始使用 64 位汇编,但仍然不知道所有的调用约定。因此,如果我使用寄存器 R8,并且只想更改 R8D 的低位,我仍然可以将它们称为 R8BLR8BH,就像我们对 AX、BX、CX 和DX?
【解决方案2】:

一般的答案是,这种访问在某些方面代价高昂,而且很少需要。

至少从 1980 年代后半期开始,以及从 1990 年代起,指令集的建模主要是为了编译器的方便,而不是人类的方便。当编译器逻辑将具有定义大小(8、16、32、64 位)的变量集投影到一组固定的寄存器上时,编译器逻辑要简单得多,并且每个寄存器一次只用于一个值。寄存器重叠让他们非常困惑。结果,编译器在内部知道单个寄存器“A”(甚至 R0),它是 AL、AX、EAX 或 RAX,具体取决于操作数大小。使用 AH 需要注意的是,AX 由 AH 和 AL 组成,目前看不到。即使它生成带有 AH 的指令(例如 LAHF),在内部它也可能被视为“用 LowFlags*256 填充 A 的操作”。 (实际上,有一些 hack 抹黑了这张强烈的图片,但它们非常本地化。)

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

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

【讨论】:

  • 在某些 Intel CPU 上,两个独立寄存器之间的movzx 可以在零延迟和无执行端口的情况下运行。所以理想情况下你会使用第三个寄存器和MOVZX ECX, DL。 (没有理由在 MOVZX 中使用 64 位操作数大小;编写 ECX 已经零扩展到 RCX 中,不需要 REX 前缀。)此外,在某些 CPU(如 Intel Nehalem 和更早版本)上,mov al,cl 将导致ROL 读取 RAX 时部分寄存器停止。将 RCX 的那个字节移动到位并使用AND RAX, mask / OR RAX, RCX 可以避免这种情况,并将涉及 RAX 的 dep 链从 3 个周期缩短到 2 个。
  • 您的 ROR / 8-bit-mov/ ROR 序列非常紧凑,在 AMD 和 Intel IvyBridge 及更高版本上运行速度很快。
  • 对于 BMI2,还有一个复制和旋转(立即)指令:rorx rdx, rax, 8 / movzx ecx, dl。 (请注意,movzx rcx, cl 浪费了 REX 前缀。让implicit zero-extension from writing ecx 完成它的工作。
【解决方案3】:

历史上还有一步,8 位 8080 出现在 8086 之前。尽管它是 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 位操作(除了一些奇数的双移位)。再也没有人试图转换旧的汇编代码了。反正从来没有这么好过。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-12
  • 2021-01-16
  • 1970-01-01
  • 1970-01-01
  • 2020-01-21
相关资源
最近更新 更多