在 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.nohwcapLD_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-&gt;cpuid[COMMON_CPUID_INDEX_7].ecx 中的AVX/ERMS? 你有没有想过如何在不重新编译 glibc 的情况下屏蔽 AVX/SSE?功能似乎已加载到 sysdeps/x86/libc-start.c 中(__libc_start_main 调用 init_cpu_features (&amp;_dl_x86_cpu_features)),但此时符号似乎已经解析(基于指向 __memmove_avx_unaligned_ermsp *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&gt;=7sysdeps/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=0cpu_features-&gt;max_cpuid 也将为 0,if (cpu_features-&gt;max_cpuid &gt;= 7) 也将被绕过。

通过nopcpuid(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_PRELOADmemcpymemove 等定义自定义例程的库。这将优先于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/' &lt; ld-linux-orig &gt; ld-linux-patched 这很容易坚持构建系统规则来修补加载程序。在我的例子中,buildroot 中有一个工具链 post staging install hook。 @TrentP:理论上,该模式可能对出现在另一条指令内或跨越指令边界的0F A2 有误报,在某处有 32 字节的 31 C0 xor eax,eaxcmp -l 您的输入/输出可能是个好主意,并确保只发生了一次替换。 @PeterCordes 我用非贪婪版本更新了帖子,以防万一有相邻的cpuid(我不希望这是诚实的)。为方便起见,我还添加了一个 Perl 单行代码。【参考方案3】:

我最近也遇到了这个问题,最终使用动态 CPUID 故障来中断 CPUID 指令的执行并覆盖其结果,从而避免接触 glibc 或动态链接器。这需要处理器支持 CPUID 故障 (Ivy Bridge+) 以及 Linux 内核支持 (4.12+),以便通过 ARCH_GET_CPUIDARCH_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 glibcsudo apt-get build-dep glibccd 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-&gt;max_cpuid, ebx, ecx, edx); 行中的init_cpu_features of the same file 填写了cpu_features-&gt;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.sopatchelf --set-interpreter ./my_program)的命令行语法。

其他可能的解决方案:

尝试使用更新的 valgrind 和 gdb 记录工具 尝试使用旧的 glibc 如果没有完成,则在 gdb 记录中实现缺少的指令仿真 在 glibc 中对 if (cpu_features-&gt;max_cpuid &gt;= 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.nohwcapLD_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_setupHAS_ARCH_FEATURE (AVX_Usable) 标志上也被选中,而不是在 hwcaps 上(这部分在某些情况下可以通过 LD_BIND_NOW=1 修复)。

以上是关于在 glibc(LD_HWCAP_MASK,/etc/ld.so.nohwcap)中为 valgrind 和 gdb 记录禁用 AVX 优化函数的主要内容,如果未能解决你的问题,请参考以下文章

安装mysqlmysql-5.7.24-linux-glibc2.12-x86_64

Mysql Cluster7.4.6安装与配置

glibc误删处理

GLIBC_2.14 not found

glibc2.12升级glibc2.14源码

Linux升级Glibc时系统奔溃是啥原因如何解决