¿Las instrucciones x86 requieren su propia codificación y todos sus argumentos para estar presentes en la memoria al mismo tiempo?

64

Estoy tratando de averiguar si es posible ejecutar una máquina virtual Linux cuya RAM solo está respaldada por una sola página física.

Para simular esto, modifiqué el controlador de falla de página anidada en KVM para eliminar el bit presente de todas las entradas de la tabla de página anidada (NPT), excepto la correspondiente a la falla de página procesada actualmente.

Al intentar iniciar un invitado Linux, observé que las instrucciones de ensamblaje que usan operandos de memoria, como

add [rbp+0x820DDA], ebp

conducir a un bucle de falla de página hasta que restaure el bit actual para la página que contiene la instrucción, así como para la página referenciada en el operando (en este ejemplo [rbp+0x820DDA]).

Me pregunto por qué este es el caso. ¿No debería la CPU acceder a las páginas de memoria secuencialmente, es decir, primero leer las instrucciones y luego acceder al operando de la memoria? ¿O requiere x86 que la página de instrucciones y todas las páginas de operandos sean accesibles al mismo tiempo?

Estoy probando en AMD Zen 1.

savvybug
fuente
2
Por qué querrías hacer esto?
SS Anne
11
Solo por interés técnico :)
savvybug
14
Votación a favor de la divertida idea del proyecto.
tubería
10
Esto es una locura en el nivel de "arranque de Linux en un emulador 486 que se ejecuta en JavaScript en el navegador". Me encanta.
Chrylis -on strike-
3
Heh, aparentemente llevé esta pregunta a la misma conclusión lógica que ya estabas pensando, sobre el conjunto de trabajo mínimo para garantizar el progreso hacia adelante. Ya había respondido eso antes de agregar ese nuevo primer párrafo a la pregunta. : PI agregó algunos enlaces y más detalles en algunos puntos (por ejemplo, el caminante de páginas puede almacenar en caché algunas entradas de directorio de páginas de invitados internamente) ya que esta pregunta está recibiendo mucha más atención de la que esperaba gracias a que de alguna manera llegó a HNQ.
Peter Cordes

Respuestas:

56

Sí, requieren el código de máquina y todos los operandos de memoria.

¿No debería la CPU acceder a las páginas de memoria secuencialmente, es decir, primero leer las instrucciones y luego acceder al operando de la memoria?

Sí, eso es lógicamente lo que sucede, pero una excepción de falla de página interrumpe ese proceso de 2 pasos y descarta cualquier progreso. La CPU no tiene forma de recordar qué instrucción estaba en el medio de cuando ocurrió un fallo de página.

Cuando un manejador de fallas de página regresa después de manejar una falla de página válida, RIP = la dirección de la instrucción de falla, por lo que la CPU vuelve a intentar ejecutarla desde cero .

Sería legal para el sistema operativo modificar el código de máquina de la instrucción de falla y esperar que ejecute una instrucción diferente después iretdel manejador de fallas de página (o cualquier otra excepción o manejador de interrupciones). Entonces, AFAIK es arquitectónicamente necesario que la CPU rehaga la búsqueda de código de CS: RIP en el caso de que esté hablando. (Suponiendo que incluso regrese al CS con errores: RIP en lugar de programar otro proceso mientras espera el disco en una falla de página dura, o entregar un SIGSEGV a un controlador de señal en una falla de página no válida).

Probablemente también sea arquitectónicamente necesario para la entrada / salida del hipervisor. E incluso si no está explícitamente prohibido en papel, no es cómo funcionan las CPU.

@torek comenta que algunos microprocesadores (CISC) decodifican parcialmente las instrucciones y vuelcan el estado del microregistro en una falla de página , pero x86 no es así.


Algunas instrucciones son interrumpibles y pueden hacer progresos parciales, como rep movs(memcpy in a can) y otras instrucciones de cadena, o reunir cargas / almacenes de dispersión. Pero el único mecanismo es actualizar los registros arquitectónicos como RCX / RSI / RDI para operaciones de cadena, o los registros de destino y máscara para los recopiladores (por ejemplo, manual para AVX2vpgatherdd ). No mantener el código de operación / decodificación da como resultado un registro interno oculto y reiniciarlo después de iret desde un controlador de fallas de página. Estas son instrucciones que hacen múltiples accesos de datos separados.

También tenga en cuenta que x86 (como la mayoría de los ISA) garantiza que las instrucciones son atómicas wrt. interrupciones / excepciones: suceden completamente o no suceden antes de una interrupción. Interrumpir una instrucción de ensamblaje mientras está en funcionamiento . Entonces, por ejemplo add [mem], reg, sería necesario descartar la carga si la parte de la tienda fallara, incluso sin un lockprefijo.


El peor número de páginas de espacio de usuario invitado presentes para avanzar puede ser 6 (más subárboles de tabla de páginas de kernel invitado separados para cada uno):

  • movsqo movswinstrucción de 2 bytes que abarca un límite de página, por lo que se necesitan ambas páginas para que se decodifique.
  • operando fuente qword [rsi]también una división de página
  • qword operando de destino [rdi]también una división de página

Si alguna de estas 6 páginas falla, volvemos al punto de partida.

rep movsdtambién es una instrucción de 2 bytes, y avanzar en un paso tendría el mismo requisito. Casos similares como push [mem]o pop [mem]podrían construirse con una pila desalineada.

Una de las razones (o beneficios secundarios) para / de hacer que las cargas de recolección / almacenamiento de dispersiones sean "interrumpibles" (actualizar el vector de máscara con su progreso) es evitar aumentar esta huella mínima para ejecutar una sola instrucción. También para mejorar la eficiencia del manejo de múltiples fallas durante una reunión o dispersión.


@Brandon señala en los comentarios que un invitado necesitará sus tablas de páginas en la memoria , y las divisiones de página del espacio de usuario también pueden ser divisiones de 1GiB, por lo que los dos lados están en subárboles diferentes del nivel superior PML4. El recorrido de la página de HW deberá tocar todas estas páginas de la tabla de páginas de invitados para avanzar. Una situación tan patológica es poco probable que ocurra por casualidad.

Los TLB (y los elementos internos del caminante de páginas) pueden almacenar en caché algunos de los datos de la tabla de páginas, y no están obligados a reiniciar el recorrido de la página desde cero a menos que el sistema operativo lo haya hecho invlpgo haya establecido un nuevo directorio de página de nivel superior CR3. Ninguno de estos es necesario cuando se cambia una página de no presente a presente; x86 en papel garantiza que no es necesario (por lo que no se permite el "almacenamiento en caché negativo" de PTE no presentes, al menos no visible para el software). Por lo tanto, es posible que la CPU no VMexit incluso si algunas de las páginas de tabla de páginas físicas de invitado no están realmente presentes.

Los contadores de rendimiento de PMU se pueden habilitar y configurar de manera que la instrucción también requiera un evento de rendimiento para escribir en un búfer PEBS para esa instrucción. Con una máscara de contador configurada para contar solo las instrucciones de espacio de usuario, no el núcleo, podría ser que siga intentando desbordar el contador y almacenar una muestra en el búfer cada vez que regrese al espacio de usuario, produciendo un error de página.

Peter Cordes
fuente
15
El peor caso para una sola instrucción podría ser algo como " push dword [foo" (o incluso simplemente call [foo]) con todo desalineado a través del "límite de la tabla de puntero del directorio de páginas" (agregando hasta 6 páginas, 6 tablas de páginas, 6 directorios de páginas, 6 PDPT y un PML4); con la función de "muestreo basado en eventos precisos con el búfer PEBS" de la CPU habilitada y configurada para que pushse agreguen datos de monitoreo de rendimiento al búfer PEBS. Para un conservador "páginas mínimas proporcionadas por el host para que los invitados puedan progresar en casos patológicos" me gustaría al menos 16 páginas.
Brendan
44
Tenga en cuenta que este tipo de cosas siempre ha sido común en las arquitecturas CISC-y. Algunos microprocesadores decodifican parcialmente las instrucciones y vuelcan el estado del microregistro en una falla de página, pero otros no requieren y / o requieren que los operandos de dirección para instrucciones "loop-y" (DBRA en m68k, MOVC3 / MOVC5 en Vax, etc.) estén en registros similares a su ejemplo REP MOVS.
torek
1
@Brendan: alguien contó el peor de los casos en una instrucción VAX como unas 50 páginas. Olvidé los detalles, pero obviamente pondrías la instrucción en el límite de una página, usarías algo como la búsqueda de tabla de traducción con la tabla que abarca un límite de página, usarías (rX) [rY] con los indirectos en los límites de página, y pronto. Las instrucciones más complicadas tomaron hasta 6 operandos (cargándolos en r0-r5) y los seis podrían ser dobles indirectos, creo.
torek
3
El sistema operativo podría cambiar la instrucción, pero también puede cambiar EIP. Entonces, hay una pregunta lógica de seguimiento. ¿Cuál es el número mínimo de páginas necesarias, suponiendo un esquema de parche de instrucción inteligente? Por ejemplo, copie el valor no alineado a un búfer de memoria virtual alineado, emule la instrucción e IRET a la siguiente instrucción.
MSalters
1
La página que contiene las iretinstrucciones del sistema operativo también debe estar en la memoria. Esta es una instrucción de un byte, por lo tanto, una página adicional. La dirección de interrupción del controlador de fallas de página también debe estar en la memoria, pero puede ser la misma página que la anterior.
Stig Hemmer