Construyendo un Bootstrap
El siguiente tutorial era parte de lo que pretendía ser un gran proyecto, detallar los pasos para construir un kernel...
Todo arrancó con la idea de crear solo un bootstrap y luego se fue ampliando, aunque avancé más con la programación que con la bitácora que pensaba llevar, es decir, un informe detallado sobre lo que iba haciendo. El problema es que tuve que abandonar el proyecto por falta de tiempo y un día en un cortocircuito mental borré la carpeta donde tenia todo... por suerte la explicación se salvó, aunque como dije, no está todo lo que había logrado.
En fin, en esta entrega coloco la explicación de cómo crear un bootstrap. Como verán es mucho más sencillo de lo que suena, aunque hay que tener conocimiento de cómo funciona el hardware y de programación en assembler.
------------------------------------------------------

Construir un bootstrap es relativamente facil, sólo hay q saber ciertas cosas.
La primera es que el BIOS (Basic Input Output System) lo que hace al arrancar la máquina es buscar un bootstrap en algun medio de almacenamiento no volatil, como ser un floppy un cd-rom o el disco duro.
En los casos de los discos como floppy o hard disk, el BIOS lee el primer bloque de 512 Bytes q se encuentra en la ubicacion cabeza cero, cilindro cero, sector uno. El ultimo word de este bloque debe contener un identificador de booteo, en el caso de la arquitectura x86 este identificador es AA55h (palabra en hexa). Si encuentra este identificador, entonces el BIOS carga en algun lugar de memoria este bloque. En el caso de las x86, la posicion donde carga este programa es en 07C00h, es decir en 07C0:0000. Una vez cargado en la memoria, el BIOS salta a esa direccion y entonces el programa empieza a ejecutarse.
Un ejemplo basico sobre como debe ser la forma de un bootstrap, es la siguiente (usando NASM Netwide Assembler):
; boot1.asm
[BITS 16] ; Seteo a modo de 16bits q es como se encuentra el procesador al inicio
[ORG 0] ; Pongo el codigo desde el principio del segmento

jmp main
; declaracion de variables u otros datos

main:
jmp main ; simplemente ciclamos

TIMES 510-($-$$) DB 0 ; Hace q el archivo sea de 512 bytes de longitud
dw 0AA55h ; Indica al bios q es un bootstrap

Para compilar el programa simplemente hacemos:
$nasm boot1.asm -o boot1
Para colocar esto en un diskete si utilizamos linux (como en mi caso =P) podemos usar el comando dd. El comando dd es sencillo de utilizar, le damos un archivo de entrada y el lugar donde lo queremos copiar. El manual de dd explica bien su uso, asi que omitiré más detalles.
Para nuestro caso que queremos copiar el programa al primer sector de la cabeza cero, cilindro cero, lo unico que debemos escribir es:

$dd if=boot1 of=/dev/fd0
Si tenemos un emulador como el qemu o el bochs se puede usar el emulador en lugar de tener que resetear la maquina a cada rato. Usando el qemu podemos hacer pruebas de la siguiente forma, usando el comando:
$cat boot1 | dd of=floppy conv=notrunc
Esto nos crea un archivo llamado floppy con las caracteristicas que necesitamos para que funcione, en realidad es una imagen de diskete lo que creamos. Ahora usamos qemu de la siguiente manera:
$qemu floppy
Eso es todo, veran que como anda su programa sin la necesidad de reiniciar.
Vale una advertencia, a mi no me funciona del todo bien con ciertas interrupciones el qemu, asi q a prestar mucha atencion.
De ahora en mas, para todos los ejemplos usaremos esta sentencia, asi q no la volveré a copiar, a menos que la deba cambiar, como sucedera más adelante.

El siguiente paso que se debe dar, es hacer que el programa haga algo, como imprimir un mensaje. Hay que tener en cuenta que no tenemos las librerias de ningun sistema operativo porque no estamos corriendo ningun SO aun. Por ahora la unica herramienta que se tiene son las interrupciones de nuestro amigo el BIOS. Hay interrupciones para hacer un monton de cosas, pero las que más nos interesan son las interrupciones para imprimir un mensaje en la pantalla y las de leer y escribir datos de un dispositivo. Estas son, las int 10h y las int 13h respectivamente. De la 10h lo que mas usaremos es la 0E para imprimir texto, y de la 13h la 02 que es para leer de un dispositivo.
Para ilustrar esto, doy un ejemplo sobre como imprimir un mensaje en la pantalla:

; boot2.asm
[BITS 16] ; Seteo a modo de 16bits q es como se encuentra el procesador al inicio
[ORG 0] ; Pongo el codigo desde el principio del segmento

jmp inicializar

; datos
mensajeInicial db "Este es mi primer bootstrap!",0x0D,0x0A,0 ; 0x0D,0x0A es un enter, 0 indica final de la cadena

; ---------------------------------------------------------------------------------
impMensaje:

imprimir:
lodsb ; Carga el byte q esta en ds:si en al
or al,al ; Mira si el caracter es 0 (fin)
jz finImpMensaje
mov ah,0x0E ; Indica q la interrupcion sera para imprimir por pantalla
mov bx,0x0009 ; Se indica pagina base y color de fondo
int 10h ; Llamada al BIOS
jmp imprimir

finImpMensaje: ret
; ---------------------------------------------------------------------------------

inicializar:
;el BIOS me pone en el segmento 07C0h, asi q seteo los segmentos a mi posicion actual
;Asi no tenemos q añadir 07C00h a todos nuestros datos
mov ax, 0x07C0
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax

cli ;desabilito las interrupciones
mov ax, 0x0000
mov ss, ax
mov sp, 0xFFFF
sti ;habilito nuevamente las interrupciones

main:
mov si, mensajeInicial ; Muestro el mensaje por pantalla
call impMensaje

ciclar:
jmp ciclar

TIMES 510-($-$$) DB 0 ; Hace q el archivo sea de 512 bytes de longitud
dw 0AA55h ; Indica al bios q es un bootstrap

El código es bastante autoexplicativo, pero voy a aclarar un par de cosas. Como se ve, primero setea los segmentos para que reflejen su posicion actual, dado q el BIOS nos pone en el segmento 07C0, actualizo los segmentos de datos y los seteo para que esten superpuestos con el segmento de codigo. La otra es el seteo de la pila, como con los demas segmentos, el de la pila tambien lo coloco en la misma direccion que el de codigo y pongo el puntero de la pila apuntando a la ultima direccion del segmento, dado q la pila crece hacia las direcciones mas chicas.
El uso de la interrupcion 10h es sencillo, primero indico en ah que operación voy a realizar, en este caso eligo 0E que indica q voy a imprimir en la pantalla; y en "al" pongo el caracter que deseo mostrar. En "bx" van los atributos de la impresion, es decir en que pagina imprimir, el color de fondo y el color del texto.

El siguiente paso a lograr es cargar un ejecutable al que despues saltaremos y que sera nuestro kernel. Este puede estar tambien en modo de 16 bits, o se puede construir de 32 bits, para ello hay q cambiar a modo protegido, cosa que explicaré en otro momento.
Para leer algo de un dispositivo hay que tener en cuenta cómo se pide a bajo nivel que se cargue un bloque de datos en la memoria. Los discos se direccionan mediante número de cabeza, número de cilindro y número de sector. Esto es asi por el motivo de que un disco puede tener varios platos, cada plato o conjunto de platos se divide en cilindros y cada cilindro se divide en sectores. A su vez, cada plato tiene como mínimo dos cabezas (puede tener mas) que leen el dato, una del lado de arriba y una del lado de abajo. Entonces con el número de cabeza indicamos que plato leeremos, con el número de cilindro decimos la ubicacion en el plato que leeremos y con el número de sector dentro del cilindro tendremos nuestro bloque.
Como dije anteriormente, en la cabeza cero, cilindro cero y sector uno tenemos nuestro bootstrap, asi que los demas datos y programas que queramos guardar en nuestro disco, deberan estar ubicados en cualquiera de los demas lugares.
Para explicar mejor me apoyo en un ejemplo sobre como cargar el programa que se encuentra en el floppy:

; boot3.asm
[BITS 16] ; Seteo a modo de 16bits q es como se encuentra el procesador al inicio
[ORG 0] ; Pongo el codigo desde el principio del segmento

jmp inicializar ; Voy al inicio del programa, salto todo lo q es declaraciones

;--------------------------------------------------------------
; Datos usados en el proceso de boot-loading
;--------------------------------------------------------------

mensajeInicial db "Que buen bootstrap!", 0x0D,0x0A,0

;--------------------------------------------------------------
; impMensaje imprime un mensaje en ascii por pantalla
;--------------------------------------------------------------

impMensaje:
imprimir:
lodsb ; Carga el byte q esta en ds:si en al
or al,al ; Mira si el caracter es 0 (fin)
jz finImpMensaje
mov ah,0x0E ; Indica q la interrupcion sera para imprimir por pantalla
mov bx,0x0009 ; Se indica pagina base y color de fondo
int 10h ; Llamada al BIOS
jmp imprimir

finImpMensaje: ret

;--------------------------------------------------------------
; Procedimiento que carga un programa contenido en el floppy
;--------------------------------------------------------------

cargarKernel:
reset:
mov ax, 0x0000 ; opcion para resetear el dispositivo
mov dl, 0x00 ; drive=0 floppy
int 0x13 ; interrupcion 13 del bios
jc reset ; si hay carry es q ocurrio un error, entonces intento de nuevo

cargar:
mov ax, 0x1000 ; ES:BX es donde se carga lo leido por la interrupcion
mov es, ax ; por lo que pongo en ES 1000
mov bx, 0x0000 ; y pongo en bx la dir 0000

mov ah, 0x02 ; con ah=2 digo q voy a leer
mov al, 0x05 ; con al indico la cantidad de sectores a leer
mov ch, 0x00 ; cilindro=0
mov cl, 0x02 ; sector=2 (en el 1 tengo el booteable)
mov dh, 0x00 ; cabeza=0
mov dl, 0x00 ; drive=0 es el floppy
int 13h ; invoco la interrupcion
jc cargar ; si ocurrio un error (carry=1), intento de nuevo

jmp 0x1000:0x00 ; salto a la direccion donde cargue el programa

ret ; nunca va a llegar a ejecutarse

;--------------------------------------------------------------
; Procedimiento que inicializa los registros
;--------------------------------------------------------------
inicializar:
;el BIOS me pone en el segmento 07C00h, asi q seteo los segmentos a mi posicion actual
;Asi no tenemos q añadir 07C00h a todos nuestros datos
mov ax, 0x07C0
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax

;call inicializar
;Salvo la direccion del dispositivo desde el que estoy booteando
mov [bootdrv], dl

;inicializo los valores para la pila
cli ;desabilito las interrupciones
mov ax, 0x0000
mov ss, ax
mov sp, 0xFFFF
sti ;habilito nuevamente las interrupciones
jmp main

;--------------------------------------------------------------
; main del programa
;--------------------------------------------------------------

main:
mov si, mensajeInicial ; Muestro el mensaje por pantalla
call impMensaje

call cargarKernel

TIMES 510-($-$$) DB 0 ; Hace q el archivo sea de 512 bytes de longitud
dw 0AA55h ; Indica al bios q es un bootstrap

Los detalles que debo aclarar son los de la interrupcion 13h. A esta interrupcion hay que pasarle varios parametros, lo primero que hago es resetear el dispositivo, esto es recomendable, y lo hago eligiendo la opcion de resetear de la interrupcion (ah=00) y diciendo que dispositivo voy a resetear (dh=00 indica el floppy). Luego lo que hago es cargar mi programa, para esto necesito pasar mas parametros, el que indica que voy a realizar una lectura (ah=02), la cantidad de sectores que quiero leer (al=05), que cilindro voy a leer (ch=00), que sector voy a leer (cl=02), de que cabeza (dh=00) y de que drive (dl=00 indica el floppy).
Los datos que cargue la interrupcion los cargará en la direccion ES:BX, por lo que lo primero que hago es setear estos valores a los que yo deseo. En este caso quiero que cargue mi programa en 1000:0000.
Luego de cargar el programa lo que hago es saltar a la direccion donde cargue el programa, asi comienza a ejecutarse.
Para hacer un ejemplo sencillo de programa a cargar, hago uno que imprima un caracter. Creo un programa simple en assembler que se llame kernel1.asm lo compilo y lo coloco en el sector dos de la cabeza cero, cilindro cero:

; kernel1.asm
[BITS 16]
[ORG 0]

mov al, 'd' ; voy a imprimir una d
mov ah, 0x0E ; indico q voy a usar la opcion 0E de la interrupcion, para imprimir por pantalla
mov bx, 0x0007 ; atributos de la impresion
int 10h

ciclar:
jmp ciclar ; ciclo

Lo compilamos con:

$nasm kernel.asm -o kernel
Y lo copiamos al segundo sector del disco con:
$dd if=kernel of=/dev/fd0 seek 1
El parametro seek 1 le indica a dd que salte el primer sector del disco y que lo empiece a grabar a partir del segundo sector. Esto lo hacemos porque en el primer sector se encuentra el bootstrap.
O bien, para usar el qemu, podemos hacer:
$cat boot3 kernel1 | dd of=floppy conv=notrunc
Y como antes:
$qemu floppy


Eso es todo por ahora. Con ello ya tenemos un "decente" bootstrap. Como siempre, espero que les haya resultado interesante.
Algunos artículos que me ayudaron en su momento a entender todo esto son:
BootStrap Tutorial - Matthew Vea
The booting process - por Gregor Brunmar
Lástima que los links que tenía ya no funcionan más...
Por otro lado, es super interesante la documentación encontrada en la página http://www.osdever.net/ hay de todo para aquellos aventureros.

4 comentarios:

Anónimo dijo...

Yo tuve un problema cuando mi a.img llego a pesar 34 kb, no siguió funcionando por eso.

Anónimo dijo...

Exelente aporte y por cierto muy interesante

Unknown dijo...

Muy buen articulo amigo, tengo una pregunta, si lo que quiero es crear un bootstrap en mi PC, y que con ese bootstrap cargar un programa que este en un dispositivo USB, como tendria que hacer?.

d3m4s1@d0v1v0 dijo...

Hola Ebers.
Para que el programa lea del dispositivo USB vas a tener que programar el driver, o bien adaptar alguno. Estoy medio alejado del tema ahora, tal vez los BIOS nuevos te permitan realizar algunas operaciones básicas sobre ciertos dispositivos.

Publicar un comentario