Changelog
All notable changes to BBoeOS are documented in this file. Dates reflect when changes landed, grouped under the version that was (or will be) current at the time.
Unreleased
0.9.0 (2026-05-01)
Paging and memory protection
- Per-program page directories: every program runs with its own page table tree. Programs cannot read or write other programs’ memory or the kernel’s, and a NULL dereference now faults instead of silently reading the shell-program handoff frame.
- Userland runs at ring 3. Privileged instructions (
cli,sti,in,out, control-register writes) trap from user code. - User virtual address space expands to ~3.99 GB. Programs load at the Linux-style
PROGRAM_BASE = 0x08048000; the user stack sits flush against the user/kernel boundary at0xFF800000. - Stack-overflow guard: a 16-page user stack with an unmapped guard region below it. Recursive runaway faults cleanly and respawns the shell.
- Graceful out-of-memory during program load. A program with a giant BSS allocation no longer takes the kernel down — the partial address space is freed, the shell prints
exec: out of memory, and the prompt comes back.
Boot, RAM, and address-space layout
- Boots in 1 MB of RAM (
qemu-system-i386 -m 1). The kernel relocates to physical0x20000, fits below the VGA aperture, and reclaims the conventional low-memory regions it used to pin. - Kernel can reach any 32-bit physical frame: a 4 MB direct map plus a kmap window of demand-mapped slots above it, so the bitmap allocator hands out frames from anywhere in installed RAM.
- Frame allocator’s bitmap sizes itself from the BIOS E820 memory map — RAM cost scales with what’s actually installed.
- Filesystem and NIC scratch buffers move from fixed physical reservations into bitmap-allocated frames. Sessions without a NIC don’t pay for NIC buffers.
- Programs stream from disk directly into their own user frames; the 32 KB kernel-side staging buffer is gone.
Filesystem
- ext2: directory walker fixed for entries that straddle a block-internal sector boundary (visible at 2 KB block sizes).
- ext2: full 32-bit
i_sizehonoured on read (large files no longer truncate at 64 KB).
Userspace programs
edituses ordinary BSS for its 448 KB gap buffer and kill buffer instead of hardcoded scratch addresses.- Self-hosted
asmkeeps its symbol and jump tables in its own BSS instead of a shared 1 MB scratch region. - Shell’s Ctrl-K kill buffer moves into BSS (was reading freed memory once the per-program page-table identity map went away).
Drivers and kernel ports to C
- Drivers ported from assembly to C: ATA, FDC, NE2000, PS/2, RTC, VGA, ANSI console, serial.
- File-descriptor table, console / network / file backends ported to C.
INT 30hdispatcher consolidated; the four non-trivial network handlers ported to C.- Reboot and shutdown helpers ported to C.
Toolchain
cc.pygains 32-bit struct-field accesses, struct-member access on indexed arrays, width-correctout_registercapture, and kernel-only port-I/O builtins — enough to support the kernel C ports above.add_file.pygains a batched API;make_os.shand the test suite collapse per-file image flushes into a handful of batched invocations.
0.8.1 (2026-04-28)
-
Bugfix: floppy boot (
qemu-system-i386 -drive file=drive.img,format=raw,if=floppy) works again after being silently broken for some time.protected_mode_entrywas running the driver init chain before unmasking IRQ 0 (PIT) and issuingsti, sovfs_init→fdc_motor_start→rtc_sleep_mswould busy-wait forever onsystem_ticks. The fix moves the IRQ 0 unmask +stiahead of the driver inits. A regression test (tests/test_floppy_boot.py) now exercises the floppy path on every CI run so this can’t slip again. -
Rename the MBR-offset-508 size field from
stage2_bytes→kernel_bytes(andSTAGE2_BYTES_OFFSET→KERNEL_BYTES_OFFSETinadd_file.py). Thestage2_*name was a fossil from the pre-merge stage1/stage2/kernel split that no longer exists; the post-MBR region is just “the kernel”. Drop adjacent stale prose fromCLAUDE.md,README.md,cc/target.py, andbboeos.asm(vDSO base address comment was still showing the pre-relocation0x08046000).
Paging prep (2026-04-28)
- Move the FUNCTION_TABLE and
shared_*helpers (lib/print.asm,lib/proc.asm) into a separately-assembled vDSO blob (src/vdso/vdso.asm) loaded at virtual0x00010000. The kernel embedsvdso.binviaincbinand copies it to physicalFUNCTION_TABLEonce at boot viavdso_install. Helper bodies relocate theirSECTOR_BUFFERand internal-static references to per-AS data at0x00011000. User programs callFUNCTION_DIE/FUNCTION_PRINT_STRING/ etc. exactly as before — only the addresses change. Decouples user-side helper code from kernel-virt addressing ahead of paging. - Probe the BIOS memory map via
INT 15h AX=E820in the MBR and stash 24-byte entries at physical 0x500 (terminated by a zero entry). Result is unconsumed at this point — the post-paging bitmap frame allocator will use it to mark free vs reserved physical RAM. - Widen the BSS trailer from 16 to 32 bits. Programs now declare BSS via the new 6-byte trailer (
dd bss_size; dw 0xB032); the kernel loader still accepts the legacy 4-byte form (dw bss_size; dw 0xB055) for back-compat. Lifts the per-program BSS cap from 64 KB to 4 GB ahead of paging, whereedit’s 1 MB gap buffer becomes ordinary BSS.
Kernel
- Move userland programs to ring 3. Add user code (0x18) and user data (0x20) GDT descriptors, a 32-bit TSS at selector 0x28 with SS0/ESP0 pointing at the kernel stack, and raise the
INT 30hgate to DPL=3.program_enternow reloads DS/ES/FS/GS to the user data selector andiretds into a fresh ring-3 stack atUSER_STACK_TOP(0x8FFF0) instead ofjmp PROGRAM_BASE. IRQ handlers, exception stubs, the syscall dispatcher,sys_exit, andsys_execare all already cross-priv correct (the CPU auto-switches to TSS.ESP0 on ring-3 → 0 andiretdpops the right number of dwords on the way back). Privileged instructions (cli/sti/in/out/CR writes) now #GP from user code.
Drivers
- Port
ps2_init/ps2_getc/ps2_handle_scancode/ps2_putcfromdrivers/ps2.asmto C in a newsrc/drivers/ps2.c, retiring the asm file. The IRQ-driven Set-1 scan-code translator (16-byte ring buffer, modifier state, ANSI CSI sequences for arrow keys) becomes ~140 lines of C againstkernel_inb/kernel_outb; the IRQ stub (ps2_irq1_handler, mustiretd) and the install wrapper (ps2_install_irq, needsaddress-of-labelforidt_set_gate32) sit in a file-scopeasm("...")block at the bottom because cc.py can’t express either pattern in C. The two 59-byte keymap arrays carry the same byte sequences as the asm version.PMODE_PIC1_CMD/PMODE_PIC1_DATA/PMODE_PIC_EOIequdefinitions move fromps2.asmtoentry.asm(the only remaining consumer).
Programs
- Close the self-hosted
asmassembler’s 32-bit codegen gaps so its output is byte-identical to NASM under[bits 32], and movetests/test_asm.pyfrom its--bits 16pin to--bits 32(matchingmake_os.sh’s production path). Six fixes insrc/c/asm.c: 32-bit displacement (rel32) forcall,jmp, and conditional-jump near forms (plus the convergence loop’s short-vs-long sizing math); 3-character e-prefixed register names likeesi/ediparse at the trailing[disp+reg]position;handle_movzxhandles the direct-memory operand (type2 == 2); operand-size-prefix emission forunary_f6f7(mul/div/neg/not),adc_sbb_handler, and the 16-bit string ops (lodsw/movsw/stosw); andemit_alu_mem_imm(add/and/or/sub/xor [mem], imm) gainsdwordsupport and bits-aware encoding viaemit_modrm_direct. All 35 self-host programs assemble byte-identical to NASM at--bits 32.
Toolchain
cc.pynow defaults to--bits 32. The protected-mode merge made 32-bit the only production target (kernel + user programs both pass--bits 32explicitly inmake_os.sh); the 16-bit default was a holdover.--bits 16stays a working option (tests/test_cc_bits.pyexercises both modes for cc.py front-end coverage), but production user programs and the self-host assembler regression both run at--bits 32.make_os.shnow passes--bits 32tocc.py --target kernelso kernel C ports emit 32-bit prologues / register names matching the protected-mode runtime. The pre-existing kernel C files (fs/fd.c,net/ip.c,net/icmp.c) are pureasm("...")blocks with no compiler-emitted code, so the flag is a no-op for them; first real C port (drivers/ps2.c) needed it.
Tests
- Add
archive/kernel/(size-tracking tree mirroringarchive/for kernel-side ports, with a separateREADME.mdand the same column layout) plustests/test_kernel_archive.py(verifies snapshot files exist for every README row and the delta math is internally consistent).tests/measure_kernel_port.sh <path>runsos.binbuilds with the C port and the archived asm in turn, prints the byte sizes and delta — the workflow each port commit uses to fill in its README row.
0.8.0 (2026-04-27)
Boot
- Port the kernel to flat 32-bit ring-0 protected mode. The MBR keeps its real-mode preamble through the disk read, BIOS font load, PIC remap, A20, and 32-bit GDT load, then far-jumps into
protected_mode_entryand stays in protected mode. The previousstage1.asm/stage1_5.asm/stage2.asm/kernel.asmsplit collapses into one flat-binarysrc/arch/x86/boot/bboeos.asmwhose [bits 16] MBR fronts a [bits 32] stage 2. CPU exceptions vector throughidt.asm’sexc_commonand printEXCnnon COM1. - Wire IRQ 0 (PIT @ 100 Hz) and IRQ 6 (FDC) handlers into the protected-mode IDT in
entry.asm; the BIOS IVT-based handlers retire. program_enterresets fds, zeros the program’s BSS region, snapshots ESP, andjmp PROGRAM_BASE.sys_exitfrom any program restores ESP and re-entersshell_reload.
Drivers
- Port ATA, FDC, NE2000, PS/2, RTC, and VGA to flat 32-bit addressing. Segment register loads removed; framebuffer / device-buffer writes use linear addresses (
0xB8000for VGA text,0xA0000for mode 13h). - Restore the boot-time
vga_font_loadthat copies the BIOS ROM 8x16 font into char-gen plane 2 offset 0x4000 — required forvga_set_mode(VIDEO_MODE_TEXT_80x25)to render glyphs (mode 03h’s SR03=0x05 selects that slot). Lives insrc/arch/x86/boot/vga_font.asm, included in the MBR’s [bits 16] region so it runs whileINT 10his still mapped. - Drop the dead
rtc_tick_init/rtc_tick_irq0fromdrivers/rtc.asm— the IVT-based PIT handler was orphaned by the protected-mode port (protected_mode_entrydoes the equivalent setup against the protected-mode IDT). - Extract the COM1 driver from
ansi.asmandentry.asmintodrivers/serial.asm; renamedrivers/ansi.asmtodrivers/console.asm.
Filesystem
- Port block I/O, VFS, bbfs, and ext2 to flat 32-bit addressing.
Syscalls
- 32-bit
INT 30hdispatcher. Handlers receive args in E-regs with the same semantic shape as the 16-bit ABI (BX=fd, ESI/EDI=buffer, ECX=count, AL=flags). Saved-EFLAGS CF propagates to the user via the iretd frame. - Widen
io_read/io_writeto return full 32-bit byte counts so e.g.editcan read its 1 MB gap buffer in one call.
Programs
- Port the self-hosted
asmassembler to protected mode. The symbol and jump tables move from a dedicated ES segment to flat extended memory atSYMBOL_BASE = 0x300000; flat 32-bit DS reaches everywhere so far-memory accessors no longer need a segment override. Symbol values widen to 4 bytes forJUMP_TABLE = 0x30F000to round-trip cleanly. All 35 self-host programs assemble byte-identical to NASM. - Port
editto protected mode. The 1 MB gap buffer and 2.5 KB kill buffer move from segment 0 to extended memory above the 1 MB mark (EDIT_BUFFER_BASE= 0x100000,EDIT_KILL_BUFFER= 0x200000).archive/edit.asmretired — the 16-bit C build can’t represent a 256 KB buffer base. - Port
drawandvga_fill_blockto protected mode. Drop the ES reload (real-mode segment 0xA000 #GPs in protected mode), widen DI to EDI, fold the framebuffer base 0xA0000 into the offset.
Toolchain
- cc.py gains a
--bits 32target backing the protected-mode port, along with a tide of orthogonal improvements: kernel-only port-I/O builtins (kernel_inb/kernel_outb/kernel_inw/kernel_outw/kernel_insw/kernel_outsw),__attribute__((naked))with if/else tail-call dispatch, double-pointer types, unsigned conditional jumps where comparisons have unsigned operands,&array[i]parse desugaring,MemberIndexAST node forptr->array[i]reads,far_read32/far_write32for the wider symbol-table accessors, and four new peephole passes (peephole_dead_temp_slots,peephole_register_arithmetic,peephole_self_move,peephole_redundant_register_swap) that collectively shrink the user-program corpus by ~80 bytes per program. - Fix
returnfrommainwhenmainhas a local stack array — now always emitsjmp FUNCTION_EXITregardless ofelide_frame.
Tests
- Add
tests/test_draw.pycovering draw + post-exit font restoration. - Restore the full CI matrix (
test_archive,test_asm,test_bboefs,test_cc,test_draw,test_ext2,test_programs).
0.7.0 (2026-04-23)
Boot
- Shrink stage 1 MBR to the minimum required to load stage 2 and jump: set DS/ES/SS:SP, reset disk,
INT 13hread, jump toboot_shell.clear_screen,WELCOME/DISK_FAILUREstrings, theput_stringcall, the dead geometry-query variables (sectors_per_track/heads_per_cylinder, written but never read), and thepic_remap/rtc_tick_init/install_syscalls/network_initializecalls all move into stage 2’sboot_shellwhere the full console driver (drivers/ansi.asm) is available. On disk error stage 1 now prints!viaINT 10h AH=0Ehand halts instead of pulling in a string printer. - Drop
src/arch/x86/boot/ansi_minimal.asmentirely. Itsput_stringandserial_charactermove intodrivers/ansi.asmalongsideput_character(their natural home —serial_characteris the COM1 write primitive thatput_characteralready called;put_stringis a thin wrapper aroundput_character).put_character_rawis removed — it only existed becauseput_stringpredated the full ANSI parser and needed an escape-free output routine; the newput_stringcallsput_characterdirectly, which handles\n → \r\nthe same way. The file name was misleading anyway (“minimal” suggested a stage-1-only helper, but drivers/vga.asm and drivers/ansi.asm were callingserial_characterout of it). sys_exitno longer re-prints the welcome banner on every shell reload. Splitboot_shellsokernel_init,WELCOME, and the one-time driver inits (vga_font_load,ps2_init,fdc_init,vfs_init) stay in the boot path, and a newshell_reloadentry handles justfd_initplus the shell VFS load.sys_exitnowjmp shell_reloadinstead ofjmp boot_shell.
Tree layout
- Reorganize
src/kernel/into Linux-style subtrees. Only genuinely x86/PC-specific code lives undersrc/arch/x86/:boot/(bootloader:bboeos.asm,stage1.asm,stage1_5.asm— the protected mode switch, néeprotected mode.asm;stage2.asm),idt.asm,pic.asm,syscall.asm(INT 30hdispatcher),system.asm(8042 reboot + ACPI shutdown), and a newkernel.asmaggregator. Hardware drivers lift tosrc/drivers/(ata.asm,fdc.asm,ps2.asm,rtc.asm,vga.asm, plus the NE2000 NIC moved out ofnet/, andansi.asmas the console driver delegating to vga). Filesystem code consolidates undersrc/fs/(bbfs.asm,ext2.asm,fd.asm+fd/,block.asmblock dispatcher,vfs.asm). Network stack insrc/net/keeps the protocol layer only (arp.asm,icmp.asm,ip.asm,udp.asm). Shared utilities insrc/lib/, syscall handlers insrc/syscall/.make_os.shadds-i src/so%include "drivers/ata.asm"/"fs/fd.asm"/"net/net.asm"/ … resolve at the top level.src/arch/x86/boot/stage2.asmno longer%includes the kernel itself — it contains only the boot handoff (jump table,boot_shell,bss_setup).bboeos.asmnow composes the flat binary asstage1 + stage2 + kernel.asm, wherekernel.asmis the new aggregator that lists every subsystem in one place. Motivation: the protected mode port is about to land on a dedicatedprotectedmodebranch cut frommain; the subtree is its natural home, and the boot / kernel split keepsstage2.asmfocused on the boot-to-shell handoff instead of doubling as a kernel catalog.tests/test_pmode.shandtests/test_idt.shboth run green again (they had broken on the earlierarch/sub-move)
Kernel
- New
pic.asm/pic_remap: reprograms both 8259s so master IRQ 0-7 vector to 0x20-0x27 and slave IRQ 8-15 to 0x28-0x2F, leaving every line masked. Called fromstage1.asmright beforertc_tick_init, i.e. after the last BIOSINT 13hread but before any IRQ handler installs.rtc_tick_initmoves its IVT slot from 84 to 0x204 and now unmasks IRQ 0 at the master PIC itself (pic_remap leaves it masked);fdc_install_irqmoves from 0Eh4 to 26h4. Prerequisite for the upcoming protected mode flip — CPU exceptions 0-31 overlap the legacy BIOS PIC vectors, so IRQ 0 under BIOS defaults would alias onto the double-fault vector and IRQ 5 onto #GP rtc_tick_initreprograms the PIT from the BIOS default ~18.2 Hz to 100 Hz (10 ms/tick), givingrtc_sleep_ms10 ms granularity (was 55 ms) anduptimesub-second precision underneath theHH:MM:SSdisplay.TICKS_PER_SECONDbecomes 100;rtc_sleep_msrounds to whole 10 ms ticksfd_read_console:stiat the top of the idle polling loop so PIT IRQ 0 can advancesystem_tickswhile the shell is waiting for input. Prior behaviour held IF=0 for the entire wait (syscalls enter with IF=0 and nothing re-enabled it), which silently starved the tick counter and keptuptimepinned at00:00:00- New
SYS_RTC_MILLIS(31h) returnsDX:AX= milliseconds since boot, derived fromsystem_ticks × MS_PER_TICKso the ms count is exact. ExistingSYS_RTC_SLEEP/SYS_RTC_UPTIMEshift up to 32h / 33h to keep the group alphabetical. cc.py’sticks()builtin (which emittedint 1Ah, dead sincertc_tick_initreplaced the BIOS IRQ 0 handler) is replaced byuptime_ms()— full 32-bitDX:AXreturn when the caller assigns tounsigned long, low 16 bits when assigned toint.pingprintstime=N msaccordingly - Extract
kernel_initout ofboot_shellinto a newsrc/arch/x86/init.asm: single-entry routine runningpic_remap/rtc_tick_init/install_syscalls/network_initialize. Motivation is protected mode prep — once the flip lands,rtc_tick_init/install_syscallsbecome IDT-dependent and either move post-flip or gain 32-bit variants; encapsulating the sequence means that refactor editsinit.asm, notstage2.asm. - Rename
src/arch/x86/protected mode.asm→src/arch/x86/boot/stage1_5.asmand colocate it underboot/. The file is already the stage-1.5 of the boot flow (16→32-bit mode switch between the MBR and the protected mode kernel), so give it the positional name.tests/pmode_test.asmandtests/idt_test.asm%includepaths and their shell wrappers’nasm -isearch paths follow.
Drivers
- New native VGA mode-set driver (
vga_set_mode) replaces the lastINT 10hin stage 2 (the formerSYS_VIDEO_MODE). Table-driven register writer covering modes 03h (80x25 text) and 13h (320x200 256-colour): programs Misc Output, Sequencer 1-4, CRTC 0-18h, GC 0-8, and AC 0-14h in the standard unlock / reset / re-enable sequence. Newvga_fill_blockwrites an 8x8 tile into the mode-13h framebuffer at A000h:0 at a grid position with a palette-index colour.draw.crewritten to use mode 13h with real pixel tiles: 40x25 grid, WASD navigation, J/K palette cycle across 16 standard VGA colours, Q to quit back to text mode. - Fix VGA cursor column always zero in
vga_set_cursor/vga_teletype/vga_write_attribute.mul bxclobbered DX beforemovzx bx, dlcould read the column, socolwas always 0 and every glyph wrote to column 0 of its row. Switched toimul ax, ax, VGA_COLS(186+ three-operand form), which leaves DX intact.
Filesystem
- ext2 correctness sweep targeted at
e2fsckcleanliness.ext2_alloc_block/ext2_free_blockapply thes_first_data_blockoffset to the block-index ↔ bitmap-bit mapping. Six new BGD/superblock counter helpers (ext2_bgd_{block,inode,dir}_{alloc,free}) called from every alloc/free path keepbg_free_blocks_count/bg_free_inodes_count/bg_used_dirs_count/ superblock free counts in sync with the bitmaps.ext2_add_dir_entryrecords the filetype byte (1 = regular, 2 = directory;ext2_renamecarries it over from the old inode’si_mode).ext2_delete/ext2_rmdirzeroi_links_countalongsidei_dtimeso fsck no longer treats deleted inodes as in-use.ext2_update_sizeupdatesi_blocks = keep_blocks * sectors_per_block (+ indirect pointer block)on shrink before flushing the inode.test_ext2.pyruns e2fsck after each test; all write-path tests (1 KB and 2 KB block sizes) pass with clean fsck. - ext2 doubly-indirect block support across read, write, and shrink. Read path was already in place; the write path (
ext2_prepare_write_sec.epws_alloc_doubly) allocates and zero-fills the top pointer block ati_block[13], the sub-singly pointer block atouter_idx, and the data block atinner_idxwhenblock_idx >= 12 + ptrs_per_blk, each time updatingi_blocks(top + sub-singly blocks count assectors_per_blockeach, matching e2fsck). The shrink path inext2_update_sizeextends its saved block array to 14 entries, implements a partial-sub-singly inner loop for the fractional first entry, and guards the top-block free behinddbl_keep == 0; fixes four earlier bugs that savedptrs_per_blkinstead of the doubly-indirect block number, fell through into.eus_growwith stale inode data, orphaned all doubly-indirect blocks on partial-doubly shrinks, and miscountedi_blocks. Newext2_free_ind_blockhelper replaces the inline indirect loops inext2_delete/ext2_rmdir/ext2_update_size— uses index-based re-reads to avoidSECTOR_BUFFERclobbering. Max file size becomes 268 KB for 1 KB blocks, 1028 KB for 2 KB blocks. - ext2
i_sizenow stored as a full 32-bit value invfs_found_size.ext2_findpreviously took only the low 16 bits (hardcoding the high word to 0), so a 280 KB file withi_size = 0x46000read back as0x6000(24 KB) andfd_read_filehit EOF after the first 24 KB. ext2_add_dir_entry/ext2_remove_dir_entryscan all sectors of a directory block (the lookup path inext2_search_blkalready did). Previously entries at block offsets ≥ 512 were skipped on writes, creating a read/write mismatch in directories that spanned past 512 bytes. The “last entry in block” test now compares the absolute block offset againstblock_sizeinstead of the sector-relative offset.- ext2 frees orphaned blocks when a file is overwritten shorter (e.g.
editsave-over orcpover a larger file).ext2_update_size’s shrink path computeskeep_blocks = ceil(new_pos / block_size), zeroes the freedi_block[]entries, flushes the inode, then frees direct blocks[keep_blocks..11]and the singly-indirect block and its entries (partial or full). - ext2 records timestamps on create / write / chmod via two helpers:
ext2_set_timestamps_now(atime = mtime = ctime = now; called fromext2_create/ext2_mkdir) andext2_set_mtime_ctime_now(mtime = ctime = now; called fromext2_update_sizeon both grow and shrink, andext2_chmod). atime is not updated on reads (relatime). - ext2 cross-parent directory rename. When
mvrelocates a directory to a different parent,ext2_renameupdates the..entry in the moved directory’s data block (offset 12, block 0, sector 0), decrements the old parent’si_links_count, and increments the new parent’s. File renames were already correct; the new logic is guarded byfiletype == 2 && old_dir != new_dir. ext2_mkdirsupports nested subdirectories viaext2_resolve_path: resolves the path to a(parent_inode, basename)pair before allocating the inode / block, so the..entry andext2_add_dir_entrycall both use the resolved parent inode rather thanEXT2_ROOT_INODE. ext2-only; bbfs retains its single-level limit.- ext2 gains variable block size (1 KB / 2 KB), chmod, and subdirectory creation.
- New tests:
doubly_indirect_cat/doubly_indirect_cp_shrinkinject a 280 KB file at test build time and exercise the doubly-indirect read, write, and shrink paths.BLOCK_SIZE_TESTSexpanded from 23 to 33 entries so every write-path and directory-op test (cat_large,chmod,cp_overwrite_shrink,mkdir,mkdir_ls_root,rename,rename_dir,rm,rmdir,rmdir_nonempty) runs at both 1 KB and 2 KB block sizes. - Rename
fs/fs.asm→fs/block.asm. The file is a 14-line block-device dispatcher that routesread_sector/write_sectorto fdc or ata based onboot_disk; the old name was neither a filesystem nor thefs/orchestrator, so it’s now named for its actual role.
Syscalls
SYS_IO_IOCTL(15h): device-control dispatch keyed on fd type./dev/vga(newFD_TYPE_VGA) is a synthetic device —open("/dev/vga", O_WRONLY)allocates an fd of that type without touching the filesystem, andfd_ioctlroutes throughfd_ioctl_opsto per-type handlers. The VGA handler rejects fds that weren’t opened writable and supports three cmds:VGA_IOCTL_MODE(DL=mode, also clears screen+serial),VGA_IOCTL_FILL_BLOCK(CL=col, CH=row, DL=color),VGA_IOCTL_SET_PALETTE(CL=index, CH=r, DL=g, DH=b). The palette write lives in a new kernelvga_set_palette_colordriver function instead of cc.py inliningout dx, alin every caller.- Retire
SYS_VIDEO_MODE(40h) and theFUNCTION_VGA_FILL_BLOCKjump-table slot:video_mode/fill_block/set_palette_colorcc.py builtins now take an fd as the first argument and emit a singleint 30hto SYS_IO_IOCTL.src/c/shell.c,edit.c, anddraw.ceach open/dev/vgaonce inmain()and pass the fd through. SYS_FS_UNLINK(04h): new syscall for deleting a file.vfs_deletedispatches tobbfs_delete(zeroes the 32-byte directory entry, freeing the slot for reuse) orext2_delete(frees direct and singly-indirect data blocks viaext2_free_block, frees the inode viaext2_free_inode, removes the directory entry). Newext2_free_bit/ext2_free_block/ext2_free_inodehelpers (inverses ofext2_alloc_bit). The shell binary is protected from deletion. cc.py gains anunlink()builtin;src/c/rm.cadded.SYS_FS_RMDIR(03h): new syscall for removing an empty directory.vfs_rmdirdispatches tobbfs_rmdir(finds the directory entry, verifiesFLAG_DIRECTORY, scans allDIRECTORY_SECTORSof the subdirectory’s data for occupied entries, then zeroes the parent directory entry) orext2_rmdir(resolves the path, verifiesEXT2_S_IFDIR, scans direct blocks via newext2_check_dir_emptyhelper — skipping.and..— then frees direct+indirect blocks, frees the inode, removes the directory entry). NewERROR_NOT_EMPTY(06h) returned when the directory is non-empty. cc.py gains anrmdir()builtin;src/c/rmdir.cadded.DIRECTORY_SECTORbumps 26 → 28 → 30 → 31 across this release to fit the expanding kernel.
Userspace programs
- New
rmandrmdirC programs built onSYS_FS_UNLINK/SYS_FS_RMDIR. arp/cat/dns/edit/ls/netinit/netrecv/netsend/pingnow allocate their own BSS instead of reaching into kernel-shared buffers.
Tooling
- cc.py: extend compound-assignment lexer to cover
-=,*=,/=so the arithmetic family matches the bitwise/shift family (+=,&=,|=,^=,<<=,>>=). Normalize everyvar = var op rhs;site acrosssrc/c/*.cto the compound form. Two multi-termx = x + a + bsites indns.c/ping.cstay as-is because the left-associative chain emits a tighter sequence thanx += a + b(which parenthesizes the RHS and needs a scratch register) - cc.py: add
%=and fix a latent codegen bug it exposed —peephole_dx_to_memoryfolds themov ax, dx / mov [mem], axpair that a%expression emits into a directmov [mem], dx, leaving AX holding the pre-fold value (the quotient from the precedingdiv). Separately,peephole_store_reloadwas deleting the defensive reloademit_store_localemits, trusting the trackedax_local == nameinvariant that the dx-to-memory fold silently violated. Fix: teach_peephole_will_strand_axto recognize themov ax, dx / mov [mem], axshape soax_localgets cleared at store time, and reorder the pipeline sodx_to_memoryruns beforestore_reload.bits.cpicks up ay %= 13smoke test, andtest_programs.py’sbitsregex matches that output so a regression is caught at the runtime layer too - cc.py: extract the peephole pass into a standalone
Peepholerclass (néePeepholeMixin). The pass only readsself.linesandself.targetand shares no per-statement state, so the mixin was obscuring rather than expressing that boundary. Call site becomesself.lines = Peepholer(lines=..., target=...).run()at emission.py:115. Methods sorted to the canonical layout used bycc/codegen/base.py(dunder → underscore helpers → public). Byte-identical output on all 35 self-hosting tests. - cc.py: parse
constants.asmat compile time viaparse_asm_constants()and thread the resolved dict throughcli.py→X86CodeGenerator→CodeGenerator, replacing the stale hardcodedNAMED_CONSTANT_VALUESclass variable (which hadDIRECTORY_NAME_LENGTH=27instead of the correct 25). - cc.py: exclude BP from the pin pool when
mainhas stack arrays, avoiding the register allocator claiming a register the frame-array code needs. - cc.py: guard SI and invalidate
ax_localaround constant-base indexing. Two related codegen bugs bit functions that used anasm_register("si")-pinned global (source_cursorinasm.c) alongside a constant-base (_g_foo[...]) array index on a non-constant index: (1)_emit_constant_base_index_addrclobbered SI without the_si_scratch_guard_begin/_si_scratch_guard_endpair the variable-index path emits, leaving the pinnedsource_cursorpointing at array-internal garbage; (2)emit_comparisonwith a pinned-register left operand against a memory-backed right operand setax_local = left.nameaftermov ax, reg / cmp ax, [mem], butpeephole_compare_through_registerthen rewrote the pair ascmp reg, [mem], leavingax_localclaiming AX held the pinned value even though the load was gone.ax_clear()after the cmp forces the reload. Asm and shell pick up a small size bump where they’d been relying on the stale AX value. - cc.py: factor
emit_register_from_argument,emit_store_local’s pinned-destination fast path, andemit_si_from_argumentonto a shared_try_direct_load(*, argument, register, optimize_zero)helper covering integer literals, string literals, named constants, constant aliases, global arrays, local stack arrays, and constant-folded expressions. Each caller retains only its truly-special branches (width-aware pinned / aliased-global loads andax_localshortcut; generic expression fallback). Array Vars dispatch through_try_direct_loadbefore_is_memory_scalarso the base address is loaded (vialea/mov _l_name) instead of the contents. - Self-hosted assembler (
src/c/asm.c): factor the<op> byte|word [disp16], immparsing and emission into a sharedemit_alu_mem_imm(rfield)helper and extend coverage fromsub(the only op the old inline inhandle_subknew about) toadd,and,or,sub,xorat both byte and word widths. Byte width always emits80 /r ib(5 bytes); word width picks the 5-byte83 /r ibsign-extended short form when the immediate fits signed 8-bit and falls back to the 6-byte81 /r iwform otherwise. All shapes match NASM byte-for-byte.bits.cexercises them viay -= 5(memory-allocated local), anint counterglobal stepping through+=/|=/&=/^=, and auint8_t bcounterglobal stepping through+=/|=/&=/^=/-=; a printf between each op clobbers AX so the reload/op/store triple forms andpeephole_memory_arithmetic/_bytefuses it into the memory-direct shape - Self-hosted assembler:
%macro/%endmacrosupport. Single-parameter-token macros shaped to matchidt.asm’s needs:macro_names[]/macro_argcounts[]/macro_body_starts[]/macro_body_lengths[]/macro_body_buffer[]hold the table;macro_args_text[]/macro_arg_starts[9]are per-invocation scratch.define_macro(fromparse_directive’s%macrobranch) slurps lines into the body buffer until%endmacro;find_macrolinear-scans the name table atparse_mnemonic’s top;expand_macrosubstitutes%1..%9intoline_bufferand re-runsparse_lineon each expanded line, so labels (exc_%1:) and directives (dw,db) work without special handling.static/macro_sm.asmsmoke-tests anIDT_ENTRYdata macro and anEXC_NOERRlabel-defining / push / jump macro. - Self-hosted assembler: add
in al, dx/in ax, dx/out dx, al/out dx, ax(opcodes EC/ED/EE/EF). Each handler validates that one operand is DX and the other is AL/AX, then the data-register size picks between byte and word encodings. Needed so the self-hosted assembler can reassemble programs that talk directly to ports (e.g.draw.c’s DAC writes to 3C8h/3C9h). - Self-hosted assembler: add
leaand fix the alu-binop[reg+disp]encoding. - Self-hosted assembler (
src/c/asm.c): protected-mode extension (phase 5).parse_registeraccepts thee-prefixed 32-bit general register file (eax / ecx / edx / ebx / esp / ebp / esi / edi); a dedicatedparse_creghandles cr0..cr7;emit_sizedprepends the 0x66 operand-size prefix for 32-bit widths; newemit_dwordemits little-endian imm32 / disp32.handle_movgainsmov crN, r32/mov r32, crN(0F 22 /r, 0F 20 /r) andmov r32, imm32with the 0x66 prefix;emit_alu_reg_immextends to 32-bit operand size for theor eax, 1style encodings. Newhandle_lgdt/handle_lidt(0F 01 /2, /3) andjmp dword SEL:OFS(0x66 0xEA ptr16:32) round out the protected mode bootstrap encodings.static/pmode_sm.asmexercises the full set against NASM; byte-identical on the self-host test - Self-hosted assembler phase 5.4:
push [word|dword] immis bits-aware. Optionalword/dwordsize token overridesdefault_bits; the imm tail widens to imm32 when the push is 32-bit.0x6A ibshort form still applies whenever the value fits ±128, independent of push width; only the 0x66 operand-size prefix reflects the push size. - Self-hosted assembler phase 5.5:
movandlgdt/lidtdirect-memory encodings are now bits-aware. ModR/Mrmflips 110 ↔ 101 for mod=00, and the displacement widens 16 ↔ 32. Refactoremit_modrm_directto pick both offdefault_bits; addemit_address_dispfor the accumulator-directmoffsshort form (A0/A1/A2/A3). Adds the missing 0x66 operand-size prefix on the accumulator-short form somov eax, [foo]under bits=16 emits66 A1 disp16instead of the oldA1 disp16. - Self-hosted assembler phase 5.6: 32-bit addressing —
[eax]..[edi]base registers (with ESP’s mandatory SIB byte and EBP’s disp8=0 quirk for mod=00), plus the 0x67 address-size prefix when the address size disagrees withdefault_bits. New stateparse_operand_address_sizeset byparse_operand; new helpersemit_address_size_prefix/emit_sized_mem/emit_indexed_mem. Ten call sites acrossemit_alu_binop/handle_call/handle_cmp/handle_mov/handle_movzx/handle_test/inc_dec_handler/handle_lgdt/handle_lidtroute through them.parse_operandlearns thedwordsize prefix alongsidebyte/wordfor shapes likecmp dword [reg], immandinc dword [reg];emit_sized_immwidens to imm32 when requested. - Self-hosted assembler phase 5.6 follow-up:
push [mem]via theFF /6encoding (previously fell through toresolve_value, which silently evaluated[foo]as a 0 immediate and emitted6A 00), andresolve_valuenow recognises a leading-/+as a unary sign on the first term so[bp-4+1]evaluates left-associatively tobp-3(matching NASM) instead ofbp-5. Needed to restore self-host parity with NASM once phase 5.6 shifted cc.py’s output to shapes that exposed the miscompiles.
0.6.0 (2026-04-21)
Networking
- ICMP sockets via
(SOCK_DGRAM, IPPROTO_ICMP); ICMP echo requests now live in userspace net_opentakes a protocol argument (Linux-style(type, protocol)API)- Remove
SYS_NET_ARPandSYS_NET_PINGsyscalls — both protocols migrated to userspace — and collapse theSYS_NET_*numbering
Userspace programs
- Rewrite
shell,dns,ping,edit, andasm(the self-hosted assembler) in C;arp/netinit/netrecv/netsendjoin them editmoves its gap buffer to fixed addresses — newEDIT_BUFFER_BASE/EDIT_BUFFER_SIZE/EDIT_KILL_BUFFER/EDIT_KILL_BUFFER_SIZEconstants replace the former float-on-program_endlayoutedit.c: liftgap_start/gap_endto file-scope globals and factor 10 copies of the gap-buffer cursor-move idiom intogap_move_left/gap_move_righthelpers
Tooling
- Self-hosted assembler (
src/c/asm.c): NASM → pure C migration completed in this cycle — everyhandle_*mnemonic handler, everyparse_*stage, the symbol table, the include / file-I/O machinery, and the driver loop all live in C. A trailing file-scopeasm(...)block retains only the kernel-syscall wrapper, the mnemonic / register data tables, and theSTR_*keyword strings. The in-OS assembler also picked uppusha/popa/lodsw/adc/notso cc.py-emitted programs can be re-assembled in-place - asm.c: collapse
emit_bytesequences behind four helpers (emit_word,emit_sized,emit_modrm_disp,emit_modrm_direct) — shrinks the binary ~700 bytes and removes ~130 lines of near-duplicate operand emission - asm.c: fold shared-body handler families onto regparm(1) helpers —
unary_f6f7(mul/neg/not/div),shift_handler(shl/shr),inc_dec_handler(inc/dec) — another ~300 bytes off the binary - asm.c: unify
add/and/or/sub/xoronto oneemit_alu_binop(rfield)helper — every opcode the instruction emits is a derivable function of rfield, so five near-identical 30-line bodies become one. Another ~950 bytes off the binary, andor ax, imm16/ similar shapes now encode with the proper short forms (matching NASM instead of the previous 81 /r iw long form) - asm.c: smaller cleanups —
is_ident_char/scan_ident_dothelpers retire the five open-coded[a-zA-Z0-9_]/[a-zA-Z0-9_.]loops;parse_directive’sdw/ddbodies share one operand loop - asm.c: fold
handle_adc/handle_sbbontoadc_sbb_handler(modrm_base)(they differed only in /r field 2 vs 3) - cc.py:
emit_conditionwraps bare expressions (Call,Var,Index, …) asexpr != 0when they reach it inside&&/||, sowhile (foo() || x == 0)compiles naturally alongsideif (foo());return <expr>incarry_returnfunctions lowers the expression into CF via the same two-leg pattern the if form uses - cc.py: tail-call optimization for frameless functions — a trailing statement-level call to a user function becomes
jmp nameinstead ofcall name; retwhen the call site has no stack args and no pinned registers to save. Shrinksasm.canother 50 bytes (handle_clc, handle_mul and the other single-call-body handlers collapse tomov ax, N ; jmp target) - cc.py:
peephole_dead_ahscans forward across AX-preserving instructions (register moves not touching AX, pushes/pops of non-AX regs,cmp/teston non-AX operands) to find the AL-only consumer of a zero-extended byte load. Catches patterns likexor ah, ah ; pop si ; test ax, axthat were previously missed because the immediate-neighbor check stopped atpop si. 31 bytes off across asm / edit / shell - Host-side C compiler (
cc.py): feature and codegen work in support of the above — file-scope globals, inlineasm(...)escape,#includedirective,regparm(1)/carry_return/always_inline/asm_registerattributes,uint8_ttype with byte-codegen for byte-typed globals and body locals,far_read8/16/far_write8/16builtins, new user-callable builtins (checksum,ticks,exec,reboot,shutdown,set_exec_arg), and many peephole / calling-convention improvements
0.5.0 (2026-04-16)
2026-04-16
- Add CHANGELOG.md with full project history
- Add UDP socket support (
SOCK_DGRAM) tonet_open - Add
net_recvfromandnet_sendtosyscalls with cc.py builtins - Refactor cc.py: extract helpers, consistent
_prefix, delete dead code, sort methods
2026-04-15 – 2026-04-16
- Convert
arpfrom assembly to C using raw Ethernet sockets - Port
netinit,netsend, andnetrecvto C - Add
SYS_NET_OPENandFD_TYPE_NETfor raw Ethernet socket file descriptors - Replace
SYS_NET_INITwithSYS_NET_MAC; probe NIC at boot - Add 60-second TTL aging for ARP cache entries
- Add shared
ARGVbuffer andFUNCTION_PARSE_ARGVfor argument validation - Use
int mainin C programs, renameputc/getc, support return expressions - Add GitHub Actions CI workflow with clang syntax checking
- Configure pre-commit hooks
- Refactor test infrastructure:
run_qemu.pydriver, temp directory isolation, shared helpers - Move test files to
tests/directory - Add
test_programs.pyruntime smoke suite - Extensive cc.py compiler optimizations:
- Constant folding and constant-pointer alias tracking
- Peephole passes for redundant BX reloads, cld dedup, and memory arithmetic
- Fuse argc checks into argv startup
- Direct memory addressing for constant-base indexing and comparisons
- Byte-indexed and word-fused comparison optimization
2026-04-14
- Rewrite
drawprogram in C with ANSI escape output - Rewrite
lsandmvcommands in C - Rewrite
chmodin C withchar*byte indexing support - Add
FUNCTION_PRINTFwith cdecl calling convention - Add
rtc_datetimeepoch syscall andFUNCTION_PRINT_DATETIME - Add
rtc_sleepsyscall; drop last user-landINT 15h - Replace
SYS_SCREEN_CLEARwithSYS_VIDEO_MODE - Expand ANSI parser for draw and visual bell
- Add
fstat()builtin to cc.py - Add unsigned long support to cc.py
- Refactor C compiler AST from tuples to dataclasses
- Add
#defineobject-like macros,&&,||, bitwise&to cc.py - Compiler optimizations: constant folding, immediate-form instructions, redundant zero-init elimination, direct store peephole
2026-04-13
- Add kernel jump table; migrate all programs off direct syscall includes
- Remove
SYS_IO_PUT_STRING,SYS_IO_PUT_CHARACTER,SYS_IO_GET_CHARACTERsyscalls - Add
write_stdouthelper and convert all programs to use it - Move argv parsing into kernel
FUNCTION_PARSE_ARGV - Move assembler symbol and jump tables to ES segment
- Console read now returns escape sequences for special keys
- Expand abbreviated identifiers and sort constants
- Rename
DISK_BUFFERtoSECTOR_BUFFER
2026-04-12
- Add file descriptor table infrastructure with
sys_open,sys_close,sys_read,sys_write,sys_fstat - Add
O_CREATflag and close writeback for file creation via fd - Add directory fd support; rewrite
lswithopen/read/close - Rewrite
cat,cp, andeditto use file descriptor syscalls - Rewrite
asm.asmto use file descriptor syscalls - Remove deprecated FS syscalls
- Add block scoping for variables in the C compiler
2026-04-10 – 2026-04-11
- Rewrite
datein C with register tracking and lazy spill optimization - Rewrite
mkdirin C, beating hand-written assembly by 5 bytes - Rewrite
catin C, beating hand-written assembly by 5 bytes - Archive retired assembly sources replaced by C programs
2026-04-09
- Add
cc.pyC subset compiler with variables, while loops, arrays, char literals - Add
sizeof,*,/,%operators - Add
argc/argvsupport - Write
echo.canduptime.cas first C programs - Grow
DIRECTORY_SECTORSto 3 (48 entries)
2026-04-08
- Reorganize segment-0 memory layout and isolate the stack
- Add 32-bit file sizes and 16-bit sector numbers to filesystem
- Self-host: assemble
asm.asmwith the OS assembler - Add
%definedirective and floating buffers onprogram_end - Convert
test_asm.shtotest_asm.py - Assemble
edit: many new instruction forms and parser features
2026-04-07
- Move binaries into
bin/subdirectory - Move static
.asmreference files intosrc/ - Assemble network programs (
netinit,arp,netsend,netrecv,ping,dns) - Convert
add_file.shtoadd_file.py
2026-04-05 – 2026-04-06
- Add subdirectory support to the filesystem (one level under root)
- List subdirectory contents; fix
scan_dir_entriesCX clobber - Cross-directory
cp, same-directorymv, directory guards - Detect drive geometry for floppy and IDE boot support
2026-04-04
- Add LBA-to-CHS conversion for sectors beyond 63
- Add test script for self-hosted assembler
- Phase 2 of self-hosted assembler: assemble
chmod,date,uptime,cp,mv,ls,draw
2026-04-01 – 2026-04-03
- Add Phase 1 self-hosted x86 assembler (two-pass, byte-identical to NASM)
2026-03-31
- Add text editor with gap buffer, Ctrl+S save, Ctrl+Q quit
- Add
SYS_FS_CREATEsyscall for file creation; support new files in editor - Show save messages in editor status bar
- Increase filename limit from 10 to 26 characters
2026-03-30
- DNS lookup for arbitrary hostnames with CNAME chain and all A records
- Allow hostnames in
pingcommand - Add executable file flag,
chmod,mv, andcpcommands - Protect shell from being modified; prevent duplicate filenames
- Support arrow keys via serial console
2026-03-29
- Add NE2000 NIC driver: probe, init, ring buffer, MAC programming
- Raw Ethernet frame transmission and polled packet reception
- ARP protocol for IP-to-MAC resolution
- ICMP echo (ping) with IPv4 header and checksum
- UDP send/receive with DNS lookup
2026-03-28
- Automatic
\nto\r\nconversion — strings no longer need\r\n
0.4.0 (2026-03-28)
2026-03-28
- General cleanup across the project
0.3.0 (2026-03-27)
2026-03-27
- Major revival of the project after 8 years
- Full command-line editor: left/right arrows, delete, Ctrl+A/E/K/F/B/Y, kill buffer
- Cap input length to 256 characters
- Add
shutdown,reboot,date, anduptimecommands - Use command dispatch table for shell commands
- Display date and time at boot
- Add special character handling
- Serial console support: mirror output to COM1, poll input from both keyboard and serial
- Add trivial read-only filesystem on the floppy with
catandlscommands - Add syscall interface (
INT 30h) - Load shell as a program from filesystem
- Extract programs (
draw,date,uptime,cat,ls) from kernel into standalone executables
0.2.0 (2018-08-12)
2018-08-12
- Two-stage bootloader: load second stage from disk
- Proper backspace handling at the command prompt
- Fix bug where short
gmatchedgraphics
0.1.0 (2018-07-27)
2018-07-29
- Move input string buffer to beginning of usable address space
2018-07-27 – 2018-07-28
- Add
help,clear,color, andtimecommands - Color output mode with multiple color commands
- Extract code into functions and protect most registers
- Update version string to 0.1.0
0.0.3dev (2018-07-26)
2018-07-26
- Add simple user-input loop
- Auto-advance cursor row
- Advance cursor on carriage return
- Clear screen on escape
- Echo typed commands
- Detect whether something was entered
0.0.2dev (2018-07-26)
2018-07-26
- Add one more line of output
- Improve formatting and assembly readability
- Save bytes through origin specification and row-increment optimization
0.0.1dev (2012-08-22)
2012-08-22
- Initial BBoeOS code: minimal bootloader with welcome message