repertorio relaciona microinstrucciones lenguaje instruction instrucciones formato familia ensamblador ejemplos debug cómo con assembler arquitectura c assembly instructions cpu-cache self-modifying

relaciona - ¿Cómo se sincroniza el caché de instrucciones x86?



repertorio de instrucciones de la familia x86 (5)

Acabo de llegar a esta página en una de mis búsquedas y quiero compartir mi conocimiento sobre esta área del kernel de Linux.

Tu código se ejecuta como se esperaba y no hay sorpresas para mí aquí. El protocolo de coherencia de caché de mmap () syscall y procesador hace este truco para usted. Los indicadores "PROT_READ | PROT_WRITE | PROT_EXEC" piden al mmamp () que establezca correctamente el iTLB, dTLB de L1 Cache y TLB de L2 caché de esta página física. Este código de kernel específico de arquitectura de bajo nivel lo hace de forma diferente dependiendo de la arquitectura del procesador (x86, AMD, ARM, SPARC, etc.). ¡Cualquier error en el kernel aquí arruinará tu programa!

Esto es solo por propósito de explicación. Suponga que su sistema no está haciendo mucho y no hay cambios de proceso entre "a [0] = 0b01000000;" e inicio de "printf (" / n "):" ... Además, suponga que tiene 1K de L1 iCache, 1K dCache en su procesador y algo de caché L2 en el núcleo,. (Actualmente, estos son del orden de pocos MB)

  1. mmap () configura su espacio de direcciones virtuales y iTLB1, dTLB1 y TLB2s.
  2. "a [0] = 0b01000000;" en realidad atrapará (magia H / W) en el código del kernel y su dirección física será configurada y todos los TLB del procesador serán cargados por el kernel. Luego, volverá al modo de usuario y su procesador realmente cargará 16 bytes (H / W magic a [0] a a [3]) en L1 dCache y L2 Cache. El procesador entrará de nuevo en la memoria, solo cuando refiera un [4] y así sucesivamente (¡Ignore la carga de predicción por ahora!). Para el momento en que complete "a [7] = 0b11000011;", su procesador hizo 2 LECTURAS de ráfaga de 16 bytes cada una en el bus eterno. Todavía no hay ESCRITOS reales en la memoria física. Todos los ESCRITOS están ocurriendo dentro de L1 dCache (magia H / W, el procesador lo sabe) y la memoria caché L2 para y el bit DIRTY se establece para la línea de caché.
  3. "a [3] ++;" tendrá la instrucción STORE en el código de ensamblaje, pero el procesador almacenará eso solo en L1 dCache & L2 y no irá a la memoria física.
  4. Vayamos a la función llamada "a ()". De nuevo, el procesador hace la extracción de instrucciones de L2 Cache a L1 iCache, y así sucesivamente.
  5. El resultado de este programa de modo de usuario será el mismo en cualquier Linux bajo cualquier procesador, debido a la correcta implementación de syscall mmap () de bajo nivel y el protocolo de coherencia de caché.
  6. Si está escribiendo este código en cualquier entorno de procesador integrado sin la asistencia de sistema operativo de mmap () syscall, encontrará el problema que está esperando. Esto se debe a que no está utilizando el mecanismo H / W (TLB) ni el mecanismo del software (instrucciones de barrera de memoria).

Me gustan los ejemplos, así que escribí un poco de código de auto modificación en c ...

#include <stdio.h> #include <sys/mman.h> // linux int main(void) { unsigned char *c = mmap(NULL, 7, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE| MAP_ANONYMOUS, -1, 0); // get executable memory c[0] = 0b11000111; // mov (x86_64), immediate mode, full-sized (32 bits) c[1] = 0b11000000; // to register rax (000) which holds the return value // according to linux x86_64 calling convention c[6] = 0b11000011; // return for (c[2] = 0; c[2] < 30; c[2]++) { // incr immediate data after every run // rest of immediate data (c[3:6]) are already set to 0 by MAP_ANONYMOUS printf("%d ", ((int (*)(void)) c)()); // cast c to func ptr, call ptr } putchar(''/n''); return 0; }

... que funciona, aparentemente:

>>> gcc -Wall -Wextra -std=c11 -D_GNU_SOURCE -o test test.c; ./test 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

Pero, sinceramente, no esperaba que funcionara en absoluto. Esperaba que la instrucción que contenía c[2] = 0 fuera almacenada en la memoria caché en la primera llamada a c , después de lo cual todas las llamadas consecutivas a c ignorarían los cambios repetidos realizados en c (a menos que invalide explícitamente la caché). Afortunadamente, mi CPU parece ser más inteligente que eso.

Supongo que la CPU compara RAM (suponiendo que c incluso reside en la RAM) con la caché de instrucciones cuando el puntero de la instrucción realiza un salto de gran tamaño (como con la llamada a la memoria mmapped anterior) e invalida la caché cuando no coincide (¿todo?), pero espero obtener información más precisa sobre eso. En particular, me gustaría saber si este comportamiento se puede considerar predecible (salvo las diferencias de hardware y sistema operativo), y se puede confiar en él.

(Probablemente debería consultar el manual de Intel, pero eso tiene miles de páginas y tiendo a perderme en él ...)


Es bastante simple; la escritura en una dirección que está en una de las líneas de caché en el caché de instrucciones lo invalida desde el caché de instrucciones. No hay "sincronización" involucrada.


La CPU maneja la invalidación de caché automáticamente, no tiene que hacer nada manualmente. El software no puede predecir razonablemente qué será o no será en la memoria caché de la CPU en algún punto en el tiempo, por lo que depende del hardware para encargarse de esto. Cuando la CPU vio que usted modificó datos, actualizó sus diversos cachés en consecuencia.


Lo que haces generalmente se conoce como código de auto modificación . Las plataformas de Intel (y probablemente también las de AMD) hacen el trabajo por usted de mantener una coherencia de caché de i / d , como lo señala el manual ( Manual 3A, Programación del sistema )

11.6 CÓDIGO DE AUTO MODIFICACIÓN

Una escritura en una ubicación de memoria en un segmento de código que está actualmente almacenado en la memoria caché en el procesador causa la invalidación de la línea (o líneas) de la memoria caché asociada.

Pero esta afirmación es válida siempre que se use la misma dirección lineal para modificar y recuperar, que no es el caso para los depuradores y los cargadores binarios, ya que no se ejecutan en el mismo espacio de direcciones:

Las aplicaciones que incluyen código de modificación automática utilizan la misma dirección lineal para modificar y recuperar la instrucción. El software de sistemas, como un depurador, que posiblemente podría modificar una instrucción usando una dirección lineal diferente a la utilizada para captar 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 resincronizará automáticamente la caché de instrucciones y la cola de captación previa.

Por ejemplo, la operación de serialización siempre es solicitada por muchas otras arquitecturas como PowerPC, donde debe hacerse explícitamente ( E500 Core Manual ):

3.3.1.2.1 Código de auto modificación

Cuando un procesador modifica cualquier ubicación de memoria que puede contener una instrucción, el software debe garantizar que la caché de instrucciones se haga coherente con la memoria de datos y que las modificaciones se hagan visibles para el mecanismo de captación de instrucciones. Esto se debe hacer incluso si la memoria caché está desactivada o si la página está marcada como inhibida en el almacenamiento en caché.

Es interesante notar que PowerPC requiere el problema de una instrucción de sincronización de contexto incluso cuando las memorias caché están deshabilitadas; Sospecho que impone un enjuague de unidades de procesamiento de datos más profundas, como los almacenamientos intermedios de carga / almacenamiento.

El código que ha propuesto no es confiable en arquitecturas sin fisgoneo o facilidades avanzadas de coherencia de caché , y por lo tanto es probable que falle.

Espero que esto ayude.


Por cierto, muchos procesadores x86 (en los que trabajé) fisgonean no solo el caché de instrucciones sino también el pipeline, la ventana de instrucciones, las instrucciones que están actualmente en marcha. Entonces, el código de auto modificación tendrá efecto en la próxima instrucción. Sin embargo, se recomienda utilizar una instrucción de serialización como CPUID para garantizar que se ejecute el código recién escrito.