More information added

This commit is contained in:
Ilia Sharin 2026-04-27 18:34:07 -04:00
parent a383d4c065
commit 05452c6c12
10 changed files with 1617 additions and 10 deletions

View file

@ -98,6 +98,111 @@ Both tools output via `kprintf` to serial port (115200 8N1). Capture on host:
screen /dev/cu.usbserial-XXXX 115200
# or
minicom -D /dev/cu.usbserial-XXXX -b 115200
---
## Decision Guide — Enforcer vs MungWall vs Manual Debugging
| Scenario | Use | Why |
|---|---|---|
| Random Guru Meditation, unknown cause | Both | Enforcer catches the access violation; MungWall catches the corruption that caused it |
| Reproducible crash at known address | Enforcer first | Identifies the exact instruction and register state at the crash |
| Heap data corruption (silent, no crash) | MungWall | Guards catch overwrites on FreeMem — may be the only detection |
| Use-after-free bugs | Both | MungWall poisons freed blocks; Enforcer traps reads from unmapped freed pages |
| 68000 (no MMU) | MungWall only | Enforcer requires 68020+ MMU for hardware trapping |
| MiSTer FPGA / emulation | Both (if MMU implemented) | Verify MMU implementation supports Enforcer's page-level trapping |
---
## Named Antipatterns
### 1. "The Ignored Hit"
**What it looks like** — seeing an Enforcer hit, noting the PC, but dismissing it because "the program still runs":
```
ENFORCER HIT: READ-WORD FROM $00000012
PC: $0023AB12
```
**Why it fails:** Enforcer catches the violation and *allows the program to continue* by emulating the access or returning dummy data. The crash may not happen immediately — but the corruption is real. A null pointer read that "works" because Enforcer returned `$00000000` may cause a crash 10 minutes later when that zero propagates to a pointer dereference.
**Correct:** Every Enforcer hit is a real bug. Fix them all, even if the program appears to survive.
### 2. "The Missing MungWall on Exit"
**What it looks like** — running MungWall, seeing clean output during the program, but not checking on program exit:
```
run mungwall
myapp
; No MungWall output during run — looks clean!
; But on exit, all allocations are freed — that's when guards are checked
```
**Why it fails:** MungWall validates guards at `FreeMem()` time, not at corruption time. If the program corrupts a buffer, the corruption is detected only when that buffer is freed — typically at program exit. If you don't capture exit-time output, you miss the report.
**Correct:** Always capture serial output until the program fully exits and the CLI prompt returns.
---
## Use-Case Cookbook
### Track Down a Heap Overflow
1. `run mungwall` — intercepts AllocMem/FreeMem
2. `run enforcer QUIET LOG enforcer.log` — catches illegal accesses
3. Launch the program
4. Reproduce the crash
5. Check `enforcer.log` and serial output
6. If MungWall reports "Trailer guard CORRUPTED at +132":
- The allocation at the reported address + the offset is the corruption site
- Walk backward from `FreeMem` PC to find the caller that corrupted it
- Set a **hardware write watchpoint** on the guard address using Enforcer's MMU capability
### Verify All Allocations Are Freed (Leak Detection)
MungWall can report unfreed allocations at exit:
```bash
run mungwall LEAKCHECK
myapp
# Output on exit:
# MUNGWALL: 3 blocks still allocated (48 bytes total):
# $001A2000: size=16, alloc PC=$0023BC44
# $001A3000: size=16, alloc PC=$0023BC44
# $001A4000: size=16, alloc PC=$0023BD12
```
Cross-reference the alloc PCs with IDA to find the leaking code.
---
## Cross-Platform Comparison
| Amiga Concept | Modern Equivalent | Notes |
|---|---|---|
| Enforcer (MMU trap) | AddressSanitizer (ASan) | Same concept: trap illegal accesses, report PC + registers |
| MungWall (heap guards) | `mallocscribble` / `MALLOC_CHECK_` | Same: canary values before/after each allocation |
| MungWall use-after-free | ASan quarantine / `MALLOC_PERTURB_` | Same: poison freed memory, trap on re-read |
| Combined Enforcer + MungWall | `-fsanitize=address` (GCC/Clang) | ASan combines both approaches in one tool |
| Serial port output | `ASAN_OPTIONS=log_path=asan.log` | Same: output goes to a separate channel to survive crashes |
---
## FAQ
### Does Enforcer work on 68000 (A500/A600/A2000)?
Enforcer can work in "software mode" on 68000 by patching the bus error exception vector and using `trap #N` for software breakpoints. However, it cannot detect arbitrary illegal memory accesses without MMU hardware — the 68000 has no page tables to mark addresses as inaccessible. Use MungWall alone on 68000 systems.
### Why does Enforcer hit on perfectly valid code?
False positives are rare but possible: (1) self-modifying code that writes to code segments, (2) ROM shadowing — writing to what appears to be ROM but is actually a RAM mirror, (3) memory-mapped I/O regions that Enforcer doesn't know about (custom expansion hardware).
---
## References
```
---

View file

@ -4,7 +4,28 @@
## Overview
Live memory probing on a running Amiga means directly reading exec structures — `SysBase`, `LibList`, `TaskReady`, `MemList` — to observe system state without a traditional debugger.
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.
```mermaid
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
```
---
---
@ -124,6 +145,116 @@ Permit();
---
## 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()`:
```c
// 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:**
```c
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:
```c
// 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
```c
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
```c
/* 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
- NDK39: `exec/execbase.h`, `exec/memory.h`, `exec/tasks.h`

View file

@ -111,6 +111,66 @@ MiSTer FPGA: the UART bridge is exposed on the MiSTer IO board or via the DE10-N
---
## Decision Guide — Which Debug Output Method?
| Method | Works During... | Requires | Throughput | Use Case |
|---|---|---|---|---|
| `kprintf()` | ROM init, crashes | Debug ROM or Kickstart 1.3 | Low (polled) | Kernel-level debugging |
| `RawDoFmt + RawPutChar` | Any time after exec init | exec.library only | Medium | Universal: all Kickstart versions |
| Direct `SERDAT` write | Anytime, even without OS | Nothing — bare metal | High (custom batching) | Crash handler, bootloader |
| `dprintf` (debug.lib) | Application runtime | SAS/C debug.lib | Medium | Application-level tracing |
| `serial.device` | Full OS running | serial.device open | High (interrupt-driven) | High-volume data transfer |
---
## Named Antipatterns
### 1. "The Deadly Debug Printf"
**What it looks like** — calling `printf()` or `VPrintf()` from inside a `Forbid()`/`Disable()` block:
```c
Forbid();
printf("Processing item %d\n", i); // BROKEN — calls dos.library!
Permit();
```
**Why it fails:** `printf()` goes through `dos.library Write()` which may call `Wait()` for buffered I/O. Inside `Forbid()`, task switching is blocked — `Wait()` never returns → system deadlock. Inside `Disable()`, even worse — interrupts are off, so the system clock stops and the serial device can't transmit.
**Correct:** Use `kprintf()` or `RawDoFmt + RawPutChar` inside Forbid/Disable — both bypass dos.library entirely.
### 2. "The Baud Rate Mismatch"
**What it looks like** — the Amiga outputs at 9600 baud but the host is configured for 115200:
```bash
# Host configured for 115200
screen /dev/cu.usbserial 115200
# Output: ¥φΩ≡ƒ╤ ╚α≡α≤φσ≡ ╔╞╒ ... (garbage)
```
**Why it fails:** The Amiga's default `SERPER` value after reset is for 9600 baud (on NTSC; PAL may differ). The host-side baud rate MUST match exactly. A single bit error in the start bit cascades into every subsequent bit being wrong.
**Correct:** Set `SERPER` to a known value before output, or cycle through common baud rates on the host side (9600, 19200, 38400, 57600, 115200) until text becomes readable.
---
## FAQ
### Why doesn't kprintf work on my Kickstart 3.1 ROM?
`kprintf()` was removed from release Kickstart ROMs starting with 2.04. It only exists in debug/test ROMs and Kickstart 1.3. For 2.0+, use `RawDoFmt + RawPutChar` or the direct hardware approach.
### Can I use the serial port without a null-modem cable?
No. The Amiga's serial port is RS-232 level (not TTL). You need a null-modem cable or a USB-serial adapter with RS-232 voltage levels. Direct connection to a USB UART (3.3V TTL) will damage the hardware.
---
## References
---
## References
- NDK39: `exec/execbase.h``RawDoFmt`, `RawPutChar` LVOs

View file

@ -4,7 +4,32 @@
## Overview
`SetFunction()` is the official AmigaOS mechanism for **patching a library's JMP table** at runtime. It installs a custom function at a given LVO, replacing the original, and returns the old function pointer so a trampoline can be constructed.
You want to know every file an application opens. Or every byte it writes. Or every memory allocation it makes — with sizes, flags, and call stacks. You could patch the binary. Or you could use the operating system's own hooking mechanism: **`SetFunction()`**.
`SetFunction()` is AmigaOS's official API for **replacing a library's JMP table entry at runtime.** It atomically swaps the target address of a specific LVO, returning the original pointer so you can construct a trampoline. Every call through that LVO — from every task, in every process — now routes through your code. This is the foundation of Amiga reverse engineering tooling: file system monitors, API tracers, memory debuggers, and anti-piracy checks all begin with `SetFunction()`.
```mermaid
graph LR
subgraph "Before SetFunction"
APP1["App calls<br/>JSR -48(A6)"]
JMP["JMP table[-48]<br/>→ original Write"]
ORIG["dos.library<br/>Write_impl()"]
end
subgraph "After SetFunction"
APP2["App calls<br/>JSR -48(A6)"]
JMP2["JMP table[-48]<br/>→ my_write_hook"]
HOOK["my_write_hook()<br/>log, modify, block"]
TRAMP["Trampoline →<br/>original Write"]
end
APP1 --> JMP
JMP --> ORIG
APP2 --> JMP2
JMP2 --> HOOK
HOOK --> TRAMP
TRAMP --> ORIG
```
---
---
@ -122,6 +147,174 @@ atexit(remove_hook);
---
## Decision Guide — SetFunction vs Alternatives
```mermaid
graph TD
Q["Need to intercept<br/>library calls?"]
Q -->|"System-wide,<br/>all tasks"| SF["Use SetFunction()"]
Q -->|"Single task only"| ALT1["Consider patching<br/>the task's A6/A4"]
Q -->|"At load time,<br/>before execution"| ALT2["Binary patch<br/>or HUNK relocation"]
SF -->|"Need to call original?"| TRAMP["Write trampoline<br/>save orig pointer"]
SF -->|"Block/replace only"| BLOCK["Don't save orig<br/>simpler, no trampoline"]
```
| Approach | Scope | Invasiveness | Use Case |
|---|---|---|---|
| **SetFunction()** | System-wide | Low (official API) | API tracing, memory debugging, anti-piracy |
| **Direct JMP table patch** | System-wide | Medium (bypasses API) | Pre-OS 2.0 compatibility |
| **Task A6 replacement** | Single task | Medium | Per-application sandboxing |
| **Binary patch (file)** | Single binary | High (modifies disk) | Permanent behavior change, crack intros |
---
## Named Antipatterns
### 1. "The Leaky Hook"
**What it looks like** — installing a hook but never removing it:
```c
void setup(void) {
Forbid();
orig = SetFunction(DOSBase, -48, my_write);
Permit();
// No atexit() cleanup — hook lives forever
}
```
**Why it fails:** When the hooking program exits, `my_write` is unloaded from memory. But the JMP table still points to it. The next task that calls `Write()` jumps into freed memory → Guru Meditation.
**Correct:** Always register cleanup:
```c
void cleanup(void) {
Forbid();
SetFunction(DOSBase, -48, orig); // restore original
Permit();
}
// In main():
atexit(cleanup);
```
### 2. "The Forbid-Free Patch"
**What it looks like** — calling `SetFunction()` without `Forbid()`:
```c
// BROKEN — task switch during SetFunction may corrupt list
orig = SetFunction(DOSBase, -48, my_write);
```
**Why it fails:** `SetFunction()` modifies the library's `lib_OpenCnt` and may trigger expunge logic. If a task switch occurs during this modification, another task may see an inconsistent state. The result: corrupted open counts, premature expunge, or lost patches.
**Correct:** Always wrap in `Forbid()`/`Permit()`.
### 3. "The Register Stomper"
**What it looks like** — a hook that corrupts registers before calling the original:
```asm
_my_write:
MOVEM.L D0-D2/A0-A1, -(SP) ; save only D0-D2/A0-A1
JSR _log_args
MOVEM.L (SP)+, D0-D2/A0-A1
MOVEA.L _orig_write, A0
JMP (A0) ; D3-D7/A2-A6 may contain garbage!
```
**Why it fails:** The original `Write()` expects `D1`=file, `D2`=buffer, `D3`=length. If your logging code modified D3 and you didn't save/restore it, the original function sees a corrupted length — potentially writing gigabytes or zero bytes. Even worse: the caller may rely on other registers (D4-D7, A2-A5) being preserved per the AmigaOS ABI, and your hook trashed them.
**Correct:** Save and restore ALL registers the original function might read or the caller expects preserved. The safest approach is `MOVEM.L D0-D7/A0-A6`.
---
## Use-Case Cookbook
### File Access Tracer — Log Every Open
```c
static APTR orig_Open;
LONG __asm my_Open(register __d1 STRPTR name,
register __d2 LONG mode) {
LONG result = ((LONG(*)(STRPTR,LONG))orig_Open)(name, mode);
if (result) {
kprintf("Open: %s mode=%ld → handle=%ld\n", name, mode, result);
}
return result;
}
void install_file_tracer(void) {
Forbid();
orig_Open = SetFunction(DOSBase, -30, my_Open);
Permit();
}
```
### Write Blocker — Prevent All Disk Writes
```c
static APTR orig_Write;
static BOOL write_blocked = TRUE;
LONG __asm my_Write(register __d1 BPTR fh,
register __d2 APTR buf,
register __d3 LONG len) {
if (write_blocked) {
return 0; // pretend success, write nothing
}
return ((LONG(*)(BPTR,APTR,LONG))orig_Write)(fh, buf, len);
}
```
### Detect SetFunction Itself Being Called (Anti-Anti-Debug)
Some software detects patching by checking if `SetFunction` returns the expected original address. Counter-patch by hooking `SetFunction` itself:
```c
static APTR orig_SetFunction;
APTR __asm my_SetFunction(register __a1 struct Library *lib,
register __a0 LONG lvo,
register __d0 APTR newFunc) {
if (lib == DOSBase && lvo == -48) {
return orig_Write; // lie: return our hook as "original"
}
return ((APTR(*)(struct Library*,LONG,APTR))orig_SetFunction)(lib, lvo, newFunc);
}
```
---
## Cross-Platform Comparison
| Amiga Concept | Win32 Equivalent | Linux Equivalent | Notes |
|---|---|---|---|
| `SetFunction()` | `DetourAttach()` (Microsoft Detours) | `LD_PRELOAD` + `dlsym(RTLD_NEXT)` | Same idea: intercept library calls transparently |
| JMP table modification | IAT hooking | PLT/GOT hooking | Amiga's JMP table is simpler — one 6-byte write vs multi-level indirection |
| Trampoline pattern | Detour trampoline | `dlsym(RTLD_NEXT, "write")` | Same: call original after instrumentation |
| `Forbid()`/`Permit()` | `SuspendThread` / `ResumeThread` (crude) | Signal blocking (crude) | Amiga's task-level atomicity is unique — no per-thread suspend needed |
| System-wide by default | Per-process by default | Per-process by default | Amiga's flat address space means one hook covers everything |
---
## FAQ
### Does SetFunction work on all library types?
Yes — `SetFunction()` works on any library with a standard JMP table (exec, dos, graphics, intuition, third-party). It does NOT work on ROM-based resident modules that use a different dispatch mechanism (some Kickstart modules).
### Can multiple hooks coexist on the same function?
Yes — in a chain. Each hook saves the "original" pointer (which may itself be a previous hook's trampoline). Removal must happen in reverse order: last hooked = first removed. Removing hooks out of order breaks the chain.
### Is SetFunction safe across CPU architectures?
On 6800068060, yes. However, 68040+ systems with data cache enabled may cache the old JMP table entry. Always call `CacheClearU()` after `SetFunction()` on 040/060 to flush the data cache and ensure the new target address is visible to the instruction fetch unit.
---
## References
- NDK39: `exec/execbase.h`