D2R (Diablo II Resurrected) has no profile and its code could not run here: the host-call svcs its cuts emitted only died, the touch-run-v4 rule set needed a missing dm/PRODUCT, and the host files its hooks called were never in this repository. Removed with it: - rewriter: D2R cuts, observation gates and parameter slots, the touch-run-v4 rule set and private gates, the boot list, the cut scope column and the KRES / KASPECT key slots (fixups.tsv keeps the four thread slots; no params.tsv any more); dm/tools/product.sh - host: D2R save edits, add-on content, render-scale controller, time() takeover and Terror Zone clock, crash decoding at D2R offsets, host-call registry and stubs, i18n, UI-screen, shell-app loader, zero-stripped archives, boot profiling hooks with no implementation; D2R headers, tests and scripts; dm/ios/Sources/dm_touch_controls; dm/third_party/libdeflate - graphics: runtime extrapolation, world freeze, iPad UI shift, shop drag image, brightness LUT, visual ruler, bloom / FXAA shader hashes, DM_GFXUSE, the eco-fps setting, the loading-screen gate; shader_map.py, nometal_run.py, nvn_slots.py, nvn_slots.tsv and the PC-texture samples The legacy frame step (frame_step legacy, DM_FRAME_STEP) is retired: the Breath of the Wild and Tears of the Kingdom stubs lose their mode test. Breath of the Wild's interval-controller cuts stay: the framework keeps the controller's interval while the game's VFR override holds. patch_nso.py output is byte-identical for all 15 games on this machine except the two frame-step stubs; all 18 profiles build; the host tests pass (their Makefile now builds the general tests only).
163 lines
11 KiB
C
163 lines
11 KiB
C
// Probes DM_PROBE: when the debugger needs a system authorization prompt on the Mac, use this to print registers and a backtrace at a guest address.
|
||
#include "dm.h"
|
||
#include <TargetConditionals.h>
|
||
#include <signal.h>
|
||
#include <dlfcn.h>
|
||
#include <sys/ucontext.h>
|
||
#include <sys/mman.h>
|
||
#include <libkern/OSCacheControl.h>
|
||
#include <mach/mach.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
void crash_set_trap_hook(int (*fn)(ucontext_t *uc));
|
||
#if !TARGET_OS_IPHONE
|
||
// Syntax module:offset[@x0 offset]. Put a brk at the guest address; on hit print x0..x3 (and the readable strings they point to) and a backtrace, then execute the original instruction from a trampoline slot and jump back.
|
||
// The trampoline borrows x16: place probes only at function entries or at points where x16 is dead; the original instruction must not be PC-relative (adr/adrp/b/bl/ldr literal/cbz...).
|
||
#define MAX_PROBE 16
|
||
typedef void (*probe_fn)(ucontext_t *uc);
|
||
static struct { uint64_t at; uint32_t *slot; unsigned hits; char name[40]; long dump; probe_fn fn; int freg; long foff; uint64_t fval; int hreg; } g_probe[MAX_PROBE];
|
||
|
||
// !xN: treat xN as a sead::ExpHeap and print its free and used blocks (SMBW layout: free list +0xb0, node offset +0xc4; used list +0xc8, node offset +0xdc; block size at block +0x18)
|
||
static int rd64(uint64_t a, uint64_t *v) { vm_size_t g = 0; return !vm_read_overwrite(mach_task_self(), a, 8, (vm_address_t)v, &g) && g == 8; }
|
||
static void probe_heapwalk(uint64_t h) {
|
||
uint64_t start = 0, size = 0; rd64(h + 0x28, &start); rd64(h + 0x30, &size);
|
||
fprintf(stderr, "[dm] ExpHeap %#llx start %#llx size %#llx\n", (unsigned long long)h, (unsigned long long)start, (unsigned long long)size);
|
||
static const struct { int list, off; const char *nm; } L[2] = { { 0xb0, 0xc4, "free" }, { 0xc8, 0xdc, "used" } };
|
||
for (int k = 0; k < 2; k++) {
|
||
uint64_t offw = 0, node = 0, sum = 0; int n = 0; rd64(h + L[k].off, &offw); int32_t off = (int32_t)offw;
|
||
rd64(h + L[k].list + 8, &node);
|
||
fprintf(stderr, "[dm] %s:", L[k].nm);
|
||
while (node && node != h + L[k].list && n < 2000) {
|
||
uint64_t blk = node - off, bsz = 0, w2 = 0; rd64(blk + 0x18, &bsz); rd64(blk + 0x10, &w2);
|
||
fprintf(stderr, "%s%llx+%llx", n % 8 ? " " : "\n[dm] ", (unsigned long long)(blk - start), (unsigned long long)bsz);
|
||
sum += bsz; n++;
|
||
if (!rd64(node + 8, &node)) break;
|
||
}
|
||
fprintf(stderr, "\n[dm] %s %d blocks %#llx bytes\n", L[k].nm, n, (unsigned long long)sum);
|
||
}
|
||
} // dump: on hit print 16 bytes at [x0+dump] (-1 = do not print); fn: dedicated handler (replaces the default print)
|
||
static int g_nprobe;
|
||
static void probe_str(uint64_t p) {
|
||
char buf[80]; vm_size_t got = 0;
|
||
if (p < 0x1000 || vm_read_overwrite(mach_task_self(), p, sizeof buf - 1, (vm_address_t)buf, &got) || !got) return;
|
||
buf[got] = 0; int n = 0; while (buf[n] >= 0x20 && buf[n] < 0x7f) n++;
|
||
if (n >= 3 && (buf[n] == 0 || n == (int)got)) { fprintf(stderr, " \"%.*s\"", n, buf); return; }
|
||
// Game string object {char* pointer, u64 length, u64 capacity|top bit} (the path argument of 0xdf67f0 FJSONUtils has exactly this layout)
|
||
if (got >= 24) {
|
||
uint64_t ptr, len; memcpy(&ptr, buf, 8); memcpy(&len, buf + 8, 8);
|
||
char t[96]; got = 0;
|
||
if (len >= 3 && len < sizeof t && !vm_read_overwrite(mach_task_self(), ptr, len, (vm_address_t)t, &got) && got == len) {
|
||
int k = 0; while (k < (int)len && t[k] >= 0x20 && t[k] < 0x7f) k++;
|
||
if (k == (int)len) fprintf(stderr, " S\"%.*s\"", (int)len, t);
|
||
}
|
||
}
|
||
}
|
||
// DM_PROBE_DUMP=module:offset:length[:file]: on the first hit of any probe, write that range of guest memory to a file (default run/platform/probe_dump.bin)
|
||
static void probe_dump_once(void) {
|
||
static int done; const char *e = getenv("DM_PROBE_DUMP"); if (done || !e) return; done = 1;
|
||
char spec[512]; strlcpy(spec, e, sizeof spec);
|
||
char *mod = strtok(spec, ":"), *o = strtok(NULL, ":"), *n = strtok(NULL, ":"), *f = strtok(NULL, ":");
|
||
module_t *m = NULL; for (int i = 0; i < g_nmod; i++) if (mod && !strcmp(g_mod[i].name, mod)) m = &g_mod[i];
|
||
if (!m || !o || !n) { LOG("DM_PROBE_DUMP format: module:offset:length[:file]"); return; }
|
||
char def[512]; snprintf(def, sizeof def, "%s%s/run/platform/probe_dump.bin", getenv("DM_ROOT") ?: getenv("HOME"), getenv("DM_ROOT") ? "" : "/dm-work");
|
||
FILE *fp = fopen(f ? f : def, "wb"); if (!fp) return;
|
||
fwrite(m->base + strtoull(o, 0, 16), 1, strtoull(n, 0, 16), fp); fclose(fp);
|
||
LOG("probe dump %s+%s length %s → %s", mod, o, n, f ? f : def);
|
||
}
|
||
static int probe_hit(ucontext_t *uc) { // Registered by probe_install with the SIGTRAP branch of crash.c
|
||
uint64_t pc = uc->uc_mcontext->__ss.__pc;
|
||
for (int i = 0; i < g_nprobe; i++) if (g_probe[i].at == pc) {
|
||
probe_dump_once();
|
||
if (g_probe[i].fn) { g_probe[i].hits++; g_probe[i].fn(uc); uc->uc_mcontext->__ss.__pc = (uint64_t)g_probe[i].slot; return 1; }
|
||
if (g_probe[i].freg >= 0) { // ?xN=value / ?[xN+offset]=value: pass through silently when it does not match
|
||
uint64_t v = uc->uc_mcontext->__ss.__x[g_probe[i].freg]; vm_size_t g = 0;
|
||
if (g_probe[i].foff >= 0 && vm_read_overwrite(mach_task_self(), v + g_probe[i].foff, 8, (vm_address_t)&v, &g)) v = ~g_probe[i].fval;
|
||
if (v != g_probe[i].fval) { uc->uc_mcontext->__ss.__pc = (uint64_t)g_probe[i].slot; return 1; }
|
||
}
|
||
{ extern uint64_t g_t0_ns; uint64_t now_ns(void); uint32_t thr_cur_handle(void); fprintf(stderr, "[dm] PROBE %s #%u [%.4fs] T=%x", g_probe[i].name, ++g_probe[i].hits, (now_ns() - g_t0_ns) / 1e9, thr_cur_handle()); }
|
||
for (int r = 0; r < 8; r++) { uint64_t v = uc->uc_mcontext->__ss.__x[r]; fprintf(stderr, " x%d=%#llx", r, v); probe_str(v); }
|
||
for (int r = 19; r < 24; r++) fprintf(stderr, " x%d=%#llx", r, uc->uc_mcontext->__ss.__x[r]);
|
||
if (getenv("DM_PROBE_ALL")) for (int r = 9; r < 19; r++) fprintf(stderr, " x%d=%#llx", r, uc->uc_mcontext->__ss.__x[r]); // also print x9–x18
|
||
fprintf(stderr, " lr=%#llx", uc->uc_mcontext->__ss.__lr);
|
||
{ uint64_t x8 = uc->uc_mcontext->__ss.__x[8]; Dl_info di; fprintf(stderr, " x8=%#llx", x8); // x8: indirect call targets are often here (probe placed before blr x8)
|
||
if (dladdr((void *)x8, &di) && di.dli_sname) fprintf(stderr, " (%s+%#lx)", di.dli_sname, (unsigned long)(x8 - (uint64_t)di.dli_saddr)); }
|
||
if (g_probe[i].dump >= 0) {
|
||
uint8_t b[16]; vm_size_t got = 0;
|
||
int dr = (g_probe[i].dump >> 25) & 31; // @xN+offset: based on xN
|
||
uint64_t at = uc->uc_mcontext->__ss.__x[dr] + (g_probe[i].dump & 0xffffff);
|
||
if (g_probe[i].dump & 0x1000000) { uint64_t p = 0; vm_size_t g = 0; if (!vm_read_overwrite(mach_task_self(), at, 8, (vm_address_t)&p, &g)) at = p; else at = 0; } // @*offset: dereference first, then dump
|
||
if (at && !vm_read_overwrite(mach_task_self(), at, 16, (vm_address_t)b, &got) && got == 16) {
|
||
fprintf(stderr, " [%sx%d+%#lx]=", (g_probe[i].dump & 0x1000000) ? "*" : "", dr, g_probe[i].dump & 0xffffff); for (int k = 0; k < 16; k++) fprintf(stderr, "%02x", b[k]); }
|
||
}
|
||
fputc('\n', stderr);
|
||
{ static long xd = -2; if (xd == -2) { const char *e = getenv("DM_PROBE_X2"); xd = e ? strtol(e, 0, 16) : -1; } // DM_PROBE_X2=bytes: also print the memory x2 points to (to read object fields)
|
||
if (xd > 0) { uint64_t at = uc->uc_mcontext->__ss.__x[2]; uint8_t b[512]; vm_size_t got = 0; long n = xd > 512 ? 512 : xd;
|
||
if (!vm_read_overwrite(mach_task_self(), at, n, (vm_address_t)b, &got)) { fprintf(stderr, "[dm] [x2]:"); for (long k = 0; k < (long)got; k += 8) { uint64_t v; memcpy(&v, b + k, 8); fprintf(stderr, "%s%llx", k % 32 ? " " : "\n[dm] ", (unsigned long long)v); } fputc('\n', stderr); } } }
|
||
if (g_probe[i].hreg >= 0) probe_heapwalk(uc->uc_mcontext->__ss.__x[g_probe[i].hreg]);
|
||
dm_backtrace(uc->uc_mcontext->__ss.__fp);
|
||
uc->uc_mcontext->__ss.__pc = (uint64_t)g_probe[i].slot;
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
static uint32_t *g_slots; static int g_slots_sealed;
|
||
static void probe_add(module_t *m, uint64_t off, long dump, probe_fn fn) {
|
||
if (g_nprobe >= MAX_PROBE) die("too many probes");
|
||
if (!g_slots) g_slots = mmap(NULL, 0x4000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||
uint64_t at = (uint64_t)m->base + off;
|
||
uint32_t *w = (uint32_t *)at, *sl = g_slots + g_nprobe * 8;
|
||
sl[0] = *w; // Original instruction
|
||
sl[1] = 0x58000050; // ldr x16, #8
|
||
sl[2] = 0xD61F0200; // br x16
|
||
*(uint64_t *)(sl + 3) = at + 4;
|
||
uint64_t pg = at & ~0x3FFFull;
|
||
if (mprotect((void *)pg, 0x4000, PROT_READ | PROT_WRITE)) die("probe mprotect");
|
||
*w = 0xD43E0000; // brk #0xf000
|
||
mprotect((void *)pg, 0x4000, PROT_READ | PROT_EXEC);
|
||
sys_icache_invalidate(w, 4);
|
||
g_probe[g_nprobe].at = at; g_probe[g_nprobe].slot = sl; g_probe[g_nprobe].dump = dump; g_probe[g_nprobe].fn = fn; g_probe[g_nprobe].freg = -1; g_probe[g_nprobe].hreg = -1;
|
||
snprintf(g_probe[g_nprobe].name, sizeof g_probe[0].name, "%s+%#llx", m->name, off);
|
||
LOG("probe %s (original instruction %08x)", g_probe[g_nprobe].name, sl[0]);
|
||
g_nprobe++;
|
||
}
|
||
|
||
#endif
|
||
|
||
void probe_install(void) {
|
||
#if !TARGET_OS_IPHONE
|
||
crash_set_trap_hook(probe_hit);
|
||
const char *e = getenv("DM_PROBE");
|
||
if (e) {
|
||
char spec[512]; strlcpy(spec, e, sizeof spec);
|
||
for (char *t = strtok(spec, ","); t; t = strtok(NULL, ",")) {
|
||
char *c = strchr(t, ':'); if (!c) die("DM_PROBE format: module:offset");
|
||
*c = 0; module_t *m = NULL;
|
||
for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, t)) m = &g_mod[i];
|
||
if (!m) die("DM_PROBE: no such module %s", t);
|
||
char *at_ = strchr(c + 1, '@');
|
||
long dump = -1;
|
||
if (at_) { // @offset / @*offset / @xN+offset / @*xN+offset
|
||
const char *q = at_ + 1; long deref = 0, reg = 0;
|
||
if (*q == '*') { deref = 0x1000000; q++; }
|
||
if (*q == 'x') { char *e2; reg = strtol(q + 1, &e2, 10) & 31; q = *e2 == '+' ? e2 + 1 : e2; }
|
||
dump = deref | (reg << 25) | (strtol(q, 0, 16) & 0xffffff);
|
||
}
|
||
char *qm = strchr(c + 1, '?'), *bang = strchr(c + 1, '!');
|
||
probe_add(m, strtoull(c + 1, 0, 16), dump, NULL);
|
||
if (bang && bang[1] == 'x') g_probe[g_nprobe - 1].hreg = (int)strtol(bang + 2, 0, 10) & 31;
|
||
if (qm) { // Filter: ?x0=706c400000 or ?[x0+38]=781548 (hex)
|
||
const char *q = qm + 1; int br = *q == '[';
|
||
if (br) q++;
|
||
if (*q != 'x') die("DM_PROBE filter format: ?xN=value or ?[xN+offset]=value");
|
||
char *e2; g_probe[g_nprobe - 1].freg = (int)strtol(q + 1, &e2, 10) & 31; g_probe[g_nprobe - 1].foff = -1;
|
||
if (br) { g_probe[g_nprobe - 1].foff = *e2 == '+' ? strtol(e2 + 1, &e2, 16) : 0; if (*e2 == ']') e2++; }
|
||
if (*e2 != '=') die("DM_PROBE filter missing =");
|
||
g_probe[g_nprobe - 1].fval = strtoull(e2 + 1, 0, 16);
|
||
}
|
||
}
|
||
}
|
||
if (g_slots && !g_slots_sealed) { mprotect(g_slots, 0x4000, PROT_READ | PROT_EXEC); sys_icache_invalidate(g_slots, 0x4000); g_slots_sealed = 1; }
|
||
#endif
|
||
}
|