Files
nxnative-easy/dm/host/loader.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

435 lines
29 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.
// Multi-module loading (playing the role of Horizon's rtld): mapping, claiming thread slots, gate registration, cross-module relocation, take-over by name, segment permissions.
// The code area (original text and patch area) is set to read-only+execute as soon as it is copied in, and from then on the loader only writes data: on iOS these two blocks must be embedded as is as signed code segments.
#include "dm.h"
#include <TargetConditionals.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <mach-o/getsect.h>
#include <mach-o/ldsyms.h>
#include "hle.h"
module_t g_mod[MAX_MOD];
int g_nmod;
pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate;
enum { KEY_K18, KEY_KTLS, KEY_KTLR, KEY_KGATE, KEY_N };
uint64_t resolve_import(module_t *m, const char *name);
int hle_implements(const char *name);
void gate_register(uint64_t ret_addr, const char *kind, int arg, uint64_t orig);
static pthread_key_t key_value(unsigned i) {
switch (i) {
case KEY_K18: return g_key18; case KEY_KTLS: return g_keytls; case KEY_KTLR: return g_keytlr;
default: return g_keygate;
}
}
static void create_keys(void) {
if (pthread_key_create(&g_key18, NULL) || pthread_key_create(&g_keytls, NULL) ||
pthread_key_create(&g_keytlr, NULL) || pthread_key_create(&g_keygate, NULL)) die("cannot create the host key");
LOG("key numbers K18=%lu KTLS=%lu KTLR=%lu KGATE=%lu", g_key18, g_keytls, g_keytlr, g_keygate);
}
// K slot check: the pthread TSD layout is fixed, slot = tpidrro_el0 + 8*key
static void tsd_check(void) {
pthread_setspecific(g_key18, (void *)0x1234);
uint64_t base; __asm__ volatile("mrs %0, tpidrro_el0" : "=r"(base));
uint64_t off = *(uint64_t *)(g_mod[0].base + g_mod[0].key_page_off + g_mod[0].key_idx[KEY_K18] * 8);
if (((uint64_t *)(base & ~7ull))[off / 8] != 0x1234) die("TSD layout mismatch");
pthread_setspecific(g_key18, 0);
}
static void build_symhash(module_t *m);
static void parse_dynamic(module_t *m) {
uint32_t mod0 = *(uint32_t *)(m->base + 4);
if (memcmp(m->base + mod0, "MOD0", 4)) die("%s: no MOD0", m->name);
m->dyn = (const Elf64_Dyn *)(m->base + mod0 + *(int32_t *)(m->base + mod0 + 4));
for (const Elf64_Dyn *d = m->dyn; d->d_tag; d++) switch (d->d_tag) {
case DT_SYMTAB: m->symtab = (const Elf64_Sym *)(m->base + d->d_un.d_ptr); break;
case DT_STRTAB: m->strtab = (const char *)(m->base + d->d_un.d_ptr); break;
case DT_INIT: m->init = d->d_un.d_ptr; break;
case DT_INIT_ARRAY: m->init_array = d->d_un.d_ptr; break;
case DT_INIT_ARRAYSZ: m->init_arraysz = d->d_un.d_val; break;
}
m->nsym = (unsigned)(((const uint8_t *)m->strtab - (const uint8_t *)m->symtab) / sizeof(Elf64_Sym));
build_symhash(m);
}
static void *module_sym_linear(module_t *m, const char *name) {
for (unsigned i = 1; i < m->nsym; i++) {
const Elf64_Sym *s = &m->symtab[i];
if (s->st_shndx && !strcmp(m->strtab + s->st_name, name)) return m->base + s->st_value;
}
return NULL;
}
// Symbol name hash table: inserted in ascending index order, not overwritten if the same name is already there ⇒ lookup results are identical, entry by entry, to a linear scan (the first defined symbol with that name).
// Of the 91–99 ms of ① host loading, relocation + forced take-over took 77–86 ms, mainly because every by-name relocation scanned the symbol tables of two modules linearly
static uint32_t sym_hash(const char *s) { uint32_t h = 2166136261u; while (*s) h = (h ^ (uint8_t)*s++) * 16777619u; return h; }
static void build_symhash(module_t *m) {
unsigned cap = 16; while (cap < m->nsym * 2) cap <<= 1;
m->symh = calloc(cap, sizeof *m->symh); m->symh_mask = cap - 1;
for (unsigned i = 1; i < m->nsym; i++) {
const Elf64_Sym *s = &m->symtab[i]; if (!s->st_shndx) continue;
const char *nm = m->strtab + s->st_name;
for (unsigned j = sym_hash(nm) & m->symh_mask;; j = (j + 1) & m->symh_mask) {
if (!m->symh[j]) { m->symh[j] = i; break; }
if (!strcmp(m->strtab + m->symtab[m->symh[j]].st_name, nm)) break; // The same name already exists with a smaller index: keep that one
}
}
}
static unsigned long g_symchk_n, g_symchk_bad;
void *module_sym(module_t *m, const char *name) {
unsigned long nq; (void)nq;
if (!m->symh) return module_sym_linear(m, name);
void *r = NULL;
for (unsigned j = sym_hash(name) & m->symh_mask; m->symh[j]; j = (j + 1) & m->symh_mask) {
const Elf64_Sym *s = &m->symtab[m->symh[j]];
if (!strcmp(m->strtab + s->st_name, name)) { r = m->base + s->st_value; break; }
}
static int chk = -1; if (chk < 0) chk = getenv("DM_SYMCHECK") != NULL;
if (chk) { void *l = module_sym_linear(m, name); g_symchk_n++; nq = g_symchk_n;
if (l != r) { g_symchk_bad++; LOG("[RED] DM_SYMCHECK: %s in %s hash %p ≠ linear %p", name, m->name, r, l); } }
return r;
}
void *guest_sym(const char *name) {
for (int i = 0; i < g_nmod; i++) { void *p = module_sym(&g_mod[i], name); if (p) return p; }
return NULL;
}
// For modules with the patch area before the base (patch_off < 0) addresses in the patch area count too, as for modules whose patch area follows the image (crash reports, the GC pause's "in guest code" test)
module_t *module_of(const void *p) {
for (int i = 0; i < g_nmod; i++) {
const uint8_t *b = g_mod[i].base;
if ((const uint8_t *)p >= b && (const uint8_t *)p < b + g_mod[i].size) return &g_mod[i];
if (g_mod[i].patch_off < 0 && (const uint8_t *)p >= b + g_mod[i].patch_off && (const uint8_t *)p < b + g_mod[i].patch_off + g_mod[i].patch_len) return &g_mod[i];
}
return NULL;
}
// Cross-module symbol resolution: first look for a definition in the loaded modules, and only if not found hand over to HLE / stubs
static uint64_t resolve_sym(module_t *m, const char *name) {
for (int i = 0; i < g_nmod; i++) { void *p = module_sym(&g_mod[i], name); if (p) return (uint64_t)p; }
return resolve_import(m, name);
}
// Forced take-over table (hle.h HLE_FORCE, the __DATA,swforce section): a hit overrides the definition inside the module
static const hle_force_ent *g_force; static unsigned g_nforce; static unsigned *g_force_hits;
static void force_init(void) {
unsigned long sz; g_force = (const hle_force_ent *)getsectiondata(&_mh_execute_header, "__DATA", "swforce", &sz);
g_nforce = g_force ? (unsigned)(sz / sizeof *g_force) : 0;
g_force_hits = calloc(g_nforce + 1, sizeof *g_force_hits);
for (unsigned i = 0; i < g_nforce; i++)
for (unsigned j = i + 1; j < g_nforce; j++)
if (!strcmp(g_force[i].name, g_force[j].name)) die("duplicate entry in the forced takeover list: %s", g_force[i].name);
}
static void *force_lookup(const char *name) {
for (unsigned i = 0; i < g_nforce; i++)
if (!strcmp(g_force[i].name, name)) {
if (!g_force_hits[i]++ && g_flag.imports) LOG("force %s (%s)", name, g_force[i].why);
void *hlecount_wrap(const char *, const char *, void *); return hlecount_wrap("FORCE", name, g_force[i].fn); // A counting stub when DM_HLECOUNT is set
}
return NULL;
}
// Generic host: the list serves several games, so entries a game does not reference are only noted; with DM_FORCE_STRICT=1 it still fails loudly (for checking a mistyped list)
static void force_check(void) {
unsigned unused = 0; int strict = getenv("DM_FORCE_STRICT") != NULL;
for (unsigned i = 0; i < g_nforce; i++)
if (!g_force_hits[i]) { if (strict || g_flag.imports) LOG("forced takeover entry with no reference at all: %s", g_force[i].name); unused++; }
if (unused && strict) die("forced takeover list is wrong");
LOG("force: %u entries hit, %u not referenced by this game", g_nforce - unused, unused);
}
// Read the rewriter table header (segment boundaries, slot numbers); any row beyond the header means an old-format build output (code modified at load time), which is rejected
static void read_layout(module_t *m, const char *dir) {
char path[1024]; snprintf(path, sizeof path, "%s/%s.fixups.tsv", dir, m->name);
FILE *f = fopen(path, "r");
if (!f) die("open %s", path);
static const char *kn[KEY_N] = {"K18", "KTLS", "KTLR", "KGATE"};
char line[256], key[32]; unsigned long v; unsigned seen = 0;
while (fgets(line, sizeof line, f)) {
if (sscanf(line, "# %31s %lx", key, &v) != 2) die("%s: the output comes from an old rewriter; rewrite the modules again with dm/tools/patch_nso.py", path);
if (!strcmp(key, "PATCH_OFF")) m->patch_off = strtoll(line + 2 + strlen(key), NULL, 16); // may be negative (patch area before the base)
else if (!strcmp(key, "IMAGE_SIZE")) m->image_size = v;
else if (!strcmp(key, "FILE_IMAGE_LEN")) m->file_image_len = v;
else if (!strcmp(key, "KEY_PAGE_OFF")) m->key_page_off = v;
else if (!strcmp(key, "KEY_PAGE_SIZE")) m->key_page_size = v;
else if (!strcmp(key, "TEXT_END")) m->text_end = v;
else if (!strcmp(key, "RO_OFF")) m->ro_off = v;
else if (!strcmp(key, "DATA_OFF")) m->data_off = v;
else if (!strcmp(key, "FPS_STATE_OFF")) m->fps_state_off = v;
else if (!strcmp(key, "BUILD_ID")) sscanf(line, "# %*s %32s", m->build_id);
else for (int k = 0; k < KEY_N; k++) {
size_t n = strlen(kn[k]);
if (!strncmp(key, "KEY_", 4) && !strncmp(key + 4, kn[k], n) && !strcmp(key + 4 + n, "_IDX")) {
m->key_idx[k] = (unsigned)strtoul(strrchr(line, '\t') + 1, 0, 10); seen |= 1u << k;
}
}
}
fclose(f);
if (!m->patch_off || m->key_page_off % 0x4000 || m->key_page_size != 0x4000 || seen != (1u << KEY_N) - 1)
die("%s: the output comes from an old rewriter; rewrite the modules again with dm/tools/patch_nso.py", path);
if (m->patch_off < 0 && (!m->image_size || !m->file_image_len || m->file_image_len % 0x4000 || m->patch_off % 0x4000 || m->key_page_off + m->key_page_size > m->image_size))
die("%s: patch area before the base, but IMAGE_SIZE / FILE_IMAGE_LEN missing or wrong", path);
}
// Frame step state (the page after the key page, read-write; layout in patch_nso.py FRAME_STEP): the rewriter's stubs step the game by the host's whole vsyncs capped at this
// target, 0 = the game's own interval logic. The profile's fps_target, overridden by DM_FPS_TARGET (0, 30 or 60); the graphics layer decides the steps and paces presents
// (hle_nvn.c fstep_tick)
uint8_t *dm_fps_state;
static void install_fps_state(module_t *m) {
if (m->fps_state_off + 0x40 > m->size || m->fps_state_off < m->key_page_off + m->key_page_size) die("%s: frame step state page out of range", m->name);
const char *e = getenv("DM_FPS_TARGET"); int t = e ? atoi(e) : DM_GAME_FPS_TARGET;
if (t != 0 && t != 30 && t != 60) { LOG("[RED] DM_FPS_TARGET=%s: must be 0, 30 or 60; using the game's own frame step", e); t = 0; }
float f = (float)t; dm_fps_state = m->base + m->fps_state_off; memcpy(dm_fps_state, &f, sizeof f);
// Render scale of the 3D views (the stubs at Tears of the Kingdom's main+0xd5cc0, Breath of the Wild's main+0x1266880): 1.0 keeps the game's own size and dynamic resolution;
// another factor fixes the size at that fraction (DM_RENDER_SCALE, 0.5 to 1.0). Independent of the frame step: written at target 0 too
const char *rs = getenv("DM_RENDER_SCALE"); float sc = 1.0f;
if (rs) { char *end; sc = strtof(rs, &end); if (end == rs || *end || !(sc >= 0.5f && sc <= 1.0f)) { LOG("[RED] DM_RENDER_SCALE=%s: must be 0.5 to 1.0; keeping the game's size", rs); sc = 1.0f; } else LOG("%s: 3D views rendered at %.2f of the game's size (DM_RENDER_SCALE)", m->name, sc); }
memcpy(dm_fps_state + 0x10, &sc, sizeof sc);
char cap[32]; snprintf(cap, sizeof cap, ", capped at %d fps", t);
LOG("%s: frame step %s%s (%s)", m->name, !t ? "as the game sets it" : "in whole vsyncs decided by the host", t ? cap : "", e ? "DM_FPS_TARGET" : "game profile fps_target");
}
static void install_key_page(module_t *m) {
if (m->key_page_off + m->key_page_size > m->size) die("%s: key page out of range", m->name);
uint64_t *page = (uint64_t *)(m->base + m->key_page_off);
memset(page, 0, m->key_page_size);
for (unsigned i = 0; i < KEY_N; i++) page[m->key_idx[i]] = (uint64_t)key_value(i) * 8;
for (int k = 0; k < KEY_N; k++) if (!page[m->key_idx[k]]) die("%s: key page entry %d is 0 (host key not created) -- the game would read TSD slot 0", m->name, k);
if (mprotect(m->base + m->key_page_off, m->key_page_size, PROT_READ)) die("%s key page mprotect", m->name);
if (m->fps_state_off) install_fps_state(m);
}
static void register_gates(module_t *m, const char *dir) {
char path[1024], line[256]; snprintf(path, sizeof path, "%s/%s.gates.tsv", dir, m->name);
FILE *f = fopen(path, "r");
int ng = 0;
if (f) {
unsigned long ret, orig; char kind[16]; int arg;
while (fgets(line, sizeof line, f))
if (sscanf(line, "%lx %15s %d %lx", &ret, kind, &arg, &orig) == 4) { gate_register((uint64_t)m->base + ret, kind, arg, orig); ng++; }
fclose(f);
}
LOG("%s: gates %d patch_off %#llx keys %lu/%lu/%lu/%lu", m->name, ng, m->patch_off, g_key18, g_keytls, g_keytlr, g_keygate);
}
// DT_RELR (from SDK 20; all three modules of The Legend of Zelda: Breath of the Wild 1.9.0 have it): a compact table of relative relocations with the addend in the slot (*slot += base).
// Each 8-byte entry: even = a new start offset (relocate it, the pointer moves to the next slot); odd = a bitmap whose bits 1-63 stand for the 63 slots after the pointer, which then moves on by 63 slots.
// Without it main's GOT and vtables hold module offsets (main's RELA keeps only symbol relocations)
static unsigned relr_apply(module_t *m, uint64_t relr, uint64_t relrsz, uint64_t delta) {
const uint64_t *e = (const uint64_t *)(m->base + relr); uint64_t *where = NULL; unsigned n = 0;
for (uint64_t i = 0; i < relrsz / 8; i++) {
uint64_t v = e[i];
if (!(v & 1)) { where = (uint64_t *)(m->base + v); *where++ += delta; n++; continue; }
if (!where) die("%s: DT_RELR bitmap entry with no start before it", m->name);
for (unsigned b = 0; (v >>= 1); b++) if (v & 1) { where[b] += delta; n++; }
where += 63;
}
return n;
}
static void relocate(module_t *m) {
uint64_t rela = 0, relasz = 0, jmprel = 0, pltsz = 0, relr = 0, relrsz = 0;
for (const Elf64_Dyn *d = m->dyn; d->d_tag; d++) switch (d->d_tag) {
case DT_RELA: rela = d->d_un.d_ptr; break;
case DT_RELASZ: relasz = d->d_un.d_val; break;
case DT_JMPREL: jmprel = d->d_un.d_ptr; break;
case DT_PLTRELSZ: pltsz = d->d_un.d_val; break;
case DT_RELR: relr = d->d_un.d_ptr; break;
case DT_RELRSZ: relrsz = d->d_un.d_val; break;
}
int cnt[3] = {0}, nint = 0, nweak0 = 0;
unsigned nrelr = relr ? relr_apply(m, relr, relrsz, (uint64_t)m->base) : 0;
const Elf64_Rela *tabs[2] = {(const Elf64_Rela *)(m->base + rela), (const Elf64_Rela *)(m->base + jmprel)};
uint64_t sizes[2] = {relasz, pltsz};
for (int t = 0; t < 2; t++)
for (uint64_t i = 0; i < sizes[t] / sizeof(Elf64_Rela); i++) {
const Elf64_Rela *r = &tabs[t][i];
uint64_t *where = (uint64_t *)(m->base + r->r_offset);
unsigned type = ELF64_R_TYPE(r->r_info), si = ELF64_R_SYM(r->r_info);
if (type == R_AARCH64_RELATIVE) { *where = (uint64_t)m->base + r->r_addend; cnt[0]++; continue; }
const Elf64_Sym *s = &m->symtab[si];
uint64_t v = (uint64_t)force_lookup(m->strtab + s->st_name);
// Symbols the module defines itself are also looked up globally in load order (the ELF default; Horizon's rtld does the same): the definition of an earlier-loaded module overrides a later one; only local / non-default visibility binds to itself.
// Engage's main defines its own operator new / delete (Unity's memory manager) and sdk has a copy too (forwarding to malloc / free): previously sdk bound its own delete,
// so blocks from main's new were freed by sdk's libc++ string code into the SDK heap, and CentralHeap::Free reported NotNMallocPtr
if (!v && s->st_shndx) {
uint64_t self = (uint64_t)m->base + s->st_value;
v = ELF64_ST_BIND(s->st_info) == STB_LOCAL || ELF64_ST_VISIBILITY(s->st_other) != STV_DEFAULT ? self : resolve_sym(m, m->strtab + s->st_name);
if (!v) v = self;
if (v != self && nint++ < 8) LOG("%s: %s bound to an earlier module's definition (%s)", m->name, m->strtab + s->st_name, module_of((void *)v) ? module_of((void *)v)->name : "?");
}
// An undefined weak symbol that no module defines and the host does not implement is 0, as Horizon's rtld binds it: the code tests its address before calling it.
// Octopath Traveler 0's main imports the thread_local init function _ZTHN13LowLevelTasks5FTask10ActiveTaskE weakly; a named stub made it look defined and the game called it
const char *nm = m->strtab + s->st_name;
int weak0 = !v && !s->st_shndx && ELF64_ST_BIND(s->st_info) == STB_WEAK && !guest_sym(nm) && !hle_implements(nm);
if (weak0 && nweak0++ < 8) LOG("%s: undefined weak %s bound to 0", m->name, nm);
if (!v && !weak0) v = resolve_sym(m, nm);
if (type == R_AARCH64_JUMP_SLOT) { *where = v; cnt[2]++; }
else { *where = v + r->r_addend; cnt[1]++; } // ABS64 / GLOB_DAT
}
LOG("%s: reloc relative=%d relr=%u abs/glob=%d jump=%d%s", m->name, cnt[0], nrelr, cnt[1], cnt[2], nint ? " (some bound to an earlier module's definition)" : "");
if (nint > 8) LOG("%s: %d bound to an earlier module's definition in all", m->name, nint);
}
// DM_LOADHASH=1 (diagnostics): after loading, output an address-independent sha256 per module, to prove that the load results of "early start / no early start" are byte-identical.
// Module base addresses and host ASLR differ every run, and only the values of the relocation slots change with them: copy the image and, using the same relocation table, replace every slot with an address-independent token --
// RELATIVE → (0, addend); symbol target falls in some module → (1 + module number, offset within the module); otherwise (host forced take-over / host symbol) → (name FNV, addend); all other bytes as is.
#include <CommonCrypto/CommonDigest.h>
void loader_hash_log(void) {
if (!getenv("DM_LOADHASH")) return;
for (int k = 0; k < g_nmod; k++) {
module_t *m = &g_mod[k];
uint8_t *cp = malloc(m->size); memcpy(cp, m->base, m->size);
uint64_t rela = 0, relasz = 0, jmprel = 0, pltsz = 0, relr = 0, relrsz = 0;
for (const Elf64_Dyn *d = m->dyn; d->d_tag; d++) switch (d->d_tag) {
case DT_RELA: rela = d->d_un.d_ptr; break; case DT_RELASZ: relasz = d->d_un.d_val; break;
case DT_JMPREL: jmprel = d->d_un.d_ptr; break; case DT_PLTRELSZ: pltsz = d->d_un.d_val; break;
case DT_RELR: relr = d->d_un.d_ptr; break; case DT_RELRSZ: relrsz = d->d_un.d_val; break;
}
// DT_RELR slots: subtracting the base makes them address-independent (done on the copy, reusing relr_apply's walk)
if (relr) { uint8_t *b = m->base; m->base = cp; relr_apply(m, relr, relrsz, -(uint64_t)b); m->base = b; }
const Elf64_Rela *tabs[2] = {(const Elf64_Rela *)(m->base + rela), (const Elf64_Rela *)(m->base + jmprel)};
uint64_t sizes[2] = {relasz, pltsz}; unsigned nslot = 0;
for (int t = 0; t < 2; t++)
for (uint64_t i = 0; i < sizes[t] / sizeof(Elf64_Rela); i++) {
const Elf64_Rela *r = &tabs[t][i]; uint64_t tok[1] = {0};
if (ELF64_R_TYPE(r->r_info) != R_AARCH64_RELATIVE) {
uint64_t v = *(const uint64_t *)(m->base + r->r_offset); int hit = 0;
for (int j = 0; j < g_nmod; j++) if (v >= (uint64_t)g_mod[j].base && v < (uint64_t)g_mod[j].base + g_mod[j].size) { tok[0] = 1 + j; hit = 1; break; }
if (!hit) { const char *nm = m->strtab + m->symtab[ELF64_R_SYM(r->r_info)].st_name; uint64_t h = 0xcbf29ce484222325ull;
for (; *nm; nm++) h = (h ^ (uint8_t)*nm) * 0x100000001b3ull; tok[0] = h | 1ull << 63; }
}
memcpy(cp + r->r_offset, &tok[0], 8); // The 8 slot bytes are replaced by the token head; the token tail is appended below in slot order
nslot++;
}
CC_SHA256_CTX c; CC_SHA256_Init(&c); CC_SHA256_Update(&c, cp, (CC_LONG)m->size);
for (int t = 0; t < 2; t++) // Token tails (addend / offset) are appended in slot order, and together with the slot head uniquely determine the slot value
for (uint64_t i = 0; i < sizes[t] / sizeof(Elf64_Rela); i++) {
const Elf64_Rela *r = &tabs[t][i]; uint64_t tail = (uint64_t)r->r_addend;
if (ELF64_R_TYPE(r->r_info) != R_AARCH64_RELATIVE) {
uint64_t v = *(const uint64_t *)(m->base + r->r_offset);
for (int j = 0; j < g_nmod; j++) if (v >= (uint64_t)g_mod[j].base && v < (uint64_t)g_mod[j].base + g_mod[j].size) { tail = v - (uint64_t)g_mod[j].base; break; }
}
CC_SHA256_Update(&c, &tail, 8);
}
uint8_t d[32]; CC_SHA256_Final(d, &c); free(cp);
char hx[65]; for (int i = 0; i < 32; i++) sprintf(hx + 2 * i, "%02x", d[i]);
LOG("DM_LOADHASH %s size %#zx relocation slots %u sha256 %s", m->name, m->size, nslot, hx);
}
}
#if TARGET_OS_IPHONE
// iOS does not allow code loaded at run time to execute, and Apple allows an executable only one segment with execute permission (ITMS-90999): the image is compiled into this executable by dm/ios/tools/gen_embed.py --
// the two code blocks (original text, patch area) are sections in __TEXT, and W (the rest of ro + data + bss) is a separate RW segment. The three blocks are not adjacent in the file; here one contiguous address range is reserved,
// and they are vm_remap'ed back at their original relative positions: code pages with copy=FALSE (sharing the same signed pages with __TEXT, r-x), W with copy=TRUE (copy-on-write, paged only when used).
// The run-time layout is byte-identical to the original image. Remapping signed pages to a new address and then executing them on iOS is exactly what libffi's trampoline table (FFI_EXEC_TRAMPOLINE_TABLE) does
#include <mach/mach.h>
static void remap_at(module_t *m, vm_address_t to, const void *from, unsigned long size, boolean_t copy, vm_prot_t need) {
vm_address_t a = to; vm_prot_t cur = 0, max = 0;
kern_return_t kr = vm_remap(mach_task_self(), &a, size, 0, VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, mach_task_self(), (vm_address_t)from, copy, &cur, &max, VM_INHERIT_NONE);
if (kr != KERN_SUCCESS || a != to) die("%s vm_remap %p → %#lx (%#lx bytes) failed: %d", m->name, from, (unsigned long)to, size, kr);
if ((cur & need) != need) die("%s permissions after vm_remap %#x, missing %#x", m->name, cur, need);
}
#define IMAGE_MH (&_mh_execute_header)
static void map_image(module_t *m, const char *dir) {
(void)dir;
char sx0[17], sx1[17], dm[17]; snprintf(sx0, sizeof sx0, "__dm%sx0", m->name); snprintf(sx1, sizeof sx1, "__dm%sx1", m->name); snprintf(dm, sizeof dm, "__DM%sW", m->name);
unsigned long z0, zw, z1;
const uint8_t *x0 = getsectiondata(IMAGE_MH, "__TEXT", sx0, &z0), *w = getsectiondata(IMAGE_MH, dm, "__img", &zw), *x1 = getsectiondata(IMAGE_MH, "__TEXT", sx1, &z1);
if (!x0 || !w || !x1) die("embedded sections %s / %s / %s missing", sx0, dm, sx1);
// Patch area before the base (patch_off < 0): W ends at the end of data (FILE_IMAGE_LEN); bss and the key page are zero pages from vm_allocate and are not in the executable
int front = m->patch_off < 0;
if (z0 + zw != (front ? m->file_image_len : (uint64_t)m->patch_off) || (z0 & 0x3FFF) || (zw & 0x3FFF) || (z1 & 0x3FFF) || ((uintptr_t)x0 & 0x3FFF) || ((uintptr_t)w & 0x3FFF) || ((uintptr_t)x1 & 0x3FFF))
die("%s embedded section layout mismatch (x0 %#lx w %#lx x1 %#lx, PATCH_OFF %#llx)", m->name, z0, zw, z1, (unsigned long long)m->patch_off);
m->size = front ? m->image_size : m->patch_off + z1;
uint64_t pre = front ? (uint64_t)-m->patch_off : 0;
if (front) m->patch_len = z1;
vm_address_t base = 0;
if (vm_allocate(mach_task_self(), &base, pre + m->size, VM_FLAGS_ANYWHERE) != KERN_SUCCESS) die("%s failed to reserve %#llx bytes of address space", m->name, (unsigned long long)(pre + m->size));
base += pre;
remap_at(m, base, x0, z0, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
remap_at(m, base + z0, w, zw, TRUE, VM_PROT_READ | VM_PROT_WRITE);
remap_at(m, base + m->patch_off, x1, z1, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
m->base = (uint8_t *)base; install_key_page(m);
}
#else
static uint8_t *slurp(const char *path, size_t *n) {
int fd = open(path, O_RDONLY);
if (fd < 0) die("open %s", path);
struct stat st; fstat(fd, &st);
uint8_t *p = malloc(st.st_size);
if (read(fd, p, st.st_size) != st.st_size) die("read %s", path);
close(fd); *n = st.st_size; return p;
}
// Seal the code area immediately after copying into anonymous pages (16 KB pages): [0, text_end) and the patch area are read-only+execute, and from then on any write to the code area crashes on the spot;
// what lies between (ro and data on the same page can only go together) is writable
static void map_image(module_t *m, const char *dir) {
char path[1024]; snprintf(path, sizeof path, "%s/%s.patched.bin", dir, m->name);
size_t sz; uint8_t *raw = slurp(path, &sz);
if (m->patch_off < 0) {
// Patch area before the base: file = the first part of the image (text + ro + data) + patch area; bss and the key page are anonymous zero pages. The gap from the patch area's end to the base is left inaccessible (PROT_NONE)
uint64_t pre = (uint64_t)-m->patch_off, fl = m->file_image_len;
if (sz < fl) die("%s is shorter than FILE_IMAGE_LEN %#llx", path, (unsigned long long)fl);
m->size = m->image_size; m->patch_len = (sz - fl + 0x3FFF) & ~0x3FFFull;
if (m->patch_len > pre) die("%s patch area of %#llx bytes does not fit in the %#llx before the base", path, (unsigned long long)m->patch_len, (unsigned long long)pre);
uint8_t *a = mmap(NULL, pre + m->size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (a == MAP_FAILED) die("%s mmap %#llx", m->name, (unsigned long long)(pre + m->size));
m->base = a + pre;
memcpy(m->base, raw, fl); memcpy(a, raw + fl, sz - fl); free(raw); install_key_page(m);
uint64_t rx = (m->text_end + 0x3FFF) & ~0x3FFFull;
if (mprotect(m->base, rx, PROT_READ | PROT_EXEC)) die("%s mprotect text", m->name);
if (mprotect(a, m->patch_len, PROT_READ | PROT_EXEC)) die("%s mprotect patch", m->name);
if (pre > m->patch_len && mprotect(a + m->patch_len, pre - m->patch_len, PROT_NONE)) die("%s mprotect gap", m->name);
return;
}
m->size = (sz + 0x3FFF) & ~0x3FFFull;
m->base = mmap(NULL, m->size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (m->base == MAP_FAILED) die("%s mmap", m->name);
memcpy(m->base, raw, sz); free(raw); install_key_page(m);
uint64_t rx = (m->text_end + 0x3FFF) & ~0x3FFFull;
if (mprotect(m->base, rx, PROT_READ | PROT_EXEC)) die("%s mprotect text", m->name);
uint64_t po = m->patch_off & ~0x3FFFull;
if (mprotect(m->base + po, m->size - po, PROT_READ | PROT_EXEC)) die("%s mprotect patch", m->name);
}
#endif
void load_modules(const char *dir, const char **names, int n) {
// Map everything and parse the symbol tables first, then relocate all at once (cross-module symbols must all be in place first)
// DM_BOOTPROF: breakdown of ① host loading (milliseconds), to set the upper bound for ③ pre-relocation / mmap
uint64_t t0 = now_ns(), tm = 0, tp = 0, tg = 0, tr = 0, t;
for (int i = 0; i < n; i++) {
module_t *m = &g_mod[i];
strlcpy(m->name, names[i], sizeof m->name);
read_layout(m, dir);
}
create_keys();
for (int i = 0; i < n; i++) {
module_t *m = &g_mod[i];
t = now_ns(); map_image(m, dir); tm += now_ns() - t; // On return the code area is already read-only+execute; from here on the loader only writes data
if (!i) tsd_check();
t = now_ns(); parse_dynamic(m); tp += now_ns() - t;
t = now_ns(); register_gates(m, dir); tg += now_ns() - t;
LOG("%s: base %p size %#zx", names[i], m->base, m->size);
g_nmod++;
}
force_init();
t = now_ns(); for (int i = 0; i < g_nmod; i++) relocate(&g_mod[i]); tr = now_ns() - t;
force_check();
{ void hlecount_seal(void); hlecount_seal(); } // DM_HLECOUNT: relocation done, seal the counting stub pages as read-only executable
if (getenv("DM_SYMCHECK")) LOG("DM_SYMCHECK: %lu symbol lookups at load time, hash and linear disagree %lu", g_symchk_n, g_symchk_bad);
if (g_flag.bootprof) LOG("① breakdown: mapping (read + copy + seal pages) %.1f ms, dynamic section parsing %.1f ms, gate registration %.1f ms, relocation + forced takeover %.1f ms, total %.1f ms",
tm / 1e6, tp / 1e6, tg / 1e6, tr / 1e6, (now_ns() - t0) / 1e6);
}
// Plays the role of rtld's InitModules (rtld+0x830): call each module's DT_INIT in forward order of the load list.
// The timing is decided by nn::init::Start: after nninitStartup and _init_libc2, before nnMain (the 4th argument callback of Start).
void run_module_inits(void) {
for (int i = 0; i < g_nmod; i++) {
module_t *m = &g_mod[i];
if (!m->init) continue;
LOG("%s: DT_INIT +%#llx (init_array %llu entries)", m->name, m->init, m->init_arraysz / 8);
((void (*)(void))(m->base + m->init))();
}
}