por Dario Alejandro Alpern
El microprocesador 80286 ha añadido un nuevo nivel de satisfacción a la arquitectura básica del 8086, incluyendo una gestión de memoria con la extensión natural de las capacidades de direccionamiento del procesador. El 80286 tiene elaboradas facilidades incorporadas de protección de datos. Otras características incluyen todas las características del juego de instrucciones del 80186, así como la extensión del espacio direccionable a 16 MB, utilizando 24 bits para direccionar (224 = 16.777.216).
El 80286 revisa cada acceso a instrucciones o datos para comprobar si puede haber una violación de los derechos de acceso. Este microprocesador está diseñado para usar un sistema operativo con varios niveles de privilegio. En este tipo de sistemas operativos hay un núcleo que, como su nombre indica, es la parte más interna del sistema operativo. El núcleo tiene el máximo privilegio y los programas de aplicaciones el mínimo. Existen cuatro niveles de privilegio. La protección de datos en este tipo de sistemas se lleva a cabo teniendo segmentos de código (que incluye las instrucciones), datos (que incluye la pila aparte de las variables de los programas) y del sistema (que indican los derechos de acceso de los otros segmentos).
Para un usuario normal, los registros de segmentación (CS, DS, ES, SS) parecen tener los 16 bits usuales. Sin embargo, estos registros no apuntan directamente a memoria, como lo hacían en el 8086. En su lugar, apuntan a tablas especiales, llamadas tablas de descriptores, algunas de las cuales tienen que ver con el usuario y otras con el sistema operativo. Actualmente a los 16 bits, cada registro de segmento del 80286 mantiene otros 57 bits invisibles para el usuario. Ocho de estos bits sirven para mantener los derechos de acceso (sólo lectura, sólo escritura y otros), otros bits mantienen la dirección real (24 bits) del principio del segmento y otros mantienen la longitud permitida del segmento (16 bits, para tener la longitud máxima de 64 KB). Por ello, el usuario nunca sabe en qué posición real de memoria está ejecutando o dónde se ubican los datos y siempre se mantiene dentro de ciertas fronteras. Como protección adicional, nunca se permite que el usuario escriba en el segmento de código (en modo real se puede escribir sobre dicho segmento). Ello previene que el usuario modifique su programa para realizar actos ilegales y potencialmente peligrosos. Hay también provisiones para prever que el usuario introduzca en el sistema un "caballo de Troya" que pueda proporcionarle un estado de alto privilegio.
El 80286 tiene cuatro nuevos registros. Tres de ellos apuntan a las tablas de descriptores actualmente en uso. Estas tablas contienen información sobre los objetos protegidos en el sistema. Cualquier cambio de privilegio o de segmento debe realizarse a través de dichas tablas. Adicionalmente hay varios indicadores nuevos.
Existen varias instrucciones nuevas, además de las introducidas con el 80186. Todas estas instrucciones se refieren a la gestión de memoria y protección del sistema haciendo cosas tales como cargar y almacenar el contenido de los indicadores especiales y los punteros a las tablas de descriptores.
Como en modo real, en modo protegido se utilizan dos componentes para formar la dirección física: un selector de 16 bits se utiliza para determinar la dirección física inicial del segmento, a la cual se suma una dirección efectiva (offset) de 16 bits.
La diferencia entre los dos modos radica en el cálculo de la dirección inicial del segmento. En modo protegido el selector se utiliza para especificar un índice en una tabla definida por el sistema operativo. La tabla contiene la dirección base de 24 bits de un segmento dado. La dirección física se obtiene sumando la dirección base hallada en la tabla con el offset.
La segmentación es un método de manejo de memoria. La segmentación provee la base para la protección. Los segmentos se utilizan para encapsular regiones de memoria que tienen atributos comunes. Por ejemplo, todo el código de un programa dado podría estar contenido en un segmento, o una tabla del sistema operativo podría estar en un segmento. Toda la información sobre un segmento se almacena en una estructura de ocho bytes llamada descriptor. Todos los descriptores del sistema están en tablas en memoria que reconoce el hardware.
Los siguientes términos se utilizan en la discusión de descriptores, niveles de privilegio y protección.
Bit | 15 ... 3 | 2 | 1 0 |
---|---|---|---|
Campos | Índice | TI | RPL |
El índice selecciona uno de entre 213 = 8192
descriptores.
El bit indicador de tabla (TI) especifica la
tabla de descriptores a utilizar: si vale
cero se utiliza la tabla de descriptores globales,
mientras que si vale uno, se utiliza la
tabla de descriptores locales.
Estas tablas definen todos los segmentos utilizados en un sistema basado en el 80286. Hay tres tipos de tablas que mantienen descriptores: la tabla de descriptores globales o GDT (Global Descriptor Table), la tabla de descriptores locales o LDT (Local Descriptor Table) y la tabla de descriptores de interrupción o IDT (Interrupt Descriptor Table). Todas las tablas son arrays de longitud variable, que pueden tener entre 8 y 65.536 bytes. Cada tabla puede mantener hasta 8192 descriptores. Los 13 bits más significativos de un selector se usan como un índice dentro de la tabla de descriptores. Las tablas tienen registros asociados que contienen la dirección base de 24 bits y el límite de 16 bits de cada tabla.
Cada una de las tablas tiene un registro asociado. Estos se llaman GDTR, LDTR, IDTR (ver las siglas en inglés que aparecen en el párrafo anterior). Las instrucciones LGDT, LLDT y LIDT cargan (Load) la base y el límite de la tabla de descriptores globales, locales o de interrupción, respectivamente, en el registro apropiado. Las instrucciones SGDT, SLDT y SIDT almacenan (Store) los valores anteriormente mencionados de los registros en memoria. Estas tablas son manipuladas por el sistema operativo, por lo que las instrucciones de carga son instrucciones privilegiadas (sólo se ejecutan si el CPL vale cero).
Como se indicó anteriormente, existen cuatro registros del 80286 que apuntan a las tablas de descriptores.
GDTR e IDTR: Estos registros mantienen la dirección base de 24 bits y el límite de 16 bits de las tablas GDT e IDT, respectivamente. Estos segmentos, como son globales para todas las tareas en el sistema, se definen mediante direcciones físicas de 24 bits y límite de 16 bits.
LDTR y TR: Estos registros mantienen los selectores de 16 bits para el descriptor de LDT y de TSS, respectivamente. Estos segmentos, como son específicos para cada tarea, se definen mediante valores de selector almacenado en los registros de segmento del sistema. Éste apunta a un descriptor apropiado (de LDT o TSS). Nótese que un registro descriptor del segmento (invisible para el programador) está asociado con cada registro de segmento del sistema.
A continuación se brinda una descripción detallada de los contenidos de los descriptores utilizados en el microprocesador 80286 operando en modo protegido.
Si el bit 3 vale uno:
Los segmentos de código cuyo bit C vale 1, pueden ejecutarse y compartirse por programas con diferentes niveles de privilegio (ver la sección sobre protección, más adelante).
A continuación se verá el formato del byte de derechos de acceso para descriptores de segmentos del sistema:
0000: Inválido | 0100: Compuerta de llamada |
0001: TSS disponible | 0101: Compuerta de tarea |
0010: LDT | 0110: Compuerta de interrupción |
0011: TSS ocupado | 0111: Compuerta de trampa |
Ahora se verá con más detalle los diferentes descriptores del sistema.
- Descriptores de LDT (S = 0, tipo = 2): Contienen información sobre tablas de descriptores locales. Las LDT contienen una tabla de descriptores de segmentos, únicos para cada tarea. Como la instrucción para cargar el registro LDTR sólo está disponible en el nivel de privilegio 0 (nivel más privilegiado), el campo DPL es ignorado. Los descriptores de LDT sólo se permiten en la tabla de descriptores globales (GDT).
- Descriptores de TSS (S = 0, tipos = 1, 3): Un descriptor de segmento de estado de la tarea (TSS: Task State Segment) contiene información sobre la ubicación, el tamaño y el nivel de privilegio de un TSS. Un TSS es un segmento especial con formato fijo que contiene toda la información sobre el estado de una tarea y un campo de enlace para permitir tareas anidadas. El campo de tipo se usa para indicar si la tarea está ocupada (tipo = 3), es decir, en una cadena de tareas activas, o si el TSS está disponible (tipo = 1). El registro de tarea (TR: Task Register) contiene el selector que apunta al TSS actual dentro de la GDT.
- Descriptores de compuertas (tipos = 4 a 7): Las compuertas se utilizan para controlar el acceso a puntos de entrada dentro del segmento de código objetivo. Los distintos tipos de compuerta son: de llamada (call gate), de tarea (task gate), de interrupción (interrupt gate) y de trampa (trap gate). Las compuertas proveen un nivel de indirección entre la fuente y el destino de la transferencia de control. Esta indirección permite al procesador realizar automáticamente verificaciones de protección. También permite a los diseñadores controlar los puntos de entrada a los sistemas operativos. Las compuertas de llamada se utilizan para cambiar niveles de privilegio (ver la sección que trata sobre privilegio, más adelante), las compuertas de tarea se utilizan para hacer cambios de tarea y las de interrupción y de trampa se utilizan para especificar rutinas de servicio de interrupción.
A continuación se muestra el formato de los descriptores de compuertas, que difieren del formato general:
Las compuertas de interrupción y de trampa utilizan los campos de selector y offset como un puntero al inicio de la rutina que maneja la interrupción o trampa. La diferencia entre ambos tipos de compuertas es que la de interrupción deshabilita las interrupciones (IF <- 0), mientras que la de trampa no altera IF.
Las compuertas de tarea se usan para cambiar tareas. Las compuertas de tarea sólo se pueden referir a un TSS y por lo tanto sólo se utiliza el campo del selector, siendo ignorado el de offset.
Se genera una excepción 13 (Violación general de protección) si un selector no se refiere a un tipo de descriptor correcto (un segmento de código para una compuerta de interrupción, trampa o llamada y un segmento de estado de tarea para la compuerta de tarea).
Contenido de los cachés descriptores de segmento: El contenido varía dependiendo del modo en que opera el 80286. En modo protegido, como se explicó más arriba, los cachés se cargan con la información de los descriptores. En modo real, el contenido no se obtiene de la memoria, sino que toma unos valores fijos para lograr compatibilidad con el 8086. La base generada es 16 veces el valor del selector, el límite siempre es FFFFh, el bit P (presente) vale 1, el nivel de privilegio es cero (máximo privilegio), el bit de dirección de expansión indica que los offsets deben ser menores o iguales que el límite y están habilitados los permisos de lectura, escritura y ejecución. Todo esto hace que en modo real no se pueda acceder a los 16 MB de capacidad de direccionamiento del 80286, ya que el valor máximo ocurre cuando el selector vale FFFFh y el offset también, con lo que se logra una dirección máxima de FFFF0h + FFFFh = 10FFEFh (casi 1088 KB).
El 80286 tiene cuatro niveles de protección que están optimizados para soportar las necesidades de los sistemas operativos multitarea para aislar y proteger los programas de un usuario de otros y del sistema operativo. Los niveles de privilegio controlan el uso de instrucciones privilegiadas, instrucciones de entrada/salida, y el acceso a segmentos y descriptores de segmento. A diferencia de los sistemas tradicionales basados en microprocesadores donde esta protección sólo se logra a través de un hardware externo muy complejo con el correspondiente software, el 80286 provee esta protección como parte de la unidad de manejo de memoria (MMU: Memory Management Unit) incorporada.
El sistema de privilegio jerárquico de cuatro niveles es una extensión de los modos usuario/supervisor que se encuentran comúnmente en minicomputadoras. Los niveles de privilegio (PL: Privilege Level) se numeran de 0 a 3, siendo el 0 el nivel más privilegiado (más confiable).
Ejemplo de los niveles jerárquicos:
Las instrucciones para verificar punteros son ARPL, VERR, VERW, LSL y LAR. Todas estas instrucciones ponen el indicador de cero a uno si la verificación se pudo realizar; si no se puede realizar pone ZF <- 0, en vez de generar una excepción 13 como hace con otras instrucciones.
Los tipos de descriptores que se usan para transferencia de control son los siguientes:
Tipos de transferencia de control | Operaciones que la generan | Descriptor que se referencia | Tabla de descriptores que se utiliza para realizar la transferencia |
---|---|---|---|
Transferencia de control intersegmento dentro del mismo nivel de privilegio | JMP CALL RET IRET (sólo si el flag NT vale 0) | Segmento de código | GDT LDT |
Transferencia de control intersegmento al mismo o mayor nivel de privilegio | CALL | Compuerta de llamada | GDT LDT |
Transferencia de control intersegmento al mismo o mayor nivel de privilegio | INT excepción interrupción externa | Compuerta de interrupción o trampa | Tabla de descriptores de interrupción |
Transferencia de control intersegmento al mismo o menor nivel de privilegio | RET IRET (sólo si el flag NT vale 0) | Segmento de código | GDT LDT |
Cambio de tarea a través del segmento de estado de tarea | CALL JMP | Segmento de estado de la tarea | Tabla de descriptores globales |
Cambio de tarea a través de una compuerta de tarea | CALL JMP | Compuerta de tarea | GDT LDT |
Cambio de tarea a través de una compuerta de tarea | IRET (sólo si el flag NT vale 1) | Compuerta de tarea | Tabla de descriptores de interrupción |
Las transferencias de control sólo pueden ocurrir si la operación que cargó el selector se refiere al tipo correcto de descriptor. Cualquier violación de estas reglas causará una excepción 13 (por ejemplo, un salto a través de una compuerta de llamado, o un IRET desde una subrutina de interrupción).
Para que el sistema sea aún más seguro, todas las transferencias de control también se sujetan a las reglas de privilegio, que indican lo siguiente:
Al retornar al nivel de privilegio original, el uso de la pila menos privilegiada se restaura como parte de la ejecución de la instrucción RET o la IRET. Para subrutinas que pasan parámetros en la pila y cruzan niveles de privilegio, se copia un número fijo de palabras (especificado en el campo de cuenta de palabras de la compuerta) de la pila previa a la actual. La instrucción RET intersegmento con un valor de ajuste del puntero de pila ajusta correctamente el SP al efectuar el retorno.
Compuertas de llamado: Estas compuertas proveen llamadas (CALL) indirectas, en forma protegida. Uno de los usos más importantes de las compuertas es poder tener un método seguro de transferencias de privilegio dentro de una tarea. Como el sistema operativo define todas las compuertas en el sistema, puede asegurar que todas las compuertas permiten el acceso sólo a los procedimientos confiables (como aquéllos que realizan manejo de memoria u operaciones de entrada/salida). Un intento de acceso a otro lugar causará una excepción 13 (Violación general de protección).
Los descriptores de compuertas siguen las mismas reglas de privilegio que los datos, esto es, una tarea puede acceder una compuerta sólo si el EPL (Effective Privilege Level) es igual o más privilegiado que el DPL (Descriptor Privilege Level). Las compuertas siguen las reglas de privilegio de las transferencias de control y por lo tanto sólo pueden transferir el control a un nivel más privilegiado.
Las compuertas de llamada se acceden mediante una instrucción CALL y son sintácticamente idénticas a la llamada a una subrutina normal. Cuando se activa una llamada que cambia el nivel de privilegio, suceden las siguientes acciones:
1) Cargar CS:IP de la compuerta y verificar la validez.
2) Poner el viejo SS en la nueva pila.
3) Poner el viejo SP en la nueva pila.
4) Copiar la cantidad de parámetros de 16 bits como se indica en el
campo de cuenta de palabras de la compuerta de la vieja a la nueva pila.
5) Poner la dirección de retorno en la pila.
Las compuertas de interrupción y de trampa trabajan de la misma forma que las compuertas de llamada, excepto que no hay copia de parámetros. La única diferencia entre las compuertas de interrupción y de trampa es que las primeras deshabilitan las interrupciones (IF <- 0), mientras que las otras no alteran el indicador de interrupciones IF.
Un atributo muy importante de cualquier sistema operativo multitarea/multiusuario es su habilidad para cambiar rápidamente entre tareas o procesos. El 80286 soporta esta operación incluyendo una instrucción de cambio de tarea en el hardware. Dicha instrucción salva el estado entero de la máquina (todos los registros, espacio de direcciones y un puntero a la tarea anterior), carga un nuevo estado de ejecución, realiza verificaciones de protección y comienza la ejecución en menos de 20 microsegundos. Al igual que la transferencia de control por compuertas, la operación de cambio de tareas se invoca ejecutando una instrucción JMP o CALL intersegmento que se refiere a un segmento de estado de la tarea (TSS) o un descriptor de compuerta de tarea en el GDT (Global Descriptor Table) o en el LDT (Local Descriptor Table). Una instrucción INT n, una excepción, trampa o interrupción externa puede también invocar la operación de cambio de tarea si hay un descriptor de compuerta de tarea en la entrada correspondiente de la IDT (Interrupt Descriptor Table).
El formato del TSS es como sigue:
00: Puntero selector del TSS
02: SP para CPL = 0
04: SS para CPL = 0
06: SP para CPL = 1
08: SS para CPL = 1
0A: SP para CPL = 2
0C: SS para CPL = 2
0E: IP (Punto de entrada)
10: Flags
12: Registro AX
14: Registro CX
16: Registro DX
18: Registro BX
1A: Registro SP
1C: Registro BP
1E: Registro SI
20: Registro DI
22: Selector ES
24: Selector CS
26: Selector SS
28: Selector DS
2A: Selector LDT de la tarea
2C: Disponible
Cada tarea debe tener un TSS asociado. El TSS actual se identifica mediante un registro especial en el 80286 llamado TR (Task State Segment Register). Este registro contiene un selector que se refiere al descriptor de TSS de la tarea. Un registro de base y límite del segmento (invisible para el programador) asociado con TR se carga cada vez que TR se carga con un nuevo selector. El retorno de una tarea se realiza mediante la instrucción IRET. Cuando se ejecuta IRET, el control retorna a la tarea que había sido interrumpida. El estado de la tarea que se está ejecutando se almacena en el TSS y el estado de la vieja tarea se restaura de su propio TSS. Algunos bits en el registro de indicadores y la palabra de estado de la máquina (MSW) dan información sobre el estado de una tarea que son útiles para el sistema operativo. El bit 14 del registro de indicadores (NT = Nested Task) controla la función de la instrucción IRET. Si NT = 0, dicha instrucción realiza un retorno normal, pero si NT = 1, IRET realiza una operación de cambio de tarea para volver a ejecutar la tarea anterior. El bit NT se pone a cero o uno de la siguiente manera: cuando una instrucción CALL o INT inicia un cambio de tarea, el nuevo TSS se marca como ocupado y el puntero del nuevo TSS se carga con el selector del viejo TSS. El bit NT de la nueva tarea se pone entonces a 1. Una interrupción que no causa un cambio de tareas pondrá a cero el bit NT (el bit NT será restaurado a su valor anterior luego de la ejecución del manejador de interrupciones). El bit NT también puede ser afectado por las instrucciones POPF o IRET.
El segmento de estado de la tarea (TSS) se marca como ocupado cambiando el campo de tipo de descriptor de tipo 1 a tipo 3. El uso de un selector que referencia un TSS ocupado resultará en una excepción 13 (Violación general de protección).
El estado del coprocesador no se salva automáticamente cuando ocurre un cambio de tareas, porque la nueva tarea puede no utilizar instrucciones del coprocesador. El bit 3 del MSW llamado TS (Task Switched) ayuda en esta situación. Cuando el microprocesador realiza un cambio de tarea, pone a uno el flag TS. El 80286 detecta el primer uso de una instrucción de coprocesador después del cambio de tarea y provoca una excepción 7 (Coprocesador inexistente). El manejador de la excepción puede entonces decidir si debe salvar el estado del coprocesador. Dicha excepción ocurre cuando se trata de ejecutar una instrucción ESC o WAIT (es decir, alguna referida al coprocesador) y los bits Task Switched y Monitor coprocessor extension están ambos a uno (es decir, TS = MP = 1).
Como el 80286 comienza la ejecución (después de activar el pin RESET) en modo real, es necesario inicializar las tablas del sistema y los registros con los valores apropiados. Los registros GDTR e IDTR deben referirse a tablas de descriptores globales y de interrupción (respectivamente) que sean válidas.
Para entrar en modo protegido debe ponerse el bit PE (bit 0 de MSW) a 1 utilizando la instrucción LMSW. Después de entrar en modo protegido, la siguiente instrucción deberá ser un JMP intersegmento para cargar el registro CS y liberar la cola de instrucciones. El paso final es cargar todos los registros de segmentos de datos con los valores iniciales de los selectores.
Una forma alternativa de entrar en modo protegido que es especialmente apropiada en sistemas operativos multitarea consiste en realizar un cambio de tarea para cargar todos los registros. En este caso la GDT contendría dos descriptores de TSS además de los descriptores de código y datos necesarios para la primera tarea. La primera instrucción JMP en modo protegido saltaría al TSS causando un cambio de tarea y cargando todos los registros con los valores almacenados en el TSS. El registro TR (Task State Segment Register) deberá apuntar un descriptor de TSS válido ya que un cambio de tarea almacena el estado de la tarea actual en el TSS apuntado por TR.
Aparte de las instrucciones del 8086/8088 y las nuevas del 80186, el 80286 posee nuevas instrucciones. Éstas corresponden todas al modo protegido y son las siguientes:
ARPL dest, src (Adjust Requested Privilege Level of selector): Compara los bits RPL de dest contra src. Si el RPL de dest es menor que el RPL de src, los bits RPL del destino se cargan con los bits RPL de src y el indicador ZF se pone a uno. En caso contrario ZF se pone a cero. Ver nota 1.
CLTS (Clear Task Switched Flag): Pone a cero el indicador TS (bit 3 de la palabra de control de la máquina MSW). Ver nota 2.
LAR dest, src (Load Access Rights): El byte más alto del registro destino se carga con el byte de derechos de acceso del segmento indicado por el selector almacenado en src. Pone ZF a uno si se puede realizar la carga. Ver notas 1 y 3.
LGDT mem64 (Load Global Table register): Carga el valor del operando en el registro GDTR. Antes de ejecutar esta instrucción la tabla debe estar en memoria. Ver nota 2.
LIDT mem64 (Load Interrupt Table register): Carga el valor del operando en el registro IDTR. Antes de ejecutar esta instrucción la tabla debe estar en memoria. Ver nota 2.
LLDT {reg16|mem16} (Load Local Descriptor Table Register): Carga el selector indicado por el operando en el registro LDTR. Antes de ejecutar esta instrucción la tabla deberá estar en memoria. Ver notas 1 y 2.
LMSW {reg16|mem16} (Load Machine Status Word): Carga el valor del operando en la palabra de estado de la máquina MSW. El bit PE (bit 0) no puede ser puesto a cero por esta instrucción, por lo que una vez que se cambió a modo protegido, la única manera de volver a modo real es mediante un RESET del microprocesador. Ver nota 2.
LSL dest, src (Load Segment Limit): Carga el límite del segmento de un selector especificado en src en el registro destino si el selector es válido y visible en el nivel de privilegio actual. Si ocurre lo anterior el indicador ZF se pone a uno, en caso contrario, se pone a cero. Ver notas 1 y 3.
LTR {reg16|mem16} (Load Task Register): Carga el selector indicado por el operando en el registro TR. El TSS (Task State Segment) apuntado por el nuevo TR deberá ser válido. Ver notas 1 y 2.
SGDT mem64 (Store Global Descriptor Table register): Almacena el contenido del registro GDTR en el operando especificado.
SIDT mem64 (Store Interrupt Descriptor Table register): Almacena el contenido del registro IDTR en el operando especificado.
SLDT {reg16|mem16} (Store Global Descriptor Table register): Almacena el contenido del registro LDTR (que es un selector a la tabla de descriptores globales) en el operando especificado. Ver nota 1.
SMSW {reg16|mem16} (Store Machine Status Word): Almacena la palabra de estado de la máquina MSW en el operando especificado. Ver nota 2.
STR {reg16|mem16} (Store Task Register): Almacena el registro de tarea actual (selector a la tabla de descriptores globales) en el operando especificado. Ver nota 1.
VERR/VERW {reg16|mem16} (Verify Read/Write): Verifica si el selector de segmento especificado en el operando es válido y se puede leer/escribir en el nivel de privilegio actual. En este caso se pone ZF a uno, en caso contrario se pone ZF a cero. Ver notas 1 y 3.
Notas:
1) Si se ejecuta en modo real ocurre una excepción 6 (Código de operación inválido).
2) Si se ejecuta en modo protegido en alguno de los anillos 1-3 ocurre una excepción 13 (Violación general de protección).
3) Cualquier violación de privilegio del selector indicado en el operando no causa una excepción 13, en vez de ello, el indicador ZF se pone a cero.