amiga-bootcamp/05_reversing/dynamic/live_memory_probing.md
2026-04-27 18:34:07 -04:00

8.7 KiB
Raw Blame History

← Home · Reverse Engineering

Live Memory Probing

Overview

The Amiga has no Task Manager, no dtrace, no /proc. But it has something better: every critical OS data structure is reachable from a single pointer at absolute address $4. From SysBase, you can walk the library list, enumerate every running task, map every memory region, and even modify kernel structures — all from a user-mode program. No debugger required.

Live memory probing means reading (and sometimes writing) exec structures directly from a running Amiga without a traditional debugger. This is how tools like Scout, SysInspector, and XOpa work. It's how you verify that a hook is installed, check if a library is loaded, or inspect task state during development. This article covers the key data structures, the traversal patterns, and the safety rules.

graph TB
    SYSBASE["SysBase<br/>at absolute $4"]
    subgraph "Reachable Structures"
        LIBLIST["LibList<br/>→ every loaded library"]
        DEVLIST["DeviceList<br/>→ every loaded device"]
        TASKREADY["TaskReady<br/>→ runnable tasks"]
        TASKWAIT["TaskWait<br/>→ waiting tasks"]
        MEMLIST["MemList<br/>→ memory regions"]
    end
    SYSBASE --> LIBLIST
    SYSBASE --> DEVLIST
    SYSBASE --> TASKREADY
    SYSBASE --> TASKWAIT
    SYSBASE --> MEMLIST


SysBase: The Root of Everything

SysBase is always at absolute address $4 (a pointer to the ExecBase structure):

struct ExecBase *SysBase = *((struct ExecBase **)4);
printf("exec version: %d.%d\n",
       SysBase->LibNode.lib_Version,
       SysBase->LibNode.lib_Revision);

In assembly:

MOVEA.L  4.W, A6              ; A6 = SysBase (exec.library base)
MOVE.W   ($16,A6), D0         ; lib_Version
MOVE.W   ($18,A6), D1         ; lib_Revision

Walking the Library List

struct Node *n = SysBase->LibList.lh_Head;
while (n->ln_Succ != NULL) {
    struct Library *lib = (struct Library *)n;
    printf("%-30s v%d.%d  opens=%d\n",
           lib->lib_Node.ln_Name,
           lib->lib_Version, lib->lib_Revision,
           lib->lib_OpenCnt);
    n = n->ln_Succ;
}

This enumerates all currently loaded libraries. Useful for:

  • Finding if a target library is loaded
  • Reading lib_OpenCnt to detect if your hook is installed
  • Checking lib_Flags & LIBF_DELEXP (expunge pending)

Reading lib_OpenCnt Live

/* Check if bsdsocket.library is loaded and its open count */
struct Library *base = FindName(&SysBase->LibList, "bsdsocket.library");
if (base) {
    printf("bsdsocket: OpenCnt=%d, Version=%d\n",
           base->lib_OpenCnt, base->lib_Version);
}

FindName scans ln_Name in a linked list — it is an exec function at LVO 276.


Memory Region Map

SysBase->MemList lists all memory regions:

struct MemHeader *mh = (struct MemHeader *)SysBase->MemList.lh_Head;
while (mh->mh_Node.ln_Succ) {
    printf("Region: %s  %08lx%08lx  free=%ld\n",
           mh->mh_Node.ln_Name,
           (ULONG)mh->mh_Lower,
           (ULONG)mh->mh_Upper,
           mh->mh_Free);
    mh = (struct MemHeader *)mh->mh_Node.ln_Succ;
}

Output example:

Region: chip memory   $000000$1FFFFF  free=524288
Region: fast memory   $200000$9FFFFF  free=6291456

Task List Inspection

/* Running tasks: */
Forbid();
struct Task *t = (struct Task *)SysBase->TaskReady.lh_Head;
while (t->tc_Node.ln_Succ) {
    printf("Task: %-20s pri=%d state=%d\n",
           t->tc_Node.ln_Name,
           t->tc_Node.ln_Pri,
           t->tc_State);
    t = (struct Task *)t->tc_Node.ln_Succ;
}
Permit();

Forbid() / Permit() are mandatory — the task list must not change while walking it.


Patching Memory Live (Surgical Writes)

For RE/patching: direct longword write to an OS structure:

/* Example: force a library's version to 99 */
Forbid();
target_lib->lib_Version = 99;
Permit();

Caution

Direct memory writes to OS structures bypass all synchronization. Always use Forbid() at minimum; use Disable() if modifying interrupt-visible data.


Decision Guide — Safe Probing Rules

Operation Required Protection Risk Without Protection
Reading a single field (lib_Version) None — atomic word read None on 6800068060
Walking a linked list (LibList, TaskReady) Forbid() / Permit() Task switch mid-walk → stale pointer → crash
Modifying a structure field Forbid() (minimum) or Disable() Other task reads half-written value
Allocating/freeing memory during probing Forbid() only — don't Disable() Disable() blocks interrupts, may deadlock AllocMem
Walking interrupt-visible data Disable() / Enable() Interrupt modifies structure mid-read

Named Antipatterns

1. "The Naked List Walk"

What it looks like — walking TaskReady without Forbid():

// BROKEN — task switch mid-walk
struct Task *t = (struct Task *)SysBase->TaskReady.lh_Head;
while (t->tc_Node.ln_Succ) {
    printf("%s\n", t->tc_Node.ln_Name);
    t = (struct Task *)t->tc_Node.ln_Succ;  // ← may be stale after switch!
}

Why it fails: If the current task's time slice expires during the walk, another task can add or remove nodes from TaskReady. The ln_Succ pointer you cached is now dangling — pointing to freed or moved memory.

Correct:

Forbid();
struct Task *t = (struct Task *)SysBase->TaskReady.lh_Head;
while (t->tc_Node.ln_Succ) {
    printf("%s\n", t->tc_Node.ln_Name);
    t = (struct Task *)t->tc_Node.ln_Succ;
}
Permit();

2. "The Disable Trap"

What it looks like — using Disable() when only Forbid() is needed:

// OVERKILL — Disable blocks ALL interrupts AND task switches
Disable();
struct Library *lib = FindName(&SysBase->LibList, "intuition.library");
UWORD ver = lib->lib_Version;  // atomic word read — Forbid is enough!
Enable();

Why it fails: Disable() blocks ALL interrupts — including the vertical blank interrupt that drives the system clock. Holding Disable() for more than a few hundred cycles causes lost time, missed serial data, and audio dropouts. Forbid is sufficient for list traversal; Disable is only needed when interrupts themselves modify the same data.

Correct: Use the weakest protection that covers your access pattern.


Use-Case Cookbook

Check If a Hook Is Installed

BOOL is_hook_installed(struct Library *lib, LONG lvo, APTR expected_func) {
    APTR current = (APTR)(*(ULONG *)((UBYTE *)lib + lvo + 2));
    return (current == expected_func);
}

Live Patch Verification Script

/* Verify a library function is still at its original address */
ULONG get_jmp_target(struct Library *lib, LONG lvo) {
    UBYTE *entry = (UBYTE *)lib + lvo;
    if (*(UWORD *)entry != 0x4EF9) return 0;  // Not a JMP ABS.L
    return *(ULONG *)(entry + 2);
}

if (get_jmp_target(DOSBase, -48) != original_write_addr)
    printf("WARNING: dos.library Write() has been patched!\n");

Cross-Platform Comparison

Amiga Concept Win32 Equivalent Linux Equivalent Notes
SysBase at absolute $4 NtCurrentPeb() or fs:[0x30] No equivalent — kernel/user split blocks direct access Amiga's flat memory model makes this trivially accessible
Walking LibList EnumProcessModules() dl_iterate_phdr() Amiga's linked list is directly readable; Win32/Linux require API calls
TaskReady enumeration CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS) /proc/[pid]/stat Amiga lets you read the scheduler's run queue directly
MemList memory map VirtualQuery() /proc/self/maps Same result; Amiga reads kernel memory, Linux reads a pseudo-file
Forbid()/Permit() protection EnterCriticalSection() pthread_mutex_lock() Same purpose: prevent concurrent modification

FAQ

Is live probing safe on 68000 (no MMU)?

Yes — and it's even simpler. On 68000, there's no memory protection at all. Any address is readable. The risks are purely logical: reading a list while it's being modified. Forbid() is sufficient on all CPU models.

Can live probing crash the system?

Writing to the wrong address can corrupt OS structures and cause an immediate crash or silent data corruption. Reading is generally safe. The most common crash from reading is dereferencing a NULL pointer at the end of a list without checking ln_Succ first.


References