Forge

x86_16 Real Mode (Flat / Raw Images)

16-bit real-mode target for bare-metal boot sectors and multi-stage images.

Target Triple

x86_16-boot

Not a Rust target — built using Forge’s internal 16-bit assembler.

Build

# Boot sector: 512-byte flat binary with 0x55AA signature
./target/release/forgec examples/bootloader.dev -o boot.bin --target x86_16-boot

# Bare stage image (no boot signature), e.g. a stage-2 loader or kernel
./target/release/forgec src/kernel/kernel.dev -o kernel.raw --linker examples/os/os.fld

Output Formats

FORMAT Produces Use case
flat 512-byte boot sector, padded, 0x55AA at offset 510 Stage 1, loaded by the BIOS at 0x7C00
raw The image as-is, no padding or signature Stages 2+, loaded by a previous stage

The FORMAT raw form is selected with a .fld linker descriptor (FORMAT raw). Because a raw stage can be loaded anywhere, its in-memory load address is declared with the LOAD directive (default 0x7C00); the compiler fixes up absolute string addresses against this base, so a stage loaded elsewhere (e.g. a loader at 0x9000) must set it.

Architecture Details

Property Value
Architecture 8086/8088 (real mode)
Format Flat / raw binary (raw bytes)
Endianness Little
Pointer Size 16-bit (segmented)
Default Load Address 0x7C00
Boot Signature 0x55AA at offset 510 (flat only)

Memory Model (Real Mode)

Segmentation

Physical Address = Segment * 16 + Offset

ForgeOS sets flat segments: ds/es/ss = 0, stack top at 0x7C00.

Memory Map

0x00000 - 0x003FF   IVT (256 vectors × 4 bytes)
0x00400 - 0x004FF   BIOS Data Area
0x00500 - 0x07BFF   Free (conventional)
0x07C00 - 0x07DFF   Boot Sector (512 bytes)
0x07E00 - 0x9FFFF   Free
0xA0000 - 0xBFFFF   Video RAM
0xC0000 - 0xFFFFF   BIOS ROM

Calling Convention

No standard ABI. Forge uses a stack-based convention:

Runtime Helpers (_dev_*)

Declared extern and emitted only when referenced. Available on x86_16:

Helper Implementation
_dev_bios_teletype(c: char) INT 10h AH=0Eh teletype output
_dev_serial_putc(c: char) Debug port output (out 0xE9, al — Bochs/QEMU)
_dev_load_char(p: ptr[char]) -> char Load a byte from memory
_dev_bios_key() -> char INT 16h AH=00h blocking keystroke
_dev_bios_disk_read(drive, cyl, head, sector, count, buf) -> u16 INT 13h AH=02h disk read (ES = addr » 4, BX = addr & 0xF)
_dev_bios_disk_reset(drive: u16) INT 13h AH=00h disk reset
_dev_bios_reboot() INT 19h bootstrap reboot
_dev_bios_clear() INT 10h AH=00h AL=03h — 80x25 text mode (clears the screen)
_dev_jump(addr: u16) Far jump to a flat address
_dev_halt() cli; hlt

Boot Process

  1. BIOS loads the first sector (512 bytes) to 0x7C00, jumps to it
  2. The boot sector (FORMAT flat) sets segments/stack, reads the next stage with _dev_bios_disk_read, and far-jumps to it via _dev_jump
  3. Each subsequent stage (FORMAT raw, its own LOAD address) does the same until the kernel is in place

ForgeOS (examples/os/) is a three-stage reference implementation of this process — see the ForgeOS example.

Standard Library (Minimal)

Module Support
std.io ✅ (BIOS INT 10h/16h, debug port)
std.mem ✅
std.string ✅
std.math ✅ (int only)
std.alloc ❌
std.fmt ✅
std.volatile ✅
std.gc ❌
std.runtime ❌

Inline Assembly

asm!() templates are accepted by the parser and the x86_16 backend assembles the verbatim template text (other targets error at codegen):

asm!("cli")           # Clear interrupts
asm!("sti")           # Set interrupts
asm!("hlt")           # Halt CPU
asm!("int 0x10")      # BIOS call

ForgeOS itself uses no inline assembly — everything is built from Forge source and the _dev_* helpers.

Constraints

Constraint Limit
flat image size ≤ 510 bytes of code + 0x55AA
raw image size Whatever the loading stage can read
Stack Limited (conventional memory)
Heap None
Protection None (real mode)
Paging None
Multitasking None

Example: Minimal Bootloader

@freestanding
pub def _start() -> void:
    puts("Hello, Forge bootloader!")
    _dev_halt()

def puts(msg: ptr[char]) -> void:
    unsafe:
        var p = msg
        var c = _dev_load_char(p)
        while c != 0:
            _dev_bios_teletype(c)
            p = p + 1
            c = _dev_load_char(p)

extern def _dev_bios_teletype(c: char) -> void
extern def _dev_load_char(p: ptr[char]) -> char
extern def _dev_halt() -> void

Build & Test

# Build
forgec examples/bootloader.dev -o boot.bin --target x86_16-boot

# Verify
ls -la boot.bin        # 512 bytes
hexdump -C boot.bin     # Check 0x55AA at end

# Run in QEMU
qemu-system-x86_64 -fda boot.bin -nographic

# Expected:
# SeaBIOS...
# Booting from Floppy...
# Hello, Forge bootloader!

For multi-stage images under QEMU, prefer the IDE disk with multi-thread TCG (the floppy controller can hang single-thread TCG):

qemu-system-x86_64 -accel tcg,thread=multi \
  -drive file=build/os.img,format=raw,if=ide -nographic

Debugging

# QEMU with GDB
qemu-system-x86_64 -fda boot.bin -nographic -s -S

# In another terminal
gdb -ex "target remote localhost:1234" -ex "set architecture i8086"

# Bochs (better 16-bit debugging)
bochs -q -f bochsrc.txt

Tips

  1. Keep it small: A boot sector gets 510 bytes
  2. Use BIOS: Leverage INT 10h/13h/16h for I/O
  3. Avoid floats: No FPU in real mode by default
  4. Manual segments: The generated entry sets DS/ES/SS = 0 and SP = LOAD
  5. Declare LOAD: Any stage not loaded at 0x7C00 must set LOAD in its .fld, or its string references will point at the wrong addresses