StellatorOS: Cargando nuestro kernel en C


Ahora que ya tenemos unos vastos conocimientos sobre ASM y C podemos proceder a arrancar nuestro kernel, los pasos a seguir serán:

  • Escribir y compilar el código del kernel
  • Escribir y ensamblar el código del bootloader
  • Crear una imagen de disco que incluya nuestro bootloader y el kernel
  • Cargar el kernel en RAM
  • Cambiar a modo protegido de 32 bits
  • Ejecutar nuestro kernel

 

Antes de comenzar es recomendable que leas estos artículos anteriores:

Nuestro kernel va a ser muy sencillo ahora por ahora, tan solo escribiremos el carácter X en la esquina superior izquierda de la pantalla, para ello accederemos a la memoria de video utilizando un puntero.

vi kernel.c
void main () {
    // Create a pointer to a char , and point it to the first text cell of video memory (i.e. the top - left of the screen )
    char* video_memory = (char*) 0xb8000 ;

    // At the address pointed to by video_memory , store the character ’X’ (i.e. display ’X’ in the top - left of the screen ).
    *video_memory = 'X';
}

Generamos el fichero objeto, si estamos en un sistema de 64 bits es importante indicarle al compilador que genere código ensamblador de 32 bits, en caso contrario el bootloader funcionará pero cuando llegue a la parte del kernel las instrucciones ASM serán incorrectas:

cc -g -m32 -ffreestanding -c kernel.c -o kernel.o

Linkamos el objeto indicando en que dirección de memoria(0x1000) debe cargarse:

ld -o kernel.bin -Ttext 0x1000 kernel.o --oformat binary

Para simplificar la carga del kernel vamos a escribir la imagen justo después de nuestro bootloader, de este modo no tendremos que localizar donde está.

vi boot_sect.asm
[org 0x7c00]
    KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel

    mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot
    mov bp, 0x9000
    mov sp, bp

    mov bx, MSG_REAL_MODE 
    call print
    call print_nl

    call load_kernel ; read the kernel from disk
    call switch_to_pm ; disable interrupts, load GDT,  etc. Finally jumps to 'BEGIN_PM'
    jmp $ ; Never executed

%include "./boot_sect_print.asm"
%include "./boot_sect_print_hex.asm"
%include "./boot_sect_disk.asm"
%include "./32bit-gdt.asm"
%include "./32bit-print.asm"
%include "./32bit-switch.asm"

[bits 16]
load_kernel:
    mov bx, MSG_LOAD_KERNEL
    call print
    call print_nl

    mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000
    mov dh, 1
    mov dl, [BOOT_DRIVE]
    call disk_load
    ret

[bits 32]
BEGIN_PM:
    mov ebx, MSG_PROT_MODE
    call print_string_pm
    call KERNEL_OFFSET ; Give control to the kernel
    jmp $ ; Stay here when the kernel returns control to us (if ever)


BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten
MSG_REAL_MODE db "Started in 16-bit Real Mode", 0
MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0
MSG_LOAD_KERNEL db "Loading kernel into memory", 0

; padding
times 510 - ($-$$) db 0
dw 0xaa55

Todos los ficheros incluidos son los explicados en artículos anteriores, el único que se ha modificado ligeramente es boot_sect_disk.asm que se ha comentado la línea donde se indicaba desde que disco debía leer, ahora se le pasa desde el programa principal:

vi boot_sect_disk.asm
; load 'dh' sectors from drive 'dl' into ES:BX
disk_load:
    pusha; save all registers to stack before executing function
    push dx; save dx to stack, we are goig to use dl/dh meanwhile

    mov ah, 0x02 ; read from disk action when int13 is fired
    
    ;mov dl, 0x80 ; use first hard disk
    mov al, dh   ; number of sectors to read
    mov dh, 0x00 ; use first header
    mov ch, 0x00 ; read from first cilinder(track)
    mov cl, 0x02 ; sector number to start reading

    int 0x13      ; BIOS interrupt
    jc disk_error ; if error (stored in the carry bit)

    pop dx; recover dx content from stack
    cmp al, dh    ; BIOS also sets 'al' to the # of sectors read. Compare it.
    jne sectors_error
    
    popa; restore registers state
    ret


disk_error:
    mov bx, DISK_ERROR
    call print
    call print_nl
    mov dh, ah ; ah = error code, dl = disk drive that dropped the error
    call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html
    jmp disk_loop

sectors_error:
    mov bx, SECTORS_ERROR
    call print

disk_loop:
    jmp $

DISK_ERROR: db "Disk read error", 0
SECTORS_ERROR: db "Incorrect number of sectors read", 0

Generamos la imagen de disco tal como hemos indicado, bootloader(primer sector) + SO(siguiente sector)

nasm -f bin boot_sect.asm -o boot_sect.bin
cat boot_sect.bin kernel.bin > os-image

La cargamos en Qemu:

qemu-system-x86_64 os-image

Como véis hemos logrado cargar nuestro kernel escrito en C.


DEBUG:

Podemos ver las instrucciones en ensamblador de la imagen de disco mediante ndisasm:

ndisasm -b 32 os-image

También podemos verlas con xdd pero solo los opcodes, pero veremos de forma clara donde acaba el bootloader y donde empieza nuestro kernel:

xxd os-image
00000000: 8816 5c7d bd00 9089 ecbb 5d7d e80b 00e8  ..\}......]}....
00000010: 1a00 e820 01e8 ee00 ebfe 608a 073c 0074  ... ......`..<.t
00000020: 09b4 0ecd 1083 c301 ebf1 61c3 60b4 0eb0  ..........a.`...
00000030: 0acd 10b0 0dcd 1061 c360 b900 0083 f904  .......a.`......
00000040: 741c 89d0 83e0 0f04 303c 397e 0204 07bb  t.......0<9~....
00000050: 6b7c 29cb 8807 c1ca 0483 c101 ebdf bb66  k|)............f
00000060: 7ce8 b6ff 61c3 3078 3030 3030 0060 52b4  |...a.0x0000.`R.
00000070: 0288 f0b6 00b5 00b1 02cd 1372 075a 38f0  ...........r.Z8.
00000080: 7512 61c3 bb9c 7ce8 90ff e89f ff88 e6e8  u.a...|.........
00000090: a7ff eb06 bbac 7ce8 80ff ebfe 4469 736b  ......|.....Disk
000000a0: 2072 6561 6420 6572 726f 7200 496e 636f   read error.Inco
000000b0: 7272 6563 7420 6e75 6d62 6572 206f 6620  rrect number of 
000000c0: 7365 6374 6f72 7320 7265 6164 0000 0000  sectors read....
000000d0: 0000 0000 00ff ff00 0000 9acf 00ff ff00  ................
000000e0: 0000 92cf 0017 00cd 7c00 0060 ba00 800b  ........|..`....
000000f0: 008a 03b4 403c 0074 0b66 8902 83c3 0183  ....@<.t.f......
00000100: c202 ebed 61c3 fa0f 0116 e57c 0f20 c066  ....a......|. .f
00000110: 83c8 010f 22c0 ea1b 7d08 0066 b810 008e  ...."...}..f....
00000120: d88e d08e c08e e08e e8bd 0000 0900 89ec  ................
00000130: e816 0000 00bb 997d e8df fee8 eefe bb00  .......}........
00000140: 10b6 018a 165c 7de8 23ff c3bb 797d 0000  .....\}.#...y}..
00000150: e896 ffff ffe8 a692 ffff ebfe 0053 7461  .............Sta
00000160: 7274 6564 2069 6e20 3136 2d62 6974 2052  rted in 16-bit R
00000170: 6561 6c20 4d6f 6465 004c 616e 6465 6420  eal Mode.Landed 
00000180: 696e 2033 322d 6269 7420 5072 6f74 6563  in 32-bit Protec
00000190: 7465 6420 4d6f 6465 004c 6f61 6469 6e67  ted Mode.Loading
000001a0: 206b 6572 6e65 6c20 696e 746f 206d 656d   kernel into mem
000001b0: 6f72 7900 0000 0000 0000 0000 0000 0000  ory.............
000001c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.
00000200: 5589 e550 b800 800b 0089 45fc 8b45 fcc6  U..P......E..E..
00000210: 0058 83c4 045d c300                      .X...]..

Una forma de debugear en tiempo real es mediante GDB y Qemu, pero para que GDB pueda ver el nombre de las funciones y resulte mas sencillo depurar tnedremos que generar el fichero de símbolo:

ld -o kernel.elf -Ttext 0x1000 kernel.o

Arrancamos Qemu para que se quede a la espera de ordenes desde una conexión de depurado remota:

qemu-system-x86_64 os-image -s -S

Arrancamos GDB:

gdb -ex "set architecture i386:x86-64" -ex "set disassembly-flavor intel" -ex "target remote localhost:1234" -ex "symbol-file kernel/kernel.elf"

Ponemos un breakpoint justo en el inicio del bootloader:

break *0x7c00
continue

Podemos ver el inicio y fin del bootloader en RAM, debería de coincidir con la salida de xxd:

x/16xb 0x7C00
0x7c00: 0x88 0x16 0x5c 0x7d 0xbd 0x00 0x90 0x89
0x7c08: 0xec 0xbb 0x5d 0x7d 0xe8 0x0b 0x00 0xe8
x/16xb 0x7DF0
0x7df0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7df8: 0x00 0x00 0x00 0x00 0x00 0x00 0x55 0xaa

Ponemos un breakpoint en la posición de memoria donde el bootloader escribirá el SO:

break *0x1000
continue

Comprobamos que los valores sean correctos, deberían coincidir con los de la salida de xxd:

x/32b 0x1000
0x1000: 0x55 0x89 0xe5 0x50 0xb8 0x00 0x80 0x0b
0x1008: 0x00 0x89 0x45 0xfc 0x8b 0x45 0xfc 0xc6
0x1010: 0x00 0x58 0x83 0xc4 0x04 0x5d 0xc3 0x00
0x1018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Ejecutamos paso a paso las instrucciones ASM mediante el comando "si", deberiamos de ver la ejecución de todo el proceso incluida la ejecución de nuestro kernel.

Si te ha gustado el artículo puedes invitarme a un redbull aquí.
Autor: kr0m -- 17/06/2020 01:24:04