Intel x86 Assembly Language & Microarchitecture
Modos Real vs Protegido
Buscar..
Modo real
Cuando Intel diseñó el x86 original, el 8086 (y el derivado de 8088), incluyeron la segmentación para permitir que el procesador de 16 bits acceda a más de 16 bits de direcciones. Hicieron esto haciendo que las direcciones de 16 bits fueran relativas a un registro de segmento de 16 bits dado, de los cuales definieron cuatro: segmento de código ( CS ), segmento de datos ( DS ), segmento extra ( ES ) y segmento de pila ( SS ) .
La mayoría de las instrucciones implicaban qué Registro de segmento utilizar: las instrucciones se incluyeron en el Segmento de código, PUSH y POP implicaron el Segmento de pila, y las referencias de datos simples implicaban el Segmento de datos, aunque esto podría anularse para acceder a la memoria en cualquiera de los otros Segmentos.
La implementación fue sencilla: para cada acceso a la memoria, la CPU tomaría el Registro de segmentos implícito (o explícito), lo desplazaría cuatro lugares a la izquierda y luego agregaría la dirección indicada:
+-------------------+---------+
Segment | 16-bit value | 0 0 0 0 |
+-------------------+---------+
PLUS
+---------+-------------------+
Address | 0 0 0 0 | 16-bit value |
+---------+-------------------+
EQUALS
+-----------------------------+
Result | 20-bit memory address |
+-----------------------------+
Esto permitió varias técnicas:
- Permitir que el Código, los Datos y la Pila sean de acceso mutuo (
CS,DSySStenían el mismo valor); - Mantener el Código, los Datos y la Pila completamente separados unos de otros (
CS,DSySStodos los 4K (o más) separados entre sí, recuerde que se multiplica por 16, por lo que es 64K).
¡También permitió superposiciones extrañas y todo tipo de cosas extrañas!
Cuando se inventó el 80286, admitió este modo heredado (ahora llamado "Modo Real"), pero agregó un nuevo modo llamado "Modo Protegido" (qv).
Lo importante a notar es que en Modo Real:
- Se pudo acceder a cualquier dirección de memoria, simplemente colocando el valor correcto dentro de un registro de segmento y accediendo a la dirección de 16 bits;
- El alcance de la "protección" era permitir al programador separar diferentes áreas de la memoria para diferentes propósitos y hacer que sea más difícil escribir accidentalmente en los datos incorrectos, al mismo tiempo que es posible hacerlo.
En otras palabras ... ¡no muy protegido en absoluto!
Modo protegido
Introducción
Cuando se inventó el 80286, admitió la Segmentación 8086 heredada (ahora llamada "Modo Real"), y agregó un nuevo modo llamado "Modo Protegido". Este modo ha estado en todos los procesadores x86 desde entonces, aunque mejorado con varias mejoras, como el direccionamiento de 32 y 64 bits.
Diseño
En el modo protegido, el simple "Agregar dirección al registro de segmento cambiado" se eliminó por completo. Mantuvieron los registros de segmentos, pero en lugar de usarlos para calcular una dirección, los usaron para indexar en una tabla (en realidad, una de dos ...) que definía el segmento al que se debía acceder. Esta definición no solo describe dónde estaba en la memoria el Segmento (usando Base y Límite), sino también qué tipo de Segmento era (Código, Datos, Pila o incluso Sistema) y qué tipos de programas podrían acceder a él (Kernel del SO, programa normal). , Controlador del dispositivo, etc.).
Registro de segmento
Cada registro de segmento de 16 bits tomó la siguiente forma:
+------------+-----+------+
| Desc Index | G/L | Priv |
+------------+-----+------+
Desc Index = 13-bit index into a Descriptor Table (described below)
G/L = 1-bit flag for which Descriptor Table to Index: Global or Local
Priv = 2-bit field defining the Privilege level for access
Global / Local
El bit Global / Local definió si el acceso se encontraba en una Tabla Global de descriptores (llamada, sin duda, la Tabla de Descriptor Global o GDT), o la Tabla de Descriptor Local (LDT). La idea para el LDT era que cada programa podría tener su propia tabla de descriptores: el sistema operativo definiría un conjunto global de segmentos, y cada programa tendría su propio conjunto de códigos locales, datos y segmentos de pila. El sistema operativo gestionaría la memoria entre las diferentes tablas de descriptores.
Tabla Descriptora
Cada tabla de descriptores (global o local) era una matriz de 64K de 8.192 descriptores: cada uno de ellos era un registro de 8 bytes que definía múltiples aspectos del segmento que estaba describiendo. Los campos del Índice de Descriptor de los Registros de Segmentos permitieron 8,192 descriptores: ¡no es coincidencia!
Descriptor
Un Descriptor contenía la siguiente información: tenga en cuenta que el formato del Descriptor cambió cuando se lanzaron nuevos procesadores, pero se mantuvo el mismo tipo de información en cada uno:
- Base
Esto definió la dirección de inicio del segmento de memoria. - Límite
Esto definió el tamaño del segmento de memoria - más o menos. Tuvieron que tomar una decisión: ¿un tamaño de0x0000significaría un tamaño de0, por lo que no sería accesible? ¿O tamaño máximo?
En su lugar, eligieron una tercera opción: el campo Límite fue la última ubicación accesible dentro del Segmento. Eso significaba que se podía definir un segmento de un bye; o un tamaño máximo para el tamaño de la dirección. - Tipo
Hubo varios tipos de Segmentos: el Código, los Datos y la Pila tradicionales (ver más abajo), pero también se definieron otros Segmentos del Sistema:- Los segmentos de la tabla de descriptores locales definieron cuántos descriptores locales se podían acceder;
- Los Segmentos de Estado de Tarea podrían usarse para el cambio de contexto administrado por hardware;
- "Puertas de llamada" controladas que podrían permitir que los programas llamen al sistema operativo, pero solo a través de puntos de entrada cuidadosamente administrados.
- Atributos
También se mantuvieron ciertos atributos del Segmento, donde fue relevante:- Sólo lectura frente a lectura-escritura;
- Si el segmento estaba actualmente presente o no, lo que permite la administración de memoria a pedido;
- Qué nivel de código (SO vs Driver vs programa) podría acceder a este segmento.
¡Verdadera protección al fin!
Si el sistema operativo conservaba las Tablas de Descriptor en Segmentos a las que no podían acceder los simples programas, entonces podría administrar con precisión qué Segmentos fueron definidos, y qué memoria fue asignada y accesible para cada uno. Un programa podría fabricar cualquier valor de Registro de segmento que le gustara, pero si tuviera la audacia de cargarlo realmente en un Registro de Segmento ... el hardware de la CPU reconocería que el valor del Descriptor propuesto rompió cualquiera de una gran cantidad de reglas, y en lugar de completar la solicitud, generaría una excepción en el sistema operativo para permitirle manejar el programa errante.
Esta excepción fue generalmente la n.º 13, la excepción de protección general, que se hizo famosa en todo el mundo por Microsoft Windows ... (¿Alguien cree que un ingeniero de Intel era supersticioso?)
Los errores
Los tipos de errores que podrían ocurrir incluyen:
Si el Índice de Descriptor propuesto era más grande que el tamaño de la tabla;
Si el Descriptor propuesto era un Descriptor de Sistema en lugar de Código, Datos o Pila;
Si el Descriptor propuesto era más privilegiado que el programa solicitante;
Si el Descriptor propuesto se marcó como No legible (como un segmento de código), pero se intentó leer en lugar de ejecutarse;
Si el Descriptor propuesto fue marcado No Presente.
Tenga en cuenta que el último no puede ser un problema fatal para el programa: el sistema operativo podría observar el indicador, restablecer el segmento, marcarlo como ahora presente y permitir que la instrucción de fallas proceda con éxito.
O tal vez el Descriptor se cargó exitosamente en un Registro de Segmento, pero luego un acceso futuro con él rompió una de varias reglas:
- El registro de segmento se cargó con el índice de descriptor
0x0000para el GDT. Esto fue reservado por el hardware comoNULL; - Si el Descriptor cargado se marcó como Sólo lectura, se intentó una Escritura.
- Si alguna parte del acceso (1, 2, 4 o más bytes) estaba fuera del límite del segmento.
Cambio al modo protegido
Cambiar al modo protegido es fácil: solo necesita configurar un bit en un registro de control. Pero mantenerse en Modo Protegido, sin que la CPU levante las manos y se reinicie debido a que no sabe qué hacer a continuación, requiere mucha preparación.
En resumen, los pasos requeridos son los siguientes:
Es necesario configurar un área de memoria para la tabla global de descriptores para definir un mínimo de tres descriptores:
- El zeroeth,
NULLDescriptor; - Otro descriptor para un segmento de código;
- Otro descriptor para un segmento de datos.
Esto puede ser usado tanto para Datos como para Apilar.
- El zeroeth,
El Registro de la Tabla de Descriptor Global (
GDTR) debe inicializarse para apuntar a esta área definida de la memoria;GDT_Ptr dw SIZE GDT dd OFFSET GDT ... lgdt [GDT_Ptr]El bit
PMenCR0necesita ser configurado:mov eax, cr0 ; Get CR0 into register or eax, 0x01 ; Set the Protected Mode bit mov cr0, eax ; We're now in Protected Mode!Los Registros de segmento deben cargarse desde el GDT para eliminar los valores actuales del Modo Real:
jmp 0x0008:NowInPM ; This is a FAR Jump. 0x0008 is the Code Descriptor NowInPM: mov ax, 0x0010 ; This is the Data Descriptor mov ds, ax mov es, ax mov ss, ax mov sp, 0x0000 ; Top of stack!
Tenga en cuenta que esto es lo mínimo, sólo para obtener la CPU en modo protegido. Para realmente tener todo el sistema listo puede requerir muchos más pasos. Por ejemplo:
- Es posible que las áreas de memoria superiores tengan que estar habilitadas: desactivar la puerta
A20; - Definitivamente, las Interrupciones deberían estar deshabilitadas, pero tal vez los diversos Manejadores de fallas podrían configurarse antes de ingresar al Modo protegido, para permitir errores al inicio del proceso.
El autor original de esta sección escribió un tutorial completo sobre cómo ingresar al Modo protegido y cómo trabajar con él.
Modo irreal
El modo irreal explota dos hechos sobre cómo los procesadores Intel y AMD cargan y guardan la información para describir un segmento.
El procesador almacena en caché la información del descriptor obtenida durante un movimiento en un registro de selección en modo protegido.
Esta información se almacena en una parte invisible arquitectónica del registro de selector ellos mismos.En el modo real, los registros de selección se denominan registros de segmento, pero, aparte de eso, designan el mismo conjunto de registros y, como tales, también tienen una parte invisible. Estas partes se llenan con valores fijos, pero para la base que se deriva del valor que se acaba de cargar.
En tal vista, el modo real es solo un caso especial de modo protegido: donde la información de un segmento, como la base y el límite, se obtiene sin un GDT / LDT pero aún se lee desde la parte oculta del registro de segmentos.
Cambiando en modo protegido y creando un GDT es posible crear un segmento con los atributos deseados, por ejemplo, una base de 0 y un límite de 4GiB.
A través de una carga sucesiva de un registro selector, dichos atributos se almacenan en caché, luego es posible volver a conmutar en modo real y tener un registro de segmento a través del cual acceder a todo el espacio de direcciones de 32 bits.
BITS 16
jmp 7c0h:__START__
__START__:
push cs
pop ds
push ds
pop ss
xor sp, sp
lgdt [GDT] ;Set the GDTR register
cli ;We don't have an IDT set, we can't handle interrupts
;Entering protected mode
mov eax, cr0
or ax, 01h ;Set bit PE (bit 0) of CR0
mov cr0, eax ;Apply
;We are now in Protected mode
mov bx, 08h ;Selector to use, RPL = 0, Table = 0 (GDT), Index = 1
mov fs, bx ;Load FS with descriptor 1 info
mov gs, bx ;Load GS with descriptor 1 info
;Exit protected mode
and ax, 0fffeh ;Clear bit PE (bit0) of CR0
mov cr0, eax ;Apply
sti
;Back to real mode
;Do nothing
cli
hlt
GDT:
;First entry, number 0
;Null descriptor
;Used to store a m16&32 object that tells the GDT start and size
dw 0fh ;Size in byte -1 of the GDT (2 descriptors = 16 bytes)
dd GDT + 7c00h ;Linear address of GDT start (24 bits)
dw 00h ;Pad
dd 0000ffffh ;Base[15:00] = 0, Limit[15:00] = 0ffffh
dd 00cf9200h ;Base[31:24] = 0, G = 1, B = 1, Limit[19:16] = 0fh,
;P = 1, DPL = 0, E = 0, W = 1, A = 0, Base[23:16] = 00h
TIMES 510-($-$$) db 00h
dw 0aa55h
Consideraciones
- Tan pronto como se vuelve a cargar un registro de segmento, incluso con el mismo valor, el procesador vuelve a cargar los atributos ocultos de acuerdo con el modo actual. Esta es la razón por la que el código anterior utiliza
fsygspara contener los segmentos "extendidos": tales registros tienen menos probabilidades de ser utilizados / guardados / restaurados por los diversos servicios de 16 bits. - La instrucción
lgdtno carga un puntero lejano al GDT, en su lugar carga una dirección lineal de 24 bits (puede anularse a 32 bits). Esta no es una dirección cercana , es la dirección física (ya que la paginación debe estar deshabilitada). Esa es la razón deGDT+7c00h. - El programa anterior es un cargador de arranque (para MBR, no tiene BPB) que establece
cs/ds/sstp 7c00h e inicia el contador de ubicación a partir de 0. Entonces, un byte en el offset X en el archivo está en offset X en el segmento 7c00h y En la dirección lineal 7c00h + X. - Las interrupciones deben estar deshabilitadas ya que un IDT no está configurado para el viaje de ida y vuelta corto en modo protegido.
- El código utiliza un truco para guardar 6 bytes de código. La estructura cargada por
lgdtse guarda en el ... GDT mismo, en el descriptor nulo (el primer descriptor).
Para obtener una descripción de los descriptores GDT, consulte el Capítulo 3.4.3 de Intel Manual Volume 3A .