Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

Amiga Hunk Executables: Parse Segment Tables and Relocations

Parse classic Amiga HUNK load files safely: validate segment tables, account for CODE/DATA/BSS, bounds-check RELOC32 records, and reject variants.

An Amiga HUNK load file is a sequence of typed records, not a flat memory image with a small header. The initial HUNK_HEADER declares segment indices, allocation sizes, and memory preferences. Each segment then contributes code, initialized data, or zero-initialized storage, followed by records such as relocations, symbols, debug data, and an end marker. A parser that treats every record as a length-prefixed blob can drift out of alignment and turn untrusted offsets into unsafe memory writes.

This article focuses on a deliberately narrow profile: a regular, non-overlaid load file with one HUNK_HEADER, a table that describes segments 0 through n-1, HUNK_CODE, HUNK_DATA, or HUNK_BSS payload records, HUNK_RELOC32, optional well-formed symbol/debug metadata, and HUNK_END. It is a file-format parser profile, not an implementation of AmigaDOS LoadSeg(), an object-file linker, an overlay loader, or a claim that the examples execute natively on an Amiga.

First distinguish load files from object files

The AmigaDOS reference identifies 0x3F3 (HUNK_HEADER) as the first record of an executable/load file. Relocatable object files use HUNK_UNIT (0x3E7) instead and have external-symbol structures that a load-file parser must not silently interpret as executable segments. Read each 32-bit field as a big-endian longword; reject a file whose first longword is not the load-file marker expected by this profile.

For a regular non-overlay executable, the header contains a zero resident-library count, a table size, a first and last hunk index, then one size word per segment in the inclusive first..last range. A simple root load has first == 0 and last == table_size - 1. The table size counts possible hunk indices; it is not a byte count. Validate that table_size is nonzero, that first <= last < table_size, and that the complete size table fits in the input before iterating. Reject other index layouts if the parser does not implement overlays.

Each segment size is measured in longwords. Its two highest bits encode the memory-type request; the lower 30 bits are the size. The AmigaDOS reference maps the ordinary encodings to MEMF_ANY, MEMF_CHIP, and MEMF_FAST. When both flag bits are set, an additional longword supplies explicit memory flags; this extension was introduced in AmigaDOS version 33. Do not multiply the full flagged word by four as if it were an ordinary size. Decode flags first, then use checked arithmetic to convert the low 30-bit longword count to bytes.

Segment records describe payload and allocation separately

The initial record for a segment is normally one of these:

Record Data after the type Meaning
HUNK_CODE (0x3E9) longword count, then that many longwords executable code and constants
HUNK_DATA (0x3EA) longword count, then that many longwords initialized writable data
HUNK_BSS (0x3EB) longword count zero-initialized storage; no bytes follow as payload

The payload count for CODE or DATA may be smaller than the segment allocation in HUNK_HEADER. The difference is not a parse error; the loader reserves the header size and newer AmigaDOS versions normally zero-fill the excess. There is an important exception: when the CODE or DATA payload count is zero, the documented loader path skips loading that payload, so do not rely on the unused allocation being initialized even on later releases. BSS carries its own zero-initialized length and no payload. Validate every declared payload against both the remaining file bytes and its segment’s declared allocation. On older AmigaDOS versions, zero-fill behavior has additional documented defects: releases before version 36 could leave large BSS or excess allocation bytes not fully initialized. A parser can report these boundaries, but should not promise a target loader will produce one uniform memory image across releases.

Decode RELOC32 only after you know the segment

HUNK_RELOC32 (0x3EC) describes longword addresses to adjust in the current CODE or DATA segment. The record contains repeated groups: a 32-bit entry count, a target segment index, and that many byte offsets in the current segment. A zero count ends the relocation record. For each valid offset, the Amiga loader adds the base address of the target segment to the longword currently stored at that offset.

The order matters: relocations refer to the segment whose payload has already been read, and the record must be parsed after that payload. Check the target index against the header table, and check every four-byte relocation field against the allocated size of the source segment. A relocation may point into an allocated zero-fill tail, so checking only the on-disk payload length is too strict if the tool is modeling allocated memory. Conversely, accepting offset + 4 beyond the segment allocation permits an out-of-bounds write. The reference requires even relocation offsets; older 68000/68010 systems can crash on odd offsets, and the loader does not check that condition for the caller.

A bounded record reader can capture the structure without applying executable addresses:

def parse_reloc32(reader, source_size_bytes, segment_count):
    records = []
    while True:
        count = reader.u32_be()       # raises on a truncated longword
        if count == 0:
            return records

        target = reader.u32_be()
        if target >= segment_count:
            raise FormatError("RELOC32 target is outside the hunk table")
        if count > reader.remaining_bytes() // 4:
            raise FormatError("RELOC32 offsets exceed the remaining file")

        for _ in range(count):
            offset = reader.u32_be()
            if offset & 1:
                raise FormatError("RELOC32 offset is not word-aligned")
            if offset + 4 > source_size_bytes:
                raise FormatError("RELOC32 field exceeds the source segment")
            records.append((target, offset))

reader here is a bounded big-endian input abstraction with remaining_bytes() and exact-read behavior; it is intentionally not a drop-in Python module. A production parser also needs checked multiplication for every word count, a configurable file/segment/resource limit, a cap on record counts, and contextual errors containing the file offset and segment index. The routine records what a relocation would touch. It does not allocate Amiga memory, patch a segment, resolve an entry point, or launch code.

When building an actual loader, apply the relocation only after all target segment bases are known. Conceptually, for a 32-bit absolute relocation, read the big-endian longword at the checked source offset, add the chosen target segment base using the target CPU’s 32-bit arithmetic, and write it back. Keep parsing, allocation, relocation, and execution as separate stages with explicit failure cleanup. An emulator’s host pointers are not Amiga guest addresses; store guest bases as guest-width integers and translate them through a checked guest-memory map.

Treat format revisions and hunk variants as input

This profile deliberately rejects overlays and unimplemented relocation types. HUNK_RELOC32SHORT (0x3FC) is a distinct 16-bit-count/index/offset record introduced in AmigaDOS version 36; the reference documents a compatibility defect for versions 36–38 that use 0x3F7 instead. That same historical range has naming and implementation ambiguity around 0x3F7, so a generic parser must not guess its meaning without an explicit target/version profile and known toolchain. HUNK_RELRELOC32 (0x3FD) is a separate relative-relocation form introduced in AmigaDOS 39. None of those records should be treated as HUNK_RELOC32 just because their purpose also involves relocation.

The official AmigaDOS reference also notes a limit in AmigaDOS versions 36 and later: an individual HUNK_RELOC32 count must not exceed 65,536. If producing files for those loaders, split larger target groups and verify the exact toolchain output. If analyzing arbitrary files, record the target version as unknown and report unsupported records instead of normalizing them into a guessed interpretation.

The same rule applies to overlay headers, unknown memory flags, unknown record IDs, HUNK_EXT, and library/object-file structures: either implement their versioned rules and test them, or reject them clearly. HUNK_SYMBOL names and HUNK_DEBUG contents may be length-delimited metadata rather than segment payload; skip only after validating their counted lengths. Do not scan for the next magic value after a parse error; data bytes can coincidentally resemble a hunk tag.

Test parser boundaries without running binaries

Build tiny synthetic byte streams for a header with one CODE segment, one DATA segment, and one BSS segment. Add a RELOC32 group that targets each legal segment and terminates with a zero count. Assert the parsed segment count, allocation length, payload length, and target/offset tuples. Then mutate one field at a time: truncate a longword, claim more payload than remains in the file, exceed a header allocation, set an out-of-range target, use an odd relocation offset, omit the zero-count terminator, and change the opening tag to HUNK_UNIT.

For real compiler output, retain the compiler/linker version and requested AmigaDOS compatibility target with the test fixture. Compare parser output with an independent HUNK inspection tool and, when testing load behavior, use a disposable Amiga emulator or system image that matches the target DOS release. LoadSeg() performs scatter loading and relocation through the AmigaDOS runtime; a host-side parser passing its bounds tests proves only that it understood its supported input subset, not that a native loader accepted the file or that the program can safely run.

Source and hosting notes

The primary format reference used here is the AmigaDOS reference available on the AmigaOS 3 Developer site. The portal page identifies the PDF as the Amiga ROM Kernel Reference Manual: AmigaDOS and credits Thomas Richter; do not confuse it with an archival scan of a separate Commodore-era volume. Copies that may appear on Aminet or other mirror sites are useful archival hosts, not the AmigaOS publishing site. The Hunk wiki below is secondary format documentation and is included only as a cross-check, not as authority for AmigaDOS version behavior.

The safest parser is deliberately conservative: make segment and relocation bounds explicit, keep load-file scope separate from object and overlay formats, and reject unsupported revision-specific records. Structural inspection is valuable for preservation and emulator diagnostics, but it is not equivalent to loading or executing the file on an Amiga.

Related:

Sources:

Comments