|
23 | 23 | // 1. Signal info (signal/code/fault address/tid/thread name/uptime). |
24 | 24 | // 2. Full register state. |
25 | 25 | // 3. The faulting thread's backtrace, walked from the saved ucontext via |
26 | | -// the AArch64 frame-pointer chain. This is the diagnostic that |
27 | | -// actually matters — it shows where the crash happened. We do NOT |
28 | | -// use `_Unwind_Backtrace` for this because that walks the *handler* |
29 | | -// thread's stack (which terminates at the kernel signal trampoline |
30 | | -// in vdso, giving "crashHandler → libsigchain → vdso" — useless). |
31 | | -// 4. /proc/self/maps so any addresses dladdr couldn't symbolicate |
32 | | -// (internal-namespace / stripped-.dynsym frames) can still be matched |
33 | | -// to a library and offset post-mortem. |
| 26 | +// the AArch64 frame-pointer chain. Each frame is symbolicated by |
| 27 | +// trying, in order: |
| 28 | +// a. dladdr — resolves PCs that fall inside a loaded ELF (.so). |
| 29 | +// b. The Mono `--jitmap` perfmap (`<TMPDIR>/perf-<pid>.map`), which |
| 30 | +// names managed JIT methods. Enable by setting Mono env vars |
| 31 | +// (see `resolveViaPerfmap` comment below). |
| 32 | +// c. /proc/self/maps — labels the containing region (e.g. JIT trampoline |
| 33 | +// or stripped .so) with offset, so addresses without a symbol still |
| 34 | +// produce actionable output instead of "<unresolved>". |
| 35 | +// 4. /proc/self/maps so any addresses still without a symbol after (3c) |
| 36 | +// can be cross-checked manually against the full process memory layout. |
34 | 37 | // 5. The secondary `_Unwind_Backtrace` output for completeness. |
35 | 38 | // |
36 | 39 | // Async-signal safety: |
|
69 | 72 | #include <dlfcn.h> |
70 | 73 | #include <unwind.h> |
71 | 74 | #include <ucontext.h> |
| 75 | +#include <sys/mman.h> |
72 | 76 | #include <android/log.h> |
73 | 77 |
|
74 | 78 | #include "crash_handler.h" |
@@ -148,6 +152,331 @@ static void logcatWrite(const char* msg) { |
148 | 152 | __android_log_write(ANDROID_LOG_ERROR, CRASH_LOG_TAG, msg); |
149 | 153 | } |
150 | 154 |
|
| 155 | +// ---------------------------------------------------------------------------- |
| 156 | +// Mapping- and JIT-perfmap-based fallback symbolicators. |
| 157 | +// |
| 158 | +// Why this exists: |
| 159 | +// `dladdr` only resolves PCs that fall inside a loaded ELF (.so) image. |
| 160 | +// It cannot resolve: |
| 161 | +// a. PCs in Mono's JIT/trampoline regions (anonymous `rwxp` mappings) — |
| 162 | +// these are where every `<unresolved>` frame in our existing crash logs |
| 163 | +// sits when the crash is in managed C# code or a Mono trampoline. |
| 164 | +// b. PCs in stripped libraries with no .dynsym entries, where dladdr at |
| 165 | +// best returns the library path with no symbol. |
| 166 | +// |
| 167 | +// We add two fallbacks below, tried in order whenever dladdr fails: |
| 168 | +// 1. `resolveViaPerfmap` — search a Mono `--jitmap` file (if present) for |
| 169 | +// the managed method that owns this PC. Mono with `--jitmap` (enabled |
| 170 | +// via `MONO_ENV_OPTIONS=--jitmap`) writes one line per JIT method to |
| 171 | +// `<TMPDIR>/perf-<pid>.map` in the format |
| 172 | +// <hex_start_addr> <hex_size> <method_name> |
| 173 | +// which we parse line-by-line. |
| 174 | +// 2. `resolveViaProcMaps` — find the `/proc/self/maps` entry containing |
| 175 | +// the PC and emit `[perms start-end +offset] /path/to/lib` (or a |
| 176 | +// `[Mono JIT/trampoline (anon rwxp)]` tag for anonymous executable |
| 177 | +// mappings). This always works and turns "<unresolved>" lines into |
| 178 | +// actionable output even when no perfmap is present. |
| 179 | +// |
| 180 | +// To enable the perfmap output (step 1) for a build: |
| 181 | +// - Add an `AndroidEnvironment` text file to the project containing: |
| 182 | +// MONO_ENV_OPTIONS=--jitmap |
| 183 | +// TMPDIR=/storage/emulated/0/Android/data/<pkg>/files |
| 184 | +// so Mono writes the perfmap into the same dir as `native_crash.log`. |
| 185 | +// `crash_handler.cpp` searches that dir, plus `/tmp` and `/data/local/tmp`, |
| 186 | +// plus the directory containing `g_logPath`. |
| 187 | +// |
| 188 | +// Async-signal safety: |
| 189 | +// - All file I/O uses open/read/close (signal-safe). |
| 190 | +// - We do NOT use malloc. The perfmap is mmap'd once on first use into a |
| 191 | +// static slot; subsequent frame lookups scan that buffer in-place. |
| 192 | +// - The /proc/self/maps lookup uses a fixed-size stack buffer and |
| 193 | +// re-opens the file once per crash (it is small — typically <1 MB). |
| 194 | +// ---------------------------------------------------------------------------- |
| 195 | + |
| 196 | +// Lazily-mapped Mono perfmap. Set on first call to resolveViaPerfmap during |
| 197 | +// a crash; never unmapped (we're about to die anyway). |
| 198 | +static const char* g_perfmapData = nullptr; |
| 199 | +static size_t g_perfmapSize = 0; |
| 200 | +static volatile sig_atomic_t g_perfmapTried = 0; |
| 201 | + |
| 202 | +static int hexVal(char c) { |
| 203 | + if (c >= '0' && c <= '9') return c - '0'; |
| 204 | + if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); |
| 205 | + if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); |
| 206 | + return -1; |
| 207 | +} |
| 208 | + |
| 209 | +// Parse "<hex>" up to a non-hex char. Advances *p past the parsed digits. |
| 210 | +// Returns the parsed value (0 if no digits parsed). |
| 211 | +static uint64_t parseHexAt(const char* s, size_t len, size_t* p) { |
| 212 | + uint64_t v = 0; |
| 213 | + while (*p < len) { |
| 214 | + int d = hexVal(s[*p]); |
| 215 | + if (d < 0) break; |
| 216 | + v = (v << 4) | (uint64_t)d; |
| 217 | + ++*p; |
| 218 | + } |
| 219 | + return v; |
| 220 | +} |
| 221 | + |
| 222 | +// Append a NUL-terminated literal to a fixed buffer; advances *bp. |
| 223 | +static void appendLit(char* buf, int cap, int* bp, const char* s) { |
| 224 | + while (*s && *bp < cap - 1) buf[(*bp)++] = *s++; |
| 225 | +} |
| 226 | + |
| 227 | +// Append a length-bounded string (may contain '\n' which we stop at). |
| 228 | +static void appendBounded(char* buf, int cap, int* bp, const char* s, int slen) { |
| 229 | + for (int i = 0; i < slen && *bp < cap - 1; ++i) { |
| 230 | + char c = s[i]; |
| 231 | + if (c == '\n' || c == '\r') break; |
| 232 | + buf[(*bp)++] = c; |
| 233 | + } |
| 234 | +} |
| 235 | + |
| 236 | +// Try to mmap the Mono perfmap once. Returns true if g_perfmapData is set. |
| 237 | +// Searches several plausible locations because Mono's `--jitmap` always |
| 238 | +// writes to `<TMPDIR>/perf-<pid>.map` and TMPDIR varies by configuration. |
| 239 | +static bool ensurePerfmapLoaded() { |
| 240 | + if (g_perfmapTried) return g_perfmapData != nullptr; |
| 241 | + g_perfmapTried = 1; |
| 242 | + |
| 243 | + // Build "perf-<pid>.map" once. |
| 244 | + char nameBuf[64]; |
| 245 | + int np = 0; |
| 246 | + appendLit(nameBuf, sizeof(nameBuf), &np, "perf-"); |
| 247 | + { |
| 248 | + char tmp[16]; int tp = 0; |
| 249 | + long long n = (long long)getpid(); |
| 250 | + if (n == 0) tmp[tp++] = '0'; |
| 251 | + while (n > 0 && tp < 15) { tmp[tp++] = (char)('0' + (n % 10)); n /= 10; } |
| 252 | + while (tp > 0 && np < (int)sizeof(nameBuf) - 1) nameBuf[np++] = tmp[--tp]; |
| 253 | + } |
| 254 | + appendLit(nameBuf, sizeof(nameBuf), &np, ".map"); |
| 255 | + nameBuf[np] = '\0'; |
| 256 | + |
| 257 | + // Candidate directories, in priority order. The dir containing g_logPath |
| 258 | + // is checked first so a build that sets `TMPDIR=<external-files-dir>` |
| 259 | + // (the recommended config) finds its perfmap immediately. |
| 260 | + const char* tmpEnv = getenv("TMPDIR"); |
| 261 | + char logDir[kMaxLogPathLen] = {}; |
| 262 | + if (g_logPath[0] != '\0') { |
| 263 | + size_t len = 0; |
| 264 | + while (g_logPath[len] != '\0' && len < sizeof(logDir) - 1) { |
| 265 | + logDir[len] = g_logPath[len]; |
| 266 | + ++len; |
| 267 | + } |
| 268 | + // Strip trailing filename component. |
| 269 | + while (len > 0 && logDir[len - 1] != '/') { logDir[--len] = '\0'; } |
| 270 | + if (len > 1 && logDir[len - 1] == '/') logDir[len - 1] = '\0'; |
| 271 | + } |
| 272 | + |
| 273 | + const char* dirs[4] = { |
| 274 | + logDir[0] ? logDir : nullptr, |
| 275 | + tmpEnv, |
| 276 | + "/tmp", |
| 277 | + "/data/local/tmp", |
| 278 | + }; |
| 279 | + |
| 280 | + for (int i = 0; i < 4; ++i) { |
| 281 | + if (!dirs[i] || dirs[i][0] == '\0') continue; |
| 282 | + char path[kMaxLogPathLen + 64]; |
| 283 | + int p = 0; |
| 284 | + for (int k = 0; dirs[i][k] && p < (int)sizeof(path) - 1; ++k) path[p++] = dirs[i][k]; |
| 285 | + if (p > 0 && path[p - 1] != '/' && p < (int)sizeof(path) - 1) path[p++] = '/'; |
| 286 | + for (int k = 0; nameBuf[k] && p < (int)sizeof(path) - 1; ++k) path[p++] = nameBuf[k]; |
| 287 | + path[p] = '\0'; |
| 288 | + |
| 289 | + int fd = open(path, O_RDONLY | O_CLOEXEC); |
| 290 | + if (fd < 0) continue; |
| 291 | + struct stat st; |
| 292 | + if (fstat(fd, &st) != 0 || st.st_size <= 0) { close(fd); continue; } |
| 293 | + // Cap the mapped size at 64 MB to bound scan time. A perfmap larger |
| 294 | + // than that for a single .NET process would be extraordinary. |
| 295 | + size_t sz = (size_t)st.st_size; |
| 296 | + if (sz > 64u * 1024u * 1024u) sz = 64u * 1024u * 1024u; |
| 297 | + void* m = mmap(nullptr, sz, PROT_READ, MAP_PRIVATE, fd, 0); |
| 298 | + close(fd); |
| 299 | + if (m == MAP_FAILED) continue; |
| 300 | + g_perfmapData = static_cast<const char*>(m); |
| 301 | + g_perfmapSize = sz; |
| 302 | + return true; |
| 303 | + } |
| 304 | + return false; |
| 305 | +} |
| 306 | + |
| 307 | +// Linearly scan the perfmap for the entry containing pc. On hit, writes |
| 308 | +// " [JIT] <method_name>+0xOFF" to fd and returns true. |
| 309 | +static bool resolveViaPerfmap(int fd, uintptr_t pc) { |
| 310 | + if (!ensurePerfmapLoaded()) return false; |
| 311 | + const char* d = g_perfmapData; |
| 312 | + size_t n = g_perfmapSize; |
| 313 | + size_t i = 0; |
| 314 | + while (i < n) { |
| 315 | + // Each line: "<hex_start> <hex_size> <name>\n" |
| 316 | + size_t lineStart = i; |
| 317 | + size_t p = i; |
| 318 | + uint64_t start = parseHexAt(d, n, &p); |
| 319 | + // skip space |
| 320 | + while (p < n && d[p] == ' ') ++p; |
| 321 | + uint64_t size = parseHexAt(d, n, &p); |
| 322 | + while (p < n && d[p] == ' ') ++p; |
| 323 | + size_t nameStart = p; |
| 324 | + while (p < n && d[p] != '\n') ++p; |
| 325 | + size_t nameLen = p - nameStart; |
| 326 | + if (size != 0 && pc >= start && pc < start + size) { |
| 327 | + char buf[320]; |
| 328 | + int bp = 0; |
| 329 | + appendLit(buf, sizeof(buf), &bp, " [JIT] "); |
| 330 | + appendBounded(buf, sizeof(buf), &bp, d + nameStart, (int)nameLen); |
| 331 | + appendLit(buf, sizeof(buf), &bp, "+0x"); |
| 332 | + // hex offset |
| 333 | + uint64_t off = pc - start; |
| 334 | + char hb[17]; |
| 335 | + static const char hd[] = "0123456789abcdef"; |
| 336 | + int hp = 0; |
| 337 | + if (off == 0) hb[hp++] = '0'; |
| 338 | + char rev[17]; int rp = 0; |
| 339 | + while (off > 0) { rev[rp++] = hd[off & 0xf]; off >>= 4; } |
| 340 | + while (rp > 0) hb[hp++] = rev[--rp]; |
| 341 | + for (int k = 0; k < hp && bp < (int)sizeof(buf) - 1; ++k) buf[bp++] = hb[k]; |
| 342 | + buf[bp] = '\0'; |
| 343 | + ssize_t wn = write(fd, buf, bp); |
| 344 | + (void)wn; |
| 345 | + return true; |
| 346 | + } |
| 347 | + // advance past newline |
| 348 | + if (p < n && d[p] == '\n') ++p; |
| 349 | + // safety: if a line is malformed and we didn't advance, force progress |
| 350 | + if (p == lineStart) ++p; |
| 351 | + i = p; |
| 352 | + } |
| 353 | + return false; |
| 354 | +} |
| 355 | + |
| 356 | +// Scan /proc/self/maps for the entry containing pc. On hit writes |
| 357 | +// " [perms start-end +0xOFF] <path-or-tag>" to fd and returns true. |
| 358 | +// Reads the file fresh each call (it's small and signal-safe to do so). |
| 359 | +static bool resolveViaProcMaps(int fd, uintptr_t pc) { |
| 360 | + int mfd = open("/proc/self/maps", O_RDONLY | O_CLOEXEC); |
| 361 | + if (mfd < 0) return false; |
| 362 | + |
| 363 | + // We accumulate a single map line into `line` (max 512 chars; lines on |
| 364 | + // Android maps are well below this in practice — long ones are paths to |
| 365 | + // /data/app/.../base.apk plus offset, ~280 chars). |
| 366 | + char line[512]; |
| 367 | + int lp = 0; |
| 368 | + char chunk[4096]; |
| 369 | + bool hit = false; |
| 370 | + |
| 371 | + for (;;) { |
| 372 | + ssize_t r = read(mfd, chunk, sizeof(chunk)); |
| 373 | + if (r <= 0) break; |
| 374 | + for (ssize_t ci = 0; ci < r; ++ci) { |
| 375 | + char c = chunk[ci]; |
| 376 | + if (c == '\n') { |
| 377 | + line[lp] = '\0'; |
| 378 | + // Parse "start-end perms offset dev inode <path>" |
| 379 | + size_t p = 0; size_t lineLen = (size_t)lp; |
| 380 | + uint64_t s = parseHexAt(line, lineLen, &p); |
| 381 | + if (p < lineLen && line[p] == '-') { |
| 382 | + ++p; |
| 383 | + uint64_t e = parseHexAt(line, lineLen, &p); |
| 384 | + if (pc >= s && pc < e) { |
| 385 | + // Skip space, capture perms (4 chars). |
| 386 | + while (p < lineLen && line[p] == ' ') ++p; |
| 387 | + char perms[5] = {}; |
| 388 | + for (int k = 0; k < 4 && p < lineLen; ++k, ++p) perms[k] = line[p]; |
| 389 | + // Skip 3 fields (offset dev inode) to reach path. |
| 390 | + for (int field = 0; field < 3; ++field) { |
| 391 | + while (p < lineLen && line[p] == ' ') ++p; |
| 392 | + while (p < lineLen && line[p] != ' ') ++p; |
| 393 | + } |
| 394 | + while (p < lineLen && line[p] == ' ') ++p; |
| 395 | + const char* path = (p < lineLen) ? &line[p] : ""; |
| 396 | + |
| 397 | + char outBuf[640]; |
| 398 | + int bp = 0; |
| 399 | + appendLit(outBuf, sizeof(outBuf), &bp, " ["); |
| 400 | + for (int k = 0; k < 4 && perms[k] && bp < (int)sizeof(outBuf) - 1; ++k) |
| 401 | + outBuf[bp++] = perms[k]; |
| 402 | + appendLit(outBuf, sizeof(outBuf), &bp, " 0x"); |
| 403 | + // start hex |
| 404 | + { |
| 405 | + uint64_t v = s; |
| 406 | + char hb[17]; int hp = 0; |
| 407 | + static const char hd[] = "0123456789abcdef"; |
| 408 | + if (v == 0) hb[hp++] = '0'; |
| 409 | + char rev[17]; int rp = 0; |
| 410 | + while (v > 0) { rev[rp++] = hd[v & 0xf]; v >>= 4; } |
| 411 | + while (rp > 0) hb[hp++] = rev[--rp]; |
| 412 | + for (int k = 0; k < hp && bp < (int)sizeof(outBuf) - 1; ++k) |
| 413 | + outBuf[bp++] = hb[k]; |
| 414 | + } |
| 415 | + appendLit(outBuf, sizeof(outBuf), &bp, "-0x"); |
| 416 | + { |
| 417 | + uint64_t v = e; |
| 418 | + char hb[17]; int hp = 0; |
| 419 | + static const char hd[] = "0123456789abcdef"; |
| 420 | + if (v == 0) hb[hp++] = '0'; |
| 421 | + char rev[17]; int rp = 0; |
| 422 | + while (v > 0) { rev[rp++] = hd[v & 0xf]; v >>= 4; } |
| 423 | + while (rp > 0) hb[hp++] = rev[--rp]; |
| 424 | + for (int k = 0; k < hp && bp < (int)sizeof(outBuf) - 1; ++k) |
| 425 | + outBuf[bp++] = hb[k]; |
| 426 | + } |
| 427 | + appendLit(outBuf, sizeof(outBuf), &bp, " +0x"); |
| 428 | + { |
| 429 | + uint64_t v = pc - s; |
| 430 | + char hb[17]; int hp = 0; |
| 431 | + static const char hd[] = "0123456789abcdef"; |
| 432 | + if (v == 0) hb[hp++] = '0'; |
| 433 | + char rev[17]; int rp = 0; |
| 434 | + while (v > 0) { rev[rp++] = hd[v & 0xf]; v >>= 4; } |
| 435 | + while (rp > 0) hb[hp++] = rev[--rp]; |
| 436 | + for (int k = 0; k < hp && bp < (int)sizeof(outBuf) - 1; ++k) |
| 437 | + outBuf[bp++] = hb[k]; |
| 438 | + } |
| 439 | + outBuf[bp < (int)sizeof(outBuf) - 1 ? bp++ : bp] = ']'; |
| 440 | + if (path[0] != '\0') { |
| 441 | + appendLit(outBuf, sizeof(outBuf), &bp, " "); |
| 442 | + for (int k = 0; path[k] && bp < (int)sizeof(outBuf) - 1; ++k) |
| 443 | + outBuf[bp++] = path[k]; |
| 444 | + } else if (perms[2] == 'x') { |
| 445 | + // Anonymous executable mapping: classic Mono JIT/trampoline region. |
| 446 | + appendLit(outBuf, sizeof(outBuf), &bp, |
| 447 | + " [Mono JIT/trampoline (anon rwxp)]"); |
| 448 | + } else { |
| 449 | + appendLit(outBuf, sizeof(outBuf), &bp, " [anon]"); |
| 450 | + } |
| 451 | + outBuf[bp] = '\0'; |
| 452 | + ssize_t wn = write(fd, outBuf, bp); |
| 453 | + (void)wn; |
| 454 | + hit = true; |
| 455 | + } |
| 456 | + } |
| 457 | + lp = 0; |
| 458 | + if (hit) break; |
| 459 | + } else if (lp < (int)sizeof(line) - 1) { |
| 460 | + line[lp++] = c; |
| 461 | + } else { |
| 462 | + // overflow: drop until newline |
| 463 | + } |
| 464 | + } |
| 465 | + if (hit) break; |
| 466 | + } |
| 467 | + close(mfd); |
| 468 | + return hit; |
| 469 | +} |
| 470 | + |
| 471 | +// Convenience: try perfmap then /proc/self/maps. Writes nothing (and returns |
| 472 | +// false) if neither resolves. |
| 473 | +static bool resolveUnknownPc(int fd, uintptr_t pc) { |
| 474 | + if (pc == 0) return false; |
| 475 | + if (resolveViaPerfmap(fd, pc)) return true; |
| 476 | + return resolveViaProcMaps(fd, pc); |
| 477 | +} |
| 478 | + |
| 479 | + |
151 | 480 | // ---------------------------------------------------------------------------- |
152 | 481 | // Backtrace via libgcc/compiler-rt _Unwind_Backtrace. |
153 | 482 | // ---------------------------------------------------------------------------- |
@@ -191,7 +520,7 @@ static _Unwind_Reason_Code unwindCallback(struct _Unwind_Context* ctx, void* arg |
191 | 520 | writeHex64(st->fd, (uint64_t)off, 1); |
192 | 521 | writeStr(st->fd, ")"); |
193 | 522 | } |
194 | | - } else { |
| 523 | + } else if (!resolveUnknownPc(st->fd, pc)) { |
195 | 524 | writeStr(st->fd, " <unresolved>"); |
196 | 525 | } |
197 | 526 | writeStr(st->fd, "\n"); |
@@ -312,7 +641,12 @@ static void writeFrame(int fd, int frameNo, uintptr_t pc, const char* tagWhenUnr |
312 | 641 | } else if (tagWhenUnresolved) { |
313 | 642 | writeStr(fd, " "); |
314 | 643 | writeStr(fd, tagWhenUnresolved); |
315 | | - } else { |
| 644 | + // Even when we have a synthetic tag (e.g. "<NULL function pointer call>" |
| 645 | + // or "<LR (return address of NULL call)>"), still try to attach a |
| 646 | + // perfmap/maps annotation so we know which JIT region or library the |
| 647 | + // PC sits in. |
| 648 | + resolveUnknownPc(fd, pc); |
| 649 | + } else if (!resolveUnknownPc(fd, pc)) { |
316 | 650 | writeStr(fd, " <unresolved>"); |
317 | 651 | } |
318 | 652 | writeStr(fd, "\n"); |
|
0 commit comments