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)
This commit is contained in:
Ilia Sharin 2026-04-23 18:30:45 -04:00
parent 99a6d53f57
commit 7df1f11f15
8 changed files with 1556 additions and 360 deletions

View file

@ -10,12 +10,16 @@ This section documents how AmigaOS shared libraries work at the binary level —
| File | Topic |
|---|---|
| [library_structure.md](library_structure.md) | Library node, LVO table, OpenLibrary mechanics |
| [fd_files.md](fd_files.md) | Function Definition files — the library ABI source |
| [lvo_table.md](lvo_table.md) | JMP table layout and reconstruction |
| [compiler_stubs.md](compiler_stubs.md) | How SAS/C, GCC, VBCC call libraries |
| [setfunction.md](setfunction.md) | Runtime function patching with SetFunction |
| [startup_code.md](startup_code.md) | c.o / gcrt0.S — startup and exit sequences |
| [library_structure.md](library_structure.md) | Library memory layout, JMP table encoding, MakeLibrary construction, complete library creation example |
| [shared_libraries_runtime.md](shared_libraries_runtime.md) | OpenLibrary resolution path, ramlib disk loader, version negotiation, expunge mechanics |
| [register_conventions.md](register_conventions.md) | Register ABI: integer, FPU, varargs/TagItem, small-data model, __saveds, inter-library calls |
| [fd_files.md](fd_files.md) | Function Definition files — the library ABI source of truth, LVO calculation |
| [lvo_table.md](lvo_table.md) | JMP table layout, complete exec.library LVO table, IDA reconstruction script |
| [compiler_stubs.md](compiler_stubs.md) | How SAS/C, GCC, VBCC call libraries — compiler signature identification |
| [inline_stubs.md](inline_stubs.md) | Compiler inline stubs: pragma (SAS/C), inline asm (GCC), __reg (VBCC), stub generation tools |
| [link_libraries.md](link_libraries.md) | Static linking: amiga.lib, sc.lib, libnix, auto.lib, WBStartup glue, stack cookie |
| [startup_code.md](startup_code.md) | c.o / gcrt0.S: entry contract, CLI vs WB detection, argument parsing, WBStartup message |
| [setfunction.md](setfunction.md) | Runtime function patching: canonical pattern, chaining, removal, RE detection heuristics |
## The Library ABI Model
@ -23,10 +27,13 @@ Every AmigaOS shared library exposes its functions through a **negative-offset J
```
Library base: LIB+0 → Library node (struct Library)
LIB-6 → JMP _funcN (last function)
LIB-12 → JMP _funcN-1
LIB-6 → JMP _Open (mandatory)
LIB-12 → JMP _Close (mandatory)
LIB-18 → JMP _Expunge (mandatory)
LIB-24 → JMP _Reserved (mandatory)
LIB-30 → JMP _func1 (first user function)
LIB-36 → JMP _func2
...
LIB-6 → JMP _func1 (first user function)
```
A C call like `OpenLibrary("graphics.library", 0)` compiles to:

View file

@ -1,122 +1,361 @@
[← Home](../README.md) · [Linking & Libraries](README.md)
# AmigaOS Library Structure
# AmigaOS Library Structure — Architecture, Construction, Memory Layout
## Overview
Every AmigaOS library, device, and resource is a **struct Library** preceded by a negative-offset JMP table. This document covers the complete memory layout, how `MakeLibrary()` constructs a library from scratch, the function vector table encoding, checksum verification, and how to create your own shared libraries.
---
## Memory Layout
```
Low addresses High addresses
──────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────┬──────────────────────────────────┐
│ JMP Table (negative) │ Library Data (positive) │
│ │ │
│ base - N: JMP func_N │ base + $00: struct Library │
│ ... │ base + $22: lib_OpenCnt │
│ base - 36: JMP func_6 │ base + $24: [private data] │
│ base - 30: JMP func_5 (1st user)│ base + $XX: [more private] │
│ base - 24: JMP Reserved │ │
│ base - 18: JMP Expunge │ │
│ base - 12: JMP Close │ │
│ base - 6: JMP Open │ │
└──────────────────────────────────┴──────────────────────────────────┘
Library Base Pointer
(returned by OpenLibrary)
```
### Memory Allocation
The library occupies a **single contiguous allocation**:
```c
/* Total allocation = JMP table + Library header + private data */
ULONG total = lib_NegSize + lib_PosSize;
/* AllocMem returns the start of the block */
APTR block = AllocMem(total, MEMF_PUBLIC | MEMF_CLEAR);
/* Library base pointer = block + lib_NegSize */
struct Library *lib = (struct Library *)((UBYTE *)block + lib_NegSize);
```
---
## The Library Node
Every AmigaOS library (and device, resource) begins with a `struct Library` at its base address:
```c
/* exec/libraries.h */
struct Library {
struct Node lib_Node; /* +$00: linked list node */
struct Node lib_Node; /* +$00: linked list node (14 bytes) */
UBYTE lib_Flags; /* +$0E: LIBF_ flags */
UBYTE lib_pad; /* +$0F: (reserved) */
UWORD lib_NegSize; /* +$10: bytes of function table (negative area) */
UWORD lib_PosSize; /* +$12: bytes of struct Library + private data */
UBYTE lib_pad; /* +$0F: alignment padding */
UWORD lib_NegSize; /* +$10: bytes of JMP table */
UWORD lib_PosSize; /* +$12: bytes of Library struct + private */
UWORD lib_Version; /* +$14: major version */
UWORD lib_Revision; /* +$16: minor revision */
APTR lib_IdString; /* +$18: pointer to ID string */
ULONG lib_Sum; /* +$1C: checksum of the function table */
UWORD lib_OpenCnt; /* +$20: number of current openers */
APTR lib_IdString; /* +$18: "mylib.library 1.0 (1.1.95)\r\n" */
ULONG lib_Sum; /* +$1C: checksum of JMP table */
UWORD lib_OpenCnt; /* +$20: current open count */
};
/* sizeof(struct Library) = 34 bytes ($22) */
```
### The Node Header
```c
/* exec/nodes.h */
struct Node {
struct Node *ln_Succ; /* +$00: next node in list */
struct Node *ln_Pred; /* +$04: previous node */
UBYTE ln_Type; /* +$08: NT_LIBRARY, NT_DEVICE, NT_RESOURCE */
BYTE ln_Pri; /* +$09: priority (for Enqueue) */
char *ln_Name; /* +$0A: "dos.library" */
};
```
`lib_NegSize` is the total byte size of the JMP table (the negative-address area preceding the struct). It equals `num_functions × 6` bytes per JMP instruction.
| ln_Type Value | Hex | Type |
|---|---|---|
| `NT_LIBRARY` | $09 | Standard shared library |
| `NT_DEVICE` | $03 | I/O device (trackdisk.device, etc.) |
| `NT_RESOURCE` | $08 | Hardware resource (cia.resource, etc.) |
---
## JMP Table (Negative Offset Area)
## JMP Table Encoding
The function table is constructed **below** the library base address. Each entry is a 6-byte JMP instruction:
Each function vector is a 6-byte absolute JMP instruction:
```
Library base - 6: 4E F9 xx xx xx xx JMP AbsAddress (Open)
Library base - 12: 4E F9 xx xx xx xx JMP AbsAddress (Close)
Library base - 18: 4E F9 xx xx xx xx JMP AbsAddress (Expunge)
Library base - 24: 4E F9 xx xx xx xx JMP AbsAddress (Reserved)
Library base - 30: 4E F9 xx xx xx xx JMP AbsAddress (first user function)
Library base - 36: ...
Bytes: 4E F9 AA AA AA AA
└──┘ └──────────┘
JMP 32-bit absolute target address
.L
```
### Standard Functions (Fixed LVOs for All Libraries)
### Why 6 Bytes?
| LVO (offset) | Function |
|---|---|
| -6 | `Open` |
| -12 | `Close` |
| -18 | `Expunge` |
| -24 | `Reserved` (must return 0) |
The 68000 `JMP (xxx).L` instruction is exactly 6 bytes:
- 2 bytes: opcode `$4EF9`
- 4 bytes: 32-bit absolute address
These four are mandatory for every library. User functions start at LVO -30.
This is the most compact way to do an indirect jump to an arbitrary address. The fixed 6-byte slot size allows the LVO to be calculated as a simple multiplication: `LVO = -(function_index × 6)`.
### JMP Encoding
### Calling Through the JMP Table
`4E F9 AAAA AAAA` = `JMP.L absolute_address`
To call function at LVO -30:
```asm
MOVEA.L LibBase, A6
JSR -30(A6) ; call through JMP table
; User code:
MOVEA.L _DOSBase, A6 ; load library base into A6
JSR -48(A6) ; jump to JMP table slot at base-48
; CPU executes:
; 1. Push return address onto stack
; 2. Compute effective address: A6 + (-48) = base - 48
; 3. Read 6 bytes at that address: 4EF9 AAAAAAAA
; 4. Jump to AAAAAAAA (the actual function body)
; 5. Function executes and RTS returns to caller
```
The `JSR -30(A6)` does **not** jump directly to the function — it jumps to the JMP slot, which then jumps to the real function. This indirection is essential for `SetFunction()` patching.
The double-jump (JSR→JMP→function→RTS) costs approximately 12 extra cycles on a 68000 — negligible compared to function execution time.
### Standard Functions (Mandatory for All Libraries)
| LVO | Index | Function | Purpose |
|---|---|---|---|
| 6 | 1 | `Open()` | Called by `OpenLibrary()` — increment refcount, return base |
| 12 | 2 | `Close()` | Called by `CloseLibrary()` — decrement refcount |
| 18 | 3 | `Expunge()` | Free resources when refcount reaches 0 |
| 24 | 4 | `Reserved()` | Must return 0 — reserved for future use |
| 30 | 5 | (first public function) | Library-specific — defined by `.fd` file |
---
## MakeLibrary() — Constructing the Table
`exec.library MakeLibrary()` builds a library:
## MakeLibrary() — Building a Library
```c
struct Library *MakeLibrary(
APTR funcArray, /* array of function pointers (APTR) or LONG offsets */
APTR structInit, /* structure initialiser table (or NULL) */
ULONG (*initFunc)(), /* init function (or NULL) */
ULONG dataSize, /* size of library data area */
BPTR segList /* segment list of the library code */
APTR funcArray, /* A0: function pointer array */
APTR structInit, /* A1: struct initialiser table (or NULL) */
APTR initFunc, /* A2: init function (or NULL) */
ULONG dataSize, /* D0: sizeof(MyLibBase) */
BPTR segList /* D1: segment list (for UnLoadSeg on expunge) */
);
```
`funcArray` is a NULL-terminated list of function addresses. `MakeLibrary` allocates the combined negative+positive area and fills in the JMP table.
### Function Array Formats
---
`funcArray` can be in two formats:
## Library Initialisation
At `MakeNode()` / `MakeLibrary()` time:
1. `AllocMem(lib_NegSize + lib_PosSize, MEMF_PUBLIC | MEMF_CLEAR)`
2. Fill JMP table at negative offsets
3. Initialise `struct Library` fields at positive offsets
4. Set `lib_Sum` to the checksum of the JMP table
At `AddLibrary()`:
1. Library is added to `SysBase->LibList`
2. Future `OpenLibrary()` calls find it by name via `FindName()`
---
## OpenLibrary() Path
**Format 1: Absolute Pointers** (most common for C libraries)
```c
struct Library *base = OpenLibrary("mylib.library", MIN_VERSION);
/* NULL-terminated array of function pointers */
APTR myFuncArray[] = {
(APTR)MyOpen, /* LVO -6 */
(APTR)MyClose, /* LVO -12 */
(APTR)MyExpunge, /* LVO -18 */
(APTR)NULL, /* LVO -24: Reserved */
(APTR)MyFunc1, /* LVO -30 */
(APTR)MyFunc2, /* LVO -36 */
(APTR)-1 /* terminator */
};
```
Internally:
1. `exec` searches `SysBase->LibList` for a node with `ln_Name == "mylib.library"`
2. If found and version sufficient: calls `LVO_Open` (offset -6) on the library
3. If not found: attempts to load `LIBS:mylib.library` from disk via `LoadSeg()` + `InitResident()`
4. Returns library base pointer, or NULL on failure
**Format 2: Relative Offsets** (used by ROM modules)
```c
/* First entry = -1 signals relative mode */
/* Following entries are WORD offsets from funcArray base */
WORD myFuncArray[] = {
-1, /* flag: relative mode */
MyOpen - funcBase, /* WORD offset to Open */
MyClose - funcBase, /* WORD offset to Close */
MyExpunge - funcBase, /* WORD offset to Expunge */
0, /* Reserved = NULL (offset 0 maps to a NOP) */
MyFunc1 - funcBase,
MyFunc2 - funcBase,
-1 /* terminator */
};
```
### MakeLibrary Internals
```c
/* What MakeLibrary does internally: */
/* 1. Count functions by scanning funcArray to the terminator */
ULONG numFuncs = CountFunctions(funcArray);
ULONG negSize = numFuncs * 6; /* 6 bytes per JMP slot */
/* 2. Allocate combined block */
ULONG posSize = MAX(dataSize, sizeof(struct Library));
APTR block = AllocMem(negSize + posSize, MEMF_PUBLIC | MEMF_CLEAR);
struct Library *lib = (struct Library *)((UBYTE *)block + negSize);
/* 3. Fill JMP table using MakeFunctions() */
MakeFunctions(lib, funcArray, NULL);
/* For each function pointer, writes:
*(UWORD *)(slot) = 0x4EF9; // JMP.L opcode
*(ULONG *)(slot + 2) = funcAddr; // target address */
/* 4. Initialize struct Library fields */
lib->lib_NegSize = negSize;
lib->lib_PosSize = posSize;
/* 5. Apply structInit table (if provided) */
if (structInit)
InitStruct(structInit, lib, 0);
/* 6. Call init function (if provided) */
if (initFunc)
lib = ((struct Library *(*)(struct Library *, BPTR, struct ExecBase *))
initFunc)(lib, segList, SysBase);
/* 7. Compute and store checksum */
SumLibrary(lib);
return lib;
```
---
## ROM Libraries (Kickstart)
## Creating a Shared Library — Complete Example
Some libraries are **resident** — embedded directly in the Kickstart ROM:
- `exec.library` — always in ROM; base at `$4` in exec exception vector
- `graphics.library`, `intuition.library`, `dos.library` — loaded from ROM on boot
### Library Source (my.library)
ROM-resident libraries are listed in the **Resident module** list. During boot, `exec` calls `InitResident()` for each module marked as auto-init.
```c
#include <exec/types.h>
#include <exec/libraries.h>
#include <exec/resident.h>
#include <proto/exec.h>
/* Private library base — extends struct Library */
struct MyLibBase {
struct Library lib; /* MUST be first */
ULONG myPrivate; /* library-specific data */
BPTR segList; /* for Expunge to UnLoadSeg */
};
/* Forward declarations */
struct Library * __saveds MyOpen(void);
BPTR __saveds MyClose(void);
BPTR __saveds MyExpunge(void);
ULONG MyReserved(void);
LONG __saveds MyAdd(LONG a __asm("d0"), LONG b __asm("d1"));
/* Function table */
static APTR FuncTable[] = {
(APTR)MyOpen,
(APTR)MyClose,
(APTR)MyExpunge,
(APTR)MyReserved,
(APTR)MyAdd, /* LVO -30: first user function */
(APTR)-1
};
/* Struct init table */
static struct MyInitData {
/* ... InitStruct data to set lib_Node.ln_Type, ln_Name, etc. */
} InitData;
/* Init function — called once when library first loads */
struct Library * __saveds LibInit(
struct MyLibBase *base __asm("d0"),
BPTR segList __asm("a0"),
struct ExecBase *sysBase __asm("a6"))
{
base->segList = segList;
base->myPrivate = 0;
return (struct Library *)base;
}
/* RomTag — how exec finds us */
static struct Resident RomTag = {
RTC_MATCHWORD, /* $4AFC */
&RomTag, /* pointer to self */
&RomTag + 1, /* end skip */
RTF_AUTOINIT, /* auto-init flag */
1, /* version */
NT_LIBRARY, /* type */
0, /* priority */
"my.library", /* name */
"my.library 1.0 (1.1.95)\r\n",
&AutoInitTable /* init — points to auto-init array */
};
/* Auto-init table (for RTF_AUTOINIT) */
static ULONG AutoInitTable[] = {
sizeof(struct MyLibBase), /* data size */
(ULONG)FuncTable, /* function array */
(ULONG)&InitData, /* struct init data */
(ULONG)LibInit /* init function */
};
/* LVO -6: Open */
struct Library * __saveds MyOpen(void)
{
struct MyLibBase *base = (struct MyLibBase *)
REG_A6; /* library base passed in A6 */
base->lib.lib_OpenCnt++;
base->lib.lib_Flags &= ~LIBF_DELEXP;
return (struct Library *)base;
}
/* LVO -12: Close */
BPTR __saveds MyClose(void)
{
struct MyLibBase *base = (struct MyLibBase *)REG_A6;
base->lib.lib_OpenCnt--;
if (base->lib.lib_OpenCnt == 0 &&
(base->lib.lib_Flags & LIBF_DELEXP))
{
return MyExpunge();
}
return 0;
}
/* LVO -18: Expunge */
BPTR __saveds MyExpunge(void)
{
struct MyLibBase *base = (struct MyLibBase *)REG_A6;
if (base->lib.lib_OpenCnt > 0)
{
base->lib.lib_Flags |= LIBF_DELEXP;
return 0; /* Can't expunge yet — still in use */
}
BPTR seg = base->segList;
Remove(&base->lib.lib_Node); /* Remove from LibList */
FreeMem((UBYTE *)base - base->lib.lib_NegSize,
base->lib.lib_NegSize + base->lib.lib_PosSize);
return seg; /* Return segment list for UnLoadSeg */
}
/* LVO -24: Reserved */
ULONG MyReserved(void) { return 0; }
/* LVO -30: Your first function */
LONG __saveds MyAdd(LONG a __asm("d0"), LONG b __asm("d1"))
{
return a + b;
}
```
### Corresponding .fd File
```
##base _MyBase
##bias 30
##public
Add(a,b)(D0,D1)
##end
```
---
@ -124,19 +363,70 @@ ROM-resident libraries are listed in the **Resident module** list. During boot,
```c
/* lib_Flags bits: */
#define LIBF_SUMMING (1<<0) /* currently computing checksum */
#define LIBF_CHANGED (1<<1) /* function table was patched (SetFunction) */
#define LIBF_SUMUSED (1<<2) /* checksum is valid */
#define LIBF_DELEXP (1<<3) /* delayed expunge requested */
#define LIBF_SUMMING (1<<0) /* Currently computing checksum */
#define LIBF_CHANGED (1<<1) /* JMP table was patched (SetFunction) */
#define LIBF_SUMUSED (1<<2) /* Checksum is valid and in use */
#define LIBF_DELEXP (1<<3) /* Delayed expunge expunge when refcount=0 */
```
`LIBF_CHANGED` is set by `SetFunction()` to signal that the checksum is no longer valid — tools like `ShowConfig` use this to detect patched libraries.
| Flag | Set By | Checked By |
|---|---|---|
| `LIBF_SUMMING` | `SumLibrary()` | Internal — prevents recursive sum |
| `LIBF_CHANGED` | `SetFunction()` | `ShowConfig`, virus scanners — detect patches |
| `LIBF_SUMUSED` | `SumLibrary()` | `exec` — decides if checksum valid |
| `LIBF_DELEXP` | Memory-low handler | `Close()` — triggers deferred expunge |
---
## Checksum Verification
```c
/* SumLibrary() computes checksum over the JMP table: */
void SumLibrary(struct Library *lib)
{
lib->lib_Flags |= LIBF_SUMMING;
ULONG sum = 0;
UWORD *p = (UWORD *)((UBYTE *)lib - lib->lib_NegSize);
ULONG count = lib->lib_NegSize / 2; /* in words */
while (count--)
sum += *p++;
lib->lib_Sum = sum;
lib->lib_Flags &= ~LIBF_SUMMING;
lib->lib_Flags |= LIBF_SUMUSED;
}
/* To verify: recompute and compare to lib_Sum */
/* If mismatch: Alert(AN_LibChkSum) — Guru Meditation */
```
> **Anti-tamper**: exec periodically validates library checksums. If `SetFunction()` patches a library, it sets `LIBF_CHANGED` so exec skips the checksum check. Direct JMP table writes (bypassing `SetFunction`) will trigger a checksum Guru on the next validation pass.
---
## Library Lifecycle
```mermaid
stateDiagram-v2
[*] --> OnDisk : LIBS:my.library file
OnDisk --> Loading : OpenLibrary("my.library", 1)
Loading --> Initialised : LoadSeg + MakeLibrary + LibInit
Initialised --> Added : AddLibrary → SysBase→LibList
Added --> Open : OpenLibrary → Open() LVO → lib_OpenCnt++
Open --> Open : More OpenLibrary calls
Open --> Closing : CloseLibrary → Close() LVO → lib_OpenCnt--
Closing --> Added : lib_OpenCnt > 0
Closing --> Expunging : lib_OpenCnt == 0
Expunging --> [*] : Expunge() → FreeMem + UnLoadSeg
Expunging --> Added : LIBF_DELEXP (deferred — still has openers)
```
---
## References
- NDK39: `exec/libraries.h`, `exec/nodes.h`
- ADCD 2.1 Autodocs: exec — OpenLibrary, MakeLibrary, AddLibrary, SetFunction
- NDK39: `exec/libraries.h`, `exec/nodes.h`, `exec/resident.h`
- ADCD 2.1 Autodocs: `OpenLibrary`, `MakeLibrary`, `MakeFunctions`, `AddLibrary`, `SetFunction`, `SumLibrary`
- *Amiga ROM Kernel Reference Manual: Libraries* — library architecture chapter
- http://amigadev.elowar.com/read/ADCD_2.1/Libraries_Manual_guide/node0002.html
- See also: [SetFunction](setfunction.md) — runtime patching
- See also: [LVO Table](lvo_table.md) — complete offset tables
- See also: [Shared Libraries Runtime](shared_libraries_runtime.md) — OpenLibrary internals

View file

@ -4,112 +4,301 @@
## 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.
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
All OS library calls follow this scheme:
### Integer Registers
| Register | Role |
|---|---|
| **A6** | Library base pointer (always) |
| **D0** | Return value (32-bit integer or BOOL) |
| **D0+D1** | 64-bit return (rare; e.g., `DivideU`) |
| D1D7, A0A3 | Arguments — exact registers per `.fd` |
| D2D7, A2A3 | **Callee-preserved** (OS will not trash these) |
| D0, D1, A0, A1 | **Scratch** (may be destroyed by any OS call) |
| A4 | Global data pointer (VBCC; not used by OS) |
| A5 | Frame pointer (some compilers; not used by OS) |
| A6 | Library base — **always trashed to point to lib** |
| A7 | Stack pointer |
| 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:
- **A6 is always destroyed** — it holds the target library base after every OS call
- **D0, D1, A0, A1** are volatile — save them if needed across OS calls
- **FP0, FP1** are scratch if the FPU is present
### 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()`
## Example: dos.library Write()
From `fd/dos_lib.fd`:
```
Write(file,buffer,length)(d1,d2,d3)
```
```c
/* C call: */
LONG n = Write(fh, buf, 512);
```asm
; 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)
```
/* Compiles to: */
MOVEA.L _DOSBase, A6
MOVE.L fh, D1
MOVE.L buf, D2
MOVE.L #512, D3
JSR -48(A6)
; D0 = bytes written (1 = error)
```c
/* C — compiler generates the above automatically */
LONG n = Write(fh, buf, 512);
```
---
## Preserved vs. Scratch Register Summary
## Register Preservation Map
```
Scratch (caller must save if needed):
D0 D1 A0 A1 A6 FP0 FP1
After ANY OS library call:
Preserved (callee saves/restores):
D2 D3 D4 D5 D6 D7
A2 A3 A4 A5
FP2 FP3 FP4 FP5 FP6 FP7
┌─────────────────────────────┐
│ 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, but AmigaOS does **not** use A5 as a frame pointer (unlike the standard System V m68k ABI).
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:
When one library function calls another library internally, it must save/restore A6:
```asm
; save A6 (current lib base), load new lib base
MOVEM.L A6, -(SP)
MOVEA.L _GfxBase, A6
JSR -102(A6) ; graphics.library BltClear()
MOVEM.L (SP)+, 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
```
Failure to save/restore A6 is a common bug in hand-written assembly library code.
> **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
```c
/* SAS/C: compile with -b0 (small data model) */
/* Generates A4-relative addressing for globals */
```
```asm
; 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:
```c
/* Library function: */
LONG __saveds MyFunc(LONG arg __asm("d0"))
{
/* __saveds generates: */
/* LEA __LinkerDB, A4 ; reload small-data base */
/* ...function code... */
return arg * 2;
}
```
```asm
; 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
```c
/* 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**:
```c
/* 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
```c
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:
```c
/* 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
- Generates standard `MOVEA.L libbase,A6; JSR -lvo(A6)` via `#pragma amicall`
- Uses A5 as a frame pointer in non-leaf functions
- Stack frame: `LINK A5,#-N` on entry, `UNLK A5` on exit
- 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)
- Generates inline-asm stubs with explicit register constraints
- No frame pointer by default (`-fomit-frame-pointer`)
- D2D7/A2A3 saved on stack per function (ABI-compatible)
- Uses A4 for small data (`-mbaserel`, `-msmall-code`)
- Inline assembly with `__asm("register")` constraints
- `__attribute__((saveds))` for A4 reload
### VBCC
- Uses `__reg()` storage class for explicit register placement
- No frame pointer — tighter code than SAS/C for register-intensive functions
- `__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 Calling Convention in IDA Pro
## Detecting the Convention in Disassembly
### Identifying an OS API Call
Pattern to identify an OS API call in disassembly:
```asm
MOVEA.L (_DOSBase).L, A6 ; load library base
JSR (-138,A6) ; call at LVO 138
; 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.
@ -119,6 +308,8 @@ Cross-reference the LVO against the `.fd` file to identify the function. IDA's A
## 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 chapter
- SAS/C 6.x Programmer's Guide — calling convention, small data, __saveds
- GCC m68k-amigaos (bebbo) — `libnix` inline headers
- VBCC documentation — register specification chapter

View file

@ -1,116 +1,430 @@
[← Home](../README.md) · [Linking & Libraries](README.md)
# Shared Library Runtime Mechanics
# Shared Library Runtime — OpenLibrary, ramlib, Version Negotiation, Expunge
## Overview
AmigaOS shared libraries are **resident in memory** — once opened, the same code is shared by all tasks. The OS tracks open counts, handles version negotiation, and defers unloading until all users have closed the library.
AmigaOS shared libraries are **resident in memory** — once opened, the same code and JMP table are shared by all tasks. The OS tracks open counts, handles version negotiation, can load libraries from disk on demand, and defers unloading until all users have closed the library. This document covers every aspect of the runtime lifecycle.
---
## Library Discovery: `OpenLibrary()`
## Architecture
```mermaid
graph TB
subgraph "Application"
APP["MyApp<br/>calls OpenLibrary()"]
end
subgraph "exec.library"
OL["OpenLibrary()<br/>search LibList"]
FL["FindResident()<br/>check ROM modules"]
end
subgraph "ramlib (disk loader)"
RL["LoadSeg()<br/>from LIBS:"]
INIT["InitResident()<br/>MakeLibrary()"]
ADD["AddLibrary()<br/>→ LibList"]
end
subgraph "Library Instance"
LIB["struct Library<br/>lib_OpenCnt++<br/>Open() LVO"]
end
APP --> OL
OL -->|"Found in LibList"| LIB
OL -->|"Not found"| FL
FL -->|"ROM resident"| INIT
FL -->|"Not in ROM"| RL
RL --> INIT
INIT --> ADD
ADD --> LIB
LIB -->|"Return base"| APP
style APP fill:#e8f4fd,stroke:#2196f3,color:#333
style LIB fill:#e8f5e9,stroke:#4caf50,color:#333
```
---
## OpenLibrary() — Complete Path
```c
struct Library *OpenLibrary(CONST_STRPTR libName, ULONG version);
/* A1 = libName, D0 = version */
/* Returns: D0 = library base pointer, or NULL on failure */
```
`exec.library` searches for the library in this order:
1. `exec.library` LibList (already-open libraries in RAM)
2. Resident module list (ROM-resident: exec, graphics, etc.)
3. DOS path search — `LIBS:` assign — scan for `libName`
4. If found on disk: `LoadSeg()` + `InitLib` → add to LibList
5. Increment `lib_OpenCnt`
6. Call library's `Open()` vector
7. Return library base pointer (NULL on failure or version mismatch)
### Version Checking
### Step-by-Step Resolution
```c
DOSBase = (struct DosLibrary *)OpenLibrary("dos.library", 36);
if (!DOSBase) { /* OS older than 2.0 — handle gracefully */ }
```
/* exec.library OpenLibrary() internals: */
The `version` argument is the **minimum** acceptable `lib_Version`. Version 0 accepts any.
struct Library *OpenLibrary(CONST_STRPTR name, ULONG minVersion)
{
struct Library *lib;
| Library | Version | OS |
|---|---|---|
| `exec.library` | 33 | OS 1.2 |
| `exec.library` | 36 | OS 2.0 |
| `exec.library` | 39 | OS 3.0 |
| `exec.library` | 40 | OS 3.1 |
| `exec.library` | 44 | OS 3.2 |
/* Step 1: Search already-loaded libraries */
Forbid();
lib = (struct Library *)FindName(&SysBase->LibList, name);
Permit();
---
if (lib)
{
/* Found — check version */
if (lib->lib_Version >= minVersion || minVersion == 0)
{
/* Step 2: Call library's Open() vector */
lib = (struct Library *)
AROS_LVO_CALL0(struct Library *, lib, 1);
/* lib_OpenCnt++ happens inside the Open() function */
return lib;
}
/* Version too old — try loading a newer one from disk */
}
## Library Base Structure
/* Step 3: Check ROM resident modules */
struct Resident *res = FindResident(name);
if (res && (res->rt_Version >= minVersion || minVersion == 0))
{
/* Initialize from ROM — like during boot */
InitResident(res, 0);
/* Now it should be in LibList */
lib = (struct Library *)FindName(&SysBase->LibList, name);
if (lib)
{
lib = (struct Library *)AROS_LVO_CALL0(struct Library *, lib, 1);
return lib;
}
}
```c
/* exec/libraries.h */
struct Library {
struct Node lib_Node; /* ln_Type = NT_LIBRARY */
UBYTE lib_Flags; /* LIBF_SUMUSED, LIBF_DELEXP */
UBYTE lib_Pad;
UWORD lib_NegSize; /* bytes of JMP table preceding base */
UWORD lib_PosSize; /* sizeof(Library) + private fields */
UWORD lib_Version;
UWORD lib_Revision;
APTR lib_IdString; /* "dos.library 40.1 (16.7.93)" */
ULONG lib_Sum; /* JMP table checksum */
UWORD lib_OpenCnt; /* reference count */
};
```
/* Step 4: Ask ramlib to load from disk */
/* Send a message to the ramlib process via its port "LIBS:" */
lib = RamlibLoadLibrary(name);
if (lib && (lib->lib_Version >= minVersion || minVersion == 0))
{
lib = (struct Library *)AROS_LVO_CALL0(struct Library *, lib, 1);
return lib;
}
The pointer returned by `OpenLibrary` points to this structure. The JMP table is **below** the base at negative offsets.
---
## Standard Library Vectors
| Offset | Function | Description |
|---|---|---|
| 6 | `Open` | Increment open count, return base |
| 12 | `Close` | Decrement count, optionally expunge |
| 18 | `Expunge` | Free library if open count == 0 |
| 24 | `Reserved` | Always NULL |
| 30 and below | Library-specific | Per `.fd` file |
---
## Open Count and Expunge Deferral
```c
/* exec.library CloseLibrary() pseudo-code */
lib->lib_OpenCnt--;
if (lib->lib_OpenCnt == 0) {
BPTR seg = CallVector(lib, CLOSE_VEC);
if (seg) UnLoadSeg(seg);
return NULL; /* Not found or version mismatch */
}
```
A library sets `LIBF_DELEXP` when it cannot unload (low memory). On the next close that drops the count to zero, expunge runs.
### What the Library's Open() Does
```c
/* Typical Open() implementation: */
struct Library * __saveds MyOpen(void)
{
struct MyLibBase *base = (struct MyLibBase *)REG_A6;
base->lib.lib_OpenCnt++;
base->lib.lib_Flags &= ~LIBF_DELEXP; /* Cancel pending expunge */
return (struct Library *)base;
}
```
---
## Open/Close Lifecycle Diagram
## ramlib — The Disk Library Loader
`ramlib` is a hidden system process created during boot. It loads libraries and devices from disk when they're not already in memory.
### How ramlib Works
```c
/* ramlib is a Process created by strap during boot */
/* It listens on a MsgPort for load requests */
void RamlibMain(void)
{
struct MsgPort *port = CreateMsgPort();
port->mp_Node.ln_Name = "ramlib.port";
for (;;)
{
WaitPort(port);
struct RamlibMsg *msg = (struct RamlibMsg *)GetMsg(port);
switch (msg->rm_Type)
{
case RAMLIB_LOAD_LIB:
LoadLibraryFromDisk(msg->rm_Name);
break;
case RAMLIB_LOAD_DEV:
LoadDeviceFromDisk(msg->rm_Name);
break;
}
ReplyMsg((struct Message *)msg);
}
}
```
### Disk Search Path
ramlib searches for libraries in this order:
| Priority | Path | Source |
|---|---|---|
| 1 | `LIBS:` | Standard assign — usually `SYS:Libs` |
| 2 | `LIBS:` on each mounted volume | If `LIBS:` is a multi-assign |
| 3 | Current directory of the requesting process | Fallback |
```
; Standard LIBS: assign (set during Startup-Sequence):
Assign LIBS: SYS:Libs
; Can add additional paths:
Assign LIBS: WORK:MyLibs ADD
```
### Loading Sequence
```c
void LoadLibraryFromDisk(CONST_STRPTR name)
{
/* 1. Construct path: "LIBS:mylib.library" */
char path[256];
sprintf(path, "LIBS:%s", name);
/* 2. LoadSeg the library executable */
BPTR segList = LoadSeg(path);
if (!segList) return;
/* 3. Find RomTag in loaded segments */
struct Resident *res = FindResidentInSegList(segList);
if (!res) { UnLoadSeg(segList); return; }
/* 4. InitResident — calls MakeLibrary + LibInit */
InitResident(res, segList);
/* Library is now in SysBase->LibList */
/* The segList is stored in the library's private data */
/* for Expunge to call UnLoadSeg later */
}
```
---
## Version Negotiation
### Version Numbers
```c
struct Library {
/* ... */
UWORD lib_Version; /* Major version — matches OS release */
UWORD lib_Revision; /* Minor revision — bug fixes */
/* ... */
};
```
| Library | Version | OS Release |
|---|---|---|
| exec.library | 33 | OS 1.2 |
| exec.library | 34 | OS 1.3 |
| exec.library | 36 | OS 2.0 |
| exec.library | 37 | OS 2.04 |
| exec.library | 39 | OS 3.0 |
| exec.library | 40 | OS 3.1 |
| exec.library | 44 | OS 3.1.4 |
| exec.library | 47 | OS 3.2 |
### Version Checking Patterns
```c
/* Require minimum version: */
DOSBase = (struct DosLibrary *)OpenLibrary("dos.library", 36);
if (!DOSBase)
{
/* OS 2.0 or later not available — degrade gracefully */
FallbackToOldAPI();
}
/* Accept any version: */
GfxBase = (struct GfxBase *)OpenLibrary("graphics.library", 0);
/* version 0 = accept anything */
/* Check for specific features at runtime: */
if (GfxBase->LibNode.lib_Version >= 39)
{
/* OS 3.0+ features available: WriteChunkyPixels, etc. */
UseChunkyAPI();
}
else
{
/* Fall back to planar blitting */
UsePlanarAPI();
}
```
### Multiple Versions on Disk
If both ROM and disk versions exist:
- ROM version is initialized during boot
- If a disk version has a **higher** version number, it can replace the ROM version
- `SetPatch` loads patched modules from disk to fix ROM bugs
```
; SetPatch loads replacement modules:
C:SetPatch QUIET
; Internally does:
; 1. LoadSeg("LIBS:exec.library") — disk version
; 2. If newer than ROM version: patch the JMP table
; 3. ROM code remains but JMP slots point to disk code
```
---
## CloseLibrary() and Expunge
### CloseLibrary Path
```c
void CloseLibrary(struct Library *lib);
/* A1 = library base */
/* Internally: */
void CloseLibrary(struct Library *lib)
{
if (!lib) return;
/* Call library's Close() vector (LVO -12) */
BPTR segList = AROS_LVO_CALL0(BPTR, lib, 2);
/* If Close() returned a segment list, unload it */
if (segList)
UnLoadSeg(segList);
}
```
### The Library's Close() Implementation
```c
BPTR __saveds MyClose(void)
{
struct MyLibBase *base = (struct MyLibBase *)REG_A6;
base->lib.lib_OpenCnt--;
if (base->lib.lib_OpenCnt == 0)
{
if (base->lib.lib_Flags & LIBF_DELEXP)
{
/* Delayed expunge was requested — do it now */
return MyExpunge();
}
}
return 0; /* Don't unload yet */
}
```
### Expunge Mechanics
```c
BPTR __saveds MyExpunge(void)
{
struct MyLibBase *base = (struct MyLibBase *)REG_A6;
/* Can't expunge if still in use */
if (base->lib.lib_OpenCnt > 0)
{
base->lib.lib_Flags |= LIBF_DELEXP;
return 0; /* Set flag for deferred expunge */
}
/* 1. Remove from system library list */
Remove(&base->lib.lib_Node);
/* 2. Save segment list before freeing memory */
BPTR segList = base->segList;
/* 3. Free the library memory (JMP table + data) */
ULONG negSize = base->lib.lib_NegSize;
ULONG posSize = base->lib.lib_PosSize;
FreeMem((UBYTE *)base - negSize, negSize + posSize);
/* 4. Return segment list — caller will UnLoadSeg() */
return segList;
}
```
### When Does Expunge Happen?
| Trigger | Mechanism |
|---|---|
| Last `CloseLibrary()` call | If `LIBF_DELEXP` is set, expunge runs immediately |
| Memory-low condition | exec calls `Expunge()` on all libraries to free RAM |
| `AvailMem()` returns low | exec tries to reclaim memory from idle libraries |
| `RemLibrary()` | Force-removes library (dangerous — crashes if openers exist) |
### Memory-Low Expunge Sweep
```c
/* exec's memory reclamation when AllocMem fails: */
void TryFreeMemory(void)
{
/* Walk LibList, try to expunge idle libraries */
struct Library *lib;
Forbid();
for (lib = (struct Library *)SysBase->LibList.lh_Head;
lib->lib_Node.ln_Succ;
lib = (struct Library *)lib->lib_Node.ln_Succ)
{
if (lib->lib_OpenCnt == 0)
{
/* Try expunge — library frees its own memory */
BPTR seg = CallExpunge(lib);
if (seg) UnLoadSeg(seg);
}
}
Permit();
}
```
---
## Open/Close Lifecycle
```mermaid
stateDiagram-v2
[*] --> Unloaded
Unloaded --> Initialised : LoadSeg + InitLib
Initialised --> Open : lib_OpenCnt++ (OpenLibrary)
Open --> Open : additional opens
Open --> Initialised : CloseLibrary, count > 0
Initialised --> Expunging : CloseLibrary, count == 0
Expunging --> Unloaded : Expunge OK — FreeMem + UnLoadSeg
Expunging --> Initialised : LIBF_DELEXP — deferred
[*] --> Unloaded : File on disk
Unloaded --> Loading : OpenLibrary → ramlib
Loading --> Initialised : LoadSeg + InitResident
Initialised --> InLibList : AddLibrary → SysBase→LibList
InLibList --> Open : Open() LVO, lib_OpenCnt=1
Open --> Open : OpenLibrary (lib_OpenCnt++)
Open --> Closing : CloseLibrary (lib_OpenCnt--)
Closing --> Open : lib_OpenCnt > 0
Closing --> InLibList : lib_OpenCnt == 0, no DELEXP
InLibList --> Expunging : Memory pressure or CloseLib+DELEXP
Expunging --> Unloaded : Expunge OK → FreeMem + UnLoadSeg
Expunging --> InLibList : DELEXP set (openers returned)
```
---
## Common Pitfalls
| Pitfall | Consequence | Prevention |
|---|---|---|
| Not checking `OpenLibrary()` return | NULL dereference → Guru | Always check for NULL |
| Mismatched `Open`/`Close` counts | Memory leak — library never expunges | Match every `OpenLibrary` with `CloseLibrary` |
| Using library after `CloseLibrary` | JMP table may point to freed memory | NULL the base pointer after close |
| Calling `OpenLibrary` from interrupt | Deadlock — ramlib uses DOS (which waits) | Only open libraries from task context |
| Not replying WBStartup message | Workbench process hangs | Always `Forbid(); ReplyMsg()` on exit |
| `RemLibrary()` while openers exist | Crash — openers call freed JMP table | Only use on libraries you own with refcount 0 |
---
## References
- NDK39: `exec/libraries.h`, `exec/nodes.h`
- ADCD 2.1 Autodocs: `OpenLibrary`, `CloseLibrary`, `MakeLibrary`
- NDK39: `exec/libraries.h`, `exec/execbase.h`
- ADCD 2.1 Autodocs: `OpenLibrary`, `CloseLibrary`, `MakeLibrary`, `RemLibrary`
- *Amiga ROM Kernel Reference Manual: Libraries* — library creation chapter
- http://amigadev.elowar.com/read/ADCD_2.1/Libraries_Manual_guide/node0124.html
- See also: [Library Structure](library_structure.md) — JMP table and MakeLibrary internals
- See also: [SetFunction](setfunction.md) — runtime patching
- See also: [LVO Table](lvo_table.md) — complete offset tables