Tags

, , , , , , , , , , , , , ,

Esta entrada trata de la descripción y explicaciones sobre como elaborar un Bootloader o un microKernel (μKernel de aqui en adelante) desde 0. Me gustaria decir que no precisa conocimientos previos, pero no me es posible, creo conveniente saber programar, tener conocimientos de ensamblador y estar familiarizado con el entorno y la consola de Linux.

Este trabajo es resultado de un proyecto de mi materia de Sistemas Operativos. Más alla de simplemente crear el bootloader, el proyecto consistia en elaborar un μKernel con las funciones de un scheduler no expropiativo.

Este trabajo esta dividido en las siguientes secciones:

  1. ¿Qué es un scheduler no expropiativo?
  2. ¿Qué es un micronúcleo?
  3. Entender los principios del BootStrapping
  4. Ensamblador en Linux
  5. Máquina Virtual
  6. Primeras aproximaciones al ensamblador de Linux
  7. Primer Ejemplo
  8. Scheduler
  9. Bibliografía y Recursos

Ire actualizando esta entrada. El fin de semestre me agarra apretado.

1. ¿Qué es un scheduler no expropiativo?

Según la wikipedia: El planificador (o scheduler) es un componente funcional muy importante de los sistemas operativos multitarea y multiproceso, y es esencial en los sistemas operativos de tiempo real. Su función consiste en repartir el tiempo disponible de un microprocesador entre todos los procesos que están disponibles para su ejecución.

Existen dos tipos de algoritmos de calendarización (políticas de planificación), expropiativos y no expropiativos. Los primeros permiten que se ejecute el proceso hasta que acabe su trabajo; los segundos, asignan un tiempo de ejecución a cada proceso después del cual se calendariza otro proceso y así repetidamente, hasta que cada proceso acabe su trabajo. (Rev 26/Jun/09 : El título del proyectoera scheduler no expropiativo, pero segun la definición estamos en un error; al parecer nuestro scheduler realiza las tareas de un planificador expropiativo)

2. ¿Qué es un micronúcleo?

Micronúcleo (en inglés: microkernel) es un tipo de núcleo de un sistema operativo que provee un conjunto de primitivas o llamadas al sistema mínimas, para implementar servicios básicos como espacios de direcciones, comunicación entre procesos y planificación básica.

Nuestro μKernel va a realizar esta última tarea (planificación básica) con el código de un scheduler no expropiativo: administraremos dos procesos concurrentes que al terminar una tarea ceden voluntariamente el microprocesador.

3. Entender los principios del BootStrapping

BootStrapping es generalmente un término utilizado para describir el arranque, o proceso de inicio de cualquier ordenador.

Una vez que la PC arranca, comienza a ejecutarse el código que se encuentra en la dirección F000:FFF0 el cual pertenece al ROM-BIOS y es el encargado de realizar una serie de tests e inicializaciones, esta rutina se llama POST (Power On Self-Test). Luego inicializa sus datos: la Tabla de Interrupciones del BIOS (10h – 1Ah) y la Zona de Datos BIOS (40h:0h).

Después comienza a buscar un sector de arranque; la búsqueda mas usual consiste en mirar primero dentro del ‘floppy’ en su primer sector (comprueba que esté firmado con 55h, AAh en los bytes 511 y 512); si éste esta firmado para arrancar, lo carga en memoria y comienza a ejecutarlo.

Si no lo encuentra en el floppy, lo intenta en el disco duro. El primer sector sector se conoce como MBR (Master Boot Record). Al igual que con el floppy, Si encuentra la firma, carga y ejecuta el sector. Si no lo encuentra sale con un error (el conocido “Not Operating System found”).

NOTA: La secuencia de arranque desde un disco duro suele ser algo distinta: El MBR se carga en la dirección 0000h:7C00h, que al ejecutarse, normalmente (no hay un estándar), mueve su código a la dirección 0000h:0600h y sigue ejecutándose desde ahí. Procede entonces a escanear la tabla de particiones buscando una que sea de arranque, para cargar su primer sector en la dirección 0000h:7C00h.

Por lo tanto el sector de arranque debe colocarse en el Sector 1, Cilindro 0 y Cabeza 0 de la unidad desde la que se desea arrancar. El BIOS carga el sector de arranque en la dirección de memoria 7C00h y comienza a ejecutarlo desde esa dirección. Para ello coloca los siguientes valores: CS=0h, IP=7C00h. Además en DL coloca un valor indicativo de la unidad desde la que se ha cargado el sector de arranque: 1h = floppy, 80h = disco duro primario.

El sector de arranque cargado por el BIOS toma el control. Este sector lo llamaremos cargador primario. Normalmente este cargador primario se encargaría de mirar la tabla de particiones, buscando una que fuese ejecutable (con un SO). La elección se puede hacer mediante un menú en el caso de que hubiese varias (como el caso de GRUB o LILO). Una vez elegida la partición la carga en memoria, normalmente se trata de un cargador secundario, que será el que cargue en memoria el kernel elegido. Si el kernel es mayor de 512k o 640k, deberá pasar del Modo Real al Modo Protegido.

El Modo Real está caracterizado por 20 bits de espacio de direcciones segmentado (significando que solamente se puede direccionar 1 MB de memoria), acceso directo del software a las rutinas del BIOS y el hardware periférico, y no tiene conceptos de protección de memoria o multitarea a nivel de hardware.

Por otra parte el Modo Protegido tiene una serie de características diseñadas para mejorar las multitareas y la estabilidad del sistema, como protección de memoria, y soporte de hardware para memoria virtual así como de conmutación de tareas. En el 80386 y procesadores de 32 bits posteriores se agregó un sistema de paginación que es parte del modo protegido.

El sector de arranque o cargador primario, será lo único que necesitemos para poder pasar el control a nuestro μKernel. Las características del sector de arranque son:

  • Tiene un tamaño de 512 bytes
  • En la posición 1FEh (510 en decimal) debe tener la palabra AA55h

También debe tenerse en cuenta que el BIOS lo carga en la posición de memoria 7C00h, de forma que a la hora de programarlo hay que tener cuidado con las referencias a memoria. En la práctica esto se puede hacer de varias formas:

  • Inicializar los registros de segmento de forma que apunten al comienzo del sector de arranque.
  • Usar la pila para igualar el registro DS al registro CS. En ensamblador.
  • Sumar 7C00h a todas las referencias a memoria.

4. Ensamblador en Linux

El proyecto original que dejaron en la escuela, consistía en hacer que una computadora arrancara con el código de un Scheduler no Expropiativo, sin necesidad de un sistema operativo. El código de dicho Scheduler estaba escrito en lenguaje ensamblador con la sintaxis de DOS y se compilaba uy ensamblaba con los programas TASM y TLINK de Borland.

Cuando comencé a estudiar todo lo antes escrito, encontré muchos lugares donde se explicaba el proceso de escribir Bootloaders experimentales y algunos otros mas avanzados. Sin embargo no encontraba nada sobre Bootloaders escritos en DOS. Así que me preguntaba si tendría que pasar el código del Scheduler a algun lenguaje ensamblador de linux.

Navegando di con la siguiente información que justifica por qué fue necesaria esa transición. Las principales diferencias entre el ensamblador de Linux y el ensamblador de DOS son:

  • En el ensamblador de DOS, la mayoria de las cosas se hacen con la interrupcion 21h de y algunas interrupciones del BIOS como la 10h o la 16h. En Linux, todas estas funciones son manejadas por el kernel. Todo se realiza con “llamadas de sistema al kernel”. Y para llamar al kernel se utiliza la interrupcion 80h. Una de las cosas mas maravillosas de las llamadas al sistema de Linux, es que hay menos de ellas que en DOS, pero son por mucho, más prácticas. Por ejemplo, las llamdas crean archivos, manejan procesos, etc.
  • Linux es un verdadero sistema operativo de 32-bits de modo protegido, lo cual da opción de hacer cosas realmente en ensamblador de 32-bits. Este código de 32-bits corre en el modelo de memoria plana (que básicamente significa que no te tienes que preocupar del todo por segmentación y su manejo). Nunca tendrás que sobrecargar un segmento o modificar un segmento de registro. Cada dirección es de 32 bits de longitud y contiene una parte de offset (desplazamiento).
  • En ensamblado de 32-bits puedes usar los registros extendidos de 32-bits (EAX, EBX, ECX, etc) en lugar de los registros normales de 16-bits (AX, BX, CX,etc).
  • DOS está muerto; es de 16-bits; es obsoleto. Los que escriben código en ensamblador de DOS son viejos hackers con demasiado apego a sus 386 como para dejarlas ir. El ensamblador de Linux tiene aplicaciones prácticas (partes del Sistema Operativo, drivers de Hardware, etc.)

5. Máquina Virtual

Otro de las paradas qur tuve que hacer en el análisis de este problema fue el medio en el que iba a cargar el BootLoader y sus pruebas. Como ya vimos anteriormente en el proceso de BootStrapping el BIOS busca en distintos dispositivos (floppy, CD/DVD-ROM, HDD, red, y actualmente hasta FlashMemory o mejor conocidas como USB) el sector con la firma que lo califica como “booteable“.

La computadora desde donde pretendía escribir el BootLoader es una NetBook, de modo que no hay unidad de CD/DVD, ya ni hablemos de unidad lectora de diskettes. Cargar el BootLoader en una memoria USB equivaldría a muchisimos reinicios de mi máquina (tanto para hacer las pruebas, como para regresar de ellas). En lo personal, no me gusta apagar la computadora cortando energía o forzando el cierre (con el método de presionar durante 5 segundos el botón de apagado/encendido).

Normalmente (y por que la escuela así lo requiere) tengo que estar saltando entre Winbugs y alguna distribución de Linux. Para no tener que reiniciar cada vez, opte por usar una máquina virtual. En la red hay muchas herramientas que nos permiten emular sistemas operativos completamente funcionales dentro de otros sistemas operativos.

Pensando un poco e investigando sobre las imagenes de dispositivo (ISO para CD’s /DVD’s, IMG para diskettes, etc.) me decidí por crear una máquina virtual para tener donde probar el Bootloader. Es recomendable no hacer pruebas de Bootloader con discos físicos para no dañar la integridad de estos en algún error, y para prevenir daños al dispositivo lector/escritor.

En este caso, lo ideal fue usar una imagen de diskette y montarla sobre una máquina virtual. La herramienta que uso es VirtualBox de Sun. En la red hay muchos manuales y tutoriales sobre como instalar la herramienta y como crear y configurar máquinas virtuales. Las especificaciones de la máquina que yo ocupé son:

  • Tipo OS: Otro/Desconocido
  • Memoria Base (RAM): 64MB
  • Memoria de Video: 4MB
  • Orden de arranque: Floppy, CD/DVD, IDE primario
  • IDE primario: 2.00 GB
  • Diskette: montado con la imagen que contiene el bootloader.

Una descripción detallada de cómo instalar el software y configurar una máquina virtual, la puedes encontrar en este mismo blog.

6. Primeras aproximaciones al ensamblador de Linux

El código del Scheduler que vamos a usar estaba originalmente en lenguaje DOS. Por las razones expuestas arriba codificaremos en ensamblador de Linux. Esto planeta varias cuestiones a tratar.

En primer lugar, en Linux existen varios lenguajes para hacer código en ensamblador (NASM, GAS, MASM, etc…) es importante elegir uno para apegarnos a su sintaxis y poder explotar el lenguaje al máximo.

En el periodo de pruebas e investigación que lleve a cabo, probe NASM y GAS. Tienen algunas diferencias sútiles uno del otro, y abismales con respecto a TASM. De los dos que probé y de la información que pude recabar, probar y contrastar, elegi GAS.

GAS: GNU Assembler (tambien conocido como ‘as’. Su documentación se encuentra en: http://sourceware.org/binutils/docs/as/index.html .

NASM: Netwide Assembler. Su documentación se encuentra en: http://www.nasm.us/doc/

La razón principal por la que me decanté por GAS es que la mayoria de los ejemplos que encontré en la red para hacer Bootloaders estan escritos en este lenguaje. Sí los hay para NASM pero me acomodé mas con GAS y espero que ustedes tambien lo encuentren cómodo y fácil de entender.

7. Primer Ejemplo

El siguiente código corresponde a nuestro primer Bootloader😀. Pongo el código y lo explico a continuación.

#ejemplo1.s

.text                 # 1
.code16               # 2
.global main          # 3

main:                 # 4

movb $0x41,%al    # 8
movb $0x0e,%ah    # 7
movw $0x00,%bx    # 6
int $0x10         # 5

bucle:                # 9

jmp bucle         # 10

.org 510              # 11
.byte 0xAA            # 12
.byte 0x55            # 13

El símbolo de sharp (#) dentro de la sintaxis de GAS indica el inicio de un comentario. Explico línea por línea:

  1. .text: es la directiva para establecer el segmento de ‘texto’ que corresponde a la parte del programa donde esta el codigo del programa.
  2. .code16: esta directiva indica al programa y al enlazador (y mas tarde al procesador) que se va a trabajar en Modo Real con código de 16-bits, es decir que vamos a usar los registros normales en su tamaño de una palabra (2bytes). El Modo Protegido se consigue mediante codigo de 32-bits y usa los registros extendidos.
  3. .global: esta directiva establece el segmento marcado como ‘main’ visible a otros programas y procesos. Sin ella, el ensamblador no sabria por donde comenzar a ejecutarse.
  4. main: segmento principal. En la codificación con gcc es extremadamente común que la función principal se llame así, de hecho en C asi se define la función principal. Nosotros podríamos ponerle cualquier otro nombre, e indicarle al procesador por medio de la directiva .global que comience a correr por ahi y al momento de la compilación indicarlo tambié. Por convención dejaremos el main.
  5. Inverti el orden de estas líneas por que pertenecen a los pasos previos a la interrupción 10h. Esta línea es la llamada a la interrupción en sí, se hace por medio del comando int $0x10. Esta interrupción posee distintas funciones. Las funciones de las interrupciones del BIOS se indican por medio del paso de parámetros en los registros del μP. La función que se ocupó en este ejemplo es TeleType, y sus parametros son:
    AH = 0Eh → Función TeleType
    AL = 41h → Caracter a escribir
    BH = 00h → Número de página
    BL = 00h → Color de fondo y color de fuente (solo en modo gráfico)
  6. Esta línea corresponde al paso del parametro de número de página y del color de fondo y fuente. La página es 00h (la página actual) y el color de fondo y fuente es tambien, 00h (lo cual indica los colores predeterminados, en otro ejemplo veremos como modificar estos valores). Lo pasamos por palabra completa, de este modo el registro BX quedara como 0000h con 00h en su parte alta y 00h en su parte baja.
  7. Aqui indicamos la funcion a usar, 0Eh como ya vimos y la colocamos en la parte alta del registro AX.
  8. Aqui indicamos el caracter a imprimir, este ejemplo imprime el caracter 41h, que corresponde a la letra A en su valor hexadecimal.
  9. bucle: segmento de bucle. Indica el segmento al que saltaremos para hacer un bucle infinito.
  10. jmp bucle: salto incondicional. Una vez que la ejecución del programa alcanza este punto del código, lo que hará será brincar al punto del programa indicado; en este caso, a la etiqueta bucle definida anteriormente.
  11. .org 510: Esta directiva mueve el puntero de locación a una nueva posición, ya sea absoluta (como nuestro caso) o una subsección del código. En este caso nosotros nos movemos al byte 510. Recordemos que los bytes 511 y 512 deben contener la firma que clasifica el sector (un sector tiene 512 bytes) como booteable.
  12. .byte 0xAA: La directiva .byte reserva un espacio en memoria del tamaño de 8 bits (1 byte) con el contenido indicado (0xAA).
  13. .byte 0x55: Al igual que la anterior, reservamos un espacio en memoria del tamaño de 1 byte con el contenido 0x55. Las líneas 11, 12 y 13, corresponden a la firma que indica que el segmento es ‘booteable’ y que el procesador puede cargar y ejecutar nuestro código.

Para ensamblar el código anterior, utilizamos el siguiente comando:

as ejemplo1.s -o ejemplo1.o

  • as: llama el ensamblador con el parámetro ejemplo1.s que contiene el código en éste lenguaje.
  • -o: parámetro de as para establecer el fichero de salida que contendra el código objeto, en este caso ejemplo1.o.

Para compilar el código objeto, utilizamos:

ld -o ejemplo1.bin -Ttext 0x0 -e main ejemplo1.o --oformat binary

  • ld: el linker de AS.
  • -o: fichero de salida, en este caso ejemplo1.bin.
  • -Ttext 0x0: indica donde cargar el segmento de texto.
  • -e main: indica el punto por el cual el programa debera comenzar a correrse, en nuestro caso indicamos main que es la etiqueta donde inicia nuestro código.
  • ejemplo1.o: fichero de entrada
  • --oformat binary: indica el formato del contenido de salida, para que el programa sea ejecutable debe estar en modo binario, sin firmas de ejecución de ninguna clase, binario plano.

Para copiar a algun sector de algun dispositivo:

dd if=ejemplo1.bin of=ukernel.img

  • dd: comando de UNIX para copiar y convertir bytes RAW con distintas opciones.
  • if (in file): fichero de entrada, en este caso será ejemplo1.bin (resultado de ensamblar y compilar nuestro código a un fichero ejecutable en binario plano)
  • of (out file): fichero de salida, este puede ser un dispositivo montado, una imagen de diskette (como nuestro caso), una imagen de cd (*.iso), etc. ukernel.img corresponde a una imagen de diskette la cual será montada en la máquina virtual para ver nuestro bootloader corriendo.

8. Scheduler

Como siguiente paso, voy a explicar cómo trabaja el scheduler que vi en clase, es interesante puesto que maneja muchos conceptos de lenguaje ensamblador y de lo más elemental de un S.O.

Antes que nada, en informática el vector de interrupciones es un vector que contiene el valor que apunta a la dirección en memoria del gestor de una interrupción. En muchas arquitecturas de computación típicas, los vectores de interrupción se almacenan en una tabla en una zona de memoria, la llamada tabla de vectores de interrupción, de modo que cuando se atiende una petición de interrupción de número n, el sistema, tras realizar eventualmente algunas tareas previas (tales como salvar el valor de ciertos registros) transfiere el control a la dirección indicada por el elemento n-ésimo de dicha tabla (fuente Wikipedia).

Bibliografía y Recursos

  1. μKernel: Scheduler no Expropiativo. Versión original de este mismo documento. Reporte que entregué al finalizar el semestre.
  2. Bootstrapping. Información de la Wikipedia sobre el proceso de arranque de una computadora.
  3. Secuencia de Arranque del Procesador (i386+). Explicación del método de arranque de los procesadores i386 y superiores, y ejemplos de bootloader.
  4. Linux Assembly Tutorial. Guía paso a paso del lenguaje ensamblador bajo Linux.
  5. VirtualBox Downloads. Sitio de VirtualBox donde poder descargar las distintas versiones de esta herramienta.
  6. Linux assemblers: A comparison of GAS and NASM. Comparativa entre los dos lenguajes para escribir código en ensamblador desde Linux.
  7. Using as.  Documentación del lenguaje GAS
  8. NASM Manual. Documentación del lenguaje NASM.
  9. Int 10h. Explicación de la interrupción 10h del BIOS, funciones y parámetros (Wikipedia).