Reverse-engineering the prebuilt kernel¶
Large parts of the Amix kernel ship as prebuilt relocatable objects with no .c source — the VFS
layer, the syscall trap, the mount path. When you need to know exactly what one of these does (and the
in-guest debuggers can't help), the reliable route is to disassemble the shipped objects on the host
and, for runtime state, read a running kernel out of an emulator savestate. Everything on this page
was reproduced locally ✅ — disassembly of the objects in ~/opt/amix-cross/.../sysroot/usr/sys
with the cross objdump, cross-checked against a live kernel image and against an on-box test.
Why: the kernel ships as objects, not source¶
Amix builds the kernel from per-directory linked export objects, one exp per subsystem, not from a
flat pile of .c files (see Kernel architecture → the kernel as object libraries) ✅. The Commodore-authored core — os/, fs/ — is present only as those objects; there is no os/vfs.c / os/trap.c in /usr/sys to read. But the objects are ordinary m68k COFF/relocatable files, so they disassemble cleanly, with symbols and relocations — which means jsr targets show up by name. That makes the disassembly readable as pseudo-source.
Step 1 — find which exp defines a symbol¶
The staged kernel tree lives under the cross sysroot; use the cross binutils (never the host's) ✅:
SYS=~/opt/amix-cross/m68k-cbm-sysv4/sysroot/usr/sys
NM=~/opt/amix-cross/bin/m68k-cbm-sysv4-nm
# which object defines `mount`, `vfs_getvfssw`, `vfsinit`, `vfssw`?
for f in $(find "$SYS" -name exp -o -name '*.exp'); do
m=$("$NM" "$f" 2>/dev/null | grep -iwE '_?mount|_?vfs_getvfssw|_?vfsinit|_?vfssw')
[ -n "$m" ] && echo "### ${f#$SYS/}" && echo "$m"
done
For the mount path this points at two objects ✅:
| Symbol | Object | Offset |
|---|---|---|
mount (the syscall) |
fs/fs.exp |
0x1814 |
vfs_getvfssw |
fs/fs.exp |
0x2370 |
vfsinit |
fs/fs.exp |
0x2446 |
sysent (syscall table) |
os/exp |
0x6f8 |
systrap (trap dispatcher) |
os/exp |
0x1eaf6 |
Step 2 — disassemble with relocations¶
objdump -dr interleaves the relocation records, so every external call is annotated with its target
symbol ✅:
OD=~/opt/amix-cross/bin/m68k-cbm-sysv4-objdump
"$OD" -dr "$SYS/fs/fs.exp" > fs_exp_dis.txt # whole object
# extract one function (mount @0x1814 → next label):
awk '/^0*1814 <mount>:/{f=1} f{ if(/^0*1814/){print;next}
if(/^[0-9a-f]+ <[^>]+>:/){exit} print }' fs_exp_dis.txt
The R_68K_32 lookupname / R_68K_32 vfs_getvfssw reloc lines under each jsr tell you what is being
called without any guessing. This is what makes a source-free object tractable.
The same reloc-annotated disassembly reaches well past the VFS/mount functions tabled above. The in-kernel SCSI path was cracked the same way — sdopen (the sd disk driver's open entry) and getrdb (the RDB / Rigid Disk Block parser) — reading their jsr targets by name straight out of their exp objects ✅. Any source-free subsystem, core or driver, is fair game.
Step 3 — read a running kernel from a savestate¶
The on-guest debuggers are unreliable here: adb -k /stand/unix /dev/kmem reads garbage because the
relocated runtime kernel does not match the /stand/unix namelist (constants like nfstype read as
0), and crash errors out on /dev/mem ✅. The dependable path is a host-side memory dump.
That is a stock relocated kernel's problem: on an amigaux 1.1 module kernel the same adb
invocation reads live values (below).
Take an emulator snapshot (Amiberry: the SAVESTATE IPC verb) and pull globals out of the RAM chunk.
On an A3000-fastmem machine the kernel lives in the A3K1 chunk, identity-mapped at physical base
0x07800000 (runtime VA = 0x07800000 + chunk_offset) ✅. The .uss chunk format is
name(4) + len(4 BE) + flags(4 BE) + [uclen(4 BE) if compressed] + zlib data. Find a table by an
unambiguous anchor (e.g. the "BADVFS" string that heads vfssw[]) and walk it:
import zlib, struct
d = open("amix.uss","rb").read()
j = d.find(b"A3K1")
ram = zlib.decompressobj().decompress(d[j+16:])[:struct.unpack(">I", d[j+12:j+16])[0]]
BASE = 0x07800000
W = lambda o: struct.unpack(">I", ram[o:o+4])[0] # runtime VA = BASE + o
This recovered the live vfssw[] (each row {char *vsw_name; int (*vsw_init)(); struct vfsops *vsw_vfsops; long vsw_flag}, 16 bytes) and the int nfstype immediately after it ✅ — see the worked example below.
Grabbing the snapshot — the two-arg SAVESTATE IPC¶
Amiberry's SAVESTATE verb takes two arguments — a statefile and a configfile. Called with one it errors and saves nothing ✅:
It works even at a kernel panic screen, so you can snapshot a wedged kernel and read why it wedged ✅.
Locating a global by symbol — nm values are section-relative¶
Step 3's walk above found vfssw[] by a string anchor ("BADVFS"). When a global has no such anchor, resolve it by symbol with the cross nm on the linked kernel image /usr/sys/relocunix — but its values are section-relative, not absolute ✅. The kernel loads packed as text | data | bss at base 0x07800000, so the byte offset of a symbol into the A3K1 chunk is:
| Symbol's section | Byte offset into A3K1 |
|---|---|
| bss | text_size + data_size + nmval |
| data | text_size + nmval |
with text_size / data_size read from size /usr/sys/relocunix (cross size). Taking the raw nmval as the offset — the naïve reading — lands you in kernel code, not the variable ✅.
Validated at the computed offsets: scsicard[] shows the 0xc0de0001 / 0xc0de0002 product codes and the global block shows the UNIX_Root PART record ✅.
Live-probing a running kernel with adb on /dev/kmem — the load-bias trap ✅¶
The savestate route above reads a frozen kernel. When the box is alive, you can read and write
live kernel memory in place with adb on /dev/kmem, turning an instrumented kernel into a re-armable
probe with no rebuild/reboot cycle ✅:
echo '<runtime-VA>/D' | adb - /dev/kmem # read one long by runtime virtual address
echo '<runtime-VA>/W 0' | adb -w - /dev/kmem # write it (adb -w enables writes)
This was proven by zeroing a driver's static log counter mid-session and watching it increment again on the next I/O ✅.
The trap — adb's symbol addresses are FILE-relative, so you must add the kernel's load bias. ✅
Scope: this fix applies to defined symbols only (.text, .data, .bss). A SHN_COMMON
symbol — every bare C tentative definition in the kernel — is a different trap the bias cannot
fix: its file value is an alignment, not an offset (see the next section) ✅.
The reason the earlier adb -k /stand/unix /dev/kmem invocation
reads garbage is the same one that bites here: given a
namelist (adb /stand/unix /dev/kmem), adb resolves a symbol to the address recorded in the
kernel file, which on this relocated kernel is only an offset — reading it directly returns
garbage (freemem read back as 0xFFFFFFFF). Drop the namelist and address by runtime VA instead:
🟡 Two open re-checks on this example (flagged 2026-08-27, one measurement each, not yet run): first-party
strstat.pyclassifiesfreememas a COMMON symbol — in which case the0xFFFFFFFFabove is an instance of the COMMON trap, and the bias would not have fixed it; and the working combination measured for kernel-mode reads is-kwith/dev/mem, whereas the failing invocation above used/dev/kmem— device-versus-bias was not isolated. Until re-run, treat this paragraph's example (not its rule) as provisional. Narrowed 2026-10-31: on an amigaux 1.1 module kernel, whose/stand/unixholds runtime addresses,adb -k /stand/unix /dev/kmemreadsfreememcorrectly ✅ (below), so-kon/dev/kmemworks when the namelist is right. Both re-checks on the stock relocated kernel are still open.
On the real A4000 + Z3660 box the kernel loads at 0x08000000, verified by hand: z3660_enter
at nm offset 54932 (0xD694) is live at 0x0800D694, matching its instruction bytes ✅. (That base
is machine/config-specific — it is wherever the bootstrap relocated the kernel; the Amiberry
A3000-fastmem savestate above sees the kernel at 0x07800000 instead. Confirm your box's base with a
known symbol before trusting any poke.)
For a static-scoped variable with no global symbol: disassemble the function that touches it, read
the operand address straight out of the live instruction stream, then poke that address — the same
trick that recovers file-scoped statics from the exp objects, applied to the running image ✅.
On an amigaux 1.1 module kernel the namelist is already bound ✅¶
On a 1.1 module kernel neither trap applies. kbootset writes /stand/unix as the booted
generation's namelist: an ET_EXEC symbol table with no contents, each symbol at its address in
this boot (the bind address plus its offset in the loader's layout, COMMON symbols included). It
rewrites the file on every boot that reaches mark-good, and kbootset status names it
("namelist: /stand/unix is generation N's, bound at 0x08000000"). The symbol values adb takes from
it are runtime addresses, so no load-bias arithmetic is needed ✅:
Measured on the 1.1 68030 module kernel (emulated A4000 + Z3660 in 68030 mode, 32 MB fast RAM),
the values are live and consistent ✅: physmem 16383 (2 KiB pages); freemem 11854 and 11851
idle, 11746 with six extra processes running, 11832 after they exited; availrmem 14229, 14211,
14213.
The other namelist readers do not follow ✅. On the same box sar -r 1 1 fails with
sadc cannot copy inode table: No such device or address, and /etc/crash with
could not read namelist timestamp. On the 2.1c bench image's kernel (booted kernel not
/stand/unix, no symbol table) crash fails with process slot out of bounds and sadc with
Not enough space. For live kernel variables on 1.1, use adb.
modadmin -p knows only the exported symbols; the 68040/060 load layout gives the rest ✅¶
On a test build of the 68040/060 module kernel, modadmin -p kzero prints an address, but modadmin -p sptmap,
availrmem or tune answer symbol not found ✅. modadmin resolves the names the DDI exports
(the boot's "208 kernel symbols"), not the kernel's whole symbol table. For the rest, use the
namelist above or compute the address from the loader's layout ✅:
- The
ET_RELkernel (/stand/k46/unix) is loaded with.textat0x08000000. .datafollows.textimmediately..bssfollows.dataimmediately, with no alignment: the loader puts.bssat the end of.dataas it is. The port's tree pads.datato a 4-byte multiple for that reason.- Then come the COMMON symbols, in symbol-table order, each aligned to 4 and sized by its
st_size.
That model reproduced ten addresses modadmin printed (five in .data, five COMMON), and lbolt on
a build whose .data did not end on an 8-byte boundary ✅. In the ET_REL file itself a COMMON
symbol's st_value is its size or alignment, and a .data or .bss value is a section offset, so
neither is an address: the trap of the next section and of the avenrun story in
quirk 38.
The COMMON-symbol trap: nlist succeeds and lies ✅¶
The second, distinct reason a symbol lookup against /stand/unix hands you a bad address — and the
load-bias fix above cannot touch it. /stand/unix is an ET_REL relocatable object, and every
bare C tentative definition in the kernel is SHN_COMMON in it: such a symbol has no address
in the file at all. Its n_value is the alignment requirement, and nlist(3) returns
success anyway — no error, no zeroed entry. The caller then reads /dev/kmem at a small integer
and prints whatever lives there. The address genuinely cannot be recovered from the file: the boot
loader's elf2brel conversion allocates COMMON itself at boot and discards the symbol tables,
so the placement exists only inside the loaded image ✅.
How to tell in ten seconds ✅: nm shows COMMON in the section column; nlist returns
n_scnum = -14; the value is an implausibly small, suspiciously round number. If a "kernel
address" comes back as a single-digit number, it is an alignment — nothing lives at 4.
| Trap | Applies to | Symptom | Fix |
|---|---|---|---|
| Load bias (previous section) | defined symbols | value is a real but file-relative offset | add the kernel load base |
| COMMON placement (this section) | SHN_COMMON symbols |
value is an alignment; no address exists in the file | resolve against the running image |
What works ✅: adb -k <kernel> /dev/mem — the -k kernel mode with the physical-memory device —
because adb -k reproduces the boot loader's COMMON placement instead of trusting the file's
symbol value. Two load-bearing conditions:
- The namelist must be the kernel actually running. A different kernel's namelist resolves to a
different, plausible-looking address — measured: the stock
2.1_unixnamelist on a relinked running kernel returned convincing zeros ✅. - When the running kernel exists in no file at all (a kernel loaded from a raw slice rather
than
/stand), there is no namelist to handadb, and pointing it at/stand/unixfails silently with plausible values — the same trap class this section warns about. The recovery is to replay the loader's COMMON allocation host-side against the running artifact (first-party record, 2026-08-30), then read the computed address; validated on real hardware by an independent clock-tick cross-check ✅.
crash(1M) is not an alternative — it fails on this kernel with process slot out of bounds ✅. And
where adb is unavailable, the instruction-stream bootstrap already described above (read the
operand address of a function that touches the variable, cross-check a second site) resolves COMMON
symbols too — that is precisely how strstat.py finds strst ✅.
The checklist, for any kernel variable on Amix ✅: never trust nlist(3) (success is not
evidence); classify the symbol first (nm — COMMON or defined); COMMON → running image (adb -k
with the running kernel, or replay the allocation); defined → nm value + load base, base confirmed
against a known text symbol; and always sanity-check by moving the machine — a value that never
moves with load is a wrong address wearing a convincing disguise. The worked consequence of this
whole section is the load-average story: see
load averages.
Worked example — how mount(2) and fstype registration actually work¶
Disassembling the three fs.exp functions above yields the complete, source-accurate picture ✅:
Registration is a compile-time table + an init sweep. Filesystems live in vfssw[] (built from
master.d/filesys.c); nfstype is its length. At boot, vfsinit loops for (i=1; i<nfstype; i++) and
calls vsw_init(&vfssw[i], i) for each row — so a filesystem's init hook (e.g. s5init, cdfsinit)
runs automatically once its row is in the table. Adding a row and relinking is sufficient to register a
new FS; there is no separate init-call list to edit ✅. (This resolves a long-standing "how does the
kernel call the fs inits" question — it is vfsinit, and it is right there in fs.exp.)
mount(2) never looks at the special/spec argument. The syscall (fs.exp:0x1814) resolves the
mount point via lookupname, then selects the fstype, then dispatches — it forwards the spec
untouched to the filesystem's own VFS_MOUNT. The fstype selection uses a pointer-vs-integer
heuristic ✅:
if ((u_int)uap->fstype < 256) { /* old SVR3 numeric fstype index */
if (fstype == 0 || fstype >= nfstype) return EINVAL; /* <-- errno 22 */
vfsops = vfssw[fstype].vsw_vfsops;
} else { /* modern string fstype, e.g. "cdfs" */
copyinstr(uap->fstype, fsname, ...);
if ((vswp = vfs_getvfssw(fsname)) == NULL) return EINVAL; /* <-- errno 22 */
vfsops = vswp->vsw_vfsops;
}
vfs_getvfssw (fs.exp:0x2370) is a plain for (i=1; i<nfstype; i++) strcmp(name, vfssw[i].vsw_name)
scan of the same table. The per-FSType user helpers /usr/lib/fs/<fs>/mount — which the generic
mount(1M) execs — always pass the fstype string (mount(spec, dir, MS_DATA|MS_RDONLY, "cdfs", 0, 0)),
so real mounts take the string branch ✅.
The trap returns the syscall value verbatim. systrap (os/exp:0x1eaf6) stores the syscall's
return straight into the user register (movel ...,%a5@(4)) with no & 0xFF mask and no max-errno
clamp — only EFBIG/EINTR get special handling ✅. So a filesystem's VFS_MOUNT return reaches
errno unchanged; there is no hidden remapping to hunt for. (Amix's highest defined errno is ~151;
EINVAL is 22.)
The payoff — an errno 22 from mount -F <fs> means "not live in the booted kernel"¶
Putting the disassembly together gives a sharp diagnostic ✅. For a string mount of a filesystem on
a normal directory, the only pre-dispatch EINVAL(22) reachable is vfs_getvfssw(name) == NULL —
i.e. the fstype's row is not in the running kernel's vfssw[]. Every other 22 site is dead for that
call (the numeric-index check needs an integer fstype; VNOMOUNT is only ever set on /proc vnodes, not
s5/ufs directories; lookupname of an existing dir returns 0/ENOENT/ENOTDIR, never 22).
So when a freshly-built in-kernel filesystem returns errno 22 from mount -F <fs> ... /mnt, it is
almost always the classic SVR4 trap: the kernel was patched on disk but not relinked-and-rebooted, or
the wrong kernel is booted — not a bug in the filesystem's mount code. Confirm live registration directly
with sysfs(GETFSIND, "<fs>") (from <sys/fstyp.h>: GETFSIND=1, GETNFSTYP=3), which scans the same
vfssw[]:
#include <sys/fstyp.h>
i = sysfs(GETFSIND, "cdfs"); /* >=1 → registered live; -1/errno 22 → NOT in this boot */
This was verified end-to-end: on a kernel without cdfs compiled in, sysfs(GETFSIND,"cdfs") returns
-1 and the string mount returns errno 22; the fix is to relink the fstype into the kernel and boot
that image, not to touch the mount path ✅. See Building & installing a kernel for
the relink/install/reboot cycle and Filesystems & disks for
the /etc/vfstab side.
A throwaway-global diagnostic — the cdfs_diag pattern¶
When the console is dead but a savestate still works (Step 3), a throwaway global turns the snapshot into a poor-man's printf. cdfs used one, cdfs_diag: a struct filled as a mount/read path advanced — reached-stage, errno, the first bytes read — then dumped afterward out of the .uss ✅. The trick that makes it findable in the packed image: write its magic word 0x0DF50001 last, so the populated struct is the one whose magic is set. One caveat when you scan the A3K1 chunk for that magic — it also occurs coincidentally inside code, so take the 4-aligned hit ✅.
See also¶
- Kernel architecture — the object-library build model these
expfiles come from. - Building & installing a kernel — relinking a new object/fstype in and writing the boot partition.
- Filesystems & disks —
/etc/vfstab, device nodes, UFS vs s5. - Zorro AUTOCONFIG — the
autocon()/scsicard[]card-ordering path (also a prebuilt-object read). - Case study: A4091 / 53C710 SCSI — the driver whose CD host this method was used to debug.
Sources¶
- The
modadmin -pscope and the 68040/060 load layout (first-party record, 2026-11-01) ✅ — the 68040/060 port's emulator runs on a test build of its module kernel (A4000, 68LC060 on a Z3660):modadmin -pfor exported and unexported names, ten printed addresses reproduced from the layout model, andlbolton a build with an unaligned.dataend; portKNOWN-ISSUES.md(2026-11-01) and the port tree's.datapadding. - The 1.1 namelist reads (first-party record, 2026-10-31) ✅ — the amix-kerntools project's runs on the emulated 1.1 68030 module kernel (A4000 + Z3660 in 68030 mode, 32 MB): the
adb -k /stand/unix /dev/kmemvalues above,kbootset status, and thesar/crashmessages there and on the 2.1c bench image's kernel. The namelist's form and writer:amigaux/amixkernel/dlm/docs/spec/40-generations-and-composer.md§4.2. - Disassembly of the shipped kernel objects
usr/sys/fs/fs.exp(mount@0x1814,vfs_getvfssw@0x2370,vfsinit@0x2446) andusr/sys/os/exp(sysent@0x6f8,systrap@0x1eaf6) viam68k-cbm-sysv4-objdump -dr/-nm, cross toolchain sysroot — reproduced locally ✅. - Live
vfssw[]/nfstyperead from an AmiberrySAVESTATE(A3K1chunk, identity base0x07800000), cross-checked against the disassembly ✅. - On-box reproduction on Amix 2.1c:
sysfs(GETFSIND,"cdfs")and a string-formmount(2)probe, showingerrno 22⇔ fstype absent from the booted kernel'svfssw[]✅. <sys/mount.h>(MS_RDONLY=0x01,MS_FSS=0x02,MS_DATA=0x04;int mount(const char *, const char *, int, ...)),<sys/fstyp.h>(GETFSIND=1,GETFSTYP=2,GETNFSTYP=3),<sys/errno.h>(EINVAL=22) from the Amix sysroot ✅.- SVR4.0 reference behaviour cross-checked against
calmsacibis995/svr4-srcuts/i386/fs/vfs.c(themount()/vfs_getvfsswordering) — Amix is a straight SVR4.0 port ✅. - Two-arg Amiberry
SAVESTATEIPC (statefile+configfile; one arg saves nothing, works at a panic screen) — firsthand, amix-cdfs CD-ROM port effort (2026-07-09/10) ✅. - Section-relative
nmoffset model for the linked kernel image/usr/sys/relocunix(bss offset =text_size + data_size + nmval, data offset =text_size + nmval; sizes fromsize), validated against thescsicard[]0xc0de0001/0xc0de0002product codes and theblockglobal'sUNIX_RootPARTrecord in the A3K1 dump — firsthand ✅. - Same
objdump -drreloc-annotated method extended to the in-kernel SCSI path —sdopenandgetrdb(RDB parser) traced by name out of theirexpobjects — firsthand ✅. - cdfs's
cdfs_diagthrowaway-global diagnostic (magic0x0DF50001written last; scan theA3K1chunk for the 4-aligned hit) — firsthand from the amix-cdfs port ✅. - Live
adbon/dev/kmem(read withadb - /dev/kmem, write withadb -w; symbol addresses are file-relative so runtime VA =nmvalue + kernel load base,0x08000000on the real A4000 + Z3660, verifiedz3660_enternm0xD694→ live0x0800D694; poke astaticby reading its operand address out of the live instruction stream) — first-party record, 2026-07-12, on real hardware ✅.