mirror of
https://github.com/alfishe/amiga-bootcamp.git
synced 2026-06-12 16:16:28 +00:00
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:
parent
99a6d53f57
commit
7df1f11f15
8 changed files with 1556 additions and 360 deletions
|
|
@ -15,13 +15,13 @@ This section covers the complete lifecycle of an AmigaOS executable:
|
|||
|
||||
| File | Topic |
|
||||
|---|---|
|
||||
| [hunk_format.md](hunk_format.md) | Complete HUNK binary specification |
|
||||
| [hunk_ext_deep_dive.md](hunk_ext_deep_dive.md) | HUNK_EXT: exports, imports, commons |
|
||||
| [hunk_relocation.md](hunk_relocation.md) | HUNK_RELOC32/16/8 mechanics |
|
||||
| [hunk_debug_info.md](hunk_debug_info.md) | HUNK_SYMBOL, HUNK_DEBUG (stabs) |
|
||||
| [exe_load_pipeline.md](exe_load_pipeline.md) | LoadSeg → Process creation |
|
||||
| [object_file_format.md](object_file_format.md) | Compiler object files (HUNK_UNIT) |
|
||||
| [overlay_system.md](overlay_system.md) | HUNK_OVERLAY memory segmentation |
|
||||
| [hunk_format.md](hunk_format.md) | Complete HUNK binary specification — all 22 hunk type codes with wire format, memory flags, advisory bits |
|
||||
| [hunk_ext_deep_dive.md](hunk_ext_deep_dive.md) | HUNK_EXT: exports (EXT_DEF), imports (EXT_REF32), commons, linker resolution |
|
||||
| [hunk_relocation.md](hunk_relocation.md) | Relocation mechanics: visual before/after, patching algorithm, RELOC32/SHORT/DREL32, PC-relative impact |
|
||||
| [hunk_debug_info.md](hunk_debug_info.md) | HUNK_SYMBOL and HUNK_DEBUG: stabs format (SAS/C, GCC), debugger consumption, stripping |
|
||||
| [exe_load_pipeline.md](exe_load_pipeline.md) | LoadSeg → AllocMem → relocation → segment chain → CreateProc → entry point |
|
||||
| [object_file_format.md](object_file_format.md) | Compiler object files (HUNK_UNIT), multi-section layout, HUNK_LIB archives, linker operation |
|
||||
| [overlay_system.md](overlay_system.md) | HUNK_OVERLAY: tree architecture, runtime overlay manager, worked binary example, modern alternatives |
|
||||
|
||||
## Why HUNK?
|
||||
|
||||
|
|
|
|||
|
|
@ -4,23 +4,65 @@
|
|||
|
||||
## Overview
|
||||
|
||||
Relocation is the process of **patching absolute addresses** in a loaded executable to reflect its actual memory location. Since AmigaOS allocates memory dynamically, a program cannot know its load address at compile time — all inter-hunk references must be fixed up at runtime.
|
||||
Relocation is the process of **patching absolute addresses** in a loaded executable to reflect its actual memory location. Since AmigaOS allocates memory dynamically via `AllocMem()`, a program cannot know its load address at compile time — all inter-hunk references must be fixed up at runtime by the loader.
|
||||
|
||||
---
|
||||
|
||||
## Why Relocation Is Necessary
|
||||
|
||||
An Amiga executable contains references like:
|
||||
```asm
|
||||
LEA DataTable(PC), A0 ; PC-relative — no relocation needed
|
||||
MOVE.L #DataTable, A0 ; Absolute — MUST be relocated
|
||||
Consider a program with a code hunk and a data hunk:
|
||||
|
||||
```c
|
||||
/* Source code: */
|
||||
const char message[] = "Hello"; /* in data hunk */
|
||||
void foo(void) {
|
||||
puts(message); /* code references data hunk — absolute address needed */
|
||||
}
|
||||
```
|
||||
|
||||
The linker places `DataTable` at some hunk-relative offset (e.g., offset 0 in the data hunk). The absolute address is only known at load time. The relocation table tells the loader which longwords in the code contain these absolute values.
|
||||
The linker places `message` at offset 0 in the data hunk. But the absolute address of `message` depends on where `AllocMem` places the data hunk at runtime — this is unknown at link time.
|
||||
|
||||
```asm
|
||||
; What the linker writes (before loading):
|
||||
foo:
|
||||
PEA $00000000 ; ← linker writes offset 0 (within data hunk)
|
||||
JSR _puts
|
||||
ADDQ.L #4, SP
|
||||
|
||||
; After loading (data hunk loaded at $00040000):
|
||||
foo:
|
||||
PEA $00040000 ; ← loader patches: 0 + $40000 = $40000
|
||||
JSR _puts
|
||||
ADDQ.L #4, SP
|
||||
```
|
||||
|
||||
The relocation table tells the loader **which bytes** to patch and **what base address** to add.
|
||||
|
||||
---
|
||||
|
||||
## HUNK_RELOC32 Format
|
||||
## Visual: Before and After Relocation
|
||||
|
||||
```
|
||||
BEFORE (raw file): AFTER (loaded at runtime):
|
||||
Code hunk loaded at $00020000
|
||||
Data hunk loaded at $00040000
|
||||
|
||||
Code Hunk: Code Hunk:
|
||||
offset $00: MOVEQ #0, D0 offset $00: MOVEQ #0, D0
|
||||
offset $04: LEA $00000000, A0 ←─┐ offset $04: LEA $00040000, A0 ✓ patched
|
||||
offset $0A: BSR $00000020 │ offset $0A: BSR $00000020
|
||||
offset $0E: MOVE.L $00000010, D1 ←┤ offset $0E: MOVE.L $00040010, D1 ✓ patched
|
||||
│
|
||||
HUNK_RELOC32: │
|
||||
target_hunk = 1 (data) │
|
||||
offsets = [$06, $10] ─────────┘ Relocation adds $00040000 at these offsets
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HUNK_RELOC32 Format ($3EC)
|
||||
|
||||
The most common relocation type:
|
||||
|
||||
```
|
||||
HUNK_RELOC32 ($000003EC)
|
||||
|
|
@ -28,105 +70,204 @@ HUNK_RELOC32 ($000003EC)
|
|||
[Repeat until terminator:]
|
||||
<num_offsets> Number of longword addresses to patch for this target hunk
|
||||
<target_hunk> Index of the hunk whose base address is added
|
||||
<offset_0> Byte offset within the current hunk to patch
|
||||
<offset_0> Byte offset within the CURRENT hunk to patch
|
||||
<offset_1>
|
||||
...
|
||||
|
||||
<0> num_offsets = 0 terminates the reloc list
|
||||
HUNK_END ($000003F2)
|
||||
```
|
||||
|
||||
### Patching Algorithm
|
||||
|
||||
For each entry in HUNK_RELOC32 of hunk `H`:
|
||||
```
|
||||
foreach (target_hunk, offsets[]):
|
||||
base = segment_base_address[target_hunk]
|
||||
foreach offset in offsets:
|
||||
*(ULONG *)(H_base + offset) += base
|
||||
```c
|
||||
/* For each entry group in HUNK_RELOC32 of hunk H: */
|
||||
for (group = 0; group < num_groups; group++)
|
||||
{
|
||||
ULONG count = Read32(); /* how many patch sites */
|
||||
if (count == 0) break; /* terminator */
|
||||
ULONG target_hunk = Read32(); /* which hunk's base to add */
|
||||
ULONG target_base = segment_base_address[target_hunk];
|
||||
|
||||
for (ULONG i = 0; i < count; i++)
|
||||
{
|
||||
ULONG offset = Read32(); /* byte offset within current hunk */
|
||||
ULONG *patch = (ULONG *)(hunk_H_base + offset);
|
||||
*patch += target_base; /* add actual load address */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The value at `H_base + offset` already contains the **hunk-relative** address written by the linker. Adding the actual base produces the final absolute address.
|
||||
The value at the patch site already contains the **hunk-relative offset** written by the linker. Adding the target hunk's actual base address produces the final absolute address.
|
||||
|
||||
### Example
|
||||
### Worked Example — Two Hunks
|
||||
|
||||
Code hunk references data hunk at two sites:
|
||||
```
|
||||
Before load (raw file values):
|
||||
code[0x18] = $00000000 ; linker placed "data offset 0" here
|
||||
code[0x2C] = $00000010 ; linker placed "data offset 0x10" here
|
||||
File layout:
|
||||
Hunk 0 (CODE): 128 bytes, references data at offsets $18 and $2C
|
||||
Hunk 1 (DATA): 64 bytes
|
||||
|
||||
HUNK_RELOC32:
|
||||
num_offsets = 2
|
||||
target_hunk = 1 ; data hunk
|
||||
offsets = [0x18, 0x2C]
|
||||
Loaded at runtime:
|
||||
Hunk 0 → $00020000 (code)
|
||||
Hunk 1 → $00030000 (data)
|
||||
|
||||
After load (data hunk loaded at $20000):
|
||||
code[0x18] = $00000000 + $20000 = $00020000
|
||||
code[0x2C] = $00000010 + $20000 = $00020010
|
||||
HUNK_RELOC32 for Hunk 0:
|
||||
$00000002 ; 2 offsets to patch
|
||||
$00000001 ; target = hunk 1 (data)
|
||||
$00000018 ; patch at code+$18
|
||||
$0000002C ; patch at code+$2C
|
||||
$00000000 ; terminator
|
||||
|
||||
Before patch:
|
||||
code[$18] = $00000000 (data offset 0)
|
||||
code[$2C] = $00000010 (data offset $10)
|
||||
|
||||
After patch:
|
||||
code[$18] = $00000000 + $00030000 = $00030000 ✓
|
||||
code[$2C] = $00000010 + $00030000 = $00030010 ✓
|
||||
```
|
||||
|
||||
### Self-Referencing (Intra-Hunk) Relocation
|
||||
|
||||
Code can also reference its own hunk:
|
||||
|
||||
```
|
||||
HUNK_RELOC32 for Hunk 0:
|
||||
$00000001 ; 1 offset
|
||||
$00000000 ; target = hunk 0 (self!)
|
||||
$00000044 ; patch at code+$44
|
||||
$00000000 ; terminator
|
||||
|
||||
This happens when code contains an absolute reference to a label
|
||||
within the same hunk (e.g., a jump table with absolute addresses).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HUNK_RELOC16 and HUNK_RELOC8
|
||||
## HUNK_RELOC32SHORT ($3FC) — Compact Variant
|
||||
|
||||
Same format as HUNK_RELOC32 but patch **16-bit** or **8-bit** values:
|
||||
- `HUNK_RELOC16` ($3ED): patches UWORD at offset
|
||||
- `HUNK_RELOC8` ($3EE): patches UBYTE at offset
|
||||
|
||||
These are rare in practice — the 68000 requires even-aligned word accesses and only supports 16-bit displacement in most addressing modes.
|
||||
|
||||
---
|
||||
|
||||
## HUNK_DREL32 — Short Relocation (32-bit)
|
||||
|
||||
`HUNK_DREL32` ($3F7) is an alternative relocation format used by some linkers (e.g., BLink) for smaller reloc tables:
|
||||
Uses 16-bit values instead of 32-bit for offsets:
|
||||
|
||||
```
|
||||
HUNK_DREL32
|
||||
HUNK_RELOC32SHORT ($000003FC)
|
||||
|
||||
[Repeat:]
|
||||
<num_offsets> (WORD, not LONGWORD)
|
||||
<target_hunk> (WORD)
|
||||
<offset_0> (WORD)
|
||||
<num_offsets> (UWORD — 16-bit!)
|
||||
<target_hunk> (UWORD)
|
||||
<offset_0> (UWORD)
|
||||
...
|
||||
|
||||
<0> terminator
|
||||
<0> UWORD terminator
|
||||
[padding to longword boundary if needed]
|
||||
```
|
||||
|
||||
By using 16-bit values, this format is more compact for programs with many relocations and small hunk sizes (<64 KB). AmigaOS `InternalLoadSeg` supports both formats.
|
||||
Saves space when all patch offsets fit in 16 bits (hunk size < 64 KB). The **semantics are identical** to HUNK_RELOC32 — only the field sizes differ.
|
||||
|
||||
Modern linkers (vlink, vasm) prefer this format for small programs.
|
||||
|
||||
---
|
||||
|
||||
## HUNK_RELOC16 ($3ED) and HUNK_RELOC8 ($3EE)
|
||||
|
||||
Same format as HUNK_RELOC32 but patch **16-bit** or **8-bit** values respectively:
|
||||
|
||||
| Type | Patches | Use Case |
|
||||
|---|---|---|
|
||||
| HUNK_RELOC32 | ULONG (4 bytes) | Standard — absolute 32-bit addresses |
|
||||
| HUNK_RELOC16 | UWORD (2 bytes) | 16-bit displacement mode (rare) |
|
||||
| HUNK_RELOC8 | UBYTE (1 byte) | 8-bit short-branch offset (extremely rare) |
|
||||
|
||||
HUNK_RELOC16 and HUNK_RELOC8 are almost never seen in practice — the 68000 doesn't commonly use 16-bit absolute addresses, and linkers generate PC-relative code for short displacements instead.
|
||||
|
||||
---
|
||||
|
||||
## HUNK_DREL32 ($3F7) — Base-Relative Compact Relocation
|
||||
|
||||
An alternative relocation format used by some linkers (BLink):
|
||||
|
||||
```
|
||||
HUNK_DREL32 ($000003F7)
|
||||
|
||||
[Repeat:]
|
||||
<num_offsets> (UWORD)
|
||||
<target_hunk> (UWORD)
|
||||
<offset_0> (UWORD)
|
||||
...
|
||||
|
||||
<0> UWORD terminator
|
||||
```
|
||||
|
||||
Semantically identical to HUNK_RELOC32 but uses 16-bit fields. More compact for programs with many relocations and small hunk sizes (< 64 KB). Supported by `InternalLoadSeg`.
|
||||
|
||||
---
|
||||
|
||||
## HUNK_RELRELOC32 ($3FD) — PC-Relative Relocation
|
||||
|
||||
Patches a **32-bit displacement** rather than an absolute address:
|
||||
|
||||
```c
|
||||
/* Patch algorithm for PC-relative: */
|
||||
*patch = target_base - (current_hunk_base + offset);
|
||||
/* The patched value is a signed offset from the patch site to the target */
|
||||
```
|
||||
|
||||
Used by GCC with `-fPIC` for position-independent code. Rare in standard AmigaOS programs.
|
||||
|
||||
---
|
||||
|
||||
## PC-Relative References (No Relocation Needed)
|
||||
|
||||
The 68020+ supports **PC-relative addressing** with 32-bit displacements:
|
||||
The 68020+ supports **32-bit PC-relative addressing**, and even the 68000 supports 16-bit PC-relative:
|
||||
|
||||
```asm
|
||||
LEA symbol(PC), A0 ; PC-relative load effective address
|
||||
MOVE.L data(PC), D0 ; PC-relative data read
|
||||
; 68000 — 16-bit PC-relative (within ±32 KB):
|
||||
LEA myData(PC), A0 ; PC-relative — no reloc needed
|
||||
BSR myFunction ; PC-relative branch — no reloc
|
||||
|
||||
; 68020+ — 32-bit PC-relative:
|
||||
MOVE.L myData(PC), D0 ; PC-relative with 32-bit displacement
|
||||
```
|
||||
|
||||
PC-relative references do not require relocation — the offset is relative to the instruction, so it is valid regardless of where the code is loaded. **GCC for 68k** generates PC-relative code by default (`-fpic`), significantly reducing the size of relocation tables.
|
||||
PC-relative references are **relocation-free** — the offset is relative to the instruction pointer, so it remains valid regardless of where the code loads.
|
||||
|
||||
SAS/C generates absolute references by default and relies heavily on `HUNK_RELOC32`.
|
||||
| Compiler | Default Mode | Relocation Impact |
|
||||
|---|---|---|
|
||||
| SAS/C | Absolute addressing | Heavy relocation (many HUNK_RELOC32 entries) |
|
||||
| GCC | PC-relative (`-fpic`) | Minimal relocation — smaller executables |
|
||||
| VBCC | PC-relative (with small code model) | Similar to GCC |
|
||||
|
||||
> **Practical impact**: A program with 500 internal function calls generates 500 HUNK_RELOC32 entries with absolute addressing (SAS/C), but nearly zero with PC-relative code (GCC). This affects both file size and load time.
|
||||
|
||||
---
|
||||
|
||||
## Relocation at Runtime — Segment Chain
|
||||
## Relocation and the Segment Chain
|
||||
|
||||
The loader tracks loaded segments as a **BPTR chain** (singly-linked list). The segment list head is returned by `LoadSeg()`:
|
||||
|
||||
```
|
||||
Segment 0 (code):
|
||||
BPTR → Segment 1
|
||||
[code data]
|
||||
byte -4: allocation size (for FreeMem)
|
||||
byte 0: BPTR → Segment 1
|
||||
byte 4: [code data starts here]
|
||||
|
||||
Segment 1 (data):
|
||||
BPTR → 0 (NULL)
|
||||
[data]
|
||||
byte -4: allocation size
|
||||
byte 0: BPTR → 0 (NULL = end of chain)
|
||||
byte 4: [data starts here]
|
||||
```
|
||||
|
||||
Each segment begins with a 4-byte BPTR to the next segment. Hunk index `n` corresponds to segment `n` in this chain.
|
||||
Each segment begins with a 4-byte BPTR to the next segment. Hunk index `n` in the relocation table corresponds to segment `n` in this chain. The base address used for relocation is `segment_address + 4` (skip the BPTR link).
|
||||
|
||||
---
|
||||
|
||||
## Relocation Error Scenarios
|
||||
|
||||
| Error | Cause | Symptom |
|
||||
|---|---|---|
|
||||
| Offset beyond hunk size | Corrupt HUNK_RELOC32 | Random memory corruption; Guru |
|
||||
| Invalid target hunk index | Corrupt reloc table | Crash during load |
|
||||
| Relocation to freed memory | Hunk couldn't be allocated | Dangling pointer — crash at use time |
|
||||
| Missing relocation entry | Linker bug | Pointer has wrong value; subtle crash |
|
||||
| Unaligned offset | Not longword-aligned | Bus error on 68000 (address error) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -137,17 +278,41 @@ After loading a HUNK file with the Amiga plugin, IDA resolves relocations automa
|
|||
|
||||
### hexdump + manual
|
||||
|
||||
Locate HUNK_RELOC32 ($3EC) in raw hex:
|
||||
```bash
|
||||
# Find HUNK_RELOC32 in raw hex:
|
||||
xxd mybinary | grep "0003 ec"
|
||||
# Then read num_offsets, target_hunk, and offset longwords
|
||||
```
|
||||
|
||||
Then read num_offsets and target_hunk longwords that follow.
|
||||
### Python scanner
|
||||
|
||||
### hunkinfo (custom tool)
|
||||
```python
|
||||
import struct
|
||||
|
||||
```bash
|
||||
hunkinfo mybinary # shows all hunks, sizes, reloc counts
|
||||
def dump_relocs(filename):
|
||||
with open(filename, 'rb') as f:
|
||||
data = f.read()
|
||||
off = 0
|
||||
while off < len(data) - 4:
|
||||
tag = struct.unpack('>I', data[off:off+4])[0]
|
||||
if tag == 0x3EC: # HUNK_RELOC32
|
||||
off += 4
|
||||
while True:
|
||||
count = struct.unpack('>I', data[off:off+4])[0]
|
||||
off += 4
|
||||
if count == 0: break
|
||||
target = struct.unpack('>I', data[off:off+4])[0]
|
||||
off += 4
|
||||
offsets = []
|
||||
for i in range(count):
|
||||
o = struct.unpack('>I', data[off:off+4])[0]
|
||||
offsets.append(f'${o:04X}')
|
||||
off += 4
|
||||
print(f' target=hunk{target} offsets={offsets}')
|
||||
else:
|
||||
off += 4
|
||||
|
||||
dump_relocs('mybinary')
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -157,4 +322,5 @@ hunkinfo mybinary # shows all hunks, sizes, reloc counts
|
|||
- NDK39: `dos/doshunks.h`
|
||||
- *Amiga ROM Kernel Reference Manual: Libraries* — AmigaDOS chapter, `InternalLoadSeg`
|
||||
- vlink linker documentation — relocation section
|
||||
- http://amigadev.elowar.com/read/ADCD_2.1/Libraries_Manual_guide/node01E0.html
|
||||
- See also: [HUNK Format](hunk_format.md) — complete hunk type reference
|
||||
- See also: [Exe Load Pipeline](exe_load_pipeline.md) — how LoadSeg uses relocations
|
||||
|
|
|
|||
|
|
@ -4,80 +4,308 @@
|
|||
|
||||
## Overview
|
||||
|
||||
The **overlay system** allows programs larger than available RAM to run by dividing code into **segments loaded on demand**. Only one overlay section is present in memory at any time; others are swapped in from disk when needed.
|
||||
|
||||
This predates virtual memory and was commonly used in A500-era applications with limited Fast RAM.
|
||||
The **overlay system** allows programs larger than available RAM to run by dividing code into **segments loaded on demand** from disk. Only the resident root and one overlay branch are in memory at a time; switching to a different branch automatically unloads the previous one and loads the new one. This predates virtual memory and was essential on A500-era systems with 512 KB–1 MB RAM.
|
||||
|
||||
---
|
||||
|
||||
## When Overlays Are Used
|
||||
## Architecture
|
||||
|
||||
- Application code exceeds available RAM
|
||||
- Rarely-used code paths (setup, error handling) should not occupy memory permanently
|
||||
- The game/app has a fixed resident core and multiple interchangeable level/module overlays
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Resident Root (always in memory)"
|
||||
ROOT["Root Hunks<br/>Main code + data<br/>Overlay manager"]
|
||||
end
|
||||
|
||||
subgraph "Overlay Tree"
|
||||
OV1["Overlay 1<br/>Module A"]
|
||||
OV2["Overlay 2<br/>Module B"]
|
||||
OV3["Overlay 3<br/>Module C"]
|
||||
OV4["Overlay 4<br/>Sub-module B1"]
|
||||
OV5["Overlay 5<br/>Sub-module B2"]
|
||||
end
|
||||
|
||||
ROOT --> OV1
|
||||
ROOT --> OV2
|
||||
ROOT --> OV3
|
||||
OV2 --> OV4
|
||||
OV2 --> OV5
|
||||
|
||||
style ROOT fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style OV1 fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
style OV2 fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
style OV3 fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
```
|
||||
|
||||
### Key Concepts
|
||||
|
||||
| Term | Meaning |
|
||||
|---|---|
|
||||
| **Root** | Hunks always resident in memory — contain the overlay manager |
|
||||
| **Overlay node** | A group of hunks loaded together as a unit |
|
||||
| **Overlay level** | Depth in the tree — root is level 0 |
|
||||
| **Overlay branch** | Path from root to a leaf — only one branch loaded at any time per level |
|
||||
|
||||
---
|
||||
|
||||
## HUNK_OVERLAY Structure
|
||||
## When Overlays Were Used
|
||||
|
||||
### Historical Context
|
||||
|
||||
| Application | Why Overlays Were Needed |
|
||||
|---|---|
|
||||
| Deluxe Paint | Multiple editing modes (paint, animation, text) couldn't fit simultaneously |
|
||||
| SAS/C Compiler | Parser, optimizer, code generator loaded on demand |
|
||||
| Professional Page | Layout engine, text editor, image handler as separate overlays |
|
||||
| Large games | Different game levels or engine modules swapped in/out |
|
||||
|
||||
### Memory Reality (1987–1992)
|
||||
|
||||
| System | Available RAM | Typical Executable |
|
||||
|---|---|---|
|
||||
| A500 | 512 KB Chip | 80–200 KB code + data |
|
||||
| A500 + 512 KB expansion | 1 MB | Up to ~600 KB usable |
|
||||
| A2000 + 2 MB Fast | 2.5 MB | Overlays rarely needed |
|
||||
|
||||
With only 512 KB total (and Chip RAM shared with the display), a complex application simply couldn't fit all its code in memory at once.
|
||||
|
||||
---
|
||||
|
||||
## HUNK_OVERLAY Binary Format
|
||||
|
||||
### Executable Structure
|
||||
|
||||
```
|
||||
HUNK_HEADER
|
||||
(normal header for resident hunks)
|
||||
HUNK_HEADER ($3F3)
|
||||
[Standard header for root hunks only]
|
||||
|
||||
[Normal hunks: code, data, BSS]
|
||||
[Root hunks — always loaded]
|
||||
HUNK_CODE / HUNK_DATA / HUNK_BSS
|
||||
+ HUNK_RELOC32
|
||||
+ HUNK_END (for each root hunk)
|
||||
|
||||
HUNK_OVERLAY ($000003F5)
|
||||
<size_in_longs> total size of overlay table data
|
||||
<overlay_tree...> tree of overlay nodes
|
||||
<table_size_in_longs> Total size of overlay table data
|
||||
<overlay_table_data> Tree structure describing overlay nodes
|
||||
|
||||
HUNK_BREAK ($000003F6) marks end of overlay tree
|
||||
HUNK_BREAK ($000003F6) Marks boundary between tree and overlay hunks
|
||||
|
||||
[Overlay hunks — loaded on demand]
|
||||
HUNK_CODE / HUNK_DATA / HUNK_BSS
|
||||
+ HUNK_RELOC32
|
||||
+ HUNK_END (for each overlay hunk)
|
||||
```
|
||||
|
||||
### Overlay Tree Format
|
||||
|
||||
The overlay tree describes groups of overlays and their dependencies:
|
||||
|
||||
```
|
||||
<num_overlay_nodes>
|
||||
For each node:
|
||||
<num_hunks> number of hunks in this overlay
|
||||
<hunk_sizes...> size of each hunk in longwords
|
||||
<hunk_memory_types...> memory requirements
|
||||
```
|
||||
|
||||
The resident (non-overlay) hunks are hunk 0 through N. The overlay hunks are numbered starting at N+1.
|
||||
|
||||
---
|
||||
|
||||
## Runtime Overlay Support — overlaylibrary
|
||||
|
||||
AmigaOS provides `dos.library` support for overlays via `InternalLoadSeg` with an overlay table. The application calls `ObtainSemaphore()` + `OverlayLoad()` to swap overlays.
|
||||
|
||||
In practice, the overlay system is complex and rarely documented precisely. Most real Amiga applications avoid overlays in favour of:
|
||||
- Dynamic library loading (`OpenLibrary`)
|
||||
- Splitting functionality into separate executables run via `Execute`
|
||||
- AmigaOS shared library mechanism
|
||||
|
||||
---
|
||||
|
||||
## Practical Alternative: Dynamic Linking
|
||||
|
||||
Modern Amiga development (and OS 3.1+ best practices) uses `OpenLibrary()` instead of overlays:
|
||||
### Overlay Table Structure
|
||||
|
||||
```c
|
||||
struct MyLib *MyBase = (struct MyLib *)OpenLibrary("mycode.library", 0);
|
||||
if (MyBase) {
|
||||
MyBase->myFunction(arg1, arg2);
|
||||
CloseLibrary((struct Library *)MyBase);
|
||||
/* The overlay table describes the tree topology: */
|
||||
struct OverlayTable {
|
||||
LONG ot_TableSize; /* Size of this table in longwords */
|
||||
LONG ot_MaxLevel; /* Deepest overlay level */
|
||||
/* Array of overlay nodes: */
|
||||
struct OvlyNode {
|
||||
LONG on_NextNode; /* Offset to next node at same level */
|
||||
LONG on_FirstChild; /* Offset to first child node */
|
||||
LONG on_FileOffset; /* Byte offset of this node's hunks in the file */
|
||||
LONG on_NumHunks; /* Number of hunks in this overlay node */
|
||||
LONG on_HunkTable[]; /* Hunk sizes in longwords (parallels HUNK_HEADER) */
|
||||
} nodes[];
|
||||
};
|
||||
```
|
||||
|
||||
### Worked Binary Example
|
||||
|
||||
A program with 2 root hunks and 3 overlay nodes:
|
||||
|
||||
```
|
||||
Offset Hex Meaning
|
||||
──────────────────────────────────────────────
|
||||
$0000 00 00 03 F3 HUNK_HEADER
|
||||
$0004 00 00 00 00 No resident library names
|
||||
$0008 00 00 00 02 2 root hunks
|
||||
$000C 00 00 00 00 First hunk = 0
|
||||
$0010 00 00 00 01 Last hunk = 1
|
||||
$0014 00 00 00 80 Hunk 0 size = 512 bytes (128 longs)
|
||||
$0018 00 00 00 40 Hunk 1 size = 256 bytes (64 longs)
|
||||
|
||||
$001C 00 00 03 E9 HUNK_CODE (root hunk 0)
|
||||
[512 bytes of root code...]
|
||||
$021C 00 00 03 EC HUNK_RELOC32
|
||||
...
|
||||
$0240 00 00 03 F2 HUNK_END
|
||||
|
||||
$0244 00 00 03 EA HUNK_DATA (root hunk 1)
|
||||
[256 bytes of root data...]
|
||||
$0348 00 00 03 F2 HUNK_END
|
||||
|
||||
$034C 00 00 03 F5 HUNK_OVERLAY
|
||||
$0350 00 00 00 0C Table size = 12 longwords
|
||||
[overlay table data: 3 nodes...]
|
||||
|
||||
$0380 00 00 03 F6 HUNK_BREAK
|
||||
|
||||
$0384 00 00 03 E9 HUNK_CODE (overlay 1 code)
|
||||
[overlay 1 code...]
|
||||
$0500 00 00 03 F2 HUNK_END
|
||||
|
||||
$0504 00 00 03 E9 HUNK_CODE (overlay 2 code)
|
||||
[overlay 2 code...]
|
||||
$0700 00 00 03 F2 HUNK_END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Runtime Overlay Management
|
||||
|
||||
### The Overlay Manager
|
||||
|
||||
The root code contains an **overlay manager** — a small runtime that handles loading and unloading overlay nodes:
|
||||
|
||||
```c
|
||||
/* Overlay manager pseudo-code (embedded in root hunks): */
|
||||
|
||||
static LONG currentOverlay = -1;
|
||||
static BPTR exeFile = 0; /* File handle to the executable itself */
|
||||
|
||||
APTR LoadOverlay(LONG nodeIndex)
|
||||
{
|
||||
struct OvlyNode *node = &overlayTable[nodeIndex];
|
||||
|
||||
if (nodeIndex == currentOverlay)
|
||||
return overlayBase; /* Already loaded */
|
||||
|
||||
/* 1. Unload current overlay (if any) */
|
||||
if (currentOverlay >= 0)
|
||||
{
|
||||
/* Free memory for previous overlay hunks */
|
||||
FreeOverlayHunks(currentOverlay);
|
||||
}
|
||||
|
||||
/* 2. Seek to overlay position in executable file */
|
||||
Seek(exeFile, node->on_FileOffset, OFFSET_BEGINNING);
|
||||
|
||||
/* 3. Load overlay hunks (like a mini-LoadSeg) */
|
||||
for (int i = 0; i < node->on_NumHunks; i++)
|
||||
{
|
||||
ULONG size = node->on_HunkTable[i] * 4;
|
||||
APTR mem = AllocMem(size, MEMF_ANY);
|
||||
Read(exeFile, mem, size);
|
||||
/* Apply relocations for this overlay */
|
||||
ProcessRelocations(mem, i);
|
||||
}
|
||||
|
||||
/* 4. Update state */
|
||||
currentOverlay = nodeIndex;
|
||||
|
||||
return overlayBase;
|
||||
}
|
||||
```
|
||||
|
||||
This is functionally equivalent to overlay loading but uses the OS resource tracking system and allows multiple users.
|
||||
### Calling Into an Overlay
|
||||
|
||||
```c
|
||||
/* Application code in the root: */
|
||||
void DoModuleA(void)
|
||||
{
|
||||
APTR base = LoadOverlay(OVERLAY_MODULE_A); /* Ensure Module A is loaded */
|
||||
((void (*)(void))(base + moduleA_EntryOffset))(); /* Call into it */
|
||||
}
|
||||
|
||||
void DoModuleB(void)
|
||||
{
|
||||
APTR base = LoadOverlay(OVERLAY_MODULE_B); /* Unloads A, loads B */
|
||||
((void (*)(void))(base + moduleB_EntryOffset))();
|
||||
}
|
||||
```
|
||||
|
||||
### Important Constraints
|
||||
|
||||
| Constraint | Reason |
|
||||
|---|---|
|
||||
| Only one overlay per level at a time | They share the same memory region |
|
||||
| Cannot call across overlays | The target overlay may not be loaded |
|
||||
| Root code must manage all transitions | Overlay code cannot load another overlay |
|
||||
| File must remain accessible | The executable is re-read from disk each time |
|
||||
| Disk I/O on every switch | Slow on floppies (~200 ms per overlay load) |
|
||||
|
||||
---
|
||||
|
||||
## Linker Support
|
||||
|
||||
### SLink (SAS/C Linker)
|
||||
|
||||
```
|
||||
slink from module_root.o + module_a.o + module_b.o + module_c.o
|
||||
to myapp
|
||||
overlay
|
||||
with myapp.ovly ; overlay specification file
|
||||
```
|
||||
|
||||
The `.ovly` file describes the tree:
|
||||
|
||||
```
|
||||
; myapp.ovly — overlay specification
|
||||
ROOT module_root.o
|
||||
OVERLAY
|
||||
NODE module_a.o ; overlay node 1
|
||||
NODE module_b.o ; overlay node 2
|
||||
NODE module_b1.o ; child of node 2 (level 2)
|
||||
NODE module_c.o ; overlay node 3
|
||||
END
|
||||
```
|
||||
|
||||
### BLink
|
||||
|
||||
BLink uses a similar mechanism with the `OVERLAY` keyword in the linker script.
|
||||
|
||||
---
|
||||
|
||||
## Modern Alternatives
|
||||
|
||||
The overlay system is effectively obsolete. Modern Amiga development uses these alternatives:
|
||||
|
||||
### Shared Libraries (Recommended)
|
||||
|
||||
```c
|
||||
/* Split functionality into shared libraries loaded on demand */
|
||||
struct Library *ModuleA = OpenLibrary("myapp_modulea.library", 1);
|
||||
if (ModuleA)
|
||||
{
|
||||
ModuleA_DoWork(); /* Calls through JMP table */
|
||||
CloseLibrary(ModuleA); /* Freed when refcount = 0 */
|
||||
}
|
||||
```
|
||||
|
||||
**Advantages**: OS manages memory, multiple users, version checking, proper resource tracking.
|
||||
|
||||
### Separate Executables
|
||||
|
||||
```c
|
||||
/* Run a sub-program and return */
|
||||
SystemTagList("SYS:Tools/SubModule", NULL);
|
||||
```
|
||||
|
||||
### Dynamic LoadSeg
|
||||
|
||||
```c
|
||||
/* Manual segment loading — like overlays but simpler */
|
||||
BPTR seg = LoadSeg("PROGDIR:modules/module_a");
|
||||
if (seg)
|
||||
{
|
||||
/* Get entry point from first code hunk */
|
||||
APTR entry = BADDR(seg) + 4; /* Skip BPTR to next segment */
|
||||
((void (*)(void))entry)();
|
||||
UnLoadSeg(seg);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- *Amiga ROM Kernel Reference Manual: Libraries* — AmigaDOS overlay section
|
||||
- NDK39: `dos/doshunks.h` — HUNK_OVERLAY, HUNK_BREAK
|
||||
- Paul Tuma's Amiga HUNK format notes (community)
|
||||
- NDK39: `dos/doshunks.h` — HUNK_OVERLAY ($3F5), HUNK_BREAK ($3F6)
|
||||
- SAS/C 6.x Linker Manual — overlay chapter
|
||||
- See also: [HUNK Format](hunk_format.md) — complete hunk type reference
|
||||
- See also: [Exe Load Pipeline](exe_load_pipeline.md) — how LoadSeg processes hunks
|
||||
- See also: [Shared Libraries](../04_linking_and_libraries/shared_libraries_runtime.md) — modern alternative
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`) |
|
||||
| D1–D7, A0–A3 | Arguments — exact registers per `.fd` |
|
||||
| D2–D7, A2–A3 | **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 |
|
||||
| D2–D7 | Arguments (per .fd) | **Yes** — callee-preserved |
|
||||
| A2–A3 | 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 |
|
||||
| FP2–FP7 | 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 FP0–FP7 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`)
|
||||
- D2–D7/A2–A3 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
34
README.md
34
README.md
|
|
@ -91,27 +91,27 @@ The Amiga's documentation was scattered across out-of-print manuals, Usenet post
|
|||
### 03 — Executable Loader & HUNK Format
|
||||
| File | Topic |
|
||||
|---|---|
|
||||
| [hunk_format.md](03_loader_and_exec_format/hunk_format.md) | Complete HUNK binary specification |
|
||||
| [hunk_ext_deep_dive.md](03_loader_and_exec_format/hunk_ext_deep_dive.md) | Exports, imports, commons |
|
||||
| [hunk_relocation.md](03_loader_and_exec_format/hunk_relocation.md) | Relocation mechanics |
|
||||
| [hunk_debug_info.md](03_loader_and_exec_format/hunk_debug_info.md) | Debug symbols, stabs |
|
||||
| [exe_load_pipeline.md](03_loader_and_exec_format/exe_load_pipeline.md) | LoadSeg → Process creation |
|
||||
| [object_file_format.md](03_loader_and_exec_format/object_file_format.md) | Compiler object files |
|
||||
| [overlay_system.md](03_loader_and_exec_format/overlay_system.md) | HUNK_OVERLAY mechanism |
|
||||
| [hunk_format.md](03_loader_and_exec_format/hunk_format.md) | Complete HUNK specification — all 22 type codes, wire format, memory flags, advisory bits |
|
||||
| [hunk_ext_deep_dive.md](03_loader_and_exec_format/hunk_ext_deep_dive.md) | HUNK_EXT: exports (EXT_DEF), imports (EXT_REF32), commons, linker resolution |
|
||||
| [hunk_relocation.md](03_loader_and_exec_format/hunk_relocation.md) | Relocation mechanics: visual before/after, RELOC32/SHORT/DREL32, PC-relative impact |
|
||||
| [hunk_debug_info.md](03_loader_and_exec_format/hunk_debug_info.md) | HUNK_SYMBOL and HUNK_DEBUG: stabs format, debugger consumption, stripping |
|
||||
| [exe_load_pipeline.md](03_loader_and_exec_format/exe_load_pipeline.md) | LoadSeg → AllocMem → relocation → segment chain → CreateProc → entry point |
|
||||
| [object_file_format.md](03_loader_and_exec_format/object_file_format.md) | HUNK_UNIT object files, multi-section layout, HUNK_LIB archives, linker operation |
|
||||
| [overlay_system.md](03_loader_and_exec_format/overlay_system.md) | HUNK_OVERLAY: tree architecture, runtime overlay manager, modern alternatives |
|
||||
|
||||
### 04 — Linking & Library Integration
|
||||
| File | Topic |
|
||||
|---|---|
|
||||
| [fd_files.md](04_linking_and_libraries/fd_files.md) | .fd descriptor format, fd2pragma |
|
||||
| [lvo_table.md](04_linking_and_libraries/lvo_table.md) | Library vector offsets |
|
||||
| [library_structure.md](04_linking_and_libraries/library_structure.md) | struct Library, JMP table layout |
|
||||
| [inline_stubs.md](04_linking_and_libraries/inline_stubs.md) | SAS/C, GCC, VBCC inline stubs |
|
||||
| [compiler_stubs.md](04_linking_and_libraries/compiler_stubs.md) | Compiler-generated stub code |
|
||||
| [link_libraries.md](04_linking_and_libraries/link_libraries.md) | amiga.lib, startup objects |
|
||||
| [shared_libraries_runtime.md](04_linking_and_libraries/shared_libraries_runtime.md) | OpenLibrary lifecycle |
|
||||
| [register_conventions.md](04_linking_and_libraries/register_conventions.md) | M68k AmigaOS calling conventions |
|
||||
| [setfunction.md](04_linking_and_libraries/setfunction.md) | SetFunction patching mechanism |
|
||||
| [startup_code.md](04_linking_and_libraries/startup_code.md) | Program startup and exit |
|
||||
| [library_structure.md](04_linking_and_libraries/library_structure.md) | Library memory layout, JMP table encoding, MakeLibrary, complete library creation example |
|
||||
| [shared_libraries_runtime.md](04_linking_and_libraries/shared_libraries_runtime.md) | OpenLibrary resolution, ramlib disk loader, version negotiation, expunge mechanics |
|
||||
| [register_conventions.md](04_linking_and_libraries/register_conventions.md) | Register ABI: integer, FPU, varargs/TagItem, small-data model, __saveds |
|
||||
| [fd_files.md](04_linking_and_libraries/fd_files.md) | .fd descriptor format, LVO calculation, proto/inline generation |
|
||||
| [lvo_table.md](04_linking_and_libraries/lvo_table.md) | Complete exec.library LVO table, IDA reconstruction script |
|
||||
| [compiler_stubs.md](04_linking_and_libraries/compiler_stubs.md) | SAS/C, GCC, VBCC call patterns — compiler signature identification |
|
||||
| [inline_stubs.md](04_linking_and_libraries/inline_stubs.md) | Pragma (SAS/C), inline asm (GCC), __reg (VBCC), stub generation tools |
|
||||
| [link_libraries.md](04_linking_and_libraries/link_libraries.md) | amiga.lib, sc.lib, libnix, auto.lib, WBStartup glue, stack cookie |
|
||||
| [startup_code.md](04_linking_and_libraries/startup_code.md) | c.o / gcrt0.S: entry contract, CLI vs WB detection, argument parsing |
|
||||
| [setfunction.md](04_linking_and_libraries/setfunction.md) | Runtime function patching: canonical pattern, chaining, RE detection |
|
||||
|
||||
### 05 — Reverse Engineering
|
||||
| File | Topic |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue