[← Home](../README.md) · [Exec Kernel](README.md)
# 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
```mermaid
graph TB
SB["SysBase→MemList
(struct List)"] --> MH1["MemHeader
'chip memory'
$000000–$1FFFFF"]
SB --> MH2["MemHeader
'fast memory'
$200000–$9FFFFF"]
MH1 --> MC1["MemChunk
free block A"]
MC1 --> MC2["MemChunk
free block B"]
MC2 --> MC3["MemChunk
free block C"]
MH2 --> MC4["MemChunk
free block D"]
MC4 --> MC5["MemChunk
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.
---
## MemHeader — Memory Region Descriptor
```c
/* 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
```c
/* 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
```c
/* 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
```c
/* 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**: `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 |
---
## AllocVec / FreeVec (OS 2.0+)
```c
/* 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
```c
/* LVO -216 */
ULONG AvailMem(ULONG requirements);
```
```c
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:
```c
/* 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
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–$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
```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 KB–2 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`, `AllocPooled`, `AvailMem`
- [address_space.md](../01_hardware/common/address_space.md) — full address map
- [memory_types.md](../01_hardware/common/memory_types.md) — hardware-level Chip/Fast/Slow RAM comparison, DMA accessibility matrix, per-model configurations
- See also: [Multitasking](multitasking.md) — memory safety in multi-task environments
- *Amiga ROM Kernel Reference Manual: Exec* — memory management chapter