扫描二进制文件以获取 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
条目:
- VPBROADCAST Load Integer and Broadcast - 不,只有带有 XMM 和内存源的条目。
- VPBROADCASTB/VPBROADCASTW/VPBROADCASTD/VPBROADCASTQ — Load with Broadcast Integer Data from General Purpose Register - 这就是我们想要的
- VBROADCAST — Load with Broadcast Floating-Point Data - 不,这个也只是内存或向量寄存器源 ope运行ds。而且是
vbroadcastss
等,不是vP...
整数指令。 (Intel的约定是p...
是packed-integer,...ps/pd
是packed-single或者packed-float。)
如果助记词以v
开头,而您找不到条目,例如vaddps
,这是因为该指令在 AVX 之前就已存在,并记录在其 legacy-SSE 助记符下,例如 SSE1 addps
确实列出了 addps
和 vaddps
编码,包括允许 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 上测试。)
我正在调试在 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
条目:
- VPBROADCAST Load Integer and Broadcast - 不,只有带有 XMM 和内存源的条目。
- VPBROADCASTB/VPBROADCASTW/VPBROADCASTD/VPBROADCASTQ — Load with Broadcast Integer Data from General Purpose Register - 这就是我们想要的
- VBROADCAST — Load with Broadcast Floating-Point Data - 不,这个也只是内存或向量寄存器源 ope运行ds。而且是
vbroadcastss
等,不是vP...
整数指令。 (Intel的约定是p...
是packed-integer,...ps/pd
是packed-single或者packed-float。)
如果助记词以v
开头,而您找不到条目,例如vaddps
,这是因为该指令在 AVX 之前就已存在,并记录在其 legacy-SSE 助记符下,例如 SSE1 addps
确实列出了 addps
和 vaddps
编码,包括允许 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 上测试。)