$ 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 )"
[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
{
"taskId": "1",
"status": "completed"
}Updated task #1 status
{
"taskId": "2",
"status": "in_progress"
}Updated task #2 status
$ which objcopy && objcopy --version | head -1
/bin/objcopy GNU objcopy (GNU Binutils for Ubuntu) 2.45
; 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
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)
; 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
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)
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/kernel
(Bash completed with no output)
; 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:
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)
ENTRY(_start)
SECTIONS
{
. = 0x10000; /* matches KERNEL_LOAD_ADDR in boot/stage2.S */
.text : {
*(.text.entry)
*(.text*)
}
.rodata : {
*(.rodata*)
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
*(COMMON)
}
}
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)
#pragma once void console_clear(void); void console_putc(char c); void console_write(const char *s);
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)
#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++);
}
}
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)
#pragma once void serial_init(void); void serial_putc(char c); void serial_write(const char *s);
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)
#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++);
}
}
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)
#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");
}
}
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)
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)
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)
$ make clean >/dev/null && make 2>&1
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
$ ls -la build/kernel.bin build/stage2.bin build/boot.bin echo "kernel size vs budget (32768):" stat -c%s build/kernel.bin
-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
$ timeout 5 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial stdio -no-reboot; echo "exit=$?"
tiny-os: kernel entered qemu-system-i386: terminating on signal 15 from pid 1864212 (timeout) exit=124
$ git add -A && git status --short
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