Отключите AVX-оптимизированные функции в glibc (LD_HWCAP_MASK, /etc/ld.so.nohwcap) для записи valgrind & gdb
Современный x86_64 linux с glibc обнаружит, что процессор поддерживает расширение AVX, и переключит многие строковые функции с универсальной реализации на AVX-оптимизированную версию (с помощью диспетчеров ifunc: 1, 2).
Эта функция может быть полезна для производительности, но она предотвращает использование нескольких инструментов, таких как valgrind ( более старые libVEXs, до valgrind-3.8) и gdb's 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
Есть сообщение об ошибке Process record does not support instruction 0xc5
"из реализации GDB" целевой записи ", потому что инструкции 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 не помогает.
Я слышал, что есть /etc/ld.so.nohwcap
а также LD_HWCAP_MASK
конфигурации в glibc. Можно ли их использовать для отключения диспетчеризации ifunc для строковых функций, оптимизированных для AVX, в glibc?
Как glibc (rtld?) Обнаруживает AVX, используя cpuid
, с /proc/cpuinfo
(вероятно, нет), или HWCAP aux (LD_SHOW_AUXV=1 /bin/echo |grep HWCAP
команда дает AT_HWCAP: bfebfbff
)?
3 ответа
Похоже, что есть хороший обходной путь, реализованный в последних версиях 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
, который, по-видимому, был включен AVX_Usable
- но я нашел другой путь в ifunc-memmove.h, включенныйAVX_Fast_Unaligned_Load
. Вернуться к доске для рисования:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
В этом последнем раунде я обнаружил rdtscp
инструкции в разделяемой библиотеке ASAN, поэтому я перекомпилировал без средства очистки адресов, и, наконец, все заработало.
В итоге: немного поработав, можно отключить эти инструкции из командной строки и использовать функцию записи gdb без серьезных взломов.
Кажется, не существует простого метода времени выполнения для исправления обнаружения функций. Это обнаружение происходит довольно рано в динамическом компоновщике (ld.so).
Двоичное исправление компоновщика кажется самым простым способом на данный момент. @osgx описал один метод, в котором переход перезаписывается. Другой подход - просто подделать результат cpuid. Обычно cpuid(eax=0)
возвращает самую высокую поддерживаемую функцию в eax
в то время как идентификаторы производителя возвращаются в регистрах 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)
также будет обойден.
Бинарное исправление cpuid(eax=0)
от nop
это можно сделать с помощью этой утилиты (работает как для 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)
Это была легкая часть. Теперь я не хотел заменять общесистемный динамический компоновщик, а выполнял только одну конкретную программу с этим компоновщиком. Конечно, это можно сделать с ./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
, Должна быть возможность немного автоматизировать его с помощью GDB Catchpoints.
Альтернативный подход, который не двоичные ссылки LD_PRELOAD
в библиотеке, которая определяет пользовательские процедуры для memcpy
, memove
и т. д. Это будет иметь приоритет перед процедурами glibc. Полный список функций доступен в sysdeps/x86_64/multiarch/ifunc-impl-list.c
, Текущий заголовок имеет больше символов по сравнению с выпуском glibc 2.25, всего (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, зЬгстр, зЬгсру, strcspn, strncasecmp, strncasecmp_l, strncat, strncpy, strpbrk, strspn, strstr, wcschr, wcsrchr, wcscpy, wcslen, wcsnlen, wmemchr, wmemcmp, wmemset, __memcpy_chk, memcpy, __mempcpy_chk, mempcpy, strncmp_chk
Недавно я столкнулся с этой проблемой и решил ее с помощью динамического сбоя 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.
Не лучшее или полное решение, просто маленький бит-редактор для разрешения битов, позволяющий использовать valgrind и gdb для моей задачи.
как замаскировать AVX/SSE без перекомпиляции glibc
Я полностью перестроил немодифицированный 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 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
того же файла в __cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
линия. Было проще отключить if
утверждение путем замены jle
после cmp 0x6
с jg
(байт от 0x7e до 0x7f). (На самом деле этот двоичный патч был повторно применен вручную к __get_cpu_features
функция реальной системы ld-linux.so.2
- первый джл до mov 7 eax; xor ecx,ecx; cpuid
изменился в jg.)
Перекомпилированный пакет и измененный 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 и перекомпилировать - сделать исправления исходного кода вокруг строковых функций с поддержкой avx2 в glibc и перекомпилировать
Я слышал, что есть
/etc/ld.so.nohwcap
а такжеLD_HWCAP_MASK
конфигурации в glibc. Можно ли их использовать для отключения диспетчеризации ifunc для строковых функций, оптимизированных для AVX, в glibc?
Да: настройка LD_HWCAP_MASK=0
заставит GLIBC делать вид, что ни одна из возможностей ЦП не доступна. Код
Установка маски в 0, вероятно, вызовет ошибку, вам, вероятно, потребуется выяснить точный бит, который управляет AVX, и замаскировать именно этот бит.