Kernel mínimo en C

Para realizar el lab, se debe instalar el software necesario. La entrega se realiza en horario de clase del día indicado siguiendo las instrucciones de entrega.

El esqueleto para este lab se encuentra en el repositorio https://github.com/fisop/labs, rama kern0 (la cual no necesita ninguna integración previa).

Índice

Compilar un kernel y lanzarlo en QEMU

  • Lecturas obligatorias1

    • BRY2
      • cap. 1: §1-10
  • Lecturas recomendadas

    • BRY2
      • cap. 2: §1-3

El siguiente código, una vez lanzado en el procesador, constituye un kernel completo con una única tarea: mantener la computadora prendida.

1
2
comienzo:
    jmp comienzo

Es equivalente al siguiente bucle infinito en C:

1
2
3
4
void comienzo(void) {
    while (1)
        continue;
}

El código se compila con gcc, siempre (en estos labs) en modo 32-bits:

1
$ gcc -g -m32 -O1 -c kern0.c

Una vez compilado el código del kernel, se debe generar una imagen binaria que pueda ser ejecutada bien en una computadora física, bien en un simulador como QEMU. Para ello, se deben enlazar los objetos (archivos *.o) con las instrucciones de arranque que correspondan según la arquitectura.

El estándar Multiboot simplifica enormemente la tarea, pues permite arrancar un kernel directamente en protected mode (32-bits) sin tener que cargar la imagen desde disco, ni realizar el paso desde real mode a mano. Grub y muchos otros gestores de arranque ofrecen soporte para multiboot; en QEMU se activa mediante la opción -kernel (versión 1 de multiboot solamente).

Para indicar al gestor de arranque que configure multiboot se debe incluir, en los primeros bytes del binario final, la constante numérica 0x1BADB002 y el CRC adecuado, ambos alineados a 32-bits; por ejemplo, en un archivo boot.S:

1
2
3
4
5
6
7
8
9
#define MAGIC 0x1BADB002
#define FLAGS 0
#define CRC ( -(MAGIC + FLAGS) )

.align 4
multiboot:
    .long MAGIC
    .long FLAGS
    .long CRC

Así, el proceso completo para generar la imagen es:

1
2
3
4
5
6
7
8
# Compilar C y ASM
$ gcc -g -m32 -O1 -c kern0.c boot.S

# Enlazar
$ ld -m elf_i386 -Ttext 0x100000 kern0.o boot.o -o kern0

# Lanzar
$ qemu-system-i386 -serial mon:stdio -kernel kern0

Ej: kern0-boot

Compilar kern0 y lanzarlo en QEMU tal y como se ha indicado. Responder (en el archivo kern0.md presente en el esqueleto):

  • ¿emite algún aviso el proceso de compilado o enlazado? Si lo hubo, indicar cómo usar la opción --entry de ld(1) para subsanarlo.

  • ¿cuánta CPU consume el proceso qemu-system-i386 mientras ejecuta este kernel? ¿Qué está haciendo?2

Ej: kern0-quit

Para finalizar la ejecución de QEMU, se puede cerrar directamente la ventana. Alternativamente, y por haber especificado la opción -serial mon:stdio, se puede controlar la simulación desde la terminal en que se lanzó:

  1. Lanzar una vez más el kernel, y verificar que se puede finalizar su ejecución desde la terminal mediante la combinación de teclas Ctrl-a x.3

  2. Asimismo, la combinación Ctrl-a c permite entrar al “monitor” de QEMU, desde donde se puede obtener información adicional sobre el entorno de ejecución. Ejecutar el comando info registers en el monitor de QEMU, e incluir el resultado en la entrega. (El mismo comando, info reg, existe también en GDB.)

Ejemplo:

1
2
3
4
5
6
7
8
9
$ qemu-system-i386 -serial mon:stdio -kernel kern0
<Ctrl-a c>

QEMU 2.8.1 monitor - type 'help' for more information
(qemu) info registers↩︎
EAX=...

(qemu) <Ctrl-a x>
QEMU: Terminated

Ej: kern0-hlt

Un sistema operativo debe mantener siempre, al menos, un flujo de ejecución en la CPU. De lo contrario, finalizaría la ejecución del kernel.

El ciclo infinito en comienzo() asegura un flujo de ejecución constante, pero mantiene a la CPU permanentemente ocupada.

La instrucción hlt se usa para detener la CPU cuando no hay trabajo “real” que realizar. La instrucción se puede incluir directamente en código C así:

1
2
3
4
void comienzo(void) {
    while (1)
        asm("hlt");
}

Leer la página de Wikipedia HLT (x86 instruction), y responder:

  • una vez invocado hlt ¿cuándo se reanuda la ejecución?

Usar el comando powertop para comprobar el consumo de recursos de ambas versiones del kernel. En particular, para cada versión, anotar:

  • columna Usage: fragmento de tiempo usado por QEMU en cada segundo.

  • columna Events/s: número de veces por segundo que QEMU reclama la atención de la CPU.

Ej: kern0-gdb

  • Lecturas obligatorias

    • BRY2
      • cap. 3: §1-5
  • Lecturas recomendadas

Un kernel no puede correr directamente desde GDB, ya que la ejecución de dicho kernel debe ocurrir afuera del sistema operativo sobre el cual corre GDB. No obstante, GDB permite depurar de manera remota. Para ello, GDB y el entorno remoto se comunican por red mediante un protocolo específico (GDB Remote Serial Protocol).

En este caso, el “entorno remoto” es QEMU, el cual implementa el protocolo específico de GDB con la opción -gdb y un número de puerto TCP. Además, con la opción -S se indica a QEMU que no comience la ejecución del sistema hasta que así lo ordene remotamente GDB:

1
2
3
$ qemu-system-i386 -serial mon:stdio \
                   -S -kernel kern0  \
                   -gdb tcp:127.0.0.1:7508

Entonces, desde otra terminal GDB se puede comunicar con esta ejecución del kernel:

1
$ gdb -q -s kern0 -n -ex 'target remote 127.0.0.1:7508'

Mostrar una sesión de GDB en la que se realicen los siguientes pasos:4

  • poner un breakpoint en la función comienzo (p.ej. b comienzo)

  • continuar la ejecución hasta ese punto (c)

  • mostrar el valor del stack pointer en ese momento (p $esp), así como del registro %eax en formato hexadecimal (p/x $eax).5 Responder:

    • ¿Por qué hace falta el modificador /x al imprimir %eax, y no así %esp?
    • ¿Qué significado tiene el valor que contiene %eax, y cómo llegó hasta ahí? (Revisar la documentación de Multiboot, en particular la sección Machine state.)
  • el estándar Multiboot proporciona cierta informacion (Multiboot Information) que se puede consultar desde la función principal vía el registro %ebx. Desde el breakpoint en comienzo imprimir, con el comando x/4xw, los cuatro primeros valores enteros de dicha información, y explicar qué significan. A continuación, usar y mostrar las distintas invocaciones de x/... $ebx + ... necesarias para imprimir:

    • el campo flags en formato binario
    • la cantidad de memoria “baja” en formato decimal (en KiB)
    • la línea de comandos o “cadena de arranque” recibida por el kernel (al igual que en C, las expresiones de GDB permiten dereferenciar con el operador *)
    • si está presente (indicar cómo saberlo), el nombre del gestor de arranque.

El buffer VGA

El siguiente kernel imprime, de manera bastante rudimentaria, un mensaje por pantalla al arrancar, usando el buffer VGA:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define VGABUF ((volatile char *) 0xb8000)

void comienzo(void) {
    volatile char *buf = VGABUF;

    *buf++ = 79;
    *buf++ = 47;
    *buf++ = 75;
    *buf++ = 47;

    while (1)
        asm("hlt");
}

Ej: kern0-vga

Explicar el código anterior, en particular:

  • qué se imprime por pantalla al arrancar.
  • qué representan cada uno de los valores enteros (incluyendo 0xb8000).
  • por qué se usa el modificador volatile para el puntero al buffer.

Ahora, implementar una función más genérica para imprimir en el buffer VGA:

1
2
static void
vga_write(const char *s, int8_t linea, uint8_t color) { ... }

donde se escribe la cadena en la línea indicada de la pantalla (si linea es menor que cero, se empieza a contar desde abajo).

  1. Consultar lista de abreviaturas en la bibliografía↩︎

  2. Se puede comprobar el uso de CPU de cada proceso en el sistema mediante el comando: top -d 1 (salir pulsando q). ↩︎

  3. Esto es: presionar Ctrl-a, soltar, y a continuación teclear la letra x en minúscula. ↩︎

  4. Para cuidar tanto el medio ambiente como sus baterías, se recomienda haber completado el ejercicio kern0-hlt primero. ↩︎

  5. Existe para cada registro una variable asociada. GDB define también cuatro variables genéricas que se definen según la arquitectura actual. En el caso de x86, la asociación es:

    • $eip :: $pc (program counter)
    • $esp :: $sp (stack pointer)
    • $ebp :: $fp (frame pointer)
    • $eflags :: $ps (processor status)

    ↩︎