New articles: - 01_hardware/common/memory_types.md: comprehensive Chip/Fast/Slow RAM reference with DMA accessibility matrix, per-model configurations (A500-A4000), accelerator memory expansion (classic + modern), adaptive software behavior, pitfalls with impact analysis, FPGA/emulation notes - AGENTS.md: documentation standards and methodology guidelines Blitter programming (08_graphics/blitter_programming.md): - Rewrote minterm truth table with narrative explanation and worked example - Added 7 advanced use cases with assembly/C code: shifted BOB, hardware scroll, area fill polygon, interleaved bitplane BOBs, double-buffered game loop, GUI window drag, tile map renderer - Added Good/Bad Patterns section with 5 named antipatterns - Added Practical Limitations table (10 constraints with workarounds) - Expanded Common Minterms table with Description and Real-World Use Case columns Cross-reference updates: - Root README: added memory types to coverage, quick start, section index - 01_hardware/README: updated common/ folder description - address_space.md: linked to memory_types.md and chip_ram_expansion.md - memory_management.md (exec): linked to hardware memory types reference - bitmap.md, sprites.md, animation.md, audio.md: linked to memory_types.md explaining DMA Chip RAM requirement for each subsystem - chip_ram_expansion.md: linked to comprehensive memory types article
14 KiB
Memory Management — AllocMem, FreeMem, MemHeader
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. 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
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
- Walk
SysBase→MemList— eachMemHeaderdescribes a physical RAM region - Check
mh_Attributesagainst the requestedMEMF_*flags - Walk the
MemChunkfree-list within the matching region - Find the first chunk large enough (first-fit)
- Split the chunk: return the requested portion, keep the remainder on the free list
- If
MEMF_CLEARis set, zero-fill the returned block
How FreeMem Works
- Find the
MemHeaderwhose range contains the freed address - Walk the
MemChunkfree-list to find the correct insertion point (address-ordered) - Insert a new
MemChunkat that position - Coalesce with adjacent free chunks if they're contiguous
Warning
:
FreeMemtrusts the caller completely. Wrong address or wrong size → free-list corruption → nextAllocMemreturns overlapping memory → system crash.
MemHeader — Memory Region Descriptor
/* exec/memory.h — NDK39 */
struct MemHeader {
struct Node mh_Node; /* ln_Type=NT_MEMORY, ln_Pri=region priority */
/* ln_Name = e.g. "chip memory" */
UWORD mh_Attributes; /* MEMF_* flags describing this region */
struct MemChunk *mh_First; /* pointer to first free chunk in this region */
APTR mh_Lower; /* lowest byte address of region */
APTR mh_Upper; /* highest byte address + 1 */
ULONG mh_Free; /* total free bytes currently */
};
struct MemChunk {
struct MemChunk *mc_Next; /* next free chunk (NULL = end of list) */
ULONG mc_Bytes; /* size of this free chunk in bytes */
};
| 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)
MEMF_ Flag Constants
/* exec/memory.h — NDK39 */
#define MEMF_ANY 0L /* no placement preference */
#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. On systems with both, Exec prefers Fast RAM for MEMF_ANY allocations (higher mh_Node.ln_Pri).
AllocMem / FreeMem
/* 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
/* Allocate 512 bytes of Chip RAM, zero-filled: */
UBYTE *buf = AllocMem(512, MEMF_CHIP | MEMF_CLEAR);
if (!buf)
{
/* Handle out-of-memory — no exceptions, just NULL */
return RETURN_FAIL;
}
/* Use the buffer... */
/* Free it — size MUST match exactly: */
FreeMem(buf, 512);
Critical:
FreeMemrequires the exact same size asAllocMem. 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 |
AllocVec / FreeVec (OS 2.0+)
/* LVO -684 (exec.library 36+) */
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:
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).
AvailMem — Query Free Memory
/* LVO -216 */
ULONG AvailMem(ULONG requirements);
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 and improves performance:
/* LVO -696 */
APTR pool = CreatePool(MEMF_ANY, 4096, 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 */
APTR p2 = AllocPooled(pool, 64);
APTR p3 = AllocPooled(pool, 128);
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
- Use pools for small, frequent allocations
- Allocate large blocks early before fragmentation develops
- Use
MEMF_REVERSEfor long-lived allocations (allocate from top of memory) - Free in reverse order when possible
- Pre-allocate and sub-manage your own buffers for performance-critical code
Memory Map (A1200 Example)
| Range | Type | Used for |
|---|---|---|
$000000–$000003 |
Chip | ExecBase pointer (absolute address $4) |
$000004–$000400 |
Chip | 68k exception vectors |
$000400–$000BFF |
Chip | exec library, SysBase |
$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 | 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
/* 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
/* BUG — memory is already on the free list */
FreeMem(buf, 100);
FreeMem(buf, 100); /* Corrupts free list — duplicate MemChunk */
3. Use After Free
/* 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
/* 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
/* 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
/* 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
- Use
AllocVec/FreeVecfor new code — eliminates size-tracking bugs - Use pools for many small allocations — reduces fragmentation
- Always check for NULL — memory exhaustion is common on 512 KB–2 MB systems
- Use
MEMF_CHIPonly when required — don't waste DMA-capable memory on CPU-only data - Track all allocations — use a resource list or goto-cleanup pattern
- Free in reverse order — reduces fragmentation
- Use
MEMF_CLEARfor structures — prevents uninitialized field bugs - Pre-allocate during initialization — don't allocate in tight loops or interrupt handlers
- Never call
AllocMemfrom interrupt context — it may need toWait() - 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,AllocPooled,AvailMem - address_space.md — full address map
- memory_types.md — hardware-level Chip/Fast/Slow RAM comparison, DMA accessibility matrix, per-model configurations
- See also: Multitasking — memory safety in multi-task environments
- Amiga ROM Kernel Reference Manual: Exec — memory management chapter