Writing a simple Kernel November 15, 2020

When it comes to coding, the Linux Kernel is easily one of the coolest projects ever written. So, I figured I'll read a little bit about Operating Systems and write a simple kernel as a lazy afternoon project. This is a simple kernel written in C that runs with the GRUB bootloader on an x86 system.

Before writing the Kernel, the first and vital thing to know is what the heck is a kernel? and how a machine boots up and transfers the control to the kernel? So to answer the first question, a kernel is the main component of an OS and is the core interface between the computer's hardware, and its processes. To keep it simple, a kernel controls all the major functions of the hardware. Now coming to the second question, first, the BIOS code starts its execution as it first searches for a bootable device in the configured boot device order. It checks for a specific number to determine if the device is bootable or not. Once the BIOS finds a bootable device, it copies the contents of the device’s first sector into RAM starting from physical address 0x7c00 and then jumps into the address and executes the code just loaded. This code is called the bootloader. The bootloader then loads the kernel at the physical address 0x100000. The address 0x100000 is used as the start-address for all big kernels on x86 machines. All x86 processors begin in a simplistic 16-bit mode called real mode (real mode is a state that an x86 is in when the PC first turns on). The GRUB bootloader makes the switch to 32-bit protected mode(main operating mode for all the modern intel processors) by setting the lowest bit of CR0 register to 1. Thus the kernel loads in 32-bit protected mode.

Now when it comes to writing the kernel, the system needs certain requirements to be installed. Those are: GCC(compiler for C/C++), NASM(compiler for assembly), GRUB(Bootloader). Now, this kernel is written on Ubuntu so, I've installed the mentioned requirements using Ubuntu's default "apt-get".

Though I've said the kernel is written in C, I've also used a little bit of assembly language. The job of the assembly code is to fire the main kernel function in the C file. The assembly part will be as follows:

Here, the first line bits 32 is a NASM directive which specifies that it should generate the code to run a processor operating in 32-bit mode. Next line section .text shows that the Text section begins, which is where all the code is kept. The next 4 lines come under the multiboot specification. The multiboot specification is the boot sequence standard used for loading various x86 kernels using the bootloader. This multiboot spec insists that a kernel should have a header, called the multiboot header within its first 8kb. This multiboot header contains three fields which are 4byte aligned. The first field is the magic field - the magic number identifying the header i.e, 0x1BADB002. The second field is the flags field - specifies features that the OS image requests or requires of the bootloader. The third field is the checksum field - is a 32-bit unsigned value which, when added to the other magic fields (i.e. ‘magic’ and ‘flags’), must have a 32-bit unsigned sum of zero. Here dd stands for the double word of size 4 bytes. Next comes the global line which is the NASM directive for setting the source code to global. By setting the start to global the linker file (explained later) knows where the start is i.e, the entry point of our file. "kernelMain" is the core kernel function in the C file which we're calling from the assembly. Then we have the start function which calls our kernelMain function and halts the CPU once done. Here we used clear-interrupts instruction (cli) as interrupts can wake the CPU from halt (hlt) instruction. In general, we've to set some memory (stack_space) and point a stack pointer towards it (esp). We use resb instruction to reserve 8kb for the stack_space. With this the assembly part is done and now, moving on to the main part i.e, writing the kernel in C.

The code for kernel is as follows:

As this kernel just displays a string and halts, we need a string to display, and we'll be printing it directly to the video memory. Here, "0xB8000" is the address where the text screen video memory resides for the color monitors. To keep it simple, the entire code comes down to two loops, one for clearing the screen and the other, for writing our string on the blank screen. So the first loop for clearing the screen runs like "while(j < 80*25*2)" as the screen supports 25 lines of 80 ASCII characters of having 2 bytes of memory for each. For clearing the screen we make a blank attribute and write it to the screen with 0x02 i.e, 0 being black background and 2 being green foreground. So, we're basically writing a blank character with the black background and as it's the blank character foreground won't show anything thereby making the entire screen blank. Now on this blank screen, we need to write our string so we take the string and we write it with a black background with a green foreground. That sums up the kernel part, leaving only the linking now.

This the vital part as by using NASM we get an object file out of the assembly code and similarly, with GCC we get the object file out of the C code but we need a linker to link both of them to generate an executable file which we will be used as the kernel. The linker script will be written and saved with an extension "ld". The linker script is as follows:

Here "elf32-i386" means that the output format is set to an executable 32-bit linkable format. The next line specifies the symbol to be the entry point i.e, start which is written in the assembly code. The "SECTIONS" is the most vital part of the entire thing as here the layout of the executable is defined. "0x100000" is the address where the kernel code should start. "." represents the location counter meaning the location counter is set to the starting address. The *(.text) indicates the text input sections from all the input files, so the linker merges all the text sections of the object files to the executable text sections at the respective address i.e, the starting address (0x100000) plus the size of the text output section. Similarly, the data and bss section are merged and placed at the respective address. And, with this, the coding part is done and the only thing left to do is build the executable kernel from the code written.

First building the object file from the assembly code using NASM:

nasm -f elf32 kernel.asm -o kernasm.o

Next building the object file from the C code using GCC:

gcc -m32 -c kernel.c -o kernc.o

Finally merging both the object files i.e, kernasm.o and kernc.o using the linker script:

ld -m elf_i386 -T link.ld -o kernel kernasm.o kernc.o

And, that's it, this will generate the executable kernel file.

Now, this kernel can be tested in two ways, one is adding the kernel file to the grub and booting it and the other one is running it using a virtual machine like QEMU. To add it to the grub, the kernel file needs to be renamed as per the required format i.e, kernel-version (like kernel-001), and this kernel file needs to pasted inside the /boot directory and need to add the entry in the grub config file (grub.cfg, admin privileges required). The entry for the kernel file can be added as follows:

menuentry 'kernel 001' { set root='sda,5' multiboot /boot/kernel-001 ro }

Here "sda,5" meaning sda5 in the sda drive. This location is the boot directory path which varies per system. After adding this save the changes and reboot the system to find the kernel listed in the bootable list. Another way is simple af, just install QEMU, navigate to the kernel file location in the terminal and run "qemu-system-i386 -kernel kernel-001".

So that sums up the entire thing. Though this is the most basic intro to kernel development it is something to fill up a lazy afternoon. This can be tweaked more to add the keyboard and screen support.

Later.