Il ne semble pas y avoir de méthode d'exécution simple pour corriger la détection des fonctionnalités. Cette détection se produit assez tôt dans l'éditeur de liens dynamique (ld.so).
Le patch binaire de l'éditeur de liens semble la méthode la plus simple pour le moment. @osgx a décrit une méthode où un saut est écrasé. Une autre approche consiste simplement à simuler le résultat cpuid. Normalement cpuid(eax=0)
renvoie la fonction la plus élevée prise en charge dans eax
tandis que les identifiants des fabricants sont retournés dans les registres ebx, ecx et edx. Nous avons cet extrait dans la 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);
}
Le __cpuid
la ligne se traduit par ces instructions dans /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
Donc plutôt que de patcher les branches, on pourrait aussi bien changer le cpuid
dans un nop
instruction qui entraînerait l'invocation du dernier else
branche (car les registres ne contiendront pas "GenuineIntel"). Depuis initialement eax=0
, cpu_features->max_cpuid
sera également 0 et le if (cpu_features->max_cpuid >= 7)
sera également contourné.
Correctif binaire cpuid(eax=0)
par nop
cela peut être fait avec cet utilitaire (fonctionne à la fois pour x86 et 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)
Une variante Perl équivalente, -0777
garantit que le fichier est lu en une seule fois au lieu de séparer les enregistrements au niveau des sauts de ligne :
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
C'était la partie facile. Maintenant, je ne voulais pas remplacer l'éditeur de liens dynamique à l'échelle du système, mais exécuter un seul programme particulier avec cet éditeur de liens. Bien sûr, cela peut être fait avec ./ld-linux-x86-64-patched.so.2 ./a
, mais les invocations naïves de gdb n'ont pas réussi à définir des points d'arrêt :
$ 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
Une solution de contournement manuelle est décrite dans Comment déboguer un programme avec un interpréteur elf personnalisé ? Cela fonctionne, mais c'est malheureusement une action manuelle utilisant add-symbol-file
. Il devrait cependant être possible de l'automatiser un peu en utilisant GDB Catchpoints.
Une approche alternative qui ne fait pas de lien binaire est LD_PRELOAD
une bibliothèque qui définit des routines personnalisées pour memcpy
, memove
, etc. Cela aura alors priorité sur les routines de la glibc. La liste complète des fonctions est disponible en sysdeps/x86_64/multiarch/ifunc-impl-list.c
. HEAD actuel a plus de symboles par rapport à la version glibc 2.25, au total (grep -Po 'IFUNC_IMPL \(i, name, \K[^,]+' sysdeps/x86_64/multiarch/ifunc-impl-list.c
):
, strpbrk,strspn,strstr,wcschr,wcsrchr,wcscpy,wcslen,wcsnlen,wmemchr,wmemcmp,wmemset,__memcpy_chk,memcpy,__mempcpy_chk,mempcpy,strncmp,__wmemset_chk,
Il semble qu'il existe une bonne solution de contournement pour cela implémentée dans les versions récentes de la glibc :une fonctionnalité "tunables" qui guide la sélection des fonctions de chaîne optimisées. Vous pouvez trouver un aperçu général de cette fonctionnalité ici et le code correspondant à l'intérieur de la glibc dans ifunc-impl-list.c.
Voici comment je l'ai compris. Tout d'abord, j'ai pris l'adresse faisant l'objet de la plainte de gdb :
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
Je l'ai ensuite cherché dans le tableau des bibliothèques partagées :
(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
...
Vous pouvez voir que cette adresse se trouve dans glibc. Mais quelle fonction, précisément ?
(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
Je peux regarder dans ifunc-impl-list.c pour trouver le code qui contrôle la sélection de la version 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))
Il ressemble à AVX2_Usable
est la fonctionnalité à désactiver. Exécutons à nouveau gdb en conséquence :
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable gdb...
Sur cette itération, il s'est plaint de __memmove_avx_unaligned_erms
, qui semblait être activé par AVX_Usable
- mais j'ai trouvé un autre chemin dans ifunc-memmove.h activé par AVX_Fast_Unaligned_Load
. Retour à la planche à dessin :
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
Lors de ce tour final, j'ai découvert un rdtscp
instruction dans la bibliothèque partagée ASAN, j'ai donc recompilé sans le désinfectant d'adresse et enfin, cela a fonctionné.
En résumé :avec un peu de travail, il est possible de désactiver ces instructions à partir de la ligne de commande et d'utiliser la fonction d'enregistrement de gdb sans piratage sévère.
J'ai également rencontré ce problème récemment et j'ai fini par le résoudre en utilisant une erreur CPUID dynamique pour interrompre l'exécution de l'instruction CPUID et remplacer son résultat, ce qui évite de toucher glibc ou l'éditeur de liens dynamique. Cela nécessite la prise en charge du processeur pour les défauts CPUID (Ivy Bridge+) ainsi que la prise en charge du noyau Linux (4.12+) pour l'exposer à l'espace utilisateur via le ARCH_GET_CPUID
et ARCH_SET_CPUID
sous-fonctions de arch_prctl()
. Lorsque cette fonctionnalité est activée, un SIGSEGV
le signal sera délivré à chaque exécution de CPUID, permettant à un gestionnaire de signal d'émuler l'exécution de l'instruction et de remplacer le résultat.
La solution complète est un peu impliquée car je dois également interposer l'éditeur de liens dynamique, car la détection des capacités matérielles y a été déplacée à partir de la glibc 2.26+. J'ai téléchargé la solution complète en ligne sur https://github.com/ddcc/libcpuidoverride .