已合并
add android FP backtrace #770
liwenzhen3创建于 7月8日
add android FP backtrace #770
已合并
共 4 个文件变更+713-1
| @@ -61,6 +61,7 @@ template("ace_common_jni_source_set") { | |||
| 61 | "$ace_root/adapter/android/entrance/java/jni/ace_env_jni.cpp", | 61 | "$ace_root/adapter/android/entrance/java/jni/ace_env_jni.cpp", |
| 62 | "$ace_root/adapter/android/entrance/java/jni/ace_resource_register.cpp", | 62 | "$ace_root/adapter/android/entrance/java/jni/ace_resource_register.cpp", |
| 63 | "$ace_root/adapter/android/entrance/java/jni/ace_translate_manager.cpp", | 63 | "$ace_root/adapter/android/entrance/java/jni/ace_translate_manager.cpp", |
| 64 | + "$ace_root/adapter/android/entrance/java/jni/backtrace_handler.cpp", | ||
| 64 | "$ace_root/adapter/android/entrance/java/jni/display_info.cpp", | 65 | "$ace_root/adapter/android/entrance/java/jni/display_info.cpp", |
| 65 | "$ace_root/adapter/android/entrance/java/jni/display_info_jni.cpp", | 66 | "$ace_root/adapter/android/entrance/java/jni/display_info_jni.cpp", |
| 66 | "$ace_root/adapter/android/entrance/java/jni/display_manager_agent.cpp", | 67 | "$ace_root/adapter/android/entrance/java/jni/display_manager_agent.cpp", |
| @@ -130,7 +131,10 @@ template("ace_common_jni_source_set") { | |||
| 130 | 131 | ||
| 131 | if (is_arkui_x) { | 132 | if (is_arkui_x) { |
| 132 | defines += [ "CROSS_PLATFORM" ] | 133 | defines += [ "CROSS_PLATFORM" ] |
| 133 | - libs = [ "mediandk" ] | 134 | + libs = [ |
| 135 | + "mediandk", | ||
| 136 | + "log", | ||
| 137 | + ] | ||
| 134 | } | 138 | } |
| 135 | 139 | ||
| 136 | defines += ["ARKUI_X_VERSION=\"$arkuix_sdk_version\""] | 140 | defines += ["ARKUI_X_VERSION=\"$arkuix_sdk_version\""] |
| @@ -0,0 +1,644 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +// Engine log. Only used from the normal (non-signal) context, e.g. inside | ||
| 29 | +// InitBacktraceHandler(). NEVER call LOG* from within the signal handler: | ||
| 30 | +// HILOG routes through IPC/locks/malloc and is not async-signal-safe. | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +namespace OHOS::Ace::Platform { | ||
| 34 | +namespace { | ||
| 35 | + | ||
| 36 | +constexpr const char* LOG_TAG = "ArkUI-x"; | ||
| 37 | + | ||
| 38 | +// --------------------------------------------------------------------------- | ||
| 39 | +// Tunables | ||
| 40 | +// --------------------------------------------------------------------------- | ||
| 41 | +constexpr uint32_t MAX_STACK_DEPTH = 64; // hard cap on walked frames | ||
| 42 | +constexpr uintptr_t INSTRUCTION_LENGTH = 4; // aarch64 fixed-width instruction | ||
| 43 | +constexpr int HEX_SHIFT = 4; // bits per hex digit (AppendHex/ParseHex) | ||
| 44 | +constexpr int DECIMAL_BASE = 10; // base for decimal formatting (AppendDec/ParseHex) | ||
| 45 | +constexpr size_t FRAME_POINTER_SLOTS = 2; // [fp]=x29, [fp+8]=x30: one frame spans 2 pointers | ||
| 46 | +// An ArkUI-X process loads dozens of .so plus JIT/scudo regions and per-thread | ||
| 47 | +// stacks, so /proc/self/maps reaches several thousand entries. The original 768 | ||
| 48 | +// (and even 2048) truncated the stack/so mappings and lost frames / module | ||
| 49 | +// names; 4096 covers the measured ~3900 entries with headroom. | ||
| 50 | +constexpr size_t MAX_MAP_RECORDS = 4096; | ||
| 51 | +// 1MB; the original 256KB cannot hold the full maps text such a process emits. | ||
| 52 | +constexpr size_t MAPS_FILE_BUF_SIZE = 1024 * 1024; | ||
| 53 | +constexpr size_t DUMP_BUF_SIZE = 8192; | ||
C | |||
| 54 | +constexpr size_t FLUSH_THRESHOLD = 256; // flush when fewer than this many bytes remain | ||
| 55 | +constexpr size_t ALT_STACK_SIZE = 64 * 1024; | ||
| 56 | +constexpr size_t LOG_LINE_SIZE = 512; // per-line buffer used when emitting to the system log | ||
| 57 | + | ||
| 58 | + | ||
| 59 | +constexpr int REG_FP = 29; // x29 is the frame pointer on AArch64 | ||
| 60 | + | ||
| 61 | + | ||
| 62 | +// --------------------------------------------------------------------------- | ||
| 63 | +// /proc/self/maps model (plain data, no heap, safe to touch in a handler) | ||
| 64 | +// --------------------------------------------------------------------------- | ||
| 65 | +struct MapRecord { | ||
| 66 | + uintptr_t start; | ||
| 67 | + uintptr_t end; | ||
| 68 | + uintptr_t offset; | ||
| 69 | + bool executable; // permission has 'x' (i.e. may hold return addresses) | ||
| 70 | + const char* name; // points into g_mapsFileBuf; full length, never truncated | ||
| 71 | +}; | ||
| 72 | + | ||
| 73 | +// File-scope statics: kept off the (possibly tiny alternate) signal stack and | ||
| 74 | +// reused across crashes. Reuse is serialized by g_inHandler below. | ||
| 75 | +MapRecord g_maps[MAX_MAP_RECORDS]; | ||
| 76 | +size_t g_mapCount = 0; | ||
| 77 | +char g_mapsFileBuf[MAPS_FILE_BUF_SIZE]; | ||
| 78 | + | ||
| 79 | +// --------------------------------------------------------------------------- | ||
| 80 | +// Output buffer + async-signal-safe append helpers (zero allocation) | ||
| 81 | +// --------------------------------------------------------------------------- | ||
| 82 | +struct DumpBuffer { | ||
| 83 | + char data[DUMP_BUF_SIZE]; | ||
| 84 | + size_t len; | ||
| 85 | +}; | ||
| 86 | + | ||
| 87 | +void AppendCh(DumpBuffer& buf, char ch) | ||
| 88 | +{ | ||
| 89 | + if (buf.len < DUMP_BUF_SIZE - 1) { | ||
| 90 | + buf.data[buf.len++] = ch; | ||
| 91 | + } | ||
| 92 | +} | ||
| 93 | + | ||
| 94 | +void AppendLit(DumpBuffer& buf, const char* text) | ||
| 95 | +{ | ||
| 96 | + while (*text != '\0') { | ||
| 97 | + AppendCh(buf, *text++); | ||
| 98 | + } | ||
| 99 | +} | ||
| 100 | + | ||
| 101 | +void AppendStr(DumpBuffer& buf, const char* text) | ||
| 102 | +{ | ||
| 103 | + if (text == nullptr) { | ||
| 104 | + return; | ||
| 105 | + } | ||
| 106 | + while (*text != '\0') { | ||
| 107 | + AppendCh(buf, *text++); | ||
| 108 | + } | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +void AppendHex(DumpBuffer& buf, uintptr_t value) | ||
| 112 | +{ | ||
| 113 | + static const char* digits = "0123456789abcdef"; | ||
| 114 | + char tmp[2 * sizeof(uintptr_t)]; | ||
| 115 | + int i = 0; | ||
| 116 | + if (value == 0) { | ||
| 117 | + AppendCh(buf, '0'); | ||
| 118 | + return; | ||
| 119 | + } | ||
| 120 | + while (value != 0) { | ||
| 121 | + tmp[i++] = digits[value & 0xf]; | ||
| 122 | + value >>= HEX_SHIFT; | ||
| 123 | + } | ||
| 124 | + while (i > 0) { | ||
| 125 | + AppendCh(buf, tmp[--i]); | ||
| 126 | + } | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +void AppendHex0x(DumpBuffer& buf, uintptr_t value) | ||
| 130 | +{ | ||
| 131 | + AppendLit(buf, "0x"); | ||
| 132 | + AppendHex(buf, value); | ||
| 133 | +} | ||
| 134 | + | ||
| 135 | +void AppendDec(DumpBuffer& buf, uint32_t value) | ||
| 136 | +{ | ||
| 137 | + char tmp[10]; | ||
| 138 | + int i = 0; | ||
| 139 | + if (value == 0) { | ||
| 140 | + AppendCh(buf, '0'); | ||
| 141 | + return; | ||
| 142 | + } | ||
| 143 | + while (value != 0) { | ||
| 144 | + tmp[i++] = '0' + (value % DECIMAL_BASE); | ||
| 145 | + value /= DECIMAL_BASE; | ||
| 146 | + } | ||
| 147 | + while (i > 0) { | ||
| 148 | + AppendCh(buf, tmp[--i]); | ||
| 149 | + } | ||
| 150 | +} | ||
| 151 | + | ||
| 152 | +// Signed-decimal variant for values that may legitimately be negative on Linux, | ||
| 153 | +// e.g. siginfo_t::si_code (SI_TKILL == -6 for the SIGABRT raised by | ||
| 154 | +// abort()/raise()/pthread_kill()). AppendDec only formats the unsigned magnitude. | ||
| 155 | +void AppendDecSigned(DumpBuffer& buf, int32_t value) | ||
| 156 | +{ | ||
| 157 | + uint32_t magnitude; | ||
| 158 | + if (value < 0) { | ||
| 159 | + AppendCh(buf, '-'); | ||
| 160 | + // Negate without UB: -(value + 1) + 1 is safe even for INT32_MIN. | ||
| 161 | + magnitude = static_cast<uint32_t>(-(value + 1)) + 1U; | ||
| 162 | + } else { | ||
| 163 | + magnitude = static_cast<uint32_t>(value); | ||
| 164 | + } | ||
| 165 | + AppendDec(buf, magnitude); | ||
| 166 | +} | ||
| 167 | + | ||
| 168 | +// Writes the whole buffer to stderr (async-signal-safe) and additionally routes | ||
| 169 | +// each line to the system log via __android_log_write, the lightweight (no | ||
| 170 | +// varargs formatting) logging call. Resets buf.len to 0 afterwards, so deep | ||
| 171 | +// backtraces can batch-flush without overflowing the fixed-size buffer. | ||
| 172 | +void FlushDump(DumpBuffer& buf) | ||
| 173 | +{ | ||
| 174 | + ssize_t written = 0; | ||
| 175 | + while (written < static_cast<ssize_t>(buf.len)) { | ||
| 176 | + ssize_t bytes = write(STDERR_FILENO, buf.data + written, buf.len - written); | ||
| 177 | + if (bytes > 0) { | ||
| 178 | + written += bytes; | ||
| 179 | + } else if (bytes < 0 && errno == EINTR) { | ||
| 180 | + continue; | ||
| 181 | + } else { | ||
| 182 | + break; // give up silently; we are crashing anyway | ||
| 183 | + } | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + | ||
| 187 | + // Emit line-by-line so the system log shows one readable entry per frame. | ||
| 188 | + size_t lineStart = 0; | ||
| 189 | + char line[LOG_LINE_SIZE]; | ||
| 190 | + for (size_t i = 0; i <= buf.len; ++i) { | ||
| 191 | + if (i == buf.len || buf.data[i] == '\n') { | ||
| 192 | + size_t lineLen = i - lineStart; | ||
| 193 | + size_t cap = sizeof(line) - 1; | ||
| 194 | + size_t copyLen = lineLen < cap ? lineLen : cap; | ||
| 195 | + for (size_t k = 0; k < copyLen; ++k) { | ||
| 196 | + line[k] = buf.data[lineStart + k]; | ||
| 197 | + } | ||
| 198 | + line[copyLen] = '\0'; | ||
| 199 | + if (copyLen > 0) { | ||
| 200 | + __android_log_write(ANDROID_LOG_FATAL, LOG_TAG, line); | ||
| 201 | + } | ||
| 202 | + lineStart = i + 1; | ||
| 203 | + } | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + buf.len = 0; // allow batch flushing from DumpBacktrace | ||
| 207 | +} | ||
| 208 | + | ||
| 209 | +// --------------------------------------------------------------------------- | ||
| 210 | +// /proc/self/maps parsing (async-signal-safe: open/read/close only) | ||
| 211 | +// --------------------------------------------------------------------------- | ||
| 212 | +bool ParseHex(const char*& cursor, const char* end, uintptr_t& out) | ||
| 213 | +{ | ||
| 214 | + uintptr_t value = 0; | ||
| 215 | + bool found = false; | ||
| 216 | + while (cursor < end) { | ||
| 217 | + char ch = *cursor; | ||
| 218 | + int digit; | ||
| 219 | + if (ch >= '0' && ch <= '9') { | ||
| 220 | + digit = ch - '0'; | ||
| 221 | + } else if (ch >= 'a' && ch <= 'f') { | ||
| 222 | + digit = DECIMAL_BASE + (ch - 'a'); | ||
| 223 | + } else if (ch >= 'A' && ch <= 'F') { | ||
| 224 | + digit = DECIMAL_BASE + (ch - 'A'); | ||
| 225 | + } else { | ||
| 226 | + break; | ||
| 227 | + } | ||
| 228 | + value = (value << HEX_SHIFT) | static_cast<uintptr_t>(digit); | ||
| 229 | + found = true; | ||
| 230 | + ++cursor; | ||
| 231 | + } | ||
| 232 | + out = value; | ||
| 233 | + return found; | ||
| 234 | +} | ||
| 235 | + | ||
| 236 | +const char* SkipSpaces(const char* cursor, const char* end) | ||
| 237 | +{ | ||
| 238 | + while (cursor < end && (*cursor == ' ' || *cursor == '\t')) { | ||
| 239 | + ++cursor; | ||
| 240 | + } | ||
| 241 | + return cursor; | ||
| 242 | +} | ||
| 243 | + | ||
| 244 | +// Advances past one whitespace-delimited token, returning its end pointer. | ||
| 245 | +const char* SkipToken(const char* cursor, const char* end) | ||
| 246 | +{ | ||
| 247 | + while (cursor < end && *cursor != ' ' && *cursor != '\t' && *cursor != '\n') { | ||
| 248 | + ++cursor; | ||
| 249 | + } | ||
| 250 | + return cursor; | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +// Parses one maps line in [line, end). Returns false for malformed lines. | ||
| 254 | +// `line` is mutable so the caller's buffer can be null-terminated in place. | ||
| 255 | +bool ParseMapLine(char* line, const char* end, MapRecord& rec) | ||
| 256 | +{ | ||
| 257 | + const char* cursor = line; | ||
| 258 | + | ||
| 259 | + uintptr_t start = 0; | ||
| 260 | + if (!ParseHex(cursor, end, start)) { | ||
| 261 | + return false; | ||
| 262 | + } | ||
| 263 | + if (cursor < end && *cursor == '-') { | ||
| 264 | + ++cursor; | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + uintptr_t mapEnd = 0; | ||
| 268 | + if (!ParseHex(cursor, end, mapEnd)) { | ||
| 269 | + return false; | ||
| 270 | + } | ||
| 271 | + if (mapEnd <= start) { | ||
| 272 | + return false; | ||
| 273 | + } | ||
| 274 | + | ||
| 275 | + cursor = SkipSpaces(cursor, end); | ||
| 276 | + | ||
| 277 | + // Permissions token, e.g. "r-xp". | ||
| 278 | + const char* perms = cursor; | ||
| 279 | + cursor = SkipToken(cursor, end); | ||
| 280 | + bool isExec = false; | ||
| 281 | + for (const char* ch = perms; ch < cursor; ++ch) { | ||
| 282 | + if (*ch == 'x') { | ||
| 283 | + isExec = true; | ||
| 284 | + break; | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + cursor = SkipSpaces(cursor, end); | ||
| 288 | + | ||
| 289 | + uintptr_t offset = 0; | ||
| 290 | + ParseHex(cursor, end, offset); // file offset (may be absent for some entries) | ||
| 291 | + | ||
| 292 | + // Skip dev token and inode token. | ||
| 293 | + cursor = SkipSpaces(cursor, end); | ||
| 294 | + cursor = SkipToken(cursor, end); | ||
| 295 | + cursor = SkipSpaces(cursor, end); | ||
| 296 | + cursor = SkipToken(cursor, end); | ||
| 297 | + cursor = SkipSpaces(cursor, end); | ||
| 298 | + | ||
| 299 | + // Remainder of the line is the pathname. Point directly into g_mapsFileBuf | ||
| 300 | + // (no fixed-size copy, so the path is never truncated) and null-terminate it | ||
| 301 | + // in place by overwriting the trailing '\n' (the buffer is ours/mutable). | ||
| 302 | + rec.name = cursor; | ||
| 303 | + while (cursor < end && *cursor != '\n') { | ||
| 304 | + ++cursor; | ||
| 305 | + } | ||
| 306 | + line[cursor - line] = '\0'; // null-terminate the name in place (buffer is mutable) | ||
| 307 | + | ||
| 308 | + rec.start = start; | ||
| 309 | + rec.end = mapEnd; | ||
| 310 | + rec.offset = offset; | ||
| 311 | + rec.executable = isExec; | ||
| 312 | + return true; | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +void LoadMaps() | ||
| 316 | +{ | ||
| 317 | + g_mapCount = 0; | ||
| 318 | + int fd = open("/proc/self/maps", O_RDONLY | O_CLOEXEC); | ||
| 319 | + if (fd < 0) { | ||
| 320 | + return; | ||
| 321 | + } | ||
| 322 | + | ||
| 323 | + size_t total = 0; | ||
| 324 | + // Leave one byte so ParseMapLine can always null-terminate a name in place. | ||
| 325 | + while (total < MAPS_FILE_BUF_SIZE - 1) { | ||
| 326 | + ssize_t bytes = read(fd, g_mapsFileBuf + total, MAPS_FILE_BUF_SIZE - 1 - total); | ||
| 327 | + if (bytes > 0) { | ||
| 328 | + total += static_cast<size_t>(bytes); | ||
| 329 | + } else if (bytes == 0) { | ||
| 330 | + break; | ||
| 331 | + } else if (errno == EINTR) { | ||
| 332 | + continue; | ||
| 333 | + } else { | ||
| 334 | + break; | ||
| 335 | + } | ||
| 336 | + } | ||
| 337 | + close(fd); | ||
| 338 | + | ||
| 339 | + char* cursor = g_mapsFileBuf; | ||
| 340 | + char* fileEnd = g_mapsFileBuf + total; | ||
| 341 | + while (cursor < fileEnd && g_mapCount < MAX_MAP_RECORDS) { | ||
| 342 | + char* lineEnd = cursor; | ||
| 343 | + while (lineEnd < fileEnd && *lineEnd != '\n') { | ||
| 344 | + ++lineEnd; | ||
| 345 | + } | ||
| 346 | + if (ParseMapLine(cursor, lineEnd, g_maps[g_mapCount])) { | ||
| 347 | + ++g_mapCount; | ||
| 348 | + } | ||
| 349 | + cursor = (lineEnd < fileEnd) ? lineEnd + 1 : fileEnd; | ||
| 350 | + } | ||
| 351 | +} | ||
| 352 | + | ||
| 353 | +const MapRecord* FindRecord(uintptr_t addr) | ||
| 354 | +{ | ||
| 355 | + for (size_t i = 0; i < g_mapCount; ++i) { | ||
| 356 | + const MapRecord& rec = g_maps[i]; | ||
| 357 | + if (addr >= rec.start && addr < rec.end) { | ||
| 358 | + return &rec; | ||
| 359 | + } | ||
| 360 | + } | ||
| 361 | + return nullptr; | ||
| 362 | +} | ||
| 363 | + | ||
| 364 | +bool NamesMatch(const char* lhs, const char* rhs) | ||
| 365 | +{ | ||
| 366 | + if (lhs == nullptr || rhs == nullptr) { | ||
| 367 | + return false; | ||
| 368 | + } | ||
| 369 | + while (*lhs != '\0' && *rhs != '\0') { | ||
| 370 | + if (*lhs != *rhs) { | ||
| 371 | + return false; | ||
| 372 | + } | ||
| 373 | + ++lhs; | ||
| 374 | + ++rhs; | ||
| 375 | + } | ||
| 376 | + return *lhs == *rhs; // equal only if both ended together | ||
| 377 | +} | ||
| 378 | + | ||
| 379 | +// Returns the load base (runtime address of vaddr 0) of the shared object that | ||
| 380 | +// owns `mod`: the mapping with file offset 0 and the SAME name. We match by | ||
| 381 | +// name (MapRecord.name points into the raw maps text, so it is never truncated) | ||
| 382 | +// because an address-only heuristic fails: /proc/self/maps also contains huge | ||
| 383 | +// anonymous regions (scudo reserves, heap) whose offset is 0, and those would | ||
| 384 | +// be picked instead of the library's own base. | ||
| 385 | +uintptr_t FindLoadBase(const MapRecord* mod) | ||
| 386 | +{ | ||
| 387 | + if (mod == nullptr || mod->name[0] == '\0') { | ||
| 388 | + return 0; | ||
| 389 | + } | ||
| 390 | + uintptr_t best = 0; | ||
| 391 | + for (size_t i = 0; i < g_mapCount; ++i) { | ||
| 392 | + const MapRecord& rec = g_maps[i]; | ||
| 393 | + if (rec.offset == 0 && rec.start <= mod->start && rec.start > best && NamesMatch(rec.name, mod->name)) { | ||
| 394 | + best = rec.start; | ||
| 395 | + } | ||
| 396 | + } | ||
| 397 | + return best; | ||
| 398 | +} | ||
| 399 | + | ||
| 400 | +// --------------------------------------------------------------------------- | ||
| 401 | +// Frame-pointer chain walk + dump | ||
| 402 | +// --------------------------------------------------------------------------- | ||
| 403 | +void DumpSignalHeader(DumpBuffer& buf, int sig, siginfo_t* info) | ||
| 404 | +{ | ||
| 405 | + AppendLit(buf, "\n================ ArkUI-x native crash ================\n"); | ||
| 406 | + AppendLit(buf, "Fatal signal "); | ||
| 407 | + AppendDec(buf, static_cast<uint32_t>(sig)); | ||
| 408 | + AppendLit(buf, " ("); | ||
| 409 | + switch (sig) { | ||
| 410 | + case SIGSEGV: AppendLit(buf, "SIGSEGV"); break; | ||
| 411 | + case SIGABRT: AppendLit(buf, "SIGABRT"); break; | ||
| 412 | + case SIGBUS: AppendLit(buf, "SIGBUS"); break; | ||
| 413 | + case SIGILL: AppendLit(buf, "SIGILL"); break; | ||
| 414 | + case SIGFPE: AppendLit(buf, "SIGFPE"); break; | ||
| 415 | + default: AppendLit(buf, "?"); break; | ||
| 416 | + } | ||
| 417 | + AppendLit(buf, "), si_code "); | ||
| 418 | + AppendDecSigned(buf, info ? info->si_code : 0); | ||
| 419 | + if (info != nullptr && info->si_addr != nullptr) { | ||
| 420 | + AppendLit(buf, ", fault addr "); | ||
| 421 | + AppendHex0x(buf, reinterpret_cast<uintptr_t>(info->si_addr)); | ||
| 422 | + } | ||
| 423 | + AppendCh(buf, '\n'); | ||
| 424 | +} | ||
| 425 | + | ||
| 426 | +void DumpBacktrace(DumpBuffer& buf, uintptr_t fp) | ||
| 427 | +{ | ||
| 428 | + AppendLit(buf, "Backtrace (frame-pointer chain):\n"); | ||
| 429 | + | ||
| 430 | + | ||
| 431 | + if (fp == 0) { | ||
| 432 | + AppendLit(buf, " fp == 0, cannot walk the chain\n"); | ||
| 433 | + return; | ||
| 434 | + } | ||
| 435 | + | ||
| 436 | + // The mapping holding the initial fp is (one of) the stack mapping(s); use it | ||
| 437 | + // to bound the walk so a corrupted chain cannot run away. If unknown, fall | ||
| 438 | + // back to the monotonic-up check plus the depth cap. | ||
| 439 | + const MapRecord* stack = FindRecord(fp); | ||
| 440 | + const uintptr_t stackLo = stack ? stack->start : 0; | ||
| 441 | + const uintptr_t stackHi = stack ? stack->end : 0; | ||
| 442 | + | ||
| 443 | + for (uint32_t idx = 0; idx < MAX_STACK_DEPTH; ++idx) { | ||
| 444 | + // The whole frame [fp, fp + 2*sizeof(uintptr_t)) must lie inside the | ||
| 445 | + // stack mapping before we dereference it. Checking only `fp < stackHi` | ||
| 446 | + // is not enough: the two 8-byte reads at fp/fp+8 would still run past | ||
| 447 | + // the mapping edge when fp sits in the last 16 bytes and hit a guard | ||
| 448 | + // page, re-faulting inside the handler. When fp does not fall in any | ||
| 449 | + // known stack mapping (nullptr) there is no safe bound to rely on, so | ||
| 450 | + // stop the walk rather than dereference blindly. | ||
| 451 | + if (stack != nullptr) { | ||
| 452 | + if (fp < stackLo || fp + FRAME_POINTER_SLOTS * sizeof(uintptr_t) > stackHi) { | ||
| 453 | + break; | ||
| 454 | + } | ||
| 455 | + } else { | ||
| 456 | + // fp did not land in any parsed mapping (e.g. the stack mapping was | ||
| 457 | + // truncated by MAX_MAP_RECORDS). Rather than give up the whole | ||
| 458 | + // backtrace, do a minimal sanity check on the AArch64 16-byte frame | ||
| 459 | + // alignment and attempt the read. A second fault here is still caught | ||
| 460 | + // by the g_inHandler re-entrancy guard. | ||
| 461 | + if (fp == 0 || (fp & 0xf) != 0) { | ||
| 462 | + break; | ||
| 463 | + } | ||
| 464 | + } | ||
| 465 | + | ||
| 466 | + // Frame layout produced by the AArch64 prologue (stp x29, x30): | ||
| 467 | + // [fp + 0] = saved x29 (link to the previous frame) | ||
| 468 | + // [fp + 8] = saved x30 (return address / LR) | ||
| 469 | + uintptr_t lr = *reinterpret_cast<uintptr_t*>(fp + 8); | ||
| 470 | + uintptr_t next = *reinterpret_cast<uintptr_t*>(fp); | ||
| 471 | + uintptr_t callSite = (lr != 0) ? lr - INSTRUCTION_LENGTH : 0; | ||
| 472 | + | ||
| 473 | + // A deep native stack can hold more frames than DUMP_BUF_SIZE; flush | ||
| 474 | + // and reuse the buffer before appending this frame so frames are never | ||
| 475 | + // silently dropped. FlushDump is async-signal-safe and resets buf.len. | ||
| 476 | + if (buf.len > DUMP_BUF_SIZE - FLUSH_THRESHOLD) { | ||
| 477 | + FlushDump(buf); | ||
| 478 | + } | ||
| 479 | + | ||
| 480 | + AppendLit(buf, " #"); | ||
| 481 | + AppendDec(buf, idx); | ||
| 482 | + AppendLit(buf, " pc="); | ||
| 483 | + AppendHex0x(buf, callSite); | ||
| 484 | + | ||
| 485 | + const MapRecord* mod = FindRecord(callSite); | ||
| 486 | + if (mod != nullptr) { | ||
| 487 | + AppendLit(buf, " "); | ||
| 488 | + AppendStr(buf, mod->name); | ||
| 489 | + AppendLit(buf, " +0x"); | ||
| 490 | + // Print the vaddr = callSite - load base, which addr2line / | ||
| 491 | + // llvm-symbolizer consume directly (no manual +base correction). | ||
| 492 | + // Fall back to the mapping-relative offset if the load base is unknown. | ||
| 493 | + uintptr_t loadBase = FindLoadBase(mod); | ||
| 494 | + uintptr_t relOffset = (loadBase != 0) ? (callSite - loadBase) : (callSite - mod->start); | ||
| 495 | + AppendHex(buf, relOffset); | ||
| 496 | + } | ||
| 497 | + AppendCh(buf, '\n'); | ||
| 498 | + | ||
| 499 | + // The saved fp must point upward (stack grows down); otherwise the | ||
| 500 | + // chain has ended or is corrupted. The full [next, next+16) range is | ||
| 501 | + // re-checked against the stack mapping at the top of the next iteration. | ||
| 502 | + if (next <= fp) { | ||
| 503 | + break; | ||
| 504 | + } | ||
| 505 | + fp = next; | ||
| 506 | + } | ||
| 507 | + | ||
| 508 | + (void)fp; // FP-chain walk is aarch64-only; nothing to traverse on this ABI. | ||
| 509 | + AppendLit(buf, " FP-chain walk is only implemented on aarch64 (arch skipped)\n"); | ||
| 510 | + | ||
| 511 | +} | ||
| 512 | + | ||
| 513 | +// --------------------------------------------------------------------------- | ||
| 514 | +// Signal handler | ||
| 515 | +// --------------------------------------------------------------------------- | ||
| 516 | +std::atomic<bool> g_installed { false }; | ||
| 517 | +// Process-wide (NOT thread_local) on purpose. While one thread is dumping | ||
| 518 | +// (reading /proc/self/maps, walking the FP chain, writing the system log), any other | ||
| 519 | +// thread that crashes skips its own dump and just forwards to the previous | ||
| 520 | +// handler. This is a deliberate trade-off: the maps model (g_maps / | ||
| 521 | +// g_mapsFileBuf) is process-global, so letting two threads dump concurrently | ||
| 522 | +// would race and corrupt it (and risk a second fault). Consequence: in a | ||
| 523 | +// multi-threaded crash, only the first crashing thread emits a backtrace. | ||
| 524 | +std::atomic<bool> g_inHandler { false }; | ||
| 525 | +struct sigaction g_savedHandlers[NSIG]; | ||
| 526 | + | ||
| 527 | +// Hands control back to whatever handler (if any) was installed before us, and | ||
| 528 | +// ensures a SIG_DFL disposition still terminates with a system crash report. | ||
| 529 | +void ForwardToPrevious(int sig, siginfo_t* info, void* context) | ||
| 530 | +{ | ||
| 531 | + const struct sigaction& prev = g_savedHandlers[sig]; | ||
| 532 | + if (prev.sa_flags & SA_SIGINFO) { | ||
| 533 | + if (prev.sa_sigaction != nullptr) { | ||
| 534 | + prev.sa_sigaction(sig, info, context); | ||
| 535 | + return; | ||
| 536 | + } | ||
| 537 | + } else if (prev.sa_handler != SIG_DFL && prev.sa_handler != SIG_IGN && prev.sa_handler != SIG_ERR) { | ||
| 538 | + prev.sa_handler(sig); | ||
| 539 | + return; | ||
| 540 | + } | ||
| 541 | + | ||
| 542 | + // SIG_DFL / SIG_IGN / nothing usable: restore the prior disposition and | ||
| 543 | + // re-raise so the default action (crash report/coredump) runs. | ||
| 544 | + sigaction(sig, &prev, nullptr); | ||
| 545 | + if (prev.sa_handler != SIG_IGN) { | ||
| 546 | + sigset_t mask; | ||
| 547 | + sigemptyset(&mask); | ||
| 548 | + sigaddset(&mask, sig); | ||
| 549 | + sigprocmask(SIG_UNBLOCK, &mask, nullptr); | ||
| 550 | + raise(sig); | ||
| 551 | + } | ||
| 552 | +} | ||
| 553 | + | ||
| 554 | +void CrashHandler(int sig, siginfo_t* info, void* context) | ||
| 555 | +{ | ||
| 556 | + // Guard against re-entrancy (a fault while dumping). If we are already inside | ||
C g_inHandler 为进程级全局,转储期间其他线程崩溃不会输出回栈 注释将其描述为防"re-entrancy(重入)",但 g_inHandler 是单个 std::atomic Severity: info ![]() ![]() | |||
| 557 | + // the handler, skip the dump and just forward. | ||
| 558 | + bool expected = false; | ||
| 559 | + if (!g_inHandler.compare_exchange_strong(expected, true)) { | ||
| 560 | + ForwardToPrevious(sig, info, context); | ||
| 561 | + return; | ||
| 562 | + } | ||
| 563 | + | ||
| 564 | + uintptr_t fp = 0; | ||
| 565 | + | ||
| 566 | + if (context != nullptr) { | ||
| 567 | + auto* uc = static_cast<ucontext_t*>(context); | ||
| 568 | + fp = uc->uc_mcontext.regs[REG_FP]; // saved x29 of the interrupted frame | ||
| 569 | + } | ||
| 570 | + | ||
| 571 | + | ||
| 572 | + DumpBuffer dump {}; | ||
| 573 | + DumpSignalHeader(dump, sig, info); | ||
| 574 | + LoadMaps(); | ||
| 575 | + DumpBacktrace(dump, fp); | ||
| 576 | + AppendLit(dump, "======================================================\n"); | ||
| 577 | + FlushDump(dump); | ||
| 578 | + | ||
| 579 | + g_inHandler.store(false); | ||
| 580 | + | ||
| 581 | + ForwardToPrevious(sig, info, context); | ||
| 582 | +} | ||
| 583 | + | ||
| 584 | +// --------------------------------------------------------------------------- | ||
| 585 | +// Installation (runs in normal context) | ||
| 586 | +// --------------------------------------------------------------------------- | ||
| 587 | +void InstallAltStack() | ||
| 588 | +{ | ||
| 589 | + // Static so the buffer outlives the call. sigaltstack is per-thread on the | ||
| 590 | + // system libc, so this protects the thread that runs JNI_OnLoad; other | ||
| 591 | + // threads fall back to their own stack (stack-overflow on them may not be | ||
| 592 | + // caught, ordinary SEGV/ABRT still are). | ||
| 593 | + static char altStack[ALT_STACK_SIZE]; | ||
| 594 | + stack_t sigStack {}; | ||
| 595 | + sigStack.ss_sp = altStack; | ||
| 596 | + sigStack.ss_size = sizeof(altStack); | ||
| 597 | + sigStack.ss_flags = 0; | ||
| 598 | + sigaltstack(&sigStack, nullptr); | ||
| 599 | +} | ||
| 600 | + | ||
| 601 | +} // namespace | ||
| 602 | + | ||
| 603 | +void InitBacktraceHandler() | ||
| 604 | +{ | ||
| 605 | + bool expected = false; | ||
| 606 | + if (!g_installed.compare_exchange_strong(expected, true)) { | ||
| 607 | + return; // already installed | ||
| 608 | + } | ||
| 609 | + | ||
| 610 | + InstallAltStack(); | ||
| 611 | + | ||
| 612 | + struct sigaction sa {}; | ||
| 613 | + sa.sa_sigaction = CrashHandler; | ||
| 614 | + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; // SA_ONSTACK -> run on the alternate stack | ||
| 615 | + sigemptyset(&sa.sa_mask); | ||
| 616 | + | ||
| 617 | + const int handledSignals[] = { SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE }; | ||
| 618 | + for (int sig : handledSignals) { | ||
| 619 | + struct sigaction old {}; | ||
| 620 | + if (sigaction(sig, &sa, &old) == 0) { | ||
| 621 | + g_savedHandlers[sig] = old; | ||
| 622 | + } | ||
| 623 | + } | ||
| 624 | + | ||
| 625 | + LOGI("ArkUI-x backtrace handler installed."); | ||
| 626 | +} | ||
| 627 | + | ||
| 628 | +void DumpBacktraceFromFp(uintptr_t fp) | ||
| 629 | +{ | ||
| 630 | + bool expected = false; | ||
| 631 | + if (!g_inHandler.compare_exchange_strong(expected, true)) { | ||
| 632 | + return; // a crash dump is in progress on this thread | ||
| 633 | + } | ||
| 634 | + | ||
| 635 | + DumpBuffer dump {}; | ||
| 636 | + LoadMaps(); | ||
| 637 | + DumpBacktrace(dump, fp); | ||
| 638 | + AppendLit(dump, "(end of manual backtrace)\n"); | ||
| 639 | + FlushDump(dump); | ||
| 640 | + | ||
| 641 | + g_inHandler.store(false); | ||
| 642 | +} | ||
| 643 | + | ||
| 644 | +} // namespace OHOS::Ace::Platform | ||
| @@ -0,0 +1,57 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | +namespace OHOS::Ace::Platform { | ||
| 22 | + | ||
| 23 | +// Installs async-signal-safe crash handlers (SIGSEGV/SIGABRT/SIGBUS/SIGILL/SIGFPE) | ||
| 24 | +// that dump a frame-pointer based native backtrace. Idempotent; safe to call more | ||
| 25 | +// than once. Designed to be invoked once from JNI_OnLoad of libarkui_android.so. | ||
| 26 | +// | ||
| 27 | +// The whole dump path is async-signal-safe: no malloc, no stdio, no locks, no | ||
| 28 | +// HILOG. It reads /proc/self/maps through open()/read() and walks the FP chain | ||
| 29 | +// (x29) with hand-rolled integer formatting, emitting via write(2) and | ||
| 30 | +// __android_log_write. Any pre-existing handler (the runtime / signal chain / | ||
| 31 | +// crash reporter) is saved and chained to, so a system crash report is still | ||
| 32 | +// produced. | ||
| 33 | +// | ||
| 34 | +// The FP chain is only walked on aarch64; on other ABIs only the signal is | ||
| 35 | +// reported. Stack frames that omit the frame pointer (JIT/JS, some third-party | ||
| 36 | +// libs) terminate the chain, so the dump covers the native C++ caller frames | ||
| 37 | +// around the crash rather than the full stack. | ||
| 38 | +// | ||
| 39 | +// Each printed frame is "<module>+0x<vaddr>", where vaddr = runtime_pc - the | ||
| 40 | +// module's load base. That value is consumable by addr2line / llvm-symbolizer | ||
| 41 | +// directly (point them at the matching unstripped .so), with no manual base | ||
| 42 | +// correction needed. | ||
| 43 | +// Multi-threading: the re-entrancy guard is process-wide, so if several threads | ||
| 44 | +// crash concurrently only the first one dumps a backtrace; the others forward | ||
| 45 | +// straight to the previous handler. This avoids racing the process-global maps | ||
| 46 | +// model. | ||
| 47 | +void InitBacktraceHandler(); | ||
| 48 | + | ||
| 49 | +// Dumps the frame-pointer chain starting at the given frame pointer, using the | ||
| 50 | +// same async-signal-safe code path as the crash handler. Exposed so the walk can | ||
| 51 | +// be triggered from a normal context (e.g. diagnostics or a forced-crash test). | ||
| 52 | +// No-op when fp == 0 or on non-aarch64 ABIs. | ||
| 53 | +void DumpBacktraceFromFp(uintptr_t fp); | ||
| 54 | + | ||
| 55 | +} // namespace OHOS::Ace::Platform | ||
| 56 | + | ||
| 57 | + | ||
| @@ -15,6 +15,7 @@ | |||
| 15 | 15 | ||
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | + | ||
| 18 | 19 | ||
| 19 | 20 | ||
| 20 | 21 | ||
| @@ -55,6 +56,12 @@ jint JNI_OnLoad(JavaVM* vm, void*) | |||
| 55 | return RET_FAIL; | 56 | return RET_FAIL; |
| 56 | } | 57 | } |
| 57 | 58 | ||
| 59 | + // Install async-signal-safe FP-chain crash handlers so native crashes in | ||
| 60 | + // libarkui_android.so still produce a backtrace even when unwind tables are | ||
| 61 | + // stripped for size. Must run after the JVM/JS runtime is up so the saved | ||
| 62 | + // (previous) handlers it chains to are already in place. | ||
| 63 | + OHOS::Ace::Platform::InitBacktraceHandler(); | ||
| 64 | + | ||
| 58 | LOGI("JNI Onload: sharedlibrary has been loaded successsfully!"); | 65 | LOGI("JNI Onload: sharedlibrary has been loaded successsfully!"); |
| 59 | return OHOS::Ace::Platform::JniEnvironment::GetInstance().Version(); | 66 | return OHOS::Ace::Platform::JniEnvironment::GetInstance().Version(); |
| 60 | } | 67 | } |


DUMP_BUF_SIZE 过小,深层栈回溯会被静默截断
DumpBuffer 容量仅 8192 字节,而 MAX_STACK_DEPTH=64。单帧输出形如 " #NN pc=0x<16hex> /data/app/.../lib/arm64-v8a/libxxx.so+0x\n",真实 Android .so 路径常超过 100 字节,整帧约 130 字节;64 帧 × 130 ≈ 8320,尚未计入 DumpSignalHeader、标签行与结尾分隔行(合计约 300+ 字节)。即对深层原生栈(恰恰是本处理器的目标场景),缓冲区必然在遍历中途被填满,而 AppendCh(line 80 if (b.len < DUMP_BUF_SIZE - 1))一旦满即静默丢弃,最深处的帧被无声截断且无截断标记。建议在 DumpBacktrace 帧循环中当 dump.len 接近上限时调用 FlushDump 后将 dump.len 置 0 分批输出(保持全异步信号安全),或增大 DUMP_BUF_SIZE(需注意 DumpBuffer 作为局部变量落在信号栈上,勿超过栈容量)。
Severity: warning