我知道指令的整个格式基本上取决于操作码(当然,在操作码中定义了额外的位字段)。有时指令没有前缀,而操作码是第一个字节。解码器怎么知道呢?
我假设指令解码器能够区分它们,因为操作码字节和前缀字节不会共享相同的二进制值。因此,解码器可以判断字节中的唯一二进制数是指令还是前缀。例如(在这个例子中,我们将坚持单字节操作码),REX或LOCK前缀不会与架构指令集中的任何操作码共享相同的字节值。
解码器需要了解当前模式(和其他内容)才能进行操作;例如,x86-64已删除了1字节的inc/dec reg
操作码以用作REX前缀。(x86 32位操作码与x86-x64不同或完全删除)。我们甚至可以利用这种差异编写多语言机器代码,在32位与64位模式下解码时运行方式不同,甚至可以区分所有3种模式大小。
x86机器代码是一个字节流,不是自同步的(例如,ModRM或立即数可以是任何字节)。CPU总是知道从哪里开始解码,无论是跳转目标还是上一条指令结束后的下一个字节。这就是指令的开头(包括前缀)。
Bytes在内存中只是字节,只有在CPU解码后才变成指令。(虽然在正常程序中,从.text部分顶部简单地反汇编确实会给出程序的指令。自修改和混淆代码不是正常的。)c4
或c5
后面的字节的高2位在32位模式下始终是1
而不是0
。这就是ModRM中的“模式”字段,11
表示寄存器。
(有趣的事实:VEX前缀在16位真实模式下不能被识别,显然是因为一些软件使用了与LES / LDS相同的无效编码作为故意的陷阱,需要在#UD异常处理程序中解决。但是,在16位受保护模式下可以识别VEX前缀。)
但是现代CPU在并行查找指令边界时通常一次查看16或32个字节。(然后再将那些指令字节组按照并行方式提供给实际的解码器。) https://www.realworldtech.com/sandy-bridge/4/
同样适用于AMD XOP使用的前缀方案,这很像VEX。
Agner Fog在2009年的博客文章Stop the instruction set war(AVX宣布不久后,在第一批支持它的硬件之前)中列出了未来扩展的剩余未使用编码空间表格,并注明它被“分配”给AMD、Intel或Via。
mov r64,imm64
的立即数使用。(这与前缀无关,但通常了解规则如何适用于奇怪的情况可以帮助理解事物的确切工作方式。)
软件反汇编器确实需要知道起始点。如果混淆代码混合了代码和数据,并且实际执行跳转到您假定可以按顺序解码而不遵循跳转的位置,那么这可能会有问题。
幸运的是编译器生成的代码不会这样做,因此天真的静态反汇编(例如通过objdump -d
或ndisasm
,而不是IDA)将找到实际运行程序时的相同指令边界。
举个例子,可以查看我的codegolf.SE x86机器码回答高尔夫定制菲波那切数列。我会摘录与CPU循环回到cfib.loop
相对应的反汇编代码,但请注意第一次迭代的解码方式不同。因此,我在循环外只使用1个字节而不是2个字节,以有效地跳转到第一次迭代的中间位置。有关完整描述和其他反汇编,请参见链接的答案。
0000000000401070 <cfib>:
401070: eb .byte 0xeb # jmp rel8 consuming the 01 add opcode as a rel8
0000000000401071 <cfib.loop>:
401071: 01 d0 add eax,edx
# loop entry point on first iteration, jumping over the ModRM byte (D0) of the ADD
(entry on first iteration):
401073: 92 xchg edx,eax
401074: e2 fb loop 401071 <cfib.loop>
401076: c3 ret
3D <dword> cmp eax, imm32
。当CPU看到3D
操作码字节时,它将获取接下来的4个字节作为立即数。如果您稍后跳转到这些4个字节,它们将被视为前缀/操作码,并且无论这些字节之前如何解码为指令的不同部分,一切都将正常工作(除了性能问题)。CPU必须保持每次解码和执行1个指令的幻觉,而不是性能。
我从@Ira Baxter在Can assembled ASM code result in more than a single possible way (except for offset values)?中的答案中学到了这个技巧。
f2
和f3
)、地址和操作数大小覆盖前缀(66
和67
)、LOCK前缀(f0
)以及REX前缀系列(40
至4f
)。 - fuz0f
不是一个前缀。它实际上是引入了一个双字节操作码。前缀只在操作码之前被识别,而不是在操作码之后(显然是这样)。还有以 0f 38
和 0f 3a
开头的三字节操作码。请注意,在 VEX 和 EVEX 前缀中,0f
、0f 38
和 0f 3a
的“有效数字”都被卷入了 VEX/EVEX 前缀中。这就是为什么 AVX 指令通常不会比它们的 SSE 对应指令更长,尽管有一个必需的两字节 VEX 前缀的原因。 - fuz
0f
开头,这不是前缀。因此,解码器可以确定操作码何时开始。然而,英特尔重新使用前缀将其含义更改为操作码。解码器考虑到了这一点。例如,0f 58
是所有add{ps,pd,ss,sd}
的操作码,具体来说,f2 0f 58
是addsd
,而66 0f 58
是addpd
。有趣的是,f2 66 0f 58
是o16 addsd
而不是repne addpd
。 - Margaret Bloomf2
、f3
和66
视为提供额外操作码位而不是真正的前缀。对于 SIMD 指令,有两个这样的操作码位(最多编码一个f2
/f3
/66
),但对于标量指令,66
可以与f2
和f3
结合使用。甚至有一些指令将66
前缀与REX.W
结合使用。 - fuz66 0f 58 /r
是addpd
,而当两个前缀同时存在时,它也可以合理地被解码为那个指令。是的,当然它们是前缀,但应该将它们视为提供额外的操作码位(而不是像段前缀那样以系统化的方式修改指令)。 - fuz