Modo largo en procesadores Intel

  1. Alpertron
  2. Microprocesadores de la línea Intel
  3. Modo largo

por Dario Alejandro Alpern

Transcripción

Hola. Mi nombre es Darío Alpern y hoy vamos a ver introducción a modo largo.

Modos de operación

Los procesadores de Intel soportan varios modos de operación. A continuación se verán los más importantes:

El modo heredado es básicamente un conjunto de submodos creados antes que el modo largo.

El modo real es el que corre desde el arranque del procesador, permite direccionar 1 MB y no hay ningún método para proteger el sistema de accesos a memoria fuera de rango o usando punteros nulos.

El modo protegido permite proteger el sistema operativo de las aplicaciones y éstas entre sí. Permite acceder a 4GB y si se activa paginación con PAE, se puede acceder a direcciones físicas por arriba de 4 GB. El modo protegido permite correr sistemas operativos de 16 bits, que son obsoletos hace por lo menos dos décadas, y de 32 bits.

El modo virtual 8086 permite correr aplicaciones compiladas para modo real en sistemas operativos de 32 bits. Dicho modo es una tarea, lo que significa que el sistema operativo puede correr varias sesiones de modo virtual 8086 al mismo tiempo junto con otras aplicaciones de modo protegido de 16 o de 32 bits. El primer uso importante del modo virtual 8086 fue en el sistema operativo Windows 3.0 del año 1990 que en el modo 386 mejorado permitía correr aplicaciones de Windows y de DOS al mismo tiempo.

El modo largo fue creado por la empresa AMD y apareció por primera vez en abril de 2003 en su procesador Opteron, que estaba orientado a servidores. Este modo apareció originalmente en Intel en el modelo de procesador Xeon denominado Nocona, también para servidores, que salió al mercado en junio de 2004. En procesadores para uso personal, esta arquitectura apareció en algunos modelos del Pentium 4 denominado Prescott, que salieron pocos meses después.

Existen diferencias en la implementación del modo largo entre ambas empresas, pero son menores.

El modo largo requiere sistemas operativos de 64 bits, completamente diferentes de los sistemas operativos de 32 bits que corren en modo protegido. Se pueden correr aplicaciones de 64 bits, o bien de 32 o 16 bits en el llamado modo compatibilidad.

Aparte de poder acceder a registros de 64 bits, el código que corre en segmentos de 64 bits tiene a su disposición 16 registros de uso general, 16 registros de SSE, y 16 registros de control en vez de 8, que es lo que se puede acceder en segmentos de código de 16 o de 32 bits.

El modo largo no incluye el modo virtual 8086, por lo que no se pueden correr programas compilados para modo real, como por ejemplo, aplicaciones que corren bajo el sistema operativo DOS.

El modo de gerencia del sistema o SMM se activa mediante la pata SMI del procesador o mediante la instrucción SMI. El procesador almacena su estado en una zona determinada de la memoria y luego ejecuta el manejador de este modo de manera similar a una interrupción. Dentro de este modo, el procesador opera de manera similar al modo real, excepto que no hay verificación de límite, por lo que se puede acceder a la totalidad del espacio direccionable. La instrucción RSM sirve para salir del modo de gerencia del sistema y se recuperan todos los registros. De esta manera el procesador continúa ejecutando en el modo anterior al ingreso a SMM.

La virtualización permite correr una máquina virtual desde el arranque dentro de un sistema operativo. En este contexto, hay dos sistemas operativos, el anfitrión, que es el que estaba corriendo de antes, y el invitado, que es el que corre dentro de la máquina virtual.

Es posible por ejemplo, correr un sistema operativo de 32 bits dentro de un sistema operativo de 64 bits mediante virtualización, y de esa manera correr aplicaciones antiguas de DOS.

Dos conceptos importantes son los de entrada a la máquina virtual (VM Entry en inglés), en el que el procesador comienza a ejecutar el sistema operativo invitado, y la salida de la máquina virtual (VM Exit en inglés), en el que el procesador deja de ejecutar el invitado y continúa con el anfitrión.

De esta manera se puede usar un driver del sistema operativo anfitrión para interactuar con hardware cuando el sistema operativo invitado necesita accederlo.

Registros de uso general

En estos diagramas se ven los registros de uso general accesibles en segmentos de código de 64 bits. En el modo de compatibilidad, es decir, en 16 o 32 bits, los registros accesibles son los mismos que en el modo heredado.

A la izquierda figuran los 16 registros de uso general de 64 bits. Las extensiones de los 8 registros conocidos de 32 bits reemplazan la letra inicial "E" por la letra "R". Por ejemplo, la extensión a 64 bits del registro EDX es RDX. Los ocho nuevos registros se denominan R8 a R15.

Es posible acceder a los 32 bits menos significativos de estos registros. Por ejemplo, la parte baja de RAX es EAX. En el caso de los registros nuevos, se agrega la letra D, de doubleword, que son 32 bits, al final. Por ejemplo la parte baja de R10 es R10D.

A su vez, los registros de 32 bits se pueden subdividir por la mitad y se puede acceder a la parte baja. En el caso de los registros nuevos, se agrega el sufijo W, de word, que son 16 bits. Por ejemplo, la parte baja de R13D es R13W.

Finalmente, se pueden dividir los registros de 16 bits y acceder a la parte baja, o en algunos casos, a la parte alta de dicho registro. En el caso de los registros nuevos, se agrega el sufijo B, de byte, que son 8 bits. Por ejemplo, la parte baja de R8W es R8B.

Los registros de puntero de instrucciones y los indicadores también se expanden a 64 bits, y sus nombres son RIP y RFLAGS respectivamente.

Prefijo REX

Así como en el procesador 80386 se agregaron los prefijos de tamaño de operandos 0x66 y de tamaño de direcciones 0x67 para que se puedan usar los códigos de operación con registros de 32 bits y con direccionamiento de 32 bits, en el modo de 64 bits hubo que agregar más prefijos para poder acceder a los registros de 64 bits y para poder usar 16 registros en vez de los 8 que se permiten en segmentos de código de 16 y de 32 bits.

Como todos los bytes ya estaban usados para código de operación y los prefijos definidos en el modo heredado, la solución fue eliminar instrucciones de un byte para hacer lugar para estos prefijos.

Instrucciones INC y DEC

De esta manera, en este modo, los bytes 0x40 a 0x47 que correspondían a instrucciones INC de registros y los bytes 0x48 a 0x4F que correspondían a instrucciones DEC de registros, se convierten en los nuevos prefijos.

Esto no quita funcionalidad al procesador porque estas instrucciones de un byte también podían expresarse mediante un código de operación de dos bytes. Por ejemplo, INC EBX, que se podía codificar como 0x43, también podía hacerlo mediante el par de bytes 0xFF 0xC3.

Prefijo REX

Como se puede ver en el diagrama, el prefijo usa cuatro bits independientes: el bit W es el de tamaño de operandos, que define si se usan 32 o 64 bits. En este modo se sigue usando el prefijo 0x66 para indicar que el tamaño de operando es de 16 bits. El acceso a operandos de 8 bits se realiza mediante un código de operación diferente y este tamaño no se puede modificar mediante el prefijo 0x66 o el bit W del prefijo REX.

Luego existen tres bits adicionales que agregan un bit a cada uno de los campos que se encuentran en el código de operación: el número de registro, el registro índice y el registro base. En los tres casos los campos en el modo 64 bits pasan de tres a cuatro bits, siendo el bit definido en el prefijo REX el bit más significativo.

Los registros AH, BH, CH y DH sólo se pueden acceder si la instrucción no tiene prefijo REX. En caso de haberlo, la instrucción accede a SIL, DIL, BPL o SPL, que son la parte baja de SI, DI, BP o SP respectivamente.

Cuando la instrucción accede a memoria en el modo 64 bits, normalmente usa direccionamiento de 64 bits. Si se desea usar direccionamiento de 32 bits, se debe agregar el prefijo 0x67 de tamaño de direcciones.

Ejemplos REX

A continuación veremos varios ejemplos del uso de los prefijos en 64 bits.

En el primer ejemplo vemos el código de operación sin prefijos. En el caso de la instrucción MOV de registros que no sean de 8 bits, se usa el código de operación 89 y luego en el byte siguiente los dos bits más significativos valen 1, a continuación los tres bits del registro fuente (en nuestro caso el registro ECX tiene el valor uno) y finalmente los tres bits del registro destino (en nuestro caso EAX tiene el valor cero).

En el segundo ejemplo, lo único que modificamos es el tamaño de operandos, que pasamos de 32 a 64 bits, por lo que necesitamos un prefijo REX donde el bit W valga uno y el resto de los bits cero.

En el tercer ejemplo, usamos operandos de 32 bits, pero el registro destino es uno de los nuevos. Así que necesitamos prefijo REX. En el caso de la instrucción MOV de dos registros, el registro destino se codifica en el campo que se usa para la base cuando usamos acceso a memoria, mientras que el registro fuente se codifica en el campo que se usa para el registro cuando accedemos a memoria. El registro R8D tiene el código 1000 binario, por lo que deberemos poner el bit B a uno y el campo base ("b b b" en el diagrama) a cero. De esta manera tenemos prefijo REX y el mismo código de operación que en los dos ejemplos anteriores.

En el cuarto ejemplo, tenemos operandos de 16 bits, por lo que agregamos el prefijo 0x66 al primer ejemplo.

El quinto ejemplo es similar al tercero, pero usa operandos de 64 bits, así que en el prefijo REX ponemos a 1 el bit W aparte del bit B que ya estaba puesto a 1 en el tercer ejemplo.

En el último ejemplo, modificamos el ejemplo anterior para que el registro fuente sea R9, que tiene el valor 1001. Como dijimos que el registro fuente se codifica en el campo registro ("r r r" en el diagrama), debemos poner a 1 el bit R.

Ejemplos REX de acceso a memoria

Ahora veremos algunos ejemplos de acceso a memoria.

El formato de la instrucción para cargar un registro que no sea de 8 bits desde la posición de memoria apuntada por otro registro de 32 o 64 bits no es muy diferente a lo que vimos antes: el código de operación es 8B, en el byte siguiente los dos bits más significativos valen cero, luego siguen los tres bits que identifican el registro destino y finalmente los tres bits que identifican el registro base.

En el primer ejemplo, usamos operandos de 32 bits, direccionamiento de 64 bits y ningún registro nuevo, así que no hace falta el prefijo REX.

Como el direccionamiento por defecto es de 64 bits, en el segundo ejemplo necesitamos el prefijo 0x67 para poder usar direccionamiento de 32 bits.

En el tercer ejemplo, el registro es uno de los nuevos y además los operandos tienen 64 bits así que el bit W debe valer 1 y el bit R también.

En el cuarto ejemplo, usamos uno de los registros nuevos como registro base, así que ponemos a uno el bit B.

En el último ejemplo, usamos un registro nuevo y usamos direccionamiento de 32 bits, por lo que necesitamos el prefijo 0x67 para direccionar 32 bits y ponemos el bit R a 1 para indicar que el campo registro tiene un registro nuevo.

En estos ejemplos no se ve ningún caso en el que el bit X esté encendido. Eso requiere direccionamiento indirecto en el que se encuentren registros base e índice, donde el registro índice sea uno de los nuevos registros.

Descriptores de código y datos

En este diagrama se pueden ver los descriptores de código y datos que se usan en el modo largo.

El formato es muy similar al usado en el modo protegido. Al igual que en dicho modo, el byte de derechos de acceso, que es el byte 5, se intepreta diferente según el tipo de descriptor.

La diferencia más significativa se encuentra en el byte 6, que incluye los campos D y L. Según el contenido de estos dos bits, se puede determinar si el segmento es de 16, de 32 o de 64 bits.

El manejo de la base y el límite en segmentos de 16 y de 32 bits es igual que en el modo protegido. El manejo en 64 bits se verá más adelante.

Dirección canónica

La dirección canónica es una dirección lineal en el modo 64 bits, en donde los bits 63 a 48 son iguales al bit 47.

Si el código desea acceder a una dirección no canónica, el procesador genera una excepción 13.

Model Specific Register

Estos registros, que aparecieron originalmente en el procesador Pentium en 1993, tienen varias funcionalidades y en cada generación se agregan nuevos MSR. También existen MSR que dejan de existir en procesadores más modernos. El fabricante de procesadores señaló algunos MSR como permanentes y garantiza su existencia a futuro. Esos MSR se denominan de arquitectura.

Un ejemplo de MSR de arquitectura es el Time Stamp Counter (TSC) que arranca en cero cuando se enciende el procesador y se incrementa por cada ciclo de reloj.

La longitud de los MSR es de 64 bits y se acceden mediante un índice de 32 bits.

Las instrucciones de lectura y escritura de MSR se denominan RDMSR (de Read Model Specific Register) y WRMSR (de Write Model Specific Register). Dichas instrucciones sólo se pueden ejecutar con CPL = 0. El índice debe grabarse en el registro ECX y el resultado de la lectura se almacena en EDX:EAX.

La gran mayoría de la nueva funcionalidad se obtiene accediendo a MSR.

Segmentos de 64 bits

En segmentos de 64 bits, el procesador ignora el límite. La única validación que hace es que la dirección lineal tenga formato canónico.

Con respecto a la base, para los registros CS, DS, ES y SS también la ignora. En cambio, en el caso de los registros FS y GS, el procesador carga en el campo base de estos registros lo que esté indicado en el descriptor.

Como no se pueden representar direcciones lineales más grandes que 4 GB en los descriptores de datos, el procesador cuenta con dos MSR que permiten cargar cualquier valor de 64 bits en la base de estos registros, siempre que estén en formato canónico.

Los MSR involucrados son MSR_FS_BASE, que tiene índice 0xC0000100, y MSR_GS_BASE, que tiene índice 0xC0000101.

Además existe una instrucción que se llama SWAPGS, que intercambia la base del registro GS con el MSR que tiene el nombre MSR_KERNEL_GS_BASE y cuyo índice es 0xC0000102.

Compuertas

Las compuertas contienen direcciones lógicas, es decir selectores y offsets. Como el offset tiene 64 bits, necesita 8 bytes. Junto con los dos bytes del selector, se excede el tamaño de 8 bytes por descriptor.

Por ello se usa la siguiente potencia de 2, es decir 16 bytes como tamaño de las compuertas.

En este diagrama se puede ver el formato de las compuertas de llamada que se encuentra en la GDT y las de interrupción y excepción que se encuentran en la IDT.

Una diferencia de las compuertas entre el modo largo y el modo protegido, aparte del tamaño, es que en el caso de la compuerta de llamada no existe el campo de cantidad de palabras que permite copiar los argumentos de una pila a otra de mayor privilegio.

Otra diferencia es que las compuertas de interrupción y excepción tienen un nuevo campo, denominado IST (Interrupt Stack Table), que permite que un manejador de interrupción o excepción use una pila diferente que la normal.

Paginación

Una condición necesaria para poder ingresar al modo largo es que esté habilitada la paginación con PAE encendido.

La jerarquía de paginación en modo largo tiene cuatro niveles, a diferencia de modo protegido donde hay dos o tres.

Para poder realizar la traducción de direcciones lineales a físicas, la dirección lineal se divide en seis campos.

Los bits 63 a 48 no entran en la traducción al ser canónicas las direcciones lineales válidas. Entonces como tienen el mismo valor que el bit 47, no agregan información.

Los bits 47 a 39 corresponden al índice en la tabla PML4.

Los bits 38 a 30 de la dirección lineal corresponden al índice de la tabla de punteros de directorio de páginas.

Los bits 29 a 21 corresponden al índice del directorio de pagínas.

Los bits 20 a 12 corresponden al índice de la tabla de páginas.

Los bits 11 a 0 no se traducen y forman el offset dentro de la página.

El registro CR3 apunta a la tabla de mayor jerarquía, que es la tabla PML4.

En modo largo existen páginas grandes de 2 MB y de 1 GB.

Pila

Como en el caso de modo protegido, hay una pila por nivel de privilegio.

El puntero de pila siempre debe ser múltiplo de la cantidad de bits del segmento de código. Esto quiere decir que para segmentos de 32 bits, ESP debe ser múltiplo de 4 y para segmentos de 64 bits, RSP debe ser múltiplo de 8.

El nivel de privilegio de la pila siempre es igual al nivel de privilegio del segmento de código. Una ventaja en modo largo con respecto al modo protegido es que no hace falta crear descriptores de datos para las pilas de mayor privilegio.

Existen dos casos principales cuando hay llamadas intersegmento, interrupciones o excepciones: si hay cambio de nivel de privilegio o no.

En el segundo caso, que es más sencillo, el procesador memoriza en registros temporarios los valores de SS y RSP, luego modifica RSP para que sea múltiplo de 16 y a continuación almacena los valores de SS y RSP viejos, RFLAGS en el caso de interrupciones y excepciones, CS y RIP. Para determinadas excepciones, luego se coloca en la pila el código de error. Todas las entradas son de 8 bytes, incluyendo los selectores, donde se ponen los seis bytes más significativos a cero y los dos menos significativos con el valor del selector.

La modificación de RSP es importante en el caso de llamar a un segmento de código 64 bits desde otro de 32 bits. De esa manera nos aseguramos que RSP esté alineado como corresponde.

Cuando hay cambio de nivel de privilegio, se debe cambiar la pila.

El offset de la nueva pila se obtiene de campos dentro de la estructura del segmento de estado de la tarea (TSS o Task State Segment). Si la compuerta es de llamada o el campo IST de la compuerta vale cero, el offset de la nueva pila sa obtiene del campo RSPn de la TSS. En cambio, si el campo IST no es cero, ese campo es un índice a un array de punteros de pila ISTn que se encuentra en la TSS.

El procesador carga el selector de SS con el valor cero y atributo de nivel de privilegio con el del nuevo CPL.

Una vez hecho esto, se cargan los registros vistos anteriormente en la nueva pila.

El campo IST es útil por ejemplo para la excepción de fallo de pila ya que permite usar una pila nueva que no debería causar una nueva excepción.

TSS

En el diagrama se puede ver el segmento de estado de la tarea o Task State Segment (TSS) con los campos RSPn e ISTn ya mencionados.

En el caso de cambio de nivel de privilegio, cuando ocurre una llamada intersegmento, una interrupción o una excepción, el procesador cambia a otro segmento de mayor privilegio, es decir menor numéricamente. Por eso no existe el campo RSP3.

En el caso de ejecutar una instrucción de entrada/salida tal como IN o OUT, el procesador compara CPL contra el indicador IOPL ubicado en el registro RFLAGS. Si CPL es menor o igual que IOPL, entonces se ejecuta la instrucción. Por ejemplo, si CPL vale cero, siempre se puede ejecutar IN o OUT. SI CPL es mayor que IOPL, el procesador consulta el bitmap de entrada/salida, que es un array de bits que permite determinar si se ejecuta la instrucción: si el bit vale cero, se puede ejecutar, y si vale uno o el límite de la TSS es menor que el offset donde debería leerse el bitmap de entrada/salida ocurre una excepción 13.

Task Register

El formato del Task Register es el mismo que el de los registros de segmento. El selector apunta a un descriptor de TSS que se encuentra en la GDT.

El descriptor de TSS tiene un bit B que indica si el procesador está usando el TSS apuntado por el descriptor o no.

El TSS debe necesariamente estar por debajo de los 4 GB, ya que el descriptor tiene 32 bits para la base.

Lectura TR

Existen dos instrucciones para acceder al Task Register.

Para escribirlo usamos LTR, que significa Load Task Register. Sólo se puede ejecutar con CPL = 0 y carga el selector de registro de tarea con el valor indicado en el argumento de la instrucción.

Para leer el selector de TR usamos STR, que significa Store Task Register. Dicho selector se almacena en el registro o posición de memoria indicado en el argumento de la instrucción.

Sólo se puede ejecutar LTR con un selector cuyo descriptor apunte a un TSS libre. Luego de la instrucción queda ocupado. Como el procesador escribe el bit B del descriptor, es necesario que la GDT esté en memoria RAM.

Registro EFER

EFER es la sigla de Extended Feature Enable Register o registro de habilitación de características extendidas. Es un MSR en el que cada bit habilita una característica diferente. En ese sentido es similar a los registros de control CR0 y CR4.

Los bits que nos interesan a nosotros son el bit 8, que habilita el modo largo, el bit 10, que sirve para saber si el procesador se encuentra en dicho modo y el bit 11, que es el bit de habilitación de atributo de página No execute (NX).

El bit 10 es necesario porque para entrar en modo largo, aparte de habilitar el bit 8, hay que habilitar la paginación con PAE encendido.

Ingreso a modo largo:

Aquí se enumeran los pasos necesarios para entrar a modo largo.

Los pasos son:

Deshabilitar interrupciones.

Deshabilitar paginación si estaba activada.

Cargar tablas de paginación con los cuatro niveles de jerarquía verificando que el código que pase a modo largo esté en identity mapping tanto en modo protegido como en modo largo.

Poner a "1" el bit 5 del registro de control CR4 (PAE).

Cargar CR3 con la dirección inicial de la tabla PML4.

Poner a "1" el bit 8 de EFER.

Habilitar paginación y modo protegido (si no estaba antes) poniendo a "1" los bits 0 y 31 del registro de control CR0.

En este momento el procesador está en modo compatibilidad. Hacer salto intersegmento a segmento de código de 64 bits definido en la GDT.

Si se desea se puede poner la dirección inicial de GDT o LDT por arriba de los 4 GB una vez que se ingresa al modo 64 bits ya que el argumento de las instrucciones LGDT y LIDT apunta a la imagen de GDT o IDT compuesto por dos bytes de límite y ocho de dirección lineal.

Espero que les haya sido de interés. Hasta luego.