You see `MOVE.L ($17A,A6), A0` in disassembly. You know A6 is SysBase. But `+$17A` — what field is that? Without structure definitions, every offset is just a number. With them, `+$17A` becomes `SysBase->LibList` and the disassembly transforms from arithmetic to narrative.
Amiga executables are built on a deep stack of OS structures — `ExecBase`, `Node`, `List`, `Task`, `Process`, `IORequest`, `Message`, `MsgPort`. Recovering these structures in disassembly means matching **base register + constant offset** patterns against the NDK 3.9 header definitions. This article covers the methodology, the most commonly encountered structures, and the IDA Pro workflows that automate the process.
```mermaid
graph LR
subgraph "Disassembly"
RAW["MOVE.L ($17A,A6), A0"]
end
subgraph "NDK Headers"
HDR["exec/execbase.h<br/>LibList at +0x17A"]
end
subgraph "Annotated"
ANN["SysBase->LibList.lh_Head<br/>= first library node"]
| **Parse C header file** | 1 min setup | 100% | Importing full NDK type system |
| **Heuristic: offset clustering** | ~2 min | 70–90% | Unknown structures — group accesses by register, infer field boundaries |
---
## Named Antipatterns
### 1. "The Offset Hallucination"
**What it looks like** — assuming `($1A,A6)` is always `lib_Version` because the number looks right:
```asm
MOVE.W ($1A,A6), D0 ; looks like version?
; Actually: lib_Version is at +$14 (offset 20), NOT +$1A (offset 26)
; +$1A = lib_Node.ln_Name (upper word of STRPTR)
```
**Why it fails:** Hex offsets are deceptive. `$14` and `$1A` differ by 6 bytes — one field apart in a packed structure. Without the header definition, off-by-one-field errors are invisible until runtime.
**Correct:** Always verify against the NDK header. `lib_Version` is at `+$14` (UWORD), not `+$1A`.
### 2. "The Nested Structure Blur"
**What it looks like** — accessing `SysBase->LibList.lh_Head` but interpreting it as `SysBase->TaskWait.lh_Head`:
```asm
MOVEA.L ($17A,A6), A0 ; +$17A = LibList (correct)
; Not: +$132 = TaskWait — that's a different list entirely
```
**Why it fails:** `SysBase` has multiple `struct List` fields. `LibList` (`+$17A`), `DeviceList` (`+$182`), `TaskReady` (`+$128`), `TaskWait` (`+$132`), `MemList` (`+$280`) — all use the same `lh_Head` access pattern. Without checking the exact offset, you'll identify the wrong list.
**Correct:** The offset is the discriminator. `+$17A` = LibList, `+$128` = TaskReady. Never guess.
Trace D0 from AllocMem through the function to identify **which struct** is being allocated. If the code then accesses `($14,A4)`, you have a `struct Library` allocation.
### Batch-Annotate All ExecBase Accesses
```python
# IDA Python: apply ExecBase structure to all SysBase-relative accesses
def apply_execbase_structure():
sid = idc.get_struc_id("ExecBase")
if sid == idc.BADADDR:
idc.import_type(-1, "ExecBase")
sid = idc.get_struc_id("ExecBase")
sysbase = idc.get_name_ea_simple("SysBase")
if sysbase == idc.BADADDR:
print("SysBase not found")
return
# Find all instructions referencing SysBase-relative offsets
for xref in idautils.XrefsTo(sysbase):
ea = xref.frm
# Navigate forward looking for offset(An) operands
| Structure recovery by offset matching | PDB symbol file (debug info) | DWARF debug info `.debug_info` | Amiga lacks embedded debug info — must match offsets manually |
| NDK headers as ground truth | Windows SDK headers + PDB | GLibc headers + DWARF | Same idea: header defines layout, disassembly shows access pattern |
| IDA `T` hotkey for struct offset | IDA `T` on x86/ARM too | Same | IDA's struct offset annotation is platform-independent |
---
## FAQ
### How do I identify a struct when the base register changes?
Track register writes backward. If A4 holds a struct pointer but you can't tell which struct, find the last `MOVEA.L ..., A4`. If it came from `AllocMem`, the size in D0 tells you the struct size — match against known struct sizes from NDK. If it came from `OpenLibrary`, it's a library base.
### What if the OS version changed the struct layout?
Commodore maintained binary compatibility — fields were appended, never reordered. An offset that works on Kickstart 1.3 also works on 3.1, because the earlier fields are at the same positions. However, fields added in later versions only exist in those versions. Always check `lib_Version` before accessing fields added after OS 1.3.