9front Bare Bones Kernel
I have recently been interested in reading and understanding the processes of kernel development. In that effort I have been spending some time reading the OSDev wiki as well as this fantastic set of blogs for writing a kernel in rust. However I quickly ran in to two problems:
- The OSDev wiki is quite outdated at this point, with many getting started points using i386.
- The rust blog seemed to abstract a lot of the 'ugly' parts away (for practical cases this is great, but it pays to look at the ugly stuff when learning).
As such I thought it might be worth while to take a peek on getting a barebones kernel setup using the common tools that are available on the OS that I do most of my development in, 9front. As such I set out to first learn how 9front manages its kernel, and then see if I could strip out just the minimum to get myself a little "hello world".
Knowing where to look
Let's start by looking at how 9front organizes it's kernel code. All of the kernels are located in /sys/src/9/$objtype/
with port
referring to portable code between them. For our purposes we're only going to look at the amd64 kernel. There are three files that are good to look at first
- l.s: contains our assembly bootstrap and entrypoint from the bootloader
- main.c: contains our C entrypoint that is called from l.s once we have gotten a stack and switched over to 64 bit long mode.
- mkfile: while normally not very interesting, we should take a look to see how we are linking the kernel together.
Also worth noting is a couple additional directories:
- /sys/src/boot: contains 9boot bootloader.
- /sys/lib/dist/mkfile: script for making a bootable cdrom.
Start putting stuff together
Copying over the l.s
file we see tons of stuff that we wont need, so lets trim it down a bit. Reading it quickly we find that we call our main funciton in _start64v
, so let's delete everything after that. We also can see that l.s
requires a mem.h
so let's grab that as well. Then let's write our own very tiny kern.c
with a void main(void)
entry point. For now simply enterying a infinite loop will suffice.
#include <u.h>
u32int MemMin; //Filled by l.s, thus the symbol must be somewhere
void main(void) { for(;;); }
Now let's get each of these compiled/assembled.
; 6c kern.c && 6a l.s
Now if we check the 9pc64 mkfile for how to link them we see something a bit out of the norm. First we see a KTZERO
variable declared and then it being passed to the linker through the -T flag.
Looking at the man page for the linker, we see that the -T flag tells the linker where to start placing the .TEXT
section for the binary. To understand why this is needed, let's remind ourselves of what goes on in the average boot(in relation to our kernel).
- 9boot starts
- 9boot throws us into 32 bit protected mode
- 9boot finds our kernel listed in the plan9.ini
- 9boot reads the kernel and throws it into memory
- Longjump in to the entry point of the kernel as defined by the multi boot header.
When we first get to our kernel we have not set up virtual memory, so our first sets of jumps and addressing must use the physical addresses. Looking at mem.h
we can see that KZERO (kernel zero) is set to 0xffffffff80000000
, so this must be where 9boot puts the start of our kernel binary. However, the start of the binary is not the start of executable code, that would be the .TEXT
section. So we must have a common definition of where our executable code starts between our linker and our l.s
code. This allows l.s
to tell 9boot where exactly in physical memory to jump to.
To acomplish this we pick a common starting point, define it in our source code, and make sure to pass it to the linker so things lign up. So now that we know what is going on, let's link our kernel:
6l -o kern -T0xffffffff80110000 -l l.6 kern.6
It's worth noting that l.6
must come first or else our dance to get the .TEXT
section aligned will be pointless, as kern.6
will be placed first in to the section.
Now let's verify that we indeed set things up right by using file(1). The output should look like:
kern: amd64 plan 9 boot image
Booting our new kernel
We have one more step before we can actually get our fresh kernel booted in something like QEMU. We need to create a cdrom iso image that contains both our kernel as well as 9boot. For this we will take a look at the existing script for iso generation on 9front: /sys/lib/dist/mkfile.
Lets start by creating a new plan9.ini for 9boot to point to our new kernel:
echo 'bootfile=/amd64/9pc64' > plan9.ini
We'll also want a local copy of /sys/lib/sysconfig/proto/9bootproto so that we can add our kernel path to it.
Now that we have those, let's create our iso using disk/mk9660 like so:
; @{rfork n
# Setup our root
bind /root /n/src9
bind plan9.ini /n/src9/cfg/plan9.ini
bind kern /n/src9/amd64/9pc64
disk/mk9660 -c9j -B 386/9bootiso \
-p 9bootproto \
-s /n/src9 -v 'Plan 9 BareBones' kern.iso
}
With that, you should have a bootable iso image fit for use in something like QEMU.
Source
The source code is available on my github. It adds a small print message in kern.c as well as a mkfile from what is shown here.