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



我知道有些库可以"解析"二进制机器代码/操作码来判断x86-64 CPU指令的长度。

但我想知道,既然CPU有内部电路来确定这一点,有没有一种方法可以使用处理器本身来区分二进制代码中的指令大小?(甚至可能是黑客?)

EFLAGS/RFLAGS中的陷阱标志(TF)使CPU单步执行,即在运行一条指令后发生异常

因此,如果您编写调试器,您可以使用CPU的单步执行功能来查找代码块中的指令边界。但只有运行它,如果它出现故障(例如,从未映射的地址加载),您才会得到该异常,而不是TF单步异常。

(大多数操作系统都有连接到另一个进程并单步执行的功能,例如Linuxptrace,所以你可以创建一个无特权的沙箱进程,在那里你可以单步执行一些未知字节的机器代码…)

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


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

  • #UD:解码器看到一条完整但无效的指令
  • 未映射页面上的页面错误:解码器在判断为非法指令之前,先命中未映射页面
  • #GP:由于其他原因,指令被授予特权或出现故障

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

Christopher Domas的《破解x86 ISA》详细介绍了这项技术,包括使用它来查找未记录的非法指令,例如9a13065b8000d7是一条7字节的非法指令;这就是它停止页面错误的时候。(objdump -d只是说0x9a (bad),并对其余字节进行解码,但显然真正的英特尔硬件在再提取6个字节之前并不满意它的糟糕)。


instructions_retired.any这样的硬件性能计数器也会公开指令计数,但如果不知道指令的末尾,则不知道将rdpmc指令放在哪里。用0x90NOP填充并查看总共执行了多少指令可能不会真正起作用,因为你必须知道从哪里开始填充。


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

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

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

此外,硬件解码器只是作为代码获取路径中前端的一部分进行连接,而不是向它们提供任意数据。他们已经忙于解码大多数周期的指令,并且没有连接到数据上。添加解码x86机器代码字节的指令几乎肯定是通过在ALU执行单元中复制硬件来完成的,而不是通过查询解码的uop缓存或L1i(在L1i中标记了指令边界的设计中),或者通过实际的前端预解码器发送数据并捕获结果,而不是将其排队等待前端的其余部分。

我能想到的唯一真正的高性能用例是仿真,或者支持像英特尔软件开发模拟器(SDE)这样的新指令。但是,如果您想在旧CPU上运行新指令,关键是旧CPU不知道这些新指令。

与CPU进行浮点运算或图像处理的时间相比,分解机器代码所花费的CPU时间非常少。我们在指令集中有SIMD FMA和AVX2vpsadbw这样的东西是有原因的,它们可以加快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中,这些新指令仍然是多uop的,并且没有被广泛使用。我认为编译器很难使用它们进行自动向量化,而且大多数字符串处理都是在那些不太容易以低开销紧密集成一些内部函数的语言中完成的。


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

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

如果函数的前几个指令字节使用RIP相对寻址模式(或jcc rel8/rel32,甚至jmpcall),则将其移动到其他位置将破坏代码(感谢@Rbmn指出这个角落的案例。)

最新更新