当你的 C++ 轰然倒下:HarmonyOS Native 层(类 Linux)崩溃信号全集与生存指南
开发者们~如果你写过 HarmonyOS NEXT 的 C++ / NAPI / Cocos / 自研引擎,迟早会撞上那种让人血压飙升的时刻------真机调试一切正常,一到 QA 手上就莫名其妙闪退,DevEco 只甩你一行冷冰冰的 CppCrash: signal 11 (SIGSEGV)。
这玩意儿说穿了不神秘:HarmonyOS NEXT 的用户态跑在标准 Linux 内核之上 (或者严格说 OpenHarmony 的 Linux 变体),你的 Native 进程一旦越界,内核的异常分发就会把矛头翻译成一个 POSIX 信号(signal) 扔过来;系统再把寄存器、调用栈、maps、内存快照打包成一份崩溃报告(概念上等同于 Android 的 tombstone),最后注入 hiAppEvent 的 APP_CRASH 事件里送走。
今天我们不念 man page,把这套"信号全集 → 为什么会炸 → 怎么在现场捞线索 → 怎么别把 signal handler 写成定时炸弹"彻底说透,顺带瞄一眼 HarmonyOS 6(API 22) 可能拧紧的螺丝。
一、信号不是"错误码",是内核亲手拍醒你哦
Linux(以及 OpenHarmony 的用户 POSIX 环境)里,硬件异常(MMU 缺页保护触发、执行到非法指令编码、整数除以零陷进特定向量)全部由内核的异常路径统一收束 ,最终翻译成 signal 送到进程。它不是 C++ throw,不走栈展开,不认 RAII------信号投递的瞬间,你的执行流就被劫持了。
下面这张彩色图把"你解引用野指针 → 内核翻脸 → 系统写崩溃报告"的完整链路画清楚:
#mermaid-svg-jE5UkQwIRFUxJXSB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jE5UkQwIRFUxJXSB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jE5UkQwIRFUxJXSB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jE5UkQwIRFUxJXSB .error-icon{fill:#552222;}#mermaid-svg-jE5UkQwIRFUxJXSB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jE5UkQwIRFUxJXSB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jE5UkQwIRFUxJXSB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jE5UkQwIRFUxJXSB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jE5UkQwIRFUxJXSB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jE5UkQwIRFUxJXSB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jE5UkQwIRFUxJXSB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jE5UkQwIRFUxJXSB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jE5UkQwIRFUxJXSB .marker.cross{stroke:#333333;}#mermaid-svg-jE5UkQwIRFUxJXSB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jE5UkQwIRFUxJXSB p{margin:0;}#mermaid-svg-jE5UkQwIRFUxJXSB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jE5UkQwIRFUxJXSB .cluster-label text{fill:#333;}#mermaid-svg-jE5UkQwIRFUxJXSB .cluster-label span{color:#333;}#mermaid-svg-jE5UkQwIRFUxJXSB .cluster-label span p{background-color:transparent;}#mermaid-svg-jE5UkQwIRFUxJXSB .label text,#mermaid-svg-jE5UkQwIRFUxJXSB span{fill:#333;color:#333;}#mermaid-svg-jE5UkQwIRFUxJXSB .node rect,#mermaid-svg-jE5UkQwIRFUxJXSB .node circle,#mermaid-svg-jE5UkQwIRFUxJXSB .node ellipse,#mermaid-svg-jE5UkQwIRFUxJXSB .node polygon,#mermaid-svg-jE5UkQwIRFUxJXSB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jE5UkQwIRFUxJXSB .rough-node .label text,#mermaid-svg-jE5UkQwIRFUxJXSB .node .label text,#mermaid-svg-jE5UkQwIRFUxJXSB .image-shape .label,#mermaid-svg-jE5UkQwIRFUxJXSB .icon-shape .label{text-anchor:middle;}#mermaid-svg-jE5UkQwIRFUxJXSB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jE5UkQwIRFUxJXSB .rough-node .label,#mermaid-svg-jE5UkQwIRFUxJXSB .node .label,#mermaid-svg-jE5UkQwIRFUxJXSB .image-shape .label,#mermaid-svg-jE5UkQwIRFUxJXSB .icon-shape .label{text-align:center;}#mermaid-svg-jE5UkQwIRFUxJXSB .node.clickable{cursor:pointer;}#mermaid-svg-jE5UkQwIRFUxJXSB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jE5UkQwIRFUxJXSB .arrowheadPath{fill:#333333;}#mermaid-svg-jE5UkQwIRFUxJXSB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jE5UkQwIRFUxJXSB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jE5UkQwIRFUxJXSB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jE5UkQwIRFUxJXSB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jE5UkQwIRFUxJXSB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jE5UkQwIRFUxJXSB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jE5UkQwIRFUxJXSB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jE5UkQwIRFUxJXSB .cluster text{fill:#333;}#mermaid-svg-jE5UkQwIRFUxJXSB .cluster span{color:#333;}#mermaid-svg-jE5UkQwIRFUxJXSB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jE5UkQwIRFUxJXSB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jE5UkQwIRFUxJXSB rect.text{fill:none;stroke-width:0;}#mermaid-svg-jE5UkQwIRFUxJXSB .icon-shape,#mermaid-svg-jE5UkQwIRFUxJXSB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jE5UkQwIRFUxJXSB .icon-shape p,#mermaid-svg-jE5UkQwIRFUxJXSB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jE5UkQwIRFUxJXSB .icon-shape .label rect,#mermaid-svg-jE5UkQwIRFUxJXSB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jE5UkQwIRFUxJXSB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jE5UkQwIRFUxJXSB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jE5UkQwIRFUxJXSB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ☠️ 进程终局
📁 系统崩溃收集(类 tombstone)
📨 信号投递 & crash handler
🐧 Linux Kernel 异常分发
🧠 CPU / MMU
💣 你的 C++ 代码
执行流越界
触发异常行为
• 解引用野指针 / 悬垂引用
• 栈溢出冲刷返回地址
• 除以 0(整型
• 执行到坏指令 / 裁缝过的 so
#PF(缺页异常)/ #DE / #UD ...
→ 陷入内核异常处理
arch 异常入口
→ 判定 fault 类型
→ 决定投递哪个 signal
是哪个信号?
SIGSEGV
(Addr/Bound fault)
SIGBUS
(对齐/PF 特殊态)
SIGILL
(非法指令)
SIGFPE
(算术)
SIGABRT
(abort / assert /
__fortify_fail)
挂起其余线程
→ 读寄存器 / unwind 回溯
→ 读 /proc/pid/maps
→ 写崩溃记录
→ 送入 hiAppEvent APP_CRASH
kill(getpid(), signo)
→ 进程消亡
(或调试器 attach 接管)
一句话记住:信号是内核给进程发的"死刑判决书",不是你可以优雅 catch 住然后 pretend nothing happened 的异常。 能注册 sigaction 只是为了在死前写遗书(flush 日志、保存 minidump、fork 一个 helper 做记录),不是复活。
二、标准崩溃信号集:每个号码代表哪一种"死法"
以下是 HarmonyOS Native(类 Linux userspace)里 真正构成 CppCrash 的那组信号,按出现频率排序(经验值,不是官方比重):
1. SIGSEGV --- Signal 11 --- 段错误(出场率冠军)
含义:访问了不属于你的虚拟地址(野指针、delete 后继续使用、栈对象被返回引用出去后栈帧销毁、memcpy 越界踩进保护区)。
cpp
// ☠️ 经典
void boom_segv() {
int* p = nullptr;
*p = 42; // → SIGSEGV
}
// 另一种更阴险的:use-after-free(UA F)--- 不一定立刻炸,但迟早 SIGSEGV / SIGBUS
struct Widget { int x; };
Widget* w = new Widget{1};
delete w;
w->x = 2; // UB → 可能 SIGSEGV,也可能读脏数据"看起来正常"
内核视角 :MMU 收到访问地址 → 查页表 → no mapping → #PF → kernel delivers SIGSEGV,附带一个 si_addr(造成错误的地址)。
你会在崩溃报告里看到 :signal 11 (SIGSEGV), fault addr 0x00000000(或别的十六进制地址)。fault addr 0x0 基本坐实就是空指针解引用;非零却明显过小(< 0x1000)通常也是某个 offset 加到 null 指针上。
2. SIGABRT --- Signal 6 --- 主动自杀(第二常见,也是最"有迹可循"的)
含义 :进程自己调用了 abort(),或者(更常见)标准库的一致性检查炸了:
assert(expr)失败 → 打印Assertion \expr` failed→abort()`_FORTIFY_SOURCE的运行时检查(buffer overflow inmemcpy/strcpyfamily when sizes known at compile time)- libc++ 的
_LIBCPP_VERIFY/ iterator debug mode checks - 你自己的
if (bad) std::abort();
cpp
// 三种常见起手式,结果都是 SIGABRT
void cause_abrt_assert() {
int* p = nullptr;
assert(p && "p must not be null"); // → SIGABRT(不是 SIGSEGV!)
}
void cause_abrt_fortify() {
char buf[8];
// 如果 _FORTIFY_SOURCE 开着的(NDK/OH 工具链默认开),
// 这个会在运行时 __chk_fail → abort
strcpy(buf, "this_string_is_way_too_long");
}
void cause_abrt_manual() {
// 你的"绝不应当发生"兜底
std::abort();
}
关键差异 vs SIGSEGV :SIGABRT 意味着代码主动判定逻辑不可继续,通常前面跟着一条可读的错误信息。读崩溃日志时先看 abort message 再猜地址。
3. SIGFPE --- Signal 8 --- 算术异常
含义 :整数除以零(idiv 触发 #DE),或者(取决于架构/内核配置)浮ating point 异常被未masked时。
cpp
// ☠️ 在 x86/arm 上,整数 div-by-zero 走 SIGFPE
int divide_by_zero() {
volatile int a = 1;
volatile int b = 0;
return a / b; // → SIGFPE
}
实际经验:纯整数场景在 ARM 上有时走
SIGSEGV(如果除以零走了一个未handled 路径),但最终归类看 si_code(FPE_INTDIVvsSEGV_MAPERR)。别纠结编号,看 si_code。
4. SIGILL --- Signal 4 --- 非法指令
含义:PC 指向了一段不是合法指令编码的东西。典型原因:
- 函数指针被踩坏(栈溢出冲掉返回地址 → 跳进数据区)
.text被部分 corrupted / 自修改代码失败 / 加载了错误 ABI 的 so- 用错了指令集(跑 ARM64 binary 在 wrong context------但这通常直接 kernel kill)
- CFI / shadow call stack 检测到返回地址篡改(某些缓解措施会 deliberately
ud2→ SIGILL)
cpp
// 人为演示(别在真项目里写这种东西)
void jump_to_nonsense() {
void (*bad)() = reinterpret_cast<void(*)()>(0x41414141);
bad(); // PC = 0x41414141 → SIGILL(或 SIGSEGV,取决于 "地址是否 mapped+exec")
}
5. SIGBUS --- Signal 7 --- 总线错误
含义 :对齐违规(某些架构严格对齐,比如解引用 int* 于地址 0x...01),或者访问 mmap 区域时底层 I/O 错误(更少见在 pure RAM 场景)。在 Linux 上 SIGBUS 和 SIGSEGV 经常邻居 ,但 si_code 不同(BUS_ADRALN vs SEGV_MAPERR)。
cpp
// 对齐类:ARM 一般容忍 unaligned access,但某些配置下仍 BUS
// 更常见的是 mmap-ed 文件 truncated 后的访问
// (但 NDK/OH native 游戏里你大概率见 SIGSEGV 更多)
6. SIGTRAP --- Signal 5 --- 调试断点 / __builtin_trap()
不是"产品崩溃"的主力,但你会看到它:
- 你在代码里留了
__builtin_trap()或asm("bkpt 0")/asm("brk #0") - AddressSanitizer / UBSan 在 debug 构建里抓到 UB 后走这条
- 调试器断点命中(正常路径不算是 crash)
三、崩溃报告里你真正该盯的字段(而不只是 signal 号)
当崩溃最终落入系统收集管线后,它会沉淀到两处对你有用的地方:
- Logcat / hilog 输出 → 崩溃瞬间系统守护进程打印的 tombstone 风格片段(寄存器、backtrace
#XX pc ...) hiAppEvent.APP_CRASH的crash_info/stack/lastFrames(取决于版本/配置)
你读 tombstone 的行家方式:
Build fingerprint: ...
ABI: arm64-v8a
Timestamp: ...
Process name: com.yourapp.game
pid: 1234, tid: 1234
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
x0 0000000000000000 x1 ...
sp 0000007f12345678 lr 0000005599aabbcc pc 0000005599aabbdd
backtrace:
#00 pc 000000000001bbdd /data/storage/.../libcocos2d.so (GameObject::update()+28)
#01 pc 000000000002cc44 /data/storage/.../libcocos2d.so (Director::mainLoop()+196)
...
判读顺序:
signal+fault addr→ 定"死因大类"pc落在哪个.so+ offset → 用llvm-addr2line/ndk-stack+ 带符号 so (unstripped 或.symtab保留版)还原文件名:行号tid != pid→ 如果是子线程崩,看那线程是什么(render thread? audio? worker?)
四、举个例子哦
下面演示一个 纯粹防御性的 crash handler ,唯一目的是:崩的时候把一点上下文写到文件(并触发系统默认的 core/tombstone 收集),不尝试恢复执行(那是 undefined behavior 的地狱)。
cpp
// crash_handler.h
#pragma once
#include <signal.h>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
// 最小化:写一条遗言到一个已知可写路径,然后 re-raise 让系统接管
class CrashHandler {
public:
static void install(const char* logPath);
private:
static void sigAction(int sig, siginfo_t* info, void* ucontext);
static const char* s_logPath;
};
// crash_handler.cpp
#include "crash_handler.h"
#include <sys/types.h>
#include <fcntl.h>
const char* CrashHandler::s_logPath = nullptr;
void CrashHandler::install(const char* logPath) {
s_logPath = logPath;
struct sigaction sa{};
sa.sa_sigaction = sigAction;
sa.sa_flags = SA_SIGINFO | SA_RESTART; // SA_RESTART 其实对 fatal signals 没意义,但无害
sigemptyset(&sa.sa_mask);
// 只 hook 可生存信号子集;绝不拦截 SIGKILL(9) --- 它不能被捕获
sigaction(SIGSEGV, &sa, nullptr);
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGFPE, &sa, nullptr);
sigaction(SIGILL, &sa, nullptr);
sigaction(SIGBUS, &sa, nullptr);
}
void CrashHandler::sigAction(int sig, siginfo_t* info, void* /*ucontext*/) {
// 写遗言(注意:几乎什么都*不能*安全调用在这里;写 fd 是少数安全的之一)
if (s_logPath) {
int fd = open(s_logPath, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd >= 0) {
char buf[128];
int n = snprintf(buf, sizeof(buf),
"CrashHandler: sig=%d si_code=%d fault_addr=%p\n",
sig, info->si_code, info->si_addr);
write(fd, buf, n);
close(fd);
}
}
// 恢复默认处理(SIG_DFL)然后 re-raise ------ 把尸体交给系统 crash collector
struct sigaction sa_default{};
sa_default.sa_handler = SIG_DFL;
sigemptyset(&sa_default.sa_mask);
sigaction(sig, &sa_default, nullptr);
raise(sig); // ← 走到这,你就死了(系统接管 -> tombstone -> hiAppEvent)
}
调用点 (尽早,在 main() / NAPI init 里):
cpp
int main() {
CrashHandler::install("/data/storage/el2/base/files/.crash_trace.log");
// ... your engine init ...
}
这段话很重要(所以加粗):
永远别在这个 handler 里分配内存、调
std::string、碰 mutex(可能已死锁)、或试图做 C++ "cleanup destructor cascade"。你的进程的内存世界观已经破产。唯一合法的工具是 async-signal-safe 函数列表(read/write/open/close/fsync,直接系统调用)。
五、实际开发中差异现象
| 现象 | 你以为 | 真相(通常) |
|---|---|---|
| 只 Release 炸 / Debug 不炸 | "编译器 bug" | UB(use-after-free / uninitialized)被优化放大;或 _FORTIFY_SOURCE 在 Release 开得更狠 → 走 SIGABRT |
fault addr 看着像 0xdddddddd / 0x41414141 |
随机 | 典型 heap pattern(MSVC debug fill / 自定义 allocator fill pattern)→ 说明堆块被 free 后引用或 buffer 越界染色区 |
崩在 libc++abi.so / __cxa_pure_virtual |
链接问题 | 纯虚函数被调用 → 对象已被部分 destruct 或 vptr 踩坏 |
backtrace 全是 ?? / 无符号 |
strip 太狠 | 发版你 strip symtab,但 留一份 unstripped / split-debug .debug 文件用于 addr2line;否则 crash 报告只能靠裸 hex 猜 |
signal 31 (SIGSYS) |
??? | 你的代码(或三方 .so)触发了 seccomp filter(调用了被禁止的系统调用号)。在 HarmonyOS 收紧策略后更可能出现。 |
六、小小实践
1. _FORTIFY_SOURCE 与缓解措施升级 → 更多 SIGABRT,但更早
LLVM 系工具链的 fortify 检查在演进,OH 工具链也跟着走。后果是:过去"内存越界读侥幸活下来"的代码,新版本会在运行时被 __chk_fail 截停为 SIGABRT,并打印哪个接口炸的。
这不是坏事 ------它把潜伏炸弹提到台面上------但你需要 CI 里跑 asan/ubsan debug build 提前抓,不然验收流程里它突然冒头。
2. 更严格的 seccomp / syscall policy → 可能见 SIGSYS
如果你的 native 代码(或某个三方库)绕过了标准 libc 接口,直接发了 raw syscall(syscall(__NR_xxx)),API 22 的系统策略可能回报你一个 SIGSYS(signal 31)。
适配原则:别走 syscall bypass;用官方 NDK/NAPI 接口或 libc 公开 API。
3. tombstone / crash record 字段整理
系统崩溃报告格式可能微调(比如寄存器字段名、backtrace 行格式),但 hiAppEvent.APP_CRASH 的契约字段(crash_info/signal/stack)应是向下兼容的。你现在就养成良好的"用脚本解析 tombstone"的习惯,别手工肉眼读 200 行。
4. NDK 侧栈保护 / shadow call stack / BTI
ARM64 的缓解措施(S_CS / BTI)更多启用时,某些"靠巧合的跳转技巧"(函数指针 cast 链、手写汇编 trampoline)会从"可疑但活"变成 SIGILL。提前过一遍编译警告 -Wall -Wextra -Werror=return-type,并开 clang -fsanitize=address -fsanitize=undefined 做一轮 debug 构建冒烟,能省掉大量"API 22 突然翻脸"的 P0。
七、总结一下下
CppCrash 在 HarmonyOS Native 层,骨子里就是 POSIX 信号集,没有魔法:
- SIGSEGV = 你碰了不该碰的地址(野指针/UAF/越界)
- SIGABRT = 你(或 libc/libstdc++/fortify)判定的逻辑不可继续
- SIGFPE / SIGILL / SIGBUS = 算术/指令流/对齐层面的硬伤
- SIGTRAP = 调试/消毒器触发的断点,不是产品故障
能做的不是"catch 住继续跑",而是:
① 开 fortify + sanitizer 在 dev 阶段把 UB 逼出来;
② 发版留 unstripped .so 副本 做 addr2line 还原;
③ 崩时用 async-signal-safe 手段写遗言,然后交还系统收集管线(它会喂进 APP_CRASH)。
信号不跟你商量~但只要你把复现路径、寄存器和 backtrace 还原做成日常流水线的一部分,CppCrash 就不是鬼故事,只是修 bug 的线索。