一、问题概述

1.1 问题现象

大屏平台进行蓝牙开关机稳定性测试时,blue_host 进程发生崩溃,死机日志显示为 SIGSEGV(SEGV_MAPERR) 空指针解引用错误。

1.2 关键日志摘要

1.3 问题影响

    1)蓝牙进程崩溃导致蓝牙功能完全不可用

    2)开关机稳定性测试中断

    3)用户侧表现为蓝牙无法打开或自动关闭

 

二、初步分析

2.1 崩溃现场信息提取

从死机日志中提取关键寄存器状态:

Registers分析:

r0:00000000 r1:b667f7a8 r2:00000020 r3:000001f4r4:b576fd44 r5:0d003d05 r6:b667fc8c r7:b667f7a8pc:b5764442 lr:b576443f

     1)r0 = 0x00000000:基址寄存器为 NULL

     2)pc = 0xb5764442:崩溃指令地址

      3)访问非法地址 = 0x0000006dNULL + 0x6d 的结果

2.2 源码定位

使用 llvm-addr2line 将崩溃地址转换为源码位置

$ llvm-addr2line -e libbt_vendor.z.so 0x00024442 -f -C

 

对应源码第 655 行:

 

2.3 初步推断

崩溃发生在访问全局结构体指针 ztop_btservice 的成员 epoll_thread_running 时,初步怀疑该全局指针为 NULL

三、深度定位

3.1 结构体偏移量验证

3.1.1 结构体定义

3.1.2 使用 llvm-dwarfdump 验证偏移量

$ llvm-dwarfdump --name="epoll_thread_running" libbt_vendor.z.so

 

结论epoll_thread_running 成员相对于结构体基址的偏移量为 109 字节(0x6d),与崩溃访问地址 0x0000006d 完全吻合。

 

3.2 汇编级验证

使用 llvm-objdump 反汇编崩溃地址附近的指令:

$ llvm-objdump -d --start-address=0x24430 --stop-address=0x24460 libbt_vendor.z.so

 

 

汇编解读

  1. ldr r0, [r4]r4 存储全局变量 ztop_btservice 的地址,该指令将其值(指针本身)加载到 r0;
  2. ldrb.w r1, [r0, #109]:以 r0 为基址偏移 109 字节读取成员值;
  3.  ztop_btservice == NULL 时,r0 = 0,访问 0x6d 触发段错误;

结论:崩溃的直接原因是 ztop_btservice 全局指针为 NULL

 

四、ztop_btservice = NULL 根因分析

4.1 函数调用链分析

蓝牙初始化的函数调用链如下

上层 Vendor 接口

    

userial_socket_open()

    

ZTOP_btservice_init()

    

unix_socket_start()

 

4.2 初始化失败路径分析

4.2.1 死机前日志分析:

 

4.2.2 ZTOP_btservice_init函数分析

ZTOP_btservice_init 函数的执行流程:

  1. 分配内存ztop_btservice = malloc(...)
  2. 初始化各成员:信号量、互斥锁、epoll 等
  3. 启动 Unix socket 服务:调用 unix_socket_start()
  4. 失败处理:若 unix_socket_start() 返回失败,则:
  • 跳转到 fail1 标签,清理信号量和互斥锁
  • 跳转到 fail2 标签,执行 free(ztop_btservice)
  • 执行 ztop_btservice = NULL
  • 返回错误码 -4

 

关键代码逻辑

结论:当 unix_socket_start() 失败时,ZTOP_btservice_init 会主动释放已分配的内存并将 ztop_btservice 置为 NULL

4.3 上层调用者分析

userial_socket_open 函数调用 ZTOP_btservice_init 时没有检查返回值

 

由于未检查返回值,即使 ZTOP_btservice_init 失败并已将 ztop_btservice 置为 NULLuserial_socket_open 仍然向上层返回成功,上层代码继续执行后续流程,最终导致 epoll_thread 访问空指针崩溃。

4.4 日志时序验证

时间

事件

说明

18:01:44.301

unix_socket_start bind socket fail!

socket 绑定失败

18:01:44.301

ZTOP_btservice_init unix_socket_start fail!

初始化失败,ztop_btservice 被置为 NULL

18:01:44.301

VendorInterface BT_OP_HCI_CHANNEL_OPEN end

上层标记打开完成

18:01:44.302

VendorInterface BT_OP_INIT

继续初始化流程

18:01:44.303

VendorInterface Initialize end

上层认为初始化成功

18:01:44.400

SIGSEGV in epoll_thread

崩溃

日志清晰显示:ZTOP_btservice_init 失败后,上层仍然继续执行并标记初始化成功,最终导致空指针崩溃。

 

 

五、解决方案

 epoll_thread 函数添加空指针检查,作为兜底保护:

 

方案优点

  1. 防止崩溃:当 ztop_btservice == NULL 时,线程直接退出,避免访问空指针导致进程崩溃
  2. 兜底保护:即使上层存在错误处理缺陷,也能保证系统稳定性
  3. 改动最小:仅修改一处代码,影响范围可控

 

六、总结

6.1 问题定位结论

问题

说明

崩溃直接原因

epoll_thread 访问了值为 NULL 的全局指针 ztop_btservice

指针为何为 NULL

ZTOP_btservice_init 初始化失败时,通过失败处理路径执行了 free(ztop_btservice)  ztop_btservice = NULL

为何导致崩溃

上层调用者 userial_socket_open 忽略了ZTOP_btservice_init 的返回值,继续执行后续流程,导致 epoll_thread 在指针已置空的情况下仍尝试访问

 

 

6.2 经验总结

调试技巧沉淀

  1. 死机日志解读SEGV_MAPERR@0x0000006d 中的地址是 NULL + offset 的典型空指针特征,0x6d 即为结构体成员偏移量
  2. DWARF 调试信息利用llvm-dwarfdump 可精确提取结构体成员偏移,无需手动计算或猜测
  3. 汇编级验证llvm-objdump 反汇编能直观展示崩溃指令,是确认根因的终极手段
  4. 时序日志串联:结合业务日志和系统日志的时序分析,能还原崩溃前的完整调用链

 

设计原则反思

  1. 防御性编程:所有全局指针访问前都应进行非空检查,尤其在多线程环境中
  2. 错误传播:初始化函数的失败必须逐级向上传递,不能在任何层级被忽略

 

 

 

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐