LoadSeg and Segment Lists

LoadSeg() turns a file in the Hunk format into a segment list in memory: a chain of separately allocated segments, one per code, data or BSS hunk, relocated to their load addresses. The segment list is what CreateNewProc(), RunCommand() and the resident-command list consume. This article covers the in-memory layout, the four loader functions with their version-specific bugs, the InternalLoadSeg() call-back interface, unloading, and how overlays use the same entry points.

The segment list in memory

Each segment is one memory block laid out as:

/* not in any NDK header */
struct SegmentList {
    BPTR  NextSegment;   /* BPTR to next segment, ZERO for the last */
    ULONG Data[1];       /* payload: the hunk's code/data/BSS */
};
  • LoadSeg() returns a BPTR to the first node; follow NextSegment for the rest.
  • The long word immediately before NextSegment holds the byte size of the whole allocation including overhead — the AllocVec()/FreeVec() convention. It is handy for finding a segment's size.
  • Processes and commands using C or assembler startup begin execution at the first byte of the first segment's payload (&Data[0] of the first node). BCPL handlers (GLOBVEC 0 or -3) are started through the BCPL runtime binder instead.
/* Walk a segment list and print each segment's address and size */
void ShowSegments(BPTR seglist)
{
    while (seglist) {
        ULONG *seg  = (ULONG *)BADDR(seglist);   /* -> NextSegment */
        ULONG  size = seg[-1];                   /* AllocVec-style size */
        Printf("segment at 0x%08lx, %lu bytes\n", (ULONG)&seg[1], size);
        seglist = (BPTR)seg[0];
    }
}

Do not confuse three different things:

Name What it is
Segment list (this article) The NextSegment chain returned by LoadSeg()
struct Segment (dos/dosextens.h) A node of the resident-command list (seg_Next, seg_UC, seg_Seg, seg_Name). Its seg_Seg is a segment list
pr_SegList (struct Process) A BCPL array: a count followed by BPTRs to segment lists. See Process Creation and Structure

LoadSeg()

BPTR LoadSeg(CONST_STRPTR name);

Opens name with Open(name, MODE_OLDFILE) (so the usual path rules apply), loads every hunk, and returns the segment list or ZERO. On failure any blocks already loaded are freed and IoErr() holds an error code (for example ERROR_BAD_HUNK for an unknown hunk type); on success IoErr() is undefined. Release the result with UnLoadSeg().

Loader bugs by version

LoadSeg(), NewLoadSeg() and InternalLoadSeg() share one implementation and therefore all of these defects:

Version Defect Workaround
V34 and below BSS segments larger than 256K are not fully cleared Avoid BSS hunks over 256K; allocate such memory with exec
V34 and below Space beyond the payload of code/data hunks (l < ms[i]) is not cleared Clear it yourself
V36+ That trailing space is cleared only if the hunk has payload (l > 0) Clear it yourself, or avoid it
V36+ At most 2¹⁶ entries per relocation array Split long lists into several arrays for the same target hunk
V36–V38 HUNK_RELOC32SHORT recognised only under the wrong ID 0x3f7 Use 0x3f7, which V39+ still accepts
V39+ HUNK_RELRELOC32 fields are read as 16 bits Do not use HUNK_RELRELOC32
all Odd relocation offsets crash on 68000/68010 Linkers must emit even offsets
all HUNK_NAME/HUNK_SYMBOL names over 124 characters fail Keep names ≤ 31 long-words

Neither short relocation form loads on V34 and below; binaries for those systems must use HUNK_RELOC32. See AmigaDOS Hunk Binary Format for the hunk layouts.

NewLoadSeg()

BPTR NewLoadSeg(CONST_STRPTR file, CONST struct TagItem *tags);      /* V36 */
BPTR NewLoadSegTagList(CONST_STRPTR file, CONST struct TagItem *tags);
BPTR NewLoadSegTags(CONST_STRPTR file, ULONG tag1type, ...);

All three reach the same library entry. No tags are defined, so it does nothing LoadSeg() cannot. Version and stack information come from the $VER: and $STACK: cookies instead. Unload with plain UnLoadSeg(). Same bugs and IoErr() behaviour as LoadSeg().

InternalLoadSeg()

Loads through caller-supplied functions instead of Read() and AllocMem(). Use it to load a binary without a file system, or into special memory — for example from ROM, or from the Rigid Disk Block of a boot partition.

seglist = InternalLoadSeg(fh, table, funcs)   /* V36 */
D0                        D0  A0     A1

BPTR InternalLoadSeg(BPTR fh, BPTR table, struct LoadSegFuncs *funcs);
  • fh is an opaque value: it is never interpreted, only passed to ReadFunc() in d1. It need not be a real file handle.
  • table must be ZERO for a normal binary or an overlay root; a non-ZERO segment table selects overlay-node loading (see below).
  • funcs points to three function pointers. The structure is not officially documented:
struct LoadSegFuncs {
    LONG __asm (*ReadFunc)(register __d1 BPTR fh,
                           register __d2 APTR buffer,
                           register __d3 ULONG size,
                           register __a6 struct DosLibrary *DOSBase);
    APTR __asm (*AllocFunc)(register __d0 ULONG size,
                            register __d1 ULONG flags,
                            register __a6 struct ExecBase *SysBase);
    void __asm (*FreeFunc)(register __a1 APTR mem,
                           register __d0 ULONG size,
                           register __a6 struct ExecBase *SysBase);
};
Function Inputs Result Compatible system call
ReadFunc d1 = fh, d2 = buffer, d3 = byte count, a6 = DOSBase d0 = bytes read, negative on error Read()
AllocFunc d0 = size, d1 = MEMF_* flags, a6 = SysBase d0 = memory or NULL AllocMem()
FreeFunc a1 = memory, d0 = size, a6 = SysBase — FreeMem()

InternalLoadSeg() stores each block's size in the block itself (the AllocVec() convention), so the allocator and the release function do not need to track sizes: plain AllocMem()/FreeMem() already fit the interface.

Note: the NDK 3.2 autodoc documents the ReadFunc registers as d1 (handle), a0 (buffer), d0 (length); the manual gives d1/d2/d3 as above. The ReadFunc arguments are identical to those of Read(), which takes d1/d2/d3.

Note: the NDK 3.2 clib/dos_protos.h and autodoc declare a fourth argument LONG *stack in a2 that receives a stack size found in the file, and type funcs as LONG * (an array of three function pointers). The manual documents three arguments; on V47 use ScanStackToken() on the loaded seglist instead.

IoErr() is not set consistently, unless your functions set it. Call SetIoErr(0) beforehand if you want to distinguish errors.

Note: the NDK 3.2 autodoc states that for an overlaid file the result is the negated seglist, -(seglist), and that the result is not returned in d1.

UnLoadSeg()

success = UnLoadSeg(seglist)
D0                  D1

BOOL UnLoadSeg(BPTR seglist);

Frees every segment of a list loaded by LoadSeg() or NewLoadSeg(). For an overlaid binary — recognised by the magic 0xabcd in the first segment — it also closes the overlay file handle and frees the segment table (see AmigaOS Hunk Overlays).

  • The only error is passing ZERO: it then returns 0 and leaves IoErr() alone. Everything else is reported as success; the result of closing the overlay file is not checked.
  • The code must no longer be running. AmigaDOS has no protocol to learn when a process started from a seglist has finished; use NP_FreeSeglist with CreateNewProc() so the process frees its own code.
  • Segment lists from InternalLoadSeg() should be freed with InternalUnLoadSeg().

Note: the NDK 3.2 clib/dos_protos.h declares UnLoadSeg() as returning VOID.

InternalUnLoadSeg()

success = InternalUnLoadSeg(seglist, FreeFunc)   /* V36 */
D0                          D1       A1

BOOL InternalUnLoadSeg(BPTR seglist,
                       void __asm (*FreeFunc)(register __a1 APTR mem,
                                              register __d0 ULONG size,
                                              register __a6 struct ExecBase *SysBase));

FreeFunc should be the one from the LoadSegFuncs used for loading (or at least able to free what its AllocFunc allocated). For overlaid binaries it always calls Close() on the root's file handle, so it can only clean up overlays loaded from a real Open() handle. Returns 0 only for a ZERO seglist; the result of Close() is not checked and IoErr() is not set consistently.

If an overlaid program was loaded through custom I/O, close the stream yourself, set oh_FileHandle to ZERO (closing ZERO is a harmless no-op) and then call InternalUnLoadSeg().

Loading overlay nodes

The same entry points load overlay nodes. LoadSeg() actually takes two hidden arguments when its first argument is NULL:

seglist = LoadSeg(name, table, fh)
D0                D1    D2     D3

/* name  = NULL selects overlay-node mode
   table = oh_Segments from the root's OverlayHeader
   fh    = oh_FileHandle from the root's OverlayHeader */

The official prototype does not expose table and fh; call it through an assembler stub or a custom pragma. Rules:

  • Seek() first, every time. Position fh at the node's HUNK_HEADER (ot_FilePosition for the hierarchical overlay manager). After LoadSeg() returns, the file position is undefined.
  • LoadSeg() does not free the segments already in the table entries the node will fill. The overlay manager must unload them first.
  • On success it links the new segments, fills the table and returns the first segment; IoErr() is undefined. On failure it returns ZERO with IoErr() set.

InternalLoadSeg(fh, table, funcs) with a non-ZERO table does the same through call-backs, but its use for overlays is discouraged because the unloaders always Close() the overlay handle. Details are in AmigaOS Hunk Overlays.

Using a segment list

Consumer How the seglist is used
CreateNewProc() with NP_Seglist New process starts at the first segment. NP_FreeSeglist (default FALSE) makes the process UnLoadSeg() it at exit; on creation failure it is never freed
CreateProc() 1.3+ Same, but the seglist is never freed by dos
RunCommand(seg, stack, args, len) Runs the seglist as a command on the calling process with its own stack and arguments; it does not load anything. The caller unloads the seglist afterwards
Resident list (AddSegment()) Keeps the seglist in a struct Segment for later RunCommand()
LONG RunCommand(BPTR seg, LONG stack, CONST_STRPTR paramptr, LONG paramlen);  /* V36 */
LONG ScanStackToken(BPTR seg, LONG defaultstack);                            /* V47 */

RunCommand() does not look at the binary to choose a stack: the caller decides. The Shell uses cli_DefaultStack × 4 raised by the $STACK: cookie, which on V47 is exactly what ScanStackToken() computes. The argument string should end with a newline for ReadArgs(). RunCommand() returns -1 (with IoErr() = ERROR_NO_FREE_STORE) if it cannot allocate the stack or input buffer.

#include <string.h>
#include <dos/dos.h>
#include <proto/dos.h>

/* Load and run a command on the current (shell) process */
LONG RunFile(STRPTR file, STRPTR args)    /* args must end in "\n" */
{
    LONG rc = -1;
    BPTR seg;

    if ((seg = LoadSeg(file))) {
        LONG stack = ScanStackToken(seg, 4096);     /* V47 */
        SetProgramName(file);
        rc = RunCommand(seg, stack, args, strlen(args));
        UnLoadSeg(seg);
    }
    return rc;
}

A seglist that is never passed to UnLoadSeg() can be faked for CreateProc(); see Process Creation and Structure.

See Also


Sources: Thomas Richter, Amiga ROM Kernel Reference Manual: AmigaDOS (2024, v1.27), §11 intro, §11.2, §11.3–11.4 (pp. 162–183), §15.2.3 (pp. 328–329); NDK 3.2 R4 clib/dos_protos.h, dos/dosextens.h, Autodocs/dos.doc Updated: 2026-11-13