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): Only dependency is on an assembler
- 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)
- 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
├── util.inc # Utility functions
├── forth.inc # The Forth system
├── tests.inc # Tests
├── 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 ┌─────────────────────────────┐
│ User Dictionary (grows UP) │ ~15 MB free
0x00600500 ├─────────────────────────────┤
│ Buffer │ 256 bytes
0x00600400 ├─────────────────────────────┤
│ Data stack (grows DOWN) │ 1 KB, 256 entries
0x00600000 ├─────────────────────────────┤
│ Graphics buffer │ 4 MB
0x00200000 ├─────────────────────────────┤
│ Kernel + ESP stack │ 1 MB (16 KB stack)
0x00100000 ├─────────────────────────────┤
│ VGA text (HW) │ 4 KB
0x000B8000 ├─────────────────────────────┤
│ BIOS / System │
0x00000000 └─────────────────────────────┘
- 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
Dictionary Entry Format:
[Offset 0:3] Link pointer to previous entry (4 bytes)
[Offset 4:7] Execution Token (XT) (4 bytes)
[Offset 8] Flags (1 byte)
[Offset 9] Length (1 byte)
[Offset 10:n] Name (variable length)
[Offset n+1] NULL (1 byte)
[Offset n+2:m] Inline code (XT, variable size)
- Data stack: EBP (data stack pointer, grows downward from DATA_STK_BASE)
- Stack macros:
dPush val- Push a value onto the data stackdPop reg- Pop from data stack into a registerdDrop- Drop the top of stackgetTOS reg- Read top of stack (non-destructive)getNOS reg- Read 2nd element (non-destructive)setTOS val- Set top of stacksetNOS val- Set 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 32M# 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- Stack abstraction (EBP-based data stack)
- Dictionary infrastructure
- Core primitives (in progress)
- Number parsing (numq with multiple bases)
- Dictionary lookup (case-insensitive)
- FORTH interpreter loop
- Scancode -> ASCII conversion (raw scancodes in buffer)
- Graphics buffer allocated but unused
- Disk support
Why pure assembly?
- No dependency on a 3rd party compiler
- Total control over memory layout and execution
- Minimal overhead (very small kernel!)
- Single executable file, no dependencies
- Perfect for bare metal + FORTH experimentation
Register conventions:
- EAX, EBX, ECX, EDX: scratch
- ESI, EDI: String pointers / scratch
- ESP: Return stack (Forth and x86 stack calls/returns)
- EBP: FORTH data stack pointer (grows downward, initialized to
DATA_STK_BASE)
Calling convention:
- No STDCALL (manual stack management)
- Return/Exit via RET (Subroutine threading)
- FASM (v1.73.30) - Compact, elegant, open-source assembler
- GNU ld - Linker with custom script
- QEMU - Machine emulator (i386 mode)
- readelf/objdump - ELF inspection
- OSDev.org Wiki
- Multiboot Specification
- x86 Instruction Set Reference
- FASM Documentation
- FORTH Standards
MIT License