如何使用CPU本身来判断x86-64指令操作码的长度?

时间:2018-07-26 19:25:43

标签: x86 x86-64 cpu-architecture opcode micro-architecture

我知道有些libraries可以“解析”二进制机器码/操作码来告诉x86-64 CPU指令的长度。

但是我想知道,由于CPU具有内部电路来确定这一点,是否有一种方法可以使用处理器本身通过二进制代码来判断指令大小? (也许甚至是黑客?)

1 个答案:

答案 0 :(得分:6)

Trap Flag (TF) in EFLAGS/RFLAGS使CPU单步执行,即在执行一条指令后发生异常。

因此,如果您编写调试器,则可以使用CPU的单步执行功能在代码块中查找指令边界。但是只有通过运行它,并且如果它发生故障(例如,来自未映射地址的负载),您才会获得该异常,而不是TF单步异常。

(大多数操作系统都具有附加和单步执行另一个进程(例如Linux ptrace的功能,因此您可以创建一个无特权的沙箱进程,在其中可以逐步执行一些未知字节的机器代码...)

或者正如@Rbmn指出的那样,您可以使用操作系统辅助的调试工具来单步执行自己的工作。


@Harold和@MargaretBloom还指出,您可以将字节放在页面的末尾(后跟未映射的页面)并运行它们。查看是否收到#UD,页面错误或#GP异常。

  • #UD:解码器看到了完整但无效的指令。
  • 未映射页面上的页面错误:解码器在确定该页面为非法指令之前先将其映射到未映射页面。
  • #GP:该指令由于其他原因被授予特权或错误。

要排除解码+作为完整指令运行,然后在未映射页面上出错,请在未映射页面之前仅1个字节开始,并继续添加更多字节,直到不再出现页面错误为止。

Christopher Domas的

Breaking the x86 ISA 进一步详细介绍了此技术,包括使用它来查找未记录的非法指令,例如9a13065b8000d7是一个7字节的非法指令;那就是它停止页面错误的时候。 ({objdump -d只是说0x9a (bad)并解码其余字节,但是显然,真正的英特尔硬件并不满意它的坏处,直到它再获取6个字节为止)。


诸如instructions_retired.any之类的硬件性能计数器也公开了指令计数,但是在不了解指令末尾的情况下,您不知道放置rdpmc指令的位置。用0x90 NOP填充并查看总共执行了多少条指令可能实际上是行不通的,因为您必须知道从哪里剪切并开始填充。


  

我想知道,为什么英特尔和AMD不为此引入指令

对于调试,通常您希望完全反汇编一条指令,而不仅仅是查找insn边界。因此,您需要一个完整的软件库。

将微码反汇编程序放在某些新的操作码后面是没有意义的。

此外,硬件解码器仅被连接起来以充当代码获取路径中前端的一部分,而不是向其馈送任意数据。他们已经在大多数周期中忙于解码指令,并且没有连接起来处理数据。添加对x86机​​器代码字节进行解码的指令几乎可以肯定是通过在ALU执行单元中复制该硬件来完成的,而不是通过查询已解码的uop缓存或L1i(在设计中在L1i中标记了指令边界)或通过以下方式发送数据来完成的:实际的前端预解码器并捕获结果,而不是将其排队给其余的前端。

我能想到的唯一真正的高性能用例是仿真,或支持诸如Intel's Software Development Emulator (SDE)之类的新指令。但是,如果您想在旧的CPU上运行新指令,那么重点就是旧的CPU 不知道这些新指令。

与花在浮点数学或图像处理上的时间相比,拆卸机器代码所花费的CPU时间非常少。有一个原因是我们在指令集中有SIMD FMA和AVX2 vpsadbw之类的东西可以加快CPU花费大量时间来完成的那些特殊用途的事情,而不是我们可以用软件轻松完成的事情。 >

请记住,指令集的要点是使创建高性能代码成为可能,而不是获取所有元数据并专注于自身解码。

在特殊用途复杂性的最高端,在Nehalem中引入了SSE4.2字符串指令。他们可以做一些很酷的事情,但是很难使用。 https://www.strchr.com/strcmp_and_strlen_using_sse_4.2(还包括strstr,这是一个真实的用例,其中pcmpistri can be faster than SSE2 or AVX2, unlike for strlen / strcmp where plain old pcmpeqb / pminub`如果使用得当,效果很好 (请参阅glibc的手写内容无论如何,这些新指令即使在Skylake中也仍然是多指令集,并未得到广泛使用。我认为编译器很难与它们进行自动矢量化,并且大多数字符串处理都是在语言中完成的,这些语言很难以较低的开销紧密集成一些内在函数。


  

安装蹦床(用于热修补二进制函数。)

即使这需要解码指令,而不仅仅是找到它们的长度。

如果函数的前几个指令字节使用相对RIP寻址方式(或jcc rel8/rel32,甚至jmpcall),则将其移至其他位置将会破坏代码。(感谢@Rbmn指出了这种极端情况。)