LLX > Neil Parker > Apple II > Loader Secrets

Undocumented Secrets of the Apple IIGS System Loader

This article is a collection of undocumented information about the IIGS System Loader, the component of the operating system responsible for bringing executable code into memory and relocating to run at an arbirtrary address. These tidbits do not stand alone - the reader is assumed to be familiar with the Loader documentation in Apple IIGS GS/OS Reference, and possibly its predecessor Apple IIGS ProDOS 16 Reference.

Loader Versions

Since much of the information below varies depending on which version of the Loader is available, it's necessary to test the Loader version before using this information. Testing the major version number (the part to the left of the decimal point) is sufficient - as far as I can tell, none of information below changed when a minor version number changed.

A machine-language program can easily find the Loader major version like this (assuming 16-bit native mode):

        pha            ;Space for result
        ldx #$0411     ;LoaderVersion
        jsl $E10000
        pla
        and #$0F00     ;Mask off minor version and prototype bits
        xba            ;Get major version into low byte

Getting the GS/OS (or ProDOS 16) version number also works, since its version parallels the Loader version (or at least the major version does - the minor version is often different), but calling LoaderVersion is easier, because no parameter buffer needs to be set up.

Loader Version System Software Version Notes
1 1.0 through 3.2 Original version; ProDOS 16
2 4.0 GS/OS; new global area layout; new Pathname table format
3 5.0 through 5.0.4 ExpressLoad introduced as an optional component
4 6.0, 6.0.1 New global area layout; ExpressLoad folded into main Loader

New Loader Calls Introduced in System 6

Two new Loader calls were introduced in System 6. These are available if the major version number returned from LoaderVersion is 4.

Tool call $1E11: GetGlobalsAddr

Stack before call:

|          |
+----------+
|          |  
+          +  Longword result space
|          |
+----------+
|          |  <-- SP

Stack after call:

|          |
+----------+
|          |  
+          +  Pointer to Loader globals
|          |
+----------+
|          |  <-- SP

This call returns a pointer to the Loader's global variable area. The locations of some useful fields in this area are described below.

Tool call $1F11: Switch_handler

Stack before call:

|          |
+----------+
|          |  Word command code: 0 = switch GS/OS to P8, 1 = switch P8 to GS/OS
+----------+
|          |  <-- SP

Stack after call:

|          |
+----------+
|          |  <-- SP

This call saves (if the command word is 0) or restores (if the command word is 1...actually any non-zero value will do) the Loader's direct page.

A Save operation copies the first 234 ($EA) bytes from the Loader's direct page into a buffer in the Loader's global variable area, and disposes the direct page's handle.

A Restore operation allocates a new handle for the Loader's direct page, and copies 234 ($EA) bytes from a buffer in the Loader's global area into it.

There is only one save buffer, so save/restore calls are not nestable.

This call is part of the process of switching between GS/OS and ProDOS 8, and probably should not be called by an application.

Loader Global Variables

The Loader keeps a block of data set aside for global variables. The location and layout of this block are different in different System Software versions.

System 1.0 through System 3.2 (ProDOS 16)

In ProDOS 16 (all System Software versions up to 3.2; LoaderVersion returns major version 1), the global variable block starts at $01/E700. Here are some useful entries in it:

Location Type Contents
$01/E700 word Loader busy flag
$01/E702 long Handle to Memory Segment Table
$01/E706 long Handle to Jump Table Directory
$01/E70A long Handle to Pathname Table
$01/E70E word User ID
$01/E710 word Total errors
$01/E712 5 words? Error addresses
$01/E71C long Address of jump table load function. A JSL to this address is patched into each entry of every jump table segment loaded from disk.
$01/E72C word Function number of most-recently-called Loader function
$01/E72E word Number of LCONST records loaded
$01/E730 word Number of RELOC records loaded
$01/E732 word Number of INTERSEG records loaded
$01/E734 word Number of DS records loaded
$01/E736 word Number of cRELOC records loaded
$01/E738 word Number of cINTERSEG records loaded
$01/E73A word Number of segments loaded
$01/E73C long Number of bytes loaded
$01/E740 long System clock at start of most recent InitialLoad or Restart
$01/E744 long System clock at end of most recent InitialLoad or Restart
$01/E79E long File buffer address
$01/E7A2 word File buffer size
$01/E7A4 word Max size of small buffer
$01/E7A6 word Max size of large buffer
$01/E7A8 word Offset of next location to read from file buffer
$01/E7AA word Offset of last valid byte in file buffer
$01/E7AC long File mark of last byte read from file being loaded
$01/E7B0 long File mark of beginning of next segment in file being loaded
$01/E7B4 26 bytes Loader's GET_FILE_INFO parameter list
$01/E7CE 10 bytes Loader's OPEN parameter list
$01/E7D8 14 bytes Loader's READ parameter list
$01/E7E6 2 bytes Loader's CLOSE parameter list
$01/E7E8 6 bytes Loader's GET_EOF parameter list
$01/E7EE 6 bytes Loader's GET_MARK parameter list
$01/E7F4 6 bytes Loader's SET_MARK parameter list
$01/E7FA 6 bytes Loader's GET_PREFIX parameter list

Note that these addresses are in the language card area, so you may need to fiddle with with language card switches to see them. (This is most easily done by clearing the $08 bit in I/O location $C068. Don't forget to restore the original setting afterwords.)

System 4.0 through System 5.0.4

The first two major releases of GS/OS moved the Loader global block to $01/FB00, and rearranged its contents. This is the layout if LoaderVersion returns major version 2 or 3:

Location Type Contents
$01/FB00 long Handle to Memory Segment Table
$01/FB04 long Handle to Jump Table Directory
$01/FB08 long Handle to Pathname Table
$01/FB0C word User ID
$01/FB0E word Total errors
$01/FB10 word Last error
$01/FB12 4 words? Error addresses
$01/FB1A long Address of jump table load function. A JSL to this address is patched into each entry of every jump table segment loaded from disk.
$01/FB1E long Address of "LCReturn" (whatever that is)
$01/FB22 word Function number of most-recently-called Loader function
$01/FB24 word Number of LCONST records loaded
$01/FB26 word Number of RELOC records loaded
$01/FB28 word Number of INTERSEG records loaded
$01/FB2A word Number of DS records loaded
$01/FB2C word Number of cRELOC records loaded
$01/FB2E word Number of cINTERSEG records loaded
$01/FB30 word Number of SUPER records loaded
$01/FB32 word Number of segments loaded
$01/FB34 long Number of bytes loaded
$01/FB78 long File buffer address
$01/FB7C word File buffer size
$01/FB7E word Offset of next location to read from file buffer
$01/FB80 word Offset of last valid byte in file buffer
$01/FB82 long File mark of last byte read from file being loaded
$01/FB86 long File mark of beginning of next segment in file being loaded
$01/FB8A 38 bytes Loader's Open parameter list
$01/FBB0 14 bytes Loader's Read parameter list
$01/FBBE 2 bytes Loader's Close parameter list
$01/FBC0 6 bytes Loader's GetMark parameter list
$01/FBC6 6 bytes Loader's SetMark parameter list
$01/FBCC 12 bytes Loader's ExpandPath parameter list
$01/FBD8 4 bytes Loader's sys_prefs parameter list
$01/FBDC 6 bytes Loader's Getlevel parameter list
$01/FBE2 6 bytes Loader's Setlevel parameter list
$01/FBE8 8 bytes Loader's GetRefNum parameter list

As in the ProDOS 16 case, these addresses are in the language card area.

System 6.0.x

In System 6, the global block moved again, and was rearranged again. I've never seen it at any address other than $01/A68C, but that shouldn't be assumed - instead of hardwiring that address, check that LoaderVersion returns major version 4, and if so, use the new tool call $1E11 to find the global block, and use the offsets shown below to index into it.

Offset Type Contents
+0 word "Loader not initialized" flag (0=Loader initialized...the non-initialized value is the ASCII characters "GB", the initials of System 6 Loader author Greg Branche)
+2 long Handle to Loader direct page
+6 word Pointer to Loader direct page (in bank 0, of course)
+$1E long Handle to Memory Segment Table
+$22 long Handle to Jump Table Directory
+$26 long Handle to Pathname Table
+$2A word User ID
+$2C $48 bytes Buffer for segment header (fixed part, up to DISPDATA)
+$74 long Pointer to buffer for variable part of segment header
+$78 $10 bytes Buffer for OMF record
+$88 long File buffer address
+$8C word File buffer size
+$8E word Offset of next location to read from file buffer
+$90 word Offset of last valid byte in file buffer
+$92 long File mark of last byte read from file being loaded
+$96 long File mark of beginning of next segment in file being loaded
+$9A 38 bytes Loader's Open parameter list (class 1)
+$C0 14 bytes Loader's Read parameter list (class 0)
+$CE 6 bytes Loader's GetMark parameter list (class 0)
+$D4 6 bytes Loader's SetMark parameter list (class 0)
+$DA 12 bytes Loader's ExpandPath parameter list (class 1)
+$E6 4 bytes Loader's SysPrefs parameter list (class 1)
+$EA 6 bytes Loader's GetLevel parameter list (class 1)
+$F0 6 bytes Loader's SetLevel parameter list (class 1)
+$F6 8 bytes Loader's GetRefNum parameter list (class 1)
+$FE 32 bytes Loader's GetFileInfo parameter list (class 1)
+$11E $EA bytes Direct page save buffer
+$20A pstring "EXPRESSLOAD" (without the quotes; Pascal-style string with length byte)
+$216 pstring "~EXPRESSLOAD" (without the quotes; Pascal-style string with length byte)
+$223 long JSL to jump table load instruction. This is patched into each entry of every jump table segment loaded from disk.

Loader Lists

This section shows the layouts of the three lists referenced in the Loader global area (the Memory Segment Table, the Jump Table Directory, and the Pathname Table). These three lists contain all the information you need to find every loadabie file currently known to the Loader, and every segment that has been loaded from those files.

"Known to the Loader" means that InitialLoad (or InitialLoad2) has been called to load the file (or the file is a run-time library referenced by an InitialLoad'ed file), and that UserShutDown has not been called to remove the file from memory. But note that files handled by ExpressLoad are not entered into these lists - ExpressLoad keeps its own lists elsewhere in a different format.

Actually, most of the information in this section is documented (in Apple IIGS ProDOS 16 Reference), but is included here anyway for completeness, to accompany the undocumented new format(s) of the Pathname Table.

Memory Segment Table

The Memory Segment Table is a doubly-linked list containing one entry for every segment that has been loaded from a load file (static segments are added to this list at InitialLoad time; dynamic segments are added the first time they are actually brought into memory). The handle of the list's first entry can be found in the Loader global area as described above.

Here is the layout of a Memory Segment Table entry:

Offset Type Contents
+0 long Handle to next entry (0 = end of list)
+4 long Handle to previous entry (0 = beginning of list)
+8 word User ID for file segment was loaded from
+$A long Handle to segment contents
+$E word Load file number (essentially a serial number, starting at 1, that distinguishes different files loaded with the same User ID)
+$10 word Segment number of segment in load file (the first segment is number 1)
+$12 word KIND field from segment header

Jump Table Directory

The Jump Table Directory is a doubly-linked list containing one entry for every jump table segment currently known to the Loader. The handle of the list's first entry can be found in the Loader global area.

Here is the layout of a Jump Table Directory entry:

Offset Type Contents
+0 long Handle to next entry (0 = end of list)
+4 long Handle to previous entry (0 = beginning of list)
+8 word User ID for file segment was loaded from
+$A long Handle to jump table image in memory

Pathname Table (ProDOS 16 format)

The Pathname Table is a doubly-linked list containing one entry for every loadable file currently known to the Loader. The handle of the list's first entry can be found in the Loader global area.

Here is the layout of a Pathname Table entry under ProDOS 16 (LoaderVersion returns major version 1):

Offset Type Contents
+0 long Handle to next entry (0 = end of list)
+4 long Handle to previous entry (0 = beginning of list)
+8 word User ID for file
+$A word Load file number (essentially a serial number, starting at 1, that distinguishes files loaded with the same User ID)
+$C word File modification date (in two-byte ProDOS format)
+$E word File modification time (in two-byte ProDOS format)
+$10 word Direct page/stack address (if the file contains a direct page/stack segment, 0 if not)
+$12 word Direct page/stack length (if the file contains a direct page/stack segment, 0 if not)
+$14 word Jump-table-loaded flag (1 for InitialLoad files, initially 0 for run-time libraries)
+$16 pstring Full pathname of file (Pascal-style string with length byte)

Pathname Table (System 4.0 format)

The Pathname Table changed under GS/OS, because the date/time fields and the file path could not accommodate the new formats that GS/OS uses for such fields. Here is the new layout under GS/OS System 4.0 (LoaderVersion returns major version 2):

Offset Type Contents
+0 long Handle to next entry (0 = end of list)
+4 long Handle to previous entry (0 = beginning of list)
+8 word User ID for file
+$A word Load file number (essentially a serial number, starting at 1, that distinguishes files loaded with the same User ID)
+$C 8 bytes File modification date and time (in 8-byte GS/OS/ReadTimeHex format)
+$14 word Direct page/stack address (if the file contains a direct page/stack segment, 0 if not)
+$16 word Direct page/stack length (if the file contains a direct page/stack segment, 0 if not)
+$18 word Jump-table-loaded flag (1 for InitialLoad files, initially 0 for run-time libraries)
+$1A GS/OS string Full pathname of file (GS/OS-style string with length word). If the pathname length is 0, the file was loaded from a memory image.

Pathname Table (System 5.0.x format)

In system 5.0.x, two additional fields were added to the layout. If LoaderVersion return major version number 3, it looks like this:

Offset Type Contents
+0 long Handle to next entry (0 = end of list)
+4 long Handle to previous entry (0 = beginning of list)
+8 word User ID for file
+$A word Load file number (essentially a serial number, starting at 1, that distinguishes files loaded with the same User ID)
+$C 8 bytes File modification date and time (in 8-byte GS/OS/ReadTimeHex format)
+$14 word Direct page/stack address (if the file contains a direct page/stack segment, 0 if not)
+$16 word Direct page/stack length (if the file contains a direct page/stack segment, 0 if not)
+$18 word Jump-table-loaded flag (1 for InitialLoad files, initially 0 for run-time libraries)
+$1A long Pointer to entry point
+$1E word ?Uncertain...maybe the file reference number if the load file is currently open
+$20 GS/OS string Full pathname of file (GS/OS-style string with length word). If the pathname length is 0, the file was loaded from a memory image.

Pathname Table (System 6.0.x format)

In System 6.0.x, the file number field at +$1E was removed, making the layout look like this if LoaderVersion returns major version 4:

Offset Type Contents
+0 long Handle to next entry (0 = end of list)
+4 long Handle to previous entry (0 = beginning of list)
+8 word User ID for file
+$A word Load file number (essentially a serial number, starting at 1, that distinguishes files loaded with the same User ID)
+$C 8 bytes File modification date and time (in 8-byte GS/OS/ReadTimeHex format)
+$14 word Direct page/stack address (if the file contains a direct page/stack segment, 0 if not)
+$16 word Direct page/stack length (if the file contains a direct page/stack segment, 0 if not)
+$18 word Jump-table-loaded flag (1 for InitialLoad files, initially 0 for run-time libraries)
+$1A long Pointer to entry point
+$1E GS/OS string Full pathname of file (GS/OS-style string with length word). If the pathname length is 0, the file was loaded from a memory image.

ExpressLoad

ExpressLoad was introduced in System 5.0, and is an optional feature through System 5.0.4. It's always present in System 6. If a loadable file begins with a special ExpressLoad segment, ExpressLoad takes over, and using information in the ExpressLoad segment, loads the file much more rapidly than the Loader could.

The ExpressLoad Segment on Disk

An ExpressLoad-able file begins with an ExpressLoad segment, which is an ordinary segment with VERSION = 2 (OMF version 1 files cannot be ExpressLoaded) and KIND = $8001 (a dynamic data segment, so it will be skipped during InitialLoad if ExpressLoad is not available), and SEGNUM = 1 (it must be the file's first segment). The segment name is EXPRESSLOAD or ~EXPRESSLOAD (the latter is preferred, and the match is case-insensitive). The segment's body consists of a single LCONST record, with the following contents:

Offset Type Contents
+0 word File reference number, if the file is currently open (always 0 on disk)
+2 word Two bytes reserved for the Loader's use (always 0 on disk)
+4 word Number of segments in the file, minus 2 (note that an ExpressLoad-able file always has at least two segments - the ExpressLoad segment and one or more others)
+6 8*N bytes Segment list. An array of 8-byte elements, one for each segment (except the ExpressLoad segment), each with the following contents:
Offset Type Contents
+0 word Self-relative offset to this segment's entry in the segment header info array
+2 word Flags (always 0 on disk):
Bit Meaning
15 1 = segment currently in memory
14-3 unused (0)
2 1 = segment is static
1 1 = segment is jump table
0 1 = segment is purgeable
+4 long Handle to segment's data in memory (always 0 on disk)
(varies) 2*N bytes Segment remapping list. An array of 2-byte elements, indexed by the "old" segment number (the number the segment would have if the ExpressLoad segment were not present) minus 1, giving "new" segment number (the number the segment actually has in the file). One entry for each segment (except the ExpressLoad segment).
(varies) (varies) Segment header info array. An array of variable-length elements, one for each segment (except the ExpressLoad segment), each with the following contents:
Offset Type Contents
+0 long Offset from beginning of the file to the data part of this segment's LCONST record
+4 long Length of this segment's LCONST record
+8 long Offset from beginning of the file to this segment's first relocation record
+$C long Total length of this segment's relocation records
+$10 (varies) Copy of this segment's header. The copy is not complete - it omits the first 12 bytes (i.e. the BYTECNT, RESSPC, and LENGTH fields), and the DISPDATA field is set to 0.
Since these entries are variable-length, they must be found using the offsets given in the segment list above. Note that this structure implies that all segments in an ExpressLoad-able file must store their data in a single LCONST record (i.e., no DS records are allowed).

The offsets and lengths in the segment header info array are what makes ExpressLoad fast. Using this information, it can go directly to wherever it needs to go in the file, without having to read and parse the entire file.

The segment remapping list is used to make LoadSegNum and UnloadSegNum work as if the ExpressLoad header were not present. This is done so that applications don't need to worry about whether or not they've been made ExpressLoad-able. Paradoxically, this is also why LoadSegNum and UnloadSegNum should not be used on an ExpressLoad-able application if there's any possibility of it running on a system where ExpressLoad is not available - they would access different segments depending on whether or not the application was loaded by ExpressLoad.

The segment remapping list allows for the possibility that segments could be reordered when the ExpressLoad segment is added - for example, the APW/Orca EXPRESS utility (which converts a non-ExpressLoad file into an ExpressLoad file) likes to move dynamic code segments to the end of the file. (This feature is rarely used by development environments that produce ExpressLoad executables ab initio - in that case one usually finds segment_remapping_list[i] = i + 2.)

ExpressLoad Data Structures in Memory

ExpressLoad ignores the Loader data structures described above, and instead hangs its information off of a single master list of ExpressLoaded files. Finding the start of this list can be tricky.

So how do you find the direct page? If the Loader major version is 4, it's easy - the direct page's address is in the word at offset +6 in the Loader's global area. The hard case is major version 3 - I haven't found any easier method than scanning the Memory Manager's handle list for a 256-byte block in bank 0 with User ID $7050. If no such handle exists, then ExpressLoad is not available.

ExpressLoad's master file list is a doubly-linked list of 2048-byte blocks, each with the following layout:

Offset Type Contents
+0 long Handle to next block of list (0 = end of list)
+4 long Handle to previous block of list (0 = beginning of list) (? maybe - I've never seen seen the list get full enough to need a second block)
+8 8 bytes reserved (0)
+$10 16*N bytes Array of up to 127 16-byte entries, one for each ExpressLoaded file, each with the following layout:
Offset Type Contents
+0 word User ID for file (0 if this entry is unused)
+2 long Handle to ExpressLoad data from disk
+6 word Direct page/stack address (if the file contains a direct page/stack segment, 0 if not)
+8 word Direct page/stack length (if the file contains a direct page/stack segment, 0 if not)
+$A long Pointer to entry point
+$E word reserved (0)

The handle whose offset is at +2 in the file entry contains the ExpressLoad segment content, along with other information. In system 5.0.x (LoaderVersion returns major version 3), the handle's contents are as follows:

Offset Type Contents
+0 6 bytes Unknown use. Always 0 in the examples I've seen.
+6 word Offset from beginning of handle to first part of file's pathname
+8 word Offset from beginning of handle to second part of file's pathname
+$A 6 bytes Unknown use. Always 0 in the examples I've seen.
+$10 (varies) ExpressLoad data from disk
+N pstring First part of file's pathname (Pascal-style string with length byte, 39 chars max)
+N+40 pstring Second part of file's pathname (Pascal-style string with length byte)

The file's full pathname can be constructed by concatenating a colon (":"), the first part of the name, another colon, and the second part of the name. In all the examples I've seen, the first part contains the volume name, and the second part contains the remainder of the full path.

In System 6.0.x (LoaderVersion returns major version 4), the handle's contents look like this:

Offset Type Contents
+0 (varies) ExpressLoad data from disk. The word at offset +2 gives the offset from the beginning of the handle to the file's pathname.
+N-8 8 bytes File modification date/time, in GS/OS/ReadTimeHex format
+N GS/OS string Full pathname of file (GS/OS-style string with length word)

The offset from the beginning of the ExpressLoad handle to the pathname can be found at offset +2 of the ExpressLoad handle. To find the offset of of the modification date/time, subtract 8 from the offset of the pathname.

Download

Here's a NiftyList module, with source code (APW/ORCA assembler), that prints information from the Loader's tables.

Requirements: Apple IIGS with System 6.0.1 or System 5.0.4 (may also work with other system software versions, but that hasn't been tested) and NiftyList.

LLX > Neil Parker > Apple II > Loader Secrets

Original: July 30, 2017
Modified November 10, 2019--Fixed description of how to find Loader version; noted that the ExpressLoad segment remapping list does sometimes actually reorder the segments.
Modified September 27, 2020--Fixed several major unwarranted assumptions. It turns out System 4 and System 5 data structures are not the same as System 6!
Modified October 4, 2020--Added more info about Loader globals (mostly culled from various versions of the Loader Dumper CDA); more corrections; added NiftyList module download.
Modified October 11, 2020--Added more System 6 info (extracted from the System 6.0.1 source code); added display of ExpressLoad flags to NiftyList module's \xsegments command.
Modified November 1, 2020--Fixed a bug in the NiftyList module (the routine that searches for an ID in ExpressLoad's file list was handling the end of the block wrong).