Ejemplo de modo protegido

  1. Alpertron
  2. Microprocesadores de la línea Intel
  3. Ejemplo de modo protegido

por Dario Alejandro Alpern

Archivos necesarios

Transcripción

Hola. Mi nombre es Darío Alpern y hoy vamos a ver el ejemplo en modo protegido.

Video en modo texto

Como el ejemplo usa la pantalla, es necesario una breve introducción al video en modo texto.

En las PC, el video funciona en modo gráfico y en modo texto. Lo que vamos a ver ahora es modo texto.

En este modo, la pantalla posee 25 líneas de 80 columnas cada uno.

El sistema de video tiene una memoria RAM que es diferente de la RAM principal del sistema. Esta memoria es el buffer de video en el que el procesador escribe lo que el sistema de video debe mostrar en pantalla.

En el caso del modo texto, el buffer de video comienza en la dirección física 0xB8000. El buffer almacena primero la fila cero (fila superior), luego la fila uno y así sucesivamente hasta la fila 24, que es la fila inferior completando así 25 líneas.

Dentro de cada fila, se almacenan los 80 caracteres de izquierda a derecha.

Cada carácter ocupa dos bytes. El primero es el código ASCII del carácter y el segundo es el atributo donde se indica si el carácter parpadea y cuáles son los colores del fondo y del frente.

En este contexto, para el carácter que representa la letra "A", el frente son los puntos que forman la letra mientras que el fondo es el resto del rectángulo.

Enunciado del ejemplo

El ejemplo debe poder llenar las 25 filas de 80 caracteres. La fila superior contendrá la letra "A". La siguente fila, la letra "B" y así sucesivamente hasta la línea inferior, que contenga la letra "Y".

Se deberá usar atributo blanco sobre negro que tiene el código 0x07.

Usaremos registros como variables tales como números de fila, de columna, letra a mostrar y puntero.

Código fuente

Lo que queremos hacer ahora es editar el programa.

Hacemos CTRL++ para ver más grande, Apretamos F11 para ver los números de línea y acá podemos ver el código fuente.

En la línea 7 tenemos la directiva bits 16 que indica que lo que viene a continuación es código correspondiente a segmento de 16 bits. Eso es porque estamos en modo real.

Luego, viene la etiqueta inicio que es el destino del salto que se encuentra al final del segmento de la memoria ROM y luego un include, %include "init_pci.inc"

El archivo init_pci.inc contiene la inicialización del bus PCI y del sistema de video VGA.

Podemos abrirlo para ver qué contiene. Salimos del editor y ejecutamos kate init_pci.inc para verlo. Acá se acaba de abrir. La idea no es mostrar cómo funciona esto sino simplemente ver que el archivo es muy largo y no hace al ejercicio en sí. Entonces, estas 600 líneas más o menos, 681 líneas, son las que permiten inicializar el bus PCI y el sistema de video VGA.

Entonces, la idea es incluirlo directamente y lo que hace el nasm es, cuando llega la línea 10 automáticamente reemplaza el include por las 681 líneas. Es como si yo hubiese escrito 681 líneas en ese lugar.

Luego de eso, pongo un "magic breakpoint" con xchg bx,bx. La idea de esto es poder parar la ejecución luego de la inicialización del bus PCI y del sistema de video VGA.

Y luego hago un salto para saltearme toda la zona de datos que viene más abajo. O sea, hago un salto a pasaje_a_modo_protegido. que está definido más abajo.

Lo que vamos a ver a continuación es la definición de la GDT (Global Descriptor Table). Acá puse para optimización un align 8 que significa que lo que está a continuación va a estar en una dirección efectiva u offset múltiplo de 8.

Entonces arranca la GDT, y la GDT contiene descriptores. Cada descriptor ocupa 8 bytes. El primer descriptor no se utiliza entonces lo defino a cero. DQ sirve para definir 8 bytes, que es un define quadword. Se acuerdan que word son 16 bits, o sea 2 bytes. Entonces quadword son 8 bytes. Entonces, dejo 8 bytes libres que no me interesan.

Luego defino CS_SEL equ $-GDT Bueno, $-GDT vale 8, porque entre GDT y la definición de CS_SEL hay 8 bytes. Entonces CS_SEL vale 8, es el selector de code segment.

Luego de eso, está definido el descriptor correspondiente al segmento de código y la idea de este descriptor es que corresponda con un segmento de código que sea igual que en modo real, o sea que la base va a ser 0xFFFF0000 y el límite va a ser 0x0000FFFF, igual que en modo real, entonces vamos a ver cómo funciona esto. Como el límite se puede expresar dentro de los 20 bits que permite el descriptor, entonces la granularidad va a ser cero y el límite va a ser igual.

Entonces defino el límite con el DW (define word).

Luego vienen los dos bytes menos significativos de la base, a continuación, el siguiente byte más significativo, que tiene que ser 0xFF, o sea el tercer byte de la base, y luego los derechos de acceso que lo defino no en hexadecimal sino en binario un campo de bits, ven que acá está la "b". El sufijo "b" significa que el número está en binario.

Entonces, acá está el desgloce de la información: el bit 7 está encendido indicando que el segmento está presente, bits 6 y 5 están a cero indicando nivel de privilegio cero, o sea, máximo privilegio, bit 4 vale uno, para descriptores de segmento de código y datos, El bit 3 vale uno, para el caso de descriptores de código, el bit 2 vale cero porque es un segmento "no conforming", el bit 1 vale uno, porque está habilitado el permiso de lectura, el bit 0 de accedido vale cero, indicando que no se accedió al segmento.

Luego definimos el byte 6 de la siguiente manera: granularidad dijimos que vale cero, luego el bit "D" de default vale uno porque queremos segmento de 32 bits y luego límite 19 a 16 vale cero porque el bit 19 a 16 es el cero que está acá, entonces vale cero. Conclusión, el valor es 0x40.

Y por último tenemos la base 31 a 24 o sea el byte más significativo de la base tiene que estar a 0xFF.

Con esto definimos el descriptor de código. Ahora vamos a ver el descriptor de datos.

Para el descriptor de datos, yo quiero que sea un segmento flat. Los segmentos flat tienen base cero y límite 4 GB: 0xFFFFFFFF Entonces, decimos que como este límite supera el MB y termina en FFF ponemos granularidad a uno y el límite son FFFFF solamente. Acuérdense que el procesador agrega automáticamente FFF a la derecha.

A partir de lo que dijimos recién, definimos la parte baja del límite como 0xFFFF Luego viene la parte baja de la base, los dos bytes menos significativos valen cero, porque la base vale cero, El tercer byte de la base también vale cero, los derechos de acceso, que es un campo de bits, lo definimos de una manera similar que antes con el bit de presente a uno, DPL vale cero, el bit 4 indicando que es un descriptor de código o datos vale uno, el bit 3 es diferente, es cero porque son datos, el bit 2 vale cero porque es la dirección de expansión normal, el bit 1 vale uno porque está habilitada la lectura y la escritura, y el bit cero lo ponemos también a cero como dijimos que tenemos que hacer siempre con este bit.

Luego tenemos el byte 6 que indicamos granularidad uno, como dijimos más arriba el bit de default va a uno porque es 32 bits, y el límite 19-16 lo ponemos a F.

Y finalmente tenemos base 31 a 24 que es el byte más significativo de la base, lo ponemos a cero.

Luego definimos un símbolo que es el tamaño de la GDT, tam_GDT equ $-GDT. De esta manera, si yo por ejemplo agrego más descriptores tam_GDT va a ir incrementándose y no tengo que hacer ningún cálculo. Automáticamente, el ensamblador calcula el tamaño correcto del símbolo tam_GDT.

A continuación definimos la imagen de GDTR donde primero va el límite y después va la base. El límite es el tamaño de la GDT menos 1. La longitud del campo límite es de 16 bits y por eso se usa DW (define word). y luego pongo la base de la GDT que es la dirección lineal donde comienza la GDT, es 0xFFFF0000 más GDT, donde GDT, este símbolo indica el offset entonces le sumo la dirección lineal del arranque del segmento de código más ese offset, y me da justamente la dirección lineal de la GDT.

A continuación vemos pasaje a modo protegido.

Lo primero que hay que hacer es deshabilitar interrupciones como dijimos en la teoría.

Luego cargamos el registro GDTR apuntando a la imagen de GDTR que está más arriba, digamos por acá. Y fíjense que a la instrucción LGDT le agrego o32. Este o32 permite utilizar los cuatro bytes de la base. Si yo no usara o32, por un tema de compatibilidad con el 80286, utilizaría solo tres bytes de la base. Entonces no me sirve porque no puedo acceder a la parte superior porque en el byte más significativo tengo 0xFF. y si yo no usara o32 me pondría 00 ahí. Entonces necesito o32 sí o sí.

Una vez que está cargado el registro GDTR, pasamos a modo protegido poniendo a uno el bit cero del registro de control CR0.

Y ahora, lo que vamos a hacer es cambiar el segmento de código, para que apunte al segmento de código de modo protegido. el segmento de 32 bits que definimos en la GDT. Para eso hacemos un salto intersegmento donde el selector es CS_SEL que fue el definido más arriba en la GDT y el offset es inicio_32 que es el que está acá a continuación. Esto funciona porque la base del selector de código nuevo es igual a la base del segmento de código que estaba definido en modo real. Si no, habría que utilizar otro esquema diferente.

Una vez que hicimos el salto intersegmento, lo que viene a continuación es de 32 bits Como a continuación del salto intersegmento viene código de 32 bits, yo tengo que poner la directiva BITS 32 que es lo mismo que haber puesto use 32.

Entonces, defino la etiqueta inicio_32 y a partir de este momento ya puedo acceder al segmento de 32 bits de código.

A continuación cargo el registro DS con el selector que apunta al descriptor correspondiente al segmento flat de 4 GB de tamaño que arranca en la dirección lineal cero.

Y a continuación tengo la lógica para acceder al buffer de video.

Entonces, ¿cómo voy a hacer para operar con el buffer de video? Bueno, inicializo un registro que me indique la letra que voy a escribir. Por ejemplo el registro BL, lo cargo con la letra "A". Fíjense que el ensamblador, le puedo poner la letra "A" directamente y me lo traduce al ASCII correspondiente que es 0x41.

Luego de eso, cargo el número de fila en el registro CL y luego el puntero en 0xB8000. Fíjense que ahora que estamos en modo protegido podemos usar punteros que estén por arriba de los 64 KB. cosa que en modo real no se podía hacer. Entonces ESI apunta directamente a la dirección lineal donde se encuentra el buffer de video que coincide con la dirección física porque no está activada la paginación.

Luego, dentro de ciclo_externo vamos a definir la columna, la columna la arrancamos en cero, vamos a comenzar con la columna que está a la izquierda.

Y luego entramos en el ciclo que procesa cada carácter dentro de cada fila. Lo que hago es cargar en el puntero que apunta al buffer de video el carácter que quiero mostrar, la letra "A". Y a continuación cargo el atributo que es 0x07 que es blanco sobre negro. Fíjense que la cláusula byte es importante porque el operando a la derecha no tiene tamaño, es un número, no es un registro Entonces tengo que definir que la operación es de tipo byte.

Una vez que ya escribí el carácter y el atributo, incremento el puntero en dos lugares, luego incremento el número de columna y luego tengo que verificar si el número de columna coincide con el final de la fila. Cada fila tiene 80 columnas. Lo que quiero ver es si se terminaron las 80 columnas. Entonces, comparo contra 80 y si no es igual, JNE, "jump not equal", vuelvo al ciclo interno para terminar de completar las 80 columnas.

Una vez que terminó el ciclo de la fila, incremento el número de fila incremento el código ASCII del carácter, o sea la primera vez salta de "A" a "B", digamos, y luego hago la misma operación pero verificando si llegué a la última fila, o sea si llegué a la 25 y si no es igual, me voy al ciclo externo para seguir con la siguiente fila.

Una vez que están los caracteres puestos en el buffer de video. se ejecuta un JMP $ para que se cuelgue el código y no siga ejecutando más allá, porque ya terminó lo que debía hacer el enunciado.

Luego de eso, seguimos con lo que hacíamos siempre, que es hacer el primer relleno.

Después, esto es importante el bits 16 porque el JMP que va a continuación es un JMP de modo real. Por lo tanto, tiene que estar precedido por bits 16. Porque veníamos de código de 32 bits, antes. Entonces ejecutamos el JMP que es el que se ejecuta cuando arranca el procesador.

Y luego ponemos el segundo relleno para completar la imagen de ROM de 64 KB. Salimos del editor de texto.

Ejecución del programa de ejemplo

Vamos a compilar.

Fíjense que el archivo de salida es mi_rom.bin y la idea de eso es, justamente, modificar el archivo bochs.cfg para que la imagen de rom siempre sea mi_rom.bin así no tengo que modificar todas las veces el bochs.cfg.

Entonces, compilo, no tiró ningún error. Y ahora vamos a correr el Bochs para ver qué ocurre. Bien, ahí arrancó el programa.

Estamos en el JMP correspondiente a la instrucción inicial. Hacemos "step". Y ahora tenemos otro JMP que sirve para saltearme toda la zona de datos que viene a continuación.

Lo que vamos a hacer es seguir hasta el magic breakpoint que habíamos puesto después de la inicialización de PCI y VGA apretando el botón "continue" Ahí está. El magic breakpoint salteó toda la inicialización del bus PCI y del sistema de video VGA.

Fíjense que a continuación del JMP se encuentra la GDT, que el Bochs lo interpreta como si fueran instrucciones ejecutables y no lo son. Fíjense que tienen cualquier cosa incluyendo instrucciones inválidas.

Le doy "step" y salteo lo que es la GDT.

Ahora va a cargar el registro GDTR con imagen_GDTR que se encuentra en la dirección efectiva 0x1340,

Ahora aprieto el botón "step" y ahora podemos ver en el panel de la izquierda yendo hacia abajo información del GDTR. Entonces dice: GDTR apunta a la GDT entonces la GDT arranca en 0xFFFF1328 y el límite es 0x17, que es el valor esperado.

Yo puedo ver los valores que están en la GDT viendo con Linear Memdump por ejemplo.

Agrandamos el panel de la derecha para ver más información. La GDT arranca en la segunda mitad de la primera línea que dice address 0xFFFF1320, ya que cada línea muestra 16 bytes. Acá vemos el descriptor nulo 8 bytes a cero Después a continuación en 1330 tenemos el primer descriptor, que es el descriptor de código, con los bytes que ingresamos y después el segundo descriptor que es el descriptor de datos. Obviamente, no está muy inteligible.

Entonces, lo que podemos hacer para entender lo que sucede es poner info gdt.

info gdt me devuelve tres descriptores. GDT[0], GDT[1] y GDT[2].

El primer descriptor es el nulo, así que no lo puede decodificar.

El descriptor 1 es el de código, con base 0xFFFF0000 y límite 0x0000FFFF y en los atributos se puede ver que es de 32 bits.

El descriptor 2 es de datos que apunta a un segmento flat que tiene base cero y límite 4 GB y se puede escribir.

Ajusto el panel central para que se vean las instrucciones y ahora vamos a ingresar a modo protegido. Ahora teóricamente ya estamos en modo protegido porque CR0 le habilitamos el bit menos significativo ven que acá hay un uno.

Y ahora vamos a saltar al selector 8 y offset 0x135D Entonces ahora va a saltar a otro segmento pero en realidad el segmento de código ese coincide con el segmento de código que venimos corriendo. Cuando aprieto el botón "step" continúa en la instrucción siguiente que está en la dirección efectiva 0x135D Y las instrucciones ahora se ven bien porque el Bochs desensambla el código correspondiente a segmento de 32 bits.

Ahora vamos a cargar el valor 10 en el registro DS.

Ahora vamos a ver qué contienen los registros de segmento escribiendo sreg en el panel inferior, que muestra para cada uno de los seis registros de segmento la base, el límite y los atributos. Vamos a ver el contenido de los registros CS y DS.

En el caso de CS, tenemos que el selector es 8 que es lo que se cargó con el JMP intersegmento, después, la base es el que pusimos nosotros, el límite también, y los atributos que habíamos visto en la GDT.

En el caso de DS el selector es 0x10 y tenemos base y límite esperados para un segmento flat.

Luego, seguimos ya tenemos CS y DS Entonces podemos seguir.

BL se carga con 0x41 como vemos acá que el ASCII de la letra "A".

Después, el número de fila, que se almacena en el registro CL, se carga con cero.

ESI apunta a 0xB8000. Ya se cargó.

Después CH se carga con cero, que es el número de columna.

Ahora el procesador va a escribir el primer carácter en el buffer de video que corresponde al extremo superior izquierdo de la pantalla.

Ponemos el carácter "A" y el atributo blanco sobre negro correspondiente al primer carácter.

Si yo quiero ver que fue lo que escribió en pantalla, hay que ir a la ventana de salida de video del Bochs con los botones ALT-TAB hasta llegar a esa ventana. Ahí se ve la letra "A".

Una vez que vimos que la letra "A" se colocó correctamente Hacemos "step", el puntero se incrementó a 0xB8002 Ahora apunta a la fila 0, columna 1, Ahora incrementamos el número de columna, verifico si CH vale 80 o 0x50 Obviamente no vale Fíjense que el flag de cero está a cero Significa que no es igual porque CH vale uno y yo comparé contra 0x50 entonces no son iguales.

Si yo quiero ejecutar hasta completar la primera línea, me posiciono en la primera instrucción después de ciclo_interno y hago doble click y se ve la instrucción que marca el punto de parada en rojo. Luego, aprieto el botón de "continue" para que se llene la línea.

Para verificar que se llenó la línea superior, vamos a ver la salida del video simulado del Bochs. Y acá se pueden ver todas letras "A" que son 80 en la primera línea.

Si yo le doy "continue", me llena la segunda línea que tendría que ser con letras "B". Vamos a ver si eso anda correctamente Ahí se ven las letras "A"y "B".

Mientras tanto el puntero sigue incrementándose. Cada fila son 160 bytes porque son 80 columnas por 2.

Lo que vamos a hacer ahora es sacar el punto de parada y continuar hasta que se cuelgue que es el JMP que está en el 1387. Continuamos y le damos "break" Se para en el JMP y acá podemos ver en la pantalla simulada del Bochs todas las letras de la "A" a la "Y" en toda la pantalla completa, que es lo queríamos hacer.

Creo que con esto es suficiente por ahora. Hasta luego.