Building & Installing a Kernel¶
Amix has a monolithic SVR4 kernel, and stock Amix has no loadable modules ✅ (one
third-party exception) — 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 emulator-only kernel-image 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. This is not a bug in
the SCSI driver's completion poll loop, and not the bootstrap relocator misbehaving.
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)¶
🔴 Not ld. The corruption is not introduced when ld writes the linked kernel to disk, in
any variant: 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; at 16 MB 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). A kernel gated this way booted 5/5 on a 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) on the unfixed emulator at 8 MB —
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, build it with the
a3091.c chip-mem DMA bounce patch — whatever else is in it. ✅ The failure it cures has been
measured on the emulated A3000; on a real A3000 the patch is a safe over-approximation 🟡 (see the
reach refutation below). This is not an A4091 requirement
and not a cdfs requirement: 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 ✅.
On the emulated A3000, /etc/ddsize on a disk behind this driver hung for good, after a3091: 0x1 0 0x0 ✅ (ddsize can hang).
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 (the build policy — carry the patch in every kernel — is ✅):
| Machine | Roots through | Needs the patch? |
|---|---|---|
Emulated A3000 rooting on c6d0s1, card 0 |
A3000 onboard SCSI | Yes ✅ (measured) — regardless of which add-on HBA the kernel carries |
Real A3000 rooting on c6d0s1, card 0 |
A3000 onboard SCSI | Shipped as a safe over-approximation 🟡 — no failure without it has been seen on real hardware |
| A3000 + A4091 (A4091 sorts to card 1) | A3000 onboard SCSI | As the two rows above |
| A4000 + Z3660 rooting on the piscsi mailbox | the Z3660 | No — a3091 is linked but never carries the root read |
So a kernel built without the patch can boot fine on a Z3660-rooted A4000 and still fail on an emulated A3000 that roots through its onboard SCSI ✅.
Build-harness note. A patch keyed to a driver repo is only applied when that repo is part of the build. This one belongs in every kernel that roots through the A3000 onboard SCSI (it is boot-critical on the emulated A3000), whichever driver repos it carries. 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, and in
the failing kernels 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.
VOP_OPEN error 6has two known causes on an A3000 ✅. Either no card 0 is registered — the stock kernel's load-address guess skipped the internal SCSI (quirks §57;queue[0].fis empty) — or this DMA failure: card registered,queue[0].fpopulated, RDB buffer all zeros. The savestate diff above is what tells them apart.Distinct from the error-5 panic. The
s5mountroot 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.
"The super-DMAC cannot reach RAM at or above 0x1000000" is refuted ✅ (corrected 2026-10-02).
The ≥ 0x1000000 (16 MB) threshold is a2091's proven-safe cutoff, never a measured a3091
boundary, and the evidence on this page already contradicts a reach limit: the stock-a3091 kernel
64119 in the table reads its RDB into a buffer loaded above 0x07000000 and boots, and the bench
the failure was seen on is an emulator whose A3000 super-DMAC model is a full 32-bit master —
Amiberry's src/a2091.cpp initialises it with the 24-bit DMA mask off and no option to turn it on
(dma24bit exists only for the add-on cards) ✅. Real hardware agrees ✅: the public 68040/68060
port (asokero/amix-040-060-port, STATUS.md) runs on an A3000 whose accelerator RAM sits at 0x08000000, loads the
kernel there (load base 0x08000000, e.g. its 2026-09-06 Zorro III acceptance and the 68060 rows of
2026-09-28) and drives the A3000 SCSI with the A3091/SDMAC driver; its 2026-08-30 68060 record
(docs/REALHW-ISSUE54-STAGED-260830-06.md) prints the super-DMAC's address counter at sac=96D99F0
— about 150 MB up — with the root disk on the same controller still answering. Its "DMA coherence
(A3091 / SDMAC)" row is hardware-verified with cache-maintenance hooks, and the chip-RAM bounce in its
documents belongs to the A2091, not the A3091. So the super-DMAC reaches RAM far above 16 MB on a real
A3000, and the bench failure was real but was not an address-reach failure. What it was is
open 🔴: the leading hypothesis is a stale data cache — the CPU reading cached contents of
the static buffer instead of what the DMA put in memory — which the chip-RAM bounce cures as a side
effect, because the copy comes from a different, freshly written buffer. The port's need for explicit
cache maintenance around SDMAC transfers on the 68040/68060 is consistent with that family 🟡, but the
savestate diff (buffer all-zeros in memory) does not obviously fit a cache-only story for the 68030
bench, so the specific cause stays 🔴 until a cache-flush experiment decides it. The bounce remains the
shipped fix, as a safe over-approximation.
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).
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 the A3000's internal SCSI (WD33C93 + Super DMAC, the a3091 driver) — most likely on the
emulated A3000, which is where the CD work ran 🟡 — 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). It is not an A4091 limit (the 53C710 is a full 32-bit bus master).
It may share a cause with the a3091 boot failure above (whose "high-address reach" explanation is
refuted; the leading hypothesis there is cache coherence) 🔴. Caveat: the same day a separate
reentrancy bug in the in-kernel SCSI issue path, which also scrambled in-flight transfers, was fixed
(amix-cdfs 2172772), so how much of the multi-sector failure was that bug is not established 🟡.
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 multi-sector limit on this path was never characterized, so the
one-sector cap stays.
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).
Verify /stand/unix after an install ✅¶
The stock /usr/sys/Makefile install rule copies the new kernel to /stand/NEWrelocunix, moves
any existing /stand/unix to /stand/OLDunix, links NEWrelocunix as /stand/unix and runs
make in /stand to rewrite the boot partition ✅ — so when it completes, /stand/unix is the
new kernel. The Amiga bootstrap boots the boot-partition image written from /stand/unix, so an
install that stopped part-way (or a wrapper script that reports success regardless) silently
re-boots the old kernel, and any "verified on the new kernel" result is worthless. Check before
you reboot, and if they differ, do the copy explicitly and rebuild the bootpart:
sum -r /stand/unix /usr/sys/relocunix # the two sums must match before you reboot
# if they do not:
cp /usr/sys/relocunix /stand/unix
cd /stand; make # (or: cd /usr/sys; make bootpart KERNEL=relocunix)
sum -r /stand/unix # now matches 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 🟡¶
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). 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. - 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 ✅:
- 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¶
- Stock sources, public ✅ — the stock SCSI and console sources in
amigaux/amix-system(7cba961):usr/sys/amiga/alien/a3091.c,usr/sys/amiga/alien/a2091.c,usr/sys/amiga/alien/dd.c,usr/sys/amiga/alien/sdpart.c,usr/sys/amiga/alien/scsi.c,usr/sys/amiga/console/scrdev.c,usr/sys/amiga/console/c0.c,usr/sys/amiga/console/screen.c. -
The public 68040/68060 port,
asokero/amix-040-060-port@upstream/main(2026-09-29):STATUS.md(the A3000 + accelerator rig with RAM and load base at0x08000000; the hardware-verified "DMA coherence (A3091 / SDMAC)" row; rows68040-260906-06and68040-260928-05),docs/REALHW-Z3-040-260906.md,docs/REALHW-ISSUE54-STAGED-260830-06.md(thesac=96D99F0line) anddocs/contracts/A3091-PHASE-MISMATCH-RESUME-AUDIT.md(the chip-RAM bounce is the A2091's) ✅ — https://github.com/asokero/amix-040-060-port. -
The super-DMAC reach refutation (2026-10-02) ✅ — the
64119row of the kernel table on this page (stocka3091, RDB buffer above0x07000000, boots) and Amiberry'ssrc/a2091.cpp(the A3000 super-DMAC initialised with the 24-bit DMA mask off;dma24bitapplies only to the add-on cards), read in the Amiberry source. The stale-cache explanation is a hypothesis 🔴. - The 2026-09-29 stock-kernel RAM-layout measurement ✅ — on the emulated bench the same 64 MB A3000 motherboard bank boots a kernel that probes for the A3000 SCSI and stops the pinned stock kernel at
s5mountroot VOP_OPEN error 6(no card 0), symbolised against the stockautocon()load-address guess (quirks §57). - amix-kerntools first-party records (2026-07-16, 2026-07-21): relsim scope correction, bootchain-verify gate, measured 85% relink corruption, tdivs blast-radius map, __udivdi3 metal closure (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 emulator-only kernel-image corruption (configuration-dependent; see the D245 section), 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 chip-mem DMA bounce for buffers ≥0x1000000(the threshold isa2091's, not a measureda3091reach limit — see the refutation above), 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 root-cause write-up and its fix (first-party record,
2026-07-26/27). The bounce is carried by every kernel that roots through the A3000 onboard SCSI
(measured necessary on the emulated A3000), 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. - 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). - The stock
/usr/sys/Makefileinstallrule (install : all→cp … /stand/NEWrelocunix,mv /stand/unix /stand/OLDunix,ln NEWrelocunix unix,makein/stand), read from a stock 2.1c tree ✅; thesum -r /stand/unixversusrelocunixcheck on a real A4000 + Z3660 (first-party record, 2026-07-12) ✅. The same record also confirms SVR4grephas no\|alternation (already noted above under build-script gotchas). - The cdevsw free-row gate and the half-patch audit fixes (first-party record, 2026-07-30/31) and
the amix-z3660net patch-script hardening (
603c719) ✅ — 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.