(This post is part of a series on the subject of my hobby project, which is recreating the C source code for the 1989 game F-15 Strike Eagle II by reverse engineering the original binaries.)

In the initial post about the game, I briefly touched on the subject of overlay usage, which I want to cover in more detail now. The game uses overlay executables to reduce code duplication between the executables which make up the game, and also reduce the executable size by moving the shared code into the overlays. Interestingly, compilers of the time, including MS C 5.10 which the game originally seems to have been written in, support overlays natively, and the MS C linker is even advertised as an “overlay linker” in the compiler’s documentation. However, the approach used by the compiler seems to be using an interrupt (0x3f for MS C by default), and for some reason (probably performance?), Microprose appears to have rolled their own solution.

When launched, the game allocates a 320-paragraph (5KiB) buffer (which I call COMM in my code), and places its segment at address 0:4F0, which is the IACA, or Inter Application Communication Area. This is a rarely used, 16-byte sized area of the IBM PC low memory, and the game uses it to store the address of the shared buffer, as well as for some flags to exchange information between the executables that make up the game.

The layout of the buffer is not fully known yet, but I have discovered some things. Among others, the filename of the selected graphics driver overlay is put at offset 0x0, and the sound driver overlay is at 0xd. After the game is launched, F15.COM will execute SU.EXE (SetUp) which displays a menu for the user to select the drivers (unless special commandline switches are used which preselect the drivers and bypass the menu). SU will place the filenames of the drivers in COMM and return to F15.COM, which will load the drivers (sound, video and the MISC.EXE driver) into memory using the DOS function 21.4b03. This along with 21.4b01 is an interesting call which will load an executable into preallocated memory, but won’t execute it. Good thing too, as the overlay exes are not meant to be executed, they are more like dynamically-loaded libraries containing a set of functions to be executed when necessary. Still, the game needs to know where these functions reside, and to that end the load modules of those executables contain a header of the following form:

// Microprose overlay header format
struct OvlHeader {
    uint8_t description[0x18]; // 00h-17h: description
    uint16_t header_paragraphs; // 18h-19h: total number of header paragraphs, after relocation becomes segment pointer into code
    uint16_t base_segment; // 1ah-1bh: base load segment of overlay (relocated)
    uint16_t first_slot; // 1ch-1dh: slot index (id) of first jump entry 
    uint16_t size1; // 1eh-1fh: size1
    uint16_t size2; // 20h-21h: size2
    uint16_t jump_count; // 22h-23h: number of jump addresses 
    // 24h-...: array of jump offsets
    // Extra description or padding data follows
};

I have written a simple tool to parse and display this information, here’s an example:

ninja@dell:eaglestrike$ tools/ovltool bin/451.03/mgraphic.exe
[0x0] description: 'MGRAPHIC.EXE09-19-88'
[0x18] header_paragraphs = 1d0 / 0x1d00 bytes
[0x1a] base_segment = 0x0
[0x1c] first_slot = 0x0
[0x1e] size1 = 0x1d0d
[0x20] size2 = 0xa5a
        total size = 0x2767
[0x22] jump_count = 84
--- jump offsets:
[0x24]: slot 0x0, offset 0x25e
[0x26]: slot 0x1, offset 0x461
[0x28]: slot 0x2, offset 0x3dc
[...]

This is the driver for the MCGA graphics, and it contains 84 callable functions, whose offsets are placed in an array of jump “slots” directly following the header. The size of the header (in paragraphs) is stored in the header and relocated at driver load time, so if this driver were loaded at segment 0x1000, the header length field would contain 0x11d0, which conveniently will be a pointer to the segment where the header ends and the actual code begins, so the jump offsets into the functions are actually using this segment as the base.

I don’t know why the size of the overlay seems to be split in two in the header, in the case of some it looks like size1 is the header size but this time in bytes, while size2 is the size of the actual code, but for others it seems to be zero instead. Go figure.

The slot numbers are hardcoded, and the ranges are preassigned to the driver types. Here is a similar output for the EGA graphics driver:

ninja@dell:eaglestrike$ tools/ovltool bin/451.03/egraphic.exe
[0x0] description: 'EGRAPHIC.EXE07-27-88'
[0x18] header_paragraphs = 215 / 0x2150 bytes
[0x1a] base_segment = 0x0
[0x1c] first_slot = 0x0
[0x1e] size1 = 0x2155
[0x20] size2 = 0x1ab9
        total size = 0x3c0e
[0x22] jump_count = 84
--- jump offsets:
[0x24]: slot 0x0, offset 0xd0
[0x26]: slot 0x1, offset 0x627
[0x28]: slot 0x2, offset 0x4de
[...]

Again, 84 functions. Depending on their order (slot number) within the jump offset table, these functions perform a specific high level operation, like initialize the hardware, or display some pixels (I don’t yet possess a full map on which number does what), so the game may just say “call function 12”, and that will do the Right Thing, both for MCGA and EGA. Sometimes, the Right Thing might be to not do anything, which is the case for the NSOUND (no sound) driver, whose all slots redirect to the same jump offset, which presumably does nothing (I didn’t bother to check).

It looks like the following slot numbers are used by the drivers:

  • 0x0 - 0x53: graphics, 84 functions
  • 0x5a - 0x5f: misc.exe driver, 6 functions
  • 0x64 - 0x6d: sound, 10 functions

The misc driver is always loaded, and contains a couple functions related to handling keyboard and joystick input. It’s tiny, not sure why they bothered.

After F15.COM loads these overlays, it will place their load segments in the COMM structure at the following offsets:

#define COMM_GFXOVL_ADDR_OFFSET 0x1a
#define COMM_SNDOVL_ADDR_OFFSET 0x1c
#define COMM_MISCOVL_ADDR_OFFSET 0x1e

From there, the remaining parts of the game (START, EGAME and END) will populate a statically-allocated array which initially just contains the far jump (0xEA) opcode followed by four null bytes, repeated the required number of times to cover the number of slots necessary. At runtime, the null bytes will be patched with the code segment of the overlay and an offset into a specific overlay function based on the overlay header and jump slot table. Later, when the game needs to invoke a driver function, it will do a far call into a data segment location corresponding to the function number (remember, each entry in the array is 5 bytes: the far jump opcode + the four byte far jump location), which in turn will do a far jump into the overlay function code, and the function always returns with retf to where it was called from. Simple, but effective.

I have code for loading and setting up the overlays already reimplented in C, and it works, but the individual driver functions will need to be analyzed and understood for the source code reconstruction. They look like they have been hand-crafted in assembly - makes sense since they are low-level functions that need to perform efficiently, which makes me wonder how much of a penalty they are paying for doing a far call + far jump + far return every time a driver call is needed. In the final recreation, I probably will get rid of the overlays, since I’m planning to only support MCGA and no sound, and size is not a concern, so I think I’ll just link in the driver code statically. For now, discovering and recreating this has been necessary to understand how the game works, and also for performing automated code comparison with mzretools/mzdiff - I need the code to match in regard to the emitted far calls to be able to compare the recreation to the original on an opcode-for-opcode basis.

Interestingly, although Civilization uses an almost identical setup menu and also contains multiple exes that look like sound and graphic drivers based on their name, the overlay header format of those seems to be different, and could not be parsed by my tool. Seems likey they were updating the scheme as they went along (Civ 1 came out 1991, so after F15-II).