动态确定恶意 AVX-512 指令的执行位置
Posted
技术标签:
【中文标题】动态确定恶意 AVX-512 指令的执行位置【英文标题】:Dynamically determining where a rogue AVX-512 instruction is executing 【发布时间】:2019-01-31 04:33:47 【问题描述】:我有一个进程在支持 AVX-512 的 Intel 机器上运行,但该进程不直接使用任何 AVX-512 指令(asm 或内在函数),并且使用 -mno-avx512f
编译,因此编译器不会插入任何 AVX-512 指令。
然而,它在降低的 AVX 涡轮频率下无限期地运行。毫无疑问,有一条 AVX-512 指令通过库、(非常不可能的)系统调用或类似的东西潜入某处。
与其尝试“二进制搜索”到 AVX-512 指令的来源,有什么方法可以立即找到它,例如,捕获这样的指令?
操作系统是 Ubuntu 16.04。
【问题讨论】:
您也许可以让内核清除启用 AVX512 的控制寄存器位,并承诺将在上下文切换时保存/恢复完整的 ZMM 状态。但是您是否确定持续的 256 位 FMA 或任何不会将其降低到与偶尔的 512 位指令相同的频率?我猜你已经排除了另一个进程中的代码会减慢你正在运行的核心? 这非常了不起,因为 AVX-512 频率仅在包含 FP 和/或 int-mul 指令的重型 AVX-512 代码中有效,请参阅here。例如,我不希望这些指令出现在memcpy
函数中。轻量级 AVX-512 代码应以 AVX2 频率运行。
@wim - 我在上面说错了:这个进程在中速层运行,也就是“AVX2 turbo” - 但我发现这个名字不好,因为它实际上包含一些繁重的 AVX/AVX2 指令和绝大多数 AVX-512 指令。
顺便说一句,AVX(512) 的降频可以通过推测触发。所以你甚至不需要执行 AVX 指令。因此,试图聪明地运行繁重的 AVX 以避免时钟下降的代码可能会被错误的推测所击败。不用说,这是 Spectre 的漏洞利用之一。
@wim - 是的,我最终找到了同样的问题。它已在上游 glibc 2.23 中修复,这是 Ubuntu 使用的版本,但 Ubuntu(可能是 Debian)显然还没有修复。
【参考方案1】:
按照 cmets 的建议,您可以搜索系统的所有 ELF 文件并对其进行反汇编,以检查它们是否使用 AVX-512 指令:
$ objdump -d /lib64/ld-linux-x86-64.so.2 | grep %zmm0
14922: 62 f1 fd 48 7f 44 24 vmovdqa64 %zmm0,0xc0(%rsp)
14a2d: 62 f1 fd 48 6f 44 24 vmovdqa64 0xc0(%rsp),%zmm0
14c2c: 62 f1 fd 48 7f 81 50 vmovdqa64 %zmm0,0x50(%rcx)
14ca0: 62 f1 fd 48 6f 84 24 vmovdqa64 0x50(%rsp),%zmm0
(顺便说一句,libc 和 ld.so 确实包含 AVX-512 指令,它们不是您要查找的指令?)
但是,您可能会发现甚至不执行的二进制文件,并且会丢失动态未压缩的代码等...
如果您对进程有疑问(因为perf
报告CORE_POWER.LVL*_TURBO_LICENSE
事件),我建议如果此进程生成核心转储并对其进行反汇编(注意第一行也允许转储代码):
$ echo 0xFF > /proc/<PID>/coredump_filter
$ gdb --pid=<PID>
[...]
(gdb) gcore
Saved corefile core.19602
(gdb) quit
Detaching from program: ..., process ...
$ objdump -d core.19602 | grep %zmm0
7f73db8187cb: 62 f1 7c 48 10 06 vmovups (%rsi),%zmm0
7f73db818802: 62 f1 7c 48 11 07 vmovups %zmm0,(%rdi)
7f73db81883f: 62 f1 7c 48 10 06 vmovups (%rsi),%zmm0
[...]
接下来,您可以轻松编写一个小型 Python 脚本,在每个 AVX-512 指令上添加断点(或跟踪点)。类似的东西
(gdb) python
>import os
>with os.popen('objdump -d core.19602 | grep %zmm0 | cut -f1 -d:') as pipe:
> for line in pipe:
> gdb.Breakpoint("*" + line)
当然,它会创建数百个(或数千个)断点。但是,断点的开销小到足以让 gdb 支持(我认为每个断点
另一种方法是在虚拟机中运行您的代码。特别是,我建议使用 libvex。 libvex 用于动态检测代码(内存泄漏、内存分析等)。 libvex 解释机器代码,将其转换为中间表示并重新编码机器代码以供 CPU 执行。最著名的使用 libvex 的项目是 valgrind(公平地说,libvex 是 valgrind 的后端)。
因此,您可以使用 libvex 运行您的应用程序,而无需任何工具:
$ valgrind --tool=none YOUR_APP
现在您必须围绕 libvex 编写一个工具来检测 AVX-512 的使用情况。但是,libVEX 不(还)支持 AVX-512。因此,一旦它必须执行 AVX-512 指令,它就会因非法指令而失败。
$ valgrind --tool=none YOUR_APP
[...]
vex amd64->IR: unhandled instruction bytes: 0x62 0xF1 0xFD 0x48 0x28 0x84 0x24 0x8 0x1 0x0
vex amd64->IR: REX=0 REX.W=0 REX.R=0 REX.X=0 REX.B=0
vex amd64->IR: VEX=0 VEX.L=0 VEX.nVVVV=0x0 ESC=NONE
vex amd64->IR: PFX.66=0 PFX.F2=0 PFX.F3=0
==20061== valgrind: Unrecognised instruction at address 0x10913e.
==20061== at 0x10913E: main (in ...)
==20061== Your program just tried to execute an instruction that Valgrind
==20061== did not recognise. There are two possible reasons for this.
==20061== 1. Your program has a bug and erroneously jumped to a non-code
==20061== location. If you are running Memcheck and you just saw a
==20061== warning about a bad jump, it's probably your program's fault.
==20061== 2. The instruction is legitimate but Valgrind doesn't handle it,
==20061== i.e. it's Valgrind's fault. If you think this is the case or
==20061== you are not sure, please let us know and we'll try to fix it.
==20061== Either way, Valgrind will now raise a SIGILL signal which will
==20061== probably kill your program.
==20061==
==20061== Process terminating with default action of signal 4 (SIGILL)
==20061== Illegal opcode at address 0x10913E
==20061== at 0x10913E: main (in ...)
==20061==
注意:这个答案已经过测试:
#include <immintrin.h>
int main(int argc, char *argv[])
__m512d a, b, c;
_mm512_fnmadd_pd(a, b, c);
【讨论】:
libvex
是否虚拟化 CPUID 以不报告 AVX512 支持?我认为 OP 需要一个确实报告 AVX512 支持的虚拟机,因此库仍然可以随意使用 AVX512(并将其置于污染状态)。
@Peter - 是的 libvex 报告通过 can 不支持 AVX-512。
编辑:一旦你有了 AVX512 指令地址列表,你就可以在每个地址上放置断点。我用这个想法更新了答案。以上是关于动态确定恶意 AVX-512 指令的执行位置的主要内容,如果未能解决你的问题,请参考以下文章