Documentation
pdp11-libc
tool. A single libc.a and one crt0 serve every universe; the era is chosen when a program is linked, not when the library is built. This is the vax11-libc mechanism, ported to the PDP-11.
One universal C library for the PDP-11 cross toolchain — not a host
tool. A single libc.a and one crt0 serve every universe; the era is
chosen when a program is linked, not when the library is built. This
is the vax11-libc mechanism, ported to the
PDP-11.
make libc # from the repo root: builds lib/libc.a + crt0
Products (installed flat, not per-universe): libc.a, the crt0 flavours
crt0.o/mcrt0.o/fcrt0.o/fmcrt0.o, and the matched header set into
include/.
How one library serves every era
-
ldstamps__univ.pdp11-ld, reading--universe/$PDP11_UNIVERSE, defines the absolute symbol__univ= the era id (28, 29, …) in the executable.--universedoes only this — it does not pick a startup file or an archive path (onecrt0, onelibc.aserve all). A disagreeing input definition fails the link. -
crt0records it. The first thingcrt0does is copy$__univinto a data cell (extern int __univ;) the library can read. -
Stubs branch on it. Each syscall stub is a multi-convention stub that tests
__univ— a run-timeifon a link-time constant — and serves three families from one archive:- the V7 family (V5, V6, V7, 2.8BSD, 2.9BSD) — the inline/indirect
systrap convention, V7-era numbers (the pristine body); - the 4BSD family (2.10BSD, 2.11BSD) — args already on the C stack, the
4.x numbers (stat 18→38, fstat 28→62, …). For
__univ >= 210the stub traps directly with the era number before the V7 frame is ever built; - the First Edition family (V1 and V2,
__univ1 or 2) — the 1972 convention: the fd (if any) in r0 and the remaining args as inline words after the trap, which for a C call are run-time values, so the stub patches those words before trapping (self-modifying text; the 11/20 had no split I&D).pdp11-ldemits the 0405 format (12-byte header, code at 040014); apsim emulates the 11/20's missing EIS andsetd, so the samelibc.a—printfand all — runs on 1972 First AND Second Edition UNIX.
The 4BSD/V1 halves are machine-generated:
tools/mkdual.pywraps each pristine V7 stub, reading the 4.x numbers fromtools/sysnums.tsv(extracted from the target trees' ownsyscall.h) and the V1 inline shapes from a small table. The battery (hello/arith/str/float) compiles once per universe and runs under all twenty (oracle/cross-universe.sh). The 2.10/2.11 calls that diverge from V7 are all handled now:stat/fstat/lstattrap the 4.x number into a 52-byte 4.3-shape scratch and repack it into the one universal V7-shapestruct stat(so the header stays flat — no per-universestat.h, the edge vax11-libc left open);waitbecomeswait4(status through the pointer, not r1);exithands the status off on the stack.forkandpipekeep their V7 numbers and two-value returns across the renumber, so the pristine stub is already correct. Under V1/V2 the covered set is the common syscalls (write/read/open/close/creat/unlink/lseek/chdir); the rest stay V7-shaped there. - the V7 family (V5, V6, V7, 2.8BSD, 2.9BSD) — the inline/indirect
Source layout
The sources are 2.9BSD's libc (a functional superset of 2.8BSD — identical
syscall numbers, superset errno/signal, one struct stat layout), split
by what kind of code a file is, not by universe:
common/gen ABI-independent C + asm: string, ctype, malloc, qsort, ...
common/stdio the buffered-I/O layer (one UCB_LINEBUF buffering model)
common/sys the syscall stubs (assembled against common/include/sys.s)
common/nonfpcrt software long-arithmetic (EIS) + csv/cerror -- the no-FPU
default; fpsim (the FP interpreter) rides along
common/csu crt0 and its variants (each stamps __univ at startup)
include/ the matched header set, installed flat into ../../include
fpsim/ the FP interpreter
Byte-for-byte reproduction of the native per-era libc.a is deliberately
not a goal (that was the old per-universe build), so the archive member
order is free. Note this made the fake _cleanup/fptrap fallbacks
(fakcu/fakfp) unnecessary and, once member order stopped being
load-bearing, harmful — they are excluded so the real _cleanup (which
flushes stdio on exit) and the real fptrap always win.
Documentation
- docs/porting.md — the libc porting guide.
Build and test
make libc # from the repo root (needs the tools built first)
make check # exercised by the end-to-end suite (a bsd28 AND a bsd29
# re-run of every correctness program, from the one libc.a)
# and oracle/cross-universe.sh