这是一个关于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表示从源( ecx、rcx、r9d或r9 )到目标( eax、rax、r8d或r8 )的mov指令。考虑到操作数必须都是32位或64位宽,有八个合法的可能组合。操作数大小覆盖前缀(或没有前缀)指示这些组合中的哪一个是预期的。例如,如果前缀是44,则源操作数必须是32位新寄存器(在本例中为r9d),目标必须是32位旧寄存器(此处为信令eax)。
我可能不是完全正确,但我想我明白其中的要点。这样看来,操作数大小的覆盖前缀所做的覆盖是这样一个事实:没有它们,指令将使用32位的“旧”操作数。
但毫无疑问,有些事情让我无法理解,否则:那么谈论“一个默认操作数大小为64位的x86-64版本”(比如这里)又有什么意义呢?
或者有一种方法,运行在64位机器上,将默认的操作数大小设置为32或64,如果是,如果我的程序适当地设置机器,我会看到不同的编码?
另外:什么时候会使用66H操作数大小覆盖前缀?
发布于 2021-07-07 19:18:57
是的,在64位机器代码中,大多数指令( 64位用于堆栈和跳转/调用指令 )的默认操作数大小是32位,loop和jrcxz也是64位.(默认的地址大小是64位,所以add eax, [rdi]是一个2字节的指令,没有前缀。)不,默认值是不可更改的,不能有2字节的add rax, rdx.
64位模式下的操作数编码
0x4?的高位设置在低比特,48.4f)。清除W位的REX前缀永远不能覆盖操作数大小为32位的操作数,因为在操作码中,操作数是默认的。(如push)0x66前缀表示,如imul ax, [r8], 123。(在其他模式中,不存在REX,66将其设置为任何非缺省值。)
有趣的事实:被覆盖使用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。
同时使用REX.W和0x66前缀是没有意义的。在这种情况下,REX.W前缀似乎“赢”。在Linux GDB中,在i7-6700k (Skylake)上,单步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前缀,例如编码add r8w, [r15 + r12*4],需要在REX的低比特中设置所有其他3位。
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条目中的不同位并不重要。模式中的表是模式和默认操作数/地址大小的可能组合的完整列表。
只有一组设置允许64位操作数大小。即使在任何遗留模式下,也不可能有像16位操作数、32位地址大小这样的组合。
从硬件复杂性的角度来看,这是有意义的。它需要支持的东西越多,就会有更多的晶体管涉及到CPU中已经很复杂和功率密集的部分。
(虽然push/pop隐式使用的默认堆栈地址大小由SS选择器IIRC独立选择。因此,我认为您可以使用普通的32位模式,其中add eax, [edx]是2字节,除了使用ss:sp而不是ss:esp的push/pop/call/ret之外。这不是我试过的东西。)
请注意,16位AX对应于16位R8W,而RAX和R8是由REX前缀区分的对。
在程序集源中,没有默认的,它必须由寄存器隐含或显式指定。
除了有些汇编程序具有push/pop的默认值,或者其他情况下也有一些错误的汇编程序(包括add $1, (%rdi)默认为dword的GNU ),仅在最近的版本中才有警告。奇怪的是,GAS在模棱两可的mov上确实有错误。clang的内置汇编程序更好,在任何不明确的操作数大小上都会出错。
https://stackoverflow.com/questions/68289333
复制相似问题