Skip to content

Latest commit

 

History

History
278 lines (226 loc) · 11.4 KB

File metadata and controls

278 lines (226 loc) · 11.4 KB

05 — /QuestII: a hunk file that decrunches and relocates itself in 584 bytes

83,312 bytes, six hunks, and — as it sits on the disc — no relocations, no symbols and no debug hunks at all. That combination is impossible for a six-hunk program that has to reference its own data, and it is the first sign of what the file actually is.

Reproduce with:

python tools/hunk.py     _work/files/QuestII
python tools/decrunch.py _work/files _work/unpacked
python tools/lvo.py      _work/unpacked/QuestII.hunk1
python tools/m68kdis.py  _work/files/QuestII 0x34 584 --hunk 0x34

The hunk table

HUNK_HEADER   6 hunks, no resident list
  hunk 0        584 B   any    CODE, stored raw            <- the decruncher
  hunk 1    113,116 B   any    CODE, 58,928 B stored       <- crunched
  hunk 2     51,664 B   any    DATA, 22,708 B stored       <- crunched
  hunk 3    225,184 B   any    BSS
  hunk 4      3,540 B   chip   DATA,    976 B stored       <- crunched
  hunk 5    759,448 B   chip   BSS

Total allocation 1,153,536 bytes, of which 762,988 are chip — 37 % of a CD32's 2 MB claimed before the program calls AllocMem once. (It calls it four times afterwards.) Marvin takes 1.57 MB the same way; Liberation takes 40 bytes.

Three hunks declare far more than they store, which is the tell the platform checklist gives for a crunched hunk file. Here the ratio is not the point — the first longwords are:

hunk 1 body   00 01 73 44   00 00 5c d1   'RNC' 01 ...
hunk 2 body   00 00 b9 c8   00 00 2e 72   'RNC' 01 ...
hunk 4 body   00 00 0d d0   40 00 03 74   'RNC' 01 ...

Eight bytes of prefix and then an RNC ProPack method-1 block whose unpacked length is the hunk's declared allocation minus four, every time.

The eight-byte prefix, read out of the code that reads it

offset 0   ULONG  where the HUNK_RELOC32 block starts inside the unpacked hunk
offset 4   ULONG  that value / 4, with the hunk's memory flags in the top bits
offset 8   the RNC block

The second longword is the first divided by four with 0x40000000 set on the chip hunk — the same encoding a HUNK_HEADER size entry uses. It is redundant with the first and the stub never reads it.

The first is the interesting one. Each hunk decrunches to its real content followed by a complete HUNK_RELOC32 block, and the offset says where the join is:

unpacked content relocation block relocations
hunk 1 113,112 95,044 18,068 B 4,505
hunk 2 51,660 47,560 4,100 B 1,019
hunk 4 3,536 3,536 0 0

5,524 relocations in total, which is why the file itself has none: they are inside the compressed data, where LoadSeg cannot see them.

And the hunk numbers in those tables are off by one. The stub finds a target hunk by walking the AmigaDOS segment list from the head, and it starts counting at the second segment because the first one is the stub itself and is never a relocation target. Measured, on hunk 1's table:

hno 0  2,851 entries, stored values 852..90,724    -> hunk 1 (113,116 B)  not hunk 0 (584 B)
hno 1    706 entries,                0..47,554     -> hunk 2
hno 2    396 entries,                0..225,180    -> hunk 3 (225,184 B)  not hunk 2 (51,664 B)
hno 3    154 entries,                2..3,202      -> hunk 4
hno 4    398 entries,                0..757,648    -> hunk 5 (759,448 B)  not hunk 4 (3,540 B)

Three of the five ranges do not fit the hunk their number names and do fit the next one, so the numbering is settled by the data rather than by reading the loop.

Hunk 0 — the whole loader, 584 bytes

Disassembled at file offset 0x34. Branch targets below are computed from the raw bytes, not taken from the disassembler.

0x000  pea     (0x246,pc)              ; push the address the final rts returns to
0x004  movem.l d0-d7/a0-a6,-(a7)
0x008  lea     -0x180(a7),a7           ; 384 bytes of stack for three Huffman tables
0x00c  movea.l a7,a2
0x00e  pea     (-4,pc)                 ; &(hunk 0's next-segment BPTR)

segment loop:
0x012  movea.l (a7),a5
0x014  tst.l   (a5)
0x016  beq.w   0x22c                   ; end of the segment list -> finish
0x01a  movea.l (a5),a5
0x01c  adda.l  a5,a5
0x01e  adda.l  a5,a5                   ; BPTR -> APTR
0x020  move.l  a5,(a7)
0x022  addq.w  #4,a5                   ; a5 = this hunk's data
0x024  lea     8(a5),a0
0x028  cmpi.l  #'RNC'01,(a0)+
0x02e  bne.b   0x012                   ; not crunched: next segment

so the stub walks the loaded segment list, tests each hunk's byte 8 for the RNC magic, and skips the ones that are not crunched — which is how hunk 0 skips itself and how hunks 3 and 5, being BSS, never appear in the list at all.

For a crunched hunk it does the standard in-place ProPack setup — output end at hunk + unpacked_length, source end at packed_end, and a movem.l loop that copies the packed data up to output_end + leeway before decrunching downwards — and then the two instructions that make this disc's format its own:

0x086  moveq   #2,d1
0x088  bsr     0x14a                   ; consume the 2 RNC flag bits, discard
0x08c  bsr     0x146                   ; read 16 bits
0x090  move.w  d0,d5                   ; <- the XOR key

The token loop is stock ProPack — three canonical Huffman tables of 5-bit leaf count and 4-bit code lengths, a 16-bit sub-chunk count, literal runs and (offset, length) matches — with the XOR applied to literals only:

0x0d6  move.b  (a3)+,(a5)+
0x0d8  eor.b   d5,-1(a5)
0x0dc  dbra    d0,0x0d6
0x0e0  ror.w   #1,d5

Then it applies the relocations itself:

0x1f6  cmpi.l  #$3EC,(a4)              ; HUNK_RELOC32
0x1fc  bne.b   0x22a                   ; anything else: done with this hunk
0x1fe  clr.l   (a4)+                   ; and it CLEARS the table as it goes
0x200  move.l  (a4),d1                 ; count
0x202  beq.b   0x22a
0x204  clr.l   (a4)+
0x206  move.l  (a4),d0                 ; hunk number
0x208  clr.l   (a4)+
0x20a  lea     (-4,pc),a0              ; walk the segment list to hunk d0
        ...
0x21c  move.l  (a4),d0
0x21e  clr.l   (a4)+
0x220  add.l   d2,(a5,d0.l)            ; relocate
0x224  subq.l  #1,d1
0x226  bne.b   0x21c

and finishes by overwriting the return address it pushed at instruction one:

0x22c  move.l  (-4,pc),d0              ; hunk 0's next BPTR
0x230  lsl.l   #2,d0
0x232  addq.l  #4,d0                   ; = hunk 1's first byte
0x234  move.l  d0,0x1c0(a7)            ; patch the saved return address
0x238..0x246  restore the stack and registers, rts

so the program starts by rts-ing into hunk 1.

Two details worth carrying. The stub clears every longword of the relocation table as it consumes it, so 22 KB of table becomes 22 KB of zeros at the tail of the code hunk — free working space, at no cost. And the whole loader is position-independent: it reaches the segment list with pea (-4,pc) and lea (-4,pc), so it needs no relocations of its own, which is the other half of why the file has none.

Inside hunk 1: what the program asks the operating system for

tools/lvo.py walks the decrunched code, tracks the last value moved into a6 and names each jsr d16(a6). Full output in notes/lvo-questii.txt.

78 call sites, 33 distinct LVOs. The four library bases live in one block near the end of the code hunk, which is what makes the attribution exact:

QuestII.hunk1 + 0xd5c0   lowlevel.library
              + 0xd5c4   dos.library
              + 0xd5c8   graphics.library
              + 0xd5cc   nonvolatile.library

and all four are opened in one routine at 0xd27e, back to back, through OpenLibrary (−552) with moveq #0,d0 and with no test of the result:

Library Base Calls Which
exec 4.w, 28 loads 57 OpenLibrary ×4, OpenDevice ×3, DoIO ×4, SendIO, CheckIO ×2, WaitIO, AbortIO, CloseDevice, CloseLibrary ×2, AddPort ×3, RemPort, AllocSignal ×2, FindTask ×2, AllocMem ×4, SetIntVector ×8, AddIntServer, RemIntServer, Disable/Enable ×5 each, Forbid ×2/Permit ×4
dos 0xd5c4 9 Open ×2, Close ×2, Read, Write, Lock, UnLock, Examine
lowlevel 0xd5c0 6 ReadJoyPort ×3, GetLanguageSelection, AddTimerInt, RemVBlankInt
nonvolatile 0xd5cc 2 GetCopyNV, StoreNV
graphics 0xd5c8 0

graphics.library is opened and never called. Not once: there is no movea.l $d5c8,a6 anywhere in 113,112 bytes. The base is stored and never loaded again. The only graphics call on the whole disc is the single LoadView(NULL) in loaderblackpal (doc 03), which opens its own copy. That is the sharpest form yet of the checklist's "zero-zero" palette result — see doc 06.

SetIntVector eight times and one AddIntServer is a program that has taken the interrupt vectors apart, which is what a floppy-era Amiga game does; against that, dos.library stays alive for the whole game and every file is opened with Open/Read/Close rather than through cd.device. This title is neither of the two extremes the checklist describes: it uses AmigaDOS for its file system and the bare metal for everything else.

And there is an asymmetry in the interrupt handling worth recording. The start-up routine at 0xd25c ends with

0xd26c  lea     $d5ea,a1
0xd272  movea.l $d5c0,a6          ; lowlevel.library
0xd278  jsr     -72(a6)           ; AddTimerInt
0xd27c  rts

and the shutdown routine at 0xd2e0 is

0xd2e0  movea.l 4.w,a6
0xd2e4  jsr     -120(a6)          ; Disable
0xd2e8  movea.l a5,a0
0xd2ea  movea.l $d5c0,a6
0xd2f0  jsr     -108(a6)          ; RemVBlankInt
0xd2f4  jsr     -126(a6)          ; Enable

AddTimerInt is −72 and its handle is returned in d0 and discarded; RemVBlankInt is −108 and takes its handle in a0. There is no AddVBlankInt (−102) and no RemTimerInt (−78) anywhere in the file. So the program installs a timer interrupt whose handle it throws away and removes a vertical-blank interrupt it never added, using whatever is in a5. Both lowlevel Rem* functions unlink an interrupt server from a list, so the mismatch may be invisible at run time; it is recorded here as measured and left in doc 12.

Inside hunk 2: the file table

51,660 bytes of data, of which the last 4,100 are the relocation table. The part that matters is a single run of 91 NUL-terminated paths at hunk offset 0xa5400xb2d1, every one of the form Legacy of Sorasil DISK n:... (doc 03), followed immediately by the string input.device.

A dense run of same-length strings is the loader's own table, and finding it gave up the complete asset list, the load order, the three-floppy layout and one file that does not exist — in one string search, before anything was disassembled.

Hunk 2 also holds a seven-entry character table at hunk offset 0x36e3, the names exactly 128 bytes apart and each record carrying its own index 1..7:

0x36e3  Ravenslock   ... 05 01 04
0x3763  Grimbeard    ... 05 02 02
0x37e3  Stormbow     ... 05 03 03
0x3863  Celeste      ... 05 04 04
0x38e3  Oakheart     ... 05 05 03
0x3963  Haxar        ... 05 06 02
0x39e3  Calorflame   ... 05 07 03

Haxar is five characters and is invisible to a strings run with a minimum length of six, which is worth a line on its own: the gap it leaves in an otherwise even 128-byte stride is what made it findable.