A minimal 32-bit x86 bare metal operating system kernel written entirely in pure assembly (FASM).
This is intended to be a foundation for a subroutine threaded FORTH system.
Currently runs under QEMU (the 32-bit x86 emulator) using the -kernel option.
- 32-bit x86 protected mode: Full x86-32 architecture support
- Pure Assembly (FASM): Entire kernel in single
.asmfile (~2.5 KB object) - VGA text console: 80×25 text mode output (0xB8000)
- Serial output: COM1 (0x3F8) for debugging/secondary output
- Interrupt system: IDT + 8259 PIC with PS/2 keyboard handler
- PS/2 keyboard: Ring buffer for scancode capture (IRQ1/INT 0x21)
- Memory layout: 32 MB for FORTH (stacks, dictionary, graphics buffer)
- Direct kernel loading: Boots with QEMU
-kernelflag
# Prerequisites
sudo apt-get install fasm qemu qemu-system-x86
# Clone the repository
git clone https://github.com/CCurl/bmf32.git
# Build and run
make runQEMU window will open. You'll see boot messages. PS/2 keyboard input is buffered and ready for FORTH interpreter.
.
├── kernel.asm # Bootloader + kernel + drivers
├── linker.ld # Memory layout script
├── Makefile # Build automation
├── LICENSE # License (MIT)
└── README.md # This file
make # Full build
make clean # Remove artifacts
make run # Build and run in QEMU windowToolchain:
- FASM 1.73.30+ (assembler)
- GNU ld (linker, elf_i386 format)
0x01FFFFFF ┌─────────────────────┐
│ Free space │
0x00700800 │ Dictionary + Code │ (grows UP, ~15 MB)
│ (mixed entries) │
├─────────────────────┤
0x007FFC00 │ Data stack │ (1 KB, grows down)
├─────────────────────┤
0x00600000 │ Graphics buffer │ (4 MB, VESA 1280×1024@32-bit)
├─────────────────────┤
0x00200000 │ Kernel + data │ (1 MB)
│ + ESP stack (16 KB) │
├─────────────────────┤
0x00100000 │ VGA text (4 KB, HW) │
0x000B8000 ├─────────────────────┤
│ Reserved / BIOS │
0x00000000 └─────────────────────┘
Dictionary Entry Format:
[Offset 0:3] Link pointer to previous entry (4 bytes)
[Offset 4:7] Execution Token (XT) (4 bytes)
[Offset 8:8] Flags (1 byte)
[Offset 9:9] Length (1 byte)
[Offset 10:n] Name, NULL-terminated (variable length)
[Offset n+1:m] Inline code (XT, variable size)
- Stack setup (ESP -- stack_top, 16 KB kernel stack)
- Calls kernel_main
- IDT: 256-entry interrupt descriptor table
- PIC: Master/Slave programmable interrupt controller
- Maps IRQ0-7 -- INT 0x20-0x27
- Maps IRQ8-15 -- INT 0x28-0x2F
- IRQ1 (keyboard) enabled by default
kernel_clear()- Clear screen, reset cursorvga_putchar(AL)- Write char at cursor, advance, wrap, scrollvga_write(ESI)- Write null-terminated string- Text mode: 80×25 @ 0xB8000
ser_write(ESI)- Write null-terminated string to serial port- Port: 0x3F8 (COM1)
- Used for debugging output
- Handler:
timer_handler()(INT 0x20/IRQ0) - Counter:
timer_ticks- Incremented on each timer tick - Reader:
timer_get_ticks()- Non-blocking, returns current tick count - Init: IRQ0 enabled by default, PIC configured
- Handler:
keyboard_handler()(INT 0x21/IRQ1) - Ring buffer: 32 scancodes, power-of-2 wrap with AND
- Reader:
keyboard_read()- Non-blocking, returns scancode or 0 - Data check:
keyboard_has_data()- Non-blocking, returns 1 if buffer has data - Status check: Port 0x64 bit 0 before reading 0x60
- Init: Disables/re-enables controller, enables IRQ1
hex_to_string(EAX, ESI)- Convert 32-bit to "0xXXXXXXXX"idt_set_entry(EAX, BL, CL)- Configure IDT entryinit_idt()- Initialize IDT, load with LIDTinit_pic()- Configure PIC for IRQ remappinginit_ps2()- Initialize PS/2 keyboard hardwarepic_enable_irq(AL)- Enable timer interrupt (clear PIC mask bit AL)timer_get_ticks()- Read current timer tick countkeyboard_read()- Non-blocking read from keyboard bufferkeyboard_has_data()- Check if keyboard buffer has pending scancodes
- Dict pointer: EBP (data stack pointer, grows downward from DATA_STK_BASE)
- Entry format: [Link(4)] [XT(4)] [Flags/Len(1)] [Name(variable)] [NULL] [Code]
- Stack macros:
dPush reg- Push register onto data stackdPop reg- Pop from data stack into registergetTOS reg- Read top of stack (non-destructive)getNOS reg- Read 2nd element (non-destructive)setTOS reg- Write top of stacksetNOS reg- Write 2nd element
# Build and run (serial output to terminal)
make qemu
# Or directly:
qemu-system-i386 -kernel kernel.elf -m 32M -serial stdio
# Without serial output:
qemu-system-i386 -kernel kernel.elf -m 32MProgress:
- ✅ Stack macros: dPush, dPop, getTOS, getNOS, setTOS, setNOS (EBP-based)
- ✅ Dictionary infrastructure (linked list, case-insensitive lookup)
- ⏳ Primitives (in progress)
Roadmap for remaining FORTH:
- More stack primitives - OVER, ROT, -ROT, DEPTH, PICK, ROLL
- Arithmetic - +, -, *, /, MOD, /MOD, ABS, MIN, MAX, NEGATE
- Comparison - <, >, =, <>, <=, >=, 0<, 0>, 0=
- Memory access - @, !, C@, C!, +!
- Control flow - IF, THEN, ELSE, BEGIN, UNTIL, LOOP, DO
- FORTH I/O - EMIT, KEY, CR, SPACES
- Interpreter loop - Token parsing, execute from dictionary
- Word definition - Colon definitions (: name ... ;)
- Graphics - PIXEL drawing using 4MB buffer
- Optimizations - JIT compilation, tail call optimization
# Inspect binary
file kernel.elf
readelf -l kernel.elf # Program headers
readelf -S kernel.elf # Section headers
nm kernel.elf # Symbols
# Disassemble
objdump -d kernel.elf | less
objdump -M intel -d kernel.elf # Intel syntax
# Check multiboot magic
objdump -s -j .multiboot kernel.elf | head -5- FORTH interpreter loop not yet implemented
- No scancode--ASCII conversion (raw scancodes in buffer)
- Graphics buffer allocated but unused
- No disk support
- Stack abstraction (EBP-based data stack)
- Dictionary infrastructure
- Core primitives (in progress)
Why pure assembly?
- No dependency on a 3rd party compiler
- Total control over memory layout and execution
- Minimal overhead (~2.5 KB object code!)
- Single executable file, no dependencies
- Perfect for bare metal + FORTH experimentation
Register conventions:
- EAX, EDX: Return values / scratch
- ESI: String pointer (calls)
- EBX, ECX: General purpose
- ESP: Return stack (Forth and x86 stack calls/returns)
- EBP: FORTH data stack pointer (grows downward, initialized to
DATA_STK_BASE)
Calling convention:
- Return via RET (pops EIP)
- No STDCALL (manual stack management)
- FASM (v1.73.30) - Compact, elegant, open-source assembler
- GNU ld - Linker with custom script
- QEMU - Machine emulator (i386 mode)
- readelf/objdump - ELF inspection
MIT License