GNU/Linux >> Znalost Linux >  >> Linux

Zakázat funkce optimalizované pro AVX v glibc (LD_HWCAP_MASK, /etc/ld.so.nohwcap) pro záznam valgrind a gdb

Zdá se, že neexistuje přímočará runtime metoda pro opravu detekce funkcí. K této detekci dochází poměrně brzy v dynamickém linkeru (ld.so).

Binární záplatování linkeru se v tuto chvíli zdá nejjednodušší metodou. @osgx popsal jednu metodu, kdy se skok přepíše. Dalším přístupem je pouze falešný výsledek cpuid. Obvykle cpuid(eax=0) vrátí nejvyšší podporovanou funkci v eax zatímco ID výrobce se vrací v registrech ebx, ecx a edx. Tento fragment máme v 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 řádek se převádí na tyto instrukce v /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  

Takže spíše než záplatování větví bychom mohli také změnit cpuid do nop instrukce, která by vedla k vyvolání posledního else větev (protože registry nebudou obsahovat "GenuineIntel"). Od počátku eax=0 , cpu_features->max_cpuid bude také 0 a if (cpu_features->max_cpuid >= 7) bude také vynechán.

Binární záplatování cpuid(eax=0) podle nop to lze provést pomocí tohoto nástroje (funguje pro x86 i 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)

Ekvivalentní varianta jazyka Perl, -0777 zajišťuje, že soubor je přečten najednou namísto oddělování záznamů na řádku:

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

To byla ta snadná část. Nyní jsem nechtěl nahradit systémový dynamický linker, ale spustit pouze jeden konkrétní program s tímto linkerem. Jistě, to lze provést pomocí ./ld-linux-x86-64-patched.so.2 ./a , ale naivním vyvoláním gdb se nepodařilo nastavit zarážky:

$ 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                                                                                                                                                                         

Ruční řešení je popsáno v Jak ladit program pomocí vlastního elf interpretu? Funguje to, ale bohužel se jedná o ruční zásah pomocí add-symbol-file . Mělo by být možné to trochu automatizovat pomocí GDB Catchpoints.

Alternativní přístup, který nepoužívá binární propojení, je LD_PRELOAD vytvoření knihovny, která definuje vlastní rutiny pro memcpy , memove , atd. To pak bude mít přednost před rutinami glibc. Úplný seznam funkcí je k dispozici v sysdeps/x86_64/multiarch/ifunc-impl-list.c . Současná HEAD má více symbolů ve srovnání s vydáním glibc 2.25 celkem (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,strcmpcase,strppp strpbrk,strspn,strstr,wcschr,wcsrchr,wcscpy,wcslen,wcsnlen,wmemchr,wmemcmp,wmemset,__memcpy_chk,memcpy,__mempcpy_chk,mempcpy,strncmp,__wmemset_chk,

Zdá se, že v posledních verzích glibc je pro to implementováno pěkné řešení:funkce „laditelných funkcí“, která vede výběr optimalizovaných funkcí řetězců. Obecný přehled této funkce naleznete zde a příslušný kód v glibc na ifunc-impl-list.c.

Tady je návod, jak jsem na to přišel. Nejprve jsem vzal adresu, na kterou si stěžoval gdb:

Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.

Pak jsem to vyhledal v tabulce sdílených knihoven:

(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
...

Můžete vidět, že tato adresa je v glibc. Ale jakou funkci konkrétně?

(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

Mohu se podívat na ifunc-impl-list.c a najít kód, který řídí výběr verze 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))

Vypadá to jako AVX2_Usable je funkce k deaktivaci. Spusťte gdb podle toho znovu:

GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable gdb...

V této iteraci si stěžoval na __memmove_avx_unaligned_erms , která se zdá být povolena AVX_Usable - ale našel jsem jinou cestu v ifunc-memmove.h povolenou pomocí AVX_Fast_Unaligned_Load . Zpět na rýsovací prkno:

GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...

V tomto posledním kole jsem objevil rdtscp instrukce ve sdílené knihovně ASAN, takže jsem překompiloval bez dezinfekce adres a nakonec to fungovalo.

Shrnuto:s trochou práce je možné tyto instrukce zakázat z příkazového řádku a používat funkci záznamu gdb bez závažných hacků.


Nedávno jsem se s tímto problémem také setkal a nakonec jsem jej vyřešil pomocí dynamického chybování CPUID k přerušení provádění instrukce CPUID a přepsání jejího výsledku, což zabraňuje dotyku glibc nebo dynamického linkeru. To vyžaduje podporu procesoru pro chyby CPUID (Ivy Bridge+) a také podporu jádra Linuxu (4.12+) pro jeho vystavení uživatelskému prostoru prostřednictvím ARCH_GET_CPUID a ARCH_SET_CPUID podfunkce arch_prctl() . Když je tato funkce povolena, zobrazí se SIGSEGV signál bude doručen při každém provedení CPUID, což umožňuje obsluze signálu emulovat provedení instrukce a přepsat výsledek.

Úplné řešení je trochu zapojeno, protože také musím vložit dynamický linker, protože tam byla přesunuta detekce hardwarových schopností počínaje glibc 2.26+. Úplné řešení jsem nahrál online na https://github.com/ddcc/libcpuidoverride.


Linux
  1. Rozdíl mezi ~/.profile, ~/.bashrc, ~/.bash_profile, ~/.gnomerc, /etc/bash_bashrc, /etc/screenrc …?

  2. Jak se aktualizuje /etc/motd?

  3. CentOS / RHEL :Jak obnovit ze smazaného souboru /etc/passwd

  1. Kdy mám použít /dev/shm/ a kdy /tmp/?

  2. /etc/passwd zobrazuje uživatele ve skupině, ale /etc/group nikoli

  3. Jaké je spojení mezi adresáři /etc/init.d a /etc/rcX.d v Linuxu?

  1. Šablony pro spouštěcí skript?

  2. Proč mají všechny adresáře /home, /usr, /var atd. stejné číslo inodu (2)?

  3. Měly by weby žít ve /var/ nebo /usr/ podle doporučeného použití?