In this article, we'll start with a simple x86 kernel that can be booted using GRUB. It will print a message on the screen and then halt!
One-does-not-kernel
It is surprisingly straightforward to write a kernel by oneself. But how does an x86 machine actually start up? Before diving into kernel development, let’s first understand what happens when an x86 machine boots and hands control over to the kernel.
When the x86 CPU powers on, it starts executing from the address [0xFFFFFFF0], which is the last 16 bytes in the 32-bit addressing space. This location contains a jump instruction that directs the CPU to the BIOS code stored in memory. The BIOS then checks the boot device sequence for a specific magic number to identify the first bootable device.
Once a bootable device is found, the BIOS copies the first sector (512 bytes) of that device into physical memory at address [0x7C00] and jumps to that location to begin execution. This piece of code is known as the bootloader. The bootloader then loads the kernel into memory at [0x100000], which is the standard starting address for x86 kernels.
Tools Needed
To develop this simple kernel, you’ll need:
- An x86 architecture computer
- Linux operating system
- NASM assembler
- GCC compiler
- LD linker (GNU)
- GRUB bootloader
Source Code
The source code for this project can be found on my GitHub repository: [mkernel](https://github.com/yourusername/mkernel)
Writing the Kernel Entry in Assembly
While we prefer to use C for most of our code, we still need a small amount of assembly to serve as the kernel's entry point. This assembly code will call our C function and then halt the CPU.
To ensure the assembly code is recognized as the kernel's entry point, we use a linker script to specify that the final binary should be loaded at [0x100000]. Here's the assembly code:
```nasm
; kernel.asm
bits 32
section .text
global start
extern kmain
start:
cli ; Disable interrupts
call kmain ; Call the C function
hlt ; Halt the CPU
```
The `bits 32` directive tells NASM to generate 32-bit code. The `section .text` defines the code segment. The `global start` makes the symbol `start` visible to the linker. `extern kmain` declares that `kmain` is defined elsewhere (in our C file). The `cli` instruction disables interrupts before halting the CPU with `hlt`.
Kernel Implemented in C
The `kmain` function in `kernel.c` will handle the actual logic of printing a message to the screen. Here's a simplified version:
```c
/* kernel.c */
void kmain(void) {
char *str = "my first kernel";
char *vidptr = (char *)0xB8000; // Video memory starts here
unsigned int i = 0, j = 0;
// Clear the screen
while (j < 80 * 25 * 2) {
vidptr[j] = ' ';
vidptr[j + 1] = 0x07; // Light grey on black
j += 2;
}
j = 0;
// Print the string
while (str[j] != '\0') {
vidptr[i] = str[j];
vidptr[i + 1] = 0x07;
++j;
i += 2;
}
}
```
Video memory is located at [0xB8000] in protected mode. Each character is represented by two bytes: one for the ASCII character and one for the attribute byte. We set the attribute to 0x07, which means light grey text on a black background.
Linker Script
We use a linker script to tell the linker where to place the kernel in memory. Here's an example:
```ld
/* link.ld */
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS {
. = 0x100000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
}
```
This script ensures the kernel is loaded at [0x100000], and all sections are placed accordingly.
GRUB and Multiboot
To boot the kernel with GRUB, we need to include a Multiboot header in the first 8KB of the kernel. This header must contain a magic number, flags, and a checksum. Here's how we modify the assembly code:
```nasm
; kernel.asm
bits 32
section .text
align 4
dd 0x1BADB002 ; Magic number
dd 0x00 ; Flags
dd -(0x1BADB002 + 0x00) ; Checksum
global start
extern kmain
start:
cli
call kmain
hlt
```
Compiling and Linking
Now, we compile the files and link them using the following commands:
```bash
nasm -f elf32 kernel.asm -o kasm.o
gcc -m32 -c kernel.c -o kc.o
ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o
```
Configuring GRUB
After building the kernel, rename it to something like `kernel-701` and place it in `/boot`. Then, add an entry to your GRUB configuration file (`grub.cfg`) like this:
```grub
title myKernel
root (hd0,0)
kernel /boot/kernel-701 ro
```
If there's a `hiddenmenu` line, remove it. Reboot and select the new kernel from the GRUB menu.
Result
After booting, you should see the message "my first kernel" displayed on the screen. This confirms that the kernel has been successfully loaded and executed.
PS:
- It's recommended to test kernel development in a virtual machine.
- For GRUB2, the configuration might look slightly different.
- You can also use QEMU to run the kernel without GRUB:
```bash
qemu-system-i386 -kernel kernel
```
Yellow Led Display,0.36 Inch Yellow Led Display,3 Digits Green Led Display,3 Digits Led Segment Display
Wuxi Ark Technology Electronic Co.,Ltd. , https://www.arkledcn.com