exec_os: enrich all stubs to bootcamp-quality reference articles

Complete rewrite of 14 exec_os articles from stubs to comprehensive
deep-dive technical references with architecture diagrams, pitfalls,
and best practices.

New: multitasking.md (scheduler, IPC, memory safety, real-world scenarios)
Enriched: exec_base, tasks_processes, library_system, library_vectors,
interrupts, exceptions_traps, memory_management, message_ports, signals,
semaphores, io_requests, lists_nodes, resident_modules

Updated indexes: 06_exec_os/README.md, root README.md
This commit is contained in:
Ilia Sharin 2026-04-23 17:55:31 -04:00
parent 4d136b0672
commit 59929047d4
16 changed files with 4463 additions and 678 deletions

View file

@ -6,15 +6,17 @@
| File | Description |
|---|---|
| [exec_base.md](exec_base.md) | ExecBase structure — full field listing |
| [library_system.md](library_system.md) | Library node, OpenLibrary lifecycle |
| [library_vectors.md](library_vectors.md) | JMP table, LVO offsets, MakeFunctions |
| [tasks_processes.md](tasks_processes.md) | Task/Process structs, scheduling |
| [interrupts.md](interrupts.md) | Interrupt levels, INTENA, AddIntServer |
| [memory_management.md](memory_management.md) | AllocMem, FreeMem, MemHeader |
| [message_ports.md](message_ports.md) | MsgPort, PutMsg, GetMsg, WaitPort |
| [signals.md](signals.md) | AllocSignal, SetSignal, Wait |
| [semaphores.md](semaphores.md) | SignalSemaphore, ObtainSemaphore |
| [io_requests.md](io_requests.md) | IORequest, DoIO, SendIO, AbortIO |
| [lists_nodes.md](lists_nodes.md) | MinList/List/Node traversal |
| [resident_modules.md](resident_modules.md) | RomTag, RTF_AUTOINIT, FindResident |
| [exec_base.md](exec_base.md) | ExecBase — absolute address $4, system lists, hardware abstraction fields |
| [**multitasking.md**](multitasking.md) | **Multitasking deep-dive — scheduler, context switching, IPC, memory safety** |
| [tasks_processes.md](tasks_processes.md) | Task/Process structs, state machine, creation, scheduling |
| [library_system.md](library_system.md) | Library node, OpenLibrary lifecycle, version management |
| [library_vectors.md](library_vectors.md) | JMP table, LVO offsets, MakeFunctions, SetFunction |
| [interrupts.md](interrupts.md) | Interrupt levels 16, INTENA/INTREQ, AddIntServer, CIA interrupts |
| [memory_management.md](memory_management.md) | AllocMem, FreeMem, MemHeader, memory types, pools |
| [message_ports.md](message_ports.md) | MsgPort, PutMsg, GetMsg, WaitPort, public/private ports |
| [signals.md](signals.md) | AllocSignal, SetSignal, Wait, signal bit allocation |
| [semaphores.md](semaphores.md) | SignalSemaphore, shared/exclusive locking, deadlock avoidance |
| [io_requests.md](io_requests.md) | IORequest, DoIO, SendIO, CheckIO, AbortIO, device protocol |
| [lists_nodes.md](lists_nodes.md) | MinList/List/Node traversal, Enqueue, priority insertion |
| [resident_modules.md](resident_modules.md) | RomTag, RTF_AUTOINIT, FindResident, boot-time initialization |
| [exceptions_traps.md](exceptions_traps.md) | M68k exception vectors, Trap handlers, Guru Meditation |

View file

@ -4,16 +4,43 @@
## Overview
The M68k CPU provides a **256-entry exception vector table** starting at address `$000000`. AmigaOS manages these vectors through `exec.library`, allowing both the OS and user code to install handlers for hardware interrupts, bus errors, and software traps.
The M68k CPU provides a **256-entry exception vector table** starting at address `$000000`. AmigaOS manages these vectors through `exec.library`, allowing both the OS and user code to install handlers for hardware interrupts, bus errors, and software traps. Understanding the exception model is essential for debugger development, system programming, and diagnosing Guru Meditations.
---
## Exception Vector Table
```mermaid
graph LR
subgraph "Vectors $000$07C"
V0["$000: Reset SSP"]
V1["$004: Reset PC<br/>(→ExecBase ptr)"]
V2["$008: Bus Error"]
V3["$00C: Address Error"]
V4["$010: Illegal Instruction"]
V8["$020: Privilege Violation"]
V9["$024: Trace"]
end
subgraph "Vectors $060$07C"
AUTOVECT["$064$07C:<br/>Auto-vector<br/>Interrupts 17"]
end
subgraph "Vectors $080$0BC"
TRAPS["$080$0BC:<br/>TRAP #0#15"]
end
subgraph "Vectors $0C0$0FC"
FPU["$0C0$0FC:<br/>FPU Exceptions"]
end
```
### Complete Vector Map
| Vector | Address | Exception | AmigaOS Handler |
|---|---|---|---|
| 0 | `$000` | Reset: Initial SSP | (boot value) |
| 1 | `$004` | Reset: Initial PC | ROM entry point |
| 0 | `$000` | Reset: Initial SSP | Boot stack pointer |
| 1 | `$004` | Reset: Initial PC | ROM entry point → later ExecBase pointer |
| 2 | `$008` | Bus Error | Guru Meditation / Enforcer |
| 3 | `$00C` | Address Error | Guru Meditation |
| 4 | `$010` | Illegal Instruction | Guru Meditation |
@ -21,86 +48,273 @@ The M68k CPU provides a **256-entry exception vector table** starting at address
| 6 | `$018` | CHK Instruction | Alert |
| 7 | `$01C` | TRAPV | Alert |
| 8 | `$020` | Privilege Violation | Alert |
| 9 | `$024` | Trace | Debug (wack/BareFoot) |
| 10 | `$028` | Line-A Emulator | Unused (soft trap space) |
| 9 | `$024` | Trace | Debug (Wack / BareFoot) |
| 10 | `$028` | Line-A Emulator | Unused (available for soft traps) |
| 11 | `$02C` | Line-F Emulator | 68040/060.library FPU emulation |
| 1214 | `$030$038` | Reserved | — |
| 15 | `$03C` | Uninitialised Interrupt | Alert |
| 24 | `$060` | Spurious Interrupt | |
| 24 | `$060` | Spurious Interrupt | Ignored |
| 2531 | `$064$07C` | Auto-vector interrupts 17 | Exec interrupt dispatcher |
| 3247 | `$080$0BC` | TRAP #0#15 | User-installable traps |
| 4863 | `$0C0$0FC` | Reserved (FPU) | 68881/68882 exception handlers |
| 64255 | `$100$3FC` | User-defined vectors | User |
| 4854 | `$0C0$0D8` | FPU exceptions | 68881/68882 handlers |
| 55 | `$0DC` | FPU Unimplemented Data Type | 68040.library |
| 5658 | `$0E0$0E8` | MMU exceptions | 68030/040/060 |
| 5963 | `$0EC$0FC` | Reserved | — |
| 64255 | `$100$3FC` | User-defined vectors | Application-specific |
---
## TRAP Instructions — Software Interrupts
## Exception Stack Frames
`TRAP #n` (n = 015) generates a software exception. AmigaOS uses:
When an exception occurs, the CPU pushes an exception stack frame onto the Supervisor Stack. The frame format varies by CPU and exception type:
| TRAP | User |
|---|---|
| `TRAP #0` | exec.library `Supervisor()` — switch to supervisor mode |
| `TRAP #1#14` | Available for user programs |
| `TRAP #15` | Remote debugger breakpoint (BareFoot/wack) |
### 68000 Format
```
SP → ────────────────
│ Status Register │ (WORD)
├────────────────────┤
│ Program Counter │ (LONG)
├────────────────────┤
│ (Bus/Address Error only:) │
│ Access Address │ (LONG)
│ Instruction Register│ (WORD)
│ R/W + Function Code│ (WORD)
└────────────────────┘
```
### 68010+ Format
```
SP → ────────────────
│ Status Register │ (WORD)
├────────────────────┤
│ Program Counter │ (LONG)
├────────────────────┤
│ Frame Format | Vector Offset │ (WORD)
│ Format $0: short (4 words) │
│ Format $8: bus error (29 words on 68010) │
│ Format $7: bus error on 68040 (30 words) │
└────────────────────┘
```
The `Frame Format` field (bits 1512) identifies how many additional words are on the stack. This is critical for writing portable exception handlers:
| Format | CPU | Words | Exception Type |
|---|---|---|---|
| $0 | 010+ | 4 | Normal (short) — TRAP, interrupt |
| $1 | 010 | 4 | Throwaway (during instruction restart) |
| $2 | 020+ | 6 | Normal (long) — includes instruction address |
| $7 | 040 | 30 | Access fault |
| $8 | 010 | 29 | Bus error (68010) |
| $9 | 020/030 | 10 | Coprocessor mid-instruction |
| $A | 020/030 | 16 | Short bus error |
| $B | 020/030 | 46 | Long bus error |
---
## Installing an Exception Handler
## TRAP Instructions — Software Exceptions
`TRAP #n` (n = 015) generates a software exception via vectors 3247 ($080$0BC):
| TRAP | Vector | AmigaOS Use |
|---|---|---|
| `TRAP #0` | `$080` | `exec.library Supervisor()` — enter supervisor mode |
| `TRAP #1#14` | `$084$0B8` | Available for user programs |
| `TRAP #15` | `$0BC` | Remote debugger breakpoint (Wack / BareFoot) |
### Using TRAP for System Calls (Supervisor Mode)
```c
/* Using exec.library SetExcept/SetTrapHandler (not recommended): */
/* Direct vector patching in supervisor mode: */
/* exec.library Supervisor() — execute a function in supervisor mode */
ULONG result = Supervisor((APTR)mySuperFunc);
APTR OldVector;
__asm void MyTrapHandler(void)
/* mySuperFunc runs at supervisor level: */
ULONG __asm mySuperFunc(void)
{
/* Save registers, examine stack frame */
/* ... handle trap ... */
rte
/* Can access the Status Register, modify interrupt mask,
read/write control registers (VBR, CACR, etc.) */
ULONG vbr;
__asm volatile ("movec vbr,%0" : "=d"(vbr));
__asm volatile ("rte"); /* Return from exception — mandatory */
return vbr;
}
/* Install: */
Supervisor(function() {
OldVector = *(APTR *)0x0B0; /* TRAP #12 vector */
*(APTR *)0x0B0 = MyTrapHandler;
});
```
### Using TRAP for Debugger Breakpoints
Debuggers replace the instruction at the breakpoint address with `TRAP #0` (`$4E40`) or `TRAP #15` (`$4E4F`):
```asm
; Original code:
$00F80100: MOVE.L D0,(A0)
; With breakpoint:
$00F80100: TRAP #0 ; $4E40 — triggers vector $080
; The trap handler:
; 1. Saves all registers
; 2. Compares PC from exception frame against breakpoint list
; 3. Restores original instruction
; 4. Signals debugger task
; 5. Suspends target task
```
---
## Task-Level Exception Handling
AmigaOS provides per-task exception handlers via the `tc_ExceptCode` and `tc_ExceptData` fields:
```c
/* Install a task-level exception handler */
struct Task *me = FindTask(NULL);
me->tc_ExceptCode = MyExceptionHandler;
me->tc_ExceptData = myData;
/* Enable exception signals (bits that trigger the handler) */
SetExcept(SIGBREAKF_CTRL_C | mySig, SIGBREAKF_CTRL_C | mySig);
/* First arg = new mask, second = change mask */
/* Exception handler (called asynchronously when excepted signal arrives): */
ULONG __saveds MyExceptionHandler(
ULONG signals __asm("d0"),
APTR data __asm("a1"))
{
if (signals & SIGBREAKF_CTRL_C)
{
/* Handle Ctrl+C at exception level */
}
return signals; /* Return mask of signals to re-enable */
}
```
### Exception vs Signal
| Mechanism | Delivery | Context | Use Case |
|---|---|---|---|
| `Signal` + `Wait` | Polled — task checks when ready | Normal task context | Normal IPC |
| `tc_ExceptCode` | Asynchronous — interrupts the task immediately | Exception context (limited) | Urgent notifications, Ctrl+C handling |
---
## Guru Meditation
When a fatal exception occurs (Bus Error, Address Error), exec.library displays:
When a fatal exception occurs (Bus Error, Address Error, Illegal Instruction), exec displays:
```
Software Failure. Press left mouse button to continue.
Guru Meditation #XXYYYYYY.ZZZZZZZZ
```
| Field | Meaning |
|---|---|
| `XX` | Alert type: $00=recovery possible, $80=dead-end |
| `YYYYYY` | Error code (subsystem + specific error) |
| `ZZZZZZZZ` | Address where error occurred |
### Decoding the Guru Code
```
#XXYYYYYY.ZZZZZZZZ
│├──────┤ ├──────┤
│ │ └── Address where error occurred (PC or access address)
│ └──────────── Error code (subsystem + specific error)
└──────────────── Alert type: $00=recovery possible, $80=dead-end
```
### Common Guru Codes
| Code | Meaning |
| Code | Alert Type | Subsystem | Meaning |
|---|---|---|---|
| `$00000001` | Recoverable | exec | No memory |
| `$04000001` | Recoverable | exec | Generic recoverable alert |
| `$80000002` | Dead-end | exec | Bus error |
| `$80000003` | Dead-end | exec | Address error |
| `$80000004` | Dead-end | exec | Illegal instruction |
| `$80000005` | Dead-end | exec | Zero divide |
| `$80000008` | Dead-end | exec | Privilege violation |
| `$81000005` | Dead-end | exec | No memory (dead-end) |
| `$82000005` | Dead-end | graphics | No memory |
| `$83000005` | Dead-end | layers | No memory |
| `$84000005` | Dead-end | intuition | No memory |
| `$85000005` | Dead-end | math | No memory |
| `$87000007` | Dead-end | trackdisk | No disk inserted |
### Subsystem Codes (bits 1623)
| Code | Subsystem |
|---|---|
| `$80000003` | Address Error (dead-end) |
| `$80000004` | Illegal instruction (dead-end) |
| `$81000005` | exec: No memory |
| `$82000005` | graphics: No memory |
| `$87000007` | trackdisk: No disk |
| `$04000001` | exec: Recoverable alert |
| `$00000001` | No memory (recoverable) |
| `$01` | exec.library |
| `$02` | graphics.library |
| `$03` | layers.library |
| `$04` | intuition.library |
| `$05` | mathffp.library |
| `$07` | trackdisk.device |
| `$08` | timer.device |
| `$09` | cia.resource |
| `$0A` | disk.resource |
| `$0B` | misc.resource |
| `$10` | bootstrap |
| `$15` | audio.device |
| `$20` | dos.library |
| `$21` | ramlib |
| `$30` | workbench |
---
## Line-F Emulation (68040/060)
The 68040 and 68060 CPUs removed some FPU instructions from silicon for die-space reasons. When these instructions are encountered, the CPU generates a Line-F exception (vector 11, address `$02C`). The `68040.library` or `68060.library` installs a handler that **emulates** the missing instructions in software:
| Instruction | 68881/882 | 68040 | 68060 |
|---|---|---|---|
| `FSIN`, `FCOS`, `FTAN` | Hardware | **Emulated** | **Emulated** |
| `FASIN`, `FACOS`, `FATAN` | Hardware | **Emulated** | **Emulated** |
| `FLOG10`, `FLOG2`, `FLOGN` | Hardware | **Emulated** | **Emulated** |
| `FETOX`, `FTWOTOX`, `FTENTOX` | Hardware | **Emulated** | **Emulated** |
| `FMOVE` (packed decimal) | Hardware | **Emulated** | **Emulated** |
| `FADD`, `FSUB`, `FMUL`, `FDIV` | Hardware | Hardware | Hardware |
> **Performance**: Emulated transcendental functions on the 68040 are ~520× slower than hardware implementations on the 68882. Code that heavily uses these should consider lookup tables or polynomial approximations.
---
## Enforcer and MMU-Based Exception Monitoring
On 68020+ systems with an MMU, tools like **Enforcer** and **MuForce** configure the MMU to trap accesses to invalid addresses:
```
Enforcer Hit: READ-WORD FROM 0000000C PC: 00F80234
USP: 00321A04 ISP: 07FFE000
Data: 00000000 00F80100 00321A00 00000042 00000001 00000000 00000000 00000000
Addr: 00321A10 00F80000 00000000 00DFF000 00321C00 00320000 00321FFE 07FFE000
Stck: 00F80238 00000042
Name: "MyBuggyApp"
```
This catches:
- NULL pointer dereferences (reading from address 0)
- Access to unallocated memory
- Writes to ROM address space
- Access to the exception vector area ($000$3FF) from user mode
Enforcer doesn't prevent the crash — it just reports it with enough context to find the bug.
---
## Best Practices
1. **Never write directly to exception vectors** — use `SetFunction` on exec's trap vectors or `Supervisor()`
2. **Always use `RTE`** to return from exception handlers — `RTS` corrupts the supervisor stack
3. **Keep exception handlers minimal** — you're at supervisor level with limited stack
4. **Use Enforcer during development** — catches 90% of pointer bugs
5. **Install the correct 040/060 library** — without it, transcendental FPU instructions crash
6. **Use `Supervisor()`** instead of manual `TRAP #0` for portable supervisor access
7. **Save and restore all registers** in your exception handler — the interrupted code depends on them
---
## References
- Motorola: *MC68000 Family Reference Manual* — exception processing
- NDK39: `exec/alerts.h` — alert code definitions
- RKRM: Exception chapter
- Motorola: *MC68000 Family Programmer's Reference Manual* — exception processing
- Motorola: *MC68040 User's Manual* — exception stack frame formats
- NDK39: `exec/alerts.h`, `exec/tasks.h` (tc_ExceptCode), `exec/execbase.h`
- ADCD 2.1: `Supervisor`, `SetExcept`, `Alert`
- See also: [Interrupts](interrupts.md) — auto-vector interrupt handling
- See also: [Multitasking](multitasking.md) — how debuggers use TRAP for breakpoints
- *Amiga ROM Kernel Reference Manual: Exec* — exception handling chapter

View file

@ -4,101 +4,240 @@
## Overview
`ExecBase` is the root structure of AmigaOS, located at absolute address `$4`. It is a `struct Library` extended with all exec kernel state: memory lists, task queues, interrupt vectors, library lists, and hardware abstraction fields.
`ExecBase` is the root structure of AmigaOS, located at absolute address `$4`. It is a `struct Library` extended with all exec kernel state: memory lists, task queues, interrupt vectors, library lists, and hardware abstraction fields. Every system call goes through `ExecBase` — it is the single point of truth for the entire running system.
---
## Locating ExecBase
```c
/* C — standard method */
struct ExecBase *SysBase = *((struct ExecBase **)4);
/* Or use the auto-open variable (SAS/C, GCC): */
extern struct ExecBase *SysBase; /* Linker resolves from startup code */
```
In assembly:
```asm
MOVEA.L 4.W, A6 ; A6 = SysBase
; Assembly — canonical method
MOVEA.L 4.W,A6 ; A6 = SysBase (short absolute addressing)
; All exec LVO calls use JSR offset(A6)
```
> **Why address $4?** The 68000 stores the initial Program Counter at address $4 in the exception vector table. During cold boot, the CPU reads this address to find the ROM entry point. After boot, Exec overwrites it with a pointer to ExecBase. This is the one absolute address every Amiga program can rely on.
---
## Structure Layout
```mermaid
graph TB
EB["ExecBase at $4"] --> LIB["Library Header<br/>(struct Library)<br/>Offsets 033"]
EB --> SOFT["Software Config<br/>SoftVer, ColdCapture,<br/>WarmCapture, SysStkUpper"]
EB --> TASK["Task Scheduling<br/>TaskReady, TaskWait,<br/>ThisTask, IDNestCnt,<br/>TDNestCnt, Quantum"]
EB --> MEM["Memory Management<br/>MemList, MaxLocMem,<br/>MaxExtMem"]
EB --> LISTS["System Lists<br/>LibList, DeviceList,<br/>ResourceList, PortList,<br/>IntrList"]
EB --> HW["Hardware Detection<br/>AttnFlags, ChipRevBits0,<br/>PowerSupplyFrequency,<br/>VBlankFrequency"]
style EB fill:#e8f4fd,stroke:#2196f3,color:#333
style TASK fill:#e8f5e9,stroke:#4caf50,color:#333
style HW fill:#fff3e0,stroke:#ff9800,color:#333
```
---
## Key Field Groups
### Library Header (offset 0)
### Library Header (Offset 033)
Every library starts with this — ExecBase is exec.library's own base:
```c
struct Library LibNode; /* +0 — ln_Name = "exec.library" */
/* +20 — lib_Version (40 = OS3.1, 44 = OS3.2) */
struct Library LibNode; /* ln_Name = "exec.library" */
/* lib_Version = 40 (OS 3.1), 45 (OS 3.2), 47 (OS 3.2.2) */
/* lib_OpenCnt = open count */
```
### Interrupts (offset +84)
```c
UWORD AttnFlags; /* +0x128 — processor capability flags */
UWORD AttnResched; /* +0x12A — reschedule attention flag */
```
### Boot and Configuration
| Offset | Field | Type | Description |
|---|---|---|---|
| `$22` | `SoftVer` | `UWORD` | Kickstart software revision |
| `$24` | `LowMemChkSum` | `WORD` | Checksum of vectors $0$200 |
| `$26` | `ChkBase` | `ULONG` | ExecBase self-checksum |
| `$2A` | `ColdCapture` | `APTR` | Cold reboot intercept vector |
| `$2E` | `CoolCapture` | `APTR` | Warm reboot intercept vector (after Diag) |
| `$32` | `WarmCapture` | `APTR` | Keyboard reset intercept vector |
| `$36` | `SysStkUpper` | `APTR` | Top of supervisor stack |
| `$3A` | `SysStkLower` | `APTR` | Bottom of supervisor stack |
| `$3E` | `MaxLocMem` | `ULONG` | Top of Chip RAM (e.g., `$200000` for 2 MB) |
| `$42` | `DebugEntry` | `APTR` | Entry point for ROM debugger (Wack) |
| `$46` | `DebugData` | `APTR` | Data area for ROM debugger |
| `$4A` | `AlertData` | `APTR` | Last Alert data |
| `$4E` | `MaxExtMem` | `APTR` | Top of Extended RAM (or NULL) |
### Task Scheduling
| Offset | Field | Description |
|---|---|---|
| +0x128 | `TaskReady` | `struct List` — tasks ready to run |
| +0x132 | `TaskWait` | `struct List` — tasks waiting on signals |
| +0x126 | `IDNestCnt` | Interrupt disable nesting count |
| +0x127 | `TDNestCnt` | Task disable nesting count |
| Offset | Field | Type | Description |
|---|---|---|---|
| `$126` | `IDNestCnt` | `BYTE` | Interrupt disable nesting (1 = enabled) |
| `$127` | `TDNestCnt` | `BYTE` | Task disable nesting (1 = enabled) |
| `$128` | `ThisTask` | `APTR` | Pointer to currently running Task |
| `$12C` | `Quantum` | `UWORD` | Time slice for equal-priority round-robin |
| `$12E` | `Elapsed` | `UWORD` | Ticks elapsed in current quantum |
| `$130` | `SysFlags` | `UWORD` | Internal scheduler flags |
| `$132` | `TaskReady` | `List` | Tasks ready to run (sorted by priority) |
| `$146` | `TaskWait` | `List` | Tasks blocked on `Wait()` |
### Memory
| Offset | Field | Description |
|---|---|---|
| +0x130 | `MemList` | `struct List` of `MemHeader` regions |
| +0x134 | `ResourceList` | Resources list |
### Library and Device Lists
| Offset | Field | Description |
|---|---|---|
| +0x17A | `LibList` | `struct List` — loaded libraries |
| +0x182 | `DeviceList` | `struct List` — loaded devices |
| +0x18A | `IntrList` | Interrupt server list |
| +0x192 | `PortList` | Public message ports |
| +0x19A | `TaskList` | All tasks (not just ready/waiting) |
| Offset | Field | Type | Description |
|---|---|---|---|
| `$15A` | `MemList` | `List` | All memory regions (`MemHeader` chain) |
### Vectors and ROM
| Offset | Field | Description |
|---|---|---|
| +0x26 | `SoftVer` | Kickstart software revision |
| +0x10 | `ChkBase` | Checksum of library header |
| +0x222 | `PowerSupplyFrequency` | 50 or 60 Hz |
| +0x21E | `ChipRevBits0` | Chip revision detection flags |
### System Lists
### Chip Revision Flags (`ChipRevBits0`)
| Offset | Field | Type | Description |
|---|---|---|---|
| `$17A` | `LibList` | `List` | Loaded libraries (`NT_LIBRARY`) |
| `$18E` | `DeviceList` | `List` | Loaded devices (`NT_DEVICE`) |
| `$1A2` | `IntrList` | `List` | Interrupt server lists |
| `$1B6` | `ResourceList` | `List` | System resources |
| `$1CA` | `PortList` | `List` | Public message ports |
| `$1DE` | `SemaphoreList` | `List` | Public semaphores |
| Bit | Constant | Chip |
|---|---|---|
| 4 | `ATNF_68010` | 68010 or better |
| 5 | `ATNF_68020` | 68020 or better |
| 6 | `ATNF_68030` | 68030 |
| 7 | `ATNF_68040` | 68040 |
| 10 | `ATNF_FPU40` | 68040 internal FPU |
### Hardware Detection
| Offset | Field | Type | Description |
|---|---|---|---|
| `$128` | `AttnFlags` | `UWORD` | CPU and FPU capability flags |
| `$212` | `VBlankFrequency` | `UBYTE` | VBL rate: 50 (PAL) or 60 (NTSC) |
| `$213` | `PowerSupplyFrequency` | `UBYTE` | Mains: 50 or 60 Hz |
| `$21E` | `ChipRevBits0` | `UBYTE` | Chip revision detection |
---
## Detecting CPU and Chipset
## AttnFlags — CPU Detection
| Bit | Constant | Meaning |
|---|---|---|
| 0 | `AFF_68010` | 68010 or better detected |
| 1 | `AFF_68020` | 68020 or better |
| 2 | `AFF_68030` | 68030 or better |
| 3 | `AFF_68040` | 68040 or better |
| 4 | `AFF_68881` | 68881 FPU detected |
| 5 | `AFF_68882` | 68882 FPU detected |
| 6 | `AFF_FPU40` | 68040 internal FPU |
| 7 | `AFF_68060` | 68060 detected (OS 3.1+) |
| 10 | `AFF_PRIVATE` | Exec private — do not use |
### Usage
```c
/* CPU: */
if (SysBase->AttnFlags & AFF_68020) { /* 020+ */ }
if (SysBase->AttnFlags & AFF_68040) { /* 040 */ }
/* Check for 68020+ */
if (SysBase->AttnFlags & AFF_68020)
{
/* Can use CACHE instructions, 32-bit multiply, etc. */
}
/* Chipset (via graphics.library): */
struct GfxBase *gfx = (struct GfxBase *)OpenLibrary("graphics.library", 36);
if (gfx->ChipRevBits0 & GFXB_AA_ALICE) { /* AGA Alice chip */ }
/* Check for FPU */
if (SysBase->AttnFlags & (AFF_68881 | AFF_68882 | AFF_FPU40))
{
/* Floating-point hardware available */
}
```
---
## ExecBase in IDA Pro
## Detecting Chipset Revision
```c
/* via graphics.library */
struct GfxBase *gfx = (struct GfxBase *)OpenLibrary("graphics.library", 36);
if (gfx)
{
if (gfx->ChipRevBits0 & GFXB_AA_ALICE) /* AGA Alice */
if (gfx->ChipRevBits0 & GFXB_AA_LISA) /* AGA Lisa */
if (gfx->ChipRevBits0 & GFXB_HR_AGNUS) /* ECS Agnus */
if (gfx->ChipRevBits0 & GFXB_HR_DENISE) /* ECS Denise */
}
```
---
## Enumerating System Lists
### Walking the Library List
```c
Forbid();
struct Node *node;
for (node = SysBase->LibList.lh_Head;
node->ln_Succ != NULL;
node = node->ln_Succ)
{
struct Library *lib = (struct Library *)node;
Printf(" %s V%ld.%ld (open: %ld)\n",
lib->lib_Node.ln_Name,
lib->lib_Version,
lib->lib_Revision,
lib->lib_OpenCnt);
}
Permit();
```
### Walking the Task Lists
```c
Forbid();
/* Currently running */
Printf("Running: %s\n", SysBase->ThisTask->tc_Node.ln_Name);
/* Ready to run */
for (node = SysBase->TaskReady.lh_Head;
node->ln_Succ; node = node->ln_Succ)
{
Printf(" Ready: %s (pri %ld)\n",
node->ln_Name, node->ln_Pri);
}
/* Waiting */
for (node = SysBase->TaskWait.lh_Head;
node->ln_Succ; node = node->ln_Succ)
{
Printf(" Wait: %s (pri %ld)\n",
node->ln_Name, node->ln_Pri);
}
Permit();
```
---
## ExecBase Safety Rules
| Rule | Reason |
|---|---|
| **Never write to ExecBase fields** | Corrupts kernel state for all tasks |
| **Use `Forbid()` when walking lists** | Lists change as libraries open/close, tasks start/stop |
| **Don't cache pointers from lists** | Nodes may be removed between accesses |
| **Always use LVO functions** | Direct field manipulation bypasses safety checks |
| **Verify `SysBase` after warm reboot** | `ColdCapture`/`CoolCapture` may have altered it |
---
## ExecBase in IDA Pro / Ghidra
After loading Kickstart ROM:
1. Create a segment at `$4` containing a pointer
2. Follow the pointer to the ExecBase (in ROM)
3. Apply `struct ExecBase` type (from NDK39 headers parsed via `File → Parse C header`)
4. All `N(A6)` offsets auto-annotate as field names
1. **Create a segment** at `$000000$000400` (exception vectors)
2. **Mark `$4` as a pointer** — follow it to the ExecBase in ROM
3. **Apply `struct ExecBase` type** from NDK39 headers
- IDA: `File → Parse C header` with `exec/execbase.h`
- Ghidra: Import C headers via Data Type Manager
4. **All `N(A6)` offsets** in exec code auto-annotate as field names
5. **Label the system lists**`LibList`, `DeviceList`, `PortList` are entry points for understanding boot order
---
@ -106,5 +245,6 @@ After loading Kickstart ROM:
- NDK39: `exec/execbase.h` — authoritative field definitions
- ADCD 2.1: exec.library autodoc
- See also: [Library System](library_system.md) — how libraries relate to ExecBase
- See also: [Multitasking](multitasking.md) — TaskReady/TaskWait and scheduler internals
- *Amiga ROM Kernel Reference Manual: Exec* — ExecBase chapter
- http://amigadev.elowar.com/read/ADCD_2.1/Libraries_Manual_guide/node0072.html

View file

@ -4,123 +4,401 @@
## Overview
AmigaOS supports 7 hardware interrupt levels (68k IPL0IPL6) plus a software interrupt mechanism. Custom chip interrupts are filtered through the `INTENA` / `INTREQ` registers; CIA-generated interrupts arrive on level 2 (CIA-A) and level 6 (CIA-B).
AmigaOS supports 7 hardware interrupt levels (68k IPL0IPL6) plus a software interrupt mechanism. Custom chip interrupts are filtered through the `INTENA` / `INTREQ` registers; CIA-generated interrupts arrive on level 2 (CIA-A) and level 6 (CIA-B). The interrupt system is the foundation of all real-time behavior — audio DMA, vertical blank timing, keyboard input, and the scheduler itself all depend on it.
---
## Architecture
```mermaid
graph TB
subgraph "Hardware Sources"
SER["Serial Port"]
DSK["Disk Controller"]
KBD["Keyboard (CIA-A)"]
TMR["CIA Timers"]
COP["Copper"]
VBL["Vertical Blank"]
BLT["Blitter"]
AUD["Audio DMA"]
EXT["External (CIA-B)"]
end
subgraph "Custom Chips"
INTREQ["INTREQ<br/>($DFF09C)"]
INTENA["INTENA<br/>($DFF09A)"]
end
subgraph "68k CPU"
IPL["IPL0-IPL6<br/>(priority encoder)"]
VEC["Exception Vector<br/>($64$78)"]
ISR["Exec Interrupt<br/>Dispatcher"]
end
subgraph "Exec"
CHAIN["Interrupt Server<br/>Chain (per level)"]
SOFT["Software Interrupt<br/>Queue"]
end
SER --> INTREQ
DSK --> INTREQ
COP --> INTREQ
VBL --> INTREQ
BLT --> INTREQ
AUD --> INTREQ
KBD --> INTREQ
EXT --> INTREQ
INTREQ --> INTENA
INTENA --> IPL
IPL --> VEC
VEC --> ISR
ISR --> CHAIN
CHAIN --> SOFT
style INTREQ fill:#fff3e0,stroke:#ff9800,color:#333
style INTENA fill:#fff3e0,stroke:#ff9800,color:#333
style ISR fill:#e8f4fd,stroke:#2196f3,color:#333
```
---
## Interrupt Priority Levels
| IPL | Source | AmigaOS Use |
|---|---|---|
| 1 | TBE, DSKBLK, SOFTINT | Software interrupts (`SoftInt`) |
| 2 | PORTS (CIA-A) | Keyboard, timer, parallel, floppy motor |
| 3 | COPER, VERTB, BLIT | Copper, vertical blank, blitter |
| 4 | AUD0AUD3 | Audio DMA completion |
| 5 | RBF, DSKSYNC | Serial receive, disk sync |
| 6 | EXTER (CIA-B) | External interrupts, CIA-B timers, TOD |
| 7 | NMI | Non-maskable (unused on stock Amiga) |
| IPL | Source Bits | AmigaOS Use | Typical Latency |
|---|---|---|---|
| 1 | TBE, DSKBLK, SOFTINT | Software interrupts, serial TX, disk DMA complete | ~10 µs |
| 2 | PORTS (CIA-A) | Keyboard, CIA-A timers, parallel port, floppy index | ~15 µs |
| 3 | COPER, VERTB, BLIT | Copper, vertical blank, blitter done | ~10 µs |
| 4 | AUD0AUD3 | Audio channel DMA completion | ~10 µs |
| 5 | RBF, DSKSYNC | Serial receive, disk sync word | ~8 µs |
| 6 | EXTER (CIA-B) | CIA-B timers, external interrupts, TOD alarm | ~8 µs |
| 7 | NMI | Non-maskable (unused on stock Amiga hardware) | — |
Higher IPL = higher priority. A level 6 interrupt can preempt level 15 handlers. The CPU's SR (Status Register) mask bits determine which levels are currently enabled.
### Interrupt Sources per Level
```mermaid
graph LR
subgraph "Level 1"
L1A["TBE<br/>Serial TX"]
L1B["DSKBLK<br/>Disk DMA"]
L1C["SOFTINT<br/>Software"]
end
subgraph "Level 2"
L2["PORTS<br/>CIA-A"]
end
subgraph "Level 3"
L3A["COPER<br/>Copper"]
L3B["VERTB<br/>VBlank"]
L3C["BLIT<br/>Blitter"]
end
subgraph "Level 4"
L4A["AUD0"]
L4B["AUD1"]
L4C["AUD2"]
L4D["AUD3"]
end
subgraph "Level 5"
L5A["RBF<br/>Serial RX"]
L5B["DSKSYNC<br/>Disk sync"]
end
subgraph "Level 6"
L6["EXTER<br/>CIA-B"]
end
```
---
## Custom Chip Interrupt Registers
| Register | Address | Description |
|---|---|---|
| `INTENAR` | `$DFF01C` | Interrupt enable status (read) |
| `INTENA` | `$DFF09A` | Interrupt enable set/clear (write) |
| `INTREQR` | `$DFF01E` | Interrupt request status (read) |
| `INTREQ` | `$DFF09C` | Interrupt request clear/set (write) |
| Register | Address | R/W | Description |
|---|---|---|---|
| `INTENAR` | `$DFF01C` | R | Interrupt enable status (read) |
| `INTENA` | `$DFF09A` | W | Interrupt enable set/clear (write) |
| `INTREQR` | `$DFF01E` | R | Interrupt request status (read) |
| `INTREQ` | `$DFF09C` | W | Interrupt request clear/set (write) |
### INTENA / INTREQ Bit Map
| Bit | Constant | Source |
|---|---|---|
| 0 | `INTF_TBE` | Serial transmit buffer empty |
| 1 | `INTF_DSKBLK` | Disk DMA block complete |
| 2 | `INTF_SOFTINT` | Software interrupt |
| 3 | `INTF_PORTS` | CIA-A interrupt (level 2) |
| 4 | `INTF_COPER` | Copper interrupt |
| 5 | `INTF_VERTB` | Vertical blank |
| 6 | `INTF_BLIT` | Blitter interrupt |
| 7 | `INTF_AUD0` | Audio channel 0 |
| 8 | `INTF_AUD1` | Audio channel 1 |
| 9 | `INTF_AUD2` | Audio channel 2 |
| 10 | `INTF_AUD3` | Audio channel 3 |
| 11 | `INTF_RBF` | Serial receive buffer full |
| 12 | `INTF_DSKSYNC` | Disk sync word match |
| 13 | `INTF_EXTER` | CIA-B / external interrupt |
| 14 | `INTF_INTEN` | Master interrupt enable bit |
| Bit | Constant | Level | Source |
|---|---|---|---|
| 0 | `INTF_TBE` | 1 | Serial transmit buffer empty |
| 1 | `INTF_DSKBLK` | 1 | Disk DMA block complete |
| 2 | `INTF_SOFTINT` | 1 | Software interrupt |
| 3 | `INTF_PORTS` | 2 | CIA-A interrupt (keyboard, timers) |
| 4 | `INTF_COPER` | 3 | Copper interrupt |
| 5 | `INTF_VERTB` | 3 | Vertical blank |
| 6 | `INTF_BLIT` | 3 | Blitter finished |
| 7 | `INTF_AUD0` | 4 | Audio channel 0 DMA done |
| 8 | `INTF_AUD1` | 4 | Audio channel 1 DMA done |
| 9 | `INTF_AUD2` | 4 | Audio channel 2 DMA done |
| 10 | `INTF_AUD3` | 4 | Audio channel 3 DMA done |
| 11 | `INTF_RBF` | 5 | Serial receive buffer full |
| 12 | `INTF_DSKSYNC` | 5 | Disk sync word match |
| 13 | `INTF_EXTER` | 6 | CIA-B / external interrupt |
| 14 | `INTF_INTEN` | — | **Master interrupt enable** |
| 15 | `INTF_SETCLR` | — | Set/Clear control bit (write only) |
To enable vertical blank: write `$C005` to `INTENA` (bit 14 set = enable, bit 5 = VERTB).
To clear vertical blank request: write `$0020` to `INTREQ`.
### Enabling and Clearing Interrupts
```c
/* Enable vertical blank interrupt:
Bit 15 (SET) | Bit 14 (INTEN) | Bit 5 (VERTB) = $C020 */
custom.intena = INTF_SETCLR | INTF_INTEN | INTF_VERTB;
/* Disable vertical blank (clear bit 5): */
custom.intena = INTF_VERTB; /* Bit 15 clear = CLEAR mode */
/* Acknowledge (clear) a vertical blank request: */
custom.intreq = INTF_VERTB;
```
> **Important**: You must write `INTF_SETCLR` (bit 15) to SET bits. Without it, you CLEAR them. This is a common source of bugs.
---
## Adding an Interrupt Server
## Exec Interrupt Dispatch
### Server Chain vs Direct Handler
Exec provides two models for handling interrupts:
| Model | Function | Use Case |
|---|---|---|
| **Server chain** | `AddIntServer()` / `RemIntServer()` | Shared interrupts (multiple handlers per level) |
| **Direct handler** | `SetIntVector()` | Exclusive interrupt ownership |
For levels with multiple sources (VBL, PORTS), use the **server chain**. Each handler in the chain checks if the interrupt is for it and returns D0=0 (not mine) or D0≠0 (handled).
### Adding an Interrupt Server
```c
struct Interrupt myVBL = {
{ NULL, NULL, NT_INTERRUPT, 0, "My VBL" },
NULL, /* is_Data (passed to handler) */
myVBLHandler /* is_Code */
};
/* Vertical blank interrupt server */
struct Interrupt myVBL;
AddIntServer(INTB_VERTB, &myVBL); /* INTB_VERTB = 5 */
/* ... run ... */
RemIntServer(INTB_VERTB, &myVBL); /* always remove before exit */
myVBL.is_Node.ln_Type = NT_INTERRUPT;
myVBL.is_Node.ln_Pri = 0; /* Priority within this level */
myVBL.is_Node.ln_Name = "MyApp VBL";
myVBL.is_Data = myDataPtr; /* Passed in A1 */
myVBL.is_Code = myVBLFunc; /* Handler address */
AddIntServer(INTB_VERTB, &myVBL); /* INTB_VERTB = 5 */
/* ... application runs ... */
RemIntServer(INTB_VERTB, &myVBL); /* MUST remove before exit! */
```
### Interrupt Handler Rules
### Interrupt Handler Implementation
- Handler is called at interrupt level — **no OS calls that Wait()**
- D0D1, A0A1 may be trashed; all others preserved
- Return D0 = 0 if you did not handle it (pass to next server)
- Return D0 ≠ 0 if you handled it (stop server chain)
```c
/* C handler — called at interrupt level */
ULONG __saveds __interrupt MyVBLHandler(
struct Interrupt *irq __asm("a1")) /* is_Data in A1 */
{
struct MyData *data = (struct MyData *)irq;
/* Do fast work only */
data->frameCount++;
if (data->needUpdate)
{
data->needUpdate = FALSE;
Signal(data->mainTask, data->updateSig); /* Wake main task */
}
return 1; /* Handled — stop chain (for exclusive sources)
Return 0 to let next server in chain try */
}
```
### Assembly Handler (Maximum Performance)
Handler signature:
```asm
myVBLHandler:
; A1 = is_Data pointer (from struct Interrupt)
; do fast work only
MOVEQ #1, D0 ; handled — stop chain
RTS
; A1 = is_Data pointer
; Must preserve D2-D7, A2-A6
; May trash D0, D1, A0, A1
MyVBLHandler:
move.l (a1),a0 ; Load data pointer
addq.l #1,FRAMECOUNT(a0)
moveq #1,d0 ; Handled
rts
```
---
## Handler Rules
| Rule | Reason | Consequence |
|---|---|---|
| **No `Wait()` or `WaitPort()`** | Can't sleep at interrupt level | System freeze |
| **No `AllocMem()` / `FreeMem()`** | May internally `Wait()` | System freeze |
| **No DOS calls** | DOS is not reentrant | Corruption |
| **No Intuition calls** | May `Wait()` internally | Deadlock |
| **Preserve D2-D7, A2-A6** | Calling convention | Register corruption |
| **Minimize execution time** | Blocks all lower-priority interrupts | Audio glitches, serial data loss |
| **Use `Signal()` for deferred work** | Only safe IPC from interrupt context | — |
| **Always acknowledge the interrupt** | Write to INTREQ to clear the request | Infinite interrupt loop |
---
## CIA Interrupts
CIA-A (at `$BFEC01`) generates level 2 interrupts. CIA-B (at `$BFD000`) generates level 6. Each CIA has an ICR (Interrupt Control Register) with 5 sources:
CIA-A (`$BFE001`) generates level 2 (PORTS) interrupts. CIA-B (`$BFD000`) generates level 6 (EXTER) interrupts. Each CIA has an ICR (Interrupt Control Register) with 5 sources:
| Bit | Source |
|---|---|
| 0 | Timer A underflow |
| 1 | Timer B underflow |
| 2 | TOD alarm |
| 3 | Serial register full |
| 4 | Flag pin / FLG |
| Bit | Source | CIA-A Use | CIA-B Use |
|---|---|---|---|
| 0 | Timer A underflow | Keyboard scan timer | System timer |
| 1 | Timer B underflow | Available | Available |
| 2 | TOD alarm | Real-time clock alarm | VSync counter |
| 3 | Serial register (SP) | Keyboard data received | Available |
| 4 | FLAG pin | Accent key / disk index | Available |
CIA interrupts are serviced via AddIntServer on `INTB_PORTS` (level 2, CIA-A) or `INTB_EXTER` (level 6, CIA-B).
### CIA Interrupt Handler
```c
/* CIA-A keyboard interrupt (level 2) */
struct Interrupt kbdHandler;
kbdHandler.is_Code = KbdISR;
kbdHandler.is_Data = kbdData;
kbdHandler.is_Node.ln_Pri = 120; /* High priority within level 2 */
AddIntServer(INTB_PORTS, &kbdHandler); /* Level 2 = CIA-A */
ULONG __interrupt KbdISR(struct Interrupt *irq __asm("a1"))
{
UBYTE icr = ciaa.ciaicr; /* Read + acknowledge CIA-A interrupts */
if (icr & 0x08) /* Bit 3 = serial port (keyboard data) */
{
UBYTE rawKey = ciaa.ciasdr;
/* Process key... */
Signal(mainTask, keySig);
return 1; /* Handled */
}
return 0; /* Not ours — pass to next handler */
}
```
---
## Software Interrupts
Software interrupts run at level 1 priority but are scheduled by exec, not hardware. They're used for deferred interrupt processing — a hardware interrupt handler can queue a software interrupt to do longer processing at a lower priority:
```c
/* Cause a software interrupt */
struct Interrupt softInt;
softInt.is_Code = MySoftHandler;
softInt.is_Data = myData;
softInt.is_Node.ln_Pri = 0; /* -32, -16, 0, +16, +32 are typical */
Cause(&softInt); /* LVO -78 */
/* MySoftHandler runs at next opportunity (level 1) */
```
### Software Interrupt Priorities
| Priority | Constant | Use |
|---|---|---|
| +32 | `SIH_PRIMOUSE` | Mouse/gameport processing |
| +16 | — | High-priority deferred work |
| 0 | — | Normal deferred work |
| -16 | — | Low-priority deferred work |
| -32 | `SIH_PRISERIAL` | Serial port processing |
---
## Disable / Enable vs Forbid / Permit
| Function | Effect | Scope |
|---|---|---|
| `Forbid()` | Disables task switching | Task-level (interrupts still run) |
| `Permit()` | Re-enables task switching | Reverses `Forbid()` |
| `Disable()` | Masks all hardware interrupts | Hardware + task switching |
| `Enable()` | Unmasks hardware interrupts | Reverses `Disable()` |
| Function | Effect | Scope | Max Safe Duration |
|---|---|---|---|
| `Forbid()` | Disables task switching | Tasks only (interrupts still run) | ~100 ms |
| `Permit()` | Re-enables task switching | Reverses `Forbid()` | — |
| `Disable()` | Masks all hardware interrupts | Hardware + task switching | **~250 µs** |
| `Enable()` | Unmasks hardware interrupts | Reverses `Disable()` | — |
> [!CAUTION]
> `Disable()` / `Enable()` can be held for only a few microseconds — never do I/O or complex operations inside a `Disable()` section.
> **Caution**: `Disable()` / `Enable()` stop ALL hardware — serial data loss, audio DMA glitches, floppy read errors. Use only for accessing data structures shared between task and interrupt context.
---
## Pitfalls
### 1. Forgetting to Acknowledge
```c
/* BUG — interrupt fires infinitely */
ULONG MyHandler(void)
{
DoWork();
return 1;
/* Forgot: custom.intreq = INTF_VERTB; */
/* INTREQ bit still set → interrupt fires again immediately → system hangs */
}
```
> **Note**: For server-chain interrupts (AddIntServer), exec handles INTREQ acknowledgment. For `SetIntVector`, you must do it yourself.
### 2. RemIntServer After Handler Memory Freed
```c
/* BUG — handler struct on stack */
void SetupVBL(void)
{
struct Interrupt vbl; /* ON STACK */
AddIntServer(INTB_VERTB, &vbl);
/* Function returns — stack frame destroyed */
/* VBL interrupt fires → jumps to garbage → Guru */
}
```
### 3. Spending Too Long in Handler
```c
/* BUG — complex processing at interrupt level */
ULONG MyAudioHandler(void)
{
DecodeMP3Frame(buffer); /* Takes >250 µs on 68000 */
/* During this time, keyboard, serial, and disk are unserviced */
return 1;
}
/* CORRECT — signal task for heavy processing */
ULONG MyAudioHandler(void)
{
Signal(decoderTask, decodeSig); /* ~5 µs */
return 1;
}
```
---
## Best Practices
1. **Use server chains** (`AddIntServer`) for VBL, PORTS, EXTER — these are shared levels
2. **Always `RemIntServer`** before exit or freeing handler memory
3. **Keep handlers fast** — signal your main task for heavy work
4. **Use software interrupts** for deferred processing from high-priority hardware handlers
5. **Acknowledge interrupts** by writing to `INTREQ` when using `SetIntVector`
6. **Preserve registers** D2-D7, A2-A6 in your handler
7. **Use `Disable()`/`Enable()`** only to protect interrupt-shared data — never for general synchronization
8. **Set appropriate priority** in `is_Node.ln_Pri` — higher priority handlers check first
---
## References
- NDK39: `hardware/intbits.h`, `hardware/cia.h`
- ADCD 2.1: `AddIntServer`, `RemIntServer`, `SetIntVector`, `Disable`, `Forbid`
- [cia_chips.md](../01_hardware/common/cia_chips.md) — CIA timer and ICR details
- [custom_registers.md](../01_hardware/ocs_a500/custom_registers.md) — INTENA/INTREQ register listing
- NDK39: `hardware/intbits.h`, `hardware/cia.h`, `exec/interrupts.h`
- ADCD 2.1: `AddIntServer`, `RemIntServer`, `SetIntVector`, `Cause`, `Disable`, `Enable`
- See also: [CIA Chips](../01_hardware/common/cia_chips.md) — CIA timer and ICR details
- See also: [Custom Registers](../01_hardware/ocs_a500/custom_registers.md) — INTENA/INTREQ register listing
- See also: [Multitasking](multitasking.md) — how interrupts drive the scheduler
- *Amiga ROM Kernel Reference Manual: Exec* — interrupts chapter

View file

@ -4,7 +4,44 @@
## Overview
AmigaOS device I/O uses a **message-based** asynchronous protocol. Every device operation is described by an `IORequest` structure sent to a device's command port. The device processes it (synchronously or in the background) and replies when done.
AmigaOS device I/O uses a **message-based** asynchronous protocol. Every device operation is described by an `IORequest` structure sent to a device's command port. The device processes it (synchronously or in the background) and replies when done. This model unifies all hardware — disks, serial, parallel, audio, timers, network — under a single consistent API.
The IO system is built directly on top of [message ports](message_ports.md). An `IORequest` contains an embedded `Message`, and device I/O is literally message passing between your task and the device's task.
---
## Architecture
```mermaid
sequenceDiagram
participant App as Application
participant Port as Reply Port
participant Dev as Device Task
App->>Dev: OpenDevice("trackdisk.device", 0, ior, 0)
Note over Dev: Fills ior->io_Device, io_Unit
App->>Dev: SendIO(ior) [async]
Note over Dev: Device processes command
Dev->>Port: ReplyMsg (PutMsg to reply port)
App->>Port: WaitIO(ior) or Wait(portSig)
App->>App: Check ior->io_Error
App->>Dev: CloseDevice(ior)
```
### The IORequest Lifecycle
```
1. CreateIORequest / CreateStdIO → allocate request
2. OpenDevice → bind to device/unit
3. Fill io_Command, io_Data, io_Length, io_Offset
4. DoIO (sync) or SendIO (async) → submit
5. Check io_Error, io_Actual
6. Repeat 35 as needed
7. CloseDevice → unbind
8. DeleteIORequest / DeleteStdIO → free
```
---
@ -31,6 +68,21 @@ struct IOStdReq { /* extended version with data fields */
};
```
### IORequest Field Reference
| Field | Set By | Description |
|---|---|---|
| `io_Message.mn_ReplyPort` | App | Reply port — device sends reply here when done |
| `io_Device` | `OpenDevice` | Pointer to device base — do not modify |
| `io_Unit` | `OpenDevice` | Pointer to device unit — do not modify |
| `io_Command` | App | Operation to perform (`CMD_READ`, `CMD_WRITE`, etc.) |
| `io_Flags` | App | `IOF_QUICK` for synchronous fast path attempt |
| `io_Error` | Device | 0 = success, negative = error (set after completion) |
| `io_Actual` | Device | Bytes actually transferred |
| `io_Length` | App | Bytes to transfer |
| `io_Data` | App | Buffer pointer |
| `io_Offset` | App | Device-specific offset |
---
## Standard Command Codes
@ -49,7 +101,16 @@ struct IOStdReq { /* extended version with data fields */
#define CMD_NONSTD 9 /* first device-specific command number */
```
Device-specific commands start at `CMD_NONSTD` (9). Example: trackdisk uses `TD_FORMAT` (10), `TD_MOTOR` (11), `TD_SEEK` (12).
Device-specific commands start at `CMD_NONSTD` (9):
| Device | Command | Number | Description |
|---|---|---|---|
| trackdisk.device | `TD_FORMAT` | 10 | Low-level format a track |
| trackdisk.device | `TD_MOTOR` | 11 | Motor on/off control |
| trackdisk.device | `TD_CHANGESTATE` | 14 | Check disk insertion |
| serial.device | `SDCMD_SETPARAMS` | 10 | Set baud rate, stop bits |
| timer.device | `TR_ADDREQUEST` | 9 | Schedule a timer event |
| audio.device | `ADCMD_ALLOCATE` | 32 | Allocate audio channels |
---
@ -66,28 +127,45 @@ Device-specific commands start at `CMD_NONSTD` (9). Example: trackdisk uses `TD_
#define IOERR_SELFTEST -7 /* hardware self-test failed */
```
Device-specific error codes are positive values defined in each device's header.
---
## Opening a Device
## Opening and Closing Devices
### Opening
```c
struct IOStdReq *ior = CreateStdIO(reply_port); /* alloc + fill reply port */
if (OpenDevice("trackdisk.device", unit, (struct IORequest *)ior, 0) != 0) {
/* open failed — ior->io_Error set */
/* Create a reply port and IO request */
struct MsgPort *replyPort = CreateMsgPort();
struct IOStdReq *ior = (struct IOStdReq *)
CreateIORequest(replyPort, sizeof(struct IOStdReq));
/* Open the device */
BYTE err = OpenDevice("trackdisk.device", 0, /* unit 0 = DF0: */
(struct IORequest *)ior, 0); /* flags = 0 */
if (err != 0)
{
Printf("OpenDevice failed: %ld\n", err);
DeleteIORequest(ior);
DeleteMsgPort(replyPort);
return RETURN_FAIL;
}
```
Or manually:
### Closing
```c
struct IOStdReq *ior = AllocMem(sizeof(struct IOStdReq), MEMF_PUBLIC|MEMF_CLEAR);
ior->io_Message.mn_ReplyPort = my_reply_port;
ior->io_Message.mn_Length = sizeof(struct IOStdReq);
OpenDevice("audio.device", 0, (struct IORequest *)ior, 0);
/* Close and free — MUST abort pending requests first */
CloseDevice((struct IORequest *)ior);
DeleteIORequest((struct IORequest *)ior);
DeleteMsgPort(replyPort);
```
---
## Synchronous I/O: `DoIO`
## Synchronous I/O: DoIO
Blocks the calling task until the device completes the request:
@ -96,58 +174,226 @@ ior->io_Command = CMD_READ;
ior->io_Data = buffer;
ior->io_Length = 512;
ior->io_Offset = 0;
LONG err = DoIO((struct IORequest *)ior);
/* io_Actual = bytes actually read; io_Error = error code */
LONG err = DoIO((struct IORequest *)ior); /* LVO -456 */
if (err == 0)
{
/* Success — io_Actual contains bytes read */
Printf("Read %ld bytes\n", ior->io_Actual);
}
else
{
Printf("Error: %ld\n", ior->io_Error);
}
```
### How DoIO Works Internally
1. Sets `IOF_QUICK` in `io_Flags`
2. Calls the device's `BeginIO` vector
3. If the device completes synchronously (kept `IOF_QUICK` set), returns immediately
4. If the device cleared `IOF_QUICK` (will complete async), calls `WaitIO` to block
---
## Asynchronous I/O: `SendIO` + `WaitIO`
## Asynchronous I/O: SendIO + WaitIO
```c
/* Queue the request — returns immediately: */
SendIO((struct IORequest *)ior);
ior->io_Command = CMD_READ;
ior->io_Data = buffer;
ior->io_Length = 65536;
ior->io_Offset = 0;
SendIO((struct IORequest *)ior); /* LVO -462 */
/* Do other work while device operates... */
UpdateUI();
ProcessInput();
/* Block until this specific request completes: */
WaitIO((struct IORequest *)ior);
err = ior->io_Error;
WaitIO((struct IORequest *)ior); /* LVO -474 */
BYTE err = ior->io_Error;
```
### Poll without blocking: `CheckIO`
### Combining Async IO with Event Loop
```c
/* Returns non-NULL if request is done (removed from device queue): */
if (CheckIO((struct IORequest *)ior)) {
WaitIO((struct IORequest *)ior); /* must still call WaitIO to dequeue reply */
ULONG ioSig = 1L << replyPort->mp_SigBit;
ULONG idcmpSig = 1L << win->UserPort->mp_SigBit;
SendIO((struct IORequest *)ior);
BOOL ioDone = FALSE;
while (!ioDone)
{
ULONG sigs = Wait(ioSig | idcmpSig | SIGBREAKF_CTRL_C);
if (sigs & idcmpSig) HandleGUI();
if (sigs & ioSig)
{
if (CheckIO((struct IORequest *)ior))
{
WaitIO((struct IORequest *)ior); /* Must still call to clean up */
ioDone = TRUE;
}
}
if (sigs & SIGBREAKF_CTRL_C)
{
AbortIO((struct IORequest *)ior);
WaitIO((struct IORequest *)ior);
break;
}
}
```
---
## Aborting a Request: `AbortIO`
## CheckIO — Non-Blocking Status Check
```c
AbortIO((struct IORequest *)ior); /* ask device to cancel */
WaitIO((struct IORequest *)ior); /* wait for confirmation */
/* Returns non-NULL if request is complete: */
struct IORequest *result = CheckIO((struct IORequest *)ior); /* LVO -468 */
if (result)
{
WaitIO((struct IORequest *)ior); /* Must still call to dequeue reply */
/* io_Error is valid now */
}
else
{
/* Still in progress */
}
```
> **Warning**: Even after `CheckIO()` returns non-NULL, you **must** call `WaitIO()` to remove the reply message from the port. Failing to do so leaves a stale message in the queue.
---
## AbortIO — Cancel a Pending Request
```c
AbortIO((struct IORequest *)ior); /* LVO -480 — request cancellation */
WaitIO((struct IORequest *)ior); /* Wait for acknowledgment */
/* io_Error will be IOERR_ABORTED (-2) */
```
> **Important**: `AbortIO` is a **request** — the device may not abort immediately. Always follow with `WaitIO` to ensure the request is fully complete before reusing or freeing the IORequest.
---
## The IOF_QUICK Fast Path
When `IOF_QUICK` is set in `io_Flags`, the device MAY complete the request synchronously within `BeginIO()` — without queuing, signaling, or replying via message. This avoids the overhead of message passing for trivial operations.
```c
/* Fast-path attempt */
ior->io_Flags = IOF_QUICK;
BeginIO((struct IORequest *)ior);
if (ior->io_Flags & IOF_QUICK)
{
/* Completed synchronously — no WaitIO needed */
}
else
{
/* Device cleared IOF_QUICK — must wait */
WaitIO((struct IORequest *)ior);
}
```
`DoIO` uses this mechanism internally.
---
## Practical: Timer Device Usage
```c
/* Open timer.device */
struct MsgPort *timerPort = CreateMsgPort();
struct timerequest *tr = (struct timerequest *)
CreateIORequest(timerPort, sizeof(struct timerequest));
OpenDevice(TIMERNAME, UNIT_VBLANK, (struct IORequest *)tr, 0);
/* Set a 2-second timer */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 2;
tr->tr_time.tv_micro = 0;
SendIO((struct IORequest *)tr);
/* Wait for timer or user interrupt */
ULONG timerSig = 1L << timerPort->mp_SigBit;
ULONG sigs = Wait(timerSig | SIGBREAKF_CTRL_C);
if (sigs & timerSig)
{
WaitIO((struct IORequest *)tr);
Printf("Timer expired!\n");
}
else
{
AbortIO((struct IORequest *)tr);
WaitIO((struct IORequest *)tr);
}
/* Cleanup */
CloseDevice((struct IORequest *)tr);
DeleteIORequest((struct IORequest *)tr);
DeleteMsgPort(timerPort);
```
---
## Closing a Device
## Pitfalls
### 1. Reusing IORequest While In Flight
```c
CloseDevice((struct IORequest *)ior);
DeleteStdIO(ior); /* or FreeMem */
/* BUG — request is still being processed by device */
SendIO(ior);
ior->io_Command = CMD_WRITE; /* WRONG — device is reading from this struct */
SendIO(ior); /* Double-send = corruption */
```
### 2. Forgetting WaitIO After CheckIO
```c
/* BUG — reply message left in port */
if (CheckIO(ior)) { /* done! */ }
/* But WaitIO was never called — stale message in reply port */
/* Next Wait() on this port returns immediately with garbage */
```
### 3. Freeing IORequest Without CloseDevice
```c
/* BUG — device still references this unit */
DeleteIORequest(ior); /* Device's internal pointers now dangle */
```
---
## Best Practices
1. **Always pair** `OpenDevice` / `CloseDevice` and `CreateIORequest` / `DeleteIORequest`
2. **Always call `WaitIO`** after `CheckIO` returns non-NULL
3. **Always call `WaitIO`** after `AbortIO`
4. **Use `SendIO`** for async operations — `DoIO` blocks your task completely
5. **Combine IO with event loop** using `Wait()` on the reply port's signal
6. **Don't reuse IORequest** until the previous operation completes
7. **Check `io_Error`** after every operation — errors are common with removable media
8. **Use `UNIT_VBLANK`** for timer.device unless you need microsecond precision (use `UNIT_MICROHZ`)
---
## References
- NDK39: `exec/io.h`, `exec/errors.h`
- ADCD 2.1: `OpenDevice`, `CloseDevice`, `DoIO`, `SendIO`, `WaitIO`, `CheckIO`, `AbortIO`
- `10_devices/` — per-device command codes and structures
- NDK39: `exec/io.h`, `exec/errors.h`, `exec/devices.h`
- ADCD 2.1: `OpenDevice`, `CloseDevice`, `DoIO`, `SendIO`, `WaitIO`, `CheckIO`, `AbortIO`, `CreateIORequest`, `DeleteIORequest`
- See also: [Message Ports](message_ports.md) — IORequest is built on message passing
- See also: `10_devices/` — per-device command codes and structures
- *Amiga ROM Kernel Reference Manual: Exec* — I/O requests chapter

View file

@ -1,18 +1,55 @@
[← Home](../README.md) · [Exec Kernel](README.md)
# Library System — OpenLibrary Lifecycle
# Library System — OpenLibrary Lifecycle, Version Management
## Overview
The AmigaOS library system provides **versioned, shared code** via a standardised interface. Libraries are identified by name, opened with a version check, and reference-counted for safe unloading.
The AmigaOS library system provides **versioned, shared code** via a standardised interface. Libraries are identified by name, opened with a version check, and reference-counted for safe unloading. This system is the backbone of the Amiga's modular architecture — everything from `dos.library` to `intuition.library` to third-party libraries uses the same open/close/expunge lifecycle.
---
## Architecture
```mermaid
graph TB
subgraph "Application"
APP["OpenLibrary('dos.library', 40)"]
end
subgraph "Exec"
SCAN["Scan SysBase→LibList"]
DISK["Search LIBS: path"]
LOAD["LoadSeg + RomTag init"]
INIT["Call library Open vector"]
end
subgraph "Library"
LIB["struct Library<br/>JMP table + base data"]
OPEN["Open: lib_OpenCnt++"]
CLOSE["Close: lib_OpenCnt--"]
EXPUNGE["Expunge: if OpenCnt==0,<br/>unload and free"]
end
APP --> SCAN
SCAN -->|Found| INIT
SCAN -->|Not found| DISK
DISK -->|Found on disk| LOAD
LOAD --> INIT
INIT --> OPEN
OPEN --> LIB
style APP fill:#e8f4fd,stroke:#2196f3,color:#333
style LIB fill:#e8f5e9,stroke:#4caf50,color:#333
```
---
## Library Node
Every library is an `NT_LIBRARY` node on `SysBase->LibList`:
Every library is an `NT_LIBRARY` node on `SysBaseLibList`:
```c
/* exec/libraries.h — NDK39 */
struct Library {
struct Node lib_Node; /* ln_Name = "dos.library" */
UBYTE lib_Flags; /* LIBF_SUMUSED | LIBF_DELEXP */
@ -27,28 +64,76 @@ struct Library {
};
```
### Field Reference
| Field | Description |
|---|---|
| `lib_Node.ln_Name` | Library name used for `OpenLibrary()` lookup |
| `lib_Flags` | `LIBF_SUMUSED`, `LIBF_CHANGED`, `LIBF_DELEXP` |
| `lib_NegSize` | Total size of the JMP table (negative offsets from base) |
| `lib_PosSize` | Size of the library base structure (positive offsets) |
| `lib_Version` | Major version number — checked by `OpenLibrary()` |
| `lib_Revision` | Minor revision — informational, not checked at open time |
| `lib_IdString` | Human-readable ID string with date |
| `lib_Sum` | Checksum of the JMP table (for integrity verification) |
| `lib_OpenCnt` | Number of active openers — library can't expunge while > 0 |
### Library Memory Layout
```
JMP table (lib_NegSize bytes)
┌─────────────────────────────┐
base - N×6: │ JMP function_N │
│ ... │
base - 24: │ JMP Reserved │
base - 18: │ JMP Expunge │
base - 12: │ JMP Close │
base - 6: │ JMP Open │
├─────────────────────────────┤
base + 0: ───→ │ struct Library (header) │ ← OpenLibrary returns this
│ (library-specific data...) │
base + PosSize: │ (end of base struct) │
└─────────────────────────────┘
```
---
## OpenLibrary / CloseLibrary
### Opening
```c
/* Open — get a reference: */
struct DosLibrary *DOSBase =
(struct DosLibrary *)OpenLibrary("dos.library", 40);
/* Use the library ... */
/* Close — release reference: */
CloseLibrary((struct Library *)DOSBase);
if (!DOSBase)
{
/* Library not found, or version too old */
/* This is how you enforce minimum OS version requirements */
}
```
Internally:
1. `exec` scans `LibList` for `ln_Name == "dos.library"`
2. If not found, searches resident list and `LIBS:` path
3. If found on disk: `LoadSeg` + call `InitLib`
4. Check `lib_Version >= requested_version`
5. Call library's `Open()` vector → `lib_OpenCnt++`
6. Return library base
### What OpenLibrary Does
1. **Scan `SysBase→LibList`** for a node whose `ln_Name` matches
2. **If not found**: search the resident module list (`FindResident`)
3. **If not resident**: search `LIBS:` assign path, `LoadSeg` the file, find RomTag, initialise
4. **Check version**: `lib_Version >= requestedVersion`?
5. **Call library's `Open()` vector** — library-specific initialisation, `lib_OpenCnt++`
6. **Return** library base pointer (or NULL on failure)
### Closing
```c
CloseLibrary((struct Library *)DOSBase);
DOSBase = NULL; /* Good practice — prevent use-after-close */
```
### What CloseLibrary Does
1. **Call library's `Close()` vector**`lib_OpenCnt--`
2. **If `lib_OpenCnt == 0` and `LIBF_DELEXP` is set**: call `Expunge()` to unload
3. **If `Expunge()` returns a segment list**: `UnLoadSeg` to free the code
---
@ -56,22 +141,101 @@ Internally:
| Flag | Value | Meaning |
|---|---|---|
| `LIBF_SUMUSED` | 0x01 | Checksum is maintained |
| `LIBF_CHANGED` | 0x02 | Checksum needs recalculation |
| `LIBF_DELEXP` | 0x04 | Expunge deferred (opened while expunge pending) |
| `LIBF_SUMUSED` | `$01` | JMP table checksum is maintained — exec verifies on close |
| `LIBF_CHANGED` | `$02` | Checksum needs recalculation (after `SetFunction`) |
| `LIBF_DELEXP` | `$04` | Deferred expunge — will expunge when last opener closes |
---
## The Expunge Lifecycle
```mermaid
sequenceDiagram
participant App as Application
participant Exec as exec.library
participant Lib as Library
App->>Exec: CloseLibrary(base)
Exec->>Lib: Call Close() vector
Lib->>Lib: lib_OpenCnt--
alt lib_OpenCnt == 0
Exec->>Lib: Call Expunge() vector
alt No other openers
Lib->>Lib: Remove from LibList
Lib->>Lib: Free JMP table + base
Lib->>Exec: Return segment list
Exec->>Exec: UnLoadSeg(seglist)
else Memory pressure only
Lib->>Lib: Set LIBF_DELEXP
Lib->>Exec: Return NULL (defer)
end
end
```
### When Expunge Happens
- **Automatic**: When `CloseLibrary` drops `lib_OpenCnt` to 0 and memory is needed
- **System pressure**: Exec calls `Expunge()` on all libraries when `AllocMem` fails (trying to reclaim memory)
- **Manual**: `RemLibrary()` requests expunge regardless of open count
### Implementing Expunge in a Library
```c
BPTR __saveds LibExpunge(void)
{
struct MyLibBase *base = (struct MyLibBase *)REG_A6;
if (base->lib_OpenCnt > 0)
{
/* Can't unload — still in use */
base->lib_Flags |= LIBF_DELEXP;
return 0; /* Signal: deferred */
}
/* Remove from system list */
Remove(&base->lib_Node);
/* Free library-specific resources */
FreeMyResources(base);
/* Free the library base + JMP table */
BPTR segList = base->segList;
ULONG negSize = base->lib_NegSize;
ULONG posSize = base->lib_PosSize;
FreeMem((UBYTE *)base - negSize, negSize + posSize);
return segList; /* Exec calls UnLoadSeg on this */
}
```
---
## Version Numbering Convention
`lib_Version.lib_Revision`:
- `40.1` = OS 3.1 release
- `40.x` = OS 3.1 (various revisions)
- `44.x` = OS 3.2
| Version | OS Release | Example Libraries |
|---|---|---|
| 33.x | OS 1.2 | exec 33.180, dos 33.124 |
| 34.x | OS 1.3 | exec 34.2, dos 34.75 |
| 36.x | OS 2.0 | exec 36.174, dos 36.68 |
| 37.x | OS 2.04 | exec 37.175, dos 37.10 |
| 39.x | OS 3.0 | exec 39.46, dos 39.22 |
| 40.x | OS 3.1 | exec 40.70, dos 40.42 |
| 44.x | OS 3.1.4 | exec 44.5 |
| 45.x | OS 3.2 | exec 45.20 |
| 47.x | OS 3.2.2 | exec 47.3 |
Increment rules:
- `lib_Revision` — minor bugfix, compatible
- `lib_Version` — API change or major update (requestors check this)
### Version Check Pattern
```c
/* Require OS 3.0+ features */
struct Library *base = OpenLibrary("intuition.library", 39);
if (!base)
{
/* Display error using OS 1.x compatible methods */
/* Can't use features that require V39+ */
}
```
---
@ -80,18 +244,68 @@ Increment rules:
```c
/* Read-only peek — no open count increment */
Forbid();
struct Library *lib = FindName(&SysBase->LibList, "graphics.library");
struct Library *lib = (struct Library *)
FindName(&SysBase->LibList, "graphics.library");
if (lib)
{
Printf("Found: %s V%ld.%ld (open: %ld)\n",
lib->lib_Node.ln_Name,
lib->lib_Version,
lib->lib_Revision,
lib->lib_OpenCnt);
}
Permit();
if (lib) printf("Found v%d\n", lib->lib_Version);
```
> [!CAUTION]
> Using `FindName` without `Forbid()` is a race condition — the library could be expunged between finding it and using it.
> **Caution**: The returned pointer is only valid inside the `Forbid()` section. After `Permit()`, the library could be expunged. If you need to use it, call `OpenLibrary()` instead.
---
## Pitfalls
### 1. Using Library After Close
```c
DOSBase = OpenLibrary("dos.library", 40);
/* ... */
CloseLibrary(DOSBase);
Open("RAM:test", MODE_NEWFILE); /* CRASH — DOSBase is closed */
```
### 2. Not Checking OpenLibrary Return
```c
IntuitionBase = OpenLibrary("intuition.library", 99);
/* IntuitionBase is NULL — V99 doesn't exist */
OpenWindowTags(NULL, ...); /* Guru — calling through NULL base */
```
### 3. Version Mismatch
```c
/* Opened V36 but calling a V39 function */
OpenLibrary("exec.library", 36);
CreatePool(...); /* CreatePool was added in V39 — calling garbage */
```
---
## Best Practices
1. **Always check** the return value of `OpenLibrary()` — NULL means failure
2. **Request the minimum version** you actually need — don't over-specify
3. **Close in reverse order** of opening — prevents dangling references
4. **Set base pointer to NULL** after `CloseLibrary()` — catches use-after-close
5. **Use `OpenLibrary` for runtime version detection** — it's cleaner than checking `lib_Version` manually
6. **Don't use `FindName` as a substitute** for `OpenLibrary` — it doesn't bump the reference count
---
## References
- NDK39: `exec/libraries.h`
- ADCD 2.1: `OpenLibrary`, `CloseLibrary`, `FindName`
- [shared_libraries_runtime.md](../04_linking_and_libraries/shared_libraries_runtime.md) — expunge lifecycle
- NDK39: `exec/libraries.h`, `exec/resident.h`
- ADCD 2.1: `OpenLibrary`, `CloseLibrary`, `MakeLibrary`, `RemLibrary`, `FindName`
- See also: [Library Vectors](library_vectors.md) — JMP table and LVO details
- See also: [Resident Modules](resident_modules.md) — how libraries are found in ROM
- See also: [Shared Libraries Runtime](../04_linking_and_libraries/shared_libraries_runtime.md) — full expunge lifecycle
- *Amiga ROM Kernel Reference Manual: Exec* — libraries chapter

View file

@ -4,25 +4,46 @@
## Overview
Every AmigaOS library exposes its functions via a **JMP table** at negative offsets from the library base. This document covers the structure of the table, how LVOs are assigned, and how to create or patch one programmatically.
Every AmigaOS library exposes its functions via a **JMP table** at negative offsets from the library base. This document covers the structure of the table, how LVOs (Library Vector Offsets) are assigned, and how to create or patch one programmatically. Understanding the JMP table is essential for both library development and reverse engineering.
---
## JMP Table Structure
```
Address Content Description
lib_base - N×6: 4EF9 XXXXXXXX JMP <absolute address> ← function N
Address Content Description
─────────────────────────────────────────────────────────
lib_base - N×6: 4EF9 XXXXXXXX JMP <absolute> ← function N
...
lib_base - 24: 4EF9 XXXXXXXX JMP Reserved
lib_base - 18: 4EF9 XXXXXXXX JMP Expunge
lib_base - 12: 4EF9 XXXXXXXX JMP Close
lib_base - 6: 4EF9 XXXXXXXX JMP Open
lib_base + 0: struct Library ← pointer returned by OpenLibrary
lib_base - 30: 4EF9 XXXXXXXX JMP FirstUserFunc
lib_base - 24: 4EF9 XXXXXXXX JMP Reserved
lib_base - 18: 4EF9 XXXXXXXX JMP Expunge
lib_base - 12: 4EF9 XXXXXXXX JMP Close
lib_base - 6: 4EF9 XXXXXXXX JMP Open
─────────────────────────────────────────────────────────
lib_base + 0: struct Library ← OpenLibrary returns this
lib_base + PosSize: (end)
```
Each slot is exactly **6 bytes**: opcode `$4EF9` (JMP abs.l) + 4-byte target address.
### Calling Convention
```c
/* C — compiler generates: */
result = LibraryFunction(args);
/* Internally: */
/* MOVEA.L LibBase,A6 ; load library base into A6 */
/* JSR LVO(A6) ; jump to base+LVO → hits JMP instruction */
/* ; JMP redirects to actual function */
```
```asm
; Assembly — explicit:
MOVEA.L _DOSBase,A6
JSR -30(A6) ; LVO -30 = first user function
```
---
## LVO Formula
@ -31,89 +52,194 @@ Each slot is exactly **6 bytes**: opcode `$4EF9` (JMP abs.l) + 4-byte target add
LVO = 6 × slot_index
where slot_index counts from 1 (Open) upward:
Open = slot 1 → LVO = 6
Close = slot 2 → LVO = 12
Expunge = slot 3 → LVO = 18
Reserved = slot 4 → LVO = 24
First user fn = slot 5 → LVO = 30
Second user fn = slot 6 → LVO = 36
Open = slot 1 → LVO = 6
Close = slot 2 → LVO = 12
Expunge = slot 3 → LVO = 18
Reserved = slot 4 → LVO = 24
First user = slot 5 → LVO = 30
Second user = slot 6 → LVO = 36
...
```
The `.fd` file `##bias` value is the positive LVO: `bias 30` → LVO `30`.
### From .fd Files
```
##base _DOSBase
##bias 30
##public
Open(name,accessMode)(D1/D2)
Close(file)(D1)
Read(file,buffer,length)(D1/D2/D3)
```
`##bias 30` means the first user function has LVO `30`. Each subsequent function adds `6`.
| Function | .fd Position | LVO |
|---|---|---|
| `Open` | 1st after bias | 30 |
| `Close` | 2nd | 36 |
| `Read` | 3rd | 42 |
| `Write` | 4th | 48 |
---
## Standard Vectors (Slots 14)
Every library must implement these four vectors:
| Slot | LVO | Function | Purpose |
|---|---|---|---|
| 1 | 6 | `Open` | Called by `OpenLibrary()` — increment open count, init per-opener state |
| 2 | 12 | `Close` | Called by `CloseLibrary()` — decrement open count, cleanup |
| 3 | 18 | `Expunge` | Called to unload — free resources if `OpenCnt == 0` |
| 4 | 24 | `Reserved` | Must exist — returns NULL. Reserved for future use |
---
## MakeFunctions — Building a JMP Table
`exec.library MakeFunctions()` fills in the JMP table from a function pointer array:
```c
ULONG MakeFunctions(APTR target, APTR funcArray, APTR funcDispBase);
/* LVO -420 */
```
Writes JMP instructions into the negative offset area of `target`:
```c
ULONG MakeFunctions(APTR targetLib, APTR funcArray, APTR funcDispBase);
/* funcArray: NULL-terminated table of function pointers */
APTR myFuncs[] = {
LibOpen,
LibClose,
LibExpunge,
LibReserved,
MyFunc1,
MyFunc2,
(APTR)-1 /* terminator */
};
MakeFunctions(libBase, myFuncs, NULL);
/* Writes: JMP LibOpen at base-6, JMP LibClose at base-12, etc. */
```
Typical usage in library `InitLib`:
### Assembly Example
```asm
; funcArray: table of function pointers, terminated by -1
_LibFuncTable:
dc.l _LibOpen
dc.l _LibClose
dc.l _LibExpunge
dc.l _LibNull ; Reserved — returns NULL
dc.l _MyFunc1
dc.l _MyFunc2
dc.l -1 ; terminator
dc.l _LibOpen ; slot 1 → LVO -6
dc.l _LibClose ; slot 2 → LVO -12
dc.l _LibExpunge ; slot 3 → LVO -18
dc.l _LibNull ; slot 4 → LVO -24 (Reserved)
dc.l _MyFunc1 ; slot 5 → LVO -30
dc.l _MyFunc2 ; slot 6 → LVO -36
dc.l -1 ; terminator
LibInit:
LEA _LibFuncTable(PC), A0
MOVEA.L A6, A1 ; library base (passed in A6 by exec)
MOVEQ #0, D0 ; funcDispBase = 0 (absolute addresses)
MOVEA.L 4.W, A6
JSR (-420,A6) ; MakeFunctions(A1, A0, D0)
LEA _LibFuncTable(PC),A0
MOVEA.L A6,A1 ; library base
MOVEQ #0,D0 ; funcDispBase = 0 (absolute addresses)
MOVEA.L 4.W,A6
JSR -420(A6) ; MakeFunctions
```
`MakeFunctions` writes `JMP <ptr>` for each entry, filling the table downward from `lib_base 6`.
---
## SetFunction — Patching a Single Slot
```c
APTR SetFunction(struct Library *library, LONG funcOffset, APTR newFunction);
/* LVO -420 */
/* Returns: old function pointer */
```
- `funcOffset` is the negative LVO (e.g., `30` for the first user function)
- Returns the old function pointer
This is the primary mechanism for **system patching** — replacing a library function with your own:
```c
/* Hook dos.library Write() */
old_write = SetFunction((struct Library *)DOSBase, -48, my_write_hook);
typedef LONG (*WriteFunc)(BPTR file, APTR buf, LONG len);
WriteFunc oldWrite;
oldWrite = (WriteFunc)SetFunction(
(struct Library *)DOSBase,
-48, /* LVO for Write */
(APTR)MyWriteHook
);
/* Your hook: */
LONG __saveds MyWriteHook(
BPTR file __asm("d1"),
APTR buf __asm("d2"),
LONG len __asm("d3"))
{
/* Log the write, then call original */
LogWrite(file, len);
return oldWrite(file, buf, len);
}
/* Restore on cleanup: */
SetFunction((struct Library *)DOSBase, -48, (APTR)oldWrite);
```
See [setfunction_patching.md](../05_reversing/dynamic/setfunction_patching.md) for trampoline patterns.
### SetFunction Gotchas
| Issue | Details |
|---|---|
| **Not atomic** | Between SetFunction calls, another task may see inconsistent state |
| **Checksum invalidation** | SetFunction sets `LIBF_CHANGED` — must call `SumLibrary()` |
| **Multiple patchers** | If two programs patch the same LVO, unpatching in wrong order breaks the chain |
| **ROM functions** | SetFunction works even on ROM libraries — the JMP table is in RAM |
| **Caching on 040/060** | Must flush instruction cache after patching |
---
## Checksum Maintenance
After `MakeFunctions` or `SetFunction`, exec updates `lib_Sum` via `SumLibrary`:
```c
SumLibrary((struct Library *)myLib);
/* After modifying the JMP table: */
SumLibrary((struct Library *)myLib); /* LVO -426 */
```
If `LIBF_SUMUSED` is set, exec verifies the checksum at `CloseLibrary` time. Patching the JMP table without calling `SumLibrary` will trigger a checksum failure (alert box or guru).
If `LIBF_SUMUSED` is set, exec verifies the checksum periodically. Patching without updating the checksum triggers a "Library checksum failure" alert.
---
## Viewing Vectors in IDA Pro
## MakeLibrary — One-Shot Library Creation
1. Navigate to `lib_base 6` (first standard vector)
2. Each 6-byte group: opcode `4EF9` + 4-byte address
```c
struct Library *MakeLibrary(
APTR funcArray, /* function pointer table */
APTR structInit, /* InitStruct data table */
APTR initFunc, /* init function (receives lib base in D0) */
ULONG dataSize, /* size of library base struct */
BPTR segList /* segment list (for expunge) */
); /* LVO -84 */
```
This combines allocation, JMP table construction, data initialisation, and init-function calling into one operation. Used by `RTF_AUTOINIT` modules and direct library creation.
---
## Reverse Engineering: Viewing Vectors in IDA Pro
1. Navigate to `lib_base 6` (first standard vector = `Open`)
2. Each 6-byte group: opcode `$4EF9` + 4-byte absolute address
3. Press `C` to disassemble if not auto-detected
4. The 4-byte value is the actual function address — press `G` (Go to) to navigate
5. Name each function with the `.fd` file as reference
4. Follow the 4-byte address to the actual function body
5. Name each function using the `.fd` file as reference
6. The `.fd` bias tells you which LVO maps to which function name
### Reconstructing the Full Vector Table
```python
# Python script for reading JMP table from binary
import struct
def dump_vectors(rom_data, lib_base, num_vectors):
for i in range(1, num_vectors + 1):
offset = lib_base - (i * 6)
opcode, target = struct.unpack('>HI', rom_data[offset:offset+6])
if opcode == 0x4EF9: # JMP abs.l
print(f" LVO -{i*6:4d}: JMP ${target:08X}")
```
---
@ -121,5 +247,7 @@ If `LIBF_SUMUSED` is set, exec verifies the checksum at `CloseLibrary` time. Pat
- NDK39: `exec/execbase.h`, `exec/libraries.h`
- ADCD 2.1: `MakeFunctions`, `MakeLibrary`, `SetFunction`, `SumLibrary`
- [library_jmp_table.md](../05_reversing/static/library_jmp_table.md) — reconstruction workflow
- [lvo_table.md](../04_linking_and_libraries/lvo_table.md) — complete LVO reference tables
- See also: [Library System](library_system.md) — open/close/expunge lifecycle
- See also: [LVO Table](../04_linking_and_libraries/lvo_table.md) — complete LVO reference tables
- See also: [SetFunction Patching](../05_reversing/dynamic/setfunction_patching.md) — trampoline patterns
- *Amiga ROM Kernel Reference Manual: Exec* — library vectors chapter

View file

@ -4,7 +4,59 @@
## Overview
AmigaOS uses **intrusive doubly-linked lists** throughout exec: the task list, library list, device list, memory list, port list, and more all use the same `List`/`Node` structures defined in `exec/lists.h`.
AmigaOS uses **intrusive doubly-linked lists** throughout exec: the task list, library list, device list, memory list, port list, and more all use the same `List`/`Node` structures defined in `exec/lists.h`. Understanding this data structure is prerequisite to understanding anything else in the kernel — every system object is a node on some list.
The term "intrusive" means the link pointers are embedded directly inside the data structure, not in a separate container. This eliminates dynamic allocation overhead but means each object can only be on one list at a time.
---
## Architecture
```mermaid
graph LR
subgraph "struct List"
HEAD["lh_Head"]
TAIL["lh_Tail<br/>(always NULL)"]
TAILP["lh_TailPred"]
end
subgraph "Nodes"
A["Node A<br/>ln_Pri=10"]
B["Node B<br/>ln_Pri=5"]
C["Node C<br/>ln_Pri=0"]
end
HEAD --> A
A -->|ln_Succ| B
B -->|ln_Succ| C
C -->|ln_Succ| TAIL
C -.->|ln_Pred| B
B -.->|ln_Pred| A
TAILP --> C
style HEAD fill:#e8f4fd,stroke:#2196f3,color:#333
style TAIL fill:#ffcdd2,stroke:#e53935,color:#333
```
### The Sentinel Design
The AmigaOS list uses a clever **3-pointer sentinel** layout that eliminates special-casing for empty lists:
```
Full list:
lh_Head ──→ [Node A] ──→ [Node B] ──→ [Node C] ──→ NULL (lh_Tail)
lh_TailPred ─────────────────────────────┘
Empty list:
lh_Head ──→ NULL (lh_Tail) ← lh_Head points to lh_Tail
lh_TailPred ────┘ ← lh_TailPred points to lh_Head
```
The `lh_Tail` field is always NULL and acts as a "dummy tail node". Walking the list stops when `node->ln_Succ == NULL` — no need to compare against a separate end marker.
---
@ -24,7 +76,7 @@ struct Node {
struct MinNode {
struct MinNode *mln_Succ;
struct MinNode *mln_Pred;
/* no type, priority, or name — minimal overhead */
/* no type, priority, or name — saves 6 bytes per node */
};
```
@ -32,9 +84,9 @@ struct MinNode {
/* exec/lists.h — NDK39 */
struct List {
struct Node *lh_Head; /* first node (or tail sentinel if empty) */
struct Node *lh_Head; /* first node (or &lh_Tail if empty) */
struct Node *lh_Tail; /* always NULL — marks end of list */
struct Node *lh_TailPred; /* last node (or head sentinel if empty) */
struct Node *lh_TailPred; /* last node (or &lh_Head if empty) */
UBYTE lh_Type; /* list type */
UBYTE lh_pad;
};
@ -46,30 +98,41 @@ struct MinList {
};
```
### Node Type Constants
### Size Comparison
| Structure | Size | Use Case |
|---|---|---|
| `MinNode` | 8 bytes | Private lists, message queues, semaphore wait queues |
| `Node` | 14 bytes | Named/typed nodes — tasks, libraries, ports |
| `MinList` | 12 bytes | Lightweight list header |
| `List` | 14 bytes | Full list header with type |
---
## Node Type Constants
```c
/* exec/nodes.h */
#define NT_UNKNOWN 0
#define NT_TASK 1 /* exec Task */
#define NT_INTERRUPT 2 /* Interrupt server */
#define NT_DEVICE 3 /* Device */
#define NT_MSGPORT 4 /* MsgPort */
#define NT_MESSAGE 5 /* Message */
#define NT_FREEMSG 6
#define NT_REPLYMSG 7
#define NT_RESOURCE 8
#define NT_LIBRARY 9 /* Library */
#define NT_MEMORY 10 /* MemHeader */
#define NT_SOFTINT 11
#define NT_FONT 12
#define NT_PROCESS 13 /* dos.library Process */
#define NT_SEMAPHORE 14
#define NT_SIGNALSEM 15 /* SignalSemaphore */
#define NT_BOOTNODE 16
#define NT_KICKMEM 17
#define NT_GRAPHICS 18
#define NT_DEATHMESSAGE 19
/* exec/nodes.h — NDK39 */
#define NT_UNKNOWN 0 /* Unknown or uninitialized */
#define NT_TASK 1 /* exec Task */
#define NT_INTERRUPT 2 /* Interrupt server */
#define NT_DEVICE 3 /* Device driver */
#define NT_MSGPORT 4 /* Message port */
#define NT_MESSAGE 5 /* In-transit message */
#define NT_FREEMSG 6 /* Free message */
#define NT_REPLYMSG 7 /* Replied message */
#define NT_RESOURCE 8 /* System resource */
#define NT_LIBRARY 9 /* Shared library */
#define NT_MEMORY 10 /* Memory region (MemHeader) */
#define NT_SOFTINT 11 /* Software interrupt */
#define NT_FONT 12 /* Font */
#define NT_PROCESS 13 /* DOS Process (extends Task) */
#define NT_SEMAPHORE 14 /* Old-style semaphore */
#define NT_SIGNALSEM 15 /* SignalSemaphore */
#define NT_BOOTNODE 16 /* Boot node */
#define NT_KICKMEM 17 /* Kick memory */
#define NT_GRAPHICS 18 /* Graphics resource */
#define NT_DEATHMESSAGE 19 /* Task death notification */
```
---
@ -77,82 +140,196 @@ struct MinList {
## Initialising a List
```c
/* Stack-allocated list: */
struct List myList;
NewList(&myList); /* sets up sentinel pointers — mandatory */
NewList(&myList); /* MANDATORY — sets up sentinel pointers */
/* Or use NEWLIST() macro: */
NEWLIST(&myList);
/* Expands to: */
myList.lh_Head = (struct Node *)&myList.lh_Tail;
myList.lh_Tail = NULL;
myList.lh_TailPred = (struct Node *)&myList.lh_Head;
```
> **Critical**: An uninitialized list has garbage pointers. Calling `AddHead`/`AddTail` on an uninitialized list corrupts random memory.
---
## Adding and Removing Nodes
## Adding Nodes
```c
/* Add at head (highest LRU position): */
AddHead(&myList, &myNode); /* LVO -240 */
/* Add at head (newest first — stack semantics): */
AddHead(&myList, &myNode); /* LVO -240 */
/* Add at tail: */
AddTail(&myList, &myNode); /* LVO -246 */
/* Add at tail (oldest first — queue semantics): */
AddTail(&myList, &myNode); /* LVO -246 */
/* Remove from wherever it is (no list pointer needed): */
Remove(&myNode); /* LVO -252 */
/* Priority-ordered insert (highest ln_Pri first): */
Enqueue(&myList, &myNode); /* LVO -270 */
/* Scans from head, inserts before first node with lower priority
Equal priority: inserts AFTER existing nodes of same priority (FIFO) */
```
/* Priority-ordered insert (by ln_Pri, high first): */
Enqueue(&myList, &myNode); /* LVO -270 */
### How Enqueue Works
```
Before: [Pri 10] → [Pri 5] → [Pri 0] → NULL
Insert node with Pri 7:
After: [Pri 10] → [Pri 7] → [Pri 5] → [Pri 0] → NULL
```
This is how `SysBase->TaskReady` stays sorted — `Enqueue` ensures the highest-priority task is always at the head.
---
## Removing Nodes
```c
/* Remove a specific node (no list pointer needed): */
Remove(&myNode); /* LVO -252 */
/* Patches prev->ln_Succ and next->ln_Pred to skip this node */
/* Remove and return the first node: */
struct Node *first = RemHead(&myList); /* LVO -258 */
/* Returns NULL if list is empty */
/* Remove and return the last node: */
struct Node *last = RemTail(&myList); /* LVO -264 */
```
---
## Walking a List
### Standard Traversal
```c
struct Node *node;
for (node = myList.lh_Head;
node->ln_Succ != NULL; /* Stop at tail sentinel */
node = node->ln_Succ)
{
Printf("Node: %s (pri %ld)\n", node->ln_Name, node->ln_Pri);
}
```
### Safe Removal While Iterating
```c
/* Save next BEFORE removing, because Remove() corrupts ln_Succ */
struct Node *node, *next;
for (node = myList.lh_Head; node->ln_Succ != NULL; node = node->ln_Succ) {
/* process node */
for (node = myList.lh_Head;
(next = node->ln_Succ) != NULL;
node = next)
{
if (ShouldRemove(node))
{
Remove(node);
FreeNode(node);
}
}
```
Safe removal while iterating (save next before removing):
### Checking if a List is Empty
```c
for (node = myList.lh_Head; (next = node->ln_Succ) != NULL; node = next) {
if (should_remove(node)) Remove(node);
}
/* The canonical empty check: */
if (IsListEmpty(&myList)) { /* empty */ }
/* Expands to: */
if (myList.lh_TailPred == (struct Node *)&myList) { /* empty */ }
```
---
## Finding a Node by Name
## Finding Nodes
```c
struct Node *found = FindName(&SysBase->LibList, "dos.library");
/* Find by name (case-sensitive): */
struct Node *found = FindName(&myList, "dos.library"); /* LVO -276 */
/* Returns NULL if not found */
/* Always call under Forbid() if the list may change */
/* Scans from head — returns first match */
/* Find next occurrence (continue scanning): */
struct Node *next = FindName(found, "dos.library");
/* Starts searching from 'found' node forward */
```
> **Warning**: `FindName` on a system list (`LibList`, `PortList`) must be done under `Forbid()` — the list can change between `FindName` and your use of the returned node.
---
## Where Lists Are Used
| System List | Location | Node Type | Purpose |
|---|---|---|---|
| `SysBase->LibList` | ExecBase | `NT_LIBRARY` | All loaded libraries |
| `SysBase->DeviceList` | ExecBase | `NT_DEVICE` | All loaded devices |
| `SysBase->ResourceList` | ExecBase | `NT_RESOURCE` | System resources |
| `SysBase->PortList` | ExecBase | `NT_MSGPORT` | Public message ports |
| `SysBase->TaskReady` | ExecBase | `NT_TASK` | Tasks ready to run |
| `SysBase->TaskWait` | ExecBase | `NT_TASK` | Tasks blocked on Wait() |
| `SysBase->MemList` | ExecBase | `NT_MEMORY` | Memory regions |
| `SysBase->SemaphoreList` | ExecBase | `NT_SIGNALSEM` | Public semaphores |
| `SysBase->IntrList` | ExecBase | `NT_INTERRUPT` | Interrupt servers |
| `MsgPort->mp_MsgList` | Per-port | `NT_MESSAGE` | Pending messages |
---
## Pitfalls
### 1. Forgetting NewList
```c
/* BUG — uninitialized list */
struct List myList; /* Contains garbage */
AddTail(&myList, &node); /* Writes to garbage address → Guru */
```
### 2. Node on Two Lists
```c
/* BUG — intrusive list = one list per node */
AddTail(&listA, &node);
AddTail(&listB, &node); /* Corrupts listA — node's prev/next now point into listB */
```
### 3. Walking Without Forbid
```c
/* RACE — another task modifies the list */
struct Node *n;
for (n = SysBase->LibList.lh_Head; n->ln_Succ; n = n->ln_Succ)
{
/* Context switch here — another task calls CloseLibrary → node removed */
Printf("%s\n", n->ln_Name); /* n may be freed memory */
}
```
### 4. Using Freed Node's Links
```c
Remove(&node);
FreeMem(node, ...);
/* node->ln_Succ is now garbage — never dereference after Remove+Free */
```
---
## How the Sentinel Works
## Best Practices
The AmigaOS list design uses a **3-pointer layout** that avoids special-casing empty lists and end-of-list checks:
```
lh_Head ──→ [ Node A ]──→ [ Node B ]──→ [ tail sentinel ]
lh_Tail = NULL (always)
lh_TailPred ──────────────────────────→ [ Node B ]
Empty list:
lh_Head ──→ [ tail sentinel ]
lh_TailPred ──→ [ head sentinel ]
```
Walking stops when `ln_Succ == NULL` — that is the tail sentinel's `lh_Tail` field.
1. **Always call `NewList()`** before using a list — no exceptions
2. **Use `Forbid()`** when walking system lists
3. **Use safe iteration** (save `ln_Succ` before `Remove()`) when modifying during traversal
4. **Use `Enqueue()`** for priority-sorted insertion — don't manually scan
5. **Use `MinNode`/`MinList`** for private lists to save memory
6. **Set `ln_Type`** when adding to typed lists — debugging tools rely on it
7. **Never put a node on two lists** — use a wrapper struct with two embedded nodes if needed
8. **Check `IsListEmpty()`** before `RemHead()`/`RemTail()` if NULL return is unacceptable
---
## References
- NDK39: `exec/nodes.h`, `exec/lists.h`
- ADCD 2.1: `AddHead`, `AddTail`, `Remove`, `Enqueue`, `FindName`, `NewList`
- ADCD 2.1: `AddHead`, `AddTail`, `Remove`, `RemHead`, `RemTail`, `Enqueue`, `FindName`, `NewList`, `IsListEmpty`
- See also: [ExecBase](exec_base.md) — system lists stored in ExecBase
- *Amiga ROM Kernel Reference Manual: Exec* — lists chapter

View file

@ -4,7 +4,46 @@
## Overview
AmigaOS memory management is built directly into `exec.library`. There is no `malloc`/`free` in the OS itself — applications call `AllocMem` and `FreeMem` which operate on a linked list of `MemHeader` regions representing physical RAM.
AmigaOS memory management is built directly into `exec.library`. There is no `malloc`/`free` in the OS itself — applications call `AllocMem` and `FreeMem` which operate on a linked list of `MemHeader` regions representing physical RAM. The allocator is a simple **first-fit free-list** with no garbage collection, no automatic cleanup on task exit, and no memory protection. Understanding how it works is essential for writing stable Amiga software.
---
## Architecture
```mermaid
graph TB
SB["SysBase→MemList<br/>(struct List)"] --> MH1["MemHeader<br/>'chip memory'<br/>$000000$1FFFFF"]
SB --> MH2["MemHeader<br/>'fast memory'<br/>$200000$9FFFFF"]
MH1 --> MC1["MemChunk<br/>free block A"]
MC1 --> MC2["MemChunk<br/>free block B"]
MC2 --> MC3["MemChunk<br/>free block C"]
MH2 --> MC4["MemChunk<br/>free block D"]
MC4 --> MC5["MemChunk<br/>free block E"]
style SB fill:#e8f4fd,stroke:#2196f3,color:#333
style MH1 fill:#fff3e0,stroke:#ff9800,color:#333
style MH2 fill:#e8f5e9,stroke:#4caf50,color:#333
```
### How AllocMem Works
1. Walk `SysBase→MemList` — each `MemHeader` describes a physical RAM region
2. Check `mh_Attributes` against the requested `MEMF_*` flags
3. Walk the `MemChunk` free-list within the matching region
4. Find the first chunk large enough (first-fit)
5. Split the chunk: return the requested portion, keep the remainder on the free list
6. If `MEMF_CLEAR` is set, zero-fill the returned block
### How FreeMem Works
1. Find the `MemHeader` whose range contains the freed address
2. Walk the `MemChunk` free-list to find the correct insertion point (address-ordered)
3. Insert a new `MemChunk` at that position
4. Coalesce with adjacent free chunks if they're contiguous
> **Warning**: `FreeMem` trusts the caller completely. Wrong address or wrong size → free-list corruption → next `AllocMem` returns overlapping memory → system crash.
---
@ -28,6 +67,15 @@ struct MemChunk {
};
```
| Field | Description |
|---|---|
| `mh_Node.ln_Pri` | Region priority — higher priority regions are searched first. Fast RAM typically has higher priority than Chip RAM |
| `mh_Attributes` | `MEMF_*` flags describing this region's type |
| `mh_First` | Head of free-chunk linked list within this region |
| `mh_Lower` | Lowest byte address in this region |
| `mh_Upper` | First byte past the end of this region |
| `mh_Free` | Total bytes currently free (sum of all chunks) |
The OS maintains a doubly-linked list of `MemHeader` regions at `SysBase→MemList`. On a stock A1200:
- `"chip memory"` covering `$000000$1FFFFF` (2 MB Chip RAM)
- `"fast memory"` covering `$200000$9FFFFF` (up to 8 MB Fast RAM if fitted)
@ -42,15 +90,30 @@ The OS maintains a doubly-linked list of `MemHeader` regions at `SysBase→MemLi
#define MEMF_PUBLIC (1L<<0) /* accessible by all hardware/software */
#define MEMF_CHIP (1L<<1) /* must be in Chip RAM (DMA-reachable) */
#define MEMF_FAST (1L<<2) /* prefer Fast RAM (CPU-only, faster) */
#define MEMF_LOCAL (1L<<8) /* CPU-local (non-DMA) OS 3.1+ */
#define MEMF_24BITDMA (1L<<9) /* within 24-bit address range (for A2091) */
#define MEMF_KICK (1L<<10) /* Kickstart image memory */
#define MEMF_CLEAR (1L<<16) /* zero-fill the allocation */
#define MEMF_LARGEST (1L<<17) /* return single largest free block */
#define MEMF_REVERSE (1L<<18) /* allocate from top of list */
#define MEMF_TOTAL (1L<<19) /* AvailMem: report total, not largest free */
#define MEMF_NO_EXPUNGE (1L<<31) /* do NOT expunge libraries when low OS 3.0+ */
```
### When to Use Each Flag
| Flag | Use Case | Example |
|---|---|---|
| `MEMF_ANY` | General purpose — let the OS decide | Data buffers, structures |
| `MEMF_PUBLIC` | Shared between tasks | Message structures, port data |
| `MEMF_CHIP` | Custom chip DMA targets | Bitmaps, audio samples, Copper lists, sprite data |
| `MEMF_FAST` | CPU-only data, avoid DMA contention | Application data, code |
| `MEMF_CHIP \| MEMF_CLEAR` | Zero-filled DMA buffer | Screen bitmaps |
| `MEMF_PUBLIC \| MEMF_CLEAR` | Clean shared structure | Task structures |
**Chip RAM** is required for anything the custom chips DMA from — bitmaps, audio samples, Copper lists, blitter sources/destinations, sprite data. The custom chip DMA controllers cannot reach Fast RAM.
**Fast RAM** has no DMA contention with the custom chips, making it faster for pure CPU use.
**Fast RAM** has no DMA contention with the custom chips, making it faster for pure CPU use. On systems with both, Exec prefers Fast RAM for `MEMF_ANY` allocations (higher `mh_Node.ln_Pri`).
---
@ -60,9 +123,12 @@ The OS maintains a doubly-linked list of `MemHeader` regions at `SysBase→MemLi
/* exec/execbase.h — LVO -198 */
APTR AllocMem(ULONG byteSize, ULONG requirements);
/* Returns: pointer to allocated block, or NULL on failure */
/* Minimum allocation: 8 bytes (MemChunk header size) */
/* All allocations rounded up to 8-byte boundary */
/* LVO -210 */
void FreeMem(APTR memoryBlock, ULONG byteSize);
/* byteSize MUST match the original AllocMem size exactly */
```
### Usage
@ -70,14 +136,29 @@ void FreeMem(APTR memoryBlock, ULONG byteSize);
```c
/* Allocate 512 bytes of Chip RAM, zero-filled: */
UBYTE *buf = AllocMem(512, MEMF_CHIP | MEMF_CLEAR);
if (!buf) { /* handle out-of-memory */ }
if (!buf)
{
/* Handle out-of-memory — no exceptions, just NULL */
return RETURN_FAIL;
}
/* Free it: */
/* Use the buffer... */
/* Free it — size MUST match exactly: */
FreeMem(buf, 512);
```
> [!IMPORTANT]
> `FreeMem` requires the **exact same size** as `AllocMem`. The OS does not store the size internally — you must track it yourself.
> **Critical**: `FreeMem` requires the **exact same size** as `AllocMem`. The OS does not store the size internally — you must track it yourself. Passing the wrong size corrupts the free list.
### Alignment and Granularity
| Property | Value |
|---|---|
| Minimum allocation | 8 bytes |
| Alignment | 8-byte boundary (long-word aligned) |
| Size rounding | Up to next 8-byte multiple |
| Header overhead | 0 bytes (size is caller's responsibility) |
| Thread safety | Yes — Exec disables interrupts during alloc/free |
---
@ -89,7 +170,20 @@ APTR AllocVec(ULONG byteSize, ULONG requirements);
void FreeVec(APTR memoryBlock); /* LVO -690 */
```
`AllocVec` stores the size in the 4 bytes immediately before the returned pointer, allowing `FreeVec` to work without a size argument. Prefer this in new code.
`AllocVec` stores the size in the 4 bytes immediately before the returned pointer, allowing `FreeVec` to work without a size argument:
```
AllocVec internals:
AllocMem(byteSize + 4, requirements)
→ store byteSize at returned address
→ return (address + 4) to caller
FreeVec internals:
size = *(ULONG *)(memoryBlock - 4)
FreeMem(memoryBlock - 4, size + 4)
```
**Prefer `AllocVec`/`FreeVec` in new code** — eliminates the most common source of memory corruption (mismatched sizes).
---
@ -101,51 +195,181 @@ ULONG AvailMem(ULONG requirements);
```
```c
ULONG chip_free = AvailMem(MEMF_CHIP);
ULONG fast_free = AvailMem(MEMF_FAST);
ULONG total_chip = AvailMem(MEMF_CHIP | MEMF_TOTAL);
ULONG chip_free = AvailMem(MEMF_CHIP); /* Largest contiguous Chip block */
ULONG fast_free = AvailMem(MEMF_FAST); /* Largest contiguous Fast block */
ULONG total_chip = AvailMem(MEMF_CHIP | MEMF_TOTAL); /* Total free Chip RAM */
ULONG total_any = AvailMem(MEMF_TOTAL); /* Total free memory */
```
> **Warning**: `AvailMem()` is only a snapshot — memory can be allocated by other tasks between your check and your allocation. Never rely on it for pre-flight checks.
---
## Pool Allocator (OS 3.0+)
For many small allocations, use the pool API which reduces fragmentation:
For many small allocations, use the pool API which reduces fragmentation and improves performance:
```c
/* LVO -696 */
APTR pool = CreatePool(MEMF_ANY, 4096, 1024);
/* puddleSize=4096, threshSize=1024 */
/* puddleSize = 4096 — allocate puddles of this size from the main heap
threshSize = 1024 — allocations larger than this bypass the pool */
APTR p1 = AllocPooled(pool, 32); /* LVO -702 */
FreePooled(pool, p1, 32); /* LVO -708 */
APTR p2 = AllocPooled(pool, 64);
APTR p3 = AllocPooled(pool, 128);
DeletePool(pool); /* LVO -714 */
FreePooled(pool, p1, 32); /* LVO -708 */
/* p2, p3 still allocated */
DeletePool(pool); /* LVO -714 — frees ALL pool memory */
```
### Why Use Pools?
| Problem | Pool Solution |
|---|---|
| **Fragmentation** | Many small allocs fragment the main free list. Pools allocate large "puddles" from the main heap, sub-allocate from those |
| **Cleanup** | `DeletePool()` frees everything at once — no need to track individual allocations |
| **Performance** | Pool allocation is faster — no need to walk the entire system free list |
### Pool vs AllocMem Decision Guide
| Scenario | Use |
|---|---|
| Few large allocations (buffers, bitmaps) | `AllocMem` / `AllocVec` |
| Many small allocations (nodes, records, strings) | `CreatePool` / `AllocPooled` |
| Need to free individual items | `AllocVec` / `FreeVec` (pools can too, but no benefit) |
| Bulk cleanup on exit | `DeletePool` — frees everything |
| Need Chip RAM | `AllocMem(size, MEMF_CHIP)` (pools work too) |
---
## Memory Fragmentation
The Amiga's first-fit allocator is vulnerable to fragmentation. Over time, the free list develops many small holes that can't satisfy larger requests:
```
Initial: [████████████████████████] 512 KB free
After use: [██░░██░██░░░░██░░██░░░░] 256 KB free
Largest contiguous: 64 KB
Even though 256 KB is free, a 128 KB allocation fails!
```
### Mitigation Strategies
1. **Use pools** for small, frequent allocations
2. **Allocate large blocks early** before fragmentation develops
3. **Use `MEMF_REVERSE`** for long-lived allocations (allocate from top of memory)
4. **Free in reverse order** when possible
5. **Pre-allocate** and sub-manage your own buffers for performance-critical code
---
## Memory Map (A1200 Example)
| Range | Type | Used for |
|---|---|---|
| `$000000$000400` | Chip | 68k exception vectors |
| `$000000$000003` | Chip | ExecBase pointer (absolute address $4) |
| `$000004$000400` | Chip | 68k exception vectors |
| `$000400$000BFF` | Chip | exec library, SysBase |
| `$000C00$1FFFFF` | Chip | Application allocations, DMA buffers |
| `$200000$9FFFFF` | Fast | Fast RAM expansion (if present) |
| `$A00000$BFFFFF` | Slow/Ranger | A500 slow RAM (not on A1200) |
| `$000C00$07FFFF` | Chip | Application allocations, DMA buffers (512 KB) |
| `$080000$1FFFFF` | Chip | Additional Chip RAM (if 2 MB fitted) |
| `$200000$9FFFFF` | Fast | Fast RAM expansion (PCMCIA, trapdoor) |
| `$A00000$BEFFFF` | — | Unmapped (A1200) |
| `$BFD000$BFDFFF` | CIA | CIA-B registers |
| `$BFE001$BFEFFF` | CIA | CIA-A registers |
| `$C00000$D7FFFF` | Slow | A500 slow RAM expansion |
| `$D80000$DFFFFF` | Custom | Custom chip registers ($DFF000) |
| `$E00000$E7FFFF` | ROM mirror | (A500) |
| `$C00000$D7FFFF` | Slow | Ranger/Slow RAM (A500 only) |
| `$DC0000$DCFFFF` | RTC | Real-time clock (A1200) |
| `$DFF000$DFF1FF` | Custom | Custom chip registers |
| `$E00000$E7FFFF` | — | Reserved |
| `$E80000$EFFFFF` | Autoconfig | Zorro II autoconfig space |
| `$F00000$F7FFFF` | — | Reserved |
| `$F80000$FFFFFF` | ROM | Kickstart 3.1 (512 KB) |
---
## Pitfalls
### 1. Mismatched FreeMem Size
```c
/* BUG — corrupts the free list */
APTR buf = AllocMem(100, MEMF_ANY);
FreeMem(buf, 50); /* WRONG SIZE — free list now has a phantom 50-byte hole */
/* Next AllocMem may return memory that overlaps buf's remaining 50 bytes */
```
### 2. Double Free
```c
/* BUG — memory is already on the free list */
FreeMem(buf, 100);
FreeMem(buf, 100); /* Corrupts free list — duplicate MemChunk */
```
### 3. Use After Free
```c
/* BUG — another task may have already received this memory */
FreeMem(buf, 100);
buf[0] = 0x42; /* Writing to potentially allocated memory */
```
### 4. Not Checking for NULL
```c
/* BUG — MEMF_CHIP may fail if Chip RAM is exhausted */
UBYTE *bitmap = AllocMem(320 * 256 / 8, MEMF_CHIP);
memset(bitmap, 0, ...); /* Guru if bitmap is NULL */
```
### 5. Forgetting MEMF_CHIP for DMA
```c
/* BUG — audio.device DMA can't reach Fast RAM */
WORD *samples = AllocMem(44100, MEMF_ANY); /* May get Fast RAM */
/* Custom chip audio DMA reads garbage or causes bus error */
/* CORRECT */
WORD *samples = AllocMem(44100, MEMF_CHIP);
```
### 6. Memory Leak on Task Exit
```c
/* BUG — OS does NOT reclaim memory when task exits */
void MyTask(void)
{
APTR buf = AllocMem(4096, MEMF_ANY);
/* ... crash or return without FreeMem ... */
/* 4096 bytes leaked PERMANENTLY until reboot */
}
```
---
## Best Practices
1. **Use `AllocVec`/`FreeVec`** for new code — eliminates size-tracking bugs
2. **Use pools** for many small allocations — reduces fragmentation
3. **Always check for NULL** — memory exhaustion is common on 512 KB2 MB systems
4. **Use `MEMF_CHIP`** only when required — don't waste DMA-capable memory on CPU-only data
5. **Track all allocations** — use a resource list or goto-cleanup pattern
6. **Free in reverse order** — reduces fragmentation
7. **Use `MEMF_CLEAR`** for structures — prevents uninitialized field bugs
8. **Pre-allocate** during initialization — don't allocate in tight loops or interrupt handlers
9. **Never call `AllocMem` from interrupt context** — it may need to `Wait()`
10. **Use TypeSizeOf** — define `#define MYSIZE sizeof(struct MyStruct)` once and use everywhere
---
## References
- NDK39: `exec/memory.h`, `exec/execbase.h`
- ADCD 2.1: `AllocMem`, `FreeMem`, `AllocVec`, `FreeVec`, `CreatePool`
- ADCD 2.1: `AllocMem`, `FreeMem`, `AllocVec`, `FreeVec`, `CreatePool`, `AllocPooled`, `AvailMem`
- [address_space.md](../01_hardware/common/address_space.md) — full address map
- See also: [Multitasking](multitasking.md) — memory safety in multi-task environments
- *Amiga ROM Kernel Reference Manual: Exec* — memory management chapter

View file

@ -4,7 +4,42 @@
## Overview
AmigaOS inter-task communication uses a **message passing** system. Tasks send `Message` structures to `MsgPort` queues. The receiving task either polls (`GetMsg`) or blocks (`WaitPort`) for incoming messages. No shared memory is touched without the message handshake.
AmigaOS inter-task communication uses a **message passing** system. Tasks send `Message` structures to `MsgPort` queues. The receiving task either polls (`GetMsg`) or blocks (`WaitPort`) for incoming messages. This is the fundamental IPC mechanism — everything from [IDCMP](../09_intuition/idcmp.md) to device I/O to ARexx scripting is built on top of message ports.
Unlike pipes or sockets in Unix, Amiga messages are **zero-copy pointer exchanges**. The sender and receiver share the same physical memory — no data is copied. This makes message passing extremely fast but requires careful ownership discipline.
---
## Architecture
```mermaid
sequenceDiagram
participant S as Sender Task
participant P as MsgPort Queue
participant R as Receiver Task
S->>S: Prepare Message (mn_ReplyPort = replyPort)
S->>P: PutMsg(port, msg)
Note over P: Message added to mp_MsgList<br/>Signal(mp_SigTask, mp_SigBit)
P->>R: Signal wakes receiver
R->>P: GetMsg(port) → returns msg pointer
R->>R: Process message
R->>S: ReplyMsg(msg) → PutMsg to mn_ReplyPort
Note over S: Sender receives reply on replyPort
```
### Ownership Rules
This is the most critical concept in Amiga message passing:
| Phase | Message Owned By | Who Can Read/Write? |
|---|---|---|
| Before `PutMsg()` | Sender | Sender only |
| After `PutMsg()`, before `GetMsg()` | Port (in transit) | **Nobody** — message is in the queue |
| After `GetMsg()`, before `ReplyMsg()` | Receiver | Receiver only |
| After `ReplyMsg()` | Sender (via reply port) | Sender only |
> **Critical**: The sender must **not** modify or free the message between `PutMsg()` and receiving the reply. The message structure lives in the sender's memory, but it's logically owned by the receiver until replied.
---
@ -28,115 +63,274 @@ struct Message {
};
```
`mp_Flags` values:
### MsgPort Field Reference
| Value | Constant | Meaning |
| Field | Description |
|---|---|
| `mp_Node.ln_Name` | Port name — set for public (findable) ports, NULL for private |
| `mp_Flags` | Notification method when message arrives |
| `mp_SigBit` | Which signal bit to set when message arrives (for `PA_SIGNAL`) |
| `mp_SigTask` | Which task to signal (for `PA_SIGNAL`) or softint to trigger |
| `mp_MsgList` | Standard exec List — pending messages queue (FIFO) |
### mp_Flags Values
| Value | Constant | Behavior |
|---|---|---|
| 0 | `PA_SIGNAL` | Signal `mp_SigTask` when message arrives |
| 1 | `PA_SOFTINT` | Trigger software interrupt |
| 2 | `PA_IGNORE` | Do not wake the task (polling only) |
| 0 | `PA_SIGNAL` | Signal `mp_SigTask` with `1L << mp_SigBit` when message arrives |
| 1 | `PA_SOFTINT` | Trigger the software interrupt pointed to by `mp_SigTask` |
| 2 | `PA_IGNORE` | Do nothing — port must be polled with `GetMsg()` |
---
## Creating a Message Port
## Creating and Destroying Ports
### OS 2.0+ (Preferred)
```c
struct MsgPort *port = CreateMsgPort(); /* exec.library LVO -732 (OS 2.0+) */
/* or manually for OS 1.x compatibility: */
struct MsgPort *port = AllocMem(sizeof(struct MsgPort), MEMF_PUBLIC|MEMF_CLEAR);
struct MsgPort *port = CreateMsgPort(); /* LVO -732 */
/* Automatically:
- Allocates memory
- Allocates a signal bit
- Sets mp_Flags = PA_SIGNAL
- Sets mp_SigTask = FindTask(NULL)
- Initializes mp_MsgList */
if (!port) { /* All signal bits exhausted */ }
/* Cleanup: */
DeleteMsgPort(port); /* LVO -738 */
/* Frees signal bit and memory */
```
### Manual Creation (OS 1.x Compatible)
```c
struct MsgPort *port = AllocMem(sizeof(struct MsgPort), MEMF_PUBLIC | MEMF_CLEAR);
port->mp_Node.ln_Type = NT_MSGPORT;
port->mp_Flags = PA_SIGNAL;
port->mp_SigBit = AllocSignal(-1); /* any free signal bit */
port->mp_SigTask = FindTask(NULL); /* signal current task */
port->mp_SigBit = AllocSignal(-1);
port->mp_SigTask = FindTask(NULL);
NewList(&port->mp_MsgList);
/* Cleanup (reverse order): */
FreeSignal(port->mp_SigBit);
FreeMem(port, sizeof(struct MsgPort));
```
---
## Sending a Message
## Sending Messages
```c
/* PutMsg: add message to queue, signal receiver */
PutMsg(target_port, (struct Message *)my_msg);
/* Non-blocking — returns immediately */
PutMsg(targetPort, (struct Message *)myMsg);
/* Non-blocking — returns immediately
Can be called from interrupt context
Sender must NOT modify msg until reply received */
```
PutMsg can be called from interrupt context.
### What PutMsg Does Internally
1. Adds the message to the tail of `targetPort->mp_MsgList`
2. Sets `mn_Node.ln_Type = NT_MESSAGE`
3. If `mp_Flags == PA_SIGNAL`: calls `Signal(mp_SigTask, 1L << mp_SigBit)`
4. If `mp_Flags == PA_SOFTINT`: triggers the software interrupt
5. Returns — does not wait
---
## Receiving Messages
```c
/* Block until at least one message arrives: */
WaitPort(my_port);
### Blocking Wait
/* Then drain the queue: */
struct MyMsg *msg;
while ((msg = (struct MyMsg *)GetMsg(my_port)) != NULL) {
/* process msg */
ReplyMsg((struct Message *)msg); /* send reply if mn_ReplyPort != NULL */
```c
/* Block until at least one message is pending: */
WaitPort(myPort); /* LVO -384 */
/* Equivalent to: Wait(1L << myPort->mp_SigBit) + signal clear */
/* Drain the queue (may have multiple messages): */
struct Message *msg;
while ((msg = GetMsg(myPort)) != NULL)
{
/* Process message */
ReplyMsg(msg); /* Send reply — required for request-reply pattern */
}
```
### GetMsg (non-blocking poll)
### Non-Blocking Poll
```c
struct Message *msg = GetMsg(my_port);
/* Returns NULL if queue is empty */
struct Message *msg = GetMsg(myPort); /* LVO -372 */
/* Returns NULL if queue is empty — never blocks */
```
### Multi-Port Wait
```c
/* Wait on multiple ports simultaneously */
ULONG portSig1 = 1L << port1->mp_SigBit;
ULONG portSig2 = 1L << port2->mp_SigBit;
ULONG received = Wait(portSig1 | portSig2 | SIGBREAKF_CTRL_C);
if (received & portSig1)
{
struct Message *msg;
while ((msg = GetMsg(port1))) { /* ... */ ReplyMsg(msg); }
}
if (received & portSig2)
{
struct Message *msg;
while ((msg = GetMsg(port2))) { /* ... */ ReplyMsg(msg); }
}
```
---
## The Request-Reply Pattern
The standard bidirectional communication idiom:
```c
/* --- Sender --- */
struct MyMessage {
struct Message msg; /* MUST be first field */
ULONG command;
ULONG result;
APTR data;
};
struct MsgPort *replyPort = CreateMsgPort();
struct MyMessage request;
request.msg.mn_ReplyPort = replyPort;
request.msg.mn_Length = sizeof(struct MyMessage);
request.command = CMD_DO_WORK;
request.data = myData;
PutMsg(serverPort, &request.msg);
/* Wait for reply */
WaitPort(replyPort);
GetMsg(replyPort);
/* Now request.result contains the server's response */
DeleteMsgPort(replyPort);
/* --- Receiver (Server) --- */
WaitPort(serverPort);
struct MyMessage *req;
while ((req = (struct MyMessage *)GetMsg(serverPort)))
{
/* Process */
req->result = DoWork(req->command, req->data);
/* Reply — wakes sender */
ReplyMsg(&req->msg);
}
```
---
## Public Named Ports
Public ports are registered on `SysBase→PortList` and findable by name:
```c
/* Register a port so others can find it by name: */
/* Register: */
port->mp_Node.ln_Name = "myapp.port";
AddPort(port); /* LVO -354 — adds to SysBase→PortList */
/* Find from another task: */
Forbid();
AddPort(port);
struct MsgPort *remote = FindPort("myapp.port"); /* LVO -390 */
if (remote)
PutMsg(remote, myMsg);
Permit();
/* From another task: */
Forbid();
struct MsgPort *remote = FindPort("myapp.port");
Permit();
if (remote) PutMsg(remote, my_msg);
/* Cleanup: */
Forbid();
RemPort(port);
Permit();
/* Unregister: */
RemPort(port); /* LVO -360 */
```
`Forbid()` is required around `FindPort`/`AddPort`/`RemPort` to prevent the task list from changing mid-operation.
> **Critical**: `Forbid()` is required around `FindPort()` + `PutMsg()`. Without it, the server task could call `RemPort()` between your `FindPort()` and `PutMsg()`, leaving you with a dangling pointer.
---
## Reply Pattern
## Pitfalls
The standard request-reply idiom:
### 1. Not Replying to Messages
```c
/* Sender: */
my_msg->mn_ReplyPort = reply_port;
PutMsg(server_port, &my_msg->mn_Message);
WaitPort(reply_port);
struct MyMsg *reply = (struct MyMsg *)GetMsg(reply_port);
/* reply now contains the server's response */
/* Server: */
WaitPort(server_port);
struct MyMsg *req = (struct MyMsg *)GetMsg(server_port);
/* process req... */
req->result = 42;
ReplyMsg(&req->mn_Message); /* sends back to req->mn_ReplyPort */
/* BUG — sender is blocked waiting for reply forever */
msg = GetMsg(port);
/* ... process but forget to ReplyMsg ... */
/* Sender hangs on WaitPort(replyPort) indefinitely */
```
### 2. Modifying Message Before Reply
```c
/* BUG — message is logically owned by receiver */
PutMsg(server, &msg);
msg.data = newData; /* WRONG — receiver may be reading msg.data right now */
WaitPort(replyPort); /* Race condition */
```
### 3. Deleting Port with Pending Messages
```c
/* BUG — messages in queue still reference the port */
DeleteMsgPort(port); /* Leaked messages, dangling reply port pointers */
/* SAFE — drain first */
struct Message *msg;
while ((msg = GetMsg(port)))
ReplyMsg(msg);
DeleteMsgPort(port);
```
### 4. FindPort Without Forbid
```c
/* RACE — server may RemPort between find and send */
struct MsgPort *p = FindPort("SERVER"); /* Port exists... */
/* Context switch — server task exits, calls RemPort() */
PutMsg(p, msg); /* CRASH — p is now freed memory */
```
### 5. Stack-Allocated Messages with Async Reply
```c
/* BUG — message on stack, function returns before reply */
void SendRequest(struct MsgPort *server)
{
struct MyMessage msg;
msg.msg.mn_ReplyPort = replyPort;
PutMsg(server, &msg.msg);
/* Function returns — stack frame destroyed */
/* Reply arrives later → writes to invalid stack memory */
}
```
---
## Best Practices
1. **Always drain ports** before deleting — `while (GetMsg(port)) ReplyMsg(msg)`
2. **Always reply** to received messages — the sender may be waiting
3. **Use `Forbid()`** around `FindPort()` + `PutMsg()` for public ports
4. **Use heap-allocated messages** for async patterns — never stack-local
5. **Set `mn_Length`** correctly — some system code uses it for validation
6. **Set `mn_ReplyPort`** to NULL if no reply is expected — receiver checks this
7. **Use `PA_SIGNAL`** for normal ports — `PA_SOFTINT` only for device-level code
8. **Create separate reply ports** per conversation — don't share reply ports between multiple pending requests
---
## References
- NDK39: `exec/ports.h`, `exec/messages.h`
- ADCD 2.1: `CreateMsgPort`, `PutMsg`, `GetMsg`, `WaitPort`, `ReplyMsg`
- ADCD 2.1: `CreateMsgPort`, `DeleteMsgPort`, `PutMsg`, `GetMsg`, `WaitPort`, `ReplyMsg`, `AddPort`, `RemPort`, `FindPort`
- See also: [Signals](signals.md), [Multitasking](multitasking.md) — IPC strategies comparison
- *Amiga ROM Kernel Reference Manual: Exec* — messages and ports chapter

1156
06_exec_os/multitasking.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,44 @@
## Overview
AmigaOS ROM and disk-resident modules (libraries, devices, resources) identify themselves via a **RomTag** structure. At boot, exec scans the ROM and loaded segments for RomTags and initialises every module it finds.
AmigaOS ROM and disk-resident modules (libraries, devices, resources) identify themselves via a **RomTag** structure. At boot, exec scans the Kickstart ROM and any loaded segments for RomTags and initialises every module it finds. The RomTag system is how the kernel discovers and bootstraps itself — exec.library, graphics.library, dos.library, and all ROM-resident code use this mechanism.
---
## Architecture
```mermaid
graph TB
subgraph "Boot Sequence"
COLD["Cold Start<br/>CPU reads $4"]
SCAN["ROM Scanner<br/>Search for $4AFC magic"]
SORT["Sort by rt_Pri<br/>(higher = init first)"]
INIT["InitResident()<br/>for each module"]
end
subgraph "RomTag"
RT["struct Resident<br/>rt_MatchWord = $4AFC"]
AUTO["RTF_AUTOINIT?"]
end
subgraph "Initialization"
FUNC["Direct: rt_Init<br/>is a function pointer"]
TABLE["AutoInit: rt_Init<br/>is an InitTable pointer"]
MAKE["MakeLibrary()<br/>alloc + JMP table + init"]
end
COLD --> SCAN
SCAN --> SORT
SORT --> INIT
INIT --> RT
RT --> AUTO
AUTO -->|No| FUNC
AUTO -->|Yes| TABLE
TABLE --> MAKE
style SCAN fill:#e8f4fd,stroke:#2196f3,color:#333
style RT fill:#fff3e0,stroke:#ff9800,color:#333
```
---
@ -26,82 +63,239 @@ struct Resident {
};
```
### Magic Word
### Field Reference
`rt_MatchWord = $4AFC` is the 68k opcode for `ILLEGAL` — a deliberate trap instruction chosen so that an accidental execution of a RomTag causes an immediate CPU exception rather than silent corruption.
| Field | Description |
|---|---|
| `rt_MatchWord` | `$4AFC` — the 68k `ILLEGAL` opcode. If CPU accidentally executes a RomTag, it traps immediately instead of running garbage |
| `rt_MatchTag` | Self-referential pointer — must point back to this struct. Used to verify the match isn't a false positive |
| `rt_EndSkip` | Address past the end of this module's code/data. Scanner skips to here after finding the tag |
| `rt_Flags` | `RTF_AUTOINIT`, `RTF_SINGLETASK`, `RTF_COLDSTART`, `RTF_AFTERDOS` |
| `rt_Version` | Module version number — used by `FindResident` version checks |
| `rt_Type` | Node type: `NT_LIBRARY`, `NT_DEVICE`, `NT_RESOURCE` |
| `rt_Pri` | Initialization priority. Higher = initialized earlier in boot. exec.library has the highest |
| `rt_Name` | Module name — matches the `OpenLibrary`/`OpenDevice` name |
| `rt_IdString` | Human-readable string with version, date. Convention: `"name version.revision (dd.mm.yy)\r\n"` |
| `rt_Init` | If `RTF_AUTOINIT`: pointer to `InitTable`. Otherwise: pointer to init function |
### RTF_ Flags
---
## RTF_ Flags
```c
#define RTF_AUTOINIT (1<<7) /* use rt_Init as pointer to InitTable */
#define RTF_SINGLETASK (1<<1) /* init runs in single-task context */
#define RTF_COLDSTART (1<<0) /* init on cold boot only */
#define RTF_AUTOINIT (1<<7) /* rt_Init InitTable (auto library creation) */
#define RTF_AFTERDOS (1<<2) /* init after DOS is available (OS 2.0+) */
#define RTF_SINGLETASK (1<<1) /* init in single-task mode (before multitasking) */
#define RTF_COLDSTART (1<<0) /* init on cold boot only (not warm reset) */
```
| Flag | Timing | Use Case |
|---|---|---|
| `RTF_SINGLETASK` | Before multitasking starts | exec.library, expansion.library |
| `RTF_COLDSTART` | During cold boot, multitasking active | graphics.library, intuition.library |
| `RTF_AFTERDOS` | After dos.library is initialized | diskfont.library, workbench.library |
---
## RTF_AUTOINIT — Automatic Initialisation
When `RTF_AUTOINIT` is set, `rt_Init` points to an **InitTable** rather than a bare function:
When `RTF_AUTOINIT` is set, `rt_Init` points to an `InitTable`:
```c
struct InitTable {
ULONG it_DataSize; /* size of library instance struct */
APTR *it_FuncTable; /* pointer to function pointer table */
APTR it_DataTable; /* pointer to INITBYTE/INITWORD/INITLONG table */
APTR it_InitRoutine;/* pointer to actual LibInit() function */
ULONG it_DataSize; /* AllocMem size for library base struct */
APTR *it_FuncTable; /* function pointer array (for MakeFunctions) */
APTR it_DataTable; /* InitStruct data (INITBYTE/INITWORD/INITLONG) */
APTR it_InitRoutine; /* called after base is allocated and populated */
};
```
exec uses `MakeLibrary()` to allocate the library, install the JMP table, and initialise the data, then calls `it_InitRoutine`. For most libraries, the author only needs to provide `it_FuncTable` and `it_DataTable` and `RTF_AUTOINIT` handles the rest automatically.
### What AUTOINIT Does
1. `AllocMem(it_DataSize, MEMF_PUBLIC | MEMF_CLEAR)` — allocate library base
2. `MakeFunctions(base, it_FuncTable, NULL)` — build JMP table
3. `InitStruct(it_DataTable, base, 0)` — fill in default field values
4. `it_InitRoutine(base)` — library-specific initialization
5. `AddLibrary(base)` — add to `SysBase→LibList`
This automates the boilerplate that every library needs.
### Data Initialization Macros
```c
/* exec/initializers.h */
#define INITBYTE(offset, value) 0xE000|(offset), (value)
#define INITWORD(offset, value) 0xD000|(offset), (value)
#define INITLONG(offset, value) 0xC000|(offset), (value)
/* Example data table: */
UWORD dataTable[] = {
INITBYTE(OFFSET(Library, lib_Node.ln_Type), NT_LIBRARY),
INITLONG(OFFSET(Library, lib_Node.ln_Name), (ULONG)"mylib.library"),
INITBYTE(OFFSET(Library, lib_Flags), LIBF_SUMUSED | LIBF_CHANGED),
INITWORD(OFFSET(Library, lib_Version), 1),
INITWORD(OFFSET(Library, lib_Revision), 0),
0 /* terminator */
};
```
---
## ROM Scan at Boot
During exec initialisation:
1. Walk from Kickstart base (`$F80000` for 512K ROM, `$FC0000` for 256K) upward
2. Search for the `$4AFC` magic word at even addresses
3. For each match, verify:
- `rt_MatchTag` points back to itself (self-referential)
- `rt_EndSkip` is past the RomTag structure
4. Add valid entries to `SysBase→ResModules` (a NULL-terminated array of Resident pointers)
5. Sort by `rt_Pri` (highest first)
6. Call `InitResident()` for each, in priority order
### Boot Priority Order
| Priority | Module | Why First |
|---|---|---|
| 126 | exec.library | Kernel — must be first |
| 120 | expansion.library | Autoconfig hardware detection |
| 105 | graphics.library | Custom chip initialization |
| 100 | layers.library | Display layer management |
| 70 | intuition.library | GUI system |
| 50 | cia.resource | CIA chip management |
| 0 | dos.library | File system |
| 50 | ram-handler | RAM disk |
| 120 | workbench.library | Desktop environment |
---
## Finding a Resident by Name
```c
struct Resident *res = FindResident("dos.library"); /* LVO -60 */
if (res) {
printf("Found: %s v%d\n", res->rt_Name, res->rt_Version);
struct Resident *res = FindResident("dos.library"); /* LVO -96 */
if (res)
{
Printf("Found: %s V%ld (pri %ld)\n",
res->rt_Name, res->rt_Version, res->rt_Pri);
}
```
`FindResident` scans `SysBase->ResModules` — the list of all RomTag pointers collected at boot.
`FindResident` scans the `SysBase→ResModules` array — the list of all RomTag pointers collected at boot.
---
## ROM Scan at Boot
## Disk-Resident Modules
During exec initialisation, the ROM scanner walks from `$F80000` (Kickstart base) upward looking for the `$4AFC` magic word. For each match it verifies `rt_MatchTag == &rt` (self-referential pointer), confirms `rt_EndSkip` is beyond the RomTag, and adds valid entries to `ResModules`.
Libraries not in ROM are loaded from `LIBS:` when first opened:
The same scan is applied to any loaded segment when `AddResidentModule` is called.
1. `OpenLibrary("mylib.library", 0)` — not found in `LibList` or `ResModules`
2. Exec searches `LIBS:` assign path
3. `LoadSeg("LIBS:mylib.library")` — loads the HUNK executable
4. Scan loaded segments for RomTag (`$4AFC`)
5. `InitResident()` — creates the library
6. Add to `LibList` — subsequent opens find it in memory
---
## Writing a Minimal RomTag (Assembly)
## Writing a Minimal RomTag
### Assembly
```asm
; Minimal ROM tag for a library:
dc.w $4AFC ; rt_MatchWord
dc.l _RomTag ; rt_MatchTag (self-ref)
dc.l _EndTag ; rt_EndSkip
dc.b RTF_AUTOINIT ; rt_Flags
dc.b 1 ; rt_Version
dc.b NT_LIBRARY ; rt_Type
dc.b 0 ; rt_Pri
dc.l _Name ; rt_Name
dc.l _IdString ; rt_IdString
dc.l _InitTable ; rt_Init (InitTable when AUTOINIT)
_Name: dc.b "mylib.library", 0
_IdString: dc.b "mylib.library 1.0 (23.4.2026)", 13, 10, 0
; Minimal ROM tag for a library
CNOP 0,4 ; long-word align
_RomTag:
dc.w $4AFC ; rt_MatchWord (ILLEGAL opcode)
dc.l _RomTag ; rt_MatchTag (self-reference)
dc.l _EndTag ; rt_EndSkip
dc.b RTF_AUTOINIT ; rt_Flags
dc.b 1 ; rt_Version
dc.b NT_LIBRARY ; rt_Type
dc.b 0 ; rt_Pri
dc.l _Name ; rt_Name
dc.l _IdString ; rt_IdString
dc.l _InitTable ; rt_Init → InitTable
_Name:
dc.b "mylib.library",0
_IdString:
dc.b "mylib.library 1.0 (23.4.2026)",13,10,0
even
_InitTable:
dc.l MYLIB_SIZE ; it_DataSize
dc.l _FuncTable ; it_FuncTable
dc.l _DataTable ; it_DataTable
dc.l _InitRoutine ; it_InitRoutine
_FuncTable:
dc.l _LibOpen
dc.l _LibClose
dc.l _LibExpunge
dc.l _LibNull ; Reserved
dc.l _MyFunction1
dc.l _MyFunction2
dc.l -1 ; terminator
_EndTag:
```
### C (with SAS/C)
```c
struct Resident myRomTag = {
RTC_MATCHWORD,
&myRomTag, /* self-reference */
&endTag,
RTF_AUTOINIT,
1, /* version */
NT_LIBRARY,
0, /* priority */
"mylib.library",
"mylib.library 1.0 (23.4.2026)\r\n",
&initTable
};
```
---
## Pitfalls
### 1. Missing Self-Reference
```asm
; BUG — rt_MatchTag doesn't point back to the RomTag
_RomTag:
dc.w $4AFC
dc.l 0 ; WRONG — must be _RomTag
```
Scanner won't recognize this as a valid RomTag.
### 2. Incorrect EndSkip
```asm
; BUG — EndSkip points inside the module code
dc.l _RomTag+20 ; WRONG — scanner may skip data it shouldn't
```
`rt_EndSkip` must point past ALL code and data belonging to this module.
### 3. Wrong Priority
```c
/* BUG — initializing before a dependency is ready */
.rt_Pri = 100, /* Higher than graphics.library */
/* If your library opens graphics.library in init, it's not loaded yet */
```
---
## References
- NDK39: `exec/resident.h`, `exec/execbase.h`
- ADCD 2.1: `FindResident`, `InitResident`, `AddResidentModule`
- NDK39: `exec/resident.h`, `exec/initializers.h`, `exec/execbase.h`
- ADCD 2.1: `FindResident`, `InitResident`, `MakeLibrary`, `InitStruct`
- See also: [Library System](library_system.md) — how OpenLibrary uses RomTags
- See also: [Library Vectors](library_vectors.md) — JMP table construction
- *Amiga ROM Kernel Reference Manual: Exec* — resident modules chapter

View file

@ -4,7 +4,34 @@
## Overview
Semaphores are the AmigaOS mechanism for **mutual exclusion and shared-read access** to resources. Unlike `Forbid()` (which blocks all scheduling), semaphores allow other tasks to run while waiting — the waiting task simply sleeps until the resource is available.
Semaphores are the AmigaOS mechanism for **mutual exclusion and shared-read access** to resources. Unlike `Forbid()` (which blocks all scheduling), semaphores allow other tasks to run while waiting — the waiting task simply sleeps until the resource is available. They are the correct synchronization primitive for anything that takes more than a few microseconds.
---
## Architecture
```mermaid
stateDiagram-v2
[*] --> FREE : InitSemaphore()
FREE --> EXCLUSIVE : ObtainSemaphore() (by Task A)
FREE --> SHARED : ObtainSemaphoreShared() (by Task A)
SHARED --> SHARED : ObtainSemaphoreShared() (by Task B — multiple OK)
SHARED --> FREE : All shared holders release
EXCLUSIVE --> NESTED : ObtainSemaphore() (by same Task A — reentrant)
NESTED --> EXCLUSIVE : ReleaseSemaphore() (decrement nest count)
EXCLUSIVE --> FREE : ReleaseSemaphore() (nest count reaches 0)
EXCLUSIVE --> BLOCKED : ObtainSemaphore() by Task B → Task B sleeps
BLOCKED --> EXCLUSIVE : Task A releases → Task B wakes
```
### Shared vs Exclusive
| Mode | Multiple holders? | Use case |
|---|---|---|
| **Exclusive** (`ObtainSemaphore`) | No — only one task at a time | Writing/modifying shared data |
| **Shared** (`ObtainSemaphoreShared`) | Yes — multiple readers allowed | Read-only access to shared data |
When a task requests exclusive access while shared holders exist, it blocks until ALL shared holders release. When a task requests shared access while an exclusive holder exists, it blocks until the exclusive holder releases.
---
@ -16,27 +43,41 @@ struct SignalSemaphore {
struct Node ss_Link; /* ln_Type = NT_SIGNALSEM */
/* ln_Name = semaphore name (public) */
WORD ss_NestCount; /* how many times THIS task has obtained it */
struct MinList ss_WaitQueue;/* tasks waiting for exclusive access */
struct SemaphoreRequest ss_MultipleLink; /* shared-reader slot */
struct MinList ss_WaitQueue;/* tasks waiting for access */
struct SemaphoreRequest ss_MultipleLink; /* shared-reader management */
struct Task *ss_Owner; /* task holding exclusive lock (or NULL) */
WORD ss_QueueCount; /* number of waiters */
WORD ss_QueueCount; /* internal waiter tracking */
};
```
| Field | Description |
|---|---|
| `ss_Link.ln_Name` | Name string — set for public (findable) semaphores |
| `ss_NestCount` | How many times the current owner has obtained it (reentrant) |
| `ss_WaitQueue` | Queue of tasks waiting for access |
| `ss_Owner` | Task holding exclusive lock, or NULL if free/shared-only |
| `ss_QueueCount` | Internal — tracks waiting tasks and shared readers |
---
## Initialising a Semaphore
```c
/* Stack or AllocMem — always initialise before use: */
/* Stack or heap — always initialise before use: */
struct SignalSemaphore sem;
InitSemaphore(&sem); /* LVO -558 */
/* Public (named) semaphore — so other tasks can find it: */
/* Public (named) semaphore — findable by other tasks: */
sem.ss_Link.ln_Name = "myapp.lock";
AddSemaphore(&sem); /* LVO -564 */
sem.ss_Link.ln_Pri = 0;
AddSemaphore(&sem); /* LVO -564 — adds to SysBase→SemaphoreList */
/* Later: */
/* Find from another task: */
Forbid();
struct SignalSemaphore *found = FindSemaphore("myapp.lock"); /* LVO -576 */
Permit();
/* Cleanup: */
RemSemaphore(&sem); /* LVO -570 */
```
@ -49,6 +90,7 @@ RemSemaphore(&sem); /* LVO -570 */
ObtainSemaphore(&sem); /* LVO -534 */
/* --- critical section: only one task in here at a time --- */
ModifySharedData();
ReleaseSemaphore(&sem); /* LVO -546 */
```
@ -57,11 +99,27 @@ ReleaseSemaphore(&sem); /* LVO -546 */
```c
/* Returns TRUE if obtained, FALSE if someone else holds it: */
if (AttemptSemaphore(&sem)) { /* LVO -540 */
/* got it */
if (AttemptSemaphore(&sem)) /* LVO -540 */
{
/* Got exclusive access */
ModifySharedData();
ReleaseSemaphore(&sem);
}
else
{
/* Resource busy — do something else or retry later */
}
```
### Shared-to-Exclusive Upgrade (OS 3.0+)
```c
/* AttemptSemaphoreShared — try shared lock without blocking */
if (AttemptSemaphoreShared(&sem)) /* LVO -774 */
{
/* Got shared access */
ReadSharedData();
ReleaseSemaphore(&sem);
} else {
/* resource busy — do something else */
}
```
@ -69,45 +127,166 @@ if (AttemptSemaphore(&sem)) { /* LVO -540 */
## Shared (Read) Lock
Multiple tasks may hold a shared lock simultaneously. An exclusive lock blocks until all shared holders release.
Multiple tasks may hold a shared lock simultaneously. An exclusive lock request blocks until all shared holders release.
```c
ObtainSemaphoreShared(&sem); /* LVO -768 */
/* --- read-only access: multiple tasks may be here at once --- */
result = ReadSharedData();
ReleaseSemaphore(&sem); /* same release for both modes */
ReleaseSemaphore(&sem); /* Same release for both modes */
```
---
## Nesting
## Nesting (Reentrancy)
Semaphores are **reentrant** — the same task can call `ObtainSemaphore` multiple times. The `ss_NestCount` tracks how many times the current owner has obtained it. `ReleaseSemaphore` must be called the same number of times.
Semaphores are **reentrant** — the same task can call `ObtainSemaphore` multiple times without deadlocking itself:
```c
ObtainSemaphore(&sem); /* NestCount = 1 */
ObtainSemaphore(&sem); /* NestCount = 1, Owner = thisTask */
ObtainSemaphore(&sem); /* NestCount = 2 — safe, same task */
ObtainSemaphore(&sem); /* NestCount = 3 */
ReleaseSemaphore(&sem); /* NestCount = 2 */
ReleaseSemaphore(&sem); /* NestCount = 1 */
ReleaseSemaphore(&sem); /* NestCount = 0 — fully released, waiters wake */
```
This is essential for recursive functions or library code that may be called from contexts that already hold the lock.
---
## Semaphore vs Forbid/Disable
## Obtaining Multiple Semaphores
| Mechanism | Blocks | Other tasks run while waiting? | Interrupt safe? |
|---|---|---|---|
| `Forbid()` | All task switching | ❌ No | ✅ (interrupts still run) |
| `Disable()` | All task switching + interrupts | ❌ No | ✅ |
| `ObtainSemaphore()` | Only contending tasks | ✅ Yes | ❌ Not from interrupt context |
To avoid deadlocks when you need multiple semaphores, use `ObtainSemaphoreList`:
Use semaphores for anything that may take more than a few microseconds. Use `Forbid()` only for very short list manipulations.
```c
/* Build a list of semaphores to obtain atomically: */
struct SemaphoreRequest reqA, reqB;
struct List semList;
NewList(&semList);
reqA.sr_Semaphore = &semA;
reqB.sr_Semaphore = &semB;
AddTail(&semList, &reqA.sr_Link);
AddTail(&semList, &reqB.sr_Link);
ObtainSemaphoreList(&semList); /* LVO -582 */
/* Both semaphores held — no deadlock risk */
ReleaseSemaphore(&semA);
ReleaseSemaphore(&semB);
```
---
## Semaphore vs Forbid vs Disable
| Mechanism | Blocks | Other tasks run? | Interrupt safe? | Cost | Max duration |
|---|---|---|---|---|---|
| `Forbid()` | All task switching | ❌ No | ✅ Ints still run | Very low | ~100 ms |
| `Disable()` | All ints + tasks | ❌ No | ✅ (is the lock) | Lowest | **~250 µs** |
| `ObtainSemaphore()` | Only contending tasks | ✅ Yes | ❌ Not from IRQ | Medium | Unlimited |
| `ObtainSemaphoreShared()` | Only if exclusive held | ✅ Yes | ❌ Not from IRQ | Medium | Unlimited |
### Decision Guide
```mermaid
graph TD
Q1{"In interrupt<br/>context?"} -->|Yes| DISABLE["Use Disable()"]
Q1 -->|No| Q2{"Duration<br/>< 10 µs?"}
Q2 -->|Yes| FORBID["Use Forbid()"]
Q2 -->|No| Q3{"Multiple<br/>readers OK?"}
Q3 -->|Yes| SHARED["ObtainSemaphoreShared()"]
Q3 -->|No| EXCL["ObtainSemaphore()"]
style DISABLE fill:#ffcdd2,stroke:#e53935,color:#333
style FORBID fill:#fff3e0,stroke:#ff9800,color:#333
style SHARED fill:#e8f5e9,stroke:#4caf50,color:#333
style EXCL fill:#e8f4fd,stroke:#2196f3,color:#333
```
---
## Pitfalls
### 1. Deadlock (Lock Ordering)
```c
/* Task A */ /* Task B */
ObtainSemaphore(&semX); ObtainSemaphore(&semY);
ObtainSemaphore(&semY); /*!*/ ObtainSemaphore(&semX); /*!*/
/* Task A waits for Y Task B waits for X
→ DEADLOCK — both tasks sleep forever */
```
**Solution**: Always obtain semaphores in the same global order (alphabetical, by address, etc.).
### 2. Priority Inversion
```c
/* Low-pri task holds semaphore, medium-pri task runs,
high-pri task waits for semaphore → high-pri starves.
AmigaOS has NO priority inheritance. */
```
**Solution**: Keep critical sections short; don't hold semaphores across I/O.
### 3. ObtainSemaphore from Interrupt Context
```c
/* CRASH — ObtainSemaphore may Wait(), which is illegal from interrupts */
void __interrupt MyHandler(void)
{
ObtainSemaphore(&sem); /* DEADLOCK or crash */
}
```
**Solution**: Use `Disable()`/`Enable()` for interrupt-level synchronization, or use `AttemptSemaphore()` (non-blocking) and skip if busy.
### 4. Forgetting to Release
```c
/* BUG — semaphore held forever */
ObtainSemaphore(&sem);
if (error) return; /* Returns without releasing! */
ReleaseSemaphore(&sem);
/* CORRECT — use cleanup pattern */
ObtainSemaphore(&sem);
if (error) goto cleanup;
/* ... work ... */
cleanup:
ReleaseSemaphore(&sem);
```
### 5. Shared/Exclusive Mismatch
```c
/* Not a bug, but confusing — ReleaseSemaphore works for both modes */
ObtainSemaphoreShared(&sem);
ReleaseSemaphore(&sem); /* Correct — same function for both */
```
---
## Best Practices
1. **Use semaphores** instead of `Forbid()` for anything > ~10 µs
2. **Prefer shared locks** for read-only access — maximizes parallelism
3. **Keep critical sections short** — don't do I/O while holding a semaphore
4. **Use consistent lock ordering** to prevent deadlocks
5. **Use `AttemptSemaphore()`** for non-blocking try-lock patterns
6. **Always release** on every code path — use goto-cleanup pattern
7. **Never call from interrupts** — use `Disable()` or `AttemptSemaphore()` instead
8. **Use `ObtainSemaphoreList()`** when you need multiple semaphores atomically
---
## References
- NDK39: `exec/semaphores.h`
- ADCD 2.1: `InitSemaphore`, `ObtainSemaphore`, `ObtainSemaphoreShared`, `ReleaseSemaphore`, `AttemptSemaphore`
- ADCD 2.1: `InitSemaphore`, `ObtainSemaphore`, `ObtainSemaphoreShared`, `ReleaseSemaphore`, `AttemptSemaphore`, `ObtainSemaphoreList`
- See also: [Multitasking](multitasking.md) — priority inversion and synchronization strategies
- *Amiga ROM Kernel Reference Manual: Exec* — semaphores chapter

View file

@ -4,49 +4,108 @@
## Overview
Signals are the lightest AmigaOS synchronization primitive. Each task has 32 signal bits (`tc_SigAlloc`). A task blocks on `Wait(mask)` until any of the specified bits are set by another task or interrupt handler calling `Signal()`.
Signals are the lightest AmigaOS synchronization primitive — a single `Signal()` call compiles to a handful of 68k instructions. Each task has 32 signal bits (`tc_SigAlloc`). A task blocks on `Wait(mask)` until any of the specified bits are set by another task or interrupt handler calling `Signal()`. Signals carry no data — they are pure wake-up notifications. For data transfer, combine signals with [message ports](message_ports.md) or shared memory protected by [semaphores](semaphores.md).
---
## Signal Bit Constants
## Architecture
```c
/* exec/tasks.h — NDK39 */
/* Bits 015: application-allocated via AllocSignal() */
/* Bits 1631: reserved by exec */
```mermaid
graph LR
subgraph "Task A (Sender)"
SA["Signal(TaskB, mask)"]
end
#define SIGB_ABORT 0 /* bit 0: break signal */
#define SIGB_CHILD 1 /* bit 1: child task signal */
#define SIGB_BLIT 4 /* bit 4: blitter done (exec internal) */
#define SIGB_SINGLE 4 /* alias */
#define SIGB_INTUITION 5 /* bit 5: Intuition events (exec internal) */
#define SIGB_DOS 8 /* bit 8: DOS signal */
subgraph "Task B (Receiver)"
W["Wait(mask)"]
SR["tc_SigRecvd"]
end
/* Workbench/DOS break signals (bits 1215): */
#define SIGBREAKB_CTRL_C 12
#define SIGBREAKB_CTRL_D 13
#define SIGBREAKB_CTRL_E 14
#define SIGBREAKB_CTRL_F 15
SA -->|"Sets bits in<br/>tc_SigRecvd"| SR
SR -->|"Matches<br/>tc_SigWait?"| W
#define SIGBREAKF_CTRL_C (1L<<SIGBREAKB_CTRL_C) /* $1000 */
#define SIGBREAKF_CTRL_D (1L<<SIGBREAKB_CTRL_D) /* $2000 */
#define SIGBREAKF_CTRL_E (1L<<SIGBREAKB_CTRL_E) /* $4000 */
#define SIGBREAKF_CTRL_F (1L<<SIGBREAKB_CTRL_F) /* $8000 */
style SA fill:#e8f5e9,stroke:#4caf50,color:#333
style W fill:#e8f4fd,stroke:#2196f3,color:#333
```
### How Wait/Signal Works Internally
```
Signal(task, mask):
1. task->tc_SigRecvd |= mask (set the bits)
2. If (tc_SigRecvd & tc_SigWait): (does task want any of these?)
Move task from TaskWait → TaskReady
Trigger reschedule if task has higher priority
Wait(mask):
1. tc_SigWait = mask (record what we're waiting for)
2. If (tc_SigRecvd & mask): (already received?)
Clear matched bits, return immediately
3. Else:
Move task from Running → TaskWait
Schedule next task
(task sleeps until Signal sets matching bits)
4. Return: received = tc_SigRecvd & mask
tc_SigRecvd &= ~mask (clear returned bits)
```
---
## Signal Bit Layout
```
Bit 31 Bit 16 Bit 15 Bit 0
┌──────────────────────────┬──────────────────────────────────┐
│ System-reserved │ Application-allocatable │
│ (Exec internal) │ (AllocSignal) │
└──────────────────────────┴──────────────────────────────────┘
```
### Reserved Signal Bits
| Bit | Constant | Mask | Used By |
|---|---|---|---|
| 0 | `SIGB_ABORT` | `$00000001` | Break/abort |
| 1 | `SIGB_CHILD` | `$00000002` | Child task notification |
| 4 | `SIGB_SINGLE` / `SIGB_BLIT` | `$00000010` | Single-step, blitter completion |
| 5 | `SIGB_INTUITION` | `$00000020` | Intuition IDCMP delivery |
| 8 | `SIGB_DOS` | `$00000100` | DOS packet completion |
| 12 | `SIGBREAKB_CTRL_C` | `$00001000` | User break (Ctrl+C) |
| 13 | `SIGBREAKB_CTRL_D` | `$00002000` | User break (Ctrl+D) |
| 14 | `SIGBREAKB_CTRL_E` | `$00004000` | User break (Ctrl+E) |
| 15 | `SIGBREAKB_CTRL_F` | `$00008000` | User break (Ctrl+F) |
> **Note**: Bits 1215 (`SIGBREAKF_CTRL_C` through `SIGBREAKF_CTRL_F`) are pre-allocated but can be `Wait()`ed on by applications. The Shell sends `SIGBREAKF_CTRL_C` when the user presses Ctrl+C.
### Application Bits
Bits 015 are allocatable via `AllocSignal()`, but some are pre-reserved by the system. In practice, most tasks have **1012 free signal bits** available. If you need more concurrent signal sources than that, use message ports instead.
---
## Allocating and Freeing Signals
```c
/* Allocate an unused signal bit (-1 = any free bit): */
LONG sigBit = AllocSignal(-1); /* LVO -246 */
if (sigBit < 0) { /* all 16 user bits in use */ }
BYTE sigBit = AllocSignal(-1); /* LVO -330 */
if (sigBit < 0)
{
/* All application bits in use — serious problem */
/* Consider using message ports or sharing signals */
}
ULONG sigMask = (1L << sigBit);
ULONG sigMask = 1L << sigBit;
/* Free when done: */
FreeSignal(sigBit); /* LVO -252 */
FreeSignal(sigBit); /* LVO -336 */
```
### Requesting a Specific Bit
```c
/* Allocate a specific bit (if available): */
BYTE bit = AllocSignal(7); /* Request bit 7 specifically */
if (bit < 0) { /* Bit 7 is already in use */ }
```
---
@ -57,76 +116,233 @@ FreeSignal(sigBit); /* LVO -252 */
/* Block until any of the listed signals arrive: */
ULONG received = Wait(sigMask | SIGBREAKF_CTRL_C); /* LVO -318 */
if (received & SIGBREAKF_CTRL_C) {
/* user pressed CTRL-C */
if (received & SIGBREAKF_CTRL_C)
{
/* User pressed Ctrl+C */
cleanup_and_exit();
}
if (received & sigMask) {
/* our custom event occurred */
if (received & sigMask)
{
/* Our custom signal fired */
handle_event();
}
```
`Wait()` returns only after at least one bit in the mask is set. It is equivalent to sleeping — the task is moved to `TaskWait` and no CPU is consumed.
### Key Properties of Wait()
| Property | Behavior |
|---|---|
| **Blocks?** | Yes — task moves to `TaskWait`, CPU freed |
| **CPU cost while waiting** | Zero — task is completely dormant |
| **Returns when** | At least one bit in the mask is set |
| **Clears bits?** | Yes — only the matched bits are cleared from `tc_SigRecvd` |
| **Re-entrant?** | No — one `Wait()` per task at a time |
| **Forbid() interaction** | `Wait()` inside `Forbid()` temporarily breaks the forbid |
### Spurious Wakeups
Signals can accumulate. If a signal arrives before you call `Wait()`, the wait returns immediately:
```c
/* Signal arrives here — sets bit in tc_SigRecvd */
/* ... other code runs ... */
Wait(sigMask); /* Returns IMMEDIATELY — bit was already set */
```
This means `Wait()` never misses a signal, but you may get woken up for signals that were sent during a previous processing cycle. Always re-check your condition after waking.
---
## Sending Signals
```c
/* Signal a task from another task or interrupt handler: */
Signal(target_task, sigMask); /* LVO -324 */
/* From another task: */
Signal(targetTask, sigMask); /* LVO -324 */
/* From an interrupt handler: */
Signal(targetTask, sigMask); /* Safe — Signal is interrupt-safe */
```
`Signal()` is safe from interrupt context.
### Signal() is Atomic
`Signal()` disables interrupts internally, so it's safe to call from:
- Normal task context
- Interrupt handlers (all levels)
- Software interrupts
- Timer callbacks
No `Forbid()` or `Disable()` is needed around `Signal()`.
---
## SetSignal — Read and Clear
## SetSignal — Read and Modify Signal State
```c
/* Read and clear specific signal bits atomically: */
ULONG old = SetSignal(new_bits, change_mask); /* LVO -306 */
/* old = previous state of all 32 signal bits */
/* new value = (old & ~change_mask) | (new_bits & change_mask) */
/* Read and atomically modify signal bits: */
ULONG oldState = SetSignal(newBits, changeMask); /* LVO -306 */
/* Result: old value of all 32 bits
New value = (old & ~changeMask) | (newBits & changeMask) */
```
/* Check CTRL-C without blocking: */
if (SetSignal(0, SIGBREAKF_CTRL_C) & SIGBREAKF_CTRL_C) {
/* CTRL-C was pending — now cleared */
### Common Uses
```c
/* Check if Ctrl+C was pressed WITHOUT blocking: */
if (SetSignal(0, SIGBREAKF_CTRL_C) & SIGBREAKF_CTRL_C)
{
/* Ctrl+C was pending — now cleared */
running = FALSE;
}
/* Read all pending signals without clearing any: */
ULONG pending = SetSignal(0, 0); /* changeMask=0 → no modification */
/* Pre-clear a signal before entering a processing loop: */
SetSignal(0, mySignalMask); /* Clear bit — prevent stale wakeup */
```
---
## Typical Usage Pattern: Event Loop
## Practical Patterns
### Multi-Source Event Loop
The canonical AmigaOS event loop waits on multiple sources simultaneously:
```c
struct MsgPort *port = CreateMsgPort();
ULONG portSig = (1L << port->mp_SigBit);
ULONG waitMask = portSig | SIGBREAKF_CTRL_C;
struct MsgPort *idcmpPort = win->UserPort;
struct MsgPort *timerPort = CreateMsgPort();
BYTE customSig = AllocSignal(-1);
ULONG idcmpMask = 1L << idcmpPort->mp_SigBit;
ULONG timerMask = 1L << timerPort->mp_SigBit;
ULONG customMask = 1L << customSig;
ULONG breakMask = SIGBREAKF_CTRL_C;
ULONG waitMask = idcmpMask | timerMask | customMask | breakMask;
BOOL running = TRUE;
while (running) {
while (running)
{
ULONG sigs = Wait(waitMask);
if (sigs & SIGBREAKF_CTRL_C) {
running = FALSE;
}
if (sigs & portSig) {
struct Message *msg;
while ((msg = GetMsg(port)) != NULL) {
/* handle message */
ReplyMsg(msg);
if (sigs & idcmpMask)
{
struct IntuiMessage *imsg;
while ((imsg = GT_GetIMsg(idcmpPort)))
{
/* Handle GUI events */
GT_ReplyIMsg(imsg);
}
}
if (sigs & timerMask)
{
/* Handle timer expiry */
WaitIO(timerReq);
/* Restart timer... */
}
if (sigs & customMask)
{
/* Handle custom inter-task signal */
}
if (sigs & breakMask)
{
running = FALSE; /* Clean exit on Ctrl+C */
}
}
DeleteMsgPort(port);
```
### Producer-Consumer with Signal
```c
/* Producer (interrupt handler or high-priority task) */
volatile ULONG g_DataReady;
/* In producer: */
g_DataReady = TRUE;
Signal(consumerTask, dataSig);
/* Consumer: */
while (running)
{
Wait(dataSig | SIGBREAKF_CTRL_C);
if (g_DataReady)
{
g_DataReady = FALSE;
ProcessNewData();
}
}
```
---
## Pitfalls
### 1. Running Out of Signal Bits
Each task only has 16 application bits. Opening multiple windows, ports, and timers can exhaust them:
```c
/* Each of these allocates a signal bit: */
CreateMsgPort(); /* 1 bit */
CreateMsgPort(); /* 1 bit */
/* ... 14 more ... */
CreateMsgPort(); /* FAILS — returns NULL */
```
**Solution**: Share ports between related message sources, or use `PA_IGNORE` ports with polling.
### 2. Signaling a Dead Task
```c
/* BUG — task may have exited */
Signal(savedTaskPtr, mask); /* savedTaskPtr may point to freed memory */
/* No safe way to check — the task pointer is just an address */
/* Use message ports (FindPort) for robust inter-task communication */
```
### 3. Forgetting to Free Signals
```c
/* BUG — signal bit leaked on task exit */
BYTE sig = AllocSignal(-1);
/* ... use it ... */
/* Task exits without FreeSignal(sig) → bit is gone until reboot */
```
### 4. Not Handling All Waited Signals
```c
/* BUG — Ctrl+C accumulates but is never checked */
Wait(idcmpSig | SIGBREAKF_CTRL_C);
if (sigs & idcmpSig) { /* ... */ }
/* Forgot to check SIGBREAKF_CTRL_C — user can't break! */
```
---
## Best Practices
1. **Always handle `SIGBREAKF_CTRL_C`** — users expect Ctrl+C to work
2. **Free all signals** before task exit — `FreeSignal(sigBit)`
3. **Use `Wait()`** instead of busy-polling — zero CPU cost while sleeping
4. **Combine multiple signal sources** with `Wait(mask1 | mask2 | ...)`
5. **Pre-clear stale signals** with `SetSignal(0, mask)` before processing loops
6. **Never assume signal means data** — always re-check the condition after waking
7. **Use message ports** for data transfer — signals only carry "something happened"
8. **Don't cache task pointers** for signaling — use message ports for reliability
---
## References
- NDK39: `exec/tasks.h`, `exec/execbase.h`
- ADCD 2.1: `AllocSignal`, `FreeSignal`, `Signal`, `Wait`, `SetSignal`
- [tasks_processes.md](tasks_processes.md) — tc_SigAlloc, tc_SigRecvd fields
- See also: [Tasks & Processes](tasks_processes.md) — `tc_SigAlloc`, `tc_SigRecvd` fields
- See also: [Multitasking](multitasking.md) — Signal/Wait scheduling interaction
- *Amiga ROM Kernel Reference Manual: Exec* — signals chapter

View file

@ -1,157 +1,379 @@
[← Home](../README.md) · [Exec Kernel](README.md)
# Tasks and Processes — Structures, States, Scheduling
# Tasks and Processes — Structures, States, Creation, Scheduling
## Overview
AmigaOS uses **cooperative/preemptive** scheduling. Tasks are the fundamental unit of execution; Processes are Tasks with an additional DOS environment (message port, CLI, segment list). The scheduler runs at each quantum (50 Hz VBL interrupt) and after any `Signal()` or `Wait()` call.
AmigaOS uses **preemptive priority-based scheduling** with round-robin time-sharing among equal-priority tasks. Tasks are the fundamental unit of execution; Processes are Tasks with an additional DOS environment (message port, CLI context, segment list, filesystem handles). The scheduler runs at each vertical blank interrupt (50/60 Hz) and after any `Signal()` or `Wait()` call.
For the full multitasking deep-dive — scheduler algorithm, context switch costs, IPC strategies, and memory safety — see [Multitasking](multitasking.md).
---
## Task vs Process
```mermaid
graph TB
subgraph "struct Process"
subgraph "struct Task (embedded)"
NODE["Node<br/>ln_Name, ln_Pri, ln_Type"]
SIGS["Signal Fields<br/>tc_SigAlloc, tc_SigWait,<br/>tc_SigRecvd"]
STACK["Stack Bounds<br/>tc_SPLower, tc_SPUpper,<br/>tc_SPReg"]
EXCEPT["Exception<br/>tc_ExceptCode,<br/>tc_ExceptData"]
end
PORT["pr_MsgPort<br/>(built-in message port)"]
DOS["DOS Context<br/>pr_CurrentDir, pr_CIS, pr_COS,<br/>pr_CLI, pr_Arguments"]
SEG["pr_SegList<br/>(loaded code segments)"]
end
style NODE fill:#e8f4fd,stroke:#2196f3,color:#333
style DOS fill:#e8f5e9,stroke:#4caf50,color:#333
```
| Capability | Task | Process |
|---|---|---|
| Scheduling | ✅ | ✅ |
| Signals | ✅ | ✅ |
| Message Ports | Manual setup | ✅ Built-in `pr_MsgPort` |
| DOS I/O (Open/Read/Write) | ❌ | ✅ |
| CLI environment | ❌ | ✅ (if started from Shell) |
| Current directory | ❌ | ✅ `pr_CurrentDir` |
| stdin/stdout/stderr | ❌ | ✅ `pr_CIS`/`pr_COS`/`pr_CES` |
| Local variables | ❌ | ✅ `pr_LocalVars` |
> **Rule of thumb**: Use `CreateNewProcTags()` for everything. Use raw `AddTask()` only for bare-metal interrupt-level code or when you explicitly don't need DOS.
---
## struct Task
```c
/* exec/tasks.h */
/* exec/tasks.h — NDK39 */
struct Task {
struct Node tc_Node; /* ln_Type=NT_TASK or NT_PROCESS */
/* ln_Pri = scheduling priority */
/* ln_Name = task name string */
UBYTE tc_Flags; /* TF_LAUNCH, TF_STRIKE, TF_EXCEPT */
UBYTE tc_State; /* TS_RUN, TS_READY, TS_WAIT, TS_EXCEPT */
BYTE tc_IDNestCnt; /* interrupt disable nesting */
BYTE tc_TDNestCnt; /* task disable (Forbid) nesting */
ULONG tc_SigAlloc; /* allocated signal bits mask */
ULONG tc_SigWait; /* signals task is waiting for */
ULONG tc_SigRecvd; /* signals received */
ULONG tc_SigExcept; /* exception signals */
/* ... stack bounds, context, exception handler ... */
APTR tc_SPLower; /* lowest valid stack address */
APTR tc_SPUpper; /* highest valid stack address + 2 */
APTR tc_SPReg; /* saved stack pointer (when not running) */
struct Node tc_Node; /* ln_Type=NT_TASK or NT_PROCESS */
/* ln_Pri = scheduling priority (-128 to +127) */
/* ln_Name = task name string */
UBYTE tc_Flags; /* TF_LAUNCH, TF_SWITCH, TF_EXCEPT */
UBYTE tc_State; /* TS_RUN, TS_READY, TS_WAIT, TS_EXCEPT */
BYTE tc_IDNestCnt; /* interrupt disable nesting (-1 = enabled) */
BYTE tc_TDNestCnt; /* task disable (Forbid) nesting (-1 = enabled) */
ULONG tc_SigAlloc; /* allocated signal bits mask */
ULONG tc_SigWait; /* signals this task is waiting for */
ULONG tc_SigRecvd; /* signals received but not yet consumed */
ULONG tc_SigExcept; /* signals that trigger tc_ExceptCode */
UWORD tc_TrapAlloc; /* allocated trap vectors */
UWORD tc_TrapAble; /* enabled trap vectors */
APTR tc_ExceptData; /* data pointer passed to exception handler */
APTR tc_ExceptCode; /* exception handler function */
APTR tc_TrapData; /* data pointer passed to trap handler */
APTR tc_TrapCode; /* trap handler function */
APTR tc_SPReg; /* saved stack pointer (when not running) */
APTR tc_SPLower; /* lowest valid stack address */
APTR tc_SPUpper; /* highest valid stack address + 2 */
void (*tc_Switch)(); /* called when task is switched OUT */
void (*tc_Launch)(); /* called when task is switched IN */
struct List tc_MemEntry; /* list of memory entries to free on RemTask */
APTR tc_UserData; /* application-private data pointer */
};
```
### Key Field Reference
| Field | Description |
|---|---|
| `tc_Node.ln_Pri` | Scheduling priority (128 to +127). Higher = more CPU time |
| `tc_State` | Current state: `TS_RUN`, `TS_READY`, `TS_WAIT`, `TS_EXCEPT`, `TS_REMOVED` |
| `tc_IDNestCnt` | Interrupt disable nesting counter. 1 = interrupts enabled |
| `tc_TDNestCnt` | Task disable nesting counter. 1 = task switching enabled |
| `tc_SigAlloc` | Bitmask of allocated signal bits (1 = allocated) |
| `tc_SigWait` | Bitmask of signals this task will wake for (set by `Wait()`) |
| `tc_SigRecvd` | Bitmask of signals received (set by `Signal()`, cleared by `Wait()`) |
| `tc_SPReg` | Saved SP when task is not running — points to saved context on stack |
| `tc_SPLower` / `tc_SPUpper` | Stack bounds — exec fills these with guard patterns for stack overflow detection |
| `tc_MemEntry` | List of `MemEntry` structures — automatically freed by `RemTask()` |
| `tc_Switch` / `tc_Launch` | Optional callbacks on context switch — used by FPU context save/restore |
---
## struct Process (extends Task)
```c
/* dos/dosextens.h */
/* dos/dosextens.h — NDK39 */
struct Process {
struct Task pr_Task; /* embedded Task */
struct MsgPort pr_MsgPort; /* I/O message port */
UWORD pr_Pad;
BPTR pr_SegList; /* BPTR to segment list */
LONG pr_StackSize;
APTR pr_GlobVec; /* BCPL global vector */
LONG pr_TaskNum; /* CLI task number */
BPTR pr_StackBase; /* base of stack (BPTR) */
LONG pr_Result2; /* secondary result */
BPTR pr_CurrentDir; /* current directory lock */
BPTR pr_CIS; /* current input stream */
BPTR pr_COS; /* current output stream */
APTR pr_ConsoleTask;
APTR pr_FileSystemTask;
BPTR pr_CLI; /* CLI structure (NULL if WB) */
...
struct MsgPort *pr_ReturnAddr; /* return address for CLI tasks */
APTR pr_PktWait;
struct SaveMsg pr_ExitData;
UBYTE *pr_Arguments; /* argument string */
struct MinList pr_LocalVars; /* local shell variables */
ULONG pr_ShellPrivate;
BPTR pr_CES; /* current error stream */
struct Task pr_Task; /* embedded Task — MUST be first field */
struct MsgPort pr_MsgPort; /* built-in message port for DOS packets */
UWORD pr_Pad;
BPTR pr_SegList; /* segment list (loaded code) */
LONG pr_StackSize; /* stack size in bytes */
APTR pr_GlobVec; /* BCPL global vector (legacy) */
LONG pr_TaskNum; /* CLI task number (0 = Workbench) */
BPTR pr_StackBase; /* base of stack (BPTR) */
LONG pr_Result2; /* secondary result (IoErr()) */
BPTR pr_CurrentDir; /* current directory lock */
BPTR pr_CIS; /* current input stream (stdin) */
BPTR pr_COS; /* current output stream (stdout) */
APTR pr_ConsoleTask; /* console handler task */
APTR pr_FileSystemTask; /* filesystem handler task */
BPTR pr_CLI; /* pointer to CommandLineInterface */
APTR pr_ReturnAddr; /* return address for exit */
APTR pr_PktWait; /* custom packet wait function */
APTR pr_WindowPtr; /* window for error requesters (or 1 to suppress) */
BPTR pr_HomeDir; /* home directory of program */
LONG pr_Flags; /* PR_FREESEGLIST, PR_FREEARGS, etc. */
void (*pr_ExitCode)(); /* exit handler */
LONG pr_ExitData; /* data for exit handler */
UBYTE *pr_Arguments; /* argument string */
struct MinList pr_LocalVars; /* local shell environment variables */
ULONG pr_ShellPrivate; /* shell private data */
BPTR pr_CES; /* current error stream (stderr) — V39+ */
};
```
### Important Process Fields
| Field | Description |
|---|---|
| `pr_MsgPort` | Built-in message port — used for DOS packet communication |
| `pr_CurrentDir` | Lock on current directory — inherited from parent |
| `pr_CIS` / `pr_COS` / `pr_CES` | File handles for stdin, stdout, stderr |
| `pr_CLI` | Non-NULL if started from CLI/Shell, NULL if from Workbench |
| `pr_WindowPtr` | Window for error requesters. Set to `1` to suppress "Please insert volume" dialogs |
| `pr_Arguments` | Raw argument string (not parsed — use `ReadArgs()`) |
| `pr_Result2` | Secondary error code — retrieved by `IoErr()` |
| `pr_ExitCode` | Called when process exits — cleanup handler |
---
## Task States
| State | Value | Meaning |
|---|---|---|
| `TS_INVALID` | 0 | Not a valid task |
| `TS_ADDED` | 1 | Just added, not yet scheduled |
| `TS_RUN` | 2 | Currently running (only one task) |
| `TS_READY` | 3 | On the `TaskReady` list, waiting for CPU |
| `TS_WAIT` | 4 | Blocked on `Wait()` — on `TaskWait` list |
| `TS_EXCEPT` | 5 | Handling an exception |
| `TS_REMOVED` | 6 | Removed from scheduling |
| State | Constant | Value | Meaning |
|---|---|---|---|
| Invalid | `TS_INVALID` | 0 | Not a valid task |
| Added | `TS_ADDED` | 1 | Just added, not yet scheduled |
| Running | `TS_RUN` | 2 | Currently executing (exactly one task) |
| Ready | `TS_READY` | 3 | On `SysBase→TaskReady`, waiting for CPU |
| Waiting | `TS_WAIT` | 4 | Blocked on `Wait()` — on `SysBase→TaskWait` |
| Exception | `TS_EXCEPT` | 5 | Handling a task-level exception |
| Removed | `TS_REMOVED` | 6 | Removed from scheduling |
### State Machine
```mermaid
stateDiagram-v2
[*] --> ADDED : AddTask() / CreateNewProc()
ADDED --> READY : Exec enqueues by priority
READY --> RUN : Scheduler picks highest priority
RUN --> READY : Quantum expired / higher-priority task wakes
RUN --> WAIT : Wait(signal_mask)
WAIT --> READY : Signal() delivers matching signal
RUN --> EXCEPT : Exception signal received
EXCEPT --> RUN : Exception handler returns
RUN --> REMOVED : RemTask() / task function returns
REMOVED --> [*]
```
---
## Scheduling: Priority-Based Round Robin
The scheduler (`exec.library` internal) picks the highest-priority task from `SysBase→TaskReady`. Among equal-priority tasks, they get equal time slices (round-robin).
The scheduler (`exec.library` internal) picks the highest-priority task from `SysBase→TaskReady`:
- Default priority: 0
- Range: 128 to +127 (higher = more CPU)
- OS tasks run at priority 1020
- Input handler: priority 20
- Disk tasks: priority 10
| Priority Range | Typical Use |
|---|---|
| +127 | (Unused — would starve everything) |
| +20 | input.device handler |
| +10 | trackdisk.device, filesystem handlers |
| +5 | Real-time applications (audio players) |
| 0 | **Normal applications** |
| 1 | Background workers (file copy, indexing) |
| 128 | Idle task |
```c
SetTaskPri(FindTask(NULL), 5); /* raise current task to priority 5 */
/* Change priority of current task */
BYTE oldPri = SetTaskPri(FindTask(NULL), 5); /* LVO -300 */
/* Returns old priority */
```
> **Warning**: Priority > 20 will starve the input handler. Priority > 10 will starve filesystem tasks. Choose wisely.
---
## Creating Tasks
### Raw Task (exec level)
```c
#define STACKSIZE 4096
/* Allocate task structure and stack together */
struct Task *task = AllocMem(sizeof(struct Task), MEMF_PUBLIC | MEMF_CLEAR);
APTR stack = AllocMem(STACKSIZE, MEMF_ANY);
task->tc_Node.ln_Type = NT_TASK;
task->tc_Node.ln_Name = "MyTask";
task->tc_Node.ln_Pri = 0;
task->tc_SPLower = stack;
task->tc_SPUpper = (APTR)((ULONG)stack + STACKSIZE);
task->tc_SPReg = task->tc_SPUpper; /* Stack grows downward */
/* AddTask(task, initialPC, finalPC) */
AddTask(task, MyTaskEntry, NULL); /* LVO -282 */
/* NULL finalPC = exec's default task cleanup */
```
> **Caution**: Raw `AddTask()` tasks cannot call DOS functions (Open, Read, Write, Printf). They have no `pr_MsgPort`, no current directory, no stdin/stdout. Use `CreateNewProcTags()` for anything that needs I/O.
### Process (DOS level — preferred)
```c
struct Process *proc = CreateNewProcTags(
NP_Entry, MyProcEntry, /* entry function */
NP_Name, "MyProcess", /* process name */
NP_StackSize, 8192, /* stack size in bytes */
NP_Priority, 0, /* scheduling priority */
NP_CurrentDir, DupLock(GetProgramDir()), /* inherit directory */
NP_Input, NULL, /* or a file handle for stdin */
NP_Output, NULL, /* or a file handle for stdout */
NP_CloseInput, FALSE, /* don't close stdin on exit */
NP_CloseOutput,FALSE, /* don't close stdout on exit */
TAG_DONE
);
if (!proc) { /* creation failed */ }
```
### Task Cleanup: tc_MemEntry
Memory added to `tc_MemEntry` is automatically freed when the task is removed:
```c
/* Add stack + task struct to auto-cleanup list */
struct MemList *ml = AllocMem(sizeof(struct MemList) + sizeof(struct MemEntry),
MEMF_PUBLIC | MEMF_CLEAR);
ml->ml_NumEntries = 2;
ml->ml_ME[0].me_Un.meu_Addr = task;
ml->ml_ME[0].me_Length = sizeof(struct Task);
ml->ml_ME[1].me_Un.meu_Addr = stack;
ml->ml_ME[1].me_Length = STACKSIZE;
AddHead(&task->tc_MemEntry, &ml->ml_Node);
/* Now RemTask() will free both task struct and stack */
```
---
## Creating Tasks and Processes
## Task Identity
```c
/* Simple task (exec level): */
struct Task *t = AllocMem(sizeof(struct Task), MEMF_PUBLIC|MEMF_CLEAR);
t->tc_Node.ln_Name = "MyTask";
t->tc_Node.ln_Pri = 0;
t->tc_SPLower = stack;
t->tc_SPUpper = stack + stacksize;
t->tc_SPReg = (APTR)((ULONG)stack + stacksize);
AddTask(t, myTaskFunc, NULL);
/* Get current task */
struct Task *me = FindTask(NULL); /* LVO -294 — NULL = current */
Printf("Running as: %s (pri %ld)\n", me->tc_Node.ln_Name, me->tc_Node.ln_Pri);
/* DOS Process (with message port, filesystem access): */
struct Process *p = CreateNewProcTags(
NP_Entry, myFunc,
NP_Name, "MyProcess",
NP_StackSize, 8192,
NP_Priority, 0,
TAG_DONE);
```
/* Find another task by name */
Forbid();
struct Task *other = FindTask("TargetApp"); /* Returns NULL if not found */
Permit();
---
## Task State Machine
```mermaid
stateDiagram-v2
[*] --> READY : AddTask()
READY --> RUN : Scheduler picks task
RUN --> READY : Quantum expired or higher-priority task
RUN --> WAIT : Wait(signal_mask)
WAIT --> READY : Signal() delivers awaited signals
RUN --> EXCEPT : Exception signal received
EXCEPT --> RUN : Exception handler returns
RUN --> [*] : Task function returns / RemTask()
```
---
## FindTask and Task Identity
```c
struct Task *me = FindTask(NULL); /* NULL = current task */
printf("Running as: %s\n", me->tc_Node.ln_Name);
/* Check if we are a Process (vs plain Task): */
if (me->tc_Node.ln_Type == NT_PROCESS) {
/* Check if current task is a Process */
if (me->tc_Node.ln_Type == NT_PROCESS)
{
struct Process *pr = (struct Process *)me;
/* access pr_CLI, pr_MsgPort, etc. */
/* Safe to use pr_CLI, pr_MsgPort, pr_CurrentDir, etc. */
}
```
---
## Removing Tasks
```c
/* Remove current task (task commits suicide): */
RemTask(NULL); /* LVO -288 — NULL = remove self */
/* Never returns — task is destroyed */
/* Remove another task (dangerous!): */
Forbid();
struct Task *victim = FindTask("OtherTask");
if (victim) RemTask(victim);
Permit();
```
> **Caution**: `RemTask()` on another task does NOT:
> - Close its open files
> - Free its message ports
> - Reply to pending messages
> - Close its libraries
>
> This leaks resources permanently. Use `Signal()` + cooperative shutdown instead.
---
## Pitfalls
### 1. Calling DOS from a Raw Task
```c
/* BUG — raw Task has no DOS environment */
void __saveds MyTaskFunc(void)
{
BPTR fh = Open("RAM:test", MODE_NEWFILE); /* CRASH — no pr_FileSystemTask */
}
```
### 2. Stack Overflow
```c
/* BUG — recursive function exhausts 4 KB stack */
void MyTask(void)
{
char buffer[2048]; /* Half the stack gone in one frame */
ProcessData(buffer);
MyTask(); /* Stack overflow → corrupts next task's memory */
}
```
The system does NOT catch stack overflows — memory just gets silently corrupted. Use `StackSwap()` for deep recursion.
### 3. RemTask Without Cleanup
```c
/* BUG — resources leaked */
void MyProcess(void)
{
struct Library *base = OpenLibrary("mylib.library", 0);
struct MsgPort *port = CreateMsgPort();
/* ... */
RemTask(NULL); /* Library not closed, port not freed */
}
```
### 4. Caching Task Pointers
```c
/* BUG — task may have exited */
struct Task *t = FindTask("Worker");
/* ... some time passes ... */
Signal(t, mask); /* t may be freed memory */
```
---
## Best Practices
1. **Use `CreateNewProcTags()`** — raw `AddTask()` is for kernel-level code only
2. **Use `tc_MemEntry`** for automatic cleanup of task-allocated memory
3. **Always check `FindTask()` return** — tasks can exit at any time
4. **Use cooperative shutdown** — send a signal, let the task clean up and exit itself
5. **Set `pr_WindowPtr = -1`** to suppress "Please insert volume" dialogs in background tasks
6. **Size stacks generously** — 8192+ bytes for processes, 4096+ for tasks
7. **Use `StackSwap()`** if you need temporary deep stack for recursive algorithms
8. **Never `RemTask()` another task** in production code — it leaks everything
---
## References
- NDK39: `exec/tasks.h`, `dos/dosextens.h`
- ADCD 2.1: `AddTask`, `RemTask`, `FindTask`, `SetTaskPri`, `CreateNewProc`
- NDK39: `exec/tasks.h`, `dos/dosextens.h`, `dos/dostags.h`
- ADCD 2.1: `AddTask`, `RemTask`, `FindTask`, `SetTaskPri`, `CreateNewProcTags`, `StackSwap`
- See also: [Multitasking](multitasking.md) — scheduler algorithm, context switch, IPC strategies
- See also: [Signals](signals.md) — tc_SigAlloc, tc_SigRecvd, tc_SigWait
- *Amiga ROM Kernel Reference Manual: Exec* — tasks and scheduling chapter

View file

@ -141,19 +141,20 @@ The Amiga's documentation was scattered across out-of-print manuals, Usenet post
### 06 — Exec Kernel (OS 3.1/3.2)
| File | Topic |
|---|---|
| [exec_base.md](06_exec_os/exec_base.md) | ExecBase structure |
| [library_system.md](06_exec_os/library_system.md) | Library lifecycle |
| [library_vectors.md](06_exec_os/library_vectors.md) | JMP table, SetFunction |
| [tasks_processes.md](06_exec_os/tasks_processes.md) | Multitasking |
| [interrupts.md](06_exec_os/interrupts.md) | Interrupt levels, INTENA |
| [exceptions_traps.md](06_exec_os/exceptions_traps.md) | Exception vectors, TRAP instructions, Guru codes |
| [memory_management.md](06_exec_os/memory_management.md) | AllocMem, MEMF flags |
| [message_ports.md](06_exec_os/message_ports.md) | PutMsg/GetMsg/WaitPort |
| [signals.md](06_exec_os/signals.md) | AllocSignal, Wait |
| [semaphores.md](06_exec_os/semaphores.md) | ObtainSemaphore |
| [io_requests.md](06_exec_os/io_requests.md) | IORequest, DoIO/SendIO |
| [lists_nodes.md](06_exec_os/lists_nodes.md) | MinList, Node, Enqueue |
| [resident_modules.md](06_exec_os/resident_modules.md) | RomTag, RTF_AUTOINIT |
| [exec_base.md](06_exec_os/exec_base.md) | ExecBase — absolute address $4, system lists, hardware detection, structure layout |
| [**multitasking.md**](06_exec_os/multitasking.md) | **Multitasking deep-dive — scheduler, context switching, IPC, memory safety, real-world scenarios** |
| [tasks_processes.md](06_exec_os/tasks_processes.md) | Task/Process structs, state machine, creation, cleanup, priority guidelines |
| [library_system.md](06_exec_os/library_system.md) | Library node, OpenLibrary lifecycle, expunge mechanics, version management |
| [library_vectors.md](06_exec_os/library_vectors.md) | JMP table, LVO offsets, MakeFunctions, SetFunction patching |
| [interrupts.md](06_exec_os/interrupts.md) | Interrupt levels 16, INTENA/INTREQ, server chains, CIA interrupts, software interrupts |
| [exceptions_traps.md](06_exec_os/exceptions_traps.md) | M68k exception vectors, TRAP handlers, Guru Meditation decoder, Line-F emulation |
| [memory_management.md](06_exec_os/memory_management.md) | AllocMem/AllocVec, MEMF flags, pools, fragmentation, MemHeader internals |
| [message_ports.md](06_exec_os/message_ports.md) | MsgPort, PutMsg, GetMsg, WaitPort, ownership rules, request-reply pattern |
| [signals.md](06_exec_os/signals.md) | AllocSignal, Signal, Wait, SetSignal, multi-source event loop patterns |
| [semaphores.md](06_exec_os/semaphores.md) | SignalSemaphore, shared/exclusive locking, deadlock avoidance, decision guide |
| [io_requests.md](06_exec_os/io_requests.md) | IORequest, DoIO, SendIO, CheckIO, AbortIO, IOF_QUICK, timer device example |
| [lists_nodes.md](06_exec_os/lists_nodes.md) | MinList/List/Node traversal, sentinel design, Enqueue, safe iteration |
| [resident_modules.md](06_exec_os/resident_modules.md) | RomTag, RTF_AUTOINIT, boot priority, ROM scan, disk-resident loading |
### 07 — AmigaDOS
| File | Topic |