Overview
This appendix documents the binary execution formats used by RISC OS: the AIF (absolute) format, the relocatable module format, and the utility format. Each section first describes the original 26/32-bit format, then describes how the 64-bit (AArch64) variant of that format extends it.
This appendix covers only the file formats themselves: header layout, field meanings, and how a loader distinguishes between bitnesses and architectures. It does not document the SWIs used to load and run these formats (such as OS_Module or OS_FSControl), nor the service calls a module may handle. The 64-bit binary tools that produce these formats are covered in 64-bit Development and Compilers, linkers, and 64-bit binary tools.
The canonical sources for each format are cited at the start of its section, and gathered together in Bibliography.
AIF format
The base 26/32-bit AIF format is specified in ARM Image Format (1993). The 64-bit extension to that format is specified in the gerph/riscos64-clib repository's README.
26/32-bit AIF header
ARM Image Format (AIF) consists of a fixed, 128-byte (32-word) header followed by the image's code and initialised static data. Executable AIF is the common case: the header is part of the image, and is itself entered at its first word. Non-executable AIF instead describes an image to be prepared and entered by a separate loader; the two are distinguished by the fourth header word, which is a branch instruction for executable AIF and a plain entry-point offset for non-executable AIF.
| Offset | Size | Field | Contents |
|---|---|---|---|
| 0 | 4 | Branch to decompression code, or NOP if the image is not compressed. | |
| 4 | 4 | Branch to self-relocation code, or NOP if the image is not self-relocating. | |
| 8 | 4 | Branch to zero-initialisation code, or NOP if the image has no zero-initialised area or debug-init action. | |
| C | 4 | Executable AIF: branch to the image entry point. Non-executable AIF: the entry point's offset from the image base address. | |
| 10 | 4 | Program exit instruction, executed only if the entry point returns; the ARM linker sets this to SWI &11 by default. | |
| 14 | 4 | Image read-only size, in bytes. Includes the header size for executable AIF; excludes it for non-executable AIF. | |
| 18 | 4 | Image read-write size, in bytes. A multiple of 4. | |
| 1C | 4 | Image debug size, in bytes. A multiple of 4. | |
| 20 | 4 | Image zero-initialised size, in bytes. A multiple of 4. | |
| 24 | 4 | Image debug type: 0 none, 1 low-level, 2 source-level (ASD), 3 both. Other values are reserved. | |
| 28 | 4 | Image base: the address the image's code was linked at. | |
| 2C | 4 | Workspace: minimum free workspace, in bytes, to be reserved by a self-moving relocatable image. | |
| 30 | 4 | Address mode. Least significant byte is 26 or 32, naming the ARM mode the image was linked for, or 0 for an old-style 26-bit header. Bit 8 set indicates the image uses a separate data base, given by the next word. | |
| 34 | 4 | Data base: the address the image's data was linked at. Meaningful only when bit 8 of the address mode is set. | |
| 38 | 4 | Reserved, must be zero | |
| 3C | 4 | Reserved, must be zero | |
| 40 | 4 | Debug-initialisation instruction (a SWI that alerts a resident debugger), or NOP if unused. | |
| 44-7B | 56 | Zero-initialisation code (14 words), supplied by the linker and position-independent. |
This table can be checked against a real file. Building a minimal command skeleton and dumping its header shows every field in place:
riscos-project create --name MyCommand --type command --skeleton
riscos-amu BUILD32=1
riscos-dump aif32/MyCommand,ff8
Offset : 0 1 2 3 4 5 6 7 8 9 A B C D E F : Text
8000 : 00 00 A0 E1 00 00 A0 E1 0C 00 00 EB 85 02 00 EB : ................
8010 : 11 00 00 EF 70 13 00 00 14 00 00 00 00 00 00 00 : ....p...........
8020 : 64 0E 00 00 00 00 00 00 00 80 00 00 00 00 00 00 : d...............
8030 : 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : ...............
8040 : 00 00 A0 E1 0F C0 4E E0 0C C0 8F E0 0F 00 9C E9 : ......N.........
riscos-dump labels each row with the image's load address, &8000, so the table's file offsets map directly onto these rows by subtracting that base. Reading off a few fields confirms the table: offset +0 and +4 are both 00 00 A0 E1 (MOV r0, r0, the canonical ARM NOP encoding), so neither decompression nor self-relocation is in use; +10 is 11 00 00 EF (SWI &11), the linker's default exit instruction; +14 is 70 13 00 00, a little-endian read-only size of &1370 bytes; +20 is 64 0E 00 00, a zero-initialised size of &0E64 bytes; +28 is 00 80 00 00, an image base of &8000, matching the load address; and +30 is 20 00 00 00, an address mode whose least significant byte, &20 (32 decimal), confirms a 32-bit image.
If the image is self-relocating, the linker appends self-move and self-relocation code immediately after the image, followed by a relocation list of word offsets terminated by -1. These follow the image body rather than forming part of the fixed header.
64-bit AIF extension
The 64-bit AIF format reuses the same 32-word, 128-byte header described above, with the same field meanings at the same offsets, but with content fixed so that a 32-bit RISC OS loader sees a normal, harmless AIF file:
- Words at +0, +4, and +8 are all NOP: the image is never compressed, self-relocating, or zero-initialised in a way a 32-bit loader needs to act on.
- The entry branch at +0C goes to +40, the header's debug-initialisation slot, which is repurposed to begin an ARM32 error-reporting stub rather than a real debug hook.
- The exit instruction at +10 is SWI OS_Exit, reached only if the error report returns.
- The address mode word at +30 is &40, a value with no defined 26-bit or 32-bit meaning. This is what a 64-bit loader recognises to identify the image as AArch64, in place of the 26 or 32 values used by AArch32 absolutes.
- The zero-initialisation code area at +44-+7B is repurposed to hold the ARM32 error-report sequence's error block and message text, rather than genuine zero-initialisation code.
| Offset | Size | Encoding | Meaning |
|---|---|---|---|
| 40 | 4 | entry: &e28f0000 | ARM32 ADR r0, error_block. Start of the ARM32 error-report sequence. |
| 44 | 4 | &ef00002b | ARM32 SWI OS_GenerateError, which generates an error from the block at R0 and hands control to the installed error handler; see RISC OS PRM: OS_GenerateError. |
| 48 | 4 | error_block: &0 | ARM32 DCD 0. Error number word of the error block. |
| 4C | 48 | ARM32 error block message text: "AArch64 binaries cannot be run on 32-bit RISC OS", zero terminated. | |
| 7C | 4 | Reserved, must be zero | |
| 80-FF | Reserved, must be zero | ||
| 100 | 4 | &D503201F | AArch64 NOP. AArch64 decompression placeholder. |
| 104 | 4 | &94000004 | AArch64 BL zeroinit. AArch64 zero-initialisation. |
| 108 | 4 | &9400001F | AArch64 BL entry. AArch64 entry point. |
A 32-bit RISC OS loader therefore sees a normal AIF file whose entry point reports the "AArch64 binaries cannot be run on 32-bit RISC OS" error and exits. A 64-bit loader recognises the &40 address mode and instead transfers control to the AArch64 code beginning at +&100, beyond the end of the standard header.
Module format
The base relocatable module header is specified in RISC OS PRM: Modules. Its RISC OS 3.6 extension is specified in RISC OS PRM: Modules (changes), and its 32-bit-safety extension in RISC OS 6 PRM: Code formats. The architecture-identifying extension to the feature table, and the 64-bit-specific module behaviour, are specified in Pyromaniac PRM: Modules supplement and in the gerph/riscos64-clib repository's README.
26/32-bit module header
A relocatable module begins with a header of offsets from the start of the module to code and data within its body. All offsets up to and including the help-and-command-table offset are required; later fields may be zero if unused, except where noted:
| Offset | Field | Contains |
|---|---|---|
| 0 | Module_Start | Offset to code: start code, entered to run the module as an application. |
| 4 | Module_Initialise | Offset to code: initialisation code. |
| 8 | Module_Finalise | Offset to code: finalisation code. |
| C | Module_ServiceHandler | Offset to code: service call handler. |
| 10 | Module_TitleString | Offset to string: title string. The only field that must not be zero. |
| 14 | Module_HelpString | Offset to string: help string. |
| 18 | Module_CommandTable | Offset to table: help and command keyword table. |
| 1C | SWI chunk base number (optional). The only header entry that is a number rather than an offset. | |
| 20 | Offset to code: SWI handler code (optional). | |
| 24 | Offset to table: SWI decoding table (optional). | |
| 28 | Offset to code: SWI decoding code (optional, deprecated since RISC OS 3.7). | |
| 2C | Offset to string: message file pathname (optional). Added in RISC OS 3.6; gives a word-aligned, zero-terminated pathname used when the help and command table's strings are message-file tokens rather than literal text. | |
| 30 | Module_FeatureTable | Offset to the module's feature table (optional, but required for modules that support 32-bit operation). |
All code offsets must be word-aligned and within the module's own code area; all table and string offsets must similarly lie within the module. RISC OS rejects a module whose header fails these checks.
Module feature table and 32-bit safety
The feature table is a separate structure, addressed by the header's Module_FeatureTable offset, whose first word is a flags word describing the module's safety and architecture. The flags word is not faulted when unknown bits are set, and the feature table must be present (with this word at least) for any module that supports 32-bit operation:
| Bit(s) | Name | Meaning | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 32-bit safe | Set: | Module contains 32-bit safe code. Required by RISC OS Pyromaniac when executing ARM code, and required generally on 32-bit-only systems; ignored on 26-bit-only systems. | ||||||||||||
| 1 | Early ROM init | Set: | ROM module should be initialised early, alongside the Podule manager (the RISC OS PRM's Expansion Card Manager, which enumerates and loads expansion-card and extension-ROM modules; see RISC OS PRM: Expansion Cards and Extension ROMs) and other extension modules. For internal use only; ignored outside the ROM. | ||||||||||||
| 2-3 | Reserved, must be zero | ||||||||||||||
| 4-7 | Architecture |
Module implementation architecture. Used to identify modules built for an
incompatible architecture so that they are not run by a system that cannot
execute them. When the architecture is not AArch32, bit 0 above is
undefined and should be set to 0, and bit 30 of the
Module_Initialise offset (see below) should also be
set.
| |||||||||||||
| 8-31 | Reserved, must be zero | ||||||||||||||
A module that is deemed unsuitable for execution, either because it contains 26-bit code on a 32-bit-only system, or because its architecture is incompatible, is not initialised. Such modules are enumerated by OS_Module 19 and 20 (see RISC OS PRM: OS_Module 19, Enumerate ROM modules, which documents the original status values -1 (unplugged), 0 (dormant), 1 (active), and 2 (running)) with a status of -2. This value is a later addition, described in the RISC OS 6 PRM: OS_Module changes page as "a diagnostic aid for locating non-32bit modules within podules", indicating that they are unsuitable.
Separately, bit 30 of the Module_Initialise offset (header offset +4) can be set to indicate that the module's architecture is given by the feature table rather than being plain AArch32 code. This is a deliberately invalid offset value, which forces older versions of RISC OS that do not understand the feature table's architecture field to reject the module header outright, rather than attempting to run incompatible code.
Revised service-call-handler table (RISC OS 4 and later)
From RISC OS 4, the kernel supports a revised form of the Module_ServiceHandler entry point that lets it distribute service calls more efficiently, while remaining backward compatible with older kernels that only understand the original entry point. The revision is specified in RISC OS 6 PRM: OS_Module changes.
The meaning of the Module_ServiceHandler offset itself is unchanged. The revision instead gives meaning to the word immediately before the handler code, and optionally to a separate table that word points to:
| Offset | Size | Field | Contents |
|---|---|---|---|
| svc-4 | anchor word | Offset, from the module start, to a service table (see below), or 0 if no service table is provided. | |
| svc | 4 | magic instruction | Fixed instruction &E1A00000 (MOV r0, r0), marking that the anchor word at svc-4 is present and meaningful. |
| svc+4 | Handler code, as for the original entry point. |
Here svc is the address of the handler code, found from Module_ServiceHandler as usual. A kernel that does not recognise this revision simply executes the magic instruction (a no-op) and falls through to the handler code, so the revision is harmless on older systems. A module may set the anchor word to 0 to omit the service table, but should provide one wherever possible.
When provided, the service table lists the specific service call numbers the module is interested in, so the kernel can skip calling modules that have not registered interest in a given service:
| Offset | Field | Contents |
|---|---|---|
| 0 | flags | Flags word. Bit 0 set: call the handler with r1 set to the table index of the matched service call, instead of the service call number. Bits 1-31 reserved, must be 0. |
| 4 | handler offset | Offset, from the module start, to the handler code (normally the same code reached via Module_ServiceHandler). |
| 8 | Number of the first service call of interest. | |
| C | Number of the second service call of interest, and so on. | |
| end | terminator | Table terminator. |
The service call numbers in the table must be listed in strictly ascending order; RISC OS rejects a module whose table is not sorted. The handler code reached via the table entry may skip the logic that rejects uninteresting service call numbers, since the kernel will not call it for those; the original handler code reached directly via Module_ServiceHandler must still include that rejection logic, for kernels that do not use the table.
64-bit module extension
A 64-bit (AArch64) module sets the architecture field above to 1 and sets bit 30 of its Module_Initialise offset, as described. The gerph/riscos64-clib specification documents the following additional, 64-bit-specific behaviour:
- Feature flags bit 2 (ModFlags_ZeroInitPresent) indicates that a zero-initialisation size word is present immediately following the feature flags word; when set, the module loader extends the module's allocated memory by that many bytes and zero-fills the extension before initialisation. Confirmed directly against Pyromaniac's module loader (riscos/modules/__init__.py, reading the size word at flags_offset + 4) and the flag's definition (riscos/constants/modhand.py), where it is commented as Pyromaniac's 64-bit zero-init mechanism. The currently published Pyromaniac modules supplement documents feature-table bits 2 and 3 as reserved and required to be zero; that description predates this mechanism and is being corrected separately.
- The module is always allocated on a page boundary plus 4 bytes, so its base address always ends in 004 in hexadecimal. This allows code within the module to address its own workspace using ADRP.
- The module is never multiply instantiated.
All 64-bit module entry points follow the CMHG entry-point register assignments, rather than the original AArch32 module register assignments, with the exception of the private word pointer, which remains in r12 (as x12):
| Entry point | Register use |
|---|---|
| Initialisation |
|
| Finalisation |
|
| SWI |
|
| Command |
|
Any entry point that can return an error pointer must return it in r0, or 0 for success; the V flag need not be set. This follows the CMHG entry-point convention rather than the original AArch32 module convention. All interfaces are assumed to use the frame pointer for a call chain, and must not zero it on entry.
Utility format
The base 26/32-bit utility header is specified in RISC OS 6 PRM: Code formats. The 64-bit extension to that format is specified in the gerph/riscos64-clib repository's README.
26/32-bit utility header
Transient utilities were not originally required to carry any header. Utilities intended to run on 32-bit systems must include the header below, which begins with a branch instruction and a pair of magic values chosen to make it unlikely that a non-conformant file is mistaken for a header:
| Offset | Size | Encoding | Meaning |
|---|---|---|---|
| 0 | 4 | Entry instruction: B entry_point. | |
| 4 | 4 | &79766748 | Magic value 1. |
| 8 | 4 | &216c6776 | Magic value 2. |
| C | 4 | Read-only size, in bytes. 0 means no size check is performed and the file's extent is used instead. | |
| 10 | 4 | Read/write size, in bytes. Same zero convention as the read-only size. | |
| 14 | 4 | Literal value 26 or 32, depending on whether the utility was built for 26-bit or 32-bit RISC OS. |
If supplied, both size values must be word aligned. Because transient utilities are relocatable by design, a suitable header can be added by automated tools after the fact; such tools should assume the entire utility is read/write, since the original PRM does not require the TransientUtility module to enforce the read-only and read/write region sizes it is given.
64-bit utility extension
The 64-bit utility format keeps the same entry instruction and magic values, but replaces the 26-or-32 literal with a bitness-and-flags word, and inserts an additional field giving the offset of the AArch64 entry point. The ARM32 code that follows is correspondingly an error-report stub rather than the start of the utility's own ARM32 code:
| Offset | Size | Encoding | Meaning |
|---|---|---|---|
| 0 | 4 | &ea000005 | ARM32 B &1C. ARM32 entry, branching past the header fields to the error-report code. |
| 4 | 4 | &79766748 | Magic value 1 (unchanged). |
| 8 | 4 | &216c6776 | Magic value 2 (unchanged). |
| C | 4 | Code and static data size, in bytes (read-only size). | |
| 10 | 4 | Read/write data size, in bytes. | |
| 14 | 4 | &00000040 | Bitness and flags. &40 identifies a 64-bit utility, in place of the literal 26 or 32 used by the base format. |
| 18 | 4 | Offset, from the start of the file, of the AArch64 entry point. Not present in the base format. | |
| 1C | 4 | &e28f0004 | ARM32 ADR r0, &24. Start of the ARM32 error-report sequence. |
| 20 | 4 | &e3500102 | ARM32 CMP r0, #&80000000. |
| 24 | 4 | &e1a0f00e | ARM32 MOV pc, lr. |
| 28 | 4 | ARM32 error number word. | |
| 2C | variable | ARM32 error message text: "AArch64 binaries cannot be run on 32-bit RISC OS", zero terminated. |
A 64-bit loader transfers control to the AArch64 code at the offset given at +&18. A 32-bit loader instead runs the ARM32 code, which reports the "AArch64 binaries cannot be run on 32-bit RISC OS" error and returns.
This specification may be extended in future to require that 64-bit utilities, like modules, be loaded at a page boundary plus 4 bytes. Loaders and tools should not currently rely on that alignment for utilities.
Related information
- ARM Image Format (1993) — the canonical 26/32-bit AIF specification.
- RISC OS PRM: Modules — the canonical original module header and entry-point specification.
- RISC OS PRM: Modules (changes) — the RISC OS 3.6 message-file extension to the module header.
- RISC OS 6 PRM: Code formats, "Relocatable modules" section — the 32-bit-safety extension to the module header.
- RISC OS 6 PRM: Code formats, "Absolutes" and "Utilities" sections — the base AIF and utility execution formats. This page has no per-section anchors, so the section heading is given to help a reader locate the relevant part.
- Pyromaniac PRM: Modules supplement — the module feature table's architecture field.
- RISC OS 6 PRM: OS_Module changes — the RISC OS 4 and later revised service-call-handler table format.
- gerph/riscos64-clib — the 64-bit C library, cross-compiler tools, and the specification for the 64-bit AIF, module, and utility extensions.
- 64-bit Development — building absolutes, modules, and utilities with the 64-bit toolchain.