Tabla de Descriptores Globales (GDT)
Memoria Virtual y el Espacio Virtual de Direcciones
Muchas veces la cantidad de espacio de memoria RAM restante en nuestras computadoras es mínima. Por ejemplo, si tenemos 4 GB de memoria RAM instaladas en nuestra computadora, pero si de esos 4 GB solo nos quedan 200 mb de espacio restante y tratamos de ejecutar un programa que necesita 250 mb de RAM la ejecución si es posible. ¿Por qué? Esto ocurre debido a la memoria virtual, una capacidad que casi todos los sistemas operativos modernos tienen. Lo que pasa es que el kernel busca en el RAM y generalmente pasa partes que no se han usado en mucho o tiempo o que no esta en uso y luego las copia a una parte del disco duro y cuando se vuelven a requerir son copiadas de nuevo a la memoria RAM. Esto es una función que le hace sentir al usuario sentir que tiene memoria RAM infinita, ya que nunca le va a salir que se ha terminado la memoria RAM. Una de las cosas que hace esto posible es el Espacio Virtual de Direcciones. Cuando la computadora escribe y lee cosas de la memoria, en realidad no está viendo el espacio de direcciones físico, sino uno virtual. Partes de este espacio de direcciones están enlazados a la memoria física y partes no. Cuando un programa trata de acceder a una parte del espacio que no esta directamente enlazado a la memoria, es detenido (luego veremos cómo más adelante en la investigación). Este espacio de direcciones es esencial para el funcionamiento de programas que necesitan estar siempre en una parte de la memoria cada vez que se ejecutan, ya que con este tipo de memoria puede pedir estar en la dirección 0x080482a0 de la memoria, y creer que esta en ella cuando en realidad esta en la dirección 0x1000000 de la memoria física. Además de esto, un proceso no puede tratar de usar o leer la memoria, datos o código de otro proceso de esta manera. Sin embargo este tipo memoria depende del hardware al 100% y no puede ser emulada por software. En la arquitectura x86, la parte del hardware que se encarga de manejar este tipo de memoria es el MMU (Memory Management Unit en inglés o Unidad de Manejamiento de Memoria en español). En esta arquitectura existen 2 formas de usar memoria virtual: segmentación y paging (paginación en español). Cada vez el método de segmentación se vuelve más obsoleto, debido a que paging es el método más nuevo y tiene más ventajas. Pero sin embargo el método de segmentación es esencial para la arquitectura x86 y no hay manera de no implementarlo. Además, una de las cosas que podemos hacer con segmentación pero no con paging es establecer el anillo que queremos utilizar. Existen 4 anillos que el kernel puede utilizar. Dependiendo el anillo que se esté usando, las instrucciones que el procesador puede ejecutar cambian.
- Anillo 0: Este es el anillo que se usa en modo kernel, es el más privilegiado y puede ejecutar todas las instrucciones.
- Anillo 3: Este es el anillo que se usa en modo usuario y es el menos privilegiado. Por seguridad, muchas de las instrucciones como cli, sti _y halt_ estan restringidas.
- Anillo 1 y 2: Por lo general, estos anillos no son utilizados. Sin embargo tienen un nivel de restricción menor y por lo tanto acceso a más instrucciones. Pueden ser utilizados para cargar módulos o drivers.
Segmentación
El método que vamos a implementar primero es segmentación. En este método la memoria es evaluada en segmentos, cada uno con una función específica y permisos diferentes. En Sierra Kernel decidí usar un modelo de memoria llamado flat memory model en inglés. En este modelo todos los segmentos empiezan en 0 (el inicio de la memoria) y se extienden hasta 0xFFFFFFFF (el final de la memoria). En mi opinion este modelo es más efectivo y de hecho es necesario en la arquitectura x86_64, pero lo malo es que se puede re-escribir los datos de un segmento ya que no hay una división entre ellos. La otra opción que puede haber es darle un espacio asignado a cada segmento. Las conceptos básicos de la segmentación son los siguientes:
- Segmento: Una parte continua de la memoria con las mismas propiedades.
- Descriptor: Le da las propiedades al segmento.
- Registro de Segmento: Un registro del CPU especifico al segmento para uso especial.
- Entrada: Cada segmento representa una entrada en la GDT y es el conjunto de todas las propiedades de un segmento.
Algo que había olvidado mencionar, es que la única función de la segmentación y de paging no es solo proveer con memoria virtual al usuario; sino también es usada como medida de protección, ya que define que permisos tiene cada parte de la memoria, entonces cada vez que un proceso quiere acceder a una parte de la memoria, el CPU evalúa si tiene el permiso de hacerlo y si no lo termina. También define si una parte de la memoria es ejecutable o si son puros datos. Por todo lo anterior, si un proceso, atacante o usuario está tratando de hacer algo que no debe, el CPU tiene la oportunidad de detenerlo. La segmentación se lleva a cabo usando una Tabla de Descriptores Globales, la cual es una tabla que contiene descriptores y entradas con las propiedades de un segmento. Una entrada en la GDT (por sus siglas en inglés) consiste de los siguiente:
- limit_low: Los primeros 16 bits del limite (donde termina el segmento).
- base_low: Los primeros 16 bits de la base (donde empieza el segmento).
- base_middle: Los siguientes 8 bits de la base.
- access: Algunos parámetros y permisos del segmento.
- granularity: Otros parámetros y permisos del kernel.
- base_high: Los ultimos 8 bits de la base
Los parámetros de granularity y access son los siguientes:
Parametros de Access | Definición | Parametros de Granularity | Definición |
---|---|---|---|
P | El segmento esta presente? (SI = 1). | G | Granularidad (0 = 1 byte, 1 = 4 kbyte) |
DPL | Nivel de privilegio del descriptor (Anillos 0-3). | D | Tamaño del operando (0 = 16 bits, 1 = 32 bits). |
DT | Tipo de descriptor. | 0 | Siempre debe de ser 0. |
Type | Tipo de segmento (código o datos). | A | Disponible para el uso del sistema (0 siempre). |