Files
nxnative-easy/dm/host/probe.c
T
seedds 79d82f7b64 Remove the D2R code and the legacy frame step
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).
2026-10-21 02:09:49 +08:00

163 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}