linux - Deshabilite las funciones optimizadas para AVX en glibc(LD_HWCAP_MASK,/etc/ld.so.nohwcap) para el registro valgrind & gdb
linker (3)
Escuché que hay configuraciones
/etc/ld.so.nohwcap
yLD_HWCAP_MASK
en glibc. ¿Se pueden usar para deshabilitar el envío de ifunc a funciones de cadena optimizadas para AVX en glibc?
Sí: establecer LD_HWCAP_MASK=0
hará que GLIBC pretenda que ninguna de las capacidades de la CPU está disponible. Código .
Si configura la máscara en 0 es probable que desencadene un error, es probable que necesite averiguar el bit preciso que controla AVX y enmascarar solo ese bit.
El x86_64 linux moderno con glibc detectará que la CPU tiene soporte para la extensión AVX y cambiará muchas funciones de cadena de una implementación genérica a una versión optimizada para AVX (con la ayuda de los despachadores ifunc: 1 , 2 ).
Esta característica puede ser buena para el rendimiento, pero evita que varias herramientas como valgrind ( libVEX anteriores, valgrind-3.8 ) y el " target record
" de gdb ( ejecución inversa ) funcionen correctamente (Ubuntu "Z" 17.04 beta, 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
Hay mensaje de error " Process record does not support instruction 0xc5
" desde la implementación de gdb de "registro de destino", porque las instrucciones AVX no son compatibles con el motor de grabación / reproducción (a veces el problema se detecta en la función _dl_runtime_resolve_avx
): https: // sourceware .org / ml / gdb / 2016-08 / msg00028.html "algunas instrucciones AVX no son compatibles con el registro del proceso", 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
Solución propuesta en https://sourceware.org/ml/gdb/2016-08/msg00028.html "Puede recompilar libc (así ld.so), o hackear __init_cpu_features y así __cpu_features en tiempo de ejecución (ver, por ejemplo, strcmp)." o establece LD_BIND_NOW=1
, pero recompila glibc todavía tiene AVX, y ld bind-now no ayuda.
Escuché que hay configuraciones /etc/ld.so.nohwcap
y LD_HWCAP_MASK
en glibc. ¿Se pueden usar para deshabilitar el envío de ifunc a funciones de cadena optimizadas para AVX en glibc?
¿Cómo detecta glibc (rtld?) AVX, usando cpuid
, con /proc/cpuinfo
(probablemente no), o HWCAP aux ( LD_SHOW_AUXV=1 /bin/echo |grep HWCAP
comando AT_HWCAP: bfebfbff
da AT_HWCAP: bfebfbff
)?
No es la mejor solución ni la más completa, solo un kludge de edición de bits más pequeño para permitir el registro valgrind y gdb para mi tarea.
cómo enmascarar AVX / SSE sin recompilar glibc
Hice la reconstrucción completa de glibc no modificado, que es bastante fácil en debian y ubuntu: simplemente sudo apt-get source glibc
, sudo apt-get build-dep glibc
y cd glibc-*/; dpkg-buildpackage -us -uc
cd glibc-*/; dpkg-buildpackage -us -uc
( manual para obtener ld.so sin información de depuración eliminada).
Luego hice el parche binario (bit) del archivo ld.so de salida, en la función utilizada por __get_cpu_features
. La función de destino se compiló desde get_common_indeces
del archivo fuente sysdeps/x86/cpu-features.c
bajo el nombre de get_common_indeces.constprop.1
(es el siguiente después de __get_cpu_features
en el código binario). Tiene varios cpuids, el primero es cpuid eax=1
"Información del procesador y bits de características" ; y luego hay un cheque "jle 0x6" y salta hacia abajo alrededor del código " cpuid eax=7 ecx=0
Extended Features" solo para obtener el estado de AVX2. Está el código que se compiló en esta lógica:
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);
El valor de cpu_features->max_cpuid
se completó en init_cpu_features
del mismo archivo en __cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
línea. Era más fácil desactivar la instrucción if
al reemplazar jle
después de cmp 0x6
con jg
(byte 0x7e a 0x7f). (En realidad, este parche binario se volvió a aplicar manualmente a la función __get_cpu_features
del sistema real ld-linux.so.2
- primero jle antes de mover mov 7 eax; xor ecx,ecx; cpuid
cambiado a jg)
El paquete recompilado y ld.so modificado no se instalaron en el sistema; Utilicé la sintaxis de línea de ld.so ./my_program
de ld.so ./my_program
(o mv ld.so /some/short/path.so
y patchelf --set-interpreter ./my_program
).
Otras posibles soluciones:
- intente utilizar herramientas de registro valgrind & gdb más recientes
- intenta usar glibc antiguo
- implementar la emulación de instrucción faltante en el registro gdb si no está hecho
- Haga el código fuente parcheando
if (cpu_features->max_cpuid >= 7)
en glibc y recompile - hacer el código fuente de parches alrededor de las funciones de cadena habilitadas para avx2 en glibc y recompilar
No parece un método de tiempo de ejecución sencillo para parchear la detección de características. Esta detección ocurre bastante temprano en el enlazador dinámico (ld.so).
El parche binario del enlazador parece ser el método más fácil en este momento. @osgx describió un método donde 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 las 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);
}
La línea __cpuid
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, podríamos también cambiar el cpuid
en una instrucción nop
que daría como resultado la invocación de la última rama else
(ya que los registros no contendrán "GenuineIntel"). Como inicialmente eax=0
, cpu_features->max_cpuid
también será 0 y if (cpu_features->max_cpuid >= 7)
también será anulado.
Binary parche 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)
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 de gdb ingenuas 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
Una solución manual se describe en ¿Cómo depurar el programa con intérprete de duende 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 LD_PRELOAD
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, strcp, 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,