传统(单字节)前缀与您所说的操作码字节不同,因此状态机只能记住它看到的前缀,直到它到达操作码字节。
The 0f
2 字节操作码的转义字节并不是真正的前缀。它必须与第二个操作码字节连续。因此,遵循0f
, anybyte 是一个操作码,即使它类似于f2
否则这将是一个前缀。 (这也适用于以下0f 3a
or 0f 38
SSSE3 及更高版本的 2 字节转义,或编码这些转义序列之一的 VEX/EVEX 前缀)。
如果您查看操作码映射,就会发现单字节前缀和操作码之间没有任何不明确的条目。 (例如。http://ref.x86asm.net/coder64.html http://ref.x86asm.net/coder64.html,并注意 2 字节 0F .. 操作码是如何单独列出的)。
解码器必须知道当前的模式(以及其他事情);例如 x86-64 删除了 1 字节inc/dec reg
用作 REX 前缀的操作码。 (x86-x64 中不同或完全删除的 x86 32 位操作码 https://stackoverflow.com/questions/32868293/x86-32-bit-opcodes-that-differ-in-x86-x64-or-entirely-removed)。我们甚至可以利用这种差异来编写多语言机器代码,这些代码在解码时运行方式不同32 位与 64 位模式 https://stackoverflow.com/questions/38063529/x86-32-x86-64-polyglot-machine-code-fragment-that-detects-64bit-mode-at-run-ti, 甚至区分所有 3 种模式大小 https://codegolf.stackexchange.com/questions/139243/determine-your-languages-version/139717#139717.
x86 机器码是一个字节流not自同步(例如,ModRM 或立即数可以是任何字节)。 CPU 始终知道从哪里开始解码,要么是跳转目标,要么是上一条指令结束后的字节。这是指令的开始(包括前缀)。
内存中的字节只是字节,只有被CPU解码后才成为指令。 (虽然在正常程序中,只需从顶部反汇编.text
部分does给您程序的说明。自修改和混淆代码是不正常的。)
AVX / AVX-512:与操作码重叠的多字节前缀
多字节 VEX 和 EVEX 前缀在 32 位模式下并不那么简单。例如,VEX 前缀与 64 位以外的模式下的 LES 和 LDS 的无效编码重叠。 (这c4
and c5
LES 和 LDS 的操作码在 64 位模式下始终无效,VEX 前缀除外。)https://wiki.osdev.org/X86-64_Instruction_Encoding#VEX.2FXOP_opcodes https://wiki.osdev.org/X86-64_Instruction_Encoding#VEX.2FXOP_opcodes
在传统/兼容模式下,当 AVX(VEX 前缀)和 AVX-512(EVEX 前缀)时,没有任何剩余字节不是操作码或前缀,因此唯一的扩展空间是作为操作码的编码,这些操作码是仅对有限的 ModRM 字节集有效。 (例如 LES / LDS 需要内存源,而不是寄存器 - 这就是为什么 VEX 前缀中的某些位被反转,因此字节的前 2 位c4
or c5
一直会1
在 32 位模式下而不是0
。
这是 ModRM 中的“模式”字段,并且11
表示注册)。
(有趣的事实:VEX 前缀在 16 位实模式下无法识别,显然是因为某些软件使用了与 LES / LDS 相同的无效编码作为故意陷阱,需要在 #UD 异常处理程序中进行整理。VEX 前缀are以 16 位识别受保护的不过,模式。)
AMD64 通过删除 AAM 等指令以及 LES/LDS(以及一字节inc
/dec reg
用作 REX 前缀的编码),但 CPU 供应商继续关心 32 位模式,并且没有添加任何仅在 64 位模式下可用的扩展,这些扩展可以简单地利用这些免费的操作码字节。这意味着要找到方法将新的指令编码塞进 32 位机器代码中越来越小的间隙中。 (通常通过强制前缀,例如rep bsr
= lzcnt
在具有该功能的 CPU 上,会产生不同的结果。)
所以现代 CPU 中的解码器支持 AVX / BMI1/2 的设备必须查看多个字节来决定这是否是有效 AVX 或其他 VEX 编码指令的前缀,或者在 32 位模式下是否应解码为 LES 或 LDS。 (我想看看指令的其余部分来决定是否应该#UD)。
但现代 CPU 无论如何都会一次查看 16 或 32 字节以并行查找指令边界。 (然后将这些指令字节组馈送到实际解码器,再次并行。)https://www.realworldtech.com/sandy-bridge/4/ https://www.realworldtech.com/sandy-bridge/4/
所使用的前缀方案也是如此AMD XOP https://en.wikipedia.org/wiki/VEX_prefix#History,这很像 VEX。
阿格纳·福格的博客文章停止指令集战争 https://www.agner.org/optimize/blog/read.php?i=25从 2009 年开始(AVX 宣布后不久,在第一个支持它的硬件之前)有一个用于未来扩展的剩余未使用编码空间表,以及一些关于它被“分配”给 AMD、Intel 或 Via 的注释。
相关/示例
-
如何判断x86指令的长度? https://stackoverflow.com/questions/4567903/how-to-tell-the-length-of-an-x86-instruction(包括我的答案)有一些关于 x86 机器代码的更多细节。
-
https://codegolf.stackexchange.com/questions/133486/find-an-illegal-string/133622#133622 https://codegolf.stackexchange.com/questions/133486/find-an-illegal-string/133622#133622(在 codegolf.SE 上 - 最短的字节序列,如果不跳过,肯定会 #UD 错误。它必须足够长,以免被 CPU 用作立即执行
mov r64, imm64
例如。)
-
为什么 gdb 上的 x/i 给出不同的结果然后反汇编? https://stackoverflow.com/questions/48739930/why-does-x-i-on-gdb-give-different-results-then-disassemble- 在错误的位置开始解码并将另一条指令的中间解码为其他内容的示例。
机器代码技巧:以多种方式解码同一字节
(这是not确实与前缀相关,但总的来说,了解规则如何应用于奇怪的情况可以帮助准确理解事情的工作原理。)
软件反汇编程序确实需要知道一个起点。如果混淆的代码混合了代码和数据,并且如果您只是假设可以按顺序解码而不需要后续跳转,那么实际执行会跳转到您无法到达的位置,这可能会出现问题。
幸运的是编译器生成的代码不会这样做 https://stackoverflow.com/questions/55607052/why-do-compilers-put-data-inside-textcode-section-of-the-pe-and-elf-files-and/55609077#55609077如此幼稚的静态反汇编(例如通过objdump -d
or ndisasm
,而不是 IDA)找到与实际运行程序相同的指令边界。
这对于running混淆的机器代码; CPU 只是按照它的指示去做,从不关心你告诉它跳转到的位置之前的字节。拆解without运行/单步执行程序是一件困难的事情,特别是有可能自我修改代码并跳转到天真的反汇编程序认为是早期指令中间的地方。
混淆的机器代码甚至可以以一种方式解码指令,然后跳回到该指令的中间,以便后面的字节成为操作码(或前缀+操作码)。如果您这样做,具有 uop 缓存或在 I 缓存中标记指令边界的现代 CPU 运行速度会很慢(但正确),因此这更像是一种有趣的代码高尔夫技巧(以牺牲速度为代价的极端代码大小优化)或混淆技术。
有关此示例,请参阅我的 codegolf.SE x86 机器代码答案使用自定义斐波那契数列打高尔夫球 https://codegolf.stackexchange.com/questions/121118/golf-a-custom-fibonacci-sequence/211331#211331。我将摘录与 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
You can使用消耗更多后续字节的操作码来执行此操作,例如3D <dword> cmp eax, imm32
。当CPU看到一个3D
操作码字节,它将抓取接下来的 4 个字节作为立即数。如果您稍后跳转到这 4 个字节,它们将被视为前缀/操作码,并且无论这些字节之前如何解码为指令的不同部分,所有内容都将正常工作(性能问题除外)。除了性能之外,CPU 还必须保持一次解码和执行 1 条指令的错觉。
我从 @Ira Baxter 的回答中学到了这个技巧汇编的 ASM 代码是否可以产生多种可能的方式(偏移值除外)? https://stackoverflow.com/questions/10765317/can-assembled-asm-code-result-in-more-than-a-single-possible-way-except-for-off/10766612#10766612