How To cambio de tareas en IA-32e

Introducción

En la arquitectura IA-32 (32 bits), el procesador provee un mecanismo de cambio de tareas automático por hardware, haciendo uso de las estructuras TSS y de los descriptores TSS que se cargan en la GDT. El procesador se encarga de hacer un cambio de contexto entre una tarea y la subsiguiente, almacenando la información en la TSS de cada tarea.

Task Register (TR) en IA-32

Los contextos de las diferentes tareas se almacenan en los Task State Segments (TSS), uno por tarea. La parte visible de 16 bits del registro TR incluye en los bits 15-3 el índice del descriptor de TSS de esta tarea dentro de la GDT, por lo que se puede usar ese valor para identificar unívocamente una tarea.

Task Register (TR) en IA-32e

El registro Task Register TR no contiene el identificador de la tarea ya que el sistema usa una sola TSS. Por lo tanto, se debe establecer un identificador unívoco para cada tarea.

Contexto

Una opción es utilizar CR3 ya que este registro contiene la dirección física de cada PML4. Cada tarea debe usar un CR3 propio y diferente para que el sistema de protección funcione. Por lo tanto CR3 identifica unívocamente a cada tarea. Cada tarea debe tener una estructura creada en memoria de kernel para guardar su contexto, tal cual se hacia con las TSS en IA-32. La llamaremos espacio de contexto de la tarea actual. Esa estructura tendrá los siguientes campos:

  • RAX, RBX, RCX, RDX, RSI, RDI, RBP (Registros de propósito general extendidos a 64 bits)
  • R8, R9, R10, R11, R12, R13, R14, R15 (Registros de propósito general de 64 bits nuevos).
  • DS, ES, FS, GS.
  • RSP0 del TSS. (RSP1, RSP2 no se salvan porque no se usan en td3).
  • Registros de SIMD y de coprocesador matemático / MMX. Para utilizar las instrucciones y registros SIMD previamente se debe establecer (CR4.OSFXSR = 1, CR0.EM = 0, and CR0.TS = 0)

Los registros de segmento y los punteros de pila deberán inicializarse en forma previa a la primera ejecución de cada tarea. En general, no todas las tareas usan los registros SIMD y como la imagen en memoria de estos registros ocupa 512 bytes, la idea es salvar los registros de punto flotante sólo si hubo cambios durante el tiempo de corrida de la tarea y restaurar dichos registros del contexto sólo si la tarea usa estos registros.

Manejador de interrupción del scheduler

Opción 1: Contexto separado

No hace falta poner los RFLAGS, ni CS ni RIP en la estructura donde se guarda el contexto ya que la interrupción los salva en la pila. Haciendo uso de esta estructura el procedimiento para conmutar tareas en 64 bits es:

  1. Salvar en la pila el registro DS y uno de los registros de uso general y cargar DS con el segmento de datos del kernel. Este par se usará en el paso siguiente, por lo que necesitamos que sus datos no se pierdan.
  2. A partir de CR3 actual, ubicar la dirección del espacio de contexto de la tarea actual mediante el registro de uso general indicado arriba.
  3. Salvar en el espacio de contexto de la tarea actual los valores de los registros que conforman el contexto mínimo de la tarea actual.
  4. Si CR0.TS = 0 entonces se ejecutó el manejador de la excepción 7, por lo que los registros SIMD tendrán un valor diferente a lo que se encuentra en el contexto. De esta manera hay que salvar los nuevos valores de los registros mediante la instrucción FXSAVE pasándole como parámetro el puntero a la imagen de los registros SIMD dentro de ese contexto.
  5. Retirar los registros ingresados en la pila y ponerlos también en la estructura del contexto.
  6. Salvar el campo RSP0 de la TSS en el espacio de contexto. Generalmente los campos ISTn (1 < = n < = 7) no cambian entre diferentes tareas por lo que no hace falta salvarlos.
  7. Hallar el identificador de la tarea siguiente (nuevo valor de CR3) según la lógica del scheduler.
  8. Si este valor no coincide con el valor de CR3 actual, poner CR0.TS=1 (Task Switched flag), para que se dispare la excepción de punto flotante cuando se use una instrucción SSE.
  9. Cargar CR3 con el nuevo valor.
  10. Indicar fin de interrupción al PIC (si el scheduler corresponde a un sistema operativo no cooperativo).
  11. En base al nuevo valor de CR3 usar un registro de uso general para apuntar a la estructura del nuevo contexto.
  12. Cambiar RSP a la nueva pila usando ese contexto. El registro SS no hay que cambiarlo ya que corresponde al segmento flat de nivel de privilegio cero.
  13. Cargar uno de los registros de uso general diferente al usado en los dos pasos anteriores del nuevo contexto y salvarlo en la pila (de esa manera se puede usar ese registro para apuntar al contexto sin corromper su valor necesario para la nueva tarea). Luego copiar el puntero a este nuevo registro.
  14. Cambiar el campo RSP0 del TSS con el valor que se encuentra en la estructura del nuevo contexto.
  15. Cargar todos los registros de uso general (excepto el cargado en el paso 12), y los registros de segmento DS, ES, FS, GS con el nuevo contexto usando el registro cargado en el paso 12 para el direccionamiento indirecto. El último registro cambiado debe ser el DS.
  16. Restaurar el registro de uso general grabado en el paso 12.
  17. ejecutar IRETQ
  18. rezar…

Es importante destacar que antes de la primera ejecución de cualquier tarea y en forma previa a ejecutar IRETQ se deben almacenar en la pila (DPL=00) los registros RIP, CS, RFLAGS, RSP, SS.

Opción 2: Pila del scheduler como contexto

Se puede utilizar la pila del scheduler como contexto, lo que permite un scheduler más corto. Para eso se debe poner el campo IST de la compuerta de interrupción correspondiente al código que cambia de tarea con un valor distinto de cero e inicializar el campo correspondiente de la TSS con la dirección lineal de la dirección siguiente a la más alta del contexto (tiene que ser múltiplo de 8).

De esta manera, cuando llegue la interrupción, o cuando la tarea llame al scheduler (en caso de sistema operativo cooperativo), o cuando ocurra el cambio de pila, el puntero no será el campo RSP0 del TSS sino el ISTn, que apuntará como se expresó más arriba a la dirección lineal siguiente a la más alta del contexto.

Se debe tener la precaución de armar las tablas de paginación de forma tal que la dirección lineal de inicio del contexto, sea única para todas las tareas, pero corresponda a direcciones físicas diferentes por cada una de ellas, las cuales deben tener atributos de supervisor y lectura/escritura. Debido a lo recién mencionado, no debe habilitarse el bit global para esta página.

Los registros pueden almacenarse en cualquier orden en el contexto, excepto los que el procesador pone en la pila de forma automática: SS, RSP, RFLAGS, CS, RIP. Como en general uno desea inicializar estos registros con valores determinados según la tarea, es muy importante escribir el mapa del contexto, teniendo en cuenta que los cinco registros mencionados van a quedar en las direcciones más altas (SS en la más alta).

En este caso, los pasos a seguir son:

  1. Mediante instrucciones PUSH, salvar en el contexto los registros de uso general y de segmento. Como en 64 bits no existe PUSH reg_segmento, hay que utilizar la siguiente secuencia: MOV AX,reg_segmento y luego PUSH RAX. Siempre se debe grabar en la pila valores de 8 bytes.
  2. Cargar el registro de segmento DS con el selector de datos de 64 bits de nivel de privilegio cero.
  3. Salvar RSP0 de la TSS en la pila.
  4. Si CR0.TS = 0 entonces se ejecutó el manejador de la excepción 7, por lo que los registros SIMD tendrán un valor diferente a lo que se encuentra en el contexto. De esta manera hay que salvar los nuevos valores de los registros mediante la instrucción FXSAVE pasándole como parámetro el puntero a la imagen de los registros SIMD dentro de ese contexto.
  5. Hallar el identificador de la tarea siguiente (nuevo valor de CR3) según la lógica del scheduler.
  6. Si este valor no coincide con el valor de CR3 actual, poner CR0.TS=1 (Task Switched flag), para que se dispare la excepción de punto flotante cuando se use una instrucción SSE.
  7. Cargar CR3 con el nuevo valor. A partir de este momento el procesador se encuentra ejecutando la nueva tarea, y RSP apunta al nuevo contexto.
  8. Indicar fin de interrupción al PIC (si el scheduler corresponde a un sistema operativo no cooperativo).
  9. Retirar el valor de RSP0 de la pila y ponerlo en el TSS.
  10. Cargar los registros de uso general y de segmento en el orden inverso al realizado en el primer paso. Como en 64 bits no existe POP reg_segmento, hay que utilizar la siguiente secuencia: POP RAX y luego MOV reg_segmento,AX.
  11. Ejecutar la instrucción IRETQ.

Manejador de la excepción 7

Esta excepción ocurre cuando el procesador ejecuta una instrucción de punto flotante o SIMD y el bit de control Task Switched (CR0.TS) està a '1' y CR0.EM = '0' (valor por defecto de este bit).

Los pasos a seguir son:

  1. Guardar en la pila todos los registros de uso general que se usen en el manejador.
  2. Poner a cero el bit CR0.TS para que no se produzca nuevamente esta excepción al restaurar los registros de punto flotante.
  3. Cargar en un registro de uso general el puntero a la imagen de los registros de punto flotante dentro del contexto de la tarea.
  4. Mediante la instrucción FXRSTOR usando como parámetro el registro incializado en el paso anterior, sobreescribir los registros SIMD con los valores que corresponden a esta tarea.
  5. Restaurar de la pila todos los registros de uso general usados en este manejador.
  6. Ejectuar la instrucción IRETQ.

Para tener en cuenta

Se debe diseñar el sistema de paginación con la precaución de que las estructuras de paginación de todas las tareas apunten a las zonas del kernel cuyo código debe ser accesible desde el contexto de ejecución de las tareas: manejadores de interrupción, servicios del kernel, etc. Obviamente las páginas correspondientes debe tener los permisos con el bit U/S adecuadamente configurados. Luego cada estructura de páginas debe contener descriptores de páginas exclusivas de la tarea en cuestión. De este modo se respeta el principio básico de protección de memoria entre tareas.

Otras consideraciones

Se propone usar el CR3 como identificador de tarea debido a que contiene la dirección física del PML4, que es único para cada tarea.

Task switched y el uso de los registros XMM: al conmutar de tarea en modo protegido, se setea automáticamente el bit CR0.TS denominado Task Switched. Como en IA-32e no se conmuta automáticamente la tarea, sino mediante el procedimiento descripto, hay que tocar este bit a mano. El objeto de esto es que si la nueva tarea usa los registros XMM se genera una excepción #XM, si el bit CR0.EM además está en '0', que es el default cuando arranca el microprocesador.

Se recomienda que un área del kernel sea accesible desde todas las aplicaciones para uso de los servicios.

Las entradas de las tablas que apuntan a la zona no paginada deben estar marcadas con el atributo G (Global) a '1' para que no se recarguen innecesariamente al cambiar el registro CR3.