One Small Step for Kernel Coders, One Giant Kick for Bootstrap Kind
A new day, a new kind of madness…
In the next stop on the bus of sadistic randomness, we are going to endeavor to build a functioning operating system in golang! I know, I know…the question is always “WHY?!?!”
A pretty table of reasons why I am subjecting myself to this:
Reason | Good? |
---|---|
Boredom | Meh |
Because | Sure |
Profit? | Who knows |
Knowledge | and round and round we go down the rabbit hole… |
Anyway, enough with the random tangent of me giving terrible jokey reasons (except the last one of course) and let’s get on with building the foundation of what will get us into our operating system…
THE BOOTLOADER!
Yes, the glorious piece of 512 byte code that will kickstart us into a nightmare scenario of oblivion made of the sweetest golang code to ever hit ring 0. Let’s start with some details on what’s what inside this beast of a boot kickstarting us.
I am going to start on x86_64
systems since they have a lot more documentation around them. I will try along the way (but for sure when I am done with the x86) to get aarch64
working as I will be predominately working on my M1 Max Macbook and it doesn’t exactly play nice with x86_64
code. I will also be utilizing qemu as my emulator for now. I might change the Makefile
to use Virtualbox at some point, but for now qemu-system-x86
and qemu-system-aarch64
should get us going until things get more complex.
What you are actually here for
Now, for the juicy details of the kickstarter in command. On x86_64
systems, the BIOS
will load up and do a POST from the onboard code embedded in the hardware. During this it will look around at all the potential boot devices that it can find on all drives attached to the system. The boot sector for any potential device is the first 512 bytes of the device (in this case, since this will be on an MBR based drive), and for any such device a potential bootloader is demarcated with a special two byte signifier at the end of the 512 byte region of the device: 0x55, 0xAA
. The BIOS
will then attempt to load this section into address 0x7C00
and hand over control to the code inside it by issuing a
jmp 0x7C00
I will make a future post about the intricacies of the MBR/VBR system of booting from multiple partitions and about GPT and hopefully a few posts on UEFI as I learn more about bootstrapping from that.
As an aside, for the code I will be using NASM as the x86_64 assembler. The bootloader that I will be writing will be in real mode and as such the x86
instruction set will default to using 16-bit instructions. To make sure NASM
does this, we will start our assembly code with:
bits 16
While real mode utilizes 16-bit instructions, you can still engage with 32-bit registers and instructions by setting this bits
directive which prefixes any non 16-bit instructions with 0x66
(more info here)
Next, since the BIOS
loads our bootloader into the address space at 0x7C00
, we need to add the org
directive to tell NASM
to offset our code by 0x7C00
:
org 0x7C00
Now, we can set the stage with the first actual boot code. We can just throw in assembly, or we can package things up neater with labels! Let’s do the latter and name our first one boot
. It can be named anything, but it’s always good to make it readable for future you. Future me sure won’t remember what today me is doing, so might as well give future me the best shot he has at remembering anything at all about this…
boot:
cli
hlt ; halt and catch fire
times 510 - ($-$$) db 0
dw 0xaa55
Good ole’ halt and catch fire there to save us from burning down the qemu bus lines. The times 510 - ($-$$) db 0
tells NASM
to take the current instruction $
and subtract the program start $$
and then subtract this from 510 to give us the total non-used space and fill all unused bytes with 0 (db 0
). We only subtract from 510 as we know the final two bytes of the boot sector need to be 0x55, 0xAA
and that is where our final double word comes into play (0xaa55
since this is a little endian architecture). This is the bare minimum to get something loaded into 0x7C00
by the BIOS
and to run some code. This of course doesn’t do anything yet. Let’s get this all packaged up into a binary:
nasm -f bin bootloader-x86_64.s -o bootloader-x86_64.bin
and if we run it in qemu-system-x86
using the following:
qemu-system-x86_64 -drive format=raw,file=bootloader-x86_64.bin
we get the following:
Today's Finale
For the finale of this post, we shall just print a string to the terminal using the most complicated method I could think of: Functions! Luckily, it isn’t that terrible, we just need to set up a print
function as follows:
print:
mov ah,0x0E
.print_loop:
lodsb
or al,al
jz .return
int 0x10
jmp .print_loop
.return:
ret
This is just your standard c-style print which checks for a null terminator (0x00) at the end of the string via the or al, al
after it loads the character into the al
register with lodsb
. If it isn’t a null terminator, it will execute an interrupt via int 0x10
(the best interrupt tables can be found here and a slimmed down version of just the 0x10 interrupt table can be found here). By setting ah
to 0x0E
we are telling the BIOS
to render a character through the teletype interface (and we have access to these methods for the moment as we are still utilizing the real mode that the BIOS
initially starts in and renders control to us with). When we have found a null terminator, we do a jz
to .return
and this returns the call stack back to subroutine initiating the call.
We will also need to store the string somewhere, so at the bottom we can setup a double byte storage:
bootloading_str: db "Going deep into the gopher hole...",0
and change our boot
routine to be:
boot:
mov si, bootloading_str
call print
cli
hlt ; halt and catch fire
Running this in qemu
we now get:
That concludes this first post on my golang kernel development excursions. Followed is the final code for the minimal bootloader to print a string and die happily to await a more complex existence.
Next time, we will rip out the print statements (because who will see them anyway?!?) and replace that with a stack builder and section extender to give us more bootloading space to work with. 640Kb might be enough for anybody but 512 bytes sure isn’t…