【问题标题】:How to tell length of an x86-64 instruction opcode using CPU itself?如何使用 CPU 本身判断 x86-64 指令操作码的长度?
【发布时间】:2019-01-03 21:14:18
【问题描述】:

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

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

【问题讨论】:

  • 好吧。你希望它是一个自动化的过程吗?
  • @harold:是的,很高兴从程序/代码中知道这一点,即以编程方式。
  • 主要技巧是将指令放在不可访问的页面之前,尝试不同的偏移量,直到指令获取触发页面错误。但要让它健壮起来很棘手。
  • @harold:很有趣。谢谢。 “指令获取”是指“执行它”,对吧?
  • 例如,安装蹦床(用于热补丁二进制函数。) - 只知道指令长度是不够的。首先可以在函数的最开始处进行 jmp/call。那么那个 jmp 只能是 2/5 字节呢?此更改指令流。也可以在一开始就翻录寻址指令。如果没有在指令中设置 fixup relative (rip) 就不能简单地移动它(并且只能移动到 +/- 2GB 距离)。所以只知道指令长度不足以完成您的任务

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


【解决方案1】:

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,甚至是 jmpcall),将其移至别处将破坏代码。(感谢@Rbmn 指出这个极端情况。)

【讨论】:

  • 是的,谢谢。问题是执行那些我希望避免的指令。但它仍然是一个想法。
  • 我想知道,为什么英特尔和 AMD 不为此引入指令?对于调试器、JIT'ers 和类似类型的软件来说,这似乎是一件合乎逻辑的事情。
  • 所以如果你写了一个调试器,你可以使用 CPU 的单步功能 - 这不是必需的,在 windows 中是多么最小。进程可以自己处理异常。我从来没有这样做过 - 设置 VEH (AddVectoredExceptionHandler) 并为线程设置跟踪标志。在VectoredHandler 中再次设置TRACE_FLAG 并返回EXCEPTION_CONTINUE_EXECUTION 如果要继续跟踪,或者不设置标志并在我们要停止自我跟踪时返回EXCEPTION_CONTINUE_SEARCH(异常已由SEH 处理)
  • @MikeF - For instance, installing a trampoline (for hotpatching a binary function.) - 只知道指令长度是不够的。首先可以在函数的最开始处进行 jmp/call。那么那个 jmp 只能是 2/5 字节呢?此更改指令流。也可以在一开始就翻录寻址指令。如果没有在指令中设置修正相对(rip),它就不能简单地移动(并且只能移动到 +/- 2GB 距离)。所以只知道指令长度不足以完成您的任务
猜你喜欢
  • 2011-06-01
  • 2021-12-25
  • 1970-01-01
  • 2016-10-03
  • 1970-01-01
  • 1970-01-01
  • 2020-09-12
  • 2012-10-08
相关资源
最近更新 更多