Skip to content

Build and Run

Prerequisites

Run make init once to install the toolchain:

make init

This installs:

  • Rust nightly via rustup
  • Target x86_64-unknown-none
  • Components rust-src and llvm-tools-preview
  • bootimage cargo subcommand
  • grub2-mkrescue must be present on the host (package: grub2-tools-extra or equivalent)
  • mtools (mmd, mcopy) for floppy image creation

Build

Release build (both video modes)

make build

Produces r2.iso. Internally runs two cargo invocations (with -Z build-std=core,compiler_builtins -Z json-target-spec --target x86_64-r2.json):

Feature flag Output ELF Description
kernel_text iso/boot/kernel_text.elf VGA text-mode path
kernel_graphics iso/boot/kernel_graphics.elf VESA framebuffer path

The two are the same kernel apart from the multiboot header: the graphics one asks GRUB for a 32-bit framebuffer of no particular size, so GRUB takes its own choice (with the BIOS VBE driver, the monitor's preferred mode from its EDID). The framebuffer is mapped write-combining at boot (src/video/wc.rs); uncached, as the firmware leaves it, drawing on it on real hardware cost a bus transaction per pixel. The kernel's console still writes to VGA text memory, which is not shown in a framebuffer mode, so the graphics kernel's desktop is Memento instead: once INIT.RC has run, init_rc starts fg memento. The graphics kernel starts no kernel shell (kshell): nothing it prints could be seen, and it reads the same keys as the programs do, so what was typed into Memento would have been run as commands too. Its INIT.RC comes from the floppy when there is one, and otherwise from /mnt/tar/opt/init.rc (then /mnt/iso/opt/init.rc), which build_iso copies from configs/init.rc: booted from a USB stick there is no floppy, and that is where bg eth comes from. On either kernel only the file's own length is run, not the stale bytes after it in its last cluster. Memento fills the screen: its desktop is the framebuffer's size divided by the largest whole number that leaves at least 640x400 (1920x1080 is a 960x540 desktop drawn at 2x), in 256 colours, and only the rows that changed are sent (syscall 0x19). If GRUB picks a mode other than the monitor's own, name it in grub.cfg's graphics entry, before multiboot2: set gfxpayload=1920x1080x32. When Memento ends (Esc on its first screen) the machine restarts, by ACPI (see reboot); a Memento that ran for less than five seconds, one that could not start, does not restart it. Leave fg memento out of INIT.RC for this boot, or it starts twice. The text kernel is unchanged: its shell, and Memento in 640x400x16 on the VGA.

Both ELFs are placed inside iso/boot/. build_iso then copies the userland binaries from the apps repository (expected at ../r2_app) into iso/bin/, and grub2-mkrescue assembles the iso/ tree into r2.iso with the modules multiboot2 video video_bochs video_cirrus gfxterm all_video.

The resulting ISO is mounted at /mnt/iso at boot:

ISO path Contents
/boot kernel_text.elf, kernel_graphics.elf, GRUB config
/bin Userland programs (eth, garn, tnt, chat, sh, fsck, nsk, them, hellofs, hello, dish, gfxdemo, routtest, snake, …). Searched by bg/fg when a binary is not in the working directory.
/games Optional DOS games for the THEM emulator (not part of the repository)

Build the apps first (see SDK); build_iso fails if a listed binary is missing.

Debug build

make build_debug

Runs compile_kernel_debug and build_iso. Builds the text-mode kernel with features kernel_text,serial_debug. Serial debug output is written to COM1 (rprint!/rprintb!/rprintn! macros). Note: serial debug disables SLIP networking (both use COM1).

Optional extra features:

make build_debug EXTRA_FEATURES=serial_debug

Clean

make clean    # cargo clean

Target Specification (x86_64-r2.json)

Field Value
llvm-target x86_64-unknown-none
os none
linker rust-lld (LLD)
relocation-model static
panic-strategy abort
disable-redzone true
features -mmx,-sse,+soft-float
exe-suffix .elf

SSE is disabled at the target level; SSE is re-enabled at runtime by init::cpu::enable_sse (after the kernel stack is set up). The soft-float feature prevents LLVM from emitting SSE instructions before that point.


Linker Script (linker.ld)

The kernel is linked starting at physical address 0x100000 (1 MiB).

Region Address / size Notes
.multiboot2_header 0x100000 (4 KiB aligned) GRUB Multiboot2 header
.text follows All code
.rodata follows Read-only data, embedded fonts
.data + .dma follows Writable globals; .dma section holds the DMA: [u8; 512] floppy buffer at a known physical address
.bss follows Zero-initialised statics
.gdt follows GDT descriptor (assembly)
.idt follows IDT descriptor (assembly)
__stack_bottom/top follows + 64 KiB Kernel boot stack
__heap_start/end follows + 64 KiB Kernel linked-list heap
p4_table 4 KiB aligned PML4 page table
p3_fb_table 4 KiB P3 table for framebuffer mapping
.user_task 0x650000 Unused user-task section placeholder
.dma (DMA buffer) 0x80000 (512-byte aligned) Physical DMA target for ISA DMA channel 2

The p2_table, p2_high_tables, p3_table, ist0/ist1_stack (16 KiB each: the double fault runs on ist0_stack via TSS ist1, the page fault on ist1_stack via ist2), tss64, multiboot_ptr, and debug_flag symbols all live in assembly .bss in boot.asm.


Boot Flow

GRUB (Multiboot2)
  │  loads r2.iso, selects kernel ELF
  │  passes Multiboot2 info pointer in EBX, magic in EAX
  ▼
_start  (boot.asm, 32-bit protected mode)
  ├── saves EBX/EAX → [multiboot_ptr] / [multiboot_magic]
  ├── loads P4 table address into CR3
  ├── set_up_page_tables()
  │     identity-maps 1 GiB via P2 (512 × 2 MiB huge pages)
  │     identity-maps 1–4 GiB via 3 more P2 tables (p2_high_tables, 2 MiB pages;
  │     not 1 GiB pages, which need the optional pdpe1gb CPU feature)
  │     marks P2[2..4] USER+WRITE (0x400000–0xA00000, userland range)
  ├── load_gdt()    — lgdt from gdt_descriptor
  ├── load_idt()    — lidt (empty; real IDT installed by init later)
  ├── set segment registers to data selector 0x10
  ├── enable_paging()
  │     CR4 bit 5 (PAE), EFER bit 8 (LME), CR0 bit 31 (PG)
  └── far jump to long_mode_entry (selector 0x08 = 64-bit code)
  ▼
long_mode_entry  (boot.asm, 64-bit long mode)
  ├── TLB flush (mov cr3, cr3)
  ├── set segment registers to 0x10
  ├── RSP ← __stack_top  (64 KiB kernel stack)
  └── call kernel_main(multiboot_magic, multiboot_ptr)
  ▼
kernel_main  (src/main.rs)
  ├── init::check::init(multiboot_ptr)   — 14-step boot sequence
  │     (see docs/init/overview.md)
  └── task::scheduler::idle(0xff)        — enters scheduler; never returns

GDT Layout (from boot.asm)

Selector Descriptor Description
0x00 null Required null descriptor
0x08 0x00AF9A000000FFFF Kernel code (64-bit, DPL=0)
0x10 0x00AF92000000FFFF Kernel data (DPL=0)
0x18 0x00affa000000ffff User code (64-bit, DPL=3)
0x20 0x00aff2000000ffff User data (DPL=3)
0x28 TSS descriptor (patched at runtime) 64-bit TSS

The TSS descriptor at 0x28 is initially a placeholder; init::idt::setup_tss_descriptor overwrites it with the correct base address and limit before ltr 0x28 is issued.


Floppy Image (make build_floppy)

Creates a 1.44 MB FAT12 floppy image (fat.img by default, override with FLOPPY_IMAGE=):

make build_floppy
make build_floppy FLOPPY_IMAGE=my.img
  1. dd creates a blank 2880-sector image.
  2. mkfs.fat -F 12 formats it.
  3. mmd creates directories: BIN, GARN, GFX, SLIP, SOUND, THEM, DYNA.
  4. mcopy copies data files and demos (from ../r2_app/ and this repository):
Floppy path Source
INIT.RC configs/init.rc
GARN/GARN.CFG, GARN/INDEX.HTM, GARN/HELLO.TXT, GARN/FAVICON.ICO, GARN/SOCKETS.JSN GARN web server config and content
GFX/CUBE.ELF, GFX/GFXTEST.ELF, GFX/MEMENTO.ELF Graphics demos
SLIP/ICMPR.ELF ICMP responder over SLIP
SOUND/*.MID MIDI files for syscall 0x1b
THEM/PRG0.BIN, THEM/VLAK.COM Real-mode programs for the THEM emulator

The main programs are no longer copied to the floppy: they live in /mnt/iso/bin on the ISO, which leaves the 1.44 MB floppy for data.

The INIT.RC file is the startup script parsed by init_rc at boot (see below).


Startup Script (configs/init.rc)

INIT.RC is read from the FAT12 root directory by the init_rc task during boot. Each non-blank, non-comment line is dispatched through cmd::handle — the same function used by the interactive shell.

Example configs/init.rc:

# Start network driver
bg eth

# Start TNT with config
bg tnt eth

# Start GARN web server
bg garn --config /mnt/fat/GARN/GARN.CFG

echo INIT.RC done

Binary names are resolved like in the shell: working directory first, then /mnt/tar/bin and /mnt/iso/bin, so bg eth works without the program being on the floppy. A fg line parks init_rc until that program exits.

Lines starting with # are ignored. Trailing \r is stripped (DOS line endings tolerated).


Run Targets

Make target Description
make run_iso QEMU with CD-ROM only, 2 GB RAM, VGA std, serial PTY
make run_iso_floppy + FAT12 floppy + PC speaker audio
make run_iso_net CD + floppy + RTL8139 NIC on tap0 + PC speaker audio, GTK display, serial PTY
make run_iso_debug CD + floppy, serial → stdio, audio, no-reboot
make run_iso_debug_int Same + -d int,cpu_reset,page (interrupt tracing)
make run_iso_pty PTY_NUMBER=ptyN CD only, serial on specific PTY
make run_iso_usb CD replaced by /dev/sdb (USB stick)
make run_iso_floppy_drive Live floppy /dev/sda (requires sudo)

Standard run with networking:

make run_iso_net

QEMU network setup assumes tap0 is already created on the host. The kernel RTL8139 driver auto-detects the NIC via PCI scan.

sudo ip tuntap add dev tap0 mode tap
sudo ip link set tap0 up
sudo ip addr add 10.3.4.1/24 dev tap0

Before starting QEMU, run_iso_net also runs a few best-effort host tweaks with sudo (each one ignored if it fails): it sets the tap0 MAC address, puts tap0 and 10.3.4.0/24 into the firewalld trusted zone, disables reverse-path filtering on tap0, flushes conntrack, and adds an nftables raw-table accept rule for traffic from tap0. The guest uses 10.3.4.2 by default, with the host at 10.3.4.1.


Tests

Kernel self-test (QEMU)

make test_kernel

Builds with features kernel_test,kernel_text into target/ktest, then boots headless QEMU with the isa-debug-exit device (iobase=0xf4) and serial on stdio. With kernel_test enabled, kernel_main runs ktest::run_tests() right after init::check::init instead of entering the scheduler. Each check uses kassert!, which exits QEMU on the first failure. QEMU exit code 33 (0x10 << 1 | 1) means all tests passed and prints TESTS PASSED; anything else prints TESTS FAILED.

Tests in src/ktest.rs:

Test Checks
test_fat83 8.3 name conversion
test_heap_alloc userland heap malloc/free
test_vfs_resolve /mnt/fat and /mnt/iso prefix resolution
test_path_normalize joining, .. and relative paths in vfs::normalize_path

Host unit tests

make test

Compiles and runs tests/unit/main.rs as a standard Rust test binary on the host (no QEMU). The MIDI and path modules depend only on core and are included directly by #[path], so the tests exercise the same code that runs in the kernel:

File Covers
tests/unit/fat12.rs 8.3 name conversion (a copy of fat83)
tests/unit/midi.rs Standard MIDI File parsing and the monophonic sequencer (src/audio/smf.rs)
tests/unit/path.rs Path normalisation (src/fs/vfs/path.rs)

Code Analysis

make clippy          # cargo clippy --release, all warnings as errors
make sonar_check     # SonarQube scan (requires SONAR_HOST_URL + SONAR_TOKEN env vars)