在 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
带有 glibc 的现代 x86_64 linux 将检测到 CPU 支持 AVX 扩展,并将许多字符串函数从通用实现切换到 AVX-optimized version (with help of ifunc dispatchers: 1, 2)。
此功能可以提高性能,但它会阻止 valgrind (older libVEXs, before valgrind-3.8) and gdb's "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 执行 "target record" 时出现错误消息“Process record does not support instruction 0xc5
”,因为 record/replay 引擎不支持 AVX 指令(有时会在 _dl_runtime_resolve_avx
函数):https://sourceware.org/ml/gdb/2016-08/msg00028.html "some AVX instructions are not supported by process record", 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 "You can recompile libc (thus ld.so), or hack __init_cpu_features and thus __cpu_features at runtime (see e.g. 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
)?
I heard that there are /etc/ld.so.nohwcap
and LD_HWCAP_MASK
configurations in glibc. Can they be used to disable ifunc dispatching to AVX-optimized string functions in glibc?
是:设置 LD_HWCAP_MASK=0
将使 GLIBC 假装 CPU 功能中的 none 可用。 Code.
将掩码设置为 0 可能会触发错误,您可能需要找出控制 AVX 的精确位,然后仅屏蔽该位。
不是最好或完整的解决方案,只是允许 valgrind 和 gdb 记录我的任务的最小位编辑工具。
:
how to mask out AVX/SSE without recompiling glibc
我完全重建了未修改的 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
of source file sysdeps/x86/cpu-features.c
under the name of get_common_indeces.constprop.1
(it is just next after the __get_cpu_features
in the binary code). It has several cpuids, first one is cpuid eax=1
"Processor Info and Feature Bits"; and later there is check "jle 0x6" and jump down around the code "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);
cpu_features->max_cpuid
填入 __cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
行的 init_cpu_features
of the same file。通过将 cmp 0x6
之后的 jle
替换为 jg
(字节 0x7e 到 0x7f),可以更轻松地禁用 if
语句。 (实际上这个二进制补丁被手动重新应用到真实系统 ld-linux.so.2
的 __get_cpu_features
函数 - 在 mov 7 eax; xor ecx,ecx; cpuid
更改为 jg 之前的第一个 jle。)
重新编译的包和修改的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 的字符串函数修补源代码并重新编译
似乎没有直接的运行时方法来修补特征检测。这种检测发生在动态链接器的早期 (ld.so)。
对链接器进行二进制修补目前看来是最简单的方法。 @osgx one method where a jump is overwritten. Another approach is just to fake the cpuid result. Normally cpuid(eax=0)
returns the highest supported function in eax
while the manufacturer IDs are returned 在寄存器 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")。由于最初 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
虽然在 It works, but it is unfortunately a manual action using add-symbol-file
. It should be possible to automate it a bit using GDB Catchpoints 中描述了手动解决方法。
另一种不进行二进制链接的方法是 LD_PRELOAD
ing 一个为 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
):
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,
我最近也遇到了这个问题,最后使用动态 CPUID 错误来中断 CPUID 指令的执行并覆盖其结果来解决它,这避免了触及 glibc 或动态链接器。这需要处理器支持 CPUID 故障 (Ivy Bridge+) 以及 Linux 内核支持 (4.12+) 通过 arch_prctl()
的 ARCH_GET_CPUID
和 ARCH_SET_CPUID
子函数将其暴露给用户空间.启用此功能后,将在每次执行 CPUID 时传递一个 SIGSEGV
信号,允许信号处理程序可以模拟指令的执行并覆盖结果。
完整的解决方案有点复杂,因为我还需要插入动态链接器,因为从 glibc 2.26+ 开始,硬件功能检测已移至那里。我已经在 https://github.com/ddcc/libcpuidoverride 在线上传了完整的解决方案。
在最近的 glibc 版本中似乎有一个很好的解决方法:一个 "tunables" 功能可以指导优化字符串函数的选择。您可以找到此功能的一般概述 here and the relevant code inside glibc in ifunc-impl-list.c。
我是这样想的。首先,我把gdb抱怨的地址:
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
然后我在共享库的 table 中查找:
(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 的现代 x86_64 linux 将检测到 CPU 支持 AVX 扩展,并将许多字符串函数从通用实现切换到 AVX-optimized version (with help of ifunc dispatchers: 1, 2)。
此功能可以提高性能,但它会阻止 valgrind (older libVEXs, before valgrind-3.8) and gdb's "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 执行 "target record" 时出现错误消息“Process record does not support instruction 0xc5
”,因为 record/replay 引擎不支持 AVX 指令(有时会在 _dl_runtime_resolve_avx
函数):https://sourceware.org/ml/gdb/2016-08/msg00028.html "some AVX instructions are not supported by process record", 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 "You can recompile libc (thus ld.so), or hack __init_cpu_features and thus __cpu_features at runtime (see e.g. 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
)?
I heard that there are
/etc/ld.so.nohwcap
andLD_HWCAP_MASK
configurations in glibc. Can they be used to disable ifunc dispatching to AVX-optimized string functions in glibc?
是:设置 LD_HWCAP_MASK=0
将使 GLIBC 假装 CPU 功能中的 none 可用。 Code.
将掩码设置为 0 可能会触发错误,您可能需要找出控制 AVX 的精确位,然后仅屏蔽该位。
不是最好或完整的解决方案,只是允许 valgrind 和 gdb 记录我的任务的最小位编辑工具。
how to mask out AVX/SSE without recompiling glibc
我完全重建了未修改的 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
of source file sysdeps/x86/cpu-features.c
under the name of get_common_indeces.constprop.1
(it is just next after the __get_cpu_features
in the binary code). It has several cpuids, first one is cpuid eax=1
"Processor Info and Feature Bits"; and later there is check "jle 0x6" and jump down around the code "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);
cpu_features->max_cpuid
填入 __cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
行的 init_cpu_features
of the same file。通过将 cmp 0x6
之后的 jle
替换为 jg
(字节 0x7e 到 0x7f),可以更轻松地禁用 if
语句。 (实际上这个二进制补丁被手动重新应用到真实系统 ld-linux.so.2
的 __get_cpu_features
函数 - 在 mov 7 eax; xor ecx,ecx; cpuid
更改为 jg 之前的第一个 jle。)
重新编译的包和修改的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 的字符串函数修补源代码并重新编译
似乎没有直接的运行时方法来修补特征检测。这种检测发生在动态链接器的早期 (ld.so)。
对链接器进行二进制修补目前看来是最简单的方法。 @osgx cpuid(eax=0)
returns the highest supported function in eax
while the manufacturer IDs are returned 在寄存器 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")。由于最初 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
虽然在 add-symbol-file
. It should be possible to automate it a bit using GDB Catchpoints 中描述了手动解决方法。
另一种不进行二进制链接的方法是 LD_PRELOAD
ing 一个为 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
):
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,
我最近也遇到了这个问题,最后使用动态 CPUID 错误来中断 CPUID 指令的执行并覆盖其结果来解决它,这避免了触及 glibc 或动态链接器。这需要处理器支持 CPUID 故障 (Ivy Bridge+) 以及 Linux 内核支持 (4.12+) 通过 arch_prctl()
的 ARCH_GET_CPUID
和 ARCH_SET_CPUID
子函数将其暴露给用户空间.启用此功能后,将在每次执行 CPUID 时传递一个 SIGSEGV
信号,允许信号处理程序可以模拟指令的执行并覆盖结果。
完整的解决方案有点复杂,因为我还需要插入动态链接器,因为从 glibc 2.26+ 开始,硬件功能检测已移至那里。我已经在 https://github.com/ddcc/libcpuidoverride 在线上传了完整的解决方案。
在最近的 glibc 版本中似乎有一个很好的解决方法:一个 "tunables" 功能可以指导优化字符串函数的选择。您可以找到此功能的一般概述 here and the relevant code inside glibc in ifunc-impl-list.c。
我是这样想的。首先,我把gdb抱怨的地址:
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
然后我在共享库的 table 中查找:
(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 的记录功能而无需严重的黑客攻击。