LLX > Neil Parker > Apple II > Loader Secrets
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.
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 |
Two new Loader calls were introduced in System 6. These are available if the major version number returned from LoaderVersion is 4.
$1E11
: GetGlobalsAddrStack 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.
$1F11
: Switch_handlerStack 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.
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.
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.)
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.
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. |
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.
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 |
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 |
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) |
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. |
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. |
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 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.
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:
|
||||||||||||||||||||||||
(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:
|
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 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:
|
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.
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).