在 glibc(LD_HWCAP_MASK,/etc/ld.so.nohwcap)中为 valgrind 和 gdb 记录禁用 AVX 优化函数
Posted
技术标签:
【中文标题】在 glibc(LD_HWCAP_MASK,/etc/ld.so.nohwcap)中为 valgrind 和 gdb 记录禁用 AVX 优化函数【英文标题】:Disable AVX-optimized functions in glibc (LD_HWCAP_MASK, /etc/ld.so.nohwcap) for valgrind & gdb record 【发布时间】:2017-07-16 01:03:37 【问题描述】:带有 glibc 的现代 x86_64 linux 将检测到 CPU 支持 AVX 扩展,并将许多字符串函数从通用实现切换到 AVX-optimized 版本(在 ifunc 调度程序的帮助下:1、2)。
此功能可能对性能有好处,但它会阻止一些工具,如 valgrind(older libVEXs,valgrind-3.8 之前)和 gdb 的“target record
”(Reverse Execution)正常工作(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)。”或者设置LD_BIND_NOW=1
,但是重新编译的glibc还有AVX,ld bind-now也没用。
听说glibc中有/etc/ld.so.nohwcap
和LD_HWCAP_MASK
的配置。它们可以用于禁用 ifunc 分派到 glibc 中的 AVX 优化字符串函数吗?
glibc (rtld?) 如何检测 AVX,使用 cpuid
、/proc/cpuinfo
(可能不是)或 HWCAP aux(LD_SHOW_AUXV=1 /bin/echo |grep HWCAP
命令给出 AT_HWCAP: bfebfbff
)?
【问题讨论】:
选择代码:github.com/bminor/glibc/blob/master/sysdeps/x86_64/multiarch/…ENTRY(__new_memcpy) .type __new_memcpy, @gnu_indirect_function .. .HAS_ARCH_FEATURE (Prefer_ERMS)
其中..feature 定义在github.com/bminor/glibc/blob/master/sysdeps/x86/cpu-features.h;使用eax=7,ecx=0 的cpuid
指令,由init_cpu_features
填充测试字段。如何侵入init_cpu_features
并屏蔽cpu_features->cpuid[COMMON_CPUID_INDEX_7].ecx
中的AVX/ERMS?
你有没有想过如何在不重新编译 glibc 的情况下屏蔽 AVX/SSE?功能似乎已加载到 sysdeps/x86/libc-start.c
中(__libc_start_main
调用 init_cpu_features (&_dl_x86_cpu_features)
),但此时符号似乎已经解析(基于指向 __memmove_avx_unaligned_erms
的 p *memcpy
)。
@Lekensteyn,“如何在不重新编译 glibc 的情况下屏蔽 AVX/SSE” - 我在 __get_cpu_features
函数(@ 987654360@ / get_common_indeces.constprop.1
), cpuid,..,然后在 cpm 0xf,.. ;je ..; cmp 0x6
之后将 jle
替换为 jg
(0x7e 到 0x7f) - 可能禁用 if .. max_cpuid>=7
的 sysdeps/x86/cpu-features.c
之后的所有代码。或者尝试使用更新的 valgrind 和 gdb 记录工具或旧的 glibc 或在 gdb 记录中实现缺失指令仿真(如果没有完成)。
作为一种可能的解决方法,Mozilla 的 rr
与 AVX 一起使用:***.com/questions/40125154/…
【参考方案1】:
看起来在最近版本的 glibc 中实现了一个很好的解决方法:指导选择优化字符串函数的“可调参数”功能。您可以在here 中找到此功能的总体概述以及 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
,它似乎是由AVX_Usable
启用的——但我在ifunc-memmove.h 中发现了另一条由AVX_Fast_Unaligned_Load
启用的路径。回到绘图板:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
在最后一轮中,我在 ASAN 共享库中发现了一条 rdtscp
指令,因此我在没有使用地址清理程序的情况下重新编译,终于成功了。
总而言之:通过一些工作,可以从命令行禁用这些指令并使用 gdb 的记录功能而无需严重的黑客攻击。
【讨论】:
我正在尝试使用 GLIBC_TUNABLES 的这个标志,但我仍然遇到同样的错误。看起来它忽略了 GLIBC_TUNABLES 设置。对我有什么想法吗? 检查你的 glibc 版本,也许?【参考方案2】:似乎没有直接的运行时方法来修补特征检测。这种检测在动态链接器 (ld.so) 中发生得相当早。
目前,对链接器进行二进制修补似乎是最简单的方法。 @osgx described 一种覆盖跳转的方法。另一种方法是伪造 cpuid 结果。通常cpuid(eax=0)
返回eax
中支持的最高功能,而制造商ID 在寄存器ebx、ecx 和edx 中为returned。我们在 glibc 2.25 sysdeps/x86/cpu-features.c
中有这个 sn-p:
__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”)。由于最初eax=0
,cpu_features->max_cpuid
也将为 0,if (cpu_features->max_cpuid >= 7)
也将被绕过。
通过nop
对cpuid(eax=0)
进行二进制修补可以使用此实用程序完成(适用于x86 和x86-64):
#!/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'(\x31\xc0.0,32?)\x0f\xa2', b'\\1\x66\x90', d)
assert d != o
open(outfile, 'wb').write(o)
等效的 Perl 变体 -0777
确保文件被立即读取,而不是在换行符处分隔记录:
perl -0777 -pe 's/\x31\xc0.0,32?\K\x0f\xa2/\x66\x90/' < /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
How to debug program with custom elf interpreter? 中描述了手动解决方法,它可以工作,但不幸的是,它是使用add-symbol-file
的手动操作。不过应该可以使用GDB Catchpoints 将其自动化一点。
另一种不使用二进制链接的方法是LD_PRELOAD
为memcpy
、memove
等定义自定义例程的库。这将优先于glibc 例程。 sysdeps/x86_64/multiarch/ifunc-impl-list.c
中提供了完整的函数列表。与 glibc 2.25 版本相比,当前 HEAD 具有更多符号 (grep -Po 'IFUNC_IMPL \(i, name, \K[^,]+' sysdeps/x86_64/multiarch/ifunc-impl-list.c
):
内存, memcmp, __memmove_chk, 记忆移动, 记忆体, __memset_chk, 内存集, rawmemchr, 斯特伦, strnlen, stpncpy, stpcpy, strcasecmp, strcasecmp_l, strcat, strchr, strchrnul, 字符串, strcmp, 字符串, strcspn, strncasecmp, strncasecmp_l, strncat, strncpy, strpbrk, strspn, 字符串, wcschr, wcsrchr, wcscpy, wcslen, wcsnlen, wmemchr, wmemcmp, wmemset, __memcpy_chk, 内存, __mempcpy_chk, 记忆力, strncmp, __wmemset_chk,
【讨论】:
"alternative approach" 如果在 ld.so 中使用了支持 AVX2 的函数,则可能会失败(有一些,不知道它们是否在预加载 LD_PRELOAD 之前使用)。尝试patchelf --set-interpreter /some/short/path/ld.so ./my_program
,其中新 ld.so 的路径不长于原始 ld.so 路径 - 始终为程序使用新的 ld.so。
这是一个 perl 命令,它执行 cpuid 到 nop 修改:perl -pe 's/\x31\xc0.0,32\K\x0f\xa2/\x66\x90/' < ld-linux-orig > ld-linux-patched
这很容易坚持构建系统规则来修补加载程序。在我的例子中,buildroot 中有一个工具链 post staging install hook。
@TrentP:理论上,该模式可能对出现在另一条指令内或跨越指令边界的0F A2
有误报,在某处有 32 字节的 31 C0 xor eax,eax
。 cmp -l
您的输入/输出可能是个好主意,并确保只发生了一次替换。
@PeterCordes 我用非贪婪版本更新了帖子,以防万一有相邻的cpuid
(我不希望这是诚实的)。为方便起见,我还添加了一个 Perl 单行代码。【参考方案3】:
我最近也遇到了这个问题,最终使用动态 CPUID 故障来中断 CPUID 指令的执行并覆盖其结果,从而避免接触 glibc 或动态链接器。这需要处理器支持 CPUID 故障 (Ivy Bridge+) 以及 Linux 内核支持 (4.12+),以便通过 ARCH_GET_CPUID
和 ARCH_SET_CPUID
的子函数将其暴露给用户空间@ 987654327@。启用此功能后,每次执行 CPUID 时都会传递一个SIGSEGV
信号,允许信号处理程序模拟指令的执行并覆盖结果。
完整的解决方案有点复杂,因为我还需要插入动态链接器,因为从 glibc 2.26+ 开始,硬件能力检测被移到那里。我已经在https://github.com/ddcc/libcpuidoverride 在线上传了完整的解决方案。
【讨论】:
您好,感谢您的工作。 this feature 可以与任何“Atom”系列英特尔 CPU(MSR_PLATFORM_INFO 的第 31 位 = MSR CEh)一起使用吗?有AMD吗?更多细节在你的帖子dcddcc.com/blog/… 我不确定,网上似乎没有太多关于这个的。但是,如果您可以访问基于 Atom 的系统,那么测试应该很简单。 Intel FlexMigration 支持另一种称为 CPUID 屏蔽的模式,处理器可以直接屏蔽 CPUID 位,但这似乎没有在 Linux 内核中实现或支持:github.com/torvalds/linux/blob/master/arch/x86/include/asm/…。同样,AMD Extended Migration 有一个类似的功能,称为 CPUID Override,看起来类似于屏蔽,但我不相信它也在 Linux 中实现。【参考方案4】:不是最好的或完整的解决方案,只是一个最小的位编辑工具,允许 valgrind 和 gdb 记录我的任务。
Lekensteyn asks:
如何在不重新编译 glibc 的情况下屏蔽 AVX/SSE
我对未修改的 glibc 进行了完全重建,这在 debian 和 ubuntu 中相当容易:只需 sudo apt-get source glibc
、sudo apt-get build-dep glibc
和 cd glibc-*/; dpkg-buildpackage -us -uc
(manual 即可在不剥离调试信息的情况下获得 ld.so。
然后我在__get_cpu_features
使用的函数中对输出的 ld.so 文件进行了二进制(位)修补。目标函数是从 get_common_indeces.constprop.1
名称下的 get_common_indeces
of source file sysdeps/x86/cpu-features.c
编译而来的(它在二进制代码中的 __get_cpu_features
之后紧随其后)。它有几个cpuid,第一个是cpuid eax=1
"Processor Info and Feature Bits";然后检查“jle 0x6”并在代码"cpuid eax=7 ecx=0
Extended Features" 周围跳下来,只是为了获得 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);
__cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
行中的init_cpu_features
of the same file 填写了cpu_features->max_cpuid
。通过将cmp 0x6
之后的jle
替换为jg
(字节0x7e 到0x7f),更容易禁用if
语句。 (实际上这个二进制补丁是手动重新应用到真实系统ld-linux.so.2
的__get_cpu_features
函数-mov 7 eax; xor ecx,ecx; cpuid
之前的第一个jle变成了jg。)
重新编译的包和修改的ld.so没有安装到系统中;我使用了ld.so ./my_program
(或mv ld.so /some/short/path.so
和patchelf --set-interpreter ./my_program
)的命令行语法。
其他可能的解决方案:
尝试使用更新的 valgrind 和 gdb 记录工具 尝试使用旧的 glibc 如果没有完成,则在 gdb 记录中实现缺少的指令仿真 在 glibc 中对if (cpu_features->max_cpuid >= 7)
进行源代码修补并重新编译
围绕 glibc 中启用 avx2 的字符串函数进行源代码修补并重新编译
【讨论】:
在 Arch Linux x86_64 上使用最新的 gdb 8.0 版本和 glibc 2.25,我仍然无法在 AVX 指令(在未修补的二进制文件上)上跳闸。您是如何让 GDB 使用自定义链接器的?我在回答中描述了一种方法,但也许您有更好的方法? 我用patchelf --set-interpreter /some/short/path.so ./my_program
重写了程序的INTERP部分。【参考方案5】:
听说glibc中有
/etc/ld.so.nohwcap
和LD_HWCAP_MASK
的配置。它们可以用于禁用 ifunc 调度到 glibc 中的 AVX 优化字符串函数吗?
是的:设置LD_HWCAP_MASK=0
将使GLIBC 假装没有任何CPU 功能可用。 Code.
将掩码设置为 0 可能会触发错误,您可能需要找出控制 AVX 的精确位,并仅屏蔽该位。
【讨论】:
正如我测试的那样,这不会禁用github.com/bminor/glibc/blob/master/sysdeps/x86/cpu-features.c 中的 cpuid 检测 (ax=7,cx=0),它用于在 ifuncs (ENTRY(__new_memcpy) .. HAS_ARCH_FEATURE (Prefer_ERMS)
) 中选择 ERMS 字符串函数。并且 _dl_runtime_resolve_avx 在 github.com/bminor/glibc/blob/… elf_machine_runtime_setup
的 HAS_ARCH_FEATURE (AVX_Usable)
标志上也被选中,而不是在 hwcaps 上(这部分在某些情况下可以通过 LD_BIND_NOW=1 修复)。以上是关于在 glibc(LD_HWCAP_MASK,/etc/ld.so.nohwcap)中为 valgrind 和 gdb 记录禁用 AVX 优化函数的主要内容,如果未能解决你的问题,请参考以下文章