RISC OS Build EnvironmentROBE

Appendix43Appendix I: RISC OS execution formats

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.

OffsetSizeFieldContents
04Branch to decompression code, or NOP if the image is not compressed.
44Branch to self-relocation code, or NOP if the image is not self-relocating.
84Branch to zero-initialisation code, or NOP if the image has no zero-initialised area or debug-init action.
C4Executable AIF: branch to the image entry point. Non-executable AIF: the entry point's offset from the image base address.
104Program exit instruction, executed only if the entry point returns; the ARM linker sets this to SWI &11 by default.
144Image read-only size, in bytes. Includes the header size for executable AIF; excludes it for non-executable AIF.
184Image read-write size, in bytes. A multiple of 4.
1C4Image debug size, in bytes. A multiple of 4.
204Image zero-initialised size, in bytes. A multiple of 4.
244Image debug type: 0 none, 1 low-level, 2 source-level (ASD), 3 both. Other values are reserved.
284Image base: the address the image's code was linked at.
2C4Workspace: minimum free workspace, in bytes, to be reserved by a self-moving relocatable image.
304Address 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.
344Data base: the address the image's data was linked at. Meaningful only when bit 8 of the address mode is set.
384Reserved, must be zero
3C4Reserved, must be zero
404Debug-initialisation instruction (a SWI that alerts a resident debugger), or NOP if unused.
44-7B56Zero-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.

OffsetSizeEncodingMeaning
404entry: &e28f0000ARM32 ADR r0, error_block. Start of the ARM32 error-report sequence.
444&ef00002bARM32 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.
484error_block: &0ARM32 DCD 0. Error number word of the error block.
4C48ARM32 error block message text: "AArch64 binaries cannot be run on 32-bit RISC OS", zero terminated.
7C4Reserved, must be zero
80-FFReserved, must be zero
1004&D503201FAArch64 NOP. AArch64 decompression placeholder.
1044&94000004AArch64 BL zeroinit. AArch64 zero-initialisation.
1084&9400001FAArch64 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:

OffsetFieldContains
0Module_StartOffset to code: start code, entered to run the module as an application.
4Module_InitialiseOffset to code: initialisation code.
8Module_FinaliseOffset to code: finalisation code.
CModule_ServiceHandlerOffset to code: service call handler.
10Module_TitleStringOffset to string: title string. The only field that must not be zero.
14Module_HelpStringOffset to string: help string.
18Module_CommandTableOffset to table: help and command keyword table.
1CSWI chunk base number (optional). The only header entry that is a number rather than an offset.
20Offset to code: SWI handler code (optional).
24Offset to table: SWI decoding table (optional).
28Offset to code: SWI decoding code (optional, deprecated since RISC OS 3.7).
2COffset 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.
30Module_FeatureTableOffset 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)NameMeaning
032-bit safeSet: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.
1Early ROM initSet: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-3Reserved, must be zero
4-7Architecture 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.
ValueArchitecture
0ARM 32-bit (AArch32, including ARM 26-bit).
1ARM 64-bit (AArch64).
2x86 64-bit.
3-14Reserved.
15Python (RISC OS Pyromaniac).
8-31Reserved, 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:

OffsetSizeFieldContents
svc-4anchor wordOffset, from the module start, to a service table (see below), or 0 if no service table is provided.
svc4magic instructionFixed instruction &E1A00000 (MOV r0, r0), marking that the anchor word at svc-4 is present and meaningful.
svc+4Handler 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:

OffsetFieldContents
0flagsFlags 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.
4handler offsetOffset, from the module start, to the handler code (normally the same code reached via Module_ServiceHandler).
8Number of the first service call of interest.
CNumber of the second service call of interest, and so on.
endterminatorTable 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 pointRegister use
Initialisation
  • x0 points to the command line tail (start string).
  • x1 holds the instance number, always 0.
  • x12 holds the private word pointer.
Finalisation
  • x12 holds the private word pointer.
SWI
  • x0 holds the SWI offset within the module's SWI chunk.
  • x1 points to a register block holding the caller's x0-x9 on entry.
  • x12 holds the private word pointer.
Command
  • x0 points to the argument string.
  • x1 holds the count of arguments.
  • x2 holds the command number.
  • x12 holds the private word pointer.

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:

OffsetSizeEncodingMeaning
04Entry instruction: B entry_point.
44&79766748Magic value 1.
84&216c6776Magic value 2.
C4Read-only size, in bytes. 0 means no size check is performed and the file's extent is used instead.
104Read/write size, in bytes. Same zero convention as the read-only size.
144Literal 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:

OffsetSizeEncodingMeaning
04&ea000005ARM32 B &1C. ARM32 entry, branching past the header fields to the error-report code.
44&79766748Magic value 1 (unchanged).
84&216c6776Magic value 2 (unchanged).
C4Code and static data size, in bytes (read-only size).
104Read/write data size, in bytes.
144&00000040Bitness and flags. &40 identifies a 64-bit utility, in place of the literal 26 or 32 used by the base format.
184Offset, from the start of the file, of the AArch64 entry point. Not present in the base format.
1C4&e28f0004ARM32 ADR r0, &24. Start of the ARM32 error-report sequence.
204&e3500102ARM32 CMP r0, #&80000000.
244&e1a0f00eARM32 MOV pc, lr.
284ARM32 error number word.
2CvariableARM32 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.