是的,在 64 位机器代码中,大多数指令的默认操作数大小为 32 位,64 位堆栈和跳转/调用指令,以及 64 位loop
and jrcxz
。 (默认地址大小是 64 位,所以add eax, [rdi]
是一个 2 字节指令,没有前缀。)不,默认值是不可更改的,你不能有 2 字节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 and jrcxz被覆盖通过地址大小前缀而不是操作数大小隐式使用 ECX 而不是 RCX。 IIRC,这是有道理的,因为分支的操作数大小属性会影响它是否将 EIP 截断为 IP。
例如,上面那些 NASM 语法示例的 GNU .intel_syntax 反汇编。
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 位是not设置在 rex 前缀中,它是0x41
not 0x49
.
同时拥有 REX.W 和 a 是没有意义的0x66
字首。在这种情况下,REX.W 前缀似乎“获胜”。单步执行66 48 05 40 e2 01 00 data16 add rax,0x1e240
在 i7-6700k (Skylake) 上的 Linux GDB 中,单步使 RIP 指向整个指令的末尾(并将完整的立即数添加到 RAX),而不是将其解码为add ax, 0xe240
并使 RIP 指向 4 字节立即数的中间。 (A66
该操作码的前缀长度会发生变化,就像大多数具有 32 位立即数的操作码一样,它会变成 16 位。看https://agner.org/optimize/回复:LCP 停止运行。)
我让 NASM 发出它o16 add rax, 123456
。 REX 前缀通常是正常的并且可以使用66
前缀,例如编码add r8w, [r15 + r12*4]
,需要在 REX 的低半字节中设置所有其他 3 位。
- 32-bit address大小由一个信号表示
0x67
前缀,例如add eax, [edx]
.
当然可以combined与操作数大小的东西,完全正交。
通常 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 中本已复杂且耗电的部分可能涉及的晶体管就越多。
(虽然默认stack推入/弹出隐式使用的地址大小由 SS 选择器 IIRC 独立选择。所以我认为你可以使用正常的 32 位模式add eax, [edx]
是 2 个字节,除了使用 push/pop/call/retss:sp
代替ss:esp
。我从来没有尝试过设置。)
请注意,16 位 AX 对应于 16 位 R8W,而 RAX 和 R8 是通过 REX 前缀区分的对。
在汇编源代码中,没有默认值,它必须由寄存器隐含或显式指定。
除了一些具有push/pop默认值的汇编器,或者一些对其他情况有默认值的糟糕汇编器,包括GNU汇编器,例如add $1, (%rdi)
默认为 dword,仅在最新版本中出现警告。 GAS 在模棱两可时出错mov
,奇怪的是。 clang 的内置汇编器更好,在任何不明确的操作数大小上都会出错。