为 valgrind 和 gdb 记录禁用 glibc 中的 AVX 优化函数(LD_HWCAP_MASK、/etc/ld.so.nohwcap)
- 2024-11-04 08:43:00
- admin 原创
- 35
问题描述:
带有 glibc 的现代 x86_64 linux 将检测 CPU 是否支持 AVX 扩展,并将许多字符串函数从通用实现切换到AVX优化版本(借助 ifunc 调度程序:1、2)。
此功能可以提高性能,但它会阻止一些工具(如 valgrind(旧版 libVEX,valgrind-3.8之前)和 gdb 的“ target record
”(反向执行))正常工作(Ubuntu“Z”17.04 beta、gdb 7.12 .50.20170207-0ubuntu2、gcc 6.3.0-8ubuntu1 20170221、Ubuntu GLIBC 2.24-7ubuntu2):
$ cat a.c
#include <string.h>
#define N 1000
int main(){
char src[N], dst[N];
memcpy(dst, src, N);
return 0;
}
$ gcc a.c -o a -fno-builtin
$ gdb -q ./a
Reading symbols from ./a...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x724
Starting program: /home/user/src/a
Temporary breakpoint 1, 0x0000555555554724 in main ()
(gdb) record
(gdb) c
Continuing.
Process record does not support instruction 0xc5 at address 0x7ffff7b60d31.
Process record: failed to record execution log.
Program stopped.
__memmove_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:416
416 VMOVU (%rsi), %VEC(4)
(gdb) x/i $pc
=> 0x7ffff7b60d31 <__memmove_avx_unaligned_erms+529>: vmovdqu (%rsi),%ymm4
gdb 在执行“目标记录”时出现错误消息“ Process record does not support instruction 0xc5
”,因为记录/重放引擎不支持 AVX 指令(有时问题是在_dl_runtime_resolve_avx
函数上检测到的):https://sourceware.org/ml/gdb/2016-08/msg00028.html “进程记录不支持某些 AVX 指令”,https://bugs.launchpad.net/ubuntu/+source/gdb/+bug/1573786, https: //bugs.debian.org/cgi-bin/bugreport.cgi ? bug=836802 ,https: //bugzilla.redhat.com/show_bug.cgi ?id=1136403
https://sourceware.org/ml/gdb/2016-08/msg00028.html中提出的解决方案“您可以重新编译 libc(因此 ld.so),或者在运行时破解 __init_cpu_features 并因此破解 __cpu_features(例如,参见 strcmp)。”或 set LD_BIND_NOW=1
,但重新编译的 glibc 仍然具有 AVX,而 ld bind-now 没有帮助。
我听说 glibc 中有/etc/ld.so.nohwcap
和LD_HWCAP_MASK
配置。它们可以用来禁用 glibc 中针对 AVX 优化的字符串函数的 ifunc 调度吗?
glibc(rtld?)如何使用cpuid
、使用/proc/cpuinfo
(可能不是)或 HWCAP aux(LD_SHOW_AUXV=1 /bin/echo |grep HWCAP
命令给出AT_HWCAP: bfebfbff
)检测 AVX?
解决方案 1:
看起来在 glibc 的最新版本中实现了一个很好的解决方法:一个“可调”功能,用于指导选择优化的字符串函数。您可以在此处找到此功能的一般概述,以及 glibc 中的ifunc-impl-list.c中的相关代码。
以下是我解决问题的方法。首先,我获取了 gdb 抱怨的地址:
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
然后我在共享库表中查找:
(gdb) info shared
From To Syms Read Shared Object Library
0x00007ffff7fd3090 0x00007ffff7ff3130 Yes /lib64/ld-linux-x86-64.so.2
0x00007ffff76366b0 0x00007ffff766b52e Yes /usr/lib/x86_64-linux-gnu/libubsan.so.1
0x00007ffff746a320 0x00007ffff75d9cab Yes /lib/x86_64-linux-gnu/libc.so.6
...
可以看到这个地址在 glibc 中。但是具体是哪个函数呢?
(gdb) disassemble 0x7ffff75c65d4
Dump of assembler code for function __strcmp_avx2:
0x00007ffff75c65d0 <+0>: mov %edi,%eax
0x00007ffff75c65d2 <+2>: xor %edx,%edx
=> 0x00007ffff75c65d4 <+4>: vpxor %ymm7,%ymm7,%ymm7
我可以在ifunc-impl-list.c中找到控制选择 avx2 版本的代码:
IFUNC_IMPL (i, name, strcmp,
IFUNC_IMPL_ADD (array, i, strcmp,
HAS_ARCH_FEATURE (AVX2_Usable),
__strcmp_avx2)
IFUNC_IMPL_ADD (array, i, strcmp, HAS_CPU_FEATURE (SSE4_2),
__strcmp_sse42)
IFUNC_IMPL_ADD (array, i, strcmp, HAS_CPU_FEATURE (SSSE3),
__strcmp_ssse3)
IFUNC_IMPL_ADD (array, i, strcmp, 1, __strcmp_sse2_unaligned)
IFUNC_IMPL_ADD (array, i, strcmp, 1, __strcmp_sse2))
看起来这AVX2_Usable
是需要禁用的功能。让我们重新运行 gdb:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable gdb...
在这个迭代中,它抱怨了__memmove_avx_unaligned_erms
,这似乎是由启用的- 但我在ifunc-memmove.h中找到了另一条由启用的AVX_Usable
路径。回到绘图板:AVX_Fast_Unaligned_Load
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
在最后一轮中,我发现了rdtscp
ASAN 共享库中的一条指令,因此我重新进行了编译,没有使用地址清理器,最后,它成功了。
总之:通过一些工作,可以从命令行禁用这些指令并使用 gdb 的记录功能,而无需严重的黑客攻击。
解决方案 2:
似乎没有一种直接的运行时方法来修补功能检测。此检测在动态链接器 (ld.so) 中发生得相当早。
目前看来,二进制修补链接器是最简单的方法。@osgx描述了一种覆盖跳转的方法。另一种方法是伪造 cpuid 结果。通常cpuid(eax=0)
返回支持的最高函数,eax
而制造商 ID 则返回到寄存器 ebx、ecx 和 edx 中。我们在 glibc 2.25 中有这个代码片段sysdeps/x86/cpu-features.c
:
__cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
/* This spells out "GenuineIntel". */
if (ebx == 0x756e6547 && ecx == 0x6c65746e && edx == 0x49656e69)
{
/* feature detection for various Intel CPUs */
}
/* another case for AMD */
else
{
kind = arch_kind_other;
get_common_indeces (cpu_features, NULL, NULL, NULL, NULL);
}
该__cpuid
行翻译为/lib/ld-linux-x86-64.so.2
(/lib/ld-2.25.so
)中的以下指令:
172a8: 31 c0 xor eax,eax
172aa: c7 44 24 38 00 00 00 mov DWORD PTR [rsp+0x38],0x0
172b1: 00
172b2: c7 44 24 3c 00 00 00 mov DWORD PTR [rsp+0x3c],0x0
172b9: 00
172ba: 0f a2 cpuid
因此,我们不必修补分支,而是可以将 更改cpuid
为一条nop
将导致调用最后一个else
分支的指令(因为寄存器将不包含“GenuineIntel”)。由于 最初也将为 0,因此eax=0
也将被绕过。cpu_features->max_cpuid
`if (cpu_features->max_cpuid >= 7)`
可以使用此实用程序进行二进制修补(适用于 x86 和 x86-64 cpuid(eax=0)
):nop
#!/usr/bin/env python
import re
import sys
infile, outfile = sys.argv[1:]
d = open(infile, 'rb').read()
# Match CPUID(eax=0), "xor eax,eax" followed closely by "cpuid"
o = re.sub(b'(x31xc0.{0,32}?)x0fxa2', b'\\1x66x90', d)
assert d != o
open(outfile, 'wb').write(o)
等效的 Perl 变体-0777
可确保立即读取文件,而不是以换行符分隔记录:
perl -0777 -pe 's/x31xc0.{0,32}?Kx0fxa2/x66x90/' < /lib/ld-linux-x86-64.so.2 > ld-linux-x86-64-patched.so.2
# Verify result, should display "Success"
cmp -s /lib/ld-linux-x86-64.so.2 ld-linux-x86-64-patched.so.2 && echo 'Not patched' || echo Success
这是最简单的部分。现在,我不想替换系统范围的动态链接器,而是只用这个链接器执行一个特定的程序。当然,这可以用 来完成./ld-linux-x86-64-patched.so.2 ./a
,但简单的 gdb 调用无法设置断点:
$ gdb -q -ex "set exec-wrapper ./ld-linux-x86-64-patched.so.2" -ex start ./a
Reading symbols from ./a...done.
Temporary breakpoint 1 at 0x400502: file a.c, line 5.
Starting program: /tmp/a
During startup program exited normally.
(gdb) quit
$ gdb -q -ex start --args ./ld-linux-x86-64-patched.so.2 ./a
Reading symbols from ./ld-linux-x86-64-patched.so.2...(no debugging symbols found)...done.
Function "main" not defined.
Temporary breakpoint 1 (main) pending.
Starting program: /tmp/ld-linux-x86-64-patched.so.2 ./a
[Inferior 1 (process 27418) exited normally]
(gdb) quit
如何使用自定义 elf 解释器调试程序?中描述了一种手动解决方法。它有效,但不幸的是,使用 需要手动操作add-symbol-file
。不过,应该可以使用GDB Catchpoints稍微自动化一下。
另一种不使用二进制链接的方法是使用为、等LD_PRELOAD
定义自定义例程的库。这将优先于 glibc 例程。完整的函数列表可在 中找到。与 glibc 2.25 版本相比,当前 HEAD 总共包含更多符号():memcpy
`memovesysdeps/x86_64/multiarch/ifunc-impl-list.c
grep -Po 'IFUNC_IMPL (i, name, K1+' sysdeps/x86_64/multiarch/ifunc-impl-list.c`
memchr、memcmp、__memmove_chk、memmove、memrchr、__memset_chk、memset、rawmemchr、strlen、strnlen、stpncpy、stpcpy、strcasecmp、strcasecmp_l、strcat、strchr、strchrnul、strrchr、strcmp、strcpy、strcspn、strncasecmp、strncasecmp_l、strncat、strncpy、strpbrk、strspn、strstr、wcschr、wcsrchr、wcscpy、wcslen、wcsnlen、wmemchr、wmemcmp、wmemset、__memcpy_chk、memcpy、__mempcpy_chk、mempcpy、strncmp、__wmemset_chk,
解决方案 3:
我最近也遇到了这个问题,最后使用动态 CPUID 故障来中断 CPUID 指令的执行并覆盖其结果,从而避免触及 glibc 或动态链接器。这需要处理器支持 CPUID 故障(Ivy Bridge+)以及 Linux 内核支持(4.12+),以便通过ARCH_GET_CPUID
和 的ARCH_SET_CPUID
子函数将其暴露给用户空间arch_prctl()
。启用此功能后,SIGSEGV
每次执行 CPUID 时都会发出一个信号,允许信号处理程序模拟指令的执行并覆盖结果。
完整的解决方案有点复杂,因为我还需要插入动态链接器,因为从 glibc 2.26+ 开始,硬件能力检测就被移到了那里。我已将完整的解决方案上传到网上https://github.com/ddcc/libcpuidoverride。
解决方案 4:
这不是最好或最完整的解决方案,只是一个最小的位编辑组合,以允许 valgrind 和 gdb 记录我的任务。
Lekensteyn 问道:
如何在不重新编译 glibc 的情况下屏蔽 AVX/SSE
我对未修改的 glibc 进行了完全重建,这在 debian 和 ubuntu 中相当容易:只需sudo apt-get source glibc
,sudo apt-get build-dep glibc
和cd glibc-*/; dpkg-buildpackage -us -uc
(手册即可获得不带剥离调试信息的 ld.so。
然后,我在 使用的函数中对输出的 ld.so 文件进行了二进制(位)修补__get_cpu_features
。目标函数是从get_common_indeces
源文件sysdeps/x86/cpu-features.c
编译而来的,名称为get_common_indeces.constprop.1
(在二进制代码中,它就在 之后__get_cpu_features
)。它有几个 cpuid,第一个是cpuid eax=1
“处理器信息和功能位”;后来检查“jle 0x6”并跳转到代码“cpuid eax=7 ecx=0
扩展功能”以获取 AVX2 状态。有一段代码被编译成这个逻辑:
get_common_indeces (struct cpu_features *cpu_features,
unsigned int *family, unsigned int *model,
unsigned int *extended_model, unsigned int *stepping)
{ ...
if (cpu_features->max_cpuid >= 7)
__cpuid_count (7, 0,
cpu_features->cpuid[COMMON_CPUID_INDEX_7].eax,
cpu_features->cpuid[COMMON_CPUID_INDEX_7].ebx,
cpu_features->cpuid[COMMON_CPUID_INDEX_7].ecx,
cpu_features->cpuid[COMMON_CPUID_INDEX_7].edx);
在同一行中填写cpu_features->max_cpuid
了init_cpu_features
相同的文件。通过将后面的内容替换为(字节 0x7e 到 0x7f) __cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
,可以更轻松地禁用该语句。(实际上,此二进制补丁已手动重新应用于实际系统的功能- 首先将 jle改为 jg。)if
`jlecmp 0x6
jg__get_cpu_features
ld-linux.so.2`mov 7 eax; xor ecx,ecx; cpuid
重新编译的包和修改后的 ld.so 未安装到系统中;我使用了ld.so ./my_program
(或mv ld.so /some/short/path.so
和patchelf --set-interpreter ./my_program
)的命令行语法。
其他可能的解决方案:
尝试使用较新的 valgrind 和 gdb 记录工具
尝试使用较旧的 glibc
如果未完成,则在 gdb 记录中实现缺失指令模拟
if (cpu_features->max_cpuid >= 7)
在 glibc 中对源代码进行修补并重新编译在 glibc 中对支持 avx2 的字符串函数进行源代码修补并重新编译
解决方案 5:
我听说 glibc 中有
/etc/ld.so.nohwcap
和LD_HWCAP_MASK
配置。它们可以用来禁用 glibc 中针对 AVX 优化的字符串函数的 ifunc 调度吗?
是:设置LD_HWCAP_MASK=0
将使 GLIBC 假装没有任何 CPU 功能可用。代码。
将掩码设置为 0 可能会触发错误,您可能需要找出控制 AVX 的精确位,然后仅掩码该位。
- , ↩
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件