amiga-bootcamp/04_linking_and_libraries/register_conventions.md
Ilia Sharin 7df1f11f15 03/04: deep enrichment of loader/exec format and linking/libraries
Sections 03 and 04 augmented to bootcamp quality with targeted
enrichment based on content analysis (not just file size).

03_loader_and_exec_format:
- overlay_system.md: full rewrite — tree architecture diagram,
  HUNK_OVERLAY binary format, overlay manager runtime internals,
  worked binary example, linker support, modern alternatives
- hunk_relocation.md: full rewrite — visual before/after diagram,
  patching algorithm with code, RELOC32SHORT and DREL32 formats,
  PC-relative impact comparison table, self-referencing relocs,
  error scenarios, Python reloc scanner tool

04_linking_and_libraries:
- library_structure.md: full rewrite — ASCII memory layout diagram,
  JMP table encoding (why 6 bytes), MakeLibrary internals with both
  function array formats, complete library creation example with
  .fd file, checksum verification, lifecycle state diagram
- shared_libraries_runtime.md: full rewrite — OpenLibrary 4-step
  resolution path, ramlib disk loader internals, disk search path,
  version negotiation table (v33-v47), CloseLibrary/Expunge deep
  dive, memory-low sweep, common pitfalls table
- register_conventions.md: full rewrite — FPU register map,
  inter-library A6 save/restore pattern, small-data model with
  __saveds keyword, varargs/TagItem pattern deep dive,
  stack-based wrapper explanation, disassembly identification

Updated indexes:
- 03_loader_and_exec_format/README.md
- 04_linking_and_libraries/README.md
- Root README.md (sections 03 and 04)
2026-04-23 18:30:45 -04:00

10 KiB
Raw Blame History

← Home · Linking & Libraries

M68k Register Calling Conventions on Amiga

Overview

AmigaOS uses a pure register-based calling convention for all OS API calls. There is no stack-based C ABI for library functions. Every argument is passed in a specific CPU register defined by the .fd file for that library. This document covers the complete convention including integer, FPU, varargs, tag-based calls, small-data model, and inter-library patterns.


The AmigaOS Register Convention

Integer Registers

Register Role Preserved?
A6 Library base pointer (always) No — overwritten with target lib
D0 Return value (32-bit) No — scratch
D1 2nd return (64-bit pair) or argument No — scratch
A0 Argument or scratch No — scratch
A1 Argument or scratch No — scratch
D2D7 Arguments (per .fd) Yes — callee-preserved
A2A3 Arguments (per .fd) Yes — callee-preserved
A4 Small-data base pointer (VBCC/GCC) Yes — callee-preserved
A5 Frame pointer (SAS/C) or scratch Yes — callee-preserved
A7 (SP) Stack pointer Maintained by convention

Key Rules

  1. A6 is always destroyed — it holds the target library base before and after every OS call
  2. D0, D1, A0, A1 are volatile — the OS may destroy them in any library call
  3. Callee-preserved means the OS function saves and restores these registers internally
  4. Arguments go in the registers specified by the .fd file — never on the stack

FPU Registers (68881/68882/68040/68060)

Register Role Preserved?
FP0 Return value (floating-point) No — scratch
FP1 Scratch No — scratch
FP2FP7 Available for arguments Yes — callee-preserved

Note

: Very few AmigaOS functions use FPU registers. mathieeedoubtrans.library and mathtrans.library pass arguments via D0/D1 (as 32-bit or 64-bit integer representations of IEEE floats), not in FP registers. Only custom libraries designed for FPU-equipped systems use FP0FP7 for parameter passing.


Example: dos.library Write()

From fd/dos_lib.fd:

Write(file,buffer,length)(d1,d2,d3)
; Assembly — calling Write(fh, buf, 512):
    MOVEA.L  _DOSBase, A6       ; Library base → A6
    MOVE.L   fh,    D1          ; BPTR filehandle → D1
    MOVE.L   buf,   D2          ; Buffer address → D2
    MOVE.L   #512,  D3          ; Byte count → D3
    JSR      -48(A6)            ; Write() LVO = -48
    ; D0 = bytes written (-1 = error)
    ; D1, A0, A1, A6 are now UNDEFINED
    ; D2, D3 still hold their values (callee-preserved)
/* C — compiler generates the above automatically */
LONG n = Write(fh, buf, 512);

Register Preservation Map

After ANY OS library call:

  ┌─────────────────────────────┐
  │ DESTROYED (don't rely on):  │
  │   D0  D1  A0  A1  A6       │
  │   FP0  FP1                  │
  │   CCR (condition codes)     │
  └─────────────────────────────┘

  ┌─────────────────────────────┐
  │ PRESERVED (guaranteed):     │
  │   D2  D3  D4  D5  D6  D7   │
  │   A2  A3  A4  A5           │
  │   FP2  FP3  FP4  FP5       │
  │   FP6  FP7                 │
  │   SP (A7)                  │
  └─────────────────────────────┘

This matches the Motorola 68000 family software convention. AmigaOS does not follow the System V m68k ABI (which uses stack-based parameter passing).


Inter-Library Calls

When one library function calls another library internally, it must save/restore A6:

; Inside dos.library, calling exec.library AllocMem:
    MOVEM.L  A6, -(SP)          ; Save current A6 (dos.library base)
    MOVEA.L  _SysBase, A6       ; Load exec.library base
    MOVE.L   #1024, D0          ; byteSize
    MOVE.L   #$10001, D1        ; MEMF_PUBLIC|MEMF_CLEAR
    JSR      -198(A6)           ; AllocMem()
    MOVEA.L  (SP)+, A6          ; Restore dos.library base
    ; D0 = allocated memory or NULL

Common bug: Forgetting to save/restore A6 in hand-written assembly library code. The caller expects A6 to point to the library it called, but internally the library swapped A6 to call another library.


Small Data Model (A4-Relative)

C compilers can use A4 as a base register for accessing global/static data, reducing code size:

SAS/C Small Data

/* SAS/C: compile with -b0 (small data model) */
/* Generates A4-relative addressing for globals */
; Without small data:
    MOVE.L  _myGlobal, D0       ; 6 bytes: absolute addressing

; With small data (A4-relative):
    MOVE.L  -$1234(A4), D0     ; 4 bytes: A4-relative displacement

__saveds Keyword

The __saveds keyword tells the compiler to reload A4 on function entry — essential for library functions and interrupt handlers that may be called from any context:

/* Library function: */
LONG __saveds MyFunc(LONG arg __asm("d0"))
{
    /* __saveds generates: */
    /* LEA  __LinkerDB, A4  ; reload small-data base */
    /* ...function code... */
    return arg * 2;
}
; What __saveds generates:
_MyFunc:
    LEA     __LinkerDB, A4      ; Restore small-data base register
    ; Now A4-relative data access works
    MOVE.L  D0, D1
    ADD.L   D1, D0
    RTS

When you need __saveds:

  • Library functions (called via JMP table from any task)
  • Interrupt handlers (AddIntServer callbacks)
  • Hook functions (Intuition/BOOPSI hooks)
  • Callback functions passed to other libraries

When you DON'T need __saveds:

  • Regular functions called within the same program (A4 already set)
  • Programs compiled with the large data model (absolute addressing)

GCC Equivalent

/* GCC: -msmall-code -mbaserel flags */
/* Uses A4 as the small-data base pointer */
/* __saveds equivalent: __attribute__((saveds)) */

LONG __attribute__((saveds)) MyFunc(LONG arg __asm("d0"))
{
    return arg * 2;
}

Varargs and Tag-Based Calls

The Problem

The register convention breaks down for variable-argument functions. You can't pass an unknown number of arguments in registers. AmigaOS solves this with TagItem arrays:

/* Fixed-register call (standard): */
struct Window *OpenWindow(struct NewWindow *nw __asm("a0"));
/* → A0 = pointer to struct, A6 = IntuitionBase */

/* Tag-based call (varargs): */
struct Window *OpenWindowTagList(
    struct NewWindow *nw,      /* A0 */
    struct TagItem *tagList    /* A1 — pointer to tag array */
);

TagItem Structure

struct TagItem {
    ULONG ti_Tag;     /* tag identifier */
    ULONG ti_Data;    /* tag value */
};

/* Example tag list: */
struct TagItem tags[] = {
    { WA_Left,    100 },
    { WA_Top,     50  },
    { WA_Width,   640 },
    { WA_Height,  480 },
    { WA_Title,   (ULONG)"My Window" },
    { TAG_DONE,   0   }
};

struct Window *win = OpenWindowTagList(NULL, tags);

Stack-Based Varargs Wrappers

For convenience, AmigaOS provides ...Tags() wrappers that build the tag array on the stack:

/* This is a MACRO or inline function, NOT an OS call: */
struct Window *OpenWindowTags(struct NewWindow *nw, ULONG tag1, ...)
{
    /* Tags are pushed onto the stack by the caller */
    /* The wrapper takes the address of tag1 and passes it as a TagItem* */
    return OpenWindowTagList(nw, (struct TagItem *)&tag1);
}

/* Usage: */
struct Window *win = OpenWindowTags(NULL,
    WA_Left, 100,
    WA_Top, 50,
    WA_Width, 640,
    WA_Height, 480,
    WA_Title, "My Window",
    TAG_DONE);

Important: The ...Tags() functions are not OS calls — they are C macros or inline stubs that build a tag array on the caller's stack and pass it to the real ...TagList() function. They do NOT use the stack for parameter passing to the OS.

Common Tag Patterns

Real function (register) Convenience wrapper (stack)
OpenWindowTagList(nw, tags) OpenWindowTags(nw, ...)
CreateNewProcTags(tags) Doesn't exist — use TagList directly
AllocDosObjectTagList(type, tags) AllocDosObject(type, ...)
SetAttrsA(obj, tags) SetAttrs(obj, ...)

C Compiler Differences

SAS/C 6.x

  • Uses A5 as frame pointer (LINK A5; UNLK A5)
  • Uses A4 for small data model (opt-in with -b0)
  • #pragma amicall for register specification
  • __saveds for A4 reload in library/callback functions

GCC (bebbo m68k-amigaos)

  • No frame pointer by default (-fomit-frame-pointer)
  • Uses A4 for small data (-mbaserel, -msmall-code)
  • Inline assembly with __asm("register") constraints
  • __attribute__((saveds)) for A4 reload

VBCC

  • __reg("d0") storage class for explicit register placement
  • No frame pointer — generates very compact code
  • Uses A4 for small data model by default
  • __saveds supported

Detecting the Convention in Disassembly

Identifying an OS API Call

; Pattern 1 — Standard library call:
    MOVEA.L  (_DOSBase).L, A6     ; Load library base
    [move args to registers]       ; Per .fd specification
    JSR      (-138,A6)            ; Call at LVO -138

; Pattern 2 — PC-relative base load (GCC):
    LEA      _DOSBase(PC), A0    ; PC-relative load of base address
    MOVEA.L  (A0), A6            ; Dereference
    JSR      (-30,A6)            ; Open()

; Pattern 3 — Small-data base load (SAS/C):
    MOVEA.L  -$42(A4), A6        ; Load base from A4-relative storage
    JSR      (-48,A6)            ; Write()

Cross-reference the LVO against the .fd file to identify the function. IDA's Amiga loader applies LVO names automatically when library definitions are present.


References

  • NDK39: fd/*.fd — register assignments per function
  • NDK39: utility/tagitem.h — TagItem structure
  • Amiga ROM Kernel Reference Manual: Libraries — register conventions appendix
  • SAS/C 6.x Programmer's Guide — calling convention, small data, __saveds
  • GCC m68k-amigaos (bebbo) — libnix inline headers
  • VBCC documentation — register specification chapter