What
What we are trying to achieve
Last time around we built the cross-compiler. With it we can compile our own Kernel code in a way that is compatible to be booted on an x86 CPU. We then boot this custom kernel safely in QEmu, a hypervisor, that emulates a computer CPU.
What to expect
If all works well we will be able to boot our custom OS from a generated ISO CD-Image and see a message on the screen.
When
Stages during boot of an x86 computer
As seen in my previous post, after a computer boots it eventually loads the bootloader. We will be using GRUB, a very common bootloader for Linux OSes to load our bootcode that will then load oder Kernel written in C-Code.
Why
Required steps to build a custom Kernel
In order to achieve this we will use the cross-compiler from the previous part of the series to compile for i686-elf architectures. With it, we can:
- Compile the boot code written in assembly
- Compile the kernel C-Code
- Link the two together
- Create a bootable ISO CD-Image with Grub
- Boot that CD-Image in the QEmu Hypervisor
An important note regarding our Kernel C-Code: The code will be run in ring 1 or “Privileged Mode”. Grub itself runs mostly in Ring 0 or “Real Mode”. The difference is that some machine operations, most notable the famous “INT 13” BIOS-Interrupt for reading the disk, is not permitted in “Privileged Mode”.
Background
Sources of Code and Documentation
Much of code to initialize this bootloader is magic code that is specifically for the boot process of an x86-compatible computer.
A basic understanding of computers, as everyone will tell you, is beneficial. You can of course read the computer science literature. But there are also numerous sources of code Online. The osdev-Forums provide code examples in their barebones tutorial [1]. Searching for distinct keywords from that tutorial in GitHub yields numerous other Custom OSes that were based on the same barebones tutorial, als the boot code here.
As always in software development the question is merely at what stage do you move from your code to other people’s code. It’s important to keep in mind that the CPU itself has software running within it. The bootcode is the way it is, because it need to be compatible with the original Intel-Processors. Every major CPU since is made to be backward compatible. And we are also writing our boot code to be compatible with it.
At the same time the boot code is required to be compatible with the Multiboot-Specificaiton [2]. Otherwise the Grub-Bootloader would not be able to execute the code.
Much of the code in OS development is not written by a single person, but passed on and it is very specific to the target architecture. Be sure to check the references below.
How
Setting up the Boot-Code
In the following we will get to know the minimum required files (linker.ld, boot.asm, kernel.cpp and grub.cfg) that are required to get a PC to boot and print text on the screen.
The linker.ld is configuration file for gcc’s linker that is required to create a bootable kernel image:
linker.ld
The boot assembly code, boot.asm is what will call our “kernel_main” C-Code function for the kernel.
boot.asm
Setting the Grub-Bootloader
For booting our Custom OS we use the Grub-Bootloader. For this a small configuration for the Grub-Boot-Menu, grub.cfg, is provided:
grub.cfg
Finally our Kernel-Code
And the first kernel.cpp is where things get interesting for us. For now it contains the code to print a simple “Hello World” onto the screen. This code is taken straight from the BareBones Tutorial [1].
It contains the code for writting on the screen. This is achieved by writing to a specific memory location, the Terminal Buffer at address 0xB8000.
kernel.cpp
...and a script for compiling
In order to build our kernel image, create a bootable ISO-CD-Image with Grub and to test boot the contraption in QEmu, I’ve built a custom Bash-Script. As your code grows it might make sense to translate this into a Makefile or even move to CMake.
build-and-boot.sh
After running the script the code will be compiled, linked. Grub will generate a bootable ISO and this ISO is then booted with QEmu.
Progress
Result
You should new be seeing QEmu with the message “Hello, kernel World!” from the above screenshot.
So far we’ve built a cross-compiler and have used it to compile example code for a kernel. We’ve created a bootable ISO-Image and booted it in QEmu to verify it works.
Our custom OS now has the ability to write text onto the screen. Next time we’ll add keyboard input in order to build a simple dialog system.
Annex: Syntax differences in Bootcode for different Assemblers
You might come across assembly boot code that looks differently. This is because there are numerous assemblers with a slightly different syntax. You could be using a Microsoft Assembler with Intel-Syntax, a Linux Assembler with AT&T-Syntax and it could be either in either the GCC assembler or NASM style. The code is mostly functionally identical.
NASM (Netwide Assembler) | i686-elf-as (GNU Assembler or "GAS" in Intel Syntax) |
Pro-Tip: If you’ve got Assembler-Code in either NASM or GAS and you want to translate to the other, compile it with the repective assembler and then use objdump on the object file. This will output to NASM/Intel-Syntax:
1] https://wiki.osdev.org/Bare_Bones 2] https://www.gnu.org/software/grub/manual/multiboot/multiboot.html