Observación de instrucciones obsoletas en x86 con código de auto-modificación
caching self-modifying (2)
Me han dicho y leído en los manuales de Intel que es posible escribir instrucciones en la memoria, pero la cola de búsqueda previa de instrucciones ya ha recuperado las instrucciones obsoletas y ejecutará esas instrucciones anteriores. No he podido observar este comportamiento. Mi metodología es la siguiente.
El manual de desarrollo de software de Intel establece en la sección 11.6 que
Una escritura en una ubicación de memoria en un segmento de código que actualmente se almacena en caché en el procesador hace que la línea (o líneas) de la caché asociada se invalide. Esta verificación se basa en la dirección física de la instrucción. Además, la familia P6 y los procesadores Pentium comprueban si una escritura en un segmento de código puede modificar una instrucción que se haya ejecutado previamente. Si la escritura afecta a una instrucción precargada, la cola de captación previa se invalida. Esta última verificación se basa en la dirección lineal de la instrucción.
Entonces, parece que si espero ejecutar instrucciones obsoletas, necesito tener dos direcciones lineales diferentes para referirse a la misma página física. Entonces, asigno un archivo a dos direcciones diferentes.
int fd = open("code_area", O_RDWR | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO);
assert(fd>=0);
write(fd, zeros, 0x1000);
uint8_t *a1 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_FILE | MAP_SHARED, fd, 0);
uint8_t *a2 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_FILE | MAP_SHARED, fd, 0);
assert(a1 != a2);
Tengo una función de ensamblaje que toma un solo argumento, un puntero a la instrucción que quiero cambiar.
fun:
push %rbp
mov %rsp, %rbp
xorq %rax, %rax # Return value 0
# A far jump simulated with a far return
# Push the current code segment %cs, then the address we want to far jump to
xorq %rsi, %rsi
mov %cs, %rsi
pushq %rsi
leaq copy(%rip), %r15
pushq %r15
lretq
copy:
# Overwrite the two nops below with `inc %eax''. We will notice the change if the
# return value is 1, not zero. The passed in pointer at %rdi points to the same physical
# memory location of fun_ins, but the linear addresses will be different.
movw $0xc0ff, (%rdi)
fun_ins:
nop # Two NOPs gives enough space for the inc %eax (opcode FF C0)
nop
pop %rbp
ret
fun_end:
nop
En C, copio el código al archivo asignado en memoria. Invoco la función desde la dirección lineal a1
, pero paso un puntero a a2
como objetivo de la modificación del código.
#define DIFF(a, b) ((long)(b) - (long)(a))
long sz = DIFF(fun, fun_end);
memcpy(a1, fun, sz);
void *tochange = DIFF(fun, fun_ins);
int val = ((int (*)(void*))a1)(tochange);
Si la CPU recogió el código modificado, val == 1. De lo contrario, si se ejecutaron las instrucciones obsoletas (dos nops), val == 0.
He ejecutado esto en un Intel Core i5 de 1.7GHz (2011 macbook air) y una CPU Intel® Xeon® X3460 a 2.80GHz. Sin embargo, cada vez que veo val == 1 indica que la CPU siempre se da cuenta de la nueva instrucción.
¿Alguien ha experimentado con el comportamiento que quiero observar? ¿Mi razonamiento es correcto? Estoy un poco confundido sobre el manual que menciona los procesadores P6 y Pentium, y por la falta de mencionar mi procesador Core i5. ¿Quizás algo más está sucediendo que hace que la CPU descargue su cola de búsqueda previa de instrucciones? ¡Cualquier idea sería muy útil!
Me han dicho y leído en los manuales de Intel que es posible escribir instrucciones en la memoria, pero la cola de búsqueda previa de instrucciones ya ha [puede haber] obtenido las instrucciones obsoletas y [puede] ejecutar esas instrucciones anteriores. No he podido observar este comportamiento.
Sí, lo serías.
Todos o casi todos los procesadores Intel modernos son más estrictos que el manual:
Buscan la tubería en función de la dirección física, no solo lineal.
Las implementaciones del procesador pueden ser más estrictas que los manuales.
Pueden elegir ser así porque han encontrado un código que no se adhiere a las reglas de los manuales, que no quieren romper.
O ... porque la forma más fácil de cumplir con la especificación arquitectónica (que en el caso de SMC solía ser oficialmente "hasta la próxima instrucción de serialización" pero en la práctica, para el código heredado, era "hasta la siguiente rama tomada que es más que ??? bytes de distancia ") podría ser más estricto.
Creo que deberías consultar el contador de rendimiento MACHINE_CLEARS.SMC
(parte del evento MACHINE_CLEARS
) de la CPU (está disponible en Sandy Bridge 1 , que se usa en tu Powerbook de Air; y también está disponible en tu Xeon, que es Nehalem 2 - busca "smc"). Puedes usar oprofile
, perf
o Intel Vtune
para encontrar su valor:
Máquina limpia
Descripción métrica
Ciertos eventos requieren que toda la canalización se borre y reinicie justo después de la última instrucción retirada. Esta métrica mide tres de estos eventos: violaciones de orden de memoria, código de auto-modificación y ciertas cargas a rangos de direcciones ilegales.
Posibles problemas
Una parte significativa del tiempo de ejecución se dedica a manejar los borrados de la máquina. Examine los eventos MACHINE_CLEARS para determinar la causa específica.
MACHINE_CLEARS Código de evento: 0xC3 Máscara SMC: 0x04
Código de auto-modificación (SMC) detectado.
Número de borrados automáticos de código de auto-modificación detectados.
Intel también dice acerca de smc http://software.intel.com/en-us/forums/topic/345561 (vinculado desde la taxonomía de Intel Performance Bottleneck Analyzer
Este evento se dispara cuando se detecta un código de auto-modificación. Esto puede ser usado normalmente por personas que realizan ediciones binarias para forzarlo a tomar una ruta determinada (por ejemplo, hackers). Este evento cuenta la cantidad de veces que un programa escribe en una sección de código. El código de auto-modificación causa una severa penalización en todos los procesadores Intel 64 e IA-32. La línea de caché modificada se vuelve a escribir en los cachés L2 y LLC. Además, las instrucciones deberían volver a cargarse, lo que causaría una penalización en el rendimiento.
Creo que verás algunos de esos eventos. Si lo son, entonces la CPU pudo detectar un acto de auto-modificación del código y levantó el "Limpiar Máquina" - reinicio completo de la tubería. Las primeras etapas son Fetch y pedirán al caché L2 el nuevo código de operación. Estoy muy interesado en el conteo exacto de eventos SMC por ejecución de su código; esto nos dará una estimación de las latencias. (El SMC se cuenta en algunas unidades donde se supone que 1 unidad es de 1.5 ciclos de cpu - B.6.2. 6 del manual de optimización de Intel)
Podemos ver que Intel dice "reiniciado justo después de la última instrucción retirada", por lo que creo que la última instrucción retirada será mov
; y sus nops ya están en la tubería. Pero SMC se criará al momento de la jubilación de mov y matará todo en tramitación, incluidos los nops.
Este reinicio de la tubería inducida por SMC no es barato, Agner tiene algunas medidas en Optimizing_assembly.pdf - "17.10 Código de auto-modificación (todos los procesadores)" (Creo que cualquier Core2 / CoreiX es como PM aquí):
La penalización por ejecutar un código inmediatamente después de modificarlo es de aproximadamente 19 relojes para P1, 31 para PMMX y 150-300 para PPro, P2, P3, PM. El P4 purgará todo el caché de seguimiento después de la modificación automática del código. Los procesadores 80486 y anteriores requieren un salto entre la modificación y el código modificado para vaciar la memoria caché del código. ...
El código de auto-modificación no se considera una buena práctica de programación. Se debe usar solo si la ganancia en velocidad es sustancial y el código modificado se ejecuta tantas veces que la ventaja supera las penalizaciones por el uso de código de auto-modificación.
Se recomendó el uso de diferentes direcciones lineales para fallar. El detector SMC se recomendó aquí: https://.com/a/10994728/196561 : intentaré encontrar la documentación real de Intel ... No puedo responder a su pregunta real ahora.
Puede haber algunos consejos aquí: Manual de optimización, 248966-026, abril de 2012, "3.6.9 Código de mezcla y datos":
Colocar datos de escritura en el segmento de código puede ser imposible de distinguir del código de auto-modificación. Los datos grabables en el segmento de código pueden sufrir la misma penalización de rendimiento que el código de auto-modificación.
y la siguiente sección
El software debe evitar escribir en una página de códigos en la misma subpágina de 1 KByte que se está ejecutando o recuperar código en la misma subpágina de 2 KBytes de la que se está escribiendo. Además, compartir una página que contenga código ejecutado de forma directa o especulativa con otro procesador como página de datos puede desencadenar una condición SMC que hace que toda la línea de la máquina y la caché de rastreo se borren. Esto se debe a la condición del código de auto-modificación.
Por lo tanto, es posible que haya algunos esquemas que controlan las intersecciones de las subpáginas grabables y ejecutables.
Puede intentar modificar el otro subproceso (código de modificación cruzada), pero se necesita una cuidadosa sincronización de subprocesos y un lavado de tuberías (es posible que desee incluir algunos retrasos de forzados brutos en el subproceso del escritor; CPUID justo después de la sincronización es deseado). Pero debe saber que ELLOS ya solucionaron esto usando " nukes " - verifique la patente US6857064 .
Estoy un poco confundido acerca del manual que menciona los procesadores P6 y Pentium
Esto es posible si ha recuperado, decodificado y ejecutado alguna versión obsoleta del manual de instrucciones de Intel. Puede restablecer la tubería y verificar esta versión: download.intel.com/products/processor/manual/325462.pdf "11.6 CÓDIGO DE MODIFICACIÓN AUTOMÁTICA". Esta versión aún no dice nada acerca de las CPU más nuevas, pero menciona que cuando se modifica el uso de diferentes direcciones virtuales, el comportamiento puede no ser compatible entre las microarquitecturas (puede funcionar en su Nehalem / Sandy Bridge y puede no funcionar en ... Skymont)
11.6 CÓDIGO DE MODIFICACIÓN AUTOMÁTICA Una escritura en una ubicación de memoria en un segmento de código que actualmente está almacenado en caché en el procesador hace que la línea (o líneas) de la caché asociada se invalide. Esta verificación se basa en la dirección física de la instrucción. Además, la familia P6 y los procesadores Pentium comprueban si una escritura en un segmento de código puede modificar una instrucción que se haya ejecutado previamente. Si la escritura afecta a una instrucción precargada, la cola de captación previa se invalida. Esta última verificación se basa en la dirección lineal de la instrucción. Para los procesadores Pentium 4 e Intel Xeon, una escritura o una indagación de una instrucción en un segmento de código, donde la instrucción de destino ya está descodificada y reside en el caché de seguimiento, invalida todo el caché de seguimiento. El último comportamiento significa que los programas que se auto modifican pueden causar una severa degradación del rendimiento cuando se ejecutan en los procesadores Pentium 4 e Intel Xeon.
En la práctica, la verificación de direcciones lineales no debería crear problemas de compatibilidad entre los procesadores IA-32. Las aplicaciones que incluyen código de auto-modificación utilizan la misma dirección lineal para modificar y obtener la instrucción.
El software de sistemas, como un depurador, que posiblemente modifique una instrucción utilizando una dirección lineal diferente a la utilizada para obtener la instrucción, ejecutará una operación de serialización, como una instrucción CPUID, antes de que se ejecute la instrucción modificada, que se volverá a sincronizar automáticamente. la caché de instrucciones y la cola de captación previa. (Consulte la Sección 8.1.3, “Manejo del código de modificación automática y cruzada”, para obtener más información sobre el uso del código de modificación automática).
Para los procesadores Intel486, una escritura a una instrucción en la memoria caché la modificará tanto en la memoria caché como en la memoria, pero si la instrucción fue preestablecida antes de la escritura, la versión anterior de la instrucción podría ser la ejecutada. Para evitar que se ejecute la instrucción anterior, vacíe la unidad de búsqueda previa de instrucciones codificando una instrucción de salto inmediatamente después de cualquier escritura que modifique una instrucción
REAL Update , buscó en Google para "Detección de SMC" (con comillas) y hay algunos detalles de cómo el moderno Core2 / Core iX detecta SMC y también muchas listas de erratas con Xeons y Pentiums colgando en el detector de SMC:
http://www.google.com/patents/US6237088 Sistema y método para rastrear instrucciones en vuelo en una tubería @ 2001
DOI 10.1535 / itj.1203.03 (buscándolo en google, hay una versión gratuita en citeseerx.ist.psu.edu): se agregó el "FILTRO DE INCLUSIÓN" en Penryn para reducir el número de detecciones de SMC falsas; El "mecanismo de detección de inclusión existente" se muestra en la Fig. 9
http://www.google.com/patents/US6405307 - patente anterior en la lógica de detección de SMC
De acuerdo con la patente US6237088 (FIG. 5, resumen) hay un "búfer de dirección de línea" (con muchas direcciones lineales una dirección por instrucción buscada, o en otras palabras, el búfer lleno de IPs recuperadas con precisión de línea caché). Cada tienda, o una fase más exacta de "dirección de la tienda" de cada tienda se enviará al comparador paralelo para verificar, almacenará intersecciones con cualquiera de las instrucciones que se estén ejecutando actualmente o no.
Ambas patentes no dicen claramente si utilizarán una dirección física o lógica en la lógica SMC ... L1i en Sandy Bridge es VIPT ( virtualmente indexada, físicamente etiquetada , dirección virtual para el índice y dirección física en la etiqueta) según http://nick-black.com/dankwiki/index.php/Sandy_Bridge lo que tenemos la dirección física en el momento en que el caché L1 devuelve datos. Creo que Intel puede usar direcciones físicas en la lógica de detección de SMC.
Aún más, http://www.google.com/patents/US6594734 @ 1999 (publicado en 2003, recuerde que el ciclo de diseño de la CPU es de aproximadamente 3-5 años) dice en la sección "Resumen" que SMC ahora está en TLB y usa Direcciones físicas (o en otras palabras, por favor, no intente engañar al detector SMC):
El código de auto-modificación se detecta usando un búfer de traducción al costado .. [que] tiene direcciones de página físicas almacenadas en él sobre las cuales se pueden realizar snoops usando la dirección de memoria física de un almacén en la memoria. ... Para proporcionar una granularidad más fina que una página de direcciones, los bits FINE HIT se incluyen con cada entrada en el caché que asocia información en el caché a partes de una página dentro de la memoria.
(Parte de la página, conocida como cuadrantes en la patente US6594734, suena como subpáginas 1K, ¿no?)
Entonces ellos dicen
Por lo tanto , los snoops, activados por las instrucciones de almacenamiento en la memoria , pueden realizar la detección de SMC comparando la dirección física de todas las instrucciones almacenadas en el caché de instrucciones con la dirección de todas las instrucciones almacenadas en la página o páginas de memoria asociadas. Si hay una coincidencia de dirección, indica que se modificó una ubicación de memoria. En el caso de una coincidencia de dirección, que indica una condición de SMC, la unidad de retiro vacía la memoria caché de instrucciones y el canal de instrucciones y se recuperan nuevas instrucciones de la memoria para su almacenamiento en la memoria caché de instrucciones.
Debido a que los snoops para la detección de SMC son físicos y el ITLB generalmente acepta como entrada una dirección lineal para traducir en una dirección física, el ITLB se forma adicionalmente como una memoria de contenido direccionable en las direcciones físicas e incluye un puerto de comparación de entrada adicional (referido como un puerto snoop o puerto de traducción inversa)
- Entonces, para detectar SMC, obligan a las tiendas a reenviar la dirección física al búfer de instrucciones a través de snoop (snoops similares se enviarán desde otros cores / cpus o desde escrituras DMA a nuestros caches ...), si el fisgón de snoop. los conflictos de direcciones con las líneas de caché, almacenados en el búfer de instrucciones, reiniciaremos la canalización a través de la señal SMC entregada desde iTLB a la unidad de retiro. Puedo imaginar cuánto se desperdiciarán los relojes de la CPU en dicho bucle snoop desde dTLB a través de iTLB y hacia la unidad de retiro (no se puede retirar la próxima instrucción "nop", aunque se ejecutó antes que mov y no tiene efectos secundarios). Pero ¿qué? ITLB tiene una entrada de dirección física y una segunda CAM (grande y caliente) solo para apoyar y defenderse contra el código de auto-modificación loco y engañoso.
PD: ¿Y si trabajaremos con páginas grandes (4M o 1G)? El L1TLB tiene entradas de página enormes, y puede haber una gran cantidad de falsos SMC detectados para 1/4 de 4 MB de página ...
PPS: hay una variante, que el manejo erróneo de SMC con diferentes direcciones lineales estuvo presente solo al principio de P6 / Ppro / P2 ...