Skip to content

Latest commit

 

History

History
229 lines (189 loc) · 9.48 KB

File metadata and controls

229 lines (189 loc) · 9.48 KB

Three executables, all packed, and the library between two of them

python tools/mzinfo.py  _work/hero-quest/QUEST.EXE
python tools/exepack.py _work/hero-quest/QUEST.EXE -o _work/QUEST.unp

The headers, read rather than assumed

QUEST.EXE INTRO.EXE HERO.EXE
file size 51,209 23,017 1,075
declared image size 51,209 23,017 1,075
overlay past the image 0 0 0
header paragraphs 33 (528 B) 32 (512 B) 32 (512 B)
relocations 0 0 0
entry point cs:ip 0C2B:0012 055D:0012 000E:0012
entry point file offset 0xC4D2 0x57E2 0x2F2
initial stack ss:sp 1498:0080 0837:0080 0029:0080
min alloc 6,076 para = 97,216 B 12,287 para = 196,592 B 13 para = 208 B
max alloc 65,535 (all of it) 65,535 65,535
checksum 0 0 0
overlay number 0 0 0

Three things fall out immediately and they all point the same way.

Every declared image size equals the file size exactly. No overlay, no appended data, no resource fork. Whatever these programs load, they load from the twenty-one other data files by name — and they do, as the string tables show below.

Every one of them declares zero relocations. A real DOS program compiled for anything but the tiny model has relocations; zero means either a .COM-shaped program wearing an MZ header or a packed image whose real relocation table is hidden inside the payload.

All three entry points are at ip = 0x0012 and all three land within a few hundred bytes of the end of the file. That is not a coincidence between three independently built programs.

They are EXEPACK, and the same EXEPACK

The signature scan finds RB two bytes before the entry point in all three files, which is the classic Microsoft EXEPACK marker, and the entry bytes are identical in all three:

8C C0 05 10 00 0E 1F A3 04 00 03 06 0C 00 8E C0 8B 0E 06 00 8B F9 4F 8B F7 FD F3 A4
mov ax,es / add ax,0010 / push cs / pop ds / mov [0004],ax / add ax,[000C] / ...

ip = 0x0012 is not arbitrary either: it is the length of the EXEPACK header, which sits at cs:0000 and is followed by the decompressor stub. Reading that header:

                     QUEST.EXE   INTRO.EXE   HERO.EXE
real entry cs:ip     0000:00ED   044B:0000   0000:0000
real stack ss:sp     2403:0190   3576:0064   000A:0032
exepack_size            841         537        339
dest_len (paragraphs)  5,219       2,069         19
skip_len                  1           1          1
signature              'RB'        'RB'       'RB'
error message   "Packed file is corrupt"  in all three

Unpacking is a backwards walk: the source pointer descends from the end of the packed region and the destination pointer descends from the top of the output buffer, with three-byte commands (B0/B1 fill, B2/B3 copy, low bit ends the stream). tools/exepack.py implements it, and it reports one thing worth stating because it is where a first attempt goes wrong:

                     QUEST.EXE   INTRO.EXE   HERO.EXE
commands                165          74          5
FF padding stripped      14          10          2
verbatim head        12,273       2,722        151
src and dst meet       True        True       True
unpacked size        83,504      33,104        304
expansion            1.676x      1.508x     1.369x

When the stream terminates, the source and destination pointers are equal in all three files. That is not luck: the stub does not move the whole image up before decompressing, so the low part of the load image is already sitting where it belongs and needs no work. Everything below the meeting point is copied across verbatim. A decoder that stops at the terminate flag and leaves that region zero produces a file that looks plausible and is 15–25 % missing, which is exactly what happened on the first run here.

The three decompressor stubs share their first 283 bytes exactly and diverge afterwards, where the per-file relocation table lives. One packer, one version, three files. That fact does most of the work in 03-the-1980-stamp.md.

Resident cost

image min alloc resident (image + minalloc)
QUEST.EXE 83,504 97,216 180,720
INTRO.EXE 33,104 196,592 229,696
HERO.EXE 304 208 512

Plus a 256-byte PSP each. QUEST.EXE asks for 176 KB and INTRO.EXE for 224 KB before either has opened a file, and both then load pictures on top of that — WIZBACK.VGA alone is 22 KB compressed. Both set max alloc to 0xFFFF, i.e. "give me all of conventional memory", and both carry the message THERE IS INSUFFICIENT MEMORY FOR THE PROGRAM for when DOS cannot. The INTRO.EXE figure being the larger of the two is the intro's decompression buffers for the full-screen 320×200 pictures; the game itself never needs a whole screen at once.

QUEST.EXE also carries REQUIRES DOS 2.0 OR GREATER..... — a version gate, in a product shipping in 1992, seven years after DOS 3.0.

HERO.EXE is the launcher, and it is 304 bytes

Unpacked, HERO.EXE is 304 bytes and contains three strings and nothing else:

000000E2  Insert disk 2 and press a key
00000101  $intro.exe
0000011A  quest.exe

That is the whole program: prompt for the disk if needed, exec INTRO.EXE, exec QUEST.EXE. 304 bytes cannot hold a C runtime, so it is hand-written assembly, and its min alloc of 13 paragraphs confirms it — a C program's startup code alone would ask for more. INSTALL.BAT ends by telling the player Type HERO to play, and QUEST.EXE tells them Type 'hero' to set configuration when QUEST.CFG is missing, so HERO.EXE is both the launcher and the setup entry point.

The shared library module

QUEST.EXE and INTRO.EXE contain a block that is byte-identical in both:

QUEST.unp  0xC9CA .. 0xCEEA
INTRO.unp  0x14AC .. 0x19CC
length     1,312 bytes  (1.6 % of QUEST, 4.0 % of INTRO)

It holds, in order: the audio driver and bank filenames (beeper.drv, adlib.drv, mt32.drv, tandy.drv, game&fx.alb, game&fx.rld), the 87-glyph bitmap font (07-font-and-text.md), the configuration menus, the error messages, and the keyboard scancode tables. It is a linked library module and it is the reason both programs can draw text and both can put up the same setup screen.

Its $-terminated strings are the giveaway that the setup screens are printed with INT 21h/09h while everything else in the game is drawn with the bitmap font:

Select screen format:-
1. MCGA/VGA (256 colours)
2. EGA
3. Tandy$
Select sound board:-
1. Internal
2. Adlib
3. Roland$
Centre joystick and press fire$
$quest.cfg
Heroquest (c) Gremlin Graphics 1991
$Configuration file missing,
Type 'hero' to set configuration
$No mouse installed

Three display modes, not two. MCGA/VGA, EGA and Tandy. The archive has no .TDY files, so the Tandy path must render from one of the two existing picture sets; and the sound menu has three entries — Internal, Adlib, Roland — which does not mention Tandy at all, while TANDY.DRV exists and is the largest of the four drivers. The most economical reading is that choosing Tandy graphics also selects Tandy sound, which would explain why the sound menu is one shorter than the driver count. That is an inference from two menus, not a measurement, and it is listed as such in 12-open-questions.md.

What the executables name

QUEST.EXE's string table is a complete manifest of the game's data files, and it is laid out as two parallel lists — fourteen .vga names then the same fourteen .ega names, in the same order — which is how a program indexed by display mode stores its filenames:

book monsters sprites odds&sod maps graphics furn shopsp shopmp bordered
wizback invent spellit men      (each once as .vga, once as .ega)
questb.vga  questb.ega  equests.bin
english.bin french.bin spanish.bin german.bin italian.bin
quest1.bin .. quest24.bin
quest.sav  quest.cfg
beeper.drv adlib.drv mt32.drv tandy.drv  game&fx.alb game&fx.rld
shields.ega

INTRO.EXE's list is different and shorter, and it is the answer to why ten pictures exist only in VGA form:

hero1.vga hero2.vga hero4.vga hero6.vga hero3.vga hero7.vga
gremlin.vga anims.vga pic1.vga text1.vga
shopmp.ega flags.ega bordered.ega
quest.cfg  quest.exe  intro.exe

Note the order: hero1, hero2, hero4, hero6, hero3, hero7. There is no hero5.vga in the list, so the gap in the file numbering is a gap in the numbering and not a missing file — the intro uses six pictures numbered 1, 2, 3, 4, 6, 7 and never asks for a fifth. That closes a question that the directory listing alone could only raise.

Every file named by either executable exists in the archive, with the single exception of quest.cfg, which is created at run time. There are no paths, no drive letters and no development-machine directories anywhere in either program; the strings are bare 8.3 filenames, all lower case, which is what a program that does its own chdir looks like.

The other strings in QUEST.EXE

The copy-protection screen and the character-name entry screen live here, and they are read in 05-install-and-copy-protection.md and 07-font-and-text.md respectively. The epilogue — five hundred bytes of prose about the Witch Lord, Skulmar and Sir Ragnar — is in QUEST.EXE in plain uppercase English, not in any language file, which is part of the localisation story in 06-language-files.md.