A port of the classic Linux 0.11 kernel (Linus Torvalds, 1991) to the Zig programming language, targeting bare-metal 32-bit x86 (i386) — the same architecture the original ran on.
The goal is a faithful re-implementation of Linux 0.11's architecture: the
same subsystems, the same algorithms, the same selector/IDT layout and the same
file structure, rewritten in modern, memory-safe-ish Zig instead of C and
hand-written assembly. It boots to protected mode, enables paging, runs a
preemptive-timer-driven scheduler over multiple tasks, takes keyboard input and
services system calls through int 0x80 — all verifiable under QEMU.
Linux 0.11 (Zig port) starting...
Multiboot info block at 0x00009500
Console initialised.
GDT loaded (132 entries).
IDT loaded, exceptions and PIC initialised.
Paging enabled; page pool has 3072 free pages (12 MiB).
get_free_page -> 0x00fff000, 0x00ffe000 (3070 free)
after free/realloc -> 0x00fff000 (3070 free)
Scheduler and 100 Hz timer initialised.
System-call gate (int 0x80) installed.
Keyboard initialised.
syscall write() via int 0x80 works!
getpid() -> 0, time() -> 0s
Spawned taskA and taskB.
Type on the keyboard; timer is ticking.
[taskA] iteration 0 (jiffies=6)
...
[swapper] 1s: jiffies=100
[swapper] 2s: jiffies=200
[swapper] 3s: jiffies=300
Requires Zig 0.14.x and qemu-system-i386. If you don't have them,
scripts/setup-env.sh installs both (Zig to /opt/zig, qemu via apt):
bash scripts/setup-env.sh # optional: install the toolchainThen:
zig build # build the kernel ELF -> zig-out/bin/system
zig build run # build and boot under QEMU (serial on stdio)
zig build debug # boot and wait for GDB on :1234 (qemu -s -S)The kernel is a freestanding i386 ELF with a Multiboot header, so QEMU can load
it directly with -kernel. To watch it headless, the VGA console is mirrored to
COM1:
qemu-system-i386 -kernel zig-out/bin/system -serial stdio -display none| Subsystem | This port | Original 0.11 source |
|---|---|---|
| Boot / entry | src/boot/boot.zig — Multiboot header, _start, stack, kmain |
boot/bootsect.s, boot/setup.s, boot/head.s |
| GDT | src/boot/head.zig — kernel code/data + per-task TSS/LDT slots |
boot/head.s, include/asm/system.h |
| IDT / exceptions / PIC | src/kernel/traps.zig — comptime-generated ISR stubs, dispatcher, 8259 remap |
kernel/traps.c, kernel/asm.s |
| Descriptor helpers | src/include/system.zig — set_{intr,trap,system}_gate, lgdt/lidt/ltr/lldt |
include/asm/system.h |
| Port I/O | src/include/io.zig — inb/outb/cli/sti/hlt |
include/asm/io.h |
| VGA console | src/kernel/chr_drv/console.zig — 80×25 text, scrolling, HW cursor |
kernel/chr_drv/console.c |
| Serial | src/kernel/chr_drv/serial.zig — COM1 UART (debug console) |
kernel/chr_drv/serial.c, rs_io.s |
| Keyboard | src/kernel/chr_drv/keyboard.zig — IRQ1, scan-code tables, shift/ctrl/caps |
kernel/chr_drv/keyboard.S |
| printk | src/kernel/printk.zig — formatted kernel output |
kernel/printk.c, kernel/vsprintf.c |
| Scheduler / timer | src/kernel/sched.zig — task table, 100 Hz PIT, schedule(), context switch |
kernel/sched.c |
| Paging / page alloc | src/mm/memory.zig — identity map, get_free_page/free_page, mem_map[] |
mm/memory.c, boot/head.s |
| System calls | src/kernel/sys.zig, src/include/unistd.zig — int 0x80 gate + dispatcher |
kernel/system_call.s, kernel/sys.c, include/unistd.h |
| fork | src/kernel/fork.zig — copy_process/sys_fork (task-table half) |
kernel/fork.c |
| Block device | src/kernel/blk_drv/ramdisk.zig — RAM disk serving an embedded image |
kernel/blk_drv/{hd,floppy,ll_rw_blk}.c |
| Buffer cache | src/fs/buffer.zig — (dev,block)-keyed cache, bread/brelse |
fs/buffer.c |
| Filesystem (MINIX v1) | src/fs/{fs,super,inode,namei,file,open}.zig — mount, iget, bmap, namei, file_read, descriptors |
fs/{super,inode,namei,file_dev,open,file_table}.c |
| panic | src/kernel/panic.zig |
kernel/panic.c |
The filesystem image itself is generated by tools/mkfs_minix.py (a
self-contained MINIX v1 mkfs) and embedded into the kernel.
Each source file names the original file it ports and explains where it stays faithful and where it deliberately diverges.
Everything below was exercised under qemu-system-i386 during development:
- Boots via Multiboot straight into 32-bit protected mode and prints.
- Loads its own GDT (kernel
CS=0x08,DS=0x10) and IDT. - Catches CPU exceptions: a deliberately triggered fault is reported with a full register dump and panics.
- The 8253 PIT fires IRQ0 at exactly 100 Hz (
jiffies= 100/200/300 per second). - The scheduler context-switches between kernel tasks, which run and exit
cleanly, returning control to the idle
swappertask. - The IRQ1 keyboard handler decodes keystrokes (with shift) and echoes them.
- Paging is enabled (CR0.PG) and the kernel keeps running on the identity map; the page allocator hands out and reclaims pages.
write(),getpid()andtime()round-trip through the realint 0x80system-call gate.- The MINIX v1 root filesystem mounts off the RAM disk:
/lists correctly, files (including the nested/etc/motd) read back with exact byte counts through the buffer cache / inode /bmap/nameipath, and the same file reads back again viaopen()/read()/close()overint 0x80.
A few places intentionally depart from a byte-for-byte port, always documented in the relevant source file:
- Booting. The original 16-bit real-mode boot chain (
bootsect.sloadingsetup.s, BIOS disk reads, A20, the switch to protected mode) is replaced by a Multiboot header. A Multiboot loader (QEMU-kernel, or GRUB) drops us into 32-bit protected mode with A20 already enabled, so the port can focus on the kernel proper rather than re-deriving BIOS floppy loading in Zig. - Tasks run in ring 0. The scheduler currently switches kernel threads
with an explicit register/stack swap (
switch_ctx) rather than the CPU's hardware task switch (ljmpthrough a per-task TSS) over per-process page tables. The GDT already reserves the TSS/LDT descriptor slots (seehead.zig) so the move to ring-3 tasks is the natural next step. printkformatting uses Zig'sstd.fmtplaceholders ({d},{x},{s}) rather than C's%d/%x, giving compile-time-checked format strings.
The pieces that build on per-process address spaces are the next milestones:
- User mode: ring-3 execution, per-task TSS/LDT switching, demand paging and
copy-on-write in the
int 14(page fault) handler. - A real
fork/execvewithcopy_mem, loading a binary off the filesystem and moving to user mode to run process 1. - The filesystem write path (
bmapallocation,new_block/new_inode, buffer write-back) and real block drivers — hard disk and floppy (kernel/blk_drv/hd.c,floppy.c) in place of the RAM disk. - The full tty layer (line discipline,
tty_io.c) feeding a shell.
The read side of the MINIX v1 filesystem and the buffer cache are already in place (see the table above).
build.zig freestanding i386 build + qemu run/debug steps
linker.ld link at 1 MiB with a Multiboot header
src/
kernel.zig root module (pulls in boot/, sets src/ as the module base)
boot/ boot.zig (entry), head.zig (GDT + paging descriptors)
kernel/ main.zig, sched.zig, traps.zig, sys.zig, fork.zig,
printk.zig, panic.zig
chr_drv/ console.zig, serial.zig, keyboard.zig
blk_drv/ ramdisk.zig (+ embedded disk.img)
mm/ memory.zig (paging + page allocator)
fs/ fs.zig, buffer.zig, super.zig, inode.zig, namei.zig,
file.zig, open.zig (MINIX v1 read path)
include/ io.zig, system.zig, unistd.zig
tools/mkfs_minix.py generator for the embedded MINIX v1 image