Отключите 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, и замаскировать именно этот бит.

Другие вопросы по тегам