3. 闪退分类与根因:空指针、内存越界、堆栈溢出、死锁、资源泄漏

Camera HAL 层的闪退(Crash)是嵌入式系统中最棘手的稳定性问题之一。与上层 App 闪退不同,HAL 层的崩溃往往直接导致相机服务(cameraserver)重启,甚至引发系统级 watchdog 触发整机重启。本章将五种最常见的闪退类型进行系统化分类,并深入剖析其根因,帮助你在 log 海洋中快速定位问题本质。

核心观点: 80% 的 Camera HAL 闪退集中在空指针和内存越界两类。掌握每种闪退的 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 中(需跟踪资源生命周期)
实战提示: 在 log 中看到 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);
}
避坑指南: 不要只检查指针本身是否为 NULL。在 Camera HAL 中,经常出现指针非 NULL 但指针指向的内容已被 memset 为 0 的情况(如 buffer 被复用)。建议在关键路径上增加 magic number 校验。
空指针闪退触发流程 HAL 函数调用 指针未初始化/已释放 解引用 NULL 地址 MMU 触发缺页异常 内核发送 SIGSEGV 进程 Crash / Tombstone fault addr = 0x0 或 0x8

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,则写入操作会越界到下一行或相邻内存
}
调试技巧: 对于内存越界,建议在关键分配点启用 AddressSanitizer (ASan) 或使用 libc 的 mallopt(M_CHECK_ACTION, 3) 开启堆检查。在 log 中搜索 heap corruption 或 malloc(): memory corruption 可以快速定位。
内存越界:堆块破坏示意图 Chunk A (正常) Chunk B (正常) Chunk C (正常) Chunk A (正常) Chunk B (被越界写入) Chunk C (header 被破坏) 越界写入覆盖 越界写入破坏相邻 Chunk C 的 header → free() 时检测到 corruption → abort() log 特征:free(): invalid pointer / malloc(): memory corruption (fast)

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+,远超默认栈大小
避坑指南: 在 Camera HAL 中,绝对不要在栈上分配超过 1KB 的局部变量。对于 metadata、buffer 等大块数据,必须使用 heap 分配(malloc/new)。使用 pthread_attr_setstacksize() 可以增大线程栈,但治标不治本。
堆栈溢出:调用栈深度与栈空间消耗 main() 栈帧 func_A() 栈帧 func_B() 栈帧 func_C() 栈帧 (溢出) 栈底 栈顶 超出栈边界 → SIGSEGV log 特征:stack canary corrupted backtrace 中看到重复的递归调用

3.5 死锁(Deadlock)

死锁在 Camera HAL 的多线程架构中频繁出现,尤其是涉及 ISP 驱动、HAL 主线程和 App callback 线程之间的锁竞争。死锁不会直接产生 SIGSEGV,但会导致 watchdog 超时,进而触发系统重启。

死锁四必要条件:

  1. 互斥(Mutual Exclusion): 资源一次只能被一个线程占用。
  2. 持有并等待(Hold and Wait): 线程持有资源 A 的同时等待资源 B。
  3. 不可剥夺(No Preemption): 资源只能由持有者主动释放。
  4. 循环等待(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
// 两个线程互相等待,形成死锁
调试技巧: 在 log 中搜索 blocked、waiting for lock、deadlock。使用 debuggerd -b 获取所有线程的 backtrace,查看锁等待关系。推荐在 HAL 中启用 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK) 来检测死锁。
死锁:循环等待依赖图 Thread 1 Thread 2 Lock A (持有) Lock B (持有) 持有 Lock A 持有 Lock B 等待 Lock B 等待 Lock A log 特征:watchdog timeout / "blocked" in backtrace / ANR

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;
}
避坑指南: 在 Camera HAL 中,建议使用 RAII 模式管理资源。对于 fd,使用 ScopedFd 封装;对于 buffer,使用 sp<> 智能指针。在关键路径上增加资源计数日志,每次 open/close 都打印当前 fd 数量。
资源泄漏:从泄漏到 OOM 的演进路径 HAL 分配资源 未释放/未归还 资源池耗尽 OOM / fd 耗尽 lowmemorykiller 杀进程 log 特征:Out of memory / too many open files / lowmemorykiller: send sigkill dumpsys media.camera 中 buffer 计数持续增长

3.7 闪退根因快速定位矩阵

在实际调试中,log 中的信号和错误码可以快速缩小范围。下表总结了从 log 到根因的映射关系:

Log 信号/关键词 fault addr 特征 backtrace 特征 最可能根因
SIGSEGV + fault addr 0x0 0x0 或 0x8 解引用操作(如 ldr r0, [r1]) 空指针
SIGSEGV + fault addr 大地址 0xABCD0000 等 memcpy/memset 或 buffer 操作