Trap Flag (TF) in EFLAGS/RFLAGS 使 CPU 单步执行,即在运行一条指令后发生异常。
因此,如果您编写调试器,则可以使用 CPU 的单步执行功能来查找代码块中的指令边界。但只有通过运行它,如果它出现故障(例如从未映射的地址加载),您将得到该异常而不是 TF 单步异常。
(大多数操作系统都具有附加和单步执行另一个进程的功能,例如 Linux ptrace,因此您可以创建一个非特权沙盒进程,在其中您可以单步执行一些未知字节的机器代码...)
或者正如@Rbmn 指出的那样,您可以使用操作系统辅助的调试工具自己单步执行。
@Harold 和@MargaretBloom 还指出您可以将字节放在页面的末尾(后跟未映射的页面)并运行它们。查看您是否收到#UD、页面错误或#GP 异常。
-
#UD:解码器看到了一个完整但无效的指令。
- 未映射页面上的页面错误:解码器在确定它是非法指令之前命中了未映射页面。
-
#GP:该指令被授予特权或因其他原因出错。
要排除解码+作为一条完整指令运行,然后在未映射页面上出现错误,请从未映射页面前的 1 个字节开始,并继续添加更多字节,直到不再出现页面错误。
Breaking the x86 ISA by Christopher Domas 详细介绍了这种技术,包括使用它来查找未记录的非法指令,例如9a13065b8000d7是7字节非法指令;那是它停止页面错误的时候。 (objdump -d 只是说0x9a (bad) 并解码其余字节,但显然真正的英特尔硬件在获取更多 6 个字节之前并不满意它是坏的)。
像instructions_retired.any 这样的硬件性能计数器也会公开指令计数,但是在不知道指令结束的情况下,您不知道在哪里放置rdpmc 指令。使用0x90 NOP 填充并查看总共执行了多少指令可能不会真正起作用,因为您必须知道从哪里剪切并开始填充。
我想知道,为什么英特尔和 AMD 不为此引入指令
对于调试,通常您希望完全反汇编一条指令,而不仅仅是找到 insn 边界。所以你需要一个完整的软件库。
将微编码反汇编程序放在一些新的操作码后面是没有意义的。
此外,硬件解码器仅在代码获取路径中作为前端的一部分工作,而不是为它们提供任意数据。他们已经在大多数周期忙于解码指令,并且没有连接到处理数据。添加解码 x86 机器代码字节的指令几乎可以肯定是通过在 ALU 执行单元中复制该硬件来完成,而不是通过查询解码的微指令缓存或 L1i(在指令边界在 L1i 中标记的设计中),或通过实际的前端预解码器并捕获结果,而不是将其排队等待前端的其余部分。
我能想到的唯一真正的高性能用例是仿真,或者支持像Intel's Software Development Emulator (SDE) 这样的新指令。但如果你想在旧 CPU 上运行新指令,关键是旧 CPU 不知道这些新指令。
与 CPU 花费在浮点数学或图像处理上的时间相比,反汇编机器代码所花费的 CPU 时间非常少。我们在指令集中包含 SIMD FMA 和 AVX2 vpsadbw 之类的东西是有原因的,以加速 CPU 花费大量时间做的那些特殊用途的事情,而不是我们可以用软件轻松完成的事情。
请记住,指令集的目的是使创建高性能代码成为可能,而不是获取所有元数据并专门进行解码。
在特殊用途复杂性的上端,Nehalem 中引入了 SSE4.2 字符串指令。他们可以做一些很酷的事情,但很难使用。 https://www.strchr.com/strcmp_and_strlen_using_sse_4.2(还包括 strstr,这是一个真正的用例,其中 pcmpistri 可以比 SSE2 或 AVX2 更快,不像 strlen / strcmp 普通旧 pcmpeqb / pminub 工作非常 好吧,如果有效使用(参见 glibc 的手写 asm)。无论如何,即使在 Skylake 中,这些新指令仍然是多指令的,并且没有被广泛使用。我认为编译器很难使用它们进行自动向量化,并且大多数字符串处理都是在语言中完成的,在这些语言中,以低开销紧密集成一些内在函数并不容易。
安装蹦床(用于热补丁二进制函数。)
即使这需要解码指令,而不仅仅是找到它们的长度。
如果函数的前几个指令字节使用 RIP 相对寻址模式(或 jcc rel8/rel32,甚至是 jmp 或 call),将其移至别处将破坏代码。(感谢@Rbmn 指出这个极端情况。)