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
Added
- cc.py parser/codegen extensions for third-party C (kilo). Four constructs upstream C uses freely that cc.py previously rejected now compile:
typedef struct [TAG] { ... } ALIAS;(a struct definition bundled with a typedef, tagged and anonymous); comma-separated struct members sharing one base type (int cx, cy;); the empty statement (;, e.g. awhile (cond);spin loop); and&obj.field[i]/&ptr->field[i](address of an indexed array/pointer member, scaled by a generalimulfor struct-sized elements). - cc.py dereference of a parenthesized pointer expression (
*(p+1)). The*(base + index)and*(base)forms now desugar tobase[index], so kilo’s*(p+1)/*(p+klen)compile (previously only*name,*++p, and the*(T *)castform were accepted). - cc.py
--permissivemode for third-party C. An opt-in flag that relaxes the house-style comparison strictness: integer literal0counts as a null-pointer constant, soif (p),p == 0, andc != 0compile without the explicitNULL/ character-literal spellings. First-party code keeps the strict default; the flag is for unmodified upstream sources (kilo, lua, …). - kilo port scaffold (
ports/kilo/). Afetch.shthat pins antirez/kilo, shim headers (termios.h,time.h,sys/ioctl.h) and abboeos_kilo_compat.cproviding the handful of POSIX entry points (tcgetattr/tcsetattr/isatty/ ftruncate/time) the editor expects on top of libbboeos. Work in progress — the first fully cc.py-compiled third-party program.
Changed
-
cc.py native-Address emission flip-over (phase 1). Every lowered member-access, array-subscript, and pointer-deref access now flows through a unified
AddressPlanplanner and materializer instead of re-seating AST shapes at emit time. Output is byte-for-byte identical to the previous emitter (verified by the per-function byte gate across 361 functions in 49 files, 0-delta); the planner’s declared per-plan clobber facts are in place for the phase-2 register-allocator integration. -
ioctlis now variadic (int ioctl(int fd, int cmd, ...)). A command takes up to two unsigned-int arguments (ECX then EDX), matching POSIX’s request-dependent argument shape. bboeos’s own callers keep passing both (e.g.VGA_IOCTL_SET_PALETTE); POSIX-style callers that pass a single pointer/value (e.g. kilo’sioctl(1, TIOCGWINSZ, &ws)) now bind directly. - cc.py recognizes hand-written init/copy loops as
repstring ops. A unit-stridefor (i=0;i<n;i++) dst[i]=V;/dst[i]=src[i];now lowers torep stos{b,w,d}/rep movs{b,w,d}(element widths 1/2/4) via a new IR pass over natural loops. Signed loop bounds are guarded so a negative count is a no-op (matching C), not a 4 GBrep. The self-hosted assembler gainedmovsd/stosdto round-trip the 4-byte forms. - Shell: tab completion. Press Tab to complete command names (builtins and executables in
bin/) in first-word position, or files and directories in argument position. Single matches are inserted in-place (directories get a trailing/); ambiguous matches insert the longest common prefix, then list all candidates bash-style on a second Tab; no matches flash a visual bell. - Every user program now builds through the object-file pipeline; the flat path is gone.
shell,arp, anddnscame offmake_os.sh’sFLAT_PROGRAMSas their object-pipeline failures were fixed (see below):shellreturned “unknown command” for every command (the dropped-addend linker bug), andarp/dnspage-faulted (EXC0E) on a directjne FUNCTION_DIEin main’s argc-check.asmfollowed once object mode learned to alias its globals (see below), andtrailer_cross_page— the last holdout — was reworked to pad itself in.rodatainstead of withasm("times nop")in the code path. With nothing left on it,FLAT_PROGRAMS,compile_program_flat, andis_flat_programwere deleted frommake_os.sh. (cc.py’s flat output mode itself stays — the kernel is built with it via--target kernel.)
Fixed
- cc.py miscompiled unit-stride loops that store the induction variable.
for (i = 0; i < n; i++) buffer[i] = i;(and any loop whose body stored the advancing index itself) lowered thei++step to an opaqueBlock(IncrementDecrement)whose only reference toiis the baretarget_namestring. The SSA eligibility filter’s AST walker yielded names only fromVarnodes, so it never saw that write and versionedias loop-invariant — collapsing the guard tocmp 0, n(an infinite loop forn > 0) and the stored value to a constant0. The walker now also yields the bare-stringtarget_name(IncrementDecrement/DerefIncrement{,Assign}) andobject_name(theMember*family) lvalue references in both copies (cc/ssa.py,cc/ir_optimize.py), so such loops stay on the un-versioned scalar path. Pre-existing (not a regression); surfaced during the rep-string loop work (PR #566), which correctly left these non-idiom loops on the scalar path where the bug lived. -
Object mode left inline-asm
_g_<name>global references undefined. The self-hosted assembler (asm) reaches its file-scope globals from inline asm under the legacy_g_<name>spelling, but object mode emitted only the bare C-conformant name, so those references failed to assemble (_g_error_word not defined, then a cascade of “label changed during code generation”). cc.py now emits a_g_<name>:alias label beside every object-mode global — the mirror of flat mode’s<name> equ _g_<name>, as a real label (not anequ) so the ccobj relocation scanner records its address. This is what letasmcome offFLAT_PROGRAMS. -
Object-mode argc-check fused a direct jump to the absolute
dieentry.main’sif (argc != N) die(...)startup fusion emitted a literaljne FUNCTION_DIE— a rel32 to libbboeos’s absolute entry, correct only under the flat path’sorg 0x08048000. In object mode (noorg, relocated byccld) it landed atFUNCTION_DIE + PROGRAM_BASEand page-faulted. It now routes through the base-invariantjne .skip / jmp [FUNCTION_DIE_PTR]form like every other libbboeos jump. This is whyarp/dnscould not build through the object pipeline. -
Jump-collapsing peepholes could synthesize an illegal
jCC [mem].peephole_double_jumpandpeephole_label_forwardingretargeted a conditional jump onto an unconditional jump’s operand without checking it was a label — foldingjCC .skip / jmp [mem] / .skip:intojCC [mem], which has no valid x86 encoding. They now leave memory-indirect trampolines intact. -
Linker dropped relocation addends (
ccld.py/pack-ccobj). An object- pipeline program that referenced a global at a constant displacement —buf[i-1]compiles to[buf-1+esi]— was relinked asbuf[i]: the linker overwrote the displacement with the bare symbol address and discarded the-1NASM baked into the placeholder.pack-ccobjnow records the addend (placeholder − symbol_offset) for each bracket relocation andccld.pyapplies it (address + addend). This silently mis-compiled any object-built program using such an access;edit’s Ctrl+A (move to start of line) read the wrong byte and so stopped immediately whenever the cursor sat at end of line. edit: down-arrow on the last line desynced the cursor. Ctrl+N / down on a line with no following newline walked the gap to end-of-buffer without updatingcursor_line/cursor_column, so the drawn cursor and the real insertion point diverged. The scan now rewinds when no next line exists.
0.12.0 (2026-05-30)
The self-hosting release: the entire in-tree OS — kernel, every userland program, and the C library itself — now builds with the self-hosted cc.py compiler. Alongside that milestone, the shell grew chaining/pipes/redirection and the libbboeos shared library matured into a real libc.
Self-hosting
- The whole in-tree OS builds with
cc.py. All tenuser/libbboeos/C sources (the C library itself) join every userland program and the kernel on the self-hosted build path; clang no longer builds anything in the tree (the sole remaining clang consumer is the out-of-treeports/doom). Getting there drove a broad expansion ofcc.py’s C support:typedef,enumwithswitch/case/defaultand enum-exhaustiveness checking,gotoand labels,{ }compound statements, stack-local structs with designated initializers and bitfield members, function-pointer arrays, pointer-to-pointer,<stdint.h>/<stddef.h>width types, GCC extended inline asm, and variadicva_arg.
Shell
- Command chaining, pipes, and redirection. A line can chain commands with
;,&&, and||(bash precedence), pipe two commands with|, and redirect with>,>>, and<— builtins included. - History and scrollback. Up / Down (Ctrl-P / Ctrl-N) recall the last 16 commands; Shift+PgUp / Shift+PgDn page back through 200 rows of console output.
libbboeos (the shared library)
- Renamed from “vDSO” to libbboeos and restructured into a real shared library. The shared page now exports the full
<string.h>surface and POSIX<dirent.h>(opendir/readdir/closedir/rewinddir) through a pointer table; programs link thin stubs instead of copying function bodies into every binary. - Kernel / userspace type split. The kernel uses Linux-style
u8/u16/u32; userspace uses standarduint*_tvia<stdint.h>.
Userland
- New common utilities:
true,false,seq,yes,wc,head,tail,tee,grep,tr,uniq, andsort. - Linux-style
argv.main(int argc, char *argv[])follows the C / Linux convention —argv[0]is the program basename andargv[argc] == NULL. - POSIX directory iteration via the new
SYS_IO_GETDENTSsyscall;lsoutput is now alphabetically sorted.
Kernel
- Much smaller on disk. Moving zero-initialized storage into a real
.bsssection cut the kernel image from ~93 KB to ~40 KB; a long run ofcc.pyregister-allocation and peephole improvements shrank the kernel and every program further. - Idle CPU parks on
hltinstead of busy-spinning; the NE2000 (IRQ 3) and COM1 (IRQ 4) now wake blocked syscalls on the actual interrupt edge. - SIGPIPE is raised on a write to a pipe whose read end has closed.
- Socket receive timeouts via
SYS_NET_SETSOCKOPT/SO_RCVTIMEO. - Typed bitfield register structs for the NE2000, ATA, FDC, and 8237 DMA / PIC registers replace magic-byte port writes.
0.11.0 (2026-05-10)
- Documentation reflowed to 80 columns. All markdown files (
README.md,CLAUDE.md,CHANGELOG.md,docs/*.md,archive/*/README.md) wrap prose at 80 columns so the raw source reads cleanly on mobile. Newtools/wrap_md.pyreflows paragraphs and list items while preserving fenced/indented code, tables, YAML frontmatter, horizontal rules, and the double-space sentence convention; long unbreakable tokens (URLs) overflow rather than break. The CLAUDE.md “Documentation conventions” section points future edits at the script. - SIGINT default-kill on Ctrl+C (PS/2 IRQ 1 + serial-read paths set a kernel
pending_sigintflag; every IRET-to-user epilogue checks it and tears the dying program’s PD down + reloads the shell). Cooperative-interruption convention added tofd_read_consoleandMIDI_IOCTL_DRAINso blocking syscalls bail withERROR_INTERRUPTED/EINTR. The shell itself is killed and reloaded by Ctrl+C in this PR; a follow-up addsSIG_IGNso it can opt out. signal(SIGINT, SIG_IGN | SIG_DFL)viaSYS_SYS_SIGNAL(0xF5). The shell installsSIG_IGNat startup so its own Ctrl+C is benign; child programs inheritSIG_DFLfromprogram_enterand remain killable by default. User-handler delivery (signal(SIGINT, fn)) is not yet supported —SYS_SYS_SIGNALrejects user-virt handlers withEINVAL; a follow-up adds full handler delivery.signal(SIGINT, handler)— full user-handler delivery via 48-byte sigcontext on the user stack andSYS_SYS_SIGRETURNresume through a vDSO trampoline at0x10450. EFLAGS is sanitized in sigreturn (mask0xDD5, forceIF=1) so a malicious handler can’t escalate IOPL. Tests:tests/programs/sigint_test.cexercises the full path.- SIGALRM + per-process interval timer. New SYS_RTC_ALARM (30h) syscall arms a one-shot or repeating timer at 1 ms granularity; expiration delivers SIGALRM via the existing SIGINT plumbing. Default action is terminate (POSIX). rtc_sleep, fd_read_console, and MIDI_IOCTL_DRAIN return EINTR when interrupted by either signal. libc adds alarm() (POSIX seconds) and alarm_ms() (BBoeOS ms+interval extension).
- ports/doom/record_doom: capture SB16 audio during the recording and mux it into
docs/gifs/doom.mp4(H.264 + AAC, 128 kbps; the GIF stays silent — no audio container). QEMU’s-audiodev wav,...+-device sb16,audiodev=...writes a 16-bit stereo PCM WAV alongside the PPM frames; ffmpeg’s filter graph does three things in one pass:trim/concataround the SeaBIOS reboot interlude (replacing the old_drop_frames_in_windowfile-system PPM deletion),adelayon the audio so it lines up with the video timeline (QEMU’swavaudiodev only writes samples while SB16 is actively producing audio, so silent stretches are absent from the file and the WAV’s t=0 corresponds to “first SB16 sample” rather than “QEMU launched”), andapad+-shortestto silence-pad the audio tail to video duration. The audio offset is anchored to Doom’s[bboeos doom] OPL musicserial print so sync stays tight across QEMU builds with different load times. NewBBOE_QEMU_BINARYenv var (matches the existingBBOE_QEMU_MEMORY/BBOE_QEMU_MACHINEoverrides) lets a custom QEMU build plug in without touching code — handy for the SB16-OPL3-patched build that mixes Doom’s MIDI tracks into the captured WAV alongside the PCM SFX. CLI is--format=mp4|gif|both(mp4 default). New unit tests attests/unit/test_record_doom_audio.pylock down the pure filter-graph helpers so a silent regression to “no trim” or a bad adelay value can’t sneak through. - drivers/sb16: switch SB16 PCM playback from synchronous single-cycle DMA to auto-init double-buffering with a kernel-side software ring.
sb16_openprograms the 8237 in auto-init mode (mode byte0x59) and starts the DSP via0x48(set block size) +0x1C(8-bit auto-init PCM, no args), so the DSP loops the 4 KB DMA buffer indefinitely and fires IRQ 5 everyAUDIO_HALF_SIZE(2 KB) bytes — at which pointsb16_refill(called frompmode_irq5_handler) drains the 4 KB software ring into the just-finished DMA half and pads with silence on underrun.fd_write_audiobecomes a non-blocking ring producer: it returns as soon as user bytes are queued instead of blocking for the chunk’s playback duration. Doom’s per-tick audio write (~315 bytes) used to pin the engine at the SB16’s chunk rate (~28 ms blocking per write, magnified by the 1 kHz PIT’s hlt thrashing), starving the renderer; with the auto-init ring it’s a single memcpy into the ring and Doom hits frame 30 in ~0.5 s instead of dragging through 3 frames in 12 s. - drivers/rtc: fix
datereturning a wildly off month/day that drifted forward by ~50 ms ofsystem_ticksper call. The C-portrtc_read(drivers: port rtc.asm to C, 2026-04-28) compiledkernel_outb(0x70, reg); return kernel_inb(0x71)intomov edx, 0x70 / out dx, al / mov edx, 0x71 / in al, dx, clobberingEDX— butrtc_read_date_internalwrites the month intoDHand then callsrtc_readagain for the day, so the second call’smov edx, 0x71zeroedDHbeforemov dl, allanded. C then readepoch_month = 0, computedmonth_index = -1, andrtc_month_days[-1]indexed exactly onto_g_system_ticksin the global layout. Restorertc_readto the original immediate-port asm shape (out 0x70, al / in al, 0x71 / ret) so it preservesEDXagain.tests/test_programs.py’sdatetest now runs the command three times and requires the dates to agree. - kernel: widen
SYS_RTC_DATETIME,SYS_RTC_MILLIS,SYS_RTC_UPTIMEto return the full 32-bit value inEAX(wasDX:AXfor the first two, sign-extendedAXfor uptime — which silently truncated past 9 h and zero-wrapped at 18 h).SYS_RTC_SLEEPnow reads the full duration fromECX(wasCX, capped at 65 535 ms). vDSOshared_print_datetime, the libcgettimeofday, and the DoomDG_GetTicksMs/DG_SleepMshelpers updated to match. - kernel: new
/dev/mididevice (FD_TYPE_MIDI = 6) backed by the SB16’s hardware OPL3 chip. Wire format is 6-byte commands(delay_lo, delay_hi, bank, reg, value, reserved)with per-command delay; the kernel drains a 256-slot event ring from the IRQ 0 ISR (bounded at 16 events per tick) so writes get 1 ms timing precision. Single-opener semantics;MIDI_IOCTL_DRAIN(AL=0) blocks viasti/hltuntil the kernel ring drains. Userland probes for the chip viaopen("/dev/midi")itself —fd_open_midirejects the open ifg_opl3_present == 0. New driverkernel/drivers/opl3.c(outb-based register writes + chip probe) and dispatch inkernel/fs/fd/midi.c. - Doom music continues during level loads:
ports/doom/opl_bboeos.cinstalls a SIGALRM handler atOPL_Initand arms a 28 ms repeating alarm viaalarm_ms, soopl_bboeos_pollkeeps draining the OPL queue even when the main game loop is blocked onW_LoadLumpName/ WAD I/O during level transitions. Reentrancy with the per-frameBBoe_MusicPollcall is handled by anin_pollbyte insideopl_bboeos_poll— single-threaded with iret-only signal delivery, whichever caller arrives first runs and the other returns immediately (a skipped tick is harmless becausemusic_clock_ustracks wall time, not poll count).OPL_Shutdowndisarms the alarm and restores the prior SIGALRM disposition. - Doom music: Doom now plays its background tracks through OPL3. Pinned-commit fetch of Chocolate Doom’s OPL music stack (
i_oplmusic.c,mus2mid.c,memio.c,opl_queue.c,midifile.c,opl.h) intothird_party/chocolate-doom-opl/viaports/doom/fetch_chocolate.sh, plus a thinports/doom/opl_bboeos.cbackend that bridges Chocolate’s OPL API to/dev/midi. The 12 music stubs inports/doom/i_sound_bboeos.cnow delegate to Chocolate’smusic_opl_module;ports/doom/chocolate_compat.his a narrowly-scoped (~95 line) shim for the chocolate-vs-doomgeneric drift. The chip is run in OPL3 mode (richer than the spec’s planned OPL2 baseline) —OPL_InitreturnsOPL_INIT_OPL3andOPL_InitRegistersenables the second register bank, matching upstream Chocolate Doom on real SB16 hardware.BBoe_MusicInitlogsOPL music enabled/OPL music unavailableto the serial console; the integration test (tests/test_doom_music_qemu.py) keys off that marker. - Per-program kernel state migrated to ProgramState struct. Every per-program global (
current_pd_phys,sigint_handler, alarm fields,current_program_break,fd_table, etc.) now lives in aProgramStatestruct addressed viacurrent_program_state. No behavior change in this commit — the change is foundational for the shell-survives-child work that follows. - Shell survives child kills (synchronous spawn-and-wait).
sys_execno longer destroys the parent’s PD; the parent is suspended in-kernel while the child runs. When the child exits, is killed by a signal, or hits a CPU exception, the kernel restores the parent’s PD and resumes the parent’sint 30hwith EAX = POSIX-shaped wait status. The shell no longer reloads from disk between commands; its state (open file descriptors, line history, etc.) survives across child runs. New libc surface:exec()returns wait status (>=0) or-errno;_exit()takes an int status argument;wait.hprovidesWIFEXITED/WIFSIGNALED/WIFCRASHED/WEXITSTATUS/WTERMSIG. Shell surface:$?argument-expansion (bash-shaped:0..255clean exits,128+signumfor kills,255for crashes),[shell:start]boot marker. Recursiveexec()from a child returns-ERROR_INVALID. PIT IRQ iterates both alive slots so a parent’s alarm fires at wall-clock time even while a child is running.
Known limitations
- OPL music is silent in QEMU. QEMU 8.x’s
sb16device implements only the DSP for PCM playback; writes to the chip’s OPL register ports (0x388/0x38A) are accepted but no FM audio is synthesized in QEMU’s audiodev capture. Doom’s music is therefore audible only on real SB16 hardware (or emulators that include OPL FM synthesis, e.g. DOSBox via passthrough); Doom’s SFX path through/dev/audiois unaffected and works fine in QEMU. The CI smoke test (tests/test_doom_music_qemu.py) verifies the music initialisation path through a serial-log marker rather than through audio capture.
0.10.0 (2026-05-04)
Doom port
- BBoeOS now boots and runs doomgeneric.
ports/doom/build.pyclones the upstream third_party/doomgeneric on demand, cross-compiles with the freestanding clang toolchain, and links it withlibbboeos.a+user/libbboeos/program.ldinto a flat-binarybin/doom.ports/doom/fetch_wad.shdownloads the sharewaredoom1.wad(SHA256 verified).ports/doom/install.shis a one-shot wrapper. Auto-picks GNU-compatibleld/objcopy/ar(Linux native,x86_64-elf-*,llvm-*,ld.lld) so the same script works on Linux + macOS. ports/doom/bboeos_doomgeneric.cimplementsDG_Init/DG_DrawFrame/DG_GetKey/DG_GetTicksMs/DG_SleepMsover the new kernel surface (mode-13h framebuffer viaSYS_VIDEO_MAP,SYS_RTC_MILLIS/SYS_RTC_SLEEP, the per-fd PS/2 event ring). WASD + arrow keys move, Ctrl/F fires, Space/E uses, Esc opens the menu.ports/doom/record.pydrives QEMU through the monitor socket to capture screendumps every 200 ms, encoding to WebM (VP9 via ffmpeg) and/or GIF (palettegen + paletteuse + gifsicle).tests/test_doom_qemu.pyis a two-stage smoke test: bootstrap (always) verifies the engine reaches IWAD lookup; the main-loop stage verifiesDrawFrameticks at least 30 frames whenwads/doom1.wadis present.
Doom SFX
- 8-voice software mixer (
ports/doom/audio_mixer.c, sum-clamp at 8 bits) drives the new kernel/dev/audiodevice (see Sound below) fromports/doom/i_sound_bboeos.c’ssound_module_tadapter once per Doom tick (~315 samples / ~28 ms). Music is stubbed.ports/doom/build.pydefines-DFEATURE_SOUNDso doomgeneric registers our backend. Doom keeps booting silently when the SB16 isn’t present. Mixer math has pytest unit tests (tests/unit/test_audio_mixer.py); end-to-end smoke test intests/test_doom_sound_qemu.py(gated onwads/doom1.wad) captures Doom’s audio with-audiodev wav,...and checks RMS energy.
Sound
- Sound Blaster 16 driver (
kernel/drivers/sb16.c), exposed as OSS-style/dev/audio(kernel/fs/fd/audio.c). Probes the SB16 DSP at boot via the standard reset-and-0xAA handshake; on success allocates a 4 KB DMA frame in the direct-map range so the kernel canmemcpyinto it. Single-cycle synchronous playback model:fd_write_audiochunks the user buffer into <= 4 KB pieces, copies into the DMA buffer, andsb16_playprograms 8237 channel 1- DSP cmd
0x14then blocks viasti+hltuntil IRQ 5 (vector 0x25 inentry.asm) signals the chunk played. Single-opener;AUDIO_IOCTL_QUERYlets userspace probe presence before opening. Manual smoke programs intests/programs/audio_open.c(open/close cycle) andtests/programs/audio_tone.c(1.1 kHz square wave).
- DSP cmd
Userspace toolchain (libc)
- New freestanding libc shim
user/libbboeos/libbboeos.a(user/libbboeos/Makefile) covering ctype, errno, math, stdio, stdlib, string, syscall, and_start/setjmp. Pinned to-march=i386 -mno-{mmx,sse,sse2} -mno-implicit-float -fno-{vectorize,slp-vectorize}so Apple clang doesn’t sneak SIMD into memcpy/struct-init paths the kernel hasn’t enabled in CR4. Pytest unit tests intests/unit/test_libbboeos.pycross-check each pure function against the host libc; on-OS smoke test intests/test_libbboeos_qemu.py. - Linker script
user/libbboeos/program.ldfor clang-built userland binaries. - Headers and impls extended for doomgeneric needs (
malloc/free/realloc,printfformatters,qsort,<setjmp.h>long-jump). Also includes the compiler-rt builtins clang -O2 needs at link time (e.g.__udivdi3,__moddi3). - Doom-driven libc bug fixes plus Apple clang compatibility tweaks (more
-mno-implicit-floatpaths,-fno-vectorizeto defeat the loop+SLP vectorizers, etc.).
Kernel
SYS_IO_SEEKsyscall (fd_seekinkernel/fs/fd.c) with libclseek/fseekwrappers. Clamps out-of-range offsets so the WAD reader’s seek-past-EOF pattern works.- x87 FPU enabled (
SYS_SYS_BREAKfor sbrk-style heap probing;SYS_VIDEO_MAPto expose the mode-13h aperture into the calling program’s PD asPTE_USER_RW_SHARED). Lets clang-emitted FP code in libc + doomgeneric run without#UD. - Per-fd PS/2 keyboard event ring with positional
BBKEY_*codes (kernel/drivers/ps2.c,user/libbboeos/include/bbkeys.h). Each readable console fd gets its own queue, populated by the IRQ broadcaster and drained byCONSOLE_IOCTL_TRY_GET_EVENT. Doom’sDG_GetKeyconsumes events directly without re-parsing CSI sequences or synthesising modifier keys. - Text mode restored on every
shell_reloadso a dying program (Doom in mode 13h) leaves a usable shell. io_ioctlpreserves the full 32-bit EAX on return (the asm dispatcher previously zero-extended only AX, dropping the high half some VGA-aperture queries returned).
Build / test tooling
make_os.sh --sectorsflag for larger drive images (Doom needs more than the floppy default).tests/test_programs.pystraddle-test setup extended to look for sector boundaries across multiple ext2 blocks, so adding new test programs totests/programs/doesn’t pushbin/past the first block.
0.9.2 (2026-05-02)
- Bugfix:
bbfs_create(kernel/fs/bbfs.asm) hardcoded the new directory entry’s flag byte to0, silently dropping the mode argument the kernel handed it. Any program that asked the kernel to create an executable file (asmwriting the assembledout,cppreserving the source’s mode, etc.) ended up non-executable on bbfs and the user had to runchmod +xafterwards. ext2 already honoured the same DL register viaext2_cr_mode; bbfs now mirrors that pattern with a newbbfs_create_modestatic, masked toFLAG_EXECUTEso a strayFLAG_DIRECTORYfrom a caller doesn’t accidentally make a file directory-flagged.
0.9.1 (2026-05-02)
- Bugfix: the C-ported
fd_ioctl/fd_lookup(kernel/fs/fd.c) clobbered ECX and EDX during dispatch —fd_lookup’s entry-pointer math trashed EDX, andfd_ioctl’s array-index multiply trashed ECX — silently destroying the user-passed sub-command args beforefd_ioctl_vgaread them. Every VGA ioctl feeds args through those registers (DL=mode, CL/CH=col/row+DL=color, CL/CH/DL/DH=index/r/g/b), sovga_set_modesaw a garbage byte in AL, found no matching mode-table entry, and bailed early without clearing the framebuffer. User-visible fallout: Ctrl+L didn’t clear the shell screen,editre-stamped a newhon top of the previous frame each keystroke, anddrawnever actually entered mode 13h (its fill_block writes landed at0xA0000while text mode kept reading0xB8000). Fix: addpreserve_register("ecx")andpreserve_register("edx")to both functions so cc.py spills them at entry and restores them before every return path, including the tailjmp ebxintofd_ioctl_vga.
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 (user/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 newkernel/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 inuser/programs/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-binarykernel/arch/x86/boot/bboeos.asmwhose [bits 16] MBR fronts a [bits 32] stage- CPU exceptions vector through
idt.asm’sexc_commonand printEXCnnon COM1.
- CPU exceptions vector through
- 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 inkernel/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
kernel/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 underkernel/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 tokernel/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 underkernel/fs/(bbfs.asm,ext2.asm,fd.asm+fd/,block.asmblock dispatcher,vfs.asm). Network stack inkernel/net/keeps the protocol layer only (arp.asm,icmp.asm,ip.asm,udp.asm). Shared utilities insrc/lib/, syscall handlers inkernel/syscall/.make_os.shadds-i src/so%include "drivers/ata.asm"/"fs/fd.asm"/"net/net.asm"/ … resolve at the top level.kernel/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 newkernel/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
kernel/arch/x86/protected mode.asm→kernel/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.user/programs/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;user/programs/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;user/programs/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 acrossuser/programs/*.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 (
user/programs/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.user/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 (
user/programs/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.user/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 (
user/programs/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