No parece haber un método de tiempo de ejecución sencillo para parchear la detección de funciones. Esta detección ocurre bastante temprano en el enlazador dinámico (ld.so).
El parche binario del enlazador parece el método más fácil en este momento. @osgx describió un método en el que se sobrescribe un salto. Otro enfoque es simplemente falsificar el resultado de cpuid. Normalmente cpuid(eax=0)
devuelve la función admitida más alta en eax
mientras que los ID del fabricante se devuelven en los registros ebx, ecx y edx. Tenemos este fragmento en 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);
}
El __cpuid
línea se traduce a estas instrucciones en /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
Entonces, en lugar de parchear las ramas, también podríamos cambiar el cpuid
en un nop
instrucción que resultaría en la invocación del último else
rama (ya que los registros no contendrán "GenuineIntel"). Desde inicialmente eax=0
, cpu_features->max_cpuid
también será 0 y el if (cpu_features->max_cpuid >= 7)
también se omitirá.
Parcheo binario cpuid(eax=0)
por nop
esto se puede hacer con esta utilidad (funciona tanto para x86 como para 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 de Perl equivalente, -0777
asegura que el archivo se lea de una vez en lugar de separar los registros en saltos de línea:
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
Esa fue la parte fácil. Ahora, no quería reemplazar el enlazador dinámico de todo el sistema, sino ejecutar solo un programa en particular con este enlazador. Claro, eso se puede hacer con ./ld-linux-x86-64-patched.so.2 ./a
, pero las invocaciones ingenuas de gdb no pudieron establecer puntos de interrupción:
$ 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
Se describe una solución manual en ¿Cómo depurar un programa con un intérprete elf personalizado? Funciona, pero desafortunadamente es una acción manual usando add-symbol-file
. Sin embargo, debería ser posible automatizarlo un poco usando GDB Catchpoints.
Un enfoque alternativo que no vincula enlaces binarios es LD_PRELOAD
ing una biblioteca que define rutinas personalizadas para memcpy
, memove
, etc. Esto tendrá prioridad sobre las rutinas glibc. La lista completa de funciones está disponible en sysdeps/x86_64/multiarch/ifunc-impl-list.c
. HEAD actual tiene más símbolos en comparación con la versión glibc 2.25, en total (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,
Parece que hay una buena solución para esto implementada en versiones recientes de glibc:una función de "ajustables" que guía la selección de funciones de cadena optimizadas. Puede encontrar una descripción general de esta característica aquí y el código relevante dentro de glibc en ifunc-impl-list.c.
Así es como lo descubrí. Primero, tomé la dirección de la que se queja gdb:
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
Luego lo busqué en la tabla de bibliotecas compartidas:
(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
...
Puede ver que esta dirección está dentro de glibc. Pero, ¿qué función, específicamente?
(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
Puedo buscar en ifunc-impl-list.c para encontrar el código que controla la selección de la versión 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))
Parece AVX2_Usable
es la característica a deshabilitar. Volvamos a ejecutar gdb en consecuencia:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable gdb...
En esta iteración, se quejó de __memmove_avx_unaligned_erms
, que parecía estar habilitado por AVX_Usable
- pero encontré otra ruta en ifunc-memmove.h habilitada por AVX_Fast_Unaligned_Load
. De vuelta a la mesa de dibujo:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
En esta ronda final descubrí un rdtscp
instrucciones en la biblioteca compartida de ASAN, así que volví a compilar sin el desinfectante de direcciones y, por fin, funcionó.
En resumen:con un poco de trabajo es posible deshabilitar estas instrucciones desde la línea de comandos y usar la función de registro de gdb sin necesidad de hacks severos.
También encontré este problema recientemente y terminé resolviéndolo usando fallas dinámicas de CPUID para interrumpir la ejecución de la instrucción CPUID y anular su resultado, lo que evita tocar glibc o el enlazador dinámico. Esto requiere soporte de procesador para fallas de CPUID (Ivy Bridge+), así como soporte de kernel de Linux (4.12+) para exponerlo al espacio de usuario a través de ARCH_GET_CPUID
y ARCH_SET_CPUID
subfunciones de arch_prctl()
. Cuando esta característica está habilitada, un SIGSEGV
la señal se entregará en cada ejecución de CPUID, lo que permite que un controlador de señal pueda emular la ejecución de la instrucción y anular el resultado.
La solución completa es un poco complicada ya que también necesito interponer el enlazador dinámico, porque la detección de la capacidad del hardware se movió allí a partir de glibc 2.26+. Cargué la solución completa en línea en https://github.com/ddcc/libcpuidoverride.