diff --git a/doc/2_A_Bootable_Kernel.md b/doc/2_A_Bootable_Kernel.md new file mode 100644 index 0000000..5aad68a --- /dev/null +++ b/doc/2_A_Bootable_Kernel.md @@ -0,0 +1,407 @@ +# Chapter 2 - Booting a Kernel + +In this chapter we'll create a minimal kernel that can be loaded by a Multiboot compatible bootloader. + +## Makefile + +Gnu Make is a powerful tool. I don't think most people realize just how powerful it is. It's really ridiculously smart, if you trust it. + +As an example, I'm currently going through the [Build Your Own Text Editor](http://viewsourcecode.org/snaptoken/kilo/) booklet. Here's my Makefile for the project: +```make +CFLAGS := -Wall -Wextra -pedantic -std=c99 + +kilo: +``` + +And Make takes care of the rest. Of course, the makefile for a kernel +will be a bit more complex, but we'll trust Make as far as possible. + +Just for fun, you can run `make -p` to see all the built-in rules +of Make, some of which we will make use of, such as `$(LINK.C)` or +`$(COMPILE.S)` + +But let's get into it. First we create a directory for our sourc files, +and the kernel sources specifically. In it, we place a Makefile: + +`src/kernel/Makefile` +```make +ifeq ($(MITTOS64),) +$(error Unsupported environment! See README) +endif + +CC := x86_64-elf-gcc + +SRC := $(wildcard **/*.[cS]) +OBJ := $(patsubst %, %.o, $(basename $(SRC))) + +CFLAGS := -Wall -Wextra -pedantic -ffreestanding +CFLAGS += -ggdb -O0 +ASFLAGS += -ggdb +CPPFLAGS += -I include +LDFLAGS := -n -nostdlib -lgcc -T Link.ld + +kernel: $(OBJ) + $(LINK.c) $^ -o $@ + + +DEP := $(OBJ:.o=.d) +DEPFLAGS = -MT $@ -MMD -MP -MF $*.d +$(OBJ): CPPFLAGS += $(DEPFLAGS) +%.d: ; + +DESTDIR ?= $(BUILDROOT)sysroot + +# Copy kernel to sysroot +$(DESTDIR)$(PREFIX)/kernel: kernel + install -D kernel $(DESTDIR)$(PREFIX)/kernel + +install: $(DESTDIR)$(PREFIX)/kernel + +clean: + rm -rf $(OBJ) $(DEP) kernel + +.PHONY: install + +include $(DEP) +``` + +I think there are a few things here that needs explanation. + +First of all, there's the environment check + +```make +ifeq ($(MITTOS64),) +$(error Unsupported environment! See README) +endif +``` + +This is just as discussed in the previous chapter, to make sure the +makefile is only run inside the Docker container. + +Then we set `CC := x86_64-elf-gcc` to make sure the kernel is build +using our cross compiler. If you look through the built in Make +rules, you'll see that `CC` is used for compiling .c files, +compiling .cpp files, compiling .S files and linking it all +together. + +The next three lines + +```make +SRC := $(wildcard **/*.[cS]) +OBJ := $(patsubst %, %.o, $(basename $(SRC))) +``` + +scan the source directory and all subdirectories for `.c` or `.S` files, and find the names of the corresponding object files. + +Then we set some compiler and linker options + +```make +CFLAGS := -Wall -Wextra -pedantic -ffreestanding +CFLAGS += -ggdb -O0 +ASFLAGS += -ggdb +CPPFLAGS += -I include +LDFLAGS := -n -nostdlib -lgcc -T Link.ld +``` + +- We want to compile with `-Wall -Wextra and -pedantic` to help keep the +code quality high. +- `-ffreestanding` stops the compiler from assuming that there's a +standard c library present. +- `-ggdb and -O0` simplifies debugging by including gdb specific debug +symbols in the compiled object code and turning off all optimizations +- and we also want `-ggdb` for assembly files. +-The c preprocessor should look for include files in +`src/kernel/include`, that's what the `-I include` option does. +- And finally, there's the linker options. `-nostdlib` because we don't +want any libraries but `libgcc` to be linked with our kernel. `-n` tells +the linker that we don't necessarily want the sections page aligned. +`-T Link.ld` tells the linker that the file `src/kernel/Link.ld` +contains more information about how we want the file structured. + +The next two lines: + +```make +kernel: $(OBJ) + $(LINK.c) $^ -o $@ +``` + +tells `make` that the file `kernel` depends on all our objects and how +to build it. Here we're using the built in `$(LINK.c)` rule. + +And that's really all we need to build the kernel. 15 lines. The rest of +the file is just for convenience. + +Like the lines: + +```make +DEP := $(OBJ:.o=.d) +DEPFLAGS = -MT $@ -MMD -MP -MF $*.d +$(OBJ): CPPFLAGS += $(DEPFLAGS) +%.d: ; +[...] +include $(DEP) +``` + +which deserve some explanation. + +If you update a source file and run make, it will know which files were +updated and need to be rebuilt. But what about included header files? If +they update, you'd want to rebuild all files that include them. + +The book `Managing Projects with GNU Make` by Robert Meclenburg suggests +a solution that uses the `-M` option of gcc to generate a list of +dependencies from a source file, coupled with a mess of temporary files +and `sed` commands to generate make rules that you can import. I've seen +this method used in lots of projects. + +I am sorry to say I can't remember where I picked this up - I certainly +didn't come up with it myself - but it turns out that through the use +of a bunch of more `gcc` options, the rules can be generated exactly as +we want them. `-MT $@` for example changes the name of the generated +rule to the input file - which is what Meclenburg does with `sed +'s,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$ > $@;`. + +I just think this is a very neat solution. + +The part of the Makefile I skipped is for "installing" the kernel, i.e. +copying it to the sysroot directory and for cleaning up. + +### Bonus +For convenience, I added a makefile to the project root directory which +contains the following rule + +```make +kernel: +ifeq ($(shell make -sqC src/kernel || echo 1), 1) + $(MAKE) -C src/kernel install +endif +``` + +## Linking + +As described above, the linker gets passed a linker script. This is as +basic as possible for now. + +`src/kernel/Link.ld` + +```Linker Script +ENTRY(_start) + +SECTIONS +{ + .text : + { + *(.multiboot) + *(.text) + } +} +``` + +What's important here is that the `*(.multiboot)` statement is first +inside the `.text` section. The multiboot headers (described in the next +section) need to be near the beginning of the kernel executable, or the +bootloader won't be able to find them. As the kernel grows, this will +ensure they can still be found. + +Note that you can't do this: + +```Linker Script +SECTIONS +{ + .multiboot : {*(.multiboot)} + .text : {*(.text)} +} +``` + +Oh, how I wish this worked. But for some reason, the linker will put the +`.text` section first in the output file, no matter what. + +The linker script will soon grow, but for now, this is enough. + +## Multiboot Headers + +There are currently two actively used versions of the multiboot +specification, "Multiboot 1" (latest version is 0.6.96) and "Multiboot +2" (latest version is 1.6). Since Multiboot 2 has better support for 64 +bit kernels, that's what I'll use. + +In order for the bootloader to be able to recognize a multiboot kernel, +there has to be a special header near the start of the executable. Via +our linker file trick above, we can place the header at the very start +(actually after the program headers, but that's OK) by making sure it's +in the `.multiboot` section. + +`src/kernel/boot/multiboot_header.S` + +```asm +#include +.section .multiboot + +.align 0x8 +Multiboot2Header: +.long MBOOT2_MAGIC +.long MBOOT2_ARCH +.long MBOOT2_LENGTH +.long MBOOT2_CHECKSUM + +.short 0 +.short 0 +.long 8 +Multiboot2HeaderEnd: +``` + +The Multiboot 2 header is followed by a list of tags to specify further +options. We only have one; the list termination tag. + +The constants above are defined in `multiboot.h`. + +`src/kernel/include/multiboot.h` + +```c +#pragma once + +#define MBOOT2_MAGIC 0xE85250D6 +#define MBOOT2_REPLY 0x36D76289 +#define MBOOT2_ARCH 0 +#define MBOOT2_LENGTH (Multiboot2HeaderEnd - Multiboot2Header) +#define MBOOT2_CHECKSUM -(MBOOT2_MAGIC + MBOOT2_ARCH + MBOOT2_LENGTH) +``` + + +This is just for the simplest possible multiboot headers. No frills or extra +functions. + +For more information, see [Multiboot 1 +specification](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html) +or [Multiboot 2 +specification](https://www.gnu.org/software/grub/manual/multiboot2/multiboot.html). + +## The kernel code + +Ladies and gentlemen, I present to you: The Simplest Kernel + +`src/kernel/boot/boot.S` + +```asm +.intel_syntax noprefix +.section .text +.global _start +.code32 +_start: +cli +jmp $ +``` + +That's all you need to make sure the +booting procedure works as it should. + +## Testing it out + +Let's go. + +```bash +# compile +$ d make +make -C src/kernel install +make[1]: Entering directory '/opt/src/kernel' +cc -ggdb -I include -MT boot/multiboot_header.o -MMD -MP -MF boot/multiboot_header.d -c -o boot/multiboot_header.o boot/multiboot_header.S +cc -ggdb -I include -MT boot/boot.o -MMD -MP -MF boot/boot.d -c -o boot/boot.o boot/boot.S +cc -Wall -Wextra -pedantic -ffreestanding -ggdb -O0 -I include -n -nostdlib -lgcc -T Link.ld boot/multiboot_header.o boot/boot.o -o kernel +mkdir -p /opt/sysroot +cp kernel /opt/sysroot/kernel +make[1]: Leaving directory '/opt/src/kernel' +``` + +Then run the emulator as before with `d emul` and also start the debugger in +another terminal window with `d gdb`. + +Now you can load the kernel debug symbols into gdb with the command + + (gdb) file sysroot/kernel + A program is being debugged already + Are you sure you want to change the file? (y or n) y + Reading symbols from sysroot/kernel...done. + +A small problem at this point is that GRUB will start the kernel in 32 bit +protected mode, while gdb assumes we're in 64 bit mode. You can see this by +running + + (gdb) show architecture + The target architecture is set automatically (currently i386:x86-64) + +So, to get the addresses and stuff correct, you can run + + (gdb) set architecture i386 + The target architecture is assumed to be i386 + +And finally, we're ready to start the emulator + + (gdb) c + Continuing. + +This will make the familliar `grub>` prompt show up in the emulator. + +It's time to load our kernel + + grub> multiboot2 /kernel + grub> boot + +And... nothing should happen, that you can see... + +But switch back to gdb and press ctrl+C to interrupt the emulator and you should see + + Program received signal SIGINT, Interrupt. + _start () at boot/boot.S:9 + 9 jmp $ + (gdb) + +So, apparently, the processor is running our "kernel" and is stuck at the +infinite loop on line 9. Just as we expected! + +The multiboot specification says that the bootloader should leave a magic +number in the `EAX` register after boot. Let's check it. + + (gdb) p/x $eax + $1 = 0x36d76289 + +Exactly according to the specification. Great! + +And with that everything seems to work! + +## Some extra fancy stuff + +You really don't want to keep typing all those grub and gdb +instructions in manually each time you test your OS, right? + +The gdb commands can be added to `toolchain/gdbinit`. In the name of +cinsistency, the debug symbols should not be loaded from `sysroot/kernel`, +but from `/opt/sysroot/kernel`, or rather yet `${BUILDROOT}sysroot/kernel`. +Using environment variables in a gdb script is a bit tricky, but can be +done via the `python` function: + +```gdb +python +import os +gdb.execute('file ' + os.environ['BUILDROOT'] + 'sysroot/kernel') +end +``` + +In order to make grub load the kernel automatically, you need to add a file +called `grub.cfg` at `/boot/grub` in the sysroot. I suggest you add this +file to `sysroot` while building the iso in `toolchain/mkiso`. + +I did it this way + +```bash +cat > ${sysroot}/boot/grub/grub.cfg << EOF +set timeout=1 +set default=0 + +menuentry "mittos64" { + multiboot2 /kernel +} + +EOF +``` + +See the [grub 0.97 manual](https://www.gnu.org/software/grub/manual/legacy/grub.html) for more information. diff --git a/doc/README.md b/doc/README.md index eb02d48..a1a8e97 100644 --- a/doc/README.md +++ b/doc/README.md @@ -4,4 +4,5 @@ [Chapter 0: Introduction](0_Introduction.md)
[Chapter 1: Toolchain](1_Toolchain.md)
+[Chapter 2: Booting a Kernel](2_A_Bootable_Kernel.md)