Part IV : Protected Mode and C

A reminder of what we have now

After we covered so much theory we should take a quick look again at the code we have written. Part two left us with a Makefile that had this content:

BOOT_FILE = bootloader/bootloader.asm 
KERNEL_FILE = kernel/basic_kernel.asm 
        
build: $(BOOT_FILE) $(KERNEL_FILE)
    nasm -f bin $(BOOT_FILE) -o bootstrap.o
    nasm -f bin $(KERNEL_FILE) -o kernel.o
    dd if=bootstrap.o of=kernel.img
    dd seek=1 conv=sync if=kernel.o of=kernel.img bs=512
    qemu-system-x86_64 -s kernel.img
            
clean:
    rm -f *.o

We then have a bootloader.asm file, which we will not really have to change unless something breaks:

start:
    mov ax, 07C0h
    mov ds, ax

    mov si, title_string
    call print_string

    mov si, message_string
    call print_string

    call load_kernel_from_disk
    jmp 0900h:0000 ; gives control to the kernel by jumping to its starting point.

load_kernel_from_disk:
    mov ax, 0900h
    mov es, ax
    
    mov ah, 02h ; service number, 
    mov al, 01h ; number of sectors we want to read from (only simple kernel for now, so less than 512 bytes)
    
    mov ch, 0h ; cylinder number, which is 0
    mov cl, 02h ; sector number that we would like to read its content, this is the second sector

    mov dh, 0h ; head number, 0h means the first head
    mov dl, 80h ; drive number, 80h means the first hard disk, 81h would be second

    mov bx, 0h ; memory adress that content will be loaded into
    int 13h ; 13h provides services related to hard disk

    ; if successful, carry flag will be set to 0, otherwise carry flag is 1
    jc kernel_load_error

    ret

kernel_load_error:
    mov si, load_error_string
    call print_string

    jmp $

print_string:
    mov ah, 0Eh ; bios number 0Eh, sets for teletype output function
print_char:
    lodsb ; loads byte at SI, into AL and increments SI

    cmp al, 0 ; 0 stored in al if at end of string
    je printing_finished

    int 10h ;bios interrupt 0x10, to print char stored in AL

    jmp print_char
printing_finished:
    ;print new line
    mov al, 10d ; ASCII code for new line
    int 10h 

    ;read current cursor position
    mov ah, 03h ; function to read cursor position
    mov bh, 0 ; page number 0 for default page
    int 10h ; 10h now used to read cursor position

    ;move cursor to beggining
    mov ah, 02h ; function to set cursor position
    mov dl, 0 ; column number (0 for begginign of line)
    int 10h ; 0x10 to set cursor pos

    ret

title_string db 'Welcome to the lytlnybl bootloader!',0
message_string db 'Loading up the kernel for you...',0
load_error_string db 'Oh oh!, there was a problem loading the kernel',0

times 510-($-$$) db 0 ; pads the rest of the bootloader with 510 bytes, aiming for a 512 byte bootloader
dw 0xAA55 ; specifies the end of the bootloader, recognised by bios

And then we also have a basic kernel as follows:

start:
    mov ax, cs
    mov ds, ax

    mov si, hello_string
    call print_string

    jmp $

print_string:
    mov ah, 0Eh

print_char:
    lodsb ; sets al = [DS:SI++]

    cmp al, 0
    je done
    
    int 10h

    jmp print_char

done:
    ret

hello_string db 'Hello World!, i am lytlnyblOS, running in real mode', 0

In the previous part, we covered everything we needed to get our operating system into protected mode. You may notice that currently we are still relying on BIOS interrupts. These BIOS interrupts are actually pretty powerful, and you can use them to do many things (like write video games that run within the BIOS) I have done so with the game snake. Linked here.

Debugging is key moving forward

With low level programming tasks such as this, it's important that we have a clean way to debug our programs, although debugging is still important in regular programming, it's often omitted and not really learned to a degree that it should be by most people learning programming. This is why in this guide I will be intentionally making us have an error called a triple fault, and then we will be using a debugger to fix it.

You may be asking "what is a triple fault?" A triple fault is an x86 CPU reset that occurs when the processor encounters an exception, fails to invoke the exception handler (causing a double fault), and then also fails to invoke the double fault handler. At that point, the CPU resets itself. In our QEMU emulator this would look like a bunch of text flashing on the screen. This is because the system is continually rebooting itself over and over again.

The debugger we are going to be using is GDB, so make sure to install it before continuing, or install whatever debugger you prefer.

With the compiled state of our bootloader and kernel as of now, using a debugger will be pretty tricky, this is because our debugger will not be able to access function names, labels, source lines and variable names (among many other things). We can still use the debugger like this, but it will function more as a CPU monitor than a source debugger. It's important to configure our build environment so we get a lot more context when debugging.

Setting up GDB

Binary files (which is what we are compiling to now) cannot provide functions names, labels and such, so the method I am using to get access to them is going to be compiling to the .elf format. I will then be copying the .elf compilation back into .bin because if we use the .elf file we would have to refactor some of the code in our bootloader.

To compile to .elf we must make a linker script. This tells the linker where to place things in memory. Generally a linker is a program that combines object files into a final executable and fixes up all the addresses.

My linker script, called linker.ld looks like this.

ENTRY(start)

SECTIONS
{
    . = 0x9000;

    .text :
    {
        *(.text)
    }

    .data :
    {
        *(.data)
    }

    .bss :
    {
        *(.bss)
    }
}

And then we must add two lines to our Makefile, one to link the object file into an elf, and one to copy the elf into a bin file. We must also edit another line to compile our kernel into an object file in the .elf format. The bootloader is a plain binary as we will not be debugging it at the current moment and will only be changing it to add blocks. This is our new Makefile:

BOOT_FILE = bootloader/bootloader.asm 
KERNEL_FILE = kernel/basic_kernel.asm 
LINKER = kernel/linker.ld
        
build: $(BOOT_FILE) $(KERNEL_FILE)
    nasm -f bin $(BOOT_FILE) -o bootstrap.o
    nasm -f elf32 -g -F dwarf $(KERNEL_FILE) -o kernel.o
    ld -m elf_i386 -T $(LINKER) kernel.o -o kernel.elf
    objcopy -O binary kernel.elf kernel.bin
    dd if=bootstrap.o of=kernel.img
    dd if=kernel.bin of=kernel.img seek=1 conv=notrunc
    qemu-system-i386 -drive format=raw,file=kernel.img -s -S
            
clean:
    rm -f *.o

The -s flag in QEMU starts a TCP port in 1234 and -S tells QEMU to freeze at startup, both of these allow us to connect GDB.