🔧【Crash】Crash排查指南
看懂堆栈:看懂堆栈
#AI生成 #GPT
Crash 排查指南
基于 khhicar_serv SIGSEGV 崩溃排查实践总结。
1. Crash Dump 基础
1.1 Dump 结构
Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x003179786f725074
│ │ │
│ │ └── 崩溃地址(内核记录,CPU 访问的非法地址)
│ └── 信号类型(MAPERR=地址未映射,ACCERR=权限不足)
└── 崩溃信号
- SEGV_MAPERR:访问的虚拟地址没有映射到任何物理内存或文件
- SEGV_ACCERR:地址已映射,但权限不对(如写只读内存)
1.2 寄存器表
Registers:
x0:0000007548b44d60 x1:0000000000000081 ...
- x0~x30 是 ARM64 通用寄存器
- pc:程序计数器,指向当前执行的指令地址
- lr:链接寄存器,存函数返回地址
- sp:栈指针
2. 反汇编
2.1 为什么需要反汇编
crash dump 给出 PC 地址,但你不知道 CPU 在执行什么指令。反汇编把机器码翻译成人能读的汇编。
2.2 架构匹配
objdump 必须匹配目标架构。在 x86 主机上反汇编 aarch64 的 so:
# 错误:主机 objdump 不认识 aarch64
objdump -d lib.so # → can't disassemble for architecture UNKNOWN!
# 正确:用交叉工具链
llvm-objdump -d lib.so
# 或
aarch64-linux-gnu-objdump -d lib.so # 需要 sudo apt install binutils-aarch64-linux-gnu
2.3 读懂反汇编输出
1b6e8: 28 01 40 f9 ldr x8, [x9]
│ │ │
│ │ └── 汇编助记符(人能读的)
│ └── 机器码(CPU 实际执行的二进制,4 字节)
└── 该指令在 .text 段中的偏移地址
objdump 输出的地址是相对于 so 基地址的偏移量,不是运行时绝对地址。crash dump 中的 PC 也通常是偏移量(工具已自动减去基地址),所以可以直接对上。
2.4 安装交叉工具链
sudo apt install binutils-aarch64-linux-gnu
3. ARM64 汇编速查
3.1 常见指令
| 指令 | 含义 | C 类比 |
|---|---|---|
ldr x8, [x9] |
从 x9 指向的地址读 8 字节到 x8 | x8 = *(uint64_t*)x9 |
ldr w8, [x9] |
从 x9 指向的地址读 4 字节到 w8 | x8 = *(uint32_t*)x9 |
str x8, [x9] |
把 x8 的值写到 x9 指向的地址 | *(uint64_t*)x9 = x8 |
mov x19, x0 |
把 x0 的值复制到 x19 | x19 = x0 |
cbz x9, addr |
如果 x9 == 0 则跳转 | if (x9 == 0) goto addr |
b addr |
无条件跳转 | goto addr |
bl addr |
调用函数(保存返回地址到 lr) | func() |
udiv x8, x23, x25 |
无符号除法 | x8 = x23 / x25 |
msub x26, x8, x25, x23 |
乘减 | x26 = x23 - x8 * x25(即取余) |
3.2 ldr 的字节数
目标寄存器的位宽决定读取字节数:
ldr x8, [x9]:x8 是 64 位 → 读 8 字节ldr w8, [x9]:w8 是 32 位 → 读 4 字节
3.3 ARM64 调用约定(ABI)
| 寄存器 | 用途 |
|---|---|
| x0 | 第一个参数 / 返回值 |
| x1~x7 | 第 2~8 个参数 |
| x8 | 间接返回值地址 |
| x9~x15 | 临时寄存器(caller-saved) |
| x16~x17 | PLT/链接器用 |
| x18 | 平台保留 |
| x19~x28 | callee-saved(函数保证不变) |
| x29 | 帧指针(FP) |
| x30 | 链接寄存器(LR,返回地址) |
| sp | 栈指针 |
成员函数的第一个参数 x0 = this 指针。
3.4 数组寻址
ldr x9, [x8, x26, lsl #3]
等价于 x9 = *(x8 + x26 * 8),即 x9 = array[x26](每个元素 8 字节)。lsl #3 = 左移 3 位 = 乘 8。
4. 从崩溃指令到根因
4.1 排查链路
SIGSEGV @非法地址
↓
反汇编 PC 处指令 → 确定是哪条指令、用哪个寄存器存地址
↓
查寄存器表 → 找到寄存器的值 = 崩溃地址
↓
分析寄存器值 → 是合法地址?是字符串?是 0?
↓
判断破坏类型 → 空指针?use-after-free?数据竞争?栈溢出?
↓
grep 代码找所有访问点 → 分析并发、生命周期、锁保护
4.2 寄存器值分析
判断寄存器值的类型:
| 特征 | 含义 |
|---|---|
0x0000000000000000 |
空指针 |
0x00000075... |
用户态堆地址(正常) |
0xFFFF... |
可能是内核地址或 -1 |
| 高位为 0 + 低位全是可打印 ASCII | 字符串内容覆盖了指针(内存被踩) |
| 小整数(1、2、8 等) | 计数器、标志位、size |
ASCII 快速判断:把值按字节拆开,看是否连续可打印:
x9:003179786f725074
→ 74='t' 50='P' 72='r' 6f='o' 78='x' 79='y' 31='1' 00='\0'
→ "tProxy1" — 字符串,不是地址
4.3 常见崩溃模式
空指针解引用:寄存器值为 0 或接近 0。 Use-after-free:寄存器值看起来像地址,但指向已释放的内存,可能被其他数据覆盖。 数据竞争:多线程并发读写共享数据,无锁保护,导致内部数据结构被破坏。 栈溢出:SP 寄存器值异常,或递归过深。
5. 虚拟地址空间
5.1 64 位 Linux/OpenHarmony 布局
0xFFFF_FFFF_FFFF_FFFF ┌─────────────┐
│ 内核空间 │ 用户态不可访问
0xFFFF_0000_0000_0000 ├─────────────┤
│ 空洞 │ 未映射,访问即 SIGSEGV
0x0000_7FFF_FFFF_FFFF ├─────────────┤
│ 栈 ↓ │ 高地址往低增长
│ mmap 区 │ 共享库、大块分配
│ 堆 ↑ │ malloc 分配
│ .data │ 全局变量
│ .text │ 程序代码
0x0000_0000_0000_0000 └─────────────┘
- 用户态地址范围:
0x0000_0000_0000_0000~0x0000_7FFF_FFFF_FFFF - 内核态地址范围:
0xFFFF_8000_0000_0000~0xFFFF_FFFF_FFFF_FFFF - 中间空洞:未映射,访问触发 SIGSEGV,不占物理内存
- 堆地址典型范围:
0x75...或0x7f... - 设计目的:用户态和内核态之间隔巨大无效区域,防止越界访问
5.2 为什么不需要全部映射
虚拟地址空间可以远大于物理内存。进程看到的是虚拟地址,物理内存在真正访问时才按需分配(page fault)。空洞 = 没有映射的虚拟地址,不消耗物理内存。
6. C++ 数据结构崩溃排查
6.1 unordered_map 内部结构
class unordered_map {
bucket* bucket_array; // 偏移 0:bucket 数组指针
size_t bucket_count; // 偏移 8:桶数量
size_t element_count; // 偏移 16:元素数量
float max_load_factor; // 偏移 24
...
};
bucket 数组每个元素是一个链表头指针:
bucket[0] → node → node → NULL
bucket[1] → NULL
bucket[2] → node → NULL
...
6.2 数据竞争的表现
- bucket 指针变成字符串内容(内存被其他对象占用)
- 链表指针变成非法值
- 崩溃点在
emplace、erase、find等内部操作
6.3 排查方法
# 找所有访问某个容器的代码
grep -rn 'khhicarCallbacks_' --include='*.cpp' --include='*.h'
对每个访问点问:
- 读还是写?
- 在哪个线程?
- 有没有锁保护?
如果多个线程并发读写且无锁 → 数据竞争。
相关笔记
- 看懂堆栈
- crash分析
- gdb 调试
- processdump命令