408 lines
11 KiB
Markdown
408 lines
11 KiB
Markdown
# 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 <multiboot.h>
|
|
.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.
|