3. 闪退分类与根因:空指针、内存越界、堆栈溢出、死锁、资源泄漏
Camera HAL 层的闪退(Crash)是嵌入式系统中最棘手的稳定性问题之一。与上层 App 闪退不同,HAL 层的崩溃往往直接导致相机服务(cameraserver)重启,甚至引发系统级 watchdog 触发整机重启。本章将五种最常见的闪退类型进行系统化分类,并深入剖析其根因,帮助你在 log 海洋中快速定位问题本质。
3.1 闪退分类总览
根据 Android Camera HAL 的实际生产环境数据,我们将闪退归纳为以下五类。理解它们的触发条件、log 特征和根因,是后续章节实战分析的基础。
| 闪退类型 | 触发条件 | 典型 Log 关键词 | 根因定位难度 |
|---|---|---|---|
| 空指针 | 访问未初始化或已释放的对象/指针 | SIGSEGV, NULL pointer dereference, signal 11 |
低(有明确地址) |
| 内存越界 | 写入/读取超出分配内存边界 | SIGSEGV, heap corruption, invalid address |
中(需分析堆布局) |
| 堆栈溢出 | 函数调用层级过深或局部变量过大 | SIGSEGV, stack overflow, stack canary |
低(回溯调用栈) |
| 死锁 | 多个线程循环等待锁资源 | watchdog, blocked, deadlock, ANR |
高(需分析锁依赖) |
| 资源泄漏 | fd、内存、buffer 未释放导致耗尽 | OOM, too many open files, fd leak |
中(需跟踪资源生命周期) |
signal 11 (SIGSEGV) 时,不要急于断定是空指针。需要结合 fault address 的值来判断:fault addr = 0x0 或极小值(如 0x8)通常是空指针;而 fault addr 为随机大地址时,大概率是内存越界。
3.2 空指针(Null Pointer Dereference)
空指针是 Camera HAL 中最常见的闪退类型,通常发生在以下场景:
- 从
camera_device_t或camera3_stream_t获取的指针未做有效性检查。 - 异步回调中,context 指针已被释放(Use-After-Free 的一种变体)。
- HAL 模块加载失败后,直接调用其函数指针。
根因模型: 指针变量存储的地址为 0x0 或无效地址,CPU 在解引用时触发 MMU 缺页中断,内核发送 SIGSEGV 信号给进程。
// 典型空指针代码示例
void process_capture_result(camera3_capture_result_t *result) {
// 错误:未检查 result 是否为 NULL
camera3_stream_buffer_t *buf = &result->output_buffers[0];
// 如果 result == NULL,此处直接闪退
process_buffer(buf);
}
// 正确做法
void process_capture_result_safe(camera3_capture_result_t *result) {
if (result == NULL || result->output_buffers == NULL) {
ALOGE("%s: invalid result or output_buffers", __FUNCTION__);
return;
}
camera3_stream_buffer_t *buf = &result->output_buffers[0];
process_buffer(buf);
}
3.3 内存越界(Memory Out-of-Bounds)
内存越界在 Camera HAL 中极具隐蔽性,因为其崩溃点往往远离真正的 bug 写入点。常见场景包括:
- Gralloc buffer 的 stride 计算错误,导致 ISP 写入时超出分配的行宽。
- metadata buffer 的 size 字段与实际填充数据不匹配。
- stream buffer 数组索引越界(如
output_buffers[3]但实际只有 2 个 buffer)。
根因模型: 写入操作跨越了堆块(heap chunk)的边界,破坏了相邻堆块的 header 或数据。当 free() 或 realloc() 遍历堆链表时,检测到 corrupted chunk,触发 SIGSEGV 或 abort()。
// 内存越界典型场景:stride 计算错误
void fill_raw_buffer(uint8_t *buf, int width, int stride, int height) {
for (int y = 0; y < height; y++) {
// 错误:使用 width 而不是 stride 作为行偏移
uint8_t *row = buf + y * width;
for (int x = 0; x < width; x++) {
row[x] = get_pixel_value(x, y);
}
}
// 如果 stride > width,则每行末尾的 (stride - width) 字节未被初始化
// 如果 stride < width,则写入操作会越界到下一行或相邻内存
}
mallopt(M_CHECK_ACTION, 3) 开启堆检查。在 log 中搜索 heap corruption 或 malloc(): memory corruption 可以快速定位。
3.4 堆栈溢出(Stack Overflow)
Camera HAL 中的堆栈溢出通常由递归调用失控或局部变量过大引起。在嵌入式系统中,默认栈大小通常只有 8KB(针对单个线程),极易被大结构体撑爆。
常见场景:
- HAL 与 ISP 驱动之间的递归回调(如
process_capture_result内部又调用了request_stream_buffers)。 - 在栈上分配大尺寸的 metadata buffer(如 4K 分辨率的 JPEG 编码临时 buffer)。
- 深度嵌套的算法调用链(如 3A 算法中的递归搜索)。
// 堆栈溢出示例:递归调用失控
void process_frame_recursive(frame_t *frame, int depth) {
char local_buffer[4096]; // 大局部变量
if (depth > 100) {
ALOGE("depth exceeded, potential stack overflow");
return;
}
// 处理逻辑...
process_frame_recursive(frame, depth + 1); // 递归调用
}
// 每次递归消耗 4KB+ 栈空间,100 次递归即消耗 400KB+,远超默认栈大小
pthread_attr_setstacksize() 可以增大线程栈,但治标不治本。
3.5 死锁(Deadlock)
死锁在 Camera HAL 的多线程架构中频繁出现,尤其是涉及 ISP 驱动、HAL 主线程和 App callback 线程之间的锁竞争。死锁不会直接产生 SIGSEGV,但会导致 watchdog 超时,进而触发系统重启。
死锁四必要条件:
- 互斥(Mutual Exclusion): 资源一次只能被一个线程占用。
- 持有并等待(Hold and Wait): 线程持有资源 A 的同时等待资源 B。
- 不可剥夺(No Preemption): 资源只能由持有者主动释放。
- 循环等待(Circular Wait): 存在线程-资源的环形链。
// 死锁典型场景:ABBA 锁顺序问题
// Thread 1
pthread_mutex_lock(&lock_A);
// ... 处理 ...
pthread_mutex_lock(&lock_B); // 等待 lock_B
// Thread 2
pthread_mutex_lock(&lock_B);
// ... 处理 ...
pthread_mutex_lock(&lock_A); // 等待 lock_A
// 两个线程互相等待,形成死锁
blocked、waiting for lock、deadlock。使用 debuggerd -b 获取所有线程的 backtrace,查看锁等待关系。推荐在 HAL 中启用 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK) 来检测死锁。
3.6 资源泄漏(Resource Leak)
资源泄漏是 Camera HAL 中的慢性杀手。它不会立即导致闪退,但会在长时间运行后(如 4K 录像 30 分钟)触发 OOM 或 fd 耗尽,最终导致 cameraserver 被 lowmemorykiller 杀死。
常见泄漏类型:
| 资源类型 | 泄漏场景 | 检测方法 |
|---|---|---|
| 文件描述符 (fd) | 打开 ISP 节点、sensor 驱动后未 close | ls /proc/[pid]/fd 计数 |
| Gralloc Buffer | request 的 buffer 未 return 给 framework | dumpsys media.camera 查看 buffer 计数 |
| Heap 内存 | metadata、face detection 结果未释放 | AddressSanitizer / Valgrind |
| ION/DMA-BUF | 共享内存未 unmap 或未 release | cat /d/ion/heaps 查看使用量 |
// 资源泄漏示例:fd 未关闭
int open_sensor_device() {
int fd = open("/dev/v4l-sensor0", O_RDWR);
if (fd < 0) {
ALOGE("Failed to open sensor device");
return -1;
}
// 错误:在异常路径中忘记 close(fd)
if (ioctl(fd, SENSOR_INIT, &config) < 0) {
ALOGE("Sensor init failed");
return -1; // fd 泄漏!
}
return fd;
}
ScopedFd 封装;对于 buffer,使用 sp<> 智能指针。在关键路径上增加资源计数日志,每次 open/close 都打印当前 fd 数量。
3.7 闪退根因快速定位矩阵
在实际调试中,log 中的信号和错误码可以快速缩小范围。下表总结了从 log 到根因的映射关系:
| Log 信号/关键词 | fault addr 特征 | backtrace 特征 | 最可能根因 |
|---|---|---|---|
SIGSEGV + fault addr 0x0 |
0x0 或 0x8 | 解引用操作(如 ldr r0, [r1]) |
空指针 |
SIGSEGV + fault addr 大地址 |
0xABCD0000 等 | memcpy/memset 或 buffer 操作 | ← 上一章 📖 返回目录 下一章 → |