2

这是关于 x86-64 (AMD64) 架构中操作数大小覆盖前缀的问题。

这是一堆汇编指令(nasm)及其编码;的意思是 r8, ..., r15 寄存器:

                                                                   67: address-size override prefix
                                                                   |
                                                                   |  4x: operand-size override prefix
                                                                   |  |
   ;   Assembler                   ; | Dst operand | Src operand | -- --
       mov      eax,ecx            ; | 32-bit      | 32-bit      |       89 C8     |
       mov      r8d,ecx            ; | 32-bit new  | 32-bit      |    41 89 C8     |
       mov      eax,r9d            ; | 32-bit      | 32-bit new  |    44 89 C8     |
       mov      r8d,r9d            ; | 32-bit new  | 32-bit new  |    45 89 C8     |
       mov      rax,rcx            ; | 64-bit      | 64-bit      |    48 89 C8     |
       mov      r8,rcx             ; | 64-bit new  | 64-bit      |    49 89 C8     |
       mov      rax,r9             ; | 64-bit      | 64-bit new  |    4C 89 C8     |
       mov      r8,r9              ; | 64-bit new  | 64-bit new  |    4D 89 C8     |

       lea      eax,[ecx]          ; | 32-bit      | 32-bit      | 67    8D 01     |
       lea      r8d,[ecx]          ; | 32-bit new  | 32-bit      | 67 44 8D 01     |
       lea      eax,[r9d]          ; | 32-bit      | 32-bit new  | 67 41 8D 01     |
       lea      r8d,[r9d]          ; | 32-bit new  | 32-bit new  | 67 45 8D 01     |
       lea      rax,[rcx]          ; | 64-bit      | 64-bit      |    48 8D 01     |
       lea      r8,[rcx]           ; | 64-bit new  | 64-bit      |    4C 8D 01     |
       lea      rax,[r9]           ; | 64-bit      | 64-bit new  |    49 8D 01     |
       lea      r8,[r9]            ; | 64-bit new  | 64-bit new  |    4D 8D 01     |

       push     rax                ; |             | 64-bit      |       50        |
       push     r8                 ; |             | 64-bit new  |    41 50        |

通过研究这些以及与其他寄存器相同的指令,我推断出以下内容。“旧”和“新”寄存器之间存在配对。非详尽无遗:

   AX <--> R8
   CX <--> R9
   DX <--> R10
   BX <--> R11
   BP <--> R13 

忽略大小前缀,指令字节不是指特定的寄存器,而是指寄存器对。例如:字节 89 C8 表示一条 mov 指令从源(ecx、rcx、r9d 或 r9)到目标(eax、rax、r8d 或 r8)。鉴于操作数必须同时为 32 位或 64 位宽,因此有八种合法的可能组合。操作数大小覆盖前缀(或不存在)指示这些组合中的哪一个是预期的组合。例如,如果前缀存在且为 44,则源操作数必须是 32 位新寄存器(在此示例中,然后折叠为 r9d),目标必须是 32 位旧寄存器(此处为 eax 信号)。

我可能没有完全正确,但我想我明白了它的要点。这样看来,操作数大小覆盖前缀所做的覆盖是没有它们,指令将使用 32 位“旧”操作数。

但可以肯定的是,有一些事情让我无法理解,否则:谈论“默认操作数大小为 64 位的 x86-64 版本”(如这里)有什么意义?

或者有没有办法在 64 位机器上运行,将默认操作数大小设置为 32 或 64,如果是这样,并且如果我的程序适当地设置了机器,我会看到不同的编码?

另外:什么时候会使用 66H 操作数大小的覆盖前缀?

4

1 回答 1

2

是的,在 64 位机器代码中,大多数指令的默认操作数大小为 32位,堆栈和跳转/调用指令loop为 64 位, and也是 64 位jrcxz。(并且默认地址大小是 64 位,因此add eax, [rdi]是 2 字节指令,没有前缀。)不,默认值是不可更改的,你不能有 2-byte add rax, rdx

64位模式下的操作数编码编码

  • 64 位操作数大小由 REX.W 发出信号(0x4?高位设置在低半字节,48..4f)。对于默认为其他值的操作码,清除 W 位的 REX 前缀永远不会将操作数大小覆盖为 32 位。(喜欢push
  • 16 位操作数大小由0x66前缀表示,例如imul ax, [r8], 123
  • 8 位操作数大小使用不同的操作码。(8086 有 8 位和 16 位操作数大小;8 位操作数大小的操作码从那时起就没有变化。8086 的 16 位操作数大小的操作码默认为模式和前缀相关。)

(在其他模式下,没有 REX,并将66其设置为非默认值。)

有趣的事实:loop并且jrcxz被地址大小前缀而不是操作数大小隐式覆盖以使用 ECX 而不是 RCX。IIRC,这是有道理的,因为分支的操作​​数大小属性会影响它是否将 EIP 截断为 IP。

例如,GNU .intel_syntax 对上面那些 NASM 语法示例的反汇编。

objdump -drwC -Mintel foo
  401000:       6a 7b                   push   0x7b
  401002:       66 6a 7b                pushw  0x7b
  401005:       03 07                   add    eax,DWORD PTR [rdi]
  401007:       66 03 07                add    ax,WORD PTR [rdi]
  40100a:       48 03 07                add    rax,QWORD PTR [rdi]
  40100d:       66 41 6b 00 7b          imul   ax,WORD PTR [r8],0x7b

请注意,imul 示例使用了一个“高”寄存器,因此它需要一个 REX 前缀来表示 R8,而需要一个 66 前缀来表示 16 位操作数大小。.W 位在 rex 前缀中设置,它0x41不是0x49.

0x66同时拥有 REX.W 和前缀是没有意义的。在这种情况下,似乎 REX.W 前缀“获胜”。在 i7-6700k (Skylake) 上的 Linux GDB 中单66 48 05 40 e2 01 00 data16 add rax,0x1e240步执行,单步使 RIP 指向整个指令的末尾(并将完整的立即数添加到 RAX),而不是将其解码为add ax, 0xe240并将 RIP 指向中间的 4 字节立即数。(66前缀是该操作码的长度变化,就像大多数具有 32 位立即数变成 16 位的一样。请参阅https://agner.org/optimize/ re:LCP 停止。)

我让 NASM 从o16 add rax, 123456. REX 前缀通常是正常的,并且带有66前缀很好,例如为了 encode add r8w, [r15 + r12*4],需要在 REX 的低半字节中设置所有 3 个其他位。


  • 32 位地址大小由0x67前缀表示,例如add eax, [edx].

它当然可以操作数大小的东西完全正交。

通常 32 位地址大小仅对Linux x32 ABI(长模式下的 ILP32 以节省指针重数据结构上的缓存占用空间)有用,您可能希望从指针中截断高垃圾以确保地址数学正确包装以保持在低 4GiB 中,即使是 32 位负数。

  401012:       67 03 04 ba             add    eax,DWORD PTR [edx+edi*4]

在其他模式下,67将地址大小设置为非默认值。16 位地址大小也意味着 ModRM 字节的 16 位解释,因此只[bx|bp + si|di]允许,没有 SIB 字节以允许 32 / 64 位寻址的灵活性。


模式和默认设置

不,在 64 位模式下无法更改默认值。CS(或任何其他方法)选择的 GDT 条目中的不同位无关紧要。AFAIK,https: //en.wikipedia.org/wiki/X86-64#Operating_modes 中的表格是模式和默认操作数/地址大小的可能组合的完整列表。

只有一组设置完全允许 ​​64 位操作数大小。即使在任何传统模式下,也不可能拥有像 16 位操作数、32 位地址大小这样的组合。

从硬件复杂性的角度来看,这是有道理的。它需要支持的事物组合越多,CPU 中已经复杂且耗电的部分中可能涉及的晶体管就越多。

(尽管 push/pop 隐式使用的默认堆栈地址大小是由 SS 选择器 IIRC 独立选择的。所以我认为你可以使用普通的 32 位模式,其中add eax, [edx]2 个字节,除了使用 push/pop/call/retss:sp代替的ss:esp。我从来没有尝试过设置。)


请注意,16 位 AX 对应于 16 位 R8W,而 RAX 和 R8 是由 REX 前缀区分的对。


在汇编源代码中,没有默认值,它必须由寄存器隐含或明确指定。

除了一些默认为 push/pop 的汇编器,或一些在其他情况下默认为默认值的不良汇编器,包括用于add $1, (%rdi)默认为 dword 之类的 GNU 汇编器,仅在最近的版本中出现警告。奇怪的是, GAS 会在模棱两可上出错mov。clang 的内置汇编器更好,在任何不明确的操作数大小上都会出错。

于 2021-07-07T19:18:57.323 回答