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

@ -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