Ten sectors at LBA 129,342, 19,752 bytes, written at 13:57:18 on 31 January 1995 — one second before the executable and two before the volume descriptor.
Nothing on the disc names it. The executable never opens it. It is the Microware linker's global symbol table, and it went onto the glass master with everything else.
python tools/cdistb.py stats
python tools/cdistb.py list
python tools/cdistb.py groups
module cdi_merlin.stb
module size 19752 bytes
symbols 887
sections {65535: 308, 0: 83, 1: 444, 2: 52}
887 symbols with their source-level names. For comparison, Link carried 325 function names as loose NUL-terminated strings between the functions; Soccer and Origami carried none. This is a real symbol table, in its own module, with addresses attached.
It starts 4A FC, and os9mod.py validates it: type Data, name
cdi_merlin.stb at the offset in M$Name. That is the linker's doing —
Microware's tools wrap the symbol file as a loadable Data module so the
debugger can modload it next to the program.
After the module name, a flat array of 16-byte records runs to offset 8,950:
u16 hi high word of the symbol's 32-bit value
u16 value low word
u16 flags
u32 nameoff byte offset of a NUL-terminated name, module-relative
The name offsets climb monotonically and each delta equals the previous name's length plus one, which is what identifies the layout in the first place. Names live in one block from offset 8,950 to the end.
addr = (hi << 16) | value
Three independent checks against cdi_merlin settle it:
| symbol | computed address | what it must be | matches |
|---|---|---|---|
btext |
0x00000000 |
the module's first byte | yes |
etext |
0x0002196a |
M$Size, 137,578 |
exactly |
_cstart |
0x00000054 |
M$Exec, 84 |
exactly |
etext landing on the module size to the byte is the one that cannot be
coincidence. And hi == 0xffff simply means a negative number — an A6-relative
offset, which is where an OS-9/68000 C program keeps its globals. The first
instruction at _cstart writes to -32752(A6), and the symbol at −32,752 is
_totmem. See 03-executable.md.
Cross-checking the other direction: of the 576 symbols in the three code sections, 452 begin with a C-compiler prologue at the address the table gives —
4e55 LINK A5,#d 167
2f00 MOVE.L D0,-(SP) 145
48e7 MOVEM.L 140
— and the rest are data labels, section bounds and library entry points that begin with a jump or a load.
The flag word takes seven values and correlates with the section:
section 0xffff (globals) 0x0000 84 0x0001 57 0x2000 132 0x2001 35
sections 0/1/2 (code) 0x0004 0x2004 0x3004
0x0004 is set on every symbol in a code section and clear on every global, so
it marks the section kind. 0x2000 is set on 167 of 308 globals and 218 of 576
code symbols, in a pattern consistent with an export/visibility bit. 0x1000
appears only in combination, on 149 symbols. This is read off the data rather
than from documentation; treat the last two as unconfirmed.
The link is laid out in three code sections plus the globals, and each has a distinct character. This is the whole build, visible at once.
| section | base | symbols | what it is |
|---|---|---|---|
| 0 | 0x00000 |
83 | the C startup and the game |
| 1 | 0x10000 |
444 | BOLT, plus the Microware C library |
| 2 | 0x20000 |
52 | the OS-9 system-call library |
0xffff |
A6-relative | 308 | globals |
_cstart, _stkcheck, stacksiz, freemem, trapinit, then main at
0x2ea, and after that nothing but Merlin's Apprentice: the challenge engines,
the BOSS_* shell, the NVRAM pack/unpack, the menu and token screens.
Read in address order it is the game's architecture, and it is set out in
08-game-architecture.md.
The largest section, and it interleaves two things. The first two thirds are the engine:
0x10000 MM_TermScreen, ChallDirMenuBar, ... PresentationEngine
0x1096a TC_* the Traffic Cop: jukebox, animation, TOC, memory, display
0x13f78 FH_* screens, buttons, save data, NVRAM, cursor, input, sound
0x1497c SM_* plane setup, button rendering, hit testing
0x16ab4 InitBOLT, TermBOLT, InitGraphics, ... graphics and MCD212
0x1892c InitBOLTC, StopBOLTPlay, DoBOLTSeek, OpenNextFile, LibRead ...
0x19c06 OpenBOLTLib, GetBOLTGroup, GetBOLTMember, MemberAddr, MemberSize ...
0x1abec Decompress, DrawPic, RLEDrawPic, StartFade, DoFadeInt, DoCycleInt
0x1c542 InitInLine, CopyMem, FillLong, DrawLine, FillBox, ReadPix, WritePix
and then, from 0x1d452 to the end of the section, the Microware C library
with every name intact: __mulsi3, __udivsi3, _T$LMul, _T$LDiv,
os9exec, modloadp, printf, fwrite, strcmp, malloc, memcpy,
toupper, open, read, write, lseek.
Two details from that list are worth a sentence each.
memmove and memcpy are both at 0x1f98c. One implementation, two
names, aliased in the library.
A libgcc-to-Microware thunk table. Five symbols sit at 0x1d452, four
bytes apart, and every one of them is a single BRA.W:
1d452 60 00 27 08 __mulsi3 -> 1fb5c _T$UMul / _T$LMul
1d456 60 00 27 76 __udivsi3 -> 1fbce _T$UDiv
1d45a 60 00 27 38 __divsi3 -> 1fb94 _T$LDiv
1d45e 60 00 27 66 __umodsi3 -> 1fbc6 _T$UMod
1d462 60 00 27 5a __modsi3 -> 1fbbe _T$LMod
Every branch target is exactly the symbol the name calls for. __mulsi3 and
friends are GCC's libgcc integer-helper names; _T$UMul, _T$LDiv and so on
are Microware's. Something in this build was compiled by a GCC-family compiler
that emitted calls to libgcc, and somebody wrote a twenty-byte shim to land
them on the Microware runtime instead of linking libgcc. Note that
_T$UMul and _T$LMul are the same address — signed and unsigned 32-bit
multiply are one routine, correctly.
pause, crc, prerr, sleep, tsleep, _parsepath, _mkdata_module,
_get_module_dir, _get_process_table, _get_process_desc, modlink,
modload, munlink, ebrk, ibrk, sbrk, kill, wait, setpr, chain,
os9fork, getpid, intercept, exit, abort, _utinit, _dumprof,
etext.
_dumprof is the profiler's dump entry point, linked in.
All A6-relative and all negative, from _mtop at −32,764 to end at −23,208 —
9,556 bytes, which is M$Mem = 9,560 rounded. The full list is in
notes/symbol-inventory.md. What they name:
- The BOLT runtime's state:
BOLTChain,CurGroupPtr,CurMemberPtr,CurLibPtr,LibraryChain,BOLTMemPool,MemHighWaterMark,AsyncInProgress,AsyncSectors,ErrorRetry,Encrypt,XORMask. - The MCD212:
FCTA,FCTB,LCTA1,LCTB1,LCTA2,LCTB2,LCTALINK,LCTBLINK,CurLCTA,VisibleLCTA,plane_order,PlaneAMem,PlaneBMem,CDIScreenMem,Interlace,PALFlag. - Fifteen named colours four bytes apart:
BLACK,HALF_BLUE,HALF_GREEN,HALF_CYAN,HALF_RED,HALF_MAGENTA,HALF_YELLOW,HALF_WHITE,FULL_BLUE…WHITE. - A named random number generator:
R250_IndexandR250_Buffer. The gap fromR250_Bufferto the next global is 500 bytes — 250 sixteen-bit words. R250 is the Kirkpatrick–Stoll lagged-Fibonacci generator, and the symbol table gives both its name and its state size.srand,rand,InitRand,RandomandRandare all in section 1 beside it. TitleSafeRect, andScreenDim,FontRect,CursorRect._chcodes, the Microware C character-class table — 129 bytes, named.
Merlin's Apprentice has no published source. What this file gives, for free, is the name of every global and every function in a 137 KB program, in link order, with addresses that resolve. Everything in 05-bolt-library.md, 06-realtime-files.md and 08-game-architecture.md was found by reading it and then confirming the reading against the bytes.
Scan for a .stb on every CD-i disc. It costs one directory listing, and on
this one it was worth more than everything else put together.