Para descargar el Sistema Operativo tOSh, podés hacerlo desde la sección Resources de este mismo sitio.

Partes

Esta serie de artículos va a tener las siguientes partes:

ISA POST Tester CardISA POST Tester Card

Prefacio

¿Otro Sistema Operativo más? Si. ¿Por qué? Porque puedo. ¡Increíblemente tengo tiempo libre! Y como esto forma parte de un proyecto más grande, en unos meses cuando vuelva a tener el tiempo suficiente como para poder revisarlo, voy a necesitar tener un resumen y documentación y escribir este artículo de alguna manera me va a ayudar a tener todo en un solo lugar para consultarlo luego. ¿Cuál es ese proyecto? ¿Vas a competirle al Kernel de Linux? ¿Vas a tratar de reemplazar Windows por el tuyo? No. Para nada. De hecho, creo que es bastante difícil hacer algo así en estos días individualmente y todo lo que uno pueda llegar a hacer en solitario no podría llegar más lejos que lo que Terry A. Davis haya logrado con Temple OS que es de por si, cuando menos impresionante.

Sinceramente, el objetivo de este Sistema Operativo es demostrar mis habilidades como programador, el entendimiento sobre sistemas operativos, arquitecturas, lenguajes (ASM, C), la creación de herramientas "en el andar" y una ristra de cosas más para un poco cancherearle a mis amigos y otro poco para chapearle a recruiters y futuros empleadores, en caso que tengan dudas de mis habilidades.

En caso de que seas un Recruiter: ¡Hola, mucho gusto!, mandale por favor esta serie de artículos al lider técnico pertinente, le va a encantar.

En caso que seas el "Lider Técnico Pertinente": ¡Hola, ¿que tal? Encantado: ¡Mandame un mail por favor y charlemos!

Si no entrás en alguna de estas dos categorías, quizá a vos también te sirvan esta serie de artículos por vaya a saber que motivo. ¿Universidad? ¿Curiosidad? ¿Masoquismo? Da igual, bienvenido y disfrutalos tanto como yo disfruté y sufrí escribir sobre como hacer un Sistema Operativo para la Arquitectura Intel.

Descarga de tOSh (img / código fuente)

Para descargar el Sistema Operativo tOSh, podés hacerlo desde la sección Resources de este mismo sitio. En la tabla de recursos hay varias secciones, y en Software está la imagen ya compilada del sistema operativo y en la sección Sourcecode la última versión de su código fuente. ¡¡¡Bajátelo YA NO TE LO PIERDAS!!! (?)

¿Por qué se llama tOSh?

Tosh, tsh o Toshi son algunos de mis handles. Quise hacer algo parecido al acrónimo recursivo GNU is Not Unix y me quedó:

"tOSh Operating System, Huh?"

En algún momento me pareció gracioso. Hoy no tanto y me dio paja cambiar el nombre del repo ;D Bueno, basta de introducciones y arranquemos enseguida por lo primero de lo primerísimo que hay que tener en cuenta para hacer un Sistema Operativo para Intel, entender su Secuencia de Booteo.

Secuencia de Booteo en Intel

La Secuencia de booteo ó Boot_Sequence consiste en un conjunto de pasos que necesitan ser ejecutados en cierto orden para poder cargar al final, un kernel en memoria y eventualmente saltar a su código para otorgarle control a la administación los dispositivos de la computadora, interfaz de usuario, ejecución de programas, etc… Vamos a listar muy brevemente estos pasos prestando suma atención a cosas que necesitamos saber sobre como se configura el equipo desde que apretás power-on en tu PC hasta que puedas insertar algunas instrucciones cuando llegás a la instancia de poder ejecutar tu propio código.

P.O.S.T.

Cuando una computadora se enciende, la mayor parte de los registros del CPU tienen valores muy bien definidos, incluyendo el puntero de instrucción (IP) o como en otras arquitecturas o libros de texto lo llaman Program Counter. El valor de IP apenas se enciende la computadora contiene la dirección de memoria que ejecuta la CPU. Esta dirección de memoria se llama reset vector y está hardcodeado en el CPU: la dirección de memoria es particularmente 0xFFFFFFF0. El motherboard de la computadora se asegura que la instrucción que se ejecute en el reset vector sea una instrucción jump a la dirección de memoria mapeada al entry point de la BIOS. Toda esta ensalada de direcciones de memoria del BIOS están organizadas por el chipset del motherboard porque en este momento, la RAM contiene basura random. Finalmente, la CPU ejecuta el código del firmware. En un sistema con BIOS legacy, el BIOS inicializa parte del hardware de la computadora y realiza el POST - Power-On-Self-Test, que consiste en una serie de diagnósticos de algunos componentes esenciales como la placa de video. Por ejemplo, si la placa de video falla en esta instancia, el código del POST generalmente causa que el BIOS emita un par de pitidos en el speaker de la computadora para que vos puedas agarrar el manual, y dependiendo del número de pitidos, saber por qué está fallando la inicialización, y esta información generalmente está en el manual del motherboard para consultar que significan 3 pitidos largos y uno corto, 2 pitidos cortos y uno largo, etc…

Después del P.O.S.T.: Saltar al vacío

Una vez que se terminó de chequear los componentes esenciales, el BIOS va a querer bootear un sistema operativo. Y este sistema operativo debería poder encontrarse en algún lado. Entonces empieza a buscar en discos rígidos, USB sticks, disquettes o CD-ROMs. Si el BIOS no encuentra ningún dispositivo disponible, se detiene la ejecución del CPU y no sin antes mostrar algun texto acorde a lo que sucedió:

Non-System Disk or Disk Error. 

Sin embargo, si encuentra algún dispositivo, el BIOS va a tratar de leer el primer sector de 512 bytes en el dispositivo de almacenamiento que haya encontrado primero (o esté configurado en el BIOS, claro). Este primer sector, es llamado coloquialmente MBR (Master Boot Record). El BIOS carga cualquier cosa que haya en este primer sector a la dirección de memoria 0x0000:0x7c00 (segmento 0, dirección 0x7c00) y salta a esa dirección sea cual fuere el código que haya para ejecutar. Un salto de fe. (!)

Sólo 512 bytes disponibles

Bueno, no tenés exactamente 512 bytes disponibles para escribir el código del bootloader si no un poco menos. La BIOS busca en los "dispositivos booteables" una marca o magic number (si) que tiene que ser exactamente la secuencia de bytes 0x55 y 0xAA en los offsets 510 y 511 respectivamente. Ni hablar que estos 510 bytes disponibles son desde un disquette. Si necesitás bootear desde un disco rígido, entonces tu espacio se reduce a 446 bytes. Para colmo, en todo este reducido espacio necesitás hacer las siguientes cosas para tener un bootloader decente:

  • Determinar desde cual partición bootear
  • Saber donde está la imagen de tu kernel en la partición de booteo
  • Cargar la imagen del kernel en memoria (El BIOS tiene funciones para leer sectores y copiar a memoria)
  • Pasar de Modo Real a Modo Protegido (Estamos en Modo Real con un máximo de 1 MB disponible, aunque nuestra computadora tenga over 9000 GB)

El Modo Protegido es entre otras cosas, saltar de instrucciones y modo de direcciónamiento de 16 bits a 32 bits, lo que nos permite dejar de tener una limitación de acceso a 1 Megabyte de memoria, ampliando el espacio de direcciónamiento hasta 4 Gigabytes

  • Setear el stack (sí, necesitamos configurar nosotros mismos el stack)
  • Mensajes de error! (el bootloader no encuentra la imagen? no se pudo copiar el kernel a memoria? que esta pasando?)

Hay formas de resolver estos problemas, ya que de otra manera, no existirían los sistemas operativos actuales. Estos problemas a lo largo de los últimos 40 años en la historia de las PC se resolvieron y discutieron una y otra vez, como estamos haciendo acá también. Y se van a seguir discutiendo y escribiendo artículos sobre este tema.

Escribiendo un Bootloader

La información anterior es la mínima indispensable para poder entender porque algunos valores que parecen totalmente arbitrarios no lo son. A continuación vamos a escribir un bootloader de 512 bytes que nos permita al menos copiar sectores de un dispositivo a memoria y saltar a esa dirección para continuar. Esto se lo llama One-Stage loading: Nuestro bootloader carga una imagen del kernel (que debe de ser menor a 1 Megabyte), saltamos a un stub con el que tenemos que cambiar de Modo Real a Modo Protegido para eventualmente saltar apropiadamente a nuestro kernel.

¡Espera! Antes de empezar…

Vamos a instalar y a hacer los siguientes pasos:

  • Usar cualquier distribución de Linux
  • Instalar nasm (compilador de assembly)
  • Instalar qemu (emulador de varias arquitecturas, nosotros vamos a emular la arquitectura 386+ de Intel)
  • Instalar PCem (emulador de componentes de PC, emula diferentes tipos de BIOSes, motherboards, CPUs, placas de video, etc…)
  • Instalar bochs un emulador que te va a hacer compania durante todo el desarrollo del sistema operativo, ya que te permite leer registros especiales como GDT, IDT, CRn. Es un emulador que te permite debuggear, muy parecido de gdb.

… deberás aprobar un examen de historia.

Antes de seguir, una aclaración importante: todo lo que estamos haciendo acá describe el flujo clásico de BIOS legacy. Las máquinas modernas también pueden bootear usando UEFI, cuyo mecanismo de carga es distinto. En esta serie vamos a quedarnos deliberadamente con BIOS porque estamos construyendo un bootloader x86 clásico.

Vamos a crear 3 archivos:

  • bootsector.asm
  • build_and_run.sh
  • stage1.asm

Los tres vacios, salvo build_and_run.sh que más o menos, por conveniencia, debería tener el siguiente contenido:

#!/bin/sh
nasm -f bin bootsector.asm -o boot.bin
nasm -f bin stage1.asm -o stage1.bin

cat boot.bin stage1.bin > tOSh.bin
qemu-system-x86_64 -fda tOSh.bin

Primero que nada, configuramos nasm para que ensamble nuestro código assembly exactamente como queremos.

; ------------------------------------------------
; tOSh Operating System, Huh?             (c) 2019
; bootsector.asm                          by toshi
; $ nasm -f bin bootsector.asm -o boot.bin
; ------------------------------------------------
; 
; docs
; https://wiki.osdev.org/Boot_Sequence
; https://wiki.osdev.org/Rolling_Your_Own_Bootloader
; https://stackoverflow.com/questions/34178717/load-segment-from-floppy-with-int13h

bits 16         ; setup nasm to 16 bit code
cpu 186         ; assemble with 80186 instruction set (ie.: push word label)
org 0x7c00      ; all offsets will be at 0x7c00

; after here we will be able to execute code
; in x86 real mode, so these are the first instructions
section .text

global _start   ; make this symbol global so we can reference
                ; it later in a custom linker script

_start:

jmp boot

La primer instrucción de todas es jmp boot que va a un label más adelante. Para que nasm sepa exactamente hacia donde tiene que saltar (jmp boot) tenemos que forzar al ensamblador a que no tome decisiones por su cuenta y trate de usar lo último de lo último que tenga disponible. Por esto, seteamos a nasm que escriba código 16 bits, que use hasta el máximo set de instrucciones 80186 con la directiva cpu y finalmente con org 0x7c00 le decimos que todos los "ceros" de los offsets de los jumps, labels y demás empiezan en 0x7c00: es decir que nuestro "nuevo cero" relativo es lo que configuremos con org.

Esto es sumamente importante y es un concepto que mejor que lo entiendas ahora que después cuando sea demasiado tarde y que cuando tengas a veces que leer código ensamblado, las instrucciones carezcan de total sentido. Cuando saltás (jmp, jx, jxx) los saltos pueden ser far o near. Para un salto far en x86 16 bits, se tienen en cuenta el segmento y el offset de los **modos de direcciónamiento de memoria de intel 16 bits o Real Mode.

Uno de nuestros objetivos más importantes después de lograr bootear el CPU y copiar el Kernel en memoria va a ser salir del Real Mode y saltar al Modo Protegido. Es infinitamente más fácil trabajar con modos de direcciónamiento de memoria del modo protegido que los del modo real, porque el juego del cálculo de la memoria física entre segmento y offset es confuso y molesto. En cambio, en modo protegido, directamente vas a una posición de memoria de 32bits y listo.

Pulsa ENTER para continuar…

Continuamos con algunas funciones útiles para hacer que nuestra secuencia de booteo sea la más hermosa del barrio:

; http://www.ctyme.com/intr/rb-0097.htm 
; ralf brown scroll down window int10h
clrscr:

    push bp
    mov bp, sp
        mov ah, 0x07    ; set "scroll down window" for int 10h
        mov al, 0       ; clear entire window with null char

        ; https://en.wikipedia.org/wiki/BIOS_color_attributes
        mov bh, 0x4f    ; colors (nibbles background/foreground)
                        ; we choose red background, white characters
        mov cx, 0       ; top left screen (0,0) (cx register)
        mov dh, 24      ; 25 rows (from 0 to n-1) (dx register)
        mov dl, 79      ; 80 cols (from 0 to n-1)
        int 0x10
    mov sp, bp
    pop bp
    ret

sleep:
    push ax
    push cx
    push dx

        mov ah, 0x86    ; int 15h, function 86h, CX:DX interval in microseconds
        mov cx, 0x20    ; just a few seconds 
        mov dx, 0x0000
        int 0x15

    pop dx
    pop cx
    pop ax
    ret

; print(char *msg_string)
print:
    push bp
    mov bp, sp

        ; move the cursor
        ; Set cursor position    AH=02h    BH = Page Number, DH = Row, DL = Column 
        mov dh, 12  ; row
        mov dl, 16  ; col
        mov bh, 0   ; page 0
        mov ah, 0x2 ; move cursor service
        int 0x10

        ; as we have stack, we grab the parameter
        ; of print    
        mov si, [bp+4]  ; move the ptr of the first
                        ; argument
        mov ah, 0x0e    ;  write char teletype style
                        ; http://www.ctyme.com/intr/rb-0106.htm
        mov bh, 0       ; page number 0
        mov bl, 0       ; foreground (only graphics mode) 
        .loop:
            lodsb       ; load a byte from ds:si into al
                        ; glad we setup ds register before,
                    
            cmp al, 0   ; is the null caracter?
            je .print_end
            int 0x10    ;  BIOS Video services
            jmp .loop


    .print_end:
    mov sp, bp
    pop bp
    ret

load_disk:
    push dx
        mov ah, 0x02                ; int 13h, read
        mov al, dh                  ; number of sectors to read
        mov cl, 0x02                ; cl, sector, 0x01 is the boot sector
                                    ; 0x02 is where we will put our
                                    ; 'stage1' data
        mov ch, 0x00                ; ch, cylinder
                                    ; boot_drive_numbers -->
                                    ; drive number, 0 = floppy1, 1 = floppy2
                                    ; 0x80 = hdd1, 0x81 = hdd2
        mov dl, [boot_drive_number] ; the BIOS should have told us what's the 
                                    ; drive number that we grabbed in the very first
                                    ; instructions
        mov dh, 0x00                ; dh, head number (0 to F)

        int 0x13                    ; BIOS interruption
        jc load_error               ; if carry bit set, then error
    pop dx
    cmp al, dh                      ; al is now num of sectors read
    jne sectors_error
    ret

load_error:
    push word load_error_msg
        call print
    add sp, 2
    jmp error_hang

sectors_error:
    push word sectors_error_msg
        call print
    add sp, 2
    jmp error_hang

error_hang:
    jmp $

En todas las funciones que siguen a continuación, como no tengo nada de nada, salvo la parte baja de la memoria, estar en Modo Real y la posibilidad de hacer llamadas de interrupción al BIOS, la única interfaz para poder hacer algo es la BIOS. Para poder obtener una lista exhaustiva de las interrupciones al BIOS y cada una de las funciones que podés usar para interactuar con otros dispositivos sea la disquetera, el teclado o la pantalla es la vieja y quierida Ralf Brown’s Interrupt List, de valor incalculable.

clrscr: hace exactamente lo que promete su acrónimo: limpiar la pantalla.

sleep: detener la ejecución por unos segundos.

print: esta función imprime un texto que esté apuntado por el puntero como parámetro en el stack

load_disk: cargar exactamente el (head) cabezal 0x00, (cylinder) cilindro 0x00, sector 0x02 que es donde va a estar el código del stage1 que vendrá mucho más tarde.

load_error, sectors_error y error_hang: funciones que imprimen el tipo de error en pantalla y terminan colgando la CPU a propósito con un loop infinito con jmp $

Ahora lo que sigue es el código de booteo que viene debajo con boot::

boot:
    nop        ; stylish nop is stylish

    ; save DL register setup by BIOS for getting the drive number
    mov [boot_drive_number], dl

Esencialmente, lo primero que hicimos, si seguís atentamente el código, es saltar al label boot:,
y después de el simpático y elegante nop que puse ahi solamente por ponerlo porque pintó, lo primerísimo que hago es guardar el valor del registro dl en otro label boot_drive_number. Cuando booteamos, el BIOS llena algunos registros con datos, y en dl, el BIOS nos guardó el número del drive en el cual vamos a bootear.

    mov ax, 0x0000      ; if ds is 0x7c0 the ORG directive must be disabled
                        ; but OFC ds is 0, then ORG directive must be enabled!
                        ; TODO: Why? --
                        ;    BECAUSE: every memory position, or
                        ;             memory reference implicitely has
                        ;          ds inside -> [ptr] is [ds:ptr]
                        ; that's why
                        ;    ALSO: segmentation essentialy is
                        ;           << 4, so 0x7c0 << 4 is 0x7c00

    mov ds, ax          ; set the data segment register
                        ; we set ds as 0x0 because we use ORG 0x7c00
                        ; for our offsets assembled in the binary
    mov es, ax          ; and 'extended?' register too

    ; setup the stack
    mov ax, 0x7c00 + 512        ; 512 is the total size of the boot sector
    mov ss, ax                  ; so we set the stack segment
                                ; of this code, TODO: explain segment addresses
                                ; arithmetics

    mov sp, 0x1000              ; let's reserve 4k of stack

Acá seteamos un stack rudimentario para poder hacer uso de nuestras propias funciones en el bootloader. ¿Lo necesitamos? y la verdad que no, pero si voy a estar booteando también me gustaría andar mostrando mensajes en la pantalla –para debuggear o por simple facha nomás– y la forma más fácil de hacerlo es creando "funciones" que necesitan un stack para poder pasar parámetros.

Entonces seteamos a 0x0000 nuestros registros de segmento ds (data segment) y es (extended segment) y configuramos el registro de segmento ss (stack segment) con una dirección de memoria que está al final de nuestro código.

Finalmente con la instrucción mov sp, 0x1000 lo que estamos haciendo es reservar 4 kilobytes del stack asignando 0x1000 al registro stack pointer sp.

    call clrscr

    push word loading_msg
        call print
    add sp, 2

    call sleep

Habiendo configurado el stack, ahora podemos usar la instrucción call y usar nuestra bien extraña calling convention. Para pasar un parámetro a nuestra función, pusheamos el puntero del parámetro, hacemos un call inmediatamente después y al volver, sumamos un word (si el push fue un word) al registro stack pointer sp.

    ; start "stage1"

    ; TODO: make this a function,
    ; https://github.com/cfenollosa/os-tutorial/blob/master/07-bootsector-disk/boot_sect_main.asm

    mov bx, 0x9000  ; es:bx = 0:0x9000 ; where in memory are we
                    ; going to put the stage1 code
    mov dh, 4       ; read 4 sectors
               
    call load_disk

Acá simplemente configuramos los parámetros de nuestra función load_disk diciéndole que los 4 sectores que vaya a leer, los copie a 0:0x9000.

    ; uncomment the following lines from
    ; ------------ here ----------------
    ;push word 0x9000
    ;   call print
    ;add sp, 2
    ;cli
    ;hlt
    ; ------------till here------------
    ; for seeing that the sector data
    ; is displayed

    jmp 0x9000 ; execute stage1

    hlt

Y una vez copiados, saltamos al vacío a una dirección de memoria donde no pusimos todavía nada.


loading_msg: db "Loading tOSh ver -3.1416a | tw/github: @0x705h ...", 0x07, 0
load_error_msg: db "Load error", 0
sectors_error_msg: db "Sectors error", 0

boot_drive_number: db 0

times 510 - ($-$$) db 'T'   ; pad 510 bytes with 't', last two bytes
                            ; in the 512 sector will be the bootloader mark

dw 0xaa55                   ; bootloader mark. These bytes make this sector bootable

; here ends the first 512 bytes sector

Al final de nuestro bootsector.asm, rellenamos con T para poder ver como se va comportando nuestro binario y si nos queda margen. El caracter T lo uso como padding a 512 bytes y ver cuanto espacio me queda. Menos Ts, menos bytes para programar el bootloader.

Finalmente, la dw word 0xaa55 es una marca o número mágico que la BIOS espera encontrarse en ese offset exacto (511b y 512b respectivamente) para darle el hint que ese sector es, efectivamente, booteable.

Mapa de Memoria & Layout Sectores

En cada parte de esta serie de artículos, hay una parada obligada que va a ser mantener actualizado nuestro mapa de memoria. Necesitamos siempre tener en cuenta donde vamos a poner que en memoria y ser prolijo con esto nos va a evitar muchisimos dolores de cabeza.

Floppy

Sector (CHS) área Lógica Contenido/Código Propósito
C:0 H:0 S:1 boot sector bootloader.asm Lee contenido del floppy, lo copia a memoria y salta a stage1.asm

Memoria (Modo Real)

Dirección Física Notación Seg:Offset Tamaño Propósito
0x00000 - 0x003FF 0000:0000 1 KB IVT (Interrupt Vector Table de la BIOS)
0x00400 - 0x004FF 0000:0400 256 B BDA (BIOS Data Area)
0x00500 - 0x07BFF 0000:0500 ~29.7 KB Zona libre / Nuestro Stack (apuntado por SP=0x7C00 decreciendo)
0x07C00 - 0x07DF_ 0000:7C00 512 B Boot Sector (bootsector.asm cargado por la BIOS)
0x07E00 - 0x8FFFF 0000:7E00 ~544 KB Memoria Convencional Libre
0x09000 - 0x097FF 0000:9000 2 KB Stage 1 (Donde la INT 13h copia los 4 sectores del disco)
0xA0000 - 0xFFFFF A000:0000 384 KB "Memoria de Video, VRAM y ROM de la BIOS"

Referencias