mirror of
https://github.com/alfishe/amiga-bootcamp.git
synced 2026-06-13 00:26:28 +00:00
More information added
This commit is contained in:
parent
a383d4c065
commit
05452c6c12
10 changed files with 1617 additions and 10 deletions
|
|
@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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 68000–68060 |
|
||||
| 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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 68000–68060, 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`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue