WARNING: In Refactorization: This project is under refactorization. It might not be able to compile or run sometimes. Make sure to understand the directory structure before performing any operations.
ModOS-kernel is a bare-metal x86_64 kernel written in GNU Modula-2 (gm2), demonstrating that Modula-2 can be used effectively in kernel-space, with full access to inline assembly, hardware I/O, and raw memory.
- Limine boot protocol - native Limine protocol support (no Multiboot2 compatibility shim needed; portable beyond x86)
- x86_64 long mode - entered directly by Limine in 64-bit mode with paging enabled (higher-half kernel at 0xFFFFFFFF80000000)
- Kernel-owned GDT & 64-bit TSS - replaces the Limine-provided GDT with seven descriptors (null, kernel code/data, user code/data, TSS low/high) and installs a 104-byte Task State Segment with a ring-0 stack (32 KiB) and seven dedicated IST stacks (7 × 8 KiB) for critical exception handlers
- IDT & CPU exception handlers - 256-entry Interrupt Descriptor Table with 20 assembly stubs for CPU exceptions (vectors 0–21); critical exceptions (NMI, Double Fault, Stack Fault, GP, Page Fault, Machine Check, Debug) are assigned dedicated IST stacks; the Modula-2 handler prints a full register dump and halts
- Serial I/O - COM1 UART at 38400 baud for debug output
- Framebuffer - reads the Limine framebuffer response and fills the display with RGB stripes
- 100% Modula-2 - no C code anywhere in the kernel; all runtime functions (
memcpy,memset,memmove,memcmp) are implemented in pure Modula-2; privileged x86 instructions (lgdt, ltr, far return) are issued via Modula-2 inline assembly - Modula-2 kernel modules - clean separation of concerns via
DEFINITION/IMPLEMENTATIONmodules - Hierarchical build system - architecture-aware Makefile hierarchy with
ARCHandTOOLSinheritance
├── Makefile # Top-level build: kernel.elf only
├── build.sh # ISO packaging with xorriso
├── run.sh # QEMU launcher with UEFI/OVMF
├── limine.conf # Limine bootloader config (protocol: limine)
├── src/
│ ├── Makefile # Compiles Main.o, stub.S, delegates to arch/, libc/, libm2/
│ ├── Main.def # Application-level main definition
│ ├── Main.mod # Application-level main (HLT loop)
│ ├── stub.S # Assembly stubs for gm2 runtime
│ ├── boot/ # Architecture-independent boot protocol defs
│ │ └── Limine.def # Limine protocol - types, constants, externed request vars
│ ├── arch/
│ │ └── x86_64/
│ │ ├── Makefile # Compiles ArchMain.o, boot/boot.S, delegates to cpu/ and libm2/
│ │ ├── ArchMain.def # x86_64 arch entry point definition
│ │ ├── ArchMain.mod # x86_64 arch entry point (serial, framebuffer, calls GDTInit)
│ │ ├── linker.ld # Linker script (higher-half at 0xFFFFFFFF80000000)
│ │ ├── boot/
│ │ │ └── boot.S # Limine request structures + 64-bit entry (GAS, Intel syntax)
│ │ ├── cpu/
│ │ │ ├── Makefile
│ │ │ ├── GDT.def # GDT + 64-bit TSS module definition
│ │ │ ├── GDT.mod # GDT + TSS setup (Modula-2 with inline asm for lgdt/ltr)
│ │ │ ├── IDT.def # IDT module definition
│ │ │ ├── IDT.mod # IDT setup (256-entry table, InstallInterrupt, lidt)
│ │ │ ├── Exceptions.def # CPU exception handler definition + InterruptFrame type
│ │ │ ├── Exceptions.mod # Exception routing (panic + register dump)
│ │ │ └── isr.S # Assembly stubs for 20 CPU exceptions (GAS, Intel syntax)
│ │ └── libm2/
│ │ ├── Makefile
│ │ ├── BitByteOps.def # Bit manipulation library
│ │ └── BitByteOps.mod # Bit manipulation via inline assembly
│ └── libc/
│ ├── Makefile
│ ├── string.def # string module - memcpy, memset, memmove, memcmp
│ └── string.mod # Pure Modula-2 implementation (FOR "C" linkage)
├── .gitignore
└── README.md
Limine (UEFI / BIOS)
│ - Boots the ELF directly in 64-bit long mode
│ - Sets up paging, maps kernel in higher-half (0xFFFFFFFF80000000+)
│ - Sets up a ≥64 KiB stack, fills in Limine response structures
│ - CS=0x28, DS/ES/SS/FS/GS=0x30, interrupts disabled, RAX–R15 = 0
│
└── Call ArchMain_KernelMain()
└── ArchMain.mod
├── SerialInit (COM1)
├── Verify Limine base revision == 6
├── GDTInit (cpu/GDT.mod - load kernel-owned GDT + 64-bit TSS)
│ ├── Assemble 7 GDT descriptors (null / kernel code+data /
│ │ user code+data / TSS low+high)
│ ├── Populate TSS with rsp0 = top of ring-0 stack,
│ │ ist1..ist7 = top of seven 8 KiB IST stacks
│ ├── lgdt, long-return to reload CS, set DS/ES/SS/FS/GS,
│ └── ltr to load the TSS selector 0x28
├── IDTInit (cpu/IDT.mod - zero 256-entry IDT, load IDTR via lidt)
├── InstallExceptions (cpu/Exceptions.mod + cpu/isr.S)
│ ├── Install 20 interrupt gates for vectors 0–21
│ │ (NMI→IST1, DF→IST2, SS→IST3, GP→IST4, PF→IST5, MC→IST6, DB→IST7)
│ └── Each gate points to an assembly stub that saves all
│ registers and calls Exceptions_ExceptionHandler
├── Read framebuffer response from limineFramebufferRequest
├── Fill framebuffer with RGB stripes
└── Call Main.Main()
└── Main.mod (HLT loop)
| Tool | Path |
|---|---|
| GNU Modula-2 (gm2) | part of GCC 16.1.0 |
| Limine binaries | ../limine-binary |
| OVMF (UEFI) | ../edk2-ovmf |
| xorriso | system package |
| QEMU | system package |
make # Build kernel.elf only
make clean # Remove all build artifacts
./build.sh # Build kernel.elf + package limine.iso
./build.sh clean # Clean + build
# Override architecture or toolchain
ARCH=aarch64 ./build.sh./run.sh # Launch in QEMU with OVMF (UEFI), serial on stdio, debug interrupts
./run.sh --quiet # Same but without the -d int,cpu_reset noise
# Override architecture or ISO
ARCH=aarch64 ./run.sh
ISO=mykernel.iso ./run.shThe build system is designed for extensibility across architectures:
Makefile (top-level)
ARCH, TOOLS, AS, LD, M2, M2FLAGS - exported to sub-makes
|
└── src/Makefile
├── stub.o (from stub.S)
├── Main.o (from Main.mod)
├── arch/$(ARCH)/Makefile ← architecture-specific
│ ├── boot/boot.o (from boot.S - Limine requests + entry)
│ ├── ArchMain.o (from ArchMain.mod)
│ ├── cpu/Makefile
│ │ ├── GDT.o (from GDT.mod - GDT + TSS setup)
│ │ ├── IDT.o (from IDT.mod - IDT setup)
│ │ ├── Exceptions.o (from Exceptions.mod - exception routing)
│ │ └── isr.o (from isr.S - assembly exception stubs)
│ └── libm2/Makefile
│ └── BitByteOps.o
└── libc/Makefile
└── string.o (+ future stdlib.o, stdio.o, ...)
To add a new architecture:
- Create
src/arch/<arch>/with its ownMakefile, linker script, and boot code - Add any arch-specific Limine request declarations to
boot.S - Add the object files to
ARCH_OBJSin the top-levelMakefile - Add architecture-specific
M2FLAGSin the top-levelMakefile
The kernel uses no C code - every line is either Modula-2 or x86 assembly. The minimal gm2 runtime (-flibs=min) provides only SYSTEM, M2RTS, and a bare-bones interface. Heap, file I/O, exceptions, and the C standard library are all absent - just bare-metal Modula-2.
Key compiler flags:
-mcmodel=large- required for kernel-space code-mno-red-zone- interrupts would corrupt the red zone-mno-mmx -mno-sse- no floating-point/vector in kernel-fno-exceptions- no Modula-2 exception handling
Unlike Multiboot2 (which requires a 32-bit entry stub, manual page-table construction, and a long-mode transition trampoline), the Limine protocol hands off directly in 64-bit mode:
boot.Splaces a start marker, a base-revision tag, a set of request structures, and an end marker into the.limine_requests*sections- Each request is a 64-byte (or larger) struct identified by a 4×
uint64ID; the entry-point request points to_start - Limine scans the loaded ELF between the markers, collects the requests, fills in
responsepointers, and jumps to the entry point - The kernel reads
limineFramebufferRequest.response->framebuffers[0]directly - no tag-walking required
Request/response variables are declared in boot.S and externed in src/boot/Limine.def so that Modula-2 code can read them without needing C __attribute__((section(...))).
The libc/ directory follows a modular naming scheme - each sub-module is a separate Modula-2 definition/implementation pair:
| File | Module Name | Provides |
|---|---|---|
string.def / string.mod |
string |
memcpy, memset, memmove, memcmp |
stdlib.def / stdlib.mod |
(future) | Standard library functions |
stdio.def / stdio.mod |
(future) | I/O functions |
All modules use FOR "C" linkage and EXPORT UNQUALIFIED so their symbols are callable with C calling convention - satisfying compiler-generated references to memcpy, memset, etc.
| Region | Virtual Address | Notes |
|---|---|---|
| Kernel load | 0xFFFFFFFF80100000 |
Loaded and mapped by Limine |
| Framebuffer | HHDM address (e.g. 0xFFFF800080000000) |
Mapped in the Higher Half Direct Map |
| Boot stack | bootloader-reclaimable | ≥64 KiB, provided by Limine |
The kernel is a higher-half kernel. Limine maps all usable/bootloader/framebuffer memory through the Higher Half Direct Map (HHDM); the offset is available via limineHhdmRequest.response->offset if needed.
If you're setting up a fresh environment, you'll need:
- GNU Modula-2 (gm2) - part of GCC 16.1.0
- The Limine binary distribution
- OVMF (UEFI firmware) from edk2
- QEMU and xorriso
- GNU Modula-2 - compiler and runtime libraries
- Limine - bootloader and boot protocol
- Claude Code (Anthropic) - AI-assisted development for boilerplate codes, directory reorganization, testing, and debugging.