Building & Installing a Kernel¶
Amix has a monolithic SVR4 kernel with no loadable modules ✅ — every driver is statically
linked in. To add a driver (or change anything kernel-level), you drop an object file into the
/usr/sys tree, register it in the kernel switch tables, relink the kernel with make, copy
the result to /stand, and write it into the boot partition with make bootpart. Then reboot.
The short version, on a live 2.1 system, is:
# 1. add your driver .o to its subdir Makefile, edit master.d/kernel.c (see below)
# 2. clean stale objects, then relink
rm -f amiga/config/unix.o master.d/exp unix # remove stale objects
make # -> relocunix
# 3. install the new kernel and write the boot partition
cp relocunix /stand
make bootpart KERNEL=relocunix
# 4. create the device node and reboot
mknod /dev/<name> c <major> <minor>
shutdown -i6
This page is the mechanical build-and-install procedure. For what a driver is and the table schema you edit, read the driver model overview first. For a complete real patch set used as the running example here, see the VA2000 framebuffer case study.
The /usr/sys tree¶
The kernel is not shipped as one big source tree you compile from scratch. It ships as
precompiled object libraries under /usr/sys ✅, and you relink them with your additions.
Some .o files come with source, others are object-only ✅ — you cannot rebuild the
object-only ones, only link against them.
What is in source and meant to be edited:
master.d/kernel.c— the kernel configuration file holding the device switch tables (cdevsw[]/bdevsw[]), the interrupt tables (int2_tbl[], plus a level-6 table), and the boot-time init table (init_tbl[]/io_init[]). Provided in source ✅. (Ditto paper, "Adding a device driver.")- per-subdirectory Makefiles — e.g.
amiga/driver/Makefile, that link the object files in each subtree into the kernel. You add your driver's.oto the relevant one. ✅
Note: The exact subdirectory names below come from the modern repos (the VA2000 patch set), not the 1990 paper, so treat the layout as ✅ for 2.1 systems and the general procedure as the paper's ✅ spec.
Relevant subtrees seen in the VA2000 patch set (Amix 2.1) ✅:
| Path | Role |
|---|---|
master.d/kernel.c |
switch tables + extern decls + init table |
amiga/driver/Makefile |
links driver objects (add your .o here) |
amiga/console/scrdev.c, c0.c, screen.c |
console / screen-device sources (RTG example) |
amiga/config/unix.o |
a generated object — delete before relinking (see below) |
master.d/exp |
a generated symbol/export file — delete before relinking |
unix |
the previous link output — delete before relinking |
/stand |
where the bootable kernel image is staged |
Step 1 — add your driver object to a subdir Makefile¶
Compile your driver natively (a single-file char driver builds with the SVR4 cc, e.g.
cc va2000.c ✅ for the VA2000), then add the resulting .o to the Makefile in the subtree it
lives in. For the VA2000 the install script patches amiga/driver/Makefile to add va2000.o ✅.
Keep the driver source in the tree too, so the object can be rebuilt later — the paper recommends
placing the driver .o and ideally its source in a subdir under /usr/sys and adding it to
that directory's makefile ✅.
STREAMS drivers like Hydra build natively too — make in the driver dir, then make force to
relink (elf2brel converts the kernel to boot format as part of that); see the
Hydra network-driver case study and
writing a STREAMS driver. The rest of this page covers the
native path.
Step 2 — edit master.d/kernel.c (the switch tables)¶
Register the driver in the table(s) it belongs to. The switch tables are indexed by major number ✅ — the slot you put your entry points in is the device's major number.
What you typically add, in order (the VA2000 patch does exactly this) ✅:
externdeclarations for your entry points, near the top.- A
cdevsw[]slot (character driver) — for the VA2000, slot 68 ✅ — wiringopen/close/read/write/ioctl/mmap/poll, withnodev/notty/nostr/nullflagfor the entry points you don't implement. - A
bdevsw[]slot instead, if it's a block driver (core entrystrategy()). - An interrupt-table entry in
int2_tbl[]if the device raises a level-2 autovector interrupt (e.g. alongsideparintr,a2090intr,a2091intr) ✅. - An init-table entry in
init_tbl[]/io_init[]if the driver needs one-shot boot-time init — for the VA2000,va2000initis added toio_init[]✅.
Warning (ordering): the
externdeclarations must precedeio_init[]inkernel.c, or the build fails ✅ (VA2000 gotcha).Warning (pre-POSIX shell): if you script these edits, remember Amix
/bin/shis pre-POSIX — no$(...)command substitution and nogrep -q✅. Use backticks and redirect to/dev/nullinstead. This bites anyone porting a modern install script.
See the driver model overview for the full cdevsw/bdevsw struct layout and
the meaning of nodev/notty/nostr/nullflag.
Step 3 — clean stale objects, then make¶
Always remove the generated artifacts of the previous link before relinking, or you can link a stale kernel ✅:
Then relink:
A successful make produces the bootable kernel image. The name has changed across versions 🟡:
- The 1990 Ditto paper calls the kernel image
rdbunix✅. - Modern 2.1 systems and the repos call it
relocunix✅ — a historical rename.
So on a 2.1 system you are producing relocunix. (The repos run make install and treat the
output as relocunix; the VA2000 script does make install → relocunix ✅.) If you are on an
earlier release, verify which name your tree emits 🟡 — see the
open question on the kernel-image name.
Step 4 — install the kernel and write the boot partition¶
The kernel that actually boots lives in the boot/bootstrap partition (a ~2 MB partition the
installer creates, BOOTSIZE=2 MB ✅), not just on the filesystem. Two steps:
cp relocunix /standstages the new image where the boot tooling expects it ✅.make bootpart KERNEL=relocunixwrites the kernel into the boot partition so the Superkickstart bootstrap can load it on next power-on ✅.
This matches the install-time flow: the installer builds/patches the kernel, then runs
make bootpart KERNEL=relocunix to write the boot partition ✅ (reconstructed from the root.adf
install scripts). For how the bootstrap then finds and decompresses that kernel, see the
boot process.
Always keep the old
/unixas a fallback ✅. The paper's procedure explicitly says to retain the previous kernel so you can boot it if your new one panics. Don't overwrite your only known-good kernel. (Note the distinction:/unixis the on-disk kernel file; the bootable copy is whatmake bootpartwrites — preserve a working one of each.)
Step 5 — create the device node¶
A driver does nothing until there's a /dev node with the matching major/minor. The kernel keys
only on the numbers, not the name ✅:
mknod /dev/<name> c <major> <minor> # character device
mknod /dev/<name> b <major> <minor> # block device
Concrete, from the VA2000 patch set ✅:
c = character, b = block; the major must equal the cdevsw[]/bdevsw[] slot you edited in
Step 2. (Reference major numbers: the SCSI hard-disk block driver is major 18, the parallel
port char driver is major 21, the floppy block driver is major 16 ✅; see the
device list.)
Step 6 — reboot¶
shutdown -i6 brings the system to run level 6 (reboot) ✅. On the way back up the Superkickstart
ROM loads the kernel you wrote into the boot partition. If it panics, reboot and select your
retained fallback kernel.
Worked example — the VA2000 6-file patch set ✅¶
The asokero/va2000-amix char framebuffer driver is the
cleanest concrete instance of everything above. Its install script patches six kernel files and
then runs the build/install/reboot sequence:
| # | File patched | Change |
|---|---|---|
| 1 | amiga/driver/Makefile |
add va2000.o |
| 2 | amiga/console/scrdev.c |
RTG screen type |
| 3 | amiga/console/c0.c |
RTG screen type |
| 4 | amiga/console/screen.c |
RTG screen type |
| 5 | master.d/kernel.c |
extern decls + cdevsw[] slot 68 + va2000init in io_init[] |
| 6 | (node, not a file) | mknod /dev/va2000 c 68 0 |
Then, end to end ✅:
# (build the driver object first)
cc va2000.c
# clean + relink
rm -f amiga/config/unix.o master.d/exp unix
make install # -> relocunix
# create the device node
mknod /dev/va2000 c 68 0
# write the boot partition and reboot
cp relocunix /stand
make bootpart KERNEL=relocunix
shutdown -i6
The driver itself uses autocon() for Zorro II board discovery, uiomove(), and
copyin()/copyout() ✅. The full annotated walk-through is in the
VA2000 case study; for adding the same kind of driver to an install
floppy instead of a live disk, see
adding drivers to a boot disk.
Known pitfalls¶
- Stale objects. Forgetting
rm -f amiga/config/unix.o master.d/exp unixcan relink an old kernel ✅. externordering. Declarations must come beforeio_init[]inkernel.c✅.- Pre-POSIX
/bin/sh. No$(...), nogrep -qin build/install scripts ✅. - Kernel-image name.
rdbunix(1990 paper) vsrelocunix(2.1 / repos) — a historical rename; verify which your tree emits 🟡. - No fallback. If you overwrite your only working kernel and the new one panics, you have to
reinstall. Keep the old
/unix✅. - Object-only files. You can't rebuild the object-only
.os in/usr/sys; you can only link against them ✅. Anything that needs their source can't be changed by the community. - Boot-partition vs filesystem. A kernel sitting on the filesystem is not enough; it must be
written into the boot partition with
make bootpartto actually boot ✅.
The "D245 boot-breaker" — an intermittent ld corruption¶
This is a load-bearing gotcha for anyone relinking the Amix kernel, not just A4091 work. It was
first-party reproduced locally on Amix 2.1c under Amiberry. If you ever see a kernel Guru at boot
with D245 4C41, this section is the answer.
What it is not (a corrected misconception) 🔴¶
🔴 Kernels that grew past a certain size Guru'd at boot with D245 4C41, and the cause was twice
mis-attributed: first to the SCSI driver's completion poll loop, then to the bootstrap
relocator misbehaving. Both were wrong. The constant 0xD2454C41 decodes as
"RELA" | AT_DeadEnd — it is the bootstrap relocator's own Alert() in amiga/boot/rel.c,
fired when rel() is handed a corrupt kernel image to relocate. So D245 is a symptom of a
bad kernel binary, not a bug in the relocator or in any driver. ✅
The longword breaks down as: 0x52454C41 = ASCII "RELA"; setting bit 31 (AT_DeadEnd, the
"unrecoverable" flag exec ORs into a deadly alert) yields 0xD2454C41, displayed as D245 4C41 in
the Guru requester. ✅
What it actually is — an emulator MMU defect, not ld ✅ (root-caused 2026-07-26)¶
🔴 Superseded attribution. This page previously said the corruption was introduced "when ld
writes the linked kernel to disk". That was wrong, and so was every variant of it: ld is
innocent and computes correct output every time. The corruption is injected by the emulator,
inside the guest kernel's copy loop, while the guest is demand-paging.
✅ The mechanism. On the 68030, a bus-fault format-$B stack frame packs two unrelated fields
into the 16-bit word at frame offset 0x34: mmu030_state[2] in the low byte and the write-back
status wb3_status in the high byte. The emulator's RTE extracted wb3_status correctly but
restored the whole word into mmu030_state[2]. When the RTE's own retry access faulted
again — routine under heavy paging, rare otherwise — the next frame was built carrying the
stale wb3_status, so an (An)+ post-increment side effect that had already been undone was
undone a second time: a silent 4-byte rewind of an address register, mid-copy, with no
exception raised.
Every observed event landed at one guest PC: the Amix kernel's MOVES.L (A0)+ copyin loop. So
what actually happened is that the kernel's copy of ld's write() buffer was rewound — which
is why the damage looks like a linker bug, why cp/dd/FTP of the same bytes are always clean
(no paging pressure, no faults), and why real hardware never shows it.
✅ What the damage looks like (this supersedes the "~8 KB block shifted by 8 bytes" description,
which was one small-delta member of a wider family): every damaged region is a byte-exact
displaced copy of content from earlier in the same file. Per-region displacements measured
12–9012 bytes, always a multiple of 4, never a multiple of the guest page size (2048 B), and
constant across page boundaries — which is precisely what excludes every page-granular explanation
(lost dirty page, wrong swap slot, stale frame, recycled buffer). Damage begins at file offset
≡ 0x19f–0x1a4 (mod 2048) and ends on an 8192 boundary.
✅ The rate is configuration-dependent, and quoting one number is a mistake. At a3000mem_size=8
it reproduces at 85% of relinks; on the standing 16 MB bench config the same measurement
pooled 0/354 (95% upper bound 0.84%). Measurement itself suppresses it: adding I/O around each
link drove the observed rate 85% → 45% → 35% → 28%. Always state the capture mode with the rate.
✅ Fixed. Masking the restore to the field's real 8-bit width (both RTE variants) takes the
8 MB corruption rate from 55–59% on matched controls to 0/39, with the fault/paging traffic
unchanged — i.e. it removes the damage, not the workload. The defect was present in upstream WinUAE
as well as Amiberry; it was reported upstream and the maintainer's own fix (masking the local
immediately after the two halves are separated) compiles to byte-identical code and measured
40/40 clean.
✅ What was ruled out along the way, each by measurement rather than argument: the emulator's
disk stack (every guest write replayed byte-exactly and every logged read returned disk truth, at
both 8 and 16 MB), ld's inputs (byte-identical between a clean and a corrupt round), and the
MMU's own fault-resume machinery and descriptor M-bit handling.
The fix — build until the checksum is stable ✅¶
✅ A clean ld output is byte-deterministic; each corruption is random and unique.
Therefore a checksum (sum -r) that recurs is the deterministic clean kernel.
tools/build-clean-kernel.sh
(runs on the Amix box) relinks (link-only, no install) until a sum -r value repeats, then
confirms with tools/checkunix.c,
leaving a verified-clean relocunix:
sh /root/build-clean-kernel.sh # exit 0 = clean relocunix ready; 1 = could not stabilize in 25 builds
Never
make installan unverified kernel. A corrupt kernel written to the boot partition will brick the boot disk (see the safety rule on the boot process page). Always clean-gate first, and install onto a backup/throwaway disk. ✅
Two complementary detectors exist:
| Detector | What it checks | Trade-off |
|---|---|---|
tools/checkunix.c |
native big-endian .symtab integrity (flags out-of-range st_shndx) |
fast, runs on the box, but symtab-only — misses .rela/.text shifts ✅ |
tools/relsim.py |
host-side reimplementation of the boot relocator rel() |
checks .symtab and relocation records — a kernel-record oracle ✅ |
Scope correction (2026-07-16) ✅: relsim.py (and the native reltest) validate the boot
relocator's source semantics against the kernel's records — they are kernel-record oracles,
NOT will-this-disk-boot oracles. The on-disk boot2 loader is a free variable outside their
scope: a relsim-green kernel still D245s under a name-based boot2 (see the two boot2
lineages).
Disk-level assurance needs a boot-chain check: the build pipeline laminates the proven
flags-based boot2 into /stand/boot2.boot before the bootpart rebuild and fail-closes on
bootchain-verify.py (RDB walk → UNI\0 slice → boot1@+0x000, IBLK@+0x400,
boot2@+0x600 sha check, IBLK@+0x2600, kernel ELF@+0x2800; ib_chksum = folded 16-bit
SVR4 sum). The first golden gated this way validated 5/5 on the real A4000+Z3660 (2026-07-15) ✅.
Measured rate (2026-07-20) ✅: a fixed-N measurement on a RAW (non-VHD) disk image put the
emulated relink-corruption rate at 85% (17/20 rounds) — confirming the historical ~70%
figure and refuting the hypothesis that dynamic-VHD block-remapping explained it. Every
linked-but-corrupt round kept nm -h -u empty: the ~8 KB block-shift never perturbs the
symbol table, so sum-recurrence is the load-bearing arm of the gate for SCSI/combined
kernels (the nm-empty arm covers the larger cdfs kernel's distinct silent-symbol-drop mode).
One round in 20 was the separate intermittent ld "Unresolved Symbol" nolink mode, which the
gate's retry already handles ✅. (That 85% is the 8 MB figure; see the configuration
dependence above.)
Oracle coverage, scored against 21 real captured corruptions (2026-07-25) ✅. This is the number that matters when choosing a gate, and it is uncomfortable:
| Oracle | Caught |
|---|---|
nm -h -u (undefined symbols) |
0 / 21 |
checkunix (symtab st_shndx) |
7 / 21 |
relsim / relocation-record analysis |
15 / 21 |
| byte-diff against a known-good link | 21 / 21 |
A symbolic two-arm bar (nm + checkunix) passes 14 of those 21 — it would have declared
two-thirds of them STABLE. Adding a relocation-record arm takes that to 6. It does not take it
to zero, because a pure .text-content displacement is invisible to every symbolic check.
Byte-diff against a reference link is the only complete oracle (and where a gate can use it,
sum -r recurrence also scores 21/21 — every corrupt link had a unique checksum — which is why
that arm stays load-bearing on the kernels where it converges). Read a green symbolic gate as "not
corrupt in any way visible from here", never as "byte-correct".
Note also that relsim-class tools must fail closed: a kernel whose section-header table is
damaged (the dominant shape — the table is the last ~400 bytes of relocunix and gets overwritten
wholesale) can crash the analyser rather than being reported as corrupt.
Who is exposed to a cross-toolchain bug — the build-model boundary ✅¶
When the cross toolchain's tdivs divide-with-remainder mis-assembly was found (see toolchain), the blast radius was bounded by the build model, and the boundary is worth recording ✅:
- The kernel objects and all native drivers never carried the bug — the harness is a
source-push + compile-on-box model; the box's own SGS
asassembles them. - Only host cross-built artifacts were exposed: the SVR4 pkg engine, the cross
libgcc.athat a cdfs-carrying kernel links for its 64-bit soft-arithmetic (__udivdi3/__umoddi3/__lshrdi3), and the cdfs kernelexprelocatable. - A plain SCSI / SCSI+net kernel links zero cross artifacts and was end-to-end clean.
The practical consequence closed a long-open metal mystery: the kernel's __udivdi3 returning
an internal normalization intermediate on real hardware (garbage st_blocks from cdfs) was the
mis-assembled 64-bit-dividend form inside the old cross libgcc.a. After the toolchain fix and
a libgcc/exp rebuild + kernel relink, the same machine + same disc read every value correctly,
and a userland exerciser statically carrying the rebuilt __udivdi3 passed on metal with the
exact dividends that used to fail (2026-07-21) ✅. Standing style rule stays: prefer shifts to
64-bit division in Amix kernel code — now as a speed/robustness preference, not a correctness
workaround ✅.
Build checkunix natively (cc -O -o checkunix checkunix.c) and run it on the box; run relsim.py
on the host against a pulled kernel ELF:
Build-system corollary — make does not reliably recompile a changed .c ✅¶
✅ Plain cd /usr/sys; make does not reliably pick up an edited source file: the per-subsystem
exp prelink chain has incomplete dependencies, so a changed .c (or a changed /stand/CONFIG)
is silently ignored. After editing, for example, amiga/kernel/support.c, you must rm the stale
.o and the subsystem exp and amiga/exp before make:
cd /usr/sys
rm amiga/kernel/support.o amiga/kernel/exp amiga/exp # stale .o + subsystem exp + top-level exp
make
Then confirm the change took effect by the kernel sum changing — if sum -r relocunix is
unchanged, your edit was not compiled in. ✅
Two more SVR4-box gotchas that bite build scripts ✅:
/tmpis wiped on reboot — keep build scripts and saved checksums in/root, not/tmp.- SVR4
grephas no\|alternation — use separategrepinvocations instead of one alternation pattern. (This is in addition to the pre-POSIX/bin/shlimits noted above.)
The A3000 onboard-SCSI DMA bounce patch — nearly every kernel needs it ✅¶
If your kernel boots by reading its root disk through the A3000 onboard SCSI, it needs the
a3091.c chip-mem DMA bounce patch — whatever else is in it. ✅ This is not an A4091 requirement
and not a cdfs requirement, though it was documented as both for months: a3091.c is the A3000
onboard SCSI driver (super-DMAC + WD33C93 at 0xDD0000), it is in the stock amiga/alien Makefile,
and it links into every kernel. It lives under amix-a4091/src/kernel-patches/ only because that
project did the work ✅.
a3091.c.patch (commit e70c1d7; patch -p0 on amiga/alien/a3091.c) adds a high-address DMA
bounce: any buffer at or above 0x1000000 is copied through a per-unit chip-RAM buffer, which is
always DMA-reachable. Stock startdma() sets device->sac = cp->addr — the caller's 32-bit buffer
address — directly, with no bounce, unlike the A2091-card sibling a2091.c, which already bounces
any buffer ≥ 0x1000000 (16 MB) through AllocMem(cp->tc, MEMF_CHIP) (copy-out before a write,
copy-back after a read, freed in stopdma). The patch ports that same chip-mem bounce into
a3091.c ✅.
Who is exposed ✅:
| Rig | Roots through | Needs the patch? |
|---|---|---|
Any emulated bench box (c6d0s1, card 0) |
A3000 onboard SCSI | Yes — regardless of which add-on HBA the kernel carries |
| A4091 bench profiles | A3000 onboard SCSI (A4091 sorts to card 1) | Yes |
| Real A4000 + Z3660, root on the piscsi mailbox | the Z3660 | No — a3091 is linked but never carries the root read |
That last row is why the defect survived so long: the metal-proven kernels were never exposed to it, so a build path that silently skipped the patch produced kernels that were fine on real hardware and unbootable on every bench box ✅.
Build-harness note. A patch keyed to a driver repo is only applied when that repo is part of the build. Keying this one to
amix-a4091meantbuild-kernel.sh <some-other-driver>silently omitted it — a boot-critical patch skipped by an unrelated repo-selection rule. If your harness selects patches by repo, mark this one unconditional and fail closed when its file is missing ✅.
The s5mountroot VOP_OPEN error 6 panic — how to read it ✅¶
Without the bounce patch, a kernel that roots through the A3000 can fail like this ✅:
s5mountroot VOP_OPEN error 6
WARNING: nfs_mountroot called
PANIC: vfs_mountroot: cannot mount root: errno 30
Read the first line, not the last ✅. rootfstype is the empty string in every kernel here, so
vfs_mountroot iterates vfssw[1 .. nfstype-1], calls each row's mountroot op, stops at the first
that returns 0, and prints the last return value it saw. So:
errno 30is not diagnostic. It is whatever the lastvfsswrow returned — with cdfs linked that row is cdfs, and a read-only filesystem declining a root mount withEROFS(30) is unremarkable. On a kernel without cdfs the same panic prints whatevernfs_mountrootreturned. Don't chase errno 30 ✅.s5mountroot VOP_OPEN error 6is the whole story.s5is row 1, tried first.error 6isENXIOfrom opening the root block device, before any filesystem question is asked — soufs(row 2, the actual root type) fails identically one line later, silently, and the walk runs off the end. A healthy boot prints neither line ✅.
The mechanism ✅. ddopen() (amiga/alien/dd.c) has two ENXIO sources — sdopen() (no
controller on that card) and sdpartition() (the RDB/PART walk). sdpartition()
(amiga/alien/sdpart.c) reads the RigidDiskBlock into a file-static 512-byte buffer
(static union block block;) and dd.c's startio() hands the SCSI layer vtop() of that kernel
static as the DMA destination. With stock a3091.c the super-DMAC is pointed straight at it; when
that address is one the DMAC cannot reach, the transfer completes without an error and the buffer
reads back all-zeros — no RDSK, so getrdb() walks all 16 blocks and returns ENXIO.
Savestate-diff corroboration ✅ (independent, 2026-07-10): comparing an Amiberry savestate of a
booting kernel against a panicking one, the same RDB buffer held a valid RDSK/PART block named
UNIX_Root in the booting kernel and was all-zeros in the panicking one, while queue[0].f (the
registered card's dispatch function) was populated in both — so the card is registered and
autoconfig is fine; the DMA read itself is what fails.
It is not a size threshold — measured ✅¶
The tempting story is that cdfs (~64 KB) pushes the kernel past a size limit. It does not. Section
sizes read out of four linked kernels, with the load-image offset of sdpart.c's block ✅:
| kernel | drivers / patches | loaded image | block at |
boots? |
|---|---|---|---|---|
64119 |
z3660scsi, stock a3091 | 1 016 636 | 0x0EB924 |
yes |
56225 |
z3660scsi + cdfs, stock a3091 | 1 087 259 | 0x0FCD04 |
PANIC |
01159 |
a4091 + cdfs, patched a3091 | 1 151 887 | 0x0FCD78 |
yes |
55619 |
z3660scsi + cdfs, patched a3091, stock scsi.c |
1 087 911 | 0x0FCF90 |
yes |
Read the last row against the second: same builder, same drivers, same cdfs object, a loaded image
652 bytes larger — the only substantive difference is that a3091.c bounces — and it boots. Both
booting cdfs kernels also put the RDB buffer at a higher address than the panicking one.
So the failure is not a size threshold, and not "the buffer moved up" as such. It is that with a direct DMA the buffer's address matters at all ✅. With the bounce it does not, at any address — which is why every patched kernel boots from a higher buffer address than the unpatched one that fails.
The image is not a variable either: a 2×2 over two different base images and two kernels reproduces
the panic text and its backtrace addresses exactly in both panicking cells, and a host-side diff
of the two images' whole /usr/sys is byte-identical except amiga/alien/ ✅.
Unlike the D245 boot-breaker (a random emulator-injected corruption, covered above), this failure is deterministic: the same kernel fails the same way every time.
Distinct from the error-5 panic. This is
VOP_OPEN error 6(ENXIO— a super-DMAC high-address DMA failure). Thes5mountroot VOP_OPEN error 5(EIO) seen on an A4091-only machine was a different fault — the "phantom A3000" device-dispatch bug, where the root read went to a non-existent internal SCSI — see the boot process and theautocon()phantom-A3000 special case. Don't conflate them.
🔴 Open: the exact reach limit of the super-DMAC is uncharacterized. The ≥ 0x1000000 (16 MB)
threshold is a2091's proven-safe cutoff, not a measured a3091 boundary — and the failing buffer
sat around VA 0x078F0000, well above 16 MB, so a plain 24-bit-address story is incomplete. Treat the
bounce as a safe over-approximation pending real-hardware measurement.
The separate GSIO scsi.c.patch — userspace only ✅¶
amix-a4091's scsi.c.patch is a different thing and is not a boot requirement: it enlarges
the userspace /dev/scsi GSIO bounce iobuf to 64 KB ✅. The GSIO path bounces every transfer
through that iobuf, which is why userspace /dev/scsi reads can be multi-sector even though the
in-kernel SCSI path cannot (see the DMA gotcha below). The in-kernel cdfs read path goes through
sdqueue and does not need it ✅. A kernel that boots without scsi.c.patch but with the
bounce patch has been built and booted, which is what isolates the two ✅.
⚠️
scsi.c.patchplus a patcheda3091is a latent userland-reachable panic. With the 64 KB iobuf,gsioctl()accepts transfers up to 65 536 bytes; on a bench box all Fast RAM sits at0x07000000, i.e. above the patch's>= 0x1000000bounce threshold, and a transfer larger thanBOUNCESZ(one 4 KB page) reachespanic("a3091: DMA too big"). The patch's own comment assumes GSIO "bounces through a low kernel iobuf that never trips the>=0x1000000test", which is not true on this memory map ✅ (the code facts) / 🟡 (the reachable panic — reasoned from source and the bench memory map, not yet reproduced). It ships today in the a4091 bench kernels.
Two kernel gotchas that bite any module (found porting cdfs)¶
Both were hit while porting cdfs, but both are general — any kernel module doing the same thing is exposed ✅.
Gotcha: the kernel's bzero under-clears a region larger than ~1 KB ✅¶
Zeroing a buffer larger than ~1 KB with the SVR4 kernel's bzero — or with a memset that
delegates its c == 0 case to bzero — leaves the tail uncleared (stale garbage) on this m68k
build ✅. It bit cdfs twice from a single ~1.1 KB struct clear: an uncleared has_child_link
clobbered a filesystem node's extent (empty/garbage root), and an uncleared is_relocated made the
ISO directory walk skip every child (empty directory). Fix: a memset that clears with its own
byte loop (amix-cdfs ad09035). Any module relying on bzero/memset to clear a > 1 KB buffer
is exposed — audit for it. (Working theory: a size/alignment/int-truncation bug in bzero around
~1 KB.)
Gotcha: in-kernel SCSI DMA corrupts transfers larger than one 2048-byte block ✅¶
On this board an in-kernel SCSI transfer (struct sdcom + sdqueue()) of more than one CD
sector (2048 B) comes back as kernel text / garbage and can wedge the box; single-block
(2048 B) transfers are reliable (PVD, mount reads, and RDB reads are all single-block and fine) ✅.
cdfs's block-cache read-ahead issued one 16 KB (8-sector) DMA → garbage → empty/garbage directory +
hang; the fix caps the media transfer to one sector and lets the chunk loop split multi-sector
requests (amix-cdfs a5c6915). This is the same DMA-limitation family as the a3091 super-DMAC
high-address bounce above.
The userspace /dev/scsi GSIO path is not affected — it bounces through the 64 KB iobuf
(scsi.c.patch) and does do multi-sector reads; this limit is specific to the in-kernel sdqueue
path ✅. 🔴 Open: the real HBA/DMA limit is uncharacterized, so the one-sector cap can't yet be
raised.
The concrete in-kernel cdfs fixes for both gotchas above live in the amix-cdfs repo — the byte-loop
memset(ad09035) and the one-sector DMA cap (a5c6915).
make install does not update /stand/unix — copy it yourself ✅¶
A successful cd /usr/sys; make install prints installed. Old kernel kept as /stand/OLDunix but
was observed to leave /stand/unix still holding the previous kernel ✅ — confirmed by
sum -r: /usr/sys/relocunix was the freshly-built image (checksum 44416) while /stand/unix was
still the old one (38553). Because the Amiga bootstrap boots the boot-partition image written from
/stand/unix, rebooting at that point silently re-boots the old kernel and any "verified on the
new kernel" result is worthless. Do the copy explicitly and rebuild the bootpart, then re-check:
cp /usr/sys/relocunix /stand/unix
cd /stand; make # (or: cd /usr/sys; make bootpart KERNEL=relocunix)
sum -r /stand/unix # must now match sum -r /usr/sys/relocunix
This is the same hazard the D245 clean-gate guards
against, one step later in the pipeline: there sum -r proves the link is clean; here it proves the
image you're about to boot is the one you just built. ✅
Verify which kernel actually booted 🟡¶
Warm reboot (shutdown -i6) was intermittently a no-op in one bench session — the box stayed
multiuser on the old kernel — while cold boot (Amiberry down/up, which reloads the boot
partition) was reliable 🟡. Always identity-check which kernel actually booted (a version marker, or
sysfs(GETFSIND, …)) before trusting a test result.
Scripting the kernel.c edits with ed — the half-patch trap ✅¶
Sooner or later everyone automates Step 2: a script pushes an ed heredoc that inserts the
extern declarations, address-searches the target cdevsw[] row, changes it, and writes the
file. This section is uniformly first-party ✅ — every failure mode below was reproduced with GNU
ed over fixture copies of the real stock file layouts (2026-07-30/31), and the fail-closed
pattern at the end is what this family's own build tooling now enforces. It applies to any stock
file you patch in place — master.d/kernel.c, master.d/filesys.c (vfssw[]), the subdir
Makefiles — and, in general, to anyone patching SVR4 sources with ed.
A missed address search is not a no-op. ed prints its famous ? — but the commands that
already ran stay applied to the buffer, and the trailing w still writes the file. In the
canonical two-edit body that means the extern insert lands, the row change silently never
happens, and kernel.c is now half-patched: the driver links into the kernel while its
cdevsw[] slot still holds the stock filler. Nothing fails to build, nothing panics; the only
symptom is a device that never answers.
/ANCHOR-A/a <- edit 1, the insert: applies
(new extern line)
.
/ANCHOR-B/c <- edit 2, the row change: on a miss, ed prints ? ...
(replacement row)
.
w <- ... and the write still happens: a half-patched file
A name-wide idempotency guard then makes it permanent. The natural wrapper guard — "the
driver's name is already somewhere in the file, skip" — is satisfied by the surviving extern
insert alone, so every later run reports already-patched and skips the repair, forever. The
half-patch is invisible to the very tooling that created it.
Three measured variants of the same trap ✅:
- The wrong-row overwrite. An address that searches for the row's
/*N*/comment tag changes whatever line carries that tag. A claimant that kept the bare tag (community trees do — see the registry's collision notes) is overwritten outright, with no error. - The renumber skew. After a driver changes major number, a name-wide guard running on a tree
integrated before the renumber sees the old claim and no-ops — while rebuilt
/devnodes open the new major. Kernel on the old slot, nodes on the new one, every build and every symbol check green; the only runtime evidence is a dead device. This exact shape was produced by this family's own tooling whenz3660ethmoved 48 → 51, and is now mechanically refused (a "name found at a different row" probe outranks every other verdict). - The wrapping search.
ed's/re/address search wraps around the buffer, so an address computed as "the first TAB-indented};after the anchor" can resolve inside a different switch table once the file drifts. Measured: avfssw[]row landed insidefmodsw[]— exit status 0, no message.
Exit status is a weak oracle. Check $? on every ed invocation, but never let it be the
only gate: behaviour across ? errors is not something to build on — the two eds in play (GNU
host-side, SVR4 on the box) do not agree, wrappers habitually print "patched" unconditionally, and
a remote-shell transport that swallows the on-box exit status turns every other guard silent ✅.
The reliable mechanism is verification in the file itself, before and after the write.
The fail-closed pattern the family's builders converged on ✅:
- Pre-probe before any write (verify-then-apply). Probe every anchor the script depends on, row-scoped, not name-wide, and classify the file: target row free / already exactly ours (an idempotent re-run — proceed) / taken by another claimant / our name present at a different row / insert present but row absent / anchor missing. Refuse everything but the first two, with the diagnosis and the remedy spelled out.
- One
edinvocation per edit,$?checked. Never pair an insert and an address-searched change in one body — that pairing is the half-patch generator. - Post-verify the row, in the file. Read it back and confirm the expected content sits inside the intended table's span — a row can land in the wrong table with rc 0.
- Repair or refuse half states. Insert-present-row-absent gets repaired, or refused with the exact remedy — never absorbed into "already patched".
- Propagate refusal through every layer, so the calling builder actually stops instead of shipping the kernel anyway.
- Prove it with no box. Run the same script over fixtures cut from the real stock files and reproduce each failure mode first; a selftest that lifts the expected row out of the real patch script fails on drift instead of rotting quietly.
This is the scripted-edit companion to the registry's manual rule — match the row by its tag and refuse anything but the free filler (conventions) — and to the pre-POSIX shell warning in Step 2. One-line checklist entry: quirk 28.
See also¶
- Driver model overview — what
cdevsw/bdevsw, majors/minors, and the switch tables are. - VA2000 framebuffer case study — the full 6-file patch set in detail.
- Adding drivers to a boot disk — same idea, but baked into install media.
- Writing a STREAMS driver and the
Hydra case study — the native on-box build (
make/make force) for a STREAMS driver. - Boot process — how the bootstrap loads the kernel you wrote.
Sources¶
- amix-kerntools briefs
boot2-d245-trap(2026-07-16),e2-relink-corruption-measured+tdivs-cross-assembler-miscompile(2026-07-21): relsim scope correction, bootchain-verify gate, measured 85% relink corruption, tdivs blast-radius map, __udivdi3 metal closure (kernel 10842→00961, validate-metal ALL GREEN 2026-07-21). - Ditto, Writing Amix Device Drivers, 1990 European Amiga Developer's Conference (project PDF;
see bibliography) —
/usr/sysobject libraries,kernel.cswitch tables, the "Adding a device driver" procedure,rdbuniximage name, keep-old-/unixrule. asokero/va2000-amixREADME/install script — the 6-file patch set,mknod /dev/va2000 c 68 0,rm -f amiga/config/unix.o master.d/exp unix,make install→relocunix,cp relocunix /stand,make bootpart KERNEL=relocunix, theextern-before-io_init[]and pre-POSIX/bin/shgotchas.amix_21_root.adfanalysis viatools/inspect-adf.sh— install-timemake bootpart KERNEL=relocunix,BOOTSIZE=2MB boot partition,shutdown -i6.- Master research brief §3 (boot process / kernel install flow), §4 (kernel architecture),
§5 (driver model / "Adding a driver"), §6 (VA2000 patch set), §13 (open questions:
rdbunix/relocunixrename). - The A4091-on-Amix project —
NOTES.md§17–§18 (theD245boot-breaker: relocatorAlert()mechanism, the intermittent ~70%ld-write corruption, the build-until-stable gate; reproduced locally ✅) and the handoff brief §5/§10, plussrc//tools/. tools/build-clean-kernel.sh— the relink-until-sum -r-recurs clean-gate that dodges theD245corruption.tools/checkunix.c— native big-endian.symtabintegrity detector (0x52454C41 | AT_DeadEnd==D245; build withcc -O -o checkunix checkunix.c).tools/relsim.py— host-side reimplementation of the boot relocatorrel(); the full offlineD245oracle (symtab and relocation records).- a4091.device open-source project: https://github.com/A4091/a4091-software (A4091 ROM + SCRIPTS assembler), referenced by the A4091-on-Amix work.
- amix-a4091 kernel patches
src/kernel-patches/scsi.c.patch(userspace/dev/scsiGSIO bounce iobuf → 64 KB) andsrc/kernel-patches/a3091.c.patch(commite70c1d7) — the A3000 super-DMAC high-address chip-mem DMA bounce for buffers ≥0x1000000, ported from thea2091.csibling; evidence = source + Amiberry-savestate differential (amix-a4091 CD-ROM-effort handoff, 2026-07-10), including the savestate pair showing a validUNIX_RootRDSK/PARTin the booting kernel vs all-zeros in the panicking one withqueue[0].fpopulated in both. - Scope correction (2026-07-26/27) ✅ — the amix-kerntools root cause
(
docs/a3091-bounce-boot-patch.md@05d0d78, fixf459afa). The bounce is required by every kernel that roots through the A3000 onboard SCSI, not by a4091 or cdfs kernels specifically:a3091.cis the A3000 onboard driver, is in the stockamiga/alienMakefile and links into every kernel. The panic is not size-triggered — a patched kernel with a 652-byte larger loaded image boots, and both booting cdfs kernels placesdpart.c's static RDB buffer at a higher address than the panicking one; the four-kernel section-size/block-offset table, the two-image 2×2 (identical panic text and backtrace addresses in both panicking cells; whole-/usr/sysbyte-identical exceptamiga/alien/) and thevfs_mountrootdisassembly (emptyrootfstype⇒ iteratevfssw, print the last errno ⇒ errno 30 is not diagnostic;error 6=ENXIOfromddopen→sdpartition→getrdb) are all first-party measurements. Real hardware rooting through a Z3660 is indifferent to the patch, which is why metal never caught it; the 2026-07-11 kernel52550that "stopped atvfs_mountroot" and was attributed to code staleness is the same defect. - amix-cdfs
ad09035— byte-loopmemsetreplacing the SVR4 kernelbzerothat under-clears a~1 KB region (tail left as garbage); it bit cdfs twice from one ~1.1 KB struct clear (
has_child_link,is_relocated). - amix-cdfs
a5c6915— caps the in-kernel SCSI (struct sdcom+sdqueue()) media transfer to one 2048-byte sector; a multi-sector in-kernel DMA returns kernel text / wedges the box, while the userspace/dev/scsiGSIO path (64 KB iobuf) is unaffected (Amix kernel-RE + gotchas handoff, CD-ROM effort, 2026-07-10). - Bench caveat (2026-07-10): warm reboot (
shutdown -i6) was intermittently a no-op (box stayed on the old kernel); cold boot reloads the boot partition reliably — always identity-check which kernel actually booted. - The amix-kerntools bench forensics @
8a76775—cd /usr/sys; make installprints "installed" yet leaves/stand/unixholding the previous kernel (proven bysum -r:relocunix44416 vs/stand/unix38553); copyrelocunixto/stand/unixexplicitly and rebuild the bootpart, then re-checksum -r /stand/unix. Real A4000 + Z3660, 2026-07-12 ✅. Same brief also confirms SVR4grephas no\|alternation (already noted above under build-script gotchas). - The amix-kerntools cdevsw free-row gate + 2026-07-31 half-patch audit fix packets
(
2b23808,1c6a57e,a81d7c9,f0c517c,f90756b,f025444,dac78a0) and the amix-z3660net patch-script hardening (603c719), 2026-07-30/31 ✅ — theedmiss-then-write half-patch and the bare-tag wrong-row overwrite measured host-side with GNUedover the real stockkernel.clayout; the wrapping-search wrong-table landing and the discarded-exit-status mode measured on thefilesys.carm; the renumber-skew shape found live in pre-renumber integrated trees; and the fail-closed pre-probe / one-edit-per-run / post-verify-the-row / repair-half-states pattern, each failure mode first reproduced and then pinned by the repos' no-box selftests.