Phase 1: two-stage loader, protected mode

Bash · Jul 17, 19:04:37 · toolu_01GDFSajUXYDxxdtjyGEce3u
Commit Phase 0 scaffolding
$ git commit -m "$(cat <<'EOF'
Phase 0: boot scaffolding

Minimal MBR boot sector that prints to VGA teletype and COM1 serial,
plus the Makefile/build pipeline (nasm -> raw disk image -> qemu-system-i386).
Verified booting headlessly with -display none -serial stdio.
EOF
)"
result
[main 42f0af3] Phase 0: boot scaffolding
 3 files changed, 71 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Makefile
 create mode 100644 boot/boot.S
TaskUpdate · Jul 17, 19:04:40 · toolu_01WhN4idSn8Nm8NN4UmgKrNc
{
 "taskId": "1",
 "status": "completed"
}
result
Updated task #1 status
TaskUpdate · Jul 17, 19:04:40 · toolu_01W2nbHJB88RYABsxrtwsbCs
{
 "taskId": "2",
 "status": "in_progress"
}
result
Updated task #2 status
Bash · Jul 17, 19:05:32 · toolu_01Qt4r8n2Ew6tJNydQs1r1qk
Check objcopy availability for kernel binary conversion
$ which objcopy && objcopy --version | head -1
result
/bin/objcopy
GNU objcopy (GNU Binutils for Ubuntu) 2.45
agent · Jul 17, 19:05:34
Now writing the two-stage bootloader and the initial kernel skeleton.
Write · Jul 17, 19:05:41 · toolu_01Dp36xpp1A6ZTLikKDYjvr7
/home/martin/workspace/prototypes/boot-to-claude/boot/boot.S
64 lines
; Stage 1 MBR boot sector.
; BIOS loads this at 0x7c00 and jumps to it in 16-bit real mode.
; Job: load stage2 (STAGE2_SECTORS sectors starting at LBA 1) to 0x0000:0x7e00
; and jump to it. Kept deliberately minimal - all real work happens in stage2.

BITS 16
ORG 0x7c00

STAGE2_SECTORS equ 4      ; must match the padding at the end of stage2.S

start:
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7c00

    mov si, msg_boot
    call print_str16

    mov si, dap
    mov ah, 0x42        ; INT13h extended read (LBA)
    mov dl, 0x80         ; first hard disk (matches qemu -drive raw image)
    int 0x13
    jc disk_error

    jmp 0x0000:0x7e00

print_str16:
    pusha
.loop:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0e         ; BIOS teletype output
    mov bh, 0
    int 0x10
    jmp .loop
.done:
    popa
    ret

disk_error:
    mov si, msg_err
    call print_str16
.hang:
    hlt
    jmp .hang

align 4
dap:                     ; disk address packet for INT13h AH=0x42
    db 0x10               ; packet size
    db 0                  ; reserved
    dw STAGE2_SECTORS      ; sectors to read
    dw 0x7e00              ; destination offset
    dw 0x0000               ; destination segment
    dq 1                    ; starting LBA (sector right after the MBR)

msg_boot: db "boot ", 0
msg_err:  db "disk err", 0

times 510-($-$$) db 0
dw 0xAA55
result
The file /home/martin/workspace/prototypes/boot-to-claude/boot/boot.S has been updated successfully. (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:05:52 · toolu_01XnL1ZFUA1KY6rm7s9fJjWo
/home/martin/workspace/prototypes/boot-to-claude/boot/stage2.S
112 lines
; Stage 2 bootloader. Loaded by stage1 at 0x0000:0x7e00, still 16-bit real mode.
; Job: enable A20, load the kernel from disk into memory below 1MB, build a
; flat GDT, switch to 32-bit protected mode, and jump into the kernel entry.

BITS 16
ORG 0x7e00

KERNEL_LOAD_ADDR equ 0x10000  ; 64KB - plenty of conventional memory headroom
KERNEL_START_LBA equ 5        ; right after stage1 (LBA 0) + stage2 (LBA 1-4)
KERNEL_SECTORS   equ 64       ; 32KB kernel image budget for now

stage2_start:
    mov si, msg_stage2
    call print_str16

    call enable_a20
    call load_kernel

    cli
    lgdt [gdt_descriptor]

    mov eax, cr0
    or eax, 1
    mov cr0, eax

    jmp CODE_SEG:protected_mode_entry

; ---------------- 16-bit helpers ----------------

print_str16:
    pusha
.loop:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0e
    mov bh, 0
    int 0x10
    jmp .loop
.done:
    popa
    ret

enable_a20:
    in al, 0x92
    or al, 2
    out 0x92, al
    ret

load_kernel:
    mov si, dap
    mov ah, 0x42
    mov dl, 0x80
    int 0x13
    jc disk_error
    ret

disk_error:
    mov si, msg_disk_err
    call print_str16
.hang:
    hlt
    jmp .hang

align 4
dap:
    db 0x10
    db 0
    dw KERNEL_SECTORS
    dw KERNEL_LOAD_ADDR & 0xFFFF          ; offset  (0x0000)
    dw (KERNEL_LOAD_ADDR >> 4) & 0xFFFF   ; segment (0x1000)
    dq KERNEL_START_LBA

msg_stage2:   db "stage2 ", 0
msg_disk_err: db "kernel disk err", 0

; ---------------- GDT: flat 4GB code/data ----------------

align 8
gdt_start:
    dq 0                            ; null descriptor
gdt_code:
    dw 0xFFFF, 0
    db 0, 10011010b, 11001111b, 0
gdt_data:
    dw 0xFFFF, 0
    db 0, 10010010b, 11001111b, 0
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

; ---------------- 32-bit protected mode entry ----------------

BITS 32
protected_mode_entry:
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000

    jmp KERNEL_LOAD_ADDR

times 2048-($-$$) db 0   ; pad to STAGE2_SECTORS (4) * 512 bytes
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/boot/stage2.S (file state is current in your context — no need to Read it back)
Bash · Jul 17, 19:05:55 · toolu_01ThVPUzzhkcvZf9PygYaG7A
Create kernel directory
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/kernel
result
(Bash completed with no output)
Write · Jul 17, 19:05:58 · toolu_01XkDSXKi4AoxCyZGxq4h8iM
/home/martin/workspace/prototypes/boot-to-claude/kernel/entry.S
; Kernel entry point. Stage2 jumps here in 32-bit protected mode with
; segments already flat and a real-mode stack still set up. Establish our
; own stack and hand off to C.

BITS 32

section .text.entry
global _start
extern kmain

_start:
    mov esp, stack_top
    call kmain
.hang:
    hlt
    jmp .hang

section .bss
align 16
stack_bottom:
    resb 16384
stack_top:
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/entry.S (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:06:01 · toolu_01GNiNQTCEebthsdbuFBSdVR
/home/martin/workspace/prototypes/boot-to-claude/kernel/link.ld
ENTRY(_start)

SECTIONS
{
    . = 0x10000;   /* matches KERNEL_LOAD_ADDR in boot/stage2.S */

    .text : {
        *(.text.entry)
        *(.text*)
    }
    .rodata : {
        *(.rodata*)
    }
    .data : {
        *(.data*)
    }
    .bss : {
        *(.bss*)
        *(COMMON)
    }
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/link.ld (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:06:23 · toolu_014wkUvhKoyXaSvx8Kf7du4t
/home/martin/workspace/prototypes/boot-to-claude/kernel/console.h
#pragma once

void console_clear(void);
void console_putc(char c);
void console_write(const char *s);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/console.h (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:06:32 · toolu_01XwzLHtsSSvFdmLJB9Rr3D7
/home/martin/workspace/prototypes/boot-to-claude/kernel/console.c
56 lines
#include "console.h"

#include <stddef.h>
#include <stdint.h>

#define VGA_MEM ((volatile uint16_t *)0xB8000)
#define VGA_COLS 80
#define VGA_ROWS 25
#define VGA_ATTR 0x07

static size_t cursor_row = 0;
static size_t cursor_col = 0;

static void console_scroll(void) {
    for (size_t row = 1; row < VGA_ROWS; row++) {
        for (size_t col = 0; col < VGA_COLS; col++) {
            VGA_MEM[(row - 1) * VGA_COLS + col] = VGA_MEM[row * VGA_COLS + col];
        }
    }
    for (size_t col = 0; col < VGA_COLS; col++) {
        VGA_MEM[(VGA_ROWS - 1) * VGA_COLS + col] = (uint16_t)((VGA_ATTR << 8) | ' ');
    }
    cursor_row = VGA_ROWS - 1;
}

void console_clear(void) {
    for (size_t i = 0; i < VGA_COLS * VGA_ROWS; i++) {
        VGA_MEM[i] = (uint16_t)((VGA_ATTR << 8) | ' ');
    }
    cursor_row = 0;
    cursor_col = 0;
}

void console_putc(char c) {
    if (c == '\n') {
        cursor_col = 0;
        cursor_row++;
    } else {
        VGA_MEM[cursor_row * VGA_COLS + cursor_col] = (uint16_t)((VGA_ATTR << 8) | (uint8_t)c);
        cursor_col++;
        if (cursor_col >= VGA_COLS) {
            cursor_col = 0;
            cursor_row++;
        }
    }
    if (cursor_row >= VGA_ROWS) {
        console_scroll();
    }
}

void console_write(const char *s) {
    while (*s) {
        console_putc(*s++);
    }
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/console.c (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:06:35 · toolu_01SgHVNwPn3MBECBNkhW7YKS
/home/martin/workspace/prototypes/boot-to-claude/kernel/serial.h
#pragma once

void serial_init(void);
void serial_putc(char c);
void serial_write(const char *s);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/serial.h (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:06:38
Task #2 is already in_progress, tracking correctly. Continuing with the kernel skeleton.
Write · Jul 17, 19:06:42 · toolu_01PJJsuxcymmhRvc7chCSQFA
/home/martin/workspace/prototypes/boot-to-claude/kernel/serial.c
41 lines
#include "serial.h"

#include <stdint.h>

#define COM1 0x3f8

static inline void outb(uint16_t port, uint8_t val) {
    __asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
}

static inline uint8_t inb(uint16_t port) {
    uint8_t ret;
    __asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
    return ret;
}

void serial_init(void) {
    outb(COM1 + 1, 0x00); /* disable interrupts */
    outb(COM1 + 3, 0x80); /* enable DLAB */
    outb(COM1 + 0, 0x03); /* divisor low byte: 38400 baud */
    outb(COM1 + 1, 0x00); /* divisor high byte */
    outb(COM1 + 3, 0x03); /* 8 bits, no parity, one stop bit */
    outb(COM1 + 2, 0xC7); /* enable FIFO, clear, 14-byte threshold */
    outb(COM1 + 4, 0x0B); /* IRQs disabled, RTS/DSR set */
}

static int transmit_empty(void) {
    return inb(COM1 + 5) & 0x20;
}

void serial_putc(char c) {
    while (!transmit_empty()) {}
    outb(COM1, (uint8_t)c);
}

void serial_write(const char *s) {
    while (*s) {
        serial_putc(*s++);
    }
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/serial.c (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:06:44 · toolu_01Cn1dakfEj6dFkGnwszEJ9K
/home/martin/workspace/prototypes/boot-to-claude/kernel/kmain.c
#include "console.h"
#include "serial.h"

void kmain(void) {
    serial_init();

    console_clear();
    console_write("tiny-os: kernel entered\n");
    serial_write("tiny-os: kernel entered\n");

    for (;;) {
        __asm__ volatile ("hlt");
    }
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/kmain.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:06:48
Now updating the Makefile to build and link the kernel and assemble the full disk image.
Write · Jul 17, 19:06:55 · toolu_01TorXxo9HTBp7AiwFveDcWC
/home/martin/workspace/prototypes/boot-to-claude/Makefile
57 lines
AS      := nasm
CC      := gcc
LD      := ld
OBJCOPY := objcopy
QEMU    := qemu-system-i386
BUILD   := build

CFLAGS  := -m32 -ffreestanding -fno-pic -fno-stack-protector -nostdlib \
           -Wall -Wextra -O2 -std=c11 -Ikernel
LDFLAGS := -m elf_i386 -T kernel/link.ld -nostdlib

BOOT_BIN   := $(BUILD)/boot.bin
STAGE2_BIN := $(BUILD)/stage2.bin
DISK_IMG   := $(BUILD)/disk.img

KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c
KERNEL_OBJS   := $(BUILD)/entry.o $(patsubst kernel/%.c,$(BUILD)/%.o,$(KERNEL_C_SRCS))
KERNEL_ELF    := $(BUILD)/kernel.elf
KERNEL_BIN    := $(BUILD)/kernel.bin

.PHONY: all run clean

all: $(DISK_IMG)

$(BUILD):
	mkdir -p $(BUILD)

$(BOOT_BIN): boot/boot.S | $(BUILD)
	$(AS) -f bin -o $@ $<

$(STAGE2_BIN): boot/stage2.S | $(BUILD)
	$(AS) -f bin -o $@ $<

$(BUILD)/entry.o: kernel/entry.S | $(BUILD)
	$(AS) -f elf32 -o $@ $<

$(BUILD)/%.o: kernel/%.c | $(BUILD)
	$(CC) $(CFLAGS) -c -o $@ $<

$(KERNEL_ELF): $(KERNEL_OBJS) kernel/link.ld
	$(LD) $(LDFLAGS) -o $@ $(KERNEL_OBJS)

$(KERNEL_BIN): $(KERNEL_ELF)
	$(OBJCOPY) -O binary $< $@

$(DISK_IMG): $(BOOT_BIN) $(STAGE2_BIN) $(KERNEL_BIN)
	dd if=/dev/zero of=$@ bs=512 count=2880 status=none
	dd if=$(BOOT_BIN) of=$@ conv=notrunc status=none
	dd if=$(STAGE2_BIN) of=$@ bs=512 seek=1 conv=notrunc status=none
	dd if=$(KERNEL_BIN) of=$@ bs=512 seek=5 conv=notrunc status=none

run: $(DISK_IMG)
	$(QEMU) -drive file=$(DISK_IMG),format=raw -display none -serial stdio -no-reboot

clean:
	rm -rf $(BUILD)
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
Bash · Jul 17, 19:06:57 · toolu_01Uane6zT8h32juA3RjKnXYx
Build full bootloader + kernel image
$ make clean >/dev/null && make 2>&1
result
mkdir -p build
nasm -f bin -o build/boot.bin boot/boot.S
nasm -f bin -o build/stage2.bin boot/stage2.S
nasm -f elf32 -o build/entry.o kernel/entry.S
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/kmain.o kernel/kmain.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/console.o kernel/console.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/serial.o kernel/serial.c
ld -m elf_i386 -T kernel/link.ld -nostdlib -o build/kernel.elf build/entry.o build/kmain.o build/console.o build/serial.o
ld: warning: build/kernel.elf has a LOAD segment with RWX permissions
objcopy -O binary build/kernel.elf build/kernel.bin
dd if=/dev/zero of=build/disk.img bs=512 count=2880 status=none
dd if=build/boot.bin of=build/disk.img conv=notrunc status=none
dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none
dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none
Bash · Jul 17, 19:07:03 · toolu_01YLbBsFhD6dcCeEWir9xdS2
Check kernel binary size against the 64-sector budget
$ ls -la build/kernel.bin build/stage2.bin build/boot.bin
echo "kernel size vs budget (32768):"
stat -c%s build/kernel.bin
result
-rw-r----- 1 martin martin  512 Jul 17 19:06 build/boot.bin
-rwxr-x--- 1 martin martin  904 Jul 17 19:06 build/kernel.bin
-rw-r----- 1 martin martin 2048 Jul 17 19:06 build/stage2.bin
kernel size vs budget (32768):
904
agent · Jul 17, 19:07:05
Well within budget. Now boot it and verify.
Bash · Jul 17, 19:07:06 · toolu_019Z8qzGQH8kTLZBb1GBEnVW
Boot the full stage1+stage2+kernel image headlessly and capture serial output
$ timeout 5 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial stdio -no-reboot; echo "exit=$?"
result
tiny-os: kernel entered
qemu-system-i386: terminating on signal 15 from pid 1864212 (timeout)
exit=124
agent · Jul 17, 19:07:18
Full boot chain verified — stage1 → stage2 → protected-mode kernel → `kmain` printed to serial, proving A20/GDT/pmode switch all worked. Committing.
Bash · Jul 17, 19:07:18 · toolu_01CsLUoJzQzenfq6w6wCgA3b
Stage Phase 1 files
$ git add -A && git status --short
result
M  Makefile
M  boot/boot.S
A  boot/stage2.S
A  kernel/console.c
A  kernel/console.h
A  kernel/entry.S
A  kernel/kmain.c
A  kernel/link.ld
A  kernel/serial.c
A  kernel/serial.h