This page looks best with JavaScript enabled

MercuryOS Loading our kernel in C

 ·  🎃 kr0m

Now that we have vast knowledge about ASM and C, we can proceed to boot our kernel. The steps to follow are: write and compile the kernel code, write and assemble the bootloader code, create a disk image that includes our bootloader and kernel, load the kernel into RAM, switch to 32-bit protected mode, and execute our kernel.

Before starting, it is recommended that you read these previous articles:


Our kernel will be very simple for now, we will only write the character X in the upper left corner of the screen, for which we will access the video memory using a pointer.

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';
}

We generate the object file. If we are on a 64-bit system, it is important to tell the compiler to generate 32-bit assembly code. Otherwise, the bootloader will work, but when it reaches the kernel part, the ASM instructions will be incorrect:

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

We link the object indicating at which memory address (0x1000) it should be loaded:

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

To simplify the kernel loading, we will write the image just after our bootloader, so we won’t have to locate where it is.

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

All included files are explained in previous articles, the only one that has been slightly modified is boot_sect_disk.asm, where the line indicating from which disk it should read has been commented out, now it is passed from the main program:

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

We generate the disk image as we have indicated, bootloader (first sector) + OS (next sector):

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

The contents of the RAM and the disk image will be as shown in the following image:

We load it in Qemu:

qemu-system-x86_64 os-image

As you can see, we have managed to load our kernel written in C.


DEBUG:

We can see the assembly instructions of the disk image using ndisasm:

ndisasm -b 32 os-image

We can also see them with xdd but only the opcodes, but we will clearly see where the bootloader ends and where our kernel begins:

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...]..

One way to debug in real-time is through GDB and Qemu, but for GDB to be able to see the function names and make debugging easier, we will have to generate the symbol file:

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

We start Qemu so that it waits for commands from a remote debugging connection:

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

We start GDB:

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

We set a breakpoint right at the beginning of the bootloader:

break *0x7c00
continue

We can see the start and end of the bootloader in RAM, it should match the output of 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

We set a breakpoint at the memory position where the bootloader will write the OS:

break *0x1000
continue

We check that the values are correct, they should match those of the xxd output:

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

We step through the ASM instructions using the “si” command, we should see the execution of the entire process including the execution of our kernel.

If you liked the article, you can treat me to a RedBull here