Capítulo 7: Del modo real al modo protegido


Ha llegado el momento de abandonar los 16 bits. En la introducción a este bloque vimos que entre las funciones del bootloader se encuentra la de crear un entorno favorable para el kernel. Con esa frase, nos referimos principalmente a pasar a un entorno de 32 bits. Para eso, es indispensable pasar del modo real del procesador (que hemos estado usando todo este tiempo) al modo protegido.

En el capítulo 3 se explican las características del modo real del procesador. Por resumir: la mayoría de los valores están limitados a 65.535 y solo son accesibles los primeros 64 KB de la RAM, pero a cambio podemos acceder a los BIOS interrupts. Ahora que hemos pasado a la stage2 y tenemos más espacio, podemos ocuparnos de dejar el modo real y pasar al modo protegido.

Pasar a los 32 bits es una mejora en casi todos los sentidos: podremos utilizar código escrito en C para nuestro bootloader, podremos acceder hasta los primeros 4 GB de la RAM... sin embargo, hay un problema: no podremos usar los BIOS interrupts.

En este capítulo veremos cómo pasar del modo real al modo protegido, y en el siguiente veremos cómo podemos volver del modo protegido al modo real, en caso de que nos haga falta usar un BIOS interrupt.

Tabla de contenidos


  1. Paso 1: Habilitar la linea A20
  2. Paso 2: Cargar la Global Descriptor Table (GDT)
  3. Paso 3: Activar la PE flag
  4. Paso 4: Saltar a un segmento de código de 32 bits
  5. Paso 5: Prepararnos para el modo protegido
  6. Extra: poner un mensaje desde el modo protegido
  7. Resumen
  8. Ejemplo descargable

Paso 1: Habilitar la linea A20


¡Jajaja! ¡Te mentí! Ese no es el primer paso. En realidad, el primer paso es deshabilitar los interrupts mediante la instrucción cli. Hacemos esto porque es esencial que ningún evento externo interrumpa el paso de un modo del procesador a otro. Si el proceso se ve interrumpido, las cosas pueden acabar muy mal.

Pero bueno, una vez hemos hecho eso tenemos que habilitar la línea A20. ¿Y qué es eso? Bueno, vamos a intentar explicarlo sin meternos a demasiadas complicaciones relacionadas con el hardware. Mi principal fuente va a ser el artículo de Wikipedia, que sintentiza muy bien toda la información, pero aún así vas a tener que tener paciencia.

¿Qué es la línea A20?

La principal característica de un procesador de 16 bits es que solo puede acceder a direcciones de memoria que ocupen 16 bits o menos. Es decir, solo puede acceder a los primeros 64 KB de cualquier memoria. Esta limitación supuso un problema para Intel, que desarrolló un sistema para que sus procesadores pudieran acceder a todo el primer MB de cualquier memoria. Es decir, que pudieran acceder a direcciones de hasta 20 bits.

Así es como nació la segmentación de memoria, ese sistema que hemos estado usando, por el cual las direcciones en la RAM se escriben siguiendo el esquema SEGMENTO:OFFSET. Esto se contrapone a la memoria lineal, en la cual tan solo escribes el OFFSET para acceder a cualquier dirección.

La dirección del código que está siendo procesado en el momento actual se almacena en una memoria especial, que consta de una línea eléctrica para cada bit. Por ejemplo, si la dirección de memoria actual es 00110101 y tenemos un procesador de 8 bits, las lineas eléctricas estarán encendidas o apagadas según su bit, de esta forma:

Estas lineas, vistas de izquierda a derecha, se llamarían A7, A6, A5, A4, A3, A2, A1 y A0. Los procesadores de 20 bits que desarrolló Intel tenían líneas de la 0 a la 19. Cuando se añadieron nuevas lineas para acceder a más memoria, IBM decidió introducir una puerta lógica llamada Gate-A20 que permitía desactivar la línea 20, y así emular el funcionamiento de los procesadores anteriores para mantener la compatibilidad con programas antiguos.

En los ordenadores de la arquitectura x86 (como el nuestro) la línea A20 viene desactivada por defecto. ¿Y cómo la activamos? Pues... interactuando con el controlador de teclado, porque en un principio se vinculó la Gate-A20 a ese controlador.

Preparación previa

Antes de nada, un recordatorio: estamos en la stage2 (en concreto, en el archivo src/stage2/entry.asm), y el único código que contiene actualmente sirve para poner un mensaje en la pantalla que hemos llamado msg_boot, cuyo valor es "Hola".

Vamos a preparar nuestra stage2 para hacer cosas más avanzadas. Para empezar, volveremos a asegurarnos de que todos los segmentos tienen el valor esperado (en este caso, 0), y también cambiaremos el nombre de nuestra función print a rmode_print y vamos a ponerle la directiva [bits 16] para dejar claro que solo funciona en modo real.

El código de nuestro archivo entry.asm debería parecerse a este:

bits 16

; SECCIÓN DEL EJECUTABLE: .entry
section .entry

; Variables importadas de linker.ld
extern __bss_start
extern __end

; Entry point (exportado para linker.ld)
global entry

entry:
    ; Poner mensaje de bienvenida a la 'stage2'
    mov si, msg_boot
    call rmode_print

    ; Copiar el número guardado en DL a una variable propia.
    mov [boot_drive], dl

    ; Configuramos el stack, partiendo de que ds = 0.
    mov ax, ds
    mov ss, ax
    mov sp, 0xFFF0
    mov bp, sp

    ; El código seguirá por aquí...

    ; Una vez acabe el código, detener el sistema
    jmp halt

; REAL MODE PRINT
rmode_print:
    [bits 16]
    mov ah, 0x0E
    mov bl, 0

rmode_print_loop:
    lodsb
    cmp al, 0
    jz rmode_print_ret

    int 0x10

    jmp rmode_print_loop

rmode_print_ret:
    ret

; DETENER SISTEMA
halt:
    hlt
    jmp halt

; VARIABLES
msg_boot:
    db "Hola", 0x0A, 0x0D, 0

boot_drive:
    db 0

Una vez realizados estos cambios, podemos expandir el código de nuestra stage2 de forma segura. Siguiendo los pasos ya explicados, vamos a desactivar los interrupts y crear una función en la que activaremos la línea A20. El siguiente código se debe insertar donde se encuentra la anotación "El código seguirá por aquí...":

cli
call a20_enable

Activar (ahora sí) la línea A20

En esta nueva función tendremos que seguir los siguientes pasos:

  1. Desactivar el teclado
  2. Mandar un comando para leer el estado del teclado
  3. Guardar el valor recibido en el stack
  4. Mandar un comando para escribir el estado del teclado
  5. Copiar el estado anterior, aunque con el segundo bit encendido. Ese segundo bit es el que determina si la línea A20 está activada o no.
  6. Guardar los cambios.
  7. Volver a activar el teclado.

A todos estos pasos, se les añade el hecho de que cuando queremos escribir o leer a un controlador, tenemos que esperar a que dicho controlador responda. Por ello, también tendremos que crear son los métodos a20_waitinput y a20_waitoutput.

¿Y cómo mandamos comandos y leemos respuestas del controlador de teclado? Pues para eso tendremos que comunicarnos con los puertos de comandos y de datos del controlador de teclado, usando las instrucciones in y out. Para una mayor legibilidad, vamos a apuntar las IDs de cada puerto en nuestro programa usando la directiva equ, tal que así:

keyboard_port_data equ 0x60
keyboard_port_cmd equ 0x64

Los comandos que vamos a enviar también tienen IDs numéricas. De nuevo, las apuntamos en la parte del programa que mejor nos venga, usando la directiva equ:

keyboard_cmd_disable equ 0xAD
keyboard_cmd_enable equ 0xAE
keyboard_cmd_readoutput equ 0xD0
keyboard_cmd_writeoutput equ 0xD1

A continuación, definimos las funciones que se encargarán de esperar a que el controlador de teclado nos permita escribir datos o nos devuelva los que le hemos solicitado. Esto lo haremos comunicándonos en bucle con keyboard_port_cmd hasta que el registro al refleje los cambios esperados:

a20_waitinput:
    [bits 16]
    ; Recibir datos de 'keyboard_cmd_port' hacia 'al'
    in al, keyboard_port_cmd

    ; Esperar hasta que su segundo bit (input buffer) sea 0.
    test al, 2
    jnz a20_waitinput
    ret

a20_waitoutput:
    [bits 16]
    ; Recibir datos de 'keyboard_cmd_port' hacia 'al'
    in al, keyboard_port_cmd

    ; Esperar hasta que su primer bit (output buffer) sea 1.
    test al, 1
    jz a20_waitoutput
    ret

Ahora que ya tenemos todo lo necesario, vamos a traducir los pasos que hemos escrito antes a Assembly para crear nuestro método a20_enable. En todos los casos, esperaremos a que el controlador esté disponible, escribiremos o leeremos los datos necesarios, y pasaremos al siguiente paso:

a20_enable:
    [bits 16]

    ; 1. Desactivar el teclado
    call a20_waitinput
    mov al, keyboard_cmd_disable
    out keyboard_port_cmd, al

    ; 2. Mandar cmd para leer estado del teclado a 'keyboard_port_data'.
    call a20_waitinput
    mov al, keyboard_cmd_readoutput
    out keyboard_port_cmd, al

    ; 3. Leer 'keyboard_port_data' hacia 'al' y guardarlo en el stack.
    call a20_waitoutput
    in al, keyboard_port_data
    push eax

    ; 4. Mandar cmd para escribir el estado del teclado en 'keyboard_port_data'
    call a20_waitinput
    mov al, keyboard_cmd_writeoutput
    out keyboard_port_cmd, al

    ; 5. Recuperar estado del teclado (guardado en el stack) y encender
    ; su segundo bit, que indica si la línea A20 está activada o no.
    call a20_waitinput
    pop eax
    or al, 2

    ; 6. Escribir el estado modificado a 'keyboard_port_data'
    out keyboard_port_data, al

    ; 7. Volver a activar el teclado.
    call a20_waitinput
    mov al, keyboard_cmd_enable
    out keyboard_port_cmd, al
    call a20_waitinput
    ret

Paso 2: Cargar la Global Descriptor Table (GDT)


Lo siguiente que vamos a poner en nuestra funcion principal (entry), después de haber llamado a a20_enable y haber vuelto, es esta línea:

call gdt_load

El siguiente método que escribiremos servirá para cargar la GDT o Global Descriptor Table. De nuevo, nos encontramos con otro concepto que no hemos tocado en ningún punto del manual... déjame explicarte antes de seguir.

¿Qué es la GDT?

Usando la terminología de Intel, un segmento es una sección de la memoria que tiene unas propiedades concretas (p. ej.: si solo ciertos procesos pueden acceder a dicho segmento, o si dentro del segmento se almacenan datos o código ejecutable).

La GDT define los segmentos de memoria que usaremos en nuestro bootloader y sistema operativo. Cada entrada de la tabla ocupa 8 bytes y puede contener un descriptor de segmento, un Task State Segment, una Local Descriptor Table (LDT) o una call gate. Por ahora, a nosotros solo nos interesan los descriptores de segmento (o segment descriptors). Su estructura es la siguiente:

0x3F 0x3E 0x3D 0x3C 0x3B 0x3A 0x39 0x38 0x37 0x36 0x35 0x34 0x33 0x32 0x31 0x30
base flags limit
0x2F 0x2E 0x2D 0x2C 0x2B 0x2A 0x29 0x28 0x27 0x26 0x25 0x24 0x23 0x22 0x21 0x20
access byte base
0x1F 0x1E 0x1D 0x1C 0x1B 0x1A 0x19 0x18 0x17 0x16 0x15 0x14 0x13 0x12 0x11 0x10
base
0xF 0xE 0xD 0xC 0xB 0xA 0x9 0x8 0x7 0x6 0x5 0x4 0x3 0x2 0x1 0x0
limit

Nota importante: algunos valores están desperdigados por la tabla, por lo que conviene pararse bien a mirar su estructura antes de experimentar con ella. Por dar un ejemplo más claro, en la tabla mostrada a continuación:

0x3F 0x3E 0x3D 0x3C 0x3B 0x3A 0x39 0x38 0x37 0x36 0x35 0x34 0x33 0x32 0x31 0x30
1 1 1 0 0 0 0 0 0 0 1 1 1 1 0 1
0x2F 0x2E 0x2D 0x2C 0x2B 0x2A 0x29 0x28 0x27 0x26 0x25 0x24 0x23 0x22 0x21 0x20
0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0
0x1F 0x1E 0x1D 0x1C 0x1B 0x1A 0x19 0x18 0x17 0x16 0x15 0x14 0x13 0x12 0x11 0x10
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0xF 0xE 0xD 0xC 0xB 0xA 0x9 0x8 0x7 0x6 0x5 0x4 0x3 0x2 0x1 0x0
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Las partes del descriptor serían las siguientes:

La variable base define el byte en el que empieza un segmento, y limit define su tamaño en bytes. Las demás características de un segmento se definen en los bits del access byte y las flags.

El access byte define la siguiente información (siguiendo el esquema de la OS Dev Wiki):

0x7 0x6 0x5 0x4 0x3 0x2 0x1 0x0
P
Present bit
Indica si el segmento es válido (1) o no (0).
DPL
Descriptor Privilege Level bit
Nivel de privilegios del segmento, del 0 (kernel) al 3 (programas de usuario).
S
Descriptor Type bit
Define si es un segmento de sistema (0) o un segmento de código/datos (1).
E
Executable bit
Define si es un segmento de datos (0) o de código ejecutable (1).
DC
Direction/Conforming bit
En segmentos de código: define si el código puede ejecutarse solo desde el nivel definido en el DPL (0) o desde cualquiera igual o inferior (1).

En segmentos de datos: define si el segmento crece normal (0) o al revés (1).
RW
Readable/Writable bit
En segmentos de código: define si el segmento se puede leer (1) o no (0). La escritura nunca es posible.

En segmentos de datos: define si en el segmento se puede escribir (1) o no (0). La lectura siempre es posible.
A
Accessed bit
La CPU le asigna el valor 1 al acceder a él.

Es recomendable asignarle manualmente un 1 desde el principio, porque si la CPU intenta modificar este bit en segmentos de solo lectura, provocará una violación de segmento.

Puedes pasar el ratón por encima de la tabla para ver una descripción más detallada de cada elemento.

Las flags disponibles son las siguientes:

0x3 0x2 0x1 0x0
G
Granularity flag
Indica si el tamaño de segmento definido en limit se expresa en unidades de 1 byte (0 o de 4 KB (1).
DB
Size flag
Define si el segmento es de 16 bits (0) o de 32 bits (1).
L
Long-mode code flag
En segmentos de código: define si el segmento contiene instrucciones de 64 bits (1) o no (0). La size flag (DB) debe ser 0 si esta está activada.

En cualquier otro caso: debe dejarse desactivada (0).
Reservado

Puedes pasar el ratón por encima de la tabla para ver una descripción más detallada de cada elemento.

Cómo escribir nuestra GDT

Teniendo esto en cuenta, ¡ya podemos definir nuestra propia GDT! Por ahora, crearemos una tabla muy simple, algo insuficiente para un sistema de uso general, como esta:

gdt_start:
    ; Primera entrada: debe estar vacía
    dq 0

    ; Todas las entradas van del byte 0 al máximo en su caso.

    ; Segunda entrada: 32-bit CODE segment (0x08)
    dw 0xFFFF
    dw 0
    db 0
    db 10011010b    ; Access: +P, DPL=0. +S, +E, -DC, +RW, -A
    db 11001111b    ; Flags: +G, +DB, -L
    db 0

    ; Tercera entrada: 32-bit DATA segment (0x10)
    dw 0xFFFF
    dw 0
    db 0
    db 10010010b    ; Access: +P, DPL=0. +S, -E, -DC, +RW, -A
    db 11001111b    ; Flags: +G, +DB, -L
    db 0

    ; Cuarta entrada: 16-bit CODE segment (0x18)
    dw 0xFFFF
    dw 0
    db 0
    db 10011010b    ; Access: +P, DPL=0. +S, +E, -DC, +RW, -A
    db 00001111b    ; Flags: -G, -DB, -L
    db 0

    ; Quinta entrada: 16-bit DATA segment (0x20)
    dw 0xFFFF
    dw 0
    db 0
    db 10010010b    ; Access: +P, DPL=0. +S, -E, -DC, +RW, -A
    db 00001111b    ; Flags: -G, -DB, -L
    db 0

Esta tabla describe 2 entradas de 16 bits y 2 de 32 bits. En ambos casos, la primera es de código y la segunda de datos. Ahora, definiremos el GDT descriptor, que contiene la información necesaria para cargar la GDT.

Todo el espacio del disco es accesible desde todos los segmentos. Hacemos esto para simplificar las cosas, aunque dependiendo de nuestra organización posterior, puede ser que nos acabe interesando cambiar los límites de los segmentos.

Definir el GDT descriptor y cargar la GDT

Por suerte, el GDT descriptor es mucho más simple: se trata de una estructura de 6 bytes. Los 4 primeros definen la base de la tabla (es decir, el byte en el que empieza), y los 2 siguientes definen el tamaño de la tabla en bytes.

Si colocamos el descriptor justo debajo de nuestra GDT, es tan fácil como hacer esto:

gdt_desc:
    dw gdt_desc - gdt_start - 1    ; Tamaño (2 bytes)
    dd gdt_start                   ; Base (4 bytes)

Con todo esto ya declarado, solo nos queda completar nuestra función gdt_load, en la que cargaremos nuestra tabla usando la instrucción lgdt, y, como argumento, incluiremos un puntero hacia nuestro GDT descriptor.

gdt_load:
    [bits 16]
    lgdt [gdt_desc]
    ret

Paso 3: Activar la PE flag


La PE flag (Protected Enable Flag) es una de las variables que se guardan en el primer registro de control, llamado cr0. Ocupa tan solo un bit (el más bajo de todo el registro), e indica al procesador si estamos en modo protegido (1) o no (0).

Este paso es muy simple. Tenemos que:

Bueno, así dicho parece más difícil de lo que es. Vamos a hacerlo directamente en Assembly:

mov eax, cr0
or al, 1
mov cr0, eax

¡Y ya está! Después de la explicación de la GDT esto se debe sentir como un soplo de aire fresco.

Paso 4: Saltar a un segmento de código de 32 bits


Ahora que tenemos nuestra GDT cargada, tenemos que tener cuidado para no producir violaciones de segmento (es decir, acceder a una parte de la memoria no disponible, o intentar ejecutar código desde desde un segmento de datos). Es importante tener claro qué segmentos tenemos declarados en nuestra GDT y qué propiedades tiene cada uno.

A nosotros nos interesa ejecutar código de 32 bits, y para ello tendremos que saltar a un segmento de código de 32 bits. El único que tenemos declarado está en el offset 0x08 de nuestra GDT. Así que para ejecutar un far jump hacia ese segmento tendríamos que ejecutar la siguiente instrucción:

jmp dword 0x08:pmode32

; Nota: el nombre 'pmode32' es arbitrario, podemos llamar a la siguiente sección como queramos.

A partir de aquí, el modo del procesador cambiará a modo protegido de 32 bits, y saltaremos hacia el símbolo pmode32 de nuestro código, que declararemos así:

pmode32:
    [bits 32]
    ; nuestro código...

Paso 5: Prepararnos para el modo protegido


Hemos cambiado el code segment (cs) al segmento 0x08, pero el segmento de datos (ds) y el del stack (ss) no los hemos actualizado. En nuestra GDT, el segmento de datos de 32 bits está en el offset 0x10, así que para evitar fallos nos conviene preparar los segmentos tal que así:

; En la sección 'pmode32'

mov ax, 0x10
mov ds, ax
mov ss, ax

Otra cosa que seguramente nos de problemas es que tenemos varios datos guardados sin inicializar en la sección .bss de nuestro proceso. Al cambiar de 16 bits a 32 bits, conviene hacer limpia para evitar lecturas erróneas. Para ello, vamos a pasar por cada byte de la sección y dejarlo en 0.

Para hacer esto, utilizaremos la instrucción stosb, que copia en el byte al que apunte edi el contenido de al y suma 1 al valor de edi. También usaremos la instrucción rep, que repite una instrucción el número de veces indicado en ecx... Es más claro verlo en código.

mov edi, __bss_start       ; edi = comienzo de BSS
mov ecx, __end             ; ecx = final del proceso
sub ecx, edi               ; ecx = tamaño de BSS
mov al, 0                  ; al = 0
cld                        ; Direction flag = adelante
rep stosb                  ; memcpy(edi, al, ecx);

A partir de aquí, ya no deberíamos tener problemas de memoria ni violaciones de segmento si hacemos las cosas bien. Como ejemplo, en la siguiente sección vamos a poner en el terminal un mensaje desde el modo protegido.

Extra: poner un mensaje desde el modo protegido


¿Recuerdas cómo poníamos mensajes cuando estábamos en modo real? Usábamos el INT 10h/AH=0Eh para poner cada caracter en el terminal. Ahora, en el modo protegido, no podemos acceder a la parte de la memoria en la que está escrito el código de los BIOS interrupts, así que... no podemos hacer eso. Tenemos que implementar la función nosotros mismos.

El buffer VGA

Nuestro procesador se encuentra ahora mismo en un modo de vídeo que se basa en un buffer VGA. Es decir, existe una sección de la memoria, que abarca desde el byte 0xA0000 hasta el byte 0xBFFFF en la que se define cada uno de los píxeles visibles en la pantalla. Si escribimos en esta región de la memoria, podemos modificar los contenidos de la pantalla.

Dentro de esa región de la memoria existen varias partes distintas. A nosotros nos interesa la sección que abarca desde el byte 0xB8000 hasta el byte 0xB8FA0. Es un área de 4000 bytes que en la que está escrito cada caracter de la pantalla, su color de fondo y su color de frente, formando una tabla de 80 carácteres por 25 líneas.

Para explicarlo, vamos a imaginar que la cuadrícula es de 8 caracteres por 4 lineas, formando esta tabla:

 
 
 
 

Cada caracter ocupa 2 bytes, que se distribuyen tal que así:

0xF 0xE 0xD 0xC 0xB 0xA 0x9 0x8 0x7 0x6 0x5 0x4 0x3 0x2 0x1 0x0
caracter fondo frente

De izquierda a derecha, el primer byte almacena la ID en código ASCII del caracter que queramos mostrar (0x41 para la A, 0x42 para la B, etc.). El segundo byte almacena en sus primeros 4 bits la ID del color de fondo, y en sus últimos 4 bits la ID del color de frente. La tabla a continuación muestra la ID de cada color:

0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xA 0xB 0xC 0xD 0xE 0xF

Por lo tanto, si definimos una estructura de 2 bytes con los siguientes datos:

0x1 0x0
0x4D 0x1F

Estaríamos definiendo la letra M mayúscula con un color de fondo azul y un color de frente blanco, como esta: M.

Debido a que los datos se deben guardar siguiendo la convención little endian, los bytes separados se escribirían 0x4D, 0x1F, pero al escribirlos como una word (2 bytes), el orden se invierte: 0x1F4D.

La dirección 0xB8000 de la memoria guarda los datos de la primera celda de nuestra tabla, por lo que si escribimos nuestra letra (0x1F4D) a esa dirección, nuestra pantalla de 8 caracteres por 4 lineas mostrará esto:

M
 
 
 

Si avanzamos 2 bytes y escribimos en la dirección 0xB8002 la letra 0x1F6F (0x6F para la o minúscula, 0x1 para el color azul de fondo y 0xF para el color blanco de fondo), veremos esto:

M o
 
 
 

Si has seguido la explicación, deberías tener las herramientas teóricas para imaginarte ya cómo se hace esto, que es nuestra idea:

M o d o   p r o
t e g i d o
 
 

¿Cómo trasladamos eso a nuestro código?

Para que el código sea legible, lo primero que vamos a hacer es definir una constante que contenga la dirección del buffer VGA. Yo la he llamado memory_screen_buffer:

memory_screen_buffer equ 0xB8000

También vamos a definir nuestra nueva cadena de texto en la parte del archivo en la que tengamos nuestras variables:

msg_protected:
    db "Modo protegido", 0

Dado que ahora no tenemos acceso al BIOS interrupt que usábamos antes, los caracteres 0x0D (retorno de carro) y 0x0A (salto de linea), que hemos incluido en todas nuestras cadenas de texto hasta ahora, no realizarán sus funciones correctamente. Llegado el momento, implementaremos los cambios necesarios para que vuelvan a ser funcionales, pero por ahora, lo mejor es no incluirlos en nuestra variable.

Ahora vamos a redactar la función en sí. Nuestra implementación nos permitirá mostrar cualquier cadena de texto, en cualquier posición de la pantalla, en cualquier color. Recibiremos esos 3 datos en los siguientes registros:

Es interesante destacar el uso de algunos registros de 32 bits (edi y esi). Esto se debe a que en el modo de 32 bits, los punteros ocupan 32 bits. Concretamente, en este caso el uso de registros de 16 bits no debería ocasionar problemas, pero conviene adaptarse. Además, estos registros no son arbitrarios. El uso de otros registros, aunque sean de 32 bits, puede ocasionar problemas.

También usamos un registro de un solo byte (bl) para el color, debido a que no hace falta más espacio. Este registro sí es arbitrario. Puede cambiarse, por ejemplo, por cl o dl.

A partir de aquí, el código es parecido al de la función rmode_print:

pmode_print:
    [bits 32]             ; Emitir código de 32 bits

pmode_print_loop:
    lodsb                 ; Copiar 'edi' a 'al' e incrementar 'al'
    or al, al             ; Comprobar si 'al' es igual a 0
    jz pmode_print_ret    ; Si lo es, terminar la función

    mov [edi], al         ; Copiar 'al' hacia nuestro buffer
    inc edi               ; Apuntar al siguiente byte del buffer
    mov [edi], bl         ; Copiar el código de color hacia buffer
    inc edi               ; Incrementar 'edi' para apuntar hacia el siguiente caracter

    call pmode_print_loop ; Comprobar el siguiente caracter

pmode_print_ret:
    ret                   ; Terminar la función

Ahora, si quisiéramos imprimir el texto Modo protegido, en la esquina superior izquierda de la pantalla, de color blanco sobre azul, solo tendríamos que invocar la función tal que así:

mov edi, memory_screen_buffer
mov esi, msg_protected
mov bl, 0x1F
call pmode_print

Por último, asegúrate de que, después de estas lineas, el código salta a la función halt. Se asume que durante todo el capítulo has ido arrastrando la linea jmp halt al final del código que hemos ido añadiendo. Si no, el programa seguirá ejecutándose linea por linea y dará error.

Resumen


En este episodio hemos aprendido a hacer dos cosas: pasar al modo protegido y escribir al buffer VGA (o memoria de vídeo). Esto nos deja muy cerca de nuestro primer archivo escrito en C. Tan solo nos queda un paso: programar el punto de entrada a C y aprender a saltar a ese punto.

Antes de llegar ahí, aprenderemos también cómo pasar del modo protegido al modo real de nuevo. Esto será necesario cuando queramos usar cualquier BIOS interrupt, lo cual resulta necesario en algunos casos. También dedicaremos un apartado a organizar nuestro código correctamente para poder extender el proyecto sin preocuparnos de rehacer build.sh.

Ejemplo descargable


Como siempre que añadimos código al proyecto, puedes descargar un ejemplo de sistema operativo con lo que llevamos escrito hasta ahora (capitulo-7.tar.gz).