Non sembra esserci un metodo di runtime diretto per correggere il rilevamento delle funzionalità. Questo rilevamento avviene piuttosto presto nel linker dinamico (ld.so).
L'applicazione di patch binarie al linker sembra il metodo più semplice al momento. @osgx ha descritto un metodo in cui un salto viene sovrascritto. Un altro approccio è solo quello di falsificare il risultato cpuid. Normalmente cpuid(eax=0)
restituisce la funzione più alta supportata in eax
mentre gli ID produttore vengono restituiti nei registri ebx, ecx ed edx. Abbiamo questo frammento in 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);
}
Il __cpuid
line si traduce in queste istruzioni in /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
Quindi, piuttosto che correggere i rami, potremmo anche cambiare il cpuid
in un nop
istruzione che risulterebbe nell'invocazione dell'ultimo else
filiale (poiché i registri non conterranno "GenuineIntel"). Dall'inizio eax=0
, cpu_features->max_cpuid
sarà anche 0 e if (cpu_features->max_cpuid >= 7)
verrà anche ignorato.
Patch binaria cpuid(eax=0)
di nop
questo può essere fatto con questa utility (funziona sia per x86 che per 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)
Una variante Perl equivalente, -0777
assicura che il file venga letto immediatamente invece di separare i record con avanzamenti di riga:
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
Quella era la parte facile. Ora, non volevo sostituire il linker dinamico a livello di sistema, ma eseguire solo un programma particolare con questo linker. Certo, questo può essere fatto con ./ld-linux-x86-64-patched.so.2 ./a
, ma le invocazioni ingenue di gdb non sono riuscite a impostare i punti di interruzione:
$ 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
Una soluzione manuale è descritta in Come eseguire il debug del programma con l'interprete elf personalizzato? Funziona, ma sfortunatamente è un'azione manuale che utilizza add-symbol-file
. Tuttavia, dovrebbe essere possibile automatizzarlo un po' utilizzando i GDB Catchpoints.
Un approccio alternativo che non prevede il collegamento binario è LD_PRELOAD
ing una libreria che definisce le routine personalizzate per memcpy
, memove
, ecc. Questo avrà quindi la precedenza sulle routine glibc. L'elenco completo delle funzioni è disponibile in sysdeps/x86_64/multiarch/ifunc-impl-list.c
. L'attuale HEAD ha più simboli rispetto alla versione glibc 2.25, in totale (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,
Sembra che ci sia una bella soluzione per questo implementata nelle recenti versioni di glibc:una funzione "sintonizzabili" che guida la selezione di funzioni di stringa ottimizzate. Puoi trovare una panoramica generale di questa funzione qui e il relativo codice all'interno di glibc in ifunc-impl-list.c.
Ecco come l'ho capito. Per prima cosa, ho preso l'indirizzo lamentato da gdb:
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
L'ho quindi cercato nella tabella delle librerie condivise:
(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
...
Puoi vedere che questo indirizzo è all'interno di glibc. Ma quale funzione, nello specifico?
(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
Posso cercare in ifunc-impl-list.c per trovare il codice che controlla la selezione della versione 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))
Sembra AVX2_Usable
è la funzione da disabilitare. Rieseguiamo gdb di conseguenza:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable gdb...
In questa iterazione si è lamentato di __memmove_avx_unaligned_erms
, che sembrava essere abilitato da AVX_Usable
- ma ho trovato un altro percorso in ifunc-memmove.h abilitato da AVX_Fast_Unaligned_Load
. Tornando al tavolo da disegno:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
In questo round finale ho scoperto un rdtscp
istruzione nella libreria condivisa ASAN, quindi l'ho ricompilata senza il disinfettante dell'indirizzo e alla fine ha funzionato.
In sintesi:con un po' di lavoro è possibile disabilitare queste istruzioni dalla riga di comando e utilizzare la funzione di registrazione di gdb senza gravi hack.
Ho riscontrato anche questo problema di recente e ho finito per risolverlo utilizzando l'errore CPUID dinamico per interrompere l'esecuzione dell'istruzione CPUID e sovrascriverne il risultato, evitando di toccare glibc o il linker dinamico. Ciò richiede il supporto del processore per l'errore CPUID (Ivy Bridge+) e il supporto del kernel Linux (4.12+) per esporlo allo spazio utente tramite ARCH_GET_CPUID
e ARCH_SET_CPUID
sottofunzioni di arch_prctl()
. Quando questa funzione è abilitata, un SIGSEGV
il segnale verrà consegnato a ogni esecuzione di CPUID, consentendo a un gestore di segnale di emulare l'esecuzione dell'istruzione e sovrascrivere il risultato.
La soluzione completa è un po' complicata poiché devo anche interporre il linker dinamico, perché il rilevamento delle capacità hardware è stato spostato lì a partire da glibc 2.26+. Ho caricato la soluzione completa online su https://github.com/ddcc/libcpuidoverride .