Chapter 2: A Bootable Kernel - COMPLETE
This commit is contained in:
parent
607266bfd3
commit
a9328d6b67
407
doc/2_A_Bootable_Kernel.md
Normal file
407
doc/2_A_Bootable_Kernel.md
Normal file
@ -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 <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.
|
@ -4,4 +4,5 @@
|
||||
|
||||
[Chapter 0: Introduction](0_Introduction.md)<br>
|
||||
[Chapter 1: Toolchain](1_Toolchain.md)<br>
|
||||
[Chapter 2: Booting a Kernel](2_A_Bootable_Kernel.md)<br>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user