6

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

但我想知道,由于 CPU 有内部电路来确定这一点,有没有办法使用处理器本身从二进制代码中判断指令大小?(甚至可能是黑客攻击?)

4

1 回答 1

8

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

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

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

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


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

  • #UD:解码器看到了一个完整但无效的指令。
  • 未映射页面上的页面错误:解码器在确定它是非法指令之前命中了未映射页面。
  • #GP: 该指令因其他原因被授予特权或出错。

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

Christopher Domas打破 x86 ISA更详细地介绍了这项技术,包括使用它来查找未记录的非法指令,例如9a13065b8000d77 字节的非法指令;那是它停止页面错误的时候。(objdump -d只是说0x9a (bad)并解码其余的字节,但显然真正的英特尔硬件不满意它的坏,直到它再获取 6 个字节)。


硬件性能计数器instructions_retired.any也暴露指令计数,但是在不知道指令结束的情况下,您不知道将rdpmc指令放在哪里。使用 NOP 填充0x90并查看总共执行了多少指令可能不会真正起作用,因为您必须知道从哪里剪切和开始填充。


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

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

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

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

我能想到的唯一真正的高性能用例是仿真,或支持新指令,如英特尔的软件开发仿真器 (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 相对寻址模式(或 a jcc rel8/rel32,甚至 a jmpor call),将其移至别处将破坏代码。 (感谢@Rbmn 指出这个极端情况。)

于 2018-07-26T19:45:15.900 回答