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 , DS y SS tenían el mismo valor);
  • Mantener el Código, los Datos y la Pila completamente separados unos de otros ( CS , DS y SS todos 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 de 0x0000 significaría un tamaño de 0 , 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 0x0000 para el GDT. Esto fue reservado por el hardware como NULL ;
  • 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:

    1. El zeroeth, NULL Descriptor;
    2. Otro descriptor para un segmento de código;
    3. Otro descriptor para un segmento de datos.

      Esto puede ser usado tanto para Datos como para Apilar.

  • 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 PM en CR0 necesita 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.

  1. 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.

  2. 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 fs y gs para contener los segmentos "extendidos": tales registros tienen menos probabilidades de ser utilizados / guardados / restaurados por los diversos servicios de 16 bits.
  • La instrucción lgdt no 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 de GDT+7c00h .
  • El programa anterior es un cargador de arranque (para MBR, no tiene BPB) que establece cs / ds / ss tp 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 lgdt se 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 .



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow