Clone
2
Home
xamidev edited this page 2026-01-02 15:25:37 +01:00

pepperOS

Welcome to pepperOS!

Disclaimer

This project is a work-in-progress toy kernel. It should never be used in any real world application as it is lacking many features. Also, the documentation provided here is non-exhaustive. This is my second attempt at writing a kernel, and I'm mainly doing it to learn more about the underlying mechanisms of operating systems, which we never really see in practice. In this wiki you will find some useful information to run, debug, and understand the kernel. Anything can change at any moment, and the information might not be up-to-date.

Project structure

The root directory contains the Makefile, the linker script (linker.ld), a GDB debug script (debug.gdb), the LICENSE, and the source code for the kernel. In the kernel, each directory is bound to a module: for instance, the io/kbd subdirectory contains code for handling input-output: more specifically, the keyboard.

Boot process

The kernel is loaded by the Limine bootloader, in 64-bit Long Mode, 4-level paging enabled. At boot, the kernel retrives a memory map (used to determine the biggest usable memory region), the HHDM offset (which will be used to translate physical/virtual addresses mapped in the higher half by Limine) and linear framebuffer information (which will be used to print text on screen).

Serial interface

One of the first things that are initialized after Limine hands off control to the kernel is the initialization of the serial interface, on port COM1. This is highly useful because it helps us debug the kernel, print formatted strings, and all that without using memory-mapped I/O like the framebuffer does. Instead, the serial interface only exists through in/out ports, and communicate via the inb / outb primitives. The information gathered from the bootloader is outputted to the serial interface.

Common basic structures

The kernel clears the CPU interrupt flag, and then sets up a GDT, and an IDT to handle interrupts.

GDT

The GDT loaded by the kernel consists respectively of the mandatory null descriptor, kernel code and kernel data, and finally user code and user data descriptors. These are set up and loaded for legacy reasons, but the main memory scheme used by the kernel is paging.

IDT

An IDT is set up with 16-byte aligned vectors. That way, when setting all IDT entries, we know that each vector is 16-byte away from each other and can loop through this to set them all up (256 entries). Each vector, depending on if the interrupt throws an error code, either pushes that error code and its vector number, or pushes a dummy "0" 64-bit error code, then its vector number, and calls an interrupt stub, which pushes all GP-registers, calls an interrupt dispatcher (giving it the stack as argument), then restores registers. This way we can centralize interrupt handling in a switch statement depending on the vector number. Notable vectors are vector 32, which is the timer interrupt, and vector 33, which handles the keyboard.

Timer

The PIT timer is used because it is the simplest to work with. It is configured by the kernel to emit a timer interrupt, remapped to IDT vector 32 (above the Exceptions/Faults), at a rate of 1000 Hz (1 interrupt every millisecond).

Keyboard

PS/2 keyboard is supported, with QWERTY (American) layout, and also AZERTY (French) layout, but without accents.

Memory management

4-level paging is enabled at handoff, and the kernel sets up its own page tables from there, mapping the kernel (2MB, hardcoded but should be plenty), framebuffer, some space (1GB) in HHDM, and also some space (16MB) is identity-mapped at the beginning of physical memory in case there's something early left.

A physical memory allocator, based on a simple freelist, can allocate/free single 4KB pages.

For now, that's pretty much it.