扫描二进制文件以获取 CPU 功能使用情况

Scan binary for CPU feature usage

我正在调试在 Intel CPU 上正常运行但在另一个更新的 AMD 处理器上运行不正常的应用程序。我怀疑它可能已被编译为使用某些特定于 Intel 的指令,这会导致崩溃。但是,我正在寻找一种方法来验证这一点。我无法访问原始源代码。

是否有一种工具可以扫描二进制文件并列出它可能使用的 CPU 特定功能?

有两个好的方法:

  • 运行 在调试器下查看导致 illegal-instruction 错误的指令
  • 运行 下的 simulator/emulator 可以向您显示指令组合,例如 SDE。

但是你的想法,静态扫描二进制文件,无法区分仅在检查后调用的函数中的代码 cpuid


使用调试器查看错误指令

选择任何调试器。 GDB 很容易安装在任何 Linux 发行版上,也可能安装在 Windows 或 Mac(或那里的 lldb)上。或者选择任何其他调试器,例如有 GUID 的。

运行 程序。一旦出现故障,请使用调试器检查故障指令。

在 Intel 或 AMD 的 x86 asm 参考手册中查找,例如https://www.felixcloutier.com/x86/ 是英特尔 PDF 的 HTML 摘要。查看此指令的这种形式需要哪个 ISA 扩展。

例如,如果您让编译器这样做,此源代码可以编译为使用 AVX-512 指令,但首先只需要 SSE2 进行编译。

#include <immintrin.h>
// stores to global vars typically aren't optimized out, even without volatile
int buf[16];
int main(int argc, char **argv)
{
        __m128i v = _mm_set1_epi32(argc);     // broadcast scalar to vector
        _mm_storeu_si128((__m128i*)buf, v);
}

(在 Godbolt 上查看不同的编译选项。)

使用 gcc -march=skylake-avx512 -O3 ill.c.
构建 然后尝试 运行 它,例如在我的 Skylake-client(非 AVX512)GNU/Linux 桌面上。 (我还使用 strip a.out 删除了符号 table(函数名称),就像 binary-only 软件版本一样。

$ ./a.out 
Illegal instruction (core dumped)
$ gdb a.out
...
(gdb) run
Starting program: /tmp/a.out 

Program received signal SIGILL, Illegal instruction.
0x0000555555555020 in ?? ()

(gdb) disas
No function contains program counter for selected frame.

(gdb) disas /r $pc,+20            # from current program counter to +20 bytes
Dump of assembler code from 0x555555555020 to 0x555555555034:
=> 0x0000555555555020:  62 f2 7d 08 7c c7       vpbroadcastd xmm0,edi
   0x0000555555555026:  c5 f9 7f 05 32 30 00 00 vmovdqa XMMWORD PTR [rip+0x3032],xmm0        # 0x555555558060
   0x000055555555502e:  31 c0   xor    eax,eax
   0x0000555555555030:  c3      ret    
   0x0000555555555031:  66 2e 0f 1f 84 00 00 00 00 00   cs nop WORD PTR [rax+rax*1+0x0]
End of assembler dump.

=> 表示当前程序计数器(x86-64 中的 RIP,但 GDB 可移植地将 $pc 定义为任何 ISA 上的别名。)

所以我们在 vpbroadcastd xmm0,edi 上犯了错误。 (当我们告诉它 AVX512 可用时,GCC 实现的方式 _mm_set1_epi32(argc)。)

这不涉及内存访问,故障是 illegal-instruction 而不是 segmentation-fault,所以我们可以确定实际尝试执行不受支持的指令是崩溃的直接原因这里。 (它也可能是一个间接原因,例如一个程序使用 lzcnt eax, ecx 但一个旧的 CPU 运行 将它作为 bsr eax, ecx,然后使用那个不同的整数作为数组索引。lzcnt/bsr 对于您的情况不太可能,因为 AMD 支持它的时间比 Intel 长。)

所以让我们检查一下 vpbroadcastd:Intel 手册中有多个 vpbroadcast 条目:

如果助记词以v开头,而您找不到条目,​​例如vaddps,这是因为该指令在 AVX 之前就已存在,并记录在其 legacy-SSE 助记符下,例如 SSE1 addps 确实列出了 addpsvaddps 编码,包括允许 ZMM 寄存器的 AVX-512 编码,x/ymm16..31,以及像 vaddps ymm0{k3}{z}, ymm1, ymm2 这样的掩码。那是一条 AVX-512F+VL 指令。

无论如何,回到我们的例子。与故障指令匹配的 table 条目如下。请注意 ModR/M (/r) 之前的 7C 操作码字节,它对 ope运行ds 进行编码。它出现在 4 字节 EVEX 前缀之后,作为 cross-check 这确实是我们正在寻找的操作码。

EVEX.128.66.0F38.W0 7C /r VPBROADCASTD xmm1 {k1}{z}, r32

根据table,需要“AVX512VL AVX512F”。 {k1}{z} 是可选的掩码。 r32 是一个 32 位 general-purpose 整数寄存器,就像本例中的 edi 一样。 xmm1表示任何XMM寄存器都可以是该指令的第一个xmm ope运行d;在这种情况下,GCC 选择了 XMM0。

我的 CPU 根本没有 AVX-512,所以它出错了。


SDE 指令组合

这在 Windows 或任何其他 OS 上应该同样有效。

Intel's SDE (Software Development Emulator) has a -mix option, whose output includes categorizing by required ISA extension. See 回复:使用它。

使用相同的示例 a.out 我在 GDB 中使用:

运行ning /opt/sde-external-8.33.0-2019-02-07-lin/sde64 -mix -- ./a.out 创建了一个文件 sde-mix-out.txt,其中包含很多内容,包括不同基本块执行频率的统计信息。 (有些在动态链接器中 运行 很多次。)IDK 如果有一个选项可以忽略它,因为它对于大型程序来说会变得非常臃肿,我预计。我认为它可能只打印前几个块,即使还有更多块。

然后我们进入我们想要的部分:

...
# END_TOP_BLOCK_STATS
# EMIT_DYNAMIC_STATS FOR TID 0  OS-TID 1168465 EMIT #1
#
# $dynamic-counts
#
# TID 0
#       opcode                 count
#
*stack-read                                                         8806
*stack-write                                                        8314
*iprel-read                                                         1003
*iprel-write                                                         437

...

*isa-ext-AVX                                                           4
*isa-ext-AVX2                                                          5
*isa-ext-AVX512EVEX                                                    1
*isa-ext-BASE                                                     133338
*isa-ext-LONGMODE                                                    545
*isa-ext-SSE                                                          56
*isa-ext-SSE2                                                       2560
*isa-ext-XSAVE                                                         1
*isa-set-AVX                                                           4
*isa-set-AVX2                                                          5
*isa-set-AVX512F_128                                                   1
*isa-set-CMOV                                                        266
*isa-set-FAT_NOP                                                     891
*isa-set-I186                                                       2676
*isa-set-I386                                                       7626
*isa-set-I486REAL                                                     71
*isa-set-I86                                                      121192
*isa-set-LONGMODE                                                    545
*isa-set-PENTIUMREAL                                                   8
*isa-set-PPRO                                                        608
*isa-set-SSE                                                          56
*isa-set-SSE2                                                       2560
*isa-set-XSAVE                                                         1

isa-set-AVX512F_128 的第 1 个计数是在我的 CPU 上出错的指令,它根本不支持 AVX-512。 AVX512F_128是AVX512F(基础)+AVX512VL(向量长度,允许512位ZMM寄存器以外的向量)。

(它也被算作isa-ext-AVX512EVEX。EVEX是AVX-512矢量指令的machine-code前缀。AVX-512掩码指令如kandw k0, k1, k2使用VEX编码,如AVX1/AVX2 SIMD 指令。但这不会区分 Ice Lake 新指令,如 vpermb 在支持 AVX-512F 但不支持 AVX512VBMI 的 Skylake-server CPU 上出错)

除了 AVX-512 之外的一切可能更简单,因为每个扩展都有一个完全独立的名称


静态反汇编

可以反汇编大多数二进制文件;如果它们没有被混淆,那么反汇编应该会找到所有可能执行的指令。 (并且 high-performance 使用新指令的代码不太可能使用抛出反汇编程序的 hack,比如跳入 straight-line 反汇编将被视为不同指令的中间;x86 机器代码是 byte-stream 条指令共 variable-length 条。)

但这并没有告诉您实际执行了哪些指令;有些函数可能只在检查 CPUID 以确定是否支持必要的扩展后调用。

(而且我不知道有什么工具可以通过 ISA 扩展对它们进行分类,尽管我从来没有寻找过这样的工具;通常开发人员希望确保他们没有在代码中使用 AVX2 指令 运行 在 AVX1-only CPU 上使用 build-time 检查,或通过 运行ning 在模拟器或真实 CPU 上测试。)