我知道有些库可以“解析”二进制机器码/操作码来判断 x86-64 CPU 指令的长度。
但我想知道,由于 CPU 有内部电路来确定这一点,有没有办法使用处理器本身从二进制代码中判断指令大小?(甚至可能是黑客攻击?)
我知道有些库可以“解析”二进制机器码/操作码来判断 x86-64 CPU 指令的长度。
但我想知道,由于 CPU 有内部电路来确定这一点,有没有办法使用处理器本身从二进制代码中判断指令大小?(甚至可能是黑客攻击?)
EFLAGS/RFLAGS 中的陷阱标志 (TF)使 CPU 单步执行,即在运行一条指令后发生异常。
因此,如果您编写一个调试器,您可以使用 CPU 的单步执行功能来查找代码块中的指令边界。但只有通过运行它,并且如果它出现故障(例如从未映射的地址加载),您将得到该异常而不是 TF 单步异常。
(大多数操作系统都具有附加和单步执行另一个进程的功能,例如 Linux ptrace
,因此您可以创建一个非特权沙盒进程,您可以在其中逐步执行一些未知字节的机器代码......)
或者正如@Rbmn 指出的那样,您可以使用操作系统辅助调试工具自己单步执行。
@Harold 和 @MargaretBloom 还指出,您可以将字节放在页面的末尾(后面是未映射的页面)并运行它们。查看您是否收到#UD、页面错误或#GP 异常。
#UD
:解码器看到了一个完整但无效的指令。#GP
: 该指令因其他原因被授予特权或出错。要排除解码+作为完整指令运行,然后在未映射页面上出现错误,请从未映射页面前的 1 个字节开始,并继续添加更多字节,直到停止出现页面错误。
Christopher Domas打破 x86 ISA更详细地介绍了这项技术,包括使用它来查找未记录的非法指令,例如9a13065b8000d7
7 字节的非法指令;那是它停止页面错误的时候。(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 jmp
or call
),将其移至别处将破坏代码。 (感谢@Rbmn 指出这个极端情况。)