P32W Unix — design proposal¶
Status: ACCEPTED (2026-10-13, D22): all eight review points as proposed.
Progress: M1 (objects, linker) and M2 (the C compiler cc/, the C
library) done; M3 (the kernel, kernel/) and M4 (users, logins on both
terminals, permissions, the shell's variables and wildcards, ed) run in
the emulator with ~50 commands (software/cmd/) — see kernel/README.md and
unix.md.
Ken's direction (2026-10-13): "a multitasking OS where each user is in a protected area with their own memory space. We will need a filesystem, system calls into OS and classic unix like commands." Choices made with Ken the same day:
| Question | Choice |
|---|---|
| Language | C, with our own compiler (and linker) |
| OS basis | Our own, in the style of Unix V6 |
| Users | Real multi-user: logins, user IDs, rwx permissions, root; one terminal per serial port |
| Filesystem | A Unix inode filesystem on the CompactFlash card, plus a host tool for disk images |
Working name: P32W Unix (the kernel /unix, as in V6).
1. What the machine already gives us¶
The hardware was designed for this (D12–D16, D50, D20):
| Need | Already there |
|---|---|
| Protected processes | user / supervisor modes; user mode cannot run privileged instructions or reach the I/O page or the upper 2 GiB |
| Own memory space per process | a software-managed TLB (64 I + 64 D entries, 4 KiB pages, V/W/X bits), 8-bit address-space IDs: a context switch is one MTCR ASID, no TLB flush |
| System calls | SYSCALL traps to the kernel; RTE returns; the faulting/trapping instruction has no effect, so page faults can restart |
| Pre-emption | the I/O card's timer (IRQ0); interrupt priorities and masks |
| Terminals | four TL16C550 UARTs with interrupts (IRQ2, IRQ3) on the I/O card |
| Disk | CompactFlash on the disk card, True-IDE, 512-byte sectors (IRQ1) |
| Memory | RAM from 0x0010_0000, 16 MiB per memory card, one to four cards (up to 64 MiB, D52); 1 MiB boot ROM (the monitor) |
| Boot | the monitor's b command loads an image from the CF card and jumps to it |
Speed: about 450,000 instructions a second (3 MHz microcycle, ~6.5 per instruction). That is the class of the PDP-11/40 that ran Unix V6 — with many times its memory. A context switch is a few hundred instructions, a TLB refill ~15, so time-sharing two users is comfortable.
The I/O card's battery-backed real-time clock (D23) keeps the date over power-off.
2. The plan in one paragraph¶
First a toolchain: the assembler learns to emit relocatable object files,
a new linker joins them into executables, and a new C compiler
(p32cc, Python like the assembler) turns C into P32W assembly. With it, a
small C library. Then the kernel, in C with a little assembly:
processes with private address spaces, a scheduler, ~35 V6-style system
calls, a terminal driver for both UARTs, a disk driver and buffer cache, and
an inode filesystem. Last, user programs: init, getty/login, a
shell, ed, and the classic commands. Everything runs first in the
emulators, under make test, with scripted terminal sessions; the same disk
image then boots the real machine.
3. Toolchain¶
3.1 Objects and the linker (p32asm -c, p32ld)¶
- The assembler gains sections (
.text,.data,.bss),.global/.extern, local labels, and relocatable output (-c, a simple.oformat: sections, symbols, relocations). Three relocation kinds cover the ISA: 32-bit absolute (.word sym), theLUI/ORIpair (LA,hi/lo), and the 24-bit word offset ofJMP/CALLacross files. p32ldlinks objects and archives (.a) into an executable or a flat binary (for the kernel), with a link map. Without-cthe assembler still makes flat images, so everything that exists keeps working.
3.2 The executable format (a.out, V7-like)¶
header (32 bytes): magic 0x50333277 ("P32w"), text size, data size, bss size,
entry, flags, symbol table size, 1 reserved
then the text, then the data, then the symbol table (debuggers; exec skips it)
The symbol table (2026-10-14; assembler/p32ld.py gives the format): every
symbol of every object linked, global and local, sorted by value; ld -s
leaves it out.
User address space (virtual, 8 MiB per process, D34):
0x0000_0000 page 0: never mapped (a NULL pointer faults)
0x0000_1000 text (read + execute, shared between processes running the same program)
data, bss (read + write)
heap: grows up with brk/sbrk
... (unmapped gap)
0x0080_0000 top of the stack, growing down (pages added on demand)
The page table for 8 MiB is 2,048 entries in two physically contiguous frames (§4.2). The window can grow with the memory without changing programs.
3.3 The C compiler (p32cc)¶
A one-pass-per-stage compiler in Python: preprocessor → lexer → parser →
type checker → code generator → assembly → p32asm -c. No optimiser at
first beyond constant folding and sensible register use.
The language: C89 without floating point (P32W has no FPU; V6 Unix needed
none), growing in stages:
1. int, char, pointers, arrays, functions, if/while/for/do/break/continue/return, expressions, globals, string literals.
2. struct, union, enum, typedef, switch, goto, sizeof, casts, static/extern, initialisers, function pointers, short, unsigned.
3. The preprocessor in full: #include, #define with arguments, #if/#ifdef/#elif, #undef.
4. Variadic functions (stdarg.h for printf) — done. Not done: long long (64-bit, via library helpers).
Types: char 8 (signed, as on the PDP-11), short 16, int and long
32, pointers 32, long long 64. Little-endian.
The calling convention (ABI) — a proposal, to fix early:
| Registers | Use |
|---|---|
| r0 | zero |
| r1–r4 | arguments 1–4 (more on the stack); r1 the result (r1:r2 for 64-bit) |
| r1–r7 | caller-saved (scratch) |
| r8–r13 | callee-saved |
| r14 | frame pointer (callee-saved) |
| r15 | stack pointer; CALL pushes the return address (D6) |
Tests: a C test suite (cc/tests/*.c), each with its expected output,
compiled by p32cc and run in p32emu; programs that are plain C are also
compiled by the host cc and must print the same. In make test.
3.4 The C library¶
Small and Unix-shaped, in C plus assembly stubs: system-call wrappers
(read, write, open, fork, exec, …), string.h, ctype.h,
stdlib.h (malloc/free on sbrk, atoi, exit), a small stdio
(printf family, getchar/putchar, fopen/fgets/fputs buffered),
errno, signal, setjmp/longjmp, dirent for reading directories, and
the 64-bit arithmetic helpers.
4. The kernel¶
Written in C (~8,000 lines expected) with assembly for trap entry, the TLB refill and the context switch (~400 lines). It runs in supervisor mode, untranslated, at physical addresses.
4.1 Physical memory¶
0x0000_0000 ROM: the monitor (loads the kernel)
0x0010_0000 kernel text and data (budget 256 KiB)
kernel stacks (4 KiB per process), process table, buffer cache (64 KiB)
page frames: the rest of the RAM, 4 KiB each (the boot
message says how much: "memory: 16 MiB of RAM, ... KiB free")
0x00FF_FFFF top of the first memory card (16 MiB; up to 0x03FF_FFFF
with four cards, D52)
4.2 Processes and memory¶
- Process table, up to 64 processes: state, PID, parent, user IDs
(real/effective), ASID, page-table frame, kernel stack, saved registers,
open files, current directory, signal state,
sleepchannel. - Each process has its own address space: its own page table, 2,048
entries in two contiguous frames, already in
TLBLOformat (§C7 of isa-system.md), so the TLB-miss handler is one indexed load,TLBW,RTE. A missing or forbidden page is a real fault: the stack grows on demand, everything else kills the process (SIGSEGV) — the other users carry on. - ASIDs: each running process gets one of 255; when they run out the TLB is flushed and they are handed out again (a rare, cheap event).
forkis copy-on-write (M6): parent and child share every page, writable ones marked read-only, and the first write to one (aPROTECTfault) copies it. Text pages are shared outright between processes running the same program. (V6 copied the whole image.)execreads thea.outfrom the filesystem into fresh pages, sets up the arguments on the new stack, and enters user mode withRTE.- The kernel checks every user pointer (
copyin/copyoutthrough the caller's page table): a system call cannot be used to read or write another process's memory or the kernel. - Swapping: not at first. A memory card's 15 MiB of frames holds dozens of
small processes; a full memory makes
forkfail withEAGAIN. Swapping to the CF card can come later if it is ever needed.
4.3 Scheduling and sleeping¶
- Pre-emptive round-robin on the timer interrupt (proposal: 50 Hz,
a 20 ms slice), with V6
sleep(channel)/wakeup(channel)for waiting on the disk, a terminal, a pipe or a child.WAITidles the CPU when nothing can run. - The kernel itself is not pre-empted (V6 style): it runs a system call
to completion or to a
sleep; interrupt handlers only queue work and wake sleepers. Simple and safe on one CPU.
4.4 System calls¶
SYSCALL with the number in r1 and arguments in r2–r5 (isa-system.md §8);
the result comes back in r1, and failure as −errno (the library turns it
into −1 and errno). The V6 set, with V7 names where clearer:
| Group | Calls |
|---|---|
| Processes | fork exec (execve) exit wait getpid getppid kill signal pause alarm nice sbrk |
| Files | open creat close read write lseek dup dup2 pipe stat fstat ioctl (terminal modes) |
| Names | link unlink mkdir rmdir chdir mknod chmod chown access rename |
| Users | getuid geteuid getgid setuid setgid umask |
| Time, system | time stime times sync uname |
Signals, V7's fifteen, with V7's numbers: SIGHUP, SIGINT (^C),
SIGQUIT (^), SIGILL, SIGTRAP, SIGABRT, SIGEMT, SIGFPE,
SIGKILL, SIGBUS, SIGSEGV, SIGSYS, SIGPIPE, SIGALRM, SIGTERM
(no SIGCHLD: a parent learns of its children through wait). A handler
runs in user mode through a small trampoline on the user stack; a caught
signal resets to the default action, as in V7.
4.5 Devices¶
| Device | Driver |
|---|---|
/dev/tty0, /dev/tty1 |
UART 0 (the console) and UART 1, interrupt-driven, with a line discipline: echo, erase (backspace / DEL), line kill (^U), ^C → SIGINT, ^\ → SIGQUIT, ^D end of file, a raw mode for programs that want characters |
/dev/disk |
the CompactFlash card's filesystem area: a block driver (PIO, 512-byte sectors) under a buffer cache (V6 bread/bwrite, write-back, flushed by sync and every 30 s) |
/dev/rdisk |
the card raw, by its own sector numbers: reads and writes go through one sector buffer, bypassing the cache |
/dev/ram |
a 256 KiB RAM disk (block), for a scratch filesystem (mkfs, mount) |
/dev/null |
discards writes; reads give end of file |
/dev/mem (root only) |
physical memory: ROM and RAM by physical address |
/dev/port |
the ports on the I/O card: the 8-bit input and output ports |
/dev/tty (character 4) |
the process's controlling terminal, whichever it is |
4.6 Boot¶
- The monitor's
bloads the kernel from the boot area at the start of the card (the existing "P32B" header) and jumps to it. - The kernel sets up memory, the TLB and the devices, mounts the root
filesystem, and starts
/etc/initas process 1. initruns/etc/rc(checks the filesystem, cleans/tmp), then agettyon each terminal;gettyrunslogin,loginchecks/etc/passwd, sets the user's IDs and directory, and runs their shell.shutdown/syncbefore power-off (no journal, as in V6: §5.4).
5. The filesystem¶
A V7-style inode filesystem, sized for a CF card, with names longer than V7's 14 characters.
5.1 Layout¶
Blocks of 1 KiB (two sectors). The card holds:
sectors 0..N boot area: "P32B" header + the kernel image (the monitor boots this)
then block 0 superblock: magic, size, inode count, the free-block bitmap's place, clean flag
block 1.. the inode table (64-byte inodes, 16 per block)
then the free-block bitmap (one bit per block)
then data blocks
Inode (64 bytes): type and permissions (16 bits: file / directory / character / block device / pipe, set-user-ID, set-group-ID, sticky, rwx × 3), link count, owner uid and gid, size, modification and change times, 11 block numbers: 8 direct, 1 single-, 1 double-, 1 triple-indirect (files to 2 GiB: the triple block — the former padding word, 2026-10-14 — reaches 16 GiB, the signed 32-bit file offset stops at 2 GiB).
Capacity (Ken: "we will want a bigger cf capacity"): the CF driver
addresses 2²⁸ sectors (LBA28, 128 GiB); block numbers are 32-bit. The
kernel's block allocator starts each search in the bitmap block where the
last one ended and steps over full bytes, so a large disk is not scanned
from its start every time. mkfs gives one inode per 8 blocks, at most
512 Ki inodes, so the on-machine fsck (one bit per block, three bytes per
inode) checks a filesystem of several GiB within a process's address space.
Still to do for very large cards: LBA48 (past 128 GiB), mount and
partitions (several filesystems on one card).
Directory entries (32 bytes): inode number (32 bits) + a name of up to
28 characters. . and .. in every directory.
5.2 What it supports¶
Hard links, mkdir/rmdir, rename, device files (mknod), pipes,
permissions with root override, set-user-ID programs (passwd, su),
umask, and mounting a second filesystem (mount, umount; for
example the RAM disk on /mnt).
5.3 The host tool p32fs¶
A Python tool, so disk images are built from the repository by make:
p32fs mkfs IMAGE SIZE, p32fs put IMAGE host-file /path, p32fs get,
ls, mkdir, chmod, chown, fsck (checks every invariant: free map
vs blocks in use, link counts vs directory entries, sizes vs blocks), and
p32fs build IMAGE tree/ to make a complete root filesystem (kernel in the
boot area, /bin, /etc, /dev, /usr, /tmp, home directories) in one
step. The same image boots in the emulator (-d) and is written to a real
card with dd.
5.4 Crash safety¶
V6 style: writes are cached and flushed by sync, every 30 seconds and at
shutdown; at every boot /etc/rc runs fsck -q (on the machine — a C
port of the host checker) before going multi-user. fsck -y repairs; after
repairing the mounted root it restarts the machine without writing the
cache back (D28), as V7's fsck did. A journal (as xv6's log) is
possible later, but adds complexity the first version does not need.
6. Users and security¶
/etc/passwd:name:password-hash:uid:gid:full name:home:shell;/etc/group. root is uid 0 and bypasses permission checks.- Every process carries a real and effective uid/gid; files carry an
owner, a group and rwx bits for owner / group / others. The kernel checks
them in
open,exec,chdir, directory searches,unlink,chmod,chown(root only),kill(own processes, or root),setuid(root, or back to the real uid). - Memory protection comes from the address spaces (§4.2): one user's processes cannot see another's memory, the kernel's, or the devices.
- Passwords are stored as a salted hash. P32W has no fast crypto, so the
proposal is a deliberately simple keyed hash (several thousand rounds of a
32-bit mixing function, a 16-bit salt): fine for a hobby machine on two
serial lines, not a defence against a stolen disk.
/etc/passwdcan be readable (as in V7) or the hashes moved to a root-only/etc/shadow.
7. The commands¶
Phase 1 — a usable system:
init getty login sh ls cat echo cp mv rm ln mkdir
rmdir pwd chmod chown chgrp ps kill wc grep head tail
sync date who passwd su ed mknod stty true false
sleep shutdown
Phase 2: more sort uniq od diff cmp find du df tee
tr cut test/[ expr write (to another user's terminal) mail
wall fsck mount/umount cc? (a native compiler is far off; programs
are compiled on the host).
Status (2026-10-14): both phases are built, and much more (about 85
commands, with vi, make, basic, sql, sed, bc and a native cc
since M7), except chgrp, mknod (the system call is there, the command
is not), write, mail and wall.
The shell (sh): V6/Thompson-shell features first — commands with
arguments, < > >> redirection, pipelines |, ;, background &,
cd, exit, $PATH search (/bin, /usr/bin), wildcards * ? [...];
later variables, if/while, and scripts (Bourne-style subset). Status:
all of that and more is in: functions, case, here-documents, subshells,
#! scripts, history and Tab completion (sh(1)).
ed is the editor: it needs no screen control and works on a serial
terminal, as on V6.
8. Emulator support¶
p32emu(instruction level, fast) is the development platform; the microcode-levelp32uemuchecks the same images more slowly.- Two terminals in the emulator: UART 0 on the emulator's own terminal,
UART 1 on a pseudo-terminal or a TCP port (
-2 tcp:2323), so two people can log in at once on the Mac. - Scripted sessions for
make test: feed input to each UART from a file (with waits for prompts), record the output, compare it with a recorded transcript — the waymonitor.expectworks today. - A per-process trace (system calls, faults, context switches) for debugging.
9. How it is tested¶
| Layer | Test (in make test) |
|---|---|
| Assembler objects, linker | link tests: cross-file calls and data, relocations at the limits, archives |
| C compiler | the C test suite against expected output and the host compiler |
| C library | unit programs in p32emu (strings, printf, malloc) |
| Kernel | scripted sessions on a fresh image: boot, login on both terminals, the commands, pipes, background jobs, permission refusals, a program that faults is killed while the others run, fork until memory is full and recover, fsck clean afterwards |
| Filesystem | p32fs fsck after every session; the kernel and the host tool must agree on every image |
| The real machine | the same image on a CF card; booting to login: is the bring-up goal |
10. Effort and phases¶
| Phase | Delivers | Size (rough) |
|---|---|---|
| M1 | relocatable assembler, linker, a.out |
~1,000 lines Python |
| M2 | p32cc (stages 1–3) and its test suite; the C library |
~5,000 lines Python, ~2,500 C |
| M3 | kernel I: boot, console, processes, fork/exec/wait, the filesystem read path, p32fs, a shell on one terminal |
~5,000 lines C |
| M4 | kernel II: writing files, pipes, signals, both terminals, users and permissions, init/getty/login |
~3,000 lines C |
| M5 | the phase-1 commands, ed, the on-machine fsck; manual pages |
~6,000 lines C |
| M6 | copy-on-write fork, shared text, phase-2 commands, mount |
done |
| M7 | the native toolchain (D24): as, ld, ar and cc in C, running on P32W Unix; cc compiles itself there |
~8,000 lines C |
M7 — the native toolchain (D24)¶
Programs are built on the host today (cc/p32cc.py, assembler/). M7 puts
the toolchain on the machine, written in C and compiled by p32cc:
as— the assembler, reading the same syntax asp32asm.pyand writing relocatable objects;ldandar— the linker and archiver, with the same layout rules asp32ld.py(a.out, data on the next page). Objects and archives get a compact binary format (the host's JSON is too bulky to parse on a 0.45 MIPS machine); the host tools learn to read and write it too, so either side can link the other's output.cc— a C rewrite ofp32cc, with the same design (token preprocessor, recursive-descent parser, the same code generator and ABI) so one test suite checks both; a driver runscpp→cc1→as→ldwith/usr/includeand/usr/lib/libc.aon the disk.- The test: the host and native tools produce byte-identical objects
and executables for every program in
cc/tests/and the library; then self-hosting —cccompiles its own source on P32W Unix and the result is identical to the host-builtcc.
Before it: keeping scalar locals in callee-saved registers in the code
generator (both compilers), since compile time on the machine depends on
the quality of the compiler's own code. Done in p32cc (2026-10-14):
up to four scalar locals or parameters whose address is never taken (the
most used, uses inside loops weighted ×8, at least 3) live in r13–r10;
reads use the register in place, an assignment's result is not copied when
unused, and a function that then runs short of temporaries is generated
again with fewer. Session 1 (boot, login, 31 commands, halt): 50.3 M →
35.9 M instructions (−29 %); the kernel 62,344 → 57,172 bytes.
P32CC_NOREGVARS=1 turns it off, for comparisons. The C cc (step 3)
does the same.
Step 1 (as / ld / ar) done (2026-10-14): objects and archives are binary
(the format is specified in assembler/p32obj.py; the host tools write it
and still read M1's JSON; p32obj.py dump shows one as JSON).
software/cmd/as.c, ld.c and ar.c (with objfmt.h) are the host tools
in C; the ISA table they use (software/include/p32isa.h) is generated
from p32isa.py like p32opc.h. Checks: built on the Mac, the C tools give
byte-identical output to the Python ones for all 104 assembly files of the
system (libc, commands, kernel, tests), all 62 command links and libc.a;
on P32W Unix (software/tests/toolchain_test.sh, in make test) they
assemble, link and archive compiler output identically to the host, and
the program runs. Speed on P32W (emulated time): as of sort.s (≈ 900
lines) 4.2 s, ld of echo against libc 5.2 s. (p32cc does not yet
return structs from functions; as.c passes results by pointer.)
Steps 2 and 3 done (2026-10-14): M7 is complete — P32W is
self-hosting. software/cc (lex.c cpp.c type.c parse.c gen.c main.c,
≈ 3,300 lines with cc.h) is p32cc in C, stage for stage; built on the
Mac it writes the same assembly as p32cc for all 106 C files of the
system and the same -E text. On P32W Unix cc (with /usr/include,
/usr/lib/crt0.o, /usr/lib/libc.a) compiles, assembles (as) and links
(ld) programs that run, and compiles its own source into a cc
byte for byte identical to the host-built one (software/tests/cc_test.sh,
in make test). Each .c file is compiled in a process of its own (as
cc1), so each starts with a whole address space: the largest, parse.c, needed
3.1 MB of heap (1.6 MB since D29: each function is generated as soon as it
is parsed, and its tree and tokens freed). Self-hosting costs about 1.0 × 10⁹ instructions: 16 minutes
at the emulators' 1 MIPS, about half an hour on the hardware.
Each phase ends with make test green and a push.
11. For review (D22)¶
- The ABI (§3.3): arguments in r1–r4, result in r1, r8–r13 + r14
callee-saved,
charsigned. - The user address space: 8 MiB per process (D34) with a two-frame page table and a one-load TLB refill; page 0 unmapped.
- The C subset: C89 without floating point, staged as in §3.3.
- Kernel model: V6 — non-pre-emptive kernel,
sleep/wakeup, copy onforkfirst (copy-on-write later), no swapping. - Filesystem: 1 KiB blocks, 64-byte inodes with 8 direct + single +
double indirect, a free-block bitmap, 28-character names, no journal
(
sync+fsck). - Users: V7 passwd/group files, real/effective IDs, setuid programs, a simple salted hash for passwords.
- Timer at 50 Hz for the scheduler and the clock; no real-time clock
for now (
dateis set by hand, or by a later RTC). - Start with M1 + M2 (the toolchain): nothing in the OS can start until C works.
(This list is the proposal as reviewed. Since then: copy-on-write fork
and shared text are in (M6); a real-time clock is on the I/O card (D23), so
the time survives power-off; the signal list is V7's, without SIGCHLD.)