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.
Respuestas:
Sí, requieren el código de máquina y todos los operandos de 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
iret
del 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 unlock
prefijo.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):
movsq
omovsw
instrucción de 2 bytes que abarca un límite de página, por lo que se necesitan ambas páginas para que se decodifique.[rsi]
también una división de página[rdi]
también una división de páginaSi alguna de estas 6 páginas falla, volvemos al punto de partida.
rep movsd
también es una instrucción de 2 bytes, y avanzar en un paso tendría el mismo requisito. Casos similares comopush [mem]
opop [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
invlpg
o 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.
fuente
push dword [foo
" (o incluso simplementecall [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 quepush
se 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.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.iret
instrucciones 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.