Capítulo 2: Iniciar un sistema operativo


Para iniciar nuestro sistema operativo, lo primero que debemos preguntarnos es qué hace un ordenador cuando inicia. Y responder a la pregunta, al menos hasta cierto punto. No nos interesa saber cómo inicia un ordenador del 2024 con Windows 11 instalado y mil programas más, pero sí saber qué hace el ordenador antes de que cargue todo eso. O más bien, preguntarnos... ¿cuándo entra nuestro sistema en juego?

Tabla de contenidos


  1. Cuándo entramos en juego: la secuencia de inicio
  2. Cómo hacer que la BIOS nos ceda el control
  3. Cómo ejecutar nuestro bootloader
  4. ¿Qué haremos a partir de ahora?

Cuándo entramos en juego: la secuencia de inicio.


Cuando se inicia un ordenador, el primer programa en ejecutarse es la BIOS o UEFI. Este programa no está ni en el disco duro ni en la memoria RAM de nuestro ordenador, sino en un chip instalado en la placa base. Más concretamente, una EEPROM: una memoria no volátil que puede ser reescrita un número limitado de veces. Al iniciarse el ordenador, el programa es copiado a la RAM y ejecutado directamente por el procesador.

La BIOS, al ejecutarse, hace lo siguiente:

  1. Un test llamado POST que comprueba que el hardware funciona correctamente.
  2. Definir una serie de funciones llamadas BIOS interrupts que facilitan el desarrollo de programas y sistemas operativos.
  3. Buscar dispositivos de arranque que contengan una firma concreta.

Y aquí entramos en juego nosotros: nuestro sistema va a estar instalado en un dispositivo de arranque (como un disco duro o un USB). Lo primero que tenemos que hacer es marcar ese dispositivo con la firma que la BIOS espera, para que lo detecte y vea que puede cedernos el control.

¿Qué dispositivo?, ¿qué firma?, ¿qué dices?

Tranqui. A ver, el dispositivo puede ser cualquier memoria no volátil que se pueda comunicar con nuestro ordenador. Las memorias no volátiles son aquellas que no pierden lo que tienen almacenado al apagarse el ordenador. Lo dicho, puede ser un disco duro, un USB e incluso un floppy. De hecho, para simplificar las cosas, de entre todos esos dispositivos emularemos un floppy.

La firma es una serie de bytes colocada en una parte concreta del disco. Sirve para decirle a la BIOS que es posible arrancar el ordenador desde el dispositivo. En concreto, vamos a escribir los 2 bytes 0xAA55 en la dirección 510 del disco. Como curiosidad, también existen sistemas que van directamente instalados en la placa base, sin necesidad de ningún dispositivo externo, como la BIOS. Estos sistemas se llaman sistemas integrados.

Como bien sabrás, un byte es un número de ocho cifras en binario, como 00001111 o 00010010. Muchas veces, por acortar, se escriben en hexadecimal. Por ejemplo, estos 2 bytes que he puesto ahora serían, respectivamente, 0F y 12. Los números en hexadecimal se suelen prefijar con 0x para diferenciarlos del decimal. Conviene familiarizarse con esto un poco antes de meterse al redil.

La cosa es que en un disco lo que hay es una serie de bytes. Por ejemplo, si creamos el archivo disco.img y le escribimos lo siguiente con un editor hexadecimal:

44 49 53 43 4F 00 00 00 00 00 00 00 00 00 00 00
53 45 52 49 45 20 44 45 20 44 41 54 4F 53 00 00

Habremos creado la imagen de un disco de 16 bytes, y si decodificamos los bytes usando la tabla ASCII veremos que pone esto. Vete acostumbrando a consultar la tabla ASCII cuando tengas bytes que representan texto, porque te vendrá muy bien:

D  I  S  C  O  .  .  .  .  .  .  .  .  .  .  .  
S  E  R  I  E     D  E     D  A  T  O  S  .  .  

Pues en nuestro disco tiene que haber 510 bytes en los que ponga lo que sea y en los siguientes 2 debe poner 55 AA.

Un pequeño aviso sobre la endianness

La firma que tenemos que poner en el byte 510 del disco es 0xAA55. Ese es un valor de 2 bytes, que cuando se pone en el disco y se lee byte a byte, se refleja como 55 AA (es decir, al revés).

Tener en cuenta esto es muy importante. Un número de 32 bits con el valor 0x12345678 se trasladará al disco como 78 56 34 12. Por tanto, podemos escribir la firma utilizando cualquiera de estas 2 opciones:

  • dw 0x55AA: escribe a la RAM un valor de 2 bytes (una word) con el valor AA 55.
  • db 0xAA, y luego db 0x55: tiene el mismo efecto, aunque usa una instrucción distinta para cada byte.

Esta forma de guardar los números en la memoria se llama little endian, y nos afectará cuando hagamos operaciones de memoria con números de diversos tamaños. Más información en Wikipedia.

Cómo hacer que la BIOS nos ceda el control


Bueno, pues vamos a escribir el código. Assembly es el lenguaje que más libertad nos da para escribir bytes directamente a un ejecutable (que luego puede transformarse en una imagen de disco), por eso vamos a usar ese lenguaje. En concreto, usaremos Assembly x86 con sintaxis de NASM.

En concreto, lo que vamos a escribir ahora no es un sistema operativo, ni siquiera forma parte de uno. Lo que vamos a escribir es un bootloader: el programa que se encarga de cargar el sistema operativo y preparar el entorno para que pueda funcionar correctamente.

En Linux, deberás descargar los siguientes paquetes (o los equivalentes para tu distribución):

Esto se trata en mayor detalle en el apartado sobre cómo generar ejecutables de la introducción.

Hecho esto, crea una carpeta. Esta carpeta contendrá todos los archivos de nuestro sistema operativo, así que asegurate de organizarla bien. En dicha carpeta, crea el archivo boot.asm (o como lo quieras llamar), y escribe en él este código:

; DIRECTIVAS PARA NASM
bits 16                   ; Emite código de 16 bits.
org 0x7C00                ; Organiza el código desde la dir. 0x7C00.

; SECCIÓN DE CÓDIGO
start:
    jmp halt              ; Salta hacia "halt".

halt:
    hlt                   ; Detén el procesador.
    jmp halt              ; Salta hacia "halt" si vuelve a funcionar.

; SECCIÓN DE DATOS
times 510-($-$$) db 0     ; Escribe ceros hasta el byte 510.
dw 0xAA55                 ; Escribe la firma que espera la BIOS.

Si vienes de capítulos anteriores, notarás que no estamos declarando las secciones usando la directiva section. Esto es porque estamos programando para un entorno freestanding, y vamos a generar un archivo binario puro. Lo que estábamos generando antes era un archivo ELF, en el que sí que debes dividir tu código en secciones predefinidas.

Este es el código de un bootloader que arranca el sistema y detiene el procesador. No hace nada más, aunque tiene algunas directivas que explicaré un poco más en detalle:

Existen muchos tutoriales para hacer este tipo de bootloader, llamado Bare Bones Bootloader. Un buen ejemplo es este de Joe Savage.

Cómo ejecutar nuestro bootloader


Primero hace falta compilarlo, es decir, convertirlo en ejecutable. En este caso, como estamos compilando un sistema operativo, no vamos a crear un ejecutable al uso, sino una imagen de disco iniciable. Por suerte, eso con NASM y algunas utilidades preinstaladas en Linux es bastante fácil.

  1. nasm -f bin -o boot.obj boot.asm
    Traduce el archivo a lenguaje máquina (en formato binario puro) y guárdalo en boot.obj.

  2. dd if=/dev/zero of=boot.img bs=512 count=2880
    Crea la imagen boot.img y llénala con 1440 KB de ceros.

  3. dd if=boot.bin of=boot.img conv=notrunc
    Copia el contenido de boot.bin a los primeros 512 bytes de la imagen de disco.

Hecho esto, puedes ejecutarlo en un emulador, cosa que te recomiendo, o puedes intentar ejecutar tu bootloader en tu propio ordenador. Esto último es algo más difícil.

Ejecutarlo en un emulador

  1. Instala el paquete qemu-full (o el equivalente en tu distribución).
  2. Ejecuta qemu-system-i386 -fda boot.img desde la carpeta del programa.

Dentro del emulador, te saldrá el mensaje Booting from floppy..., y luego se quedará el cursor parado. Eso significa que hemos hecho las cosas bien: el sistema inicia y ejecuta nuestro código, que simplemente detiene el procesador. Si no lo detuviéramos, estaría reiniciándose constantemente.

Ejecutarlo en hardware real

Importante: antes de probar a hacer esto, asegurate de que has leído bien cómo deshacer los cambios en la BIOS.

  1. Coge un USB y asegurate de copiar los datos que quieras conservar.
  2. Descarga el programa USBImager.
  3. Escribe los datos de boot.img al USB usando USBImager.
  4. Abre la BIOS de tu ordenador (pulsa repetidamente F1, F2 o Esc justo al iniciar el ordenador).
  5. En el menú Arranque, Inicio o Boot habilita las siguientes opciones si están disponibles:
    • Compatibilidad con Legacy / Modo Legacy
    • Inicio desde USB
    • Iniciar dispositivos Legacy primero
  6. En la lista de dispositivos, lleva tu USB arriba del todo.
  7. Cierra guardando los cambios.

Cómo deshacer los cambios en la BIOS

Para deshacer los cambios, desconecta el USB y desmarca las siguientes opciones de la BIOS si están disponibles:

Después de esto, deberás llevar tu disco duro interno (o el dispositivo que normalmente uses para iniciar tu ordenador) arriba del todo en la lista de dispositivos de inicio. Si no tienes claro cómo realizar alguna de estas cosas, busca bien cómo hacerlas antes de tocar nada en la BIOS.

¿Qué haremos a partir de ahora?


Estamos siguiendo un esquema llamado Two-Stage Bootloader que empieza con un bootloader llamado stage1 (el que acabamos de crear). Este bootloader debe saltar a stage2, que prepara algunas cosas y salta hacia el kernel, considerado el primer programa del sistema operativo como tal.

¿Y por qué dividirlo en dos partes? Bueno, puede que te hayas dado cuenta de que todo nuestro código tiene que estar antes de la firma. Si la firma está en el byte 510, eso significa que tenemos muy poco espacio disponible. Más aún si contamos con que parte de ese espacio lo tendremos que ocupar con datos requeridos por el sistema de archivos que usemos.

En el siguiente capítulo todavía no vamos a saltar a la parte 2 del bootloader, pero sí que aprenderemos a imprimir texto desde la stage1 y formatear el disco con el sistema de archivos FAT12. Estos pasos son esenciales para poder continuar hacia stage2.