Отключить функции оптимизации 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 "target record" (Обратное выполнение) от правильной работы (Ubuntu "Z" 17.04 бета, 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 p >

Решение, предложенное в 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 на оптимизированные по AVX строковые функции в glibc?

Как glibc (rtld?) обнаруживает AVX, используя cpuid, с /proc/cpuinfo (возможно, нет), или HWCAP aux ( Команда LD_SHOW_AUXV=1 /bin/echo |grep HWCAP дает AT_HWCAP: bfebfbff)?

Ответ 1

Кажется, не существует простого метода времени выполнения для исправления обнаружения функций. Это обнаружение происходит довольно рано в динамическом компоновщике (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. Текущий HEAD имеет больше символов по сравнению с выпуском 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

Ответ 2

Не лучшее или полное решение, это всего лишь небольшой бит-редактирующий клид, чтобы сделать запись 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 (manual, чтобы получить ld.so без отлаженной информации об отладке.

Затем я выполнил двоичное (бит) исправление выходного файла ld.so в функции, используемой __get_cpu_features. Целевая функция была скомпилирована из get_common_indeces исходного файла sysdeps/x86/cpu-features.c под именем get_common_indeces.constprop.1 (это просто после __get_cpu_features в двоичном коде). Он имеет несколько cpuids, первый - 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 - сначала jle до 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 и перекомпилировать

Ответ 3

Я также недавно столкнулся с этой проблемой и в итоге решил ее, используя динамический сбой 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.

Ответ 4

Я слышал, что в glibc есть конфигурации /etc/ld.so.nohwcap и LD_HWCAP_MASK. Могут ли они использоваться для отключения отправки ifunc на оптимизированные по AVX строковые функции в glibc?

Да: настройка LD_HWCAP_MASK=0 заставит GLIBC притвориться, что ни одна из возможностей ЦП недоступна. Код.

Установка маски на 0, скорее всего, вызовет ошибку, вам, скорее всего, нужно будет определить точный бит, который управляет AVX, и замаскировать только этот бит.