一. 背景
我们司机端App
从iOS18
系统开始出现了BackBoardServices
库的方法触发exit
调用,而exit
执行之后,C++
全局变量对象进行析构导致的崩溃。
具体崩溃堆栈用两种:
第一种:-[BKSHIDEventObserver init] + 0
libsystem_c.dylib ___cxa_finalize_ranges + 480
libsystem_c.dylib _exit + 32
BackBoardServices -[BKSHIDEventObserver init] + 0
BoardServices ___31-[BSServiceConnection activate]_block_invoke.182 + 124
BoardServices ___61-[BSXPCServiceConnectionEventHandler _connectionInvalidated:]_block_invoke + 196
BoardServices _BSXPCServiceConnectionExecuteCallOut + 240
BoardServices -[BSXPCServiceConnectionEventHandler _connectionInvalidated:] + 180


第二种:-[BKSHIDEventDeliveryManager _initForTestingWithService:] + 0
libsystem_c.dylib ___cxa_finalize_ranges + 480
libsystem_c.dylib _exit + 32
BackBoardServices -[BKSHIDEventDeliveryManager _initForTestingWithService:] + 0
BoardServices ___31-[BSServiceConnection activate]_block_invoke.182 + 124
BoardServices ___61-[BSXPCServiceConnectionEventHandler _connectionInvalidated:]_block_invoke + 196
BoardServices _BSXPCServiceConnectionExecuteCallOut + 240
BoardServices -[BSXPCServiceConnectionEventHandler _connectionInvalidated:] + 180


我们从苹果论坛也能找到对应的问题的相关反馈developer.apple.com/forums/thre...
因此很明显这是一个苹果系统在iOS18
版本做了底层变更而引起的崩溃。
二. 原因排查
这个崩溃只发生在iOS18
及以上系统,而且从统计来看,绝大部分发生在进入后台一段时间后,发生的崩溃。
从崩溃堆栈信息:

我们可以尝试分析出这个崩溃出现的主要原因:
- 我们知道当
iPhone
设备被触摸、点击的时,会由系统的后台守护进程backboardd
感知到,后台守护进程backboardd
会调用内部的BackBoardServices
服务,将触摸、点击等的数据处理打包为IOHIDEvent
对象;然后调用底层的BoardServices
,将相关事件通过进程间通信(IPC进程通信)
,传给前台的守护进程SpringBoard
,前台的守护进程SpringBoard
收到消息后,也是通过进程间通信(IPC进程通信)
将消息转发给目标的App
进行处理。
libsystem_c.dylib ___cxa_finalize_ranges + 480 /// 执行全局对象的析构和atexit注册的函数。
libsystem_c.dylib _exit + 32 /// 调用exit(0)方法
BackBoardServices -[BKSHIDEventDeliveryManager _initForTestingWithService:] + 0 ///
BackBoardServices
服务,校验发现参数异常BoardServices ___31-[BSServiceConnection activate]_block_invoke.182 + 124 /// 激活XPC连接
BoardServices ___61-[BSXPCServiceConnectionEventHandler _connectionInvalidated:]_block_invoke + 196 /// 执行XPC连接失效的
block
回调BoardServices _BSXPCServiceConnectionExecuteCallOut + 240 /// 执行XPC回调
BoardServices -[BSXPCServiceConnectionEventHandler _connectionInvalidated:] + 180 /// 连接失效处理
- 从崩溃堆栈分析,是
BoardServices
这个跨进程通信框架,检测到跟上游的BackBoardServices
提供HID
事件服务,之间的连接失效了,因此执行XPC(iOS间进程间通信)
回调,重新去激活跟BackBoardServices
的XPC
的连接,BackBoardServices
调用检测方法_initForTestingWithService
, 由于偏移指令是+0
表示在函数入口处的参数校验发现异常,直接调用了exit
方法。
libsystem_c.dylib _exit + 32 /// 为什么堆栈显示是_exit, 但对应的函数是exit(0)方法。
iOS
崩溃堆栈中C
函数名前多出的下划线_
是 编译器遵守ABI
规范的符号修饰结果,用于:
- 隔离系统库与用户代码的符号命名空间;
- 确保二进制兼容性和动态链接可靠性;
- 简化调试工具的符号解析流程。
而子线程调用exit
强制终止应用进程App
,会触发C++
全局变量对象的析构函数,PosBridgeImpl
对象析构,触发了内部的GPSHelper
对象的析构,GPSHelper
在析构函数里面调用了TimerEventRunnerObserverImp::detachTimer()
函数。
我们崩溃堆栈里面崩溃发生在函数TimerEventRunnerObserverImp::detachTimer() + 36
, 我们在release
环境相同或者相近的系统版本下,查看函数的相关汇编指令如下:
ini
`TimerEventRunnerObserverImp::detachTimer:
-> 0x10487f924 <+0>: stp x20, x19, [sp, #-0x20]! ; 保存x19,x20到栈顶,sp -= 0x20(分配32字节栈空间)
0x10487f928 <+4>: stp x29, x30, [sp, #0x10] ; 保存帧指针(x29)和返回地址(x30)到sp+0x10
0x10487f92c <+8>: add x29, sp, #0x10 ; 设置新帧指针(x29 = sp + 0x10)
0x10487f930 <+12>: ldrb w8, [x0, #0x28] ; 加载this+0x28处的字节(bool标志)到w8
0x10487f934 <+16>: cbz w8, 0x10487f958 ; 若w8==0(标志为false),跳转到<+52>(直接返回)
0x10487f938 <+20>: mov x19, x0 ; 保存this指针到x19(备份this)
0x10487f93c <+24>: ldr x0, [x0, #0x8] ; 加载this+0x8处的指针(成员变量target_)到x0
0x10487f940 <+28>: cbz x0, 0x10487f954 ; 若x0==0(target_为空),跳转到<+48>
0x10487f944 <+32>: ldr x8, [x0] ; 加载target_对象的虚表指针到x8(高危点1)
0x10487f948 <+36>: ldr x8, [x8, #0x18] ; 从虚表偏移0x18处加载函数指针(高危点2,崩溃位置)
0x10487f94c <+40>: mov x1, x19 ; 设置参数x1 = this(TimerEventRunnerObserverImp对象)
0x10487f950 <+44>: blr x8 ; 调用虚函数(实际调用target_->unregisterObserver(this))
0x10487f954 <+48>: strb wzr, [x19, #0x28] ; 将0写入this+0x28(isAttached_ = false)
0x10487f958 <+52>: ldp x29, x30, [sp, #0x10] ; 恢复帧指针和返回地址
0x10487f95c <+56>: ldp x20, x19, [sp], #0x20 ; 恢复x19,x20,sp += 0x20(释放栈空间)
0x10487f960 <+60>: ret ; 函数返回
崩溃发生在偏移指令<+36>: ldr x8, [x8, #0x18] ; 从虚表偏移0x18处加载函数指针
这里,而崩溃类型是EXC_BAD_ACCESS (SIGSEGV)
, 也就表明了成员变量target
指向的对象已经被释放了, 其虚表指针无效,虚表偏移0x18
,访问了无效的地址,触发了崩溃。
至于target
对象被释放的原因,大概是因为多线程竞争原因,因为是第三方库,看不到具体源码也无法进行进一步的分析。
而另外一个崩溃,由于第三方库对函数符号进行了重命名,无法进行获知具体函数和对象名称,无法进行调试,但是崩溃的原因应该是一致的。
三. 解决方案
从上面的分析,我们已经清楚了崩溃的具体原因,因此我们也将对应的崩溃和原因分析告知第三方,让第三方去进行排查,但三方一直没给到对应的新的版本库,而且由于两个三方库都是重要基础库,升级造成的影响面大,所以就算三方给了修复的版本,整个升级的过程,也是比较冗长。
因此需要思考,如何在业务侧来通过其他的方法来进行修复。
方案一(失败方案)
A. 治理方案
首先想到的方法是能否通过hook
,来感知exit
函数的调用,因此尝试hook
了libsystem_c.dylib
库的exit
函数和BackBoardServices
的如下两个函数:
-[BKSHIDEventDeliveryManager _initForTestingWithService:] + 0
-[BKSHIDEventObserver init] + 0
然后在exit
函数里面获取当前线程的堆栈,判断线程里面是否包含BackBoardServices
相关库函数,如果包含,则判断是BackBoardServices
调用exit
,这时候直接在exit
的函数里面直接调用_exit(0)
方法。不走
exit(0)
函数的清理流程,直接通过_exit(0)
,退出当前应用。
同理对BackBoardServices
函数的hook
也执行相应的逻辑。
同时添加降级方案,以及生效的版本范围,目前只对iOS18
及以上的系统生效。
exit(0)和_exit(0)的区别:
总结:
_exit(0)
会绕过清理的流程直接终结应用程序,是有一定风险,比如说不刷新I/O
缓冲区、不关闭文件描述符,但这些iOS
操作系统都会帮我们进行善后处理,因此可以放心。
B. 上线结果
上线之后发现压根不起作用,没有任何的埋点或者日志上报,崩溃堆栈里面也没有hook
信息,显然是hook
这几个方法没有生效,但我自测的时候,在debug
和release
环境都是正常生效的, 。
经我这边搜集到的资料分析,给出的原因如下:
- 我自测的时候,之所以在
debug
和release
进行hook
系统库的函数,都能支持hook
,并执行相关逻辑,主要原因是debug
和release
是使用开发证书进行签名,构建的App
包含调试权限(get-task-allow
),可绕过部分系统保护。 - 而
AppStore
上的线上包,之所以hook
的相关代码没有效果,主要原因在于在iOS 14+
中引入的指针认证码(PAC
)技术对函数指针的完整性验证具有严格的熔断机制。当Hook
篡改指针触发PAC
校验失败时,系统不会仅跳过相关代码的执行,而且触发了指令级熔断,将违规线程挂起,后续指令不再继续执行。 - 这里判断是指令级熔断,而非其他失败处理的原因是司机可以正常使用
App
,没有反馈闪退和不可用等问题。
PAC 验证失败的处理机制
指令级熔断(CPU 硬件层) PAC 在 ARM 架构中由 CPU 硬件直接实现。当内核检测到被篡改的指针(如 Hook 修改的
objc_msgSend
或exit(0
函数指针)时:
- CPU 会立即抛出
EXC_BAD_ACCESS
异常(代码0x8badf00d
),标记为 "指针完整性违规"。- 违规线程被强制挂起,后续指令不再执行。
内核级进程清除
- 内核捕获异常后,向违规进程注入
SIGKILL
信号(不可阻塞)。- 进程内存被标记为 "污染状态" ,所有动态库卸载,线程栈销毁。
用户态响应
- 应用瞬间闪退,无崩溃日志(因日志系统未响应)。
- iOS 14+ 弹窗提示 "App 因安全问题终止"
方案二
由于第一种hook
方案失败,我们继续探究,是否有方法可以阻止,C++
全局变量对象/静态变量对象的析构函数的调用,理论上只有全局变量对象的析构函数在exit
方法后,不调用,也就从中间环境拦截了这个崩溃。
A. [[clang::no_destroy]]
探索
在iOS
开发中,[[clang::no_destroy]]
是 Clang
编译器提供的一个属性,用于控制变量的析构行为。以下是其核心特性和应用场景的总结:
- 禁用析构函数调用
默认情况下,C++
中的全局或静态变量会在程序退出时(如 main
函数结束或 exit
被调用)自动触发析构函数。而 [[clang::no_destroy]]
会显式禁止这一行为。
- 适用对象类型
主要用于修饰 C++
对象(如类实例、智能指针等),尤其适用于需要持久化至程序结束的全局状态(如单例、资源管理器等)。
- 编译时控制
该属性在编译阶段生效,编译器会跳过生成析构函数的注册代码,而非运行时干预。
但这个方法的缺陷是必现要有源码,然后针对全局变量或者静态变量进行设置,但我们目前崩溃主要在三方库,没有相关源码,所以该方法暂时不适合。
B. -fno-c++-static-destructors
探索
Xcode11
以后是支持设置-fno-c++-static-destructors
标识来禁用所有的静态变量和全局变量的析构函数,

- 配置编译设置
在 Xcode 项目中启用该标志的步骤:
打开项目:选择 Target → Build Settings → Apple Clang - Code Generation
。
添加编译标志:
在Other C++ Flags
(或 Other C Flags
)中添加-fno-c++-static-destructors
。
默认情况下这个编译设置是关闭的,主要原因在于启用该选项是有一些副作用。
- 资源泄漏风险
如果静态变量持有需要释放的资源(如动态内存、文件句柄、网络连接等),禁用析构会导致这些资源无法自动释放。例如:
static std::vector<int>* data = new std::vector<int>()
; // 启用选项后,data
的析构函数不会调用,内存泄漏
- 破坏
RAII
原则
C++
的RAII
(资源获取即初始化)机制依赖析构函数实现自动资源管理。禁用静态析构会破坏这一机制,需手动管理资源,增加代码复杂度。
- 跨平台兼容性问题
不同编译器对静态变量析构的实现可能不同,此选项可能引发不可预测的行为(如某些系统下全局对象析构顺序异常导致崩溃)。
- 特定场景的未定义行为
若程序依赖静态变量析构完成关键操作(如日志写入、状态保存),禁用后可能导致逻辑错误或数据丢失。
虽然在编译设置中开启-fno-c++-static-destructors
可能会产生一些副作用,但是在苹果操作系统里面,当进程退出的时候,即使没有通过析构函数去释放资源,苹果操作系统本身也会帮忙进行资源释放,比如操作系统会帮忙关闭文件、统一回收内存资源。
但这种方法也不起作用,原因在于第三方给过来的.framework
的库,是按照他们项目编译设置出来的库,这个库不会跟随我们现在的项目编译设置,所以在我们项目里面加上这个-fno-c++-static-destructors
,是无法影响到第三方库。
方案三(成功方案)
A. 治理方案
由于方案二也行不通,那是否有其他方法,能够在exit
方法调用后,比C++
全局变量的析构,更早执行呢?
经研究我们发现,可以通过atexit
函数,注册程序正常终止时需要执行的清理函数。
atexit
允许注册一个无参数、无返回值的函数,该函数会在程序正常退出时(如调用 exit()
或 main
函数返回时)按注册逆序执行。
atexit
函数的注册是一个栈,也就是后注册的清理函数会先执行,atexit
允许同一个方法,多次注册。
而C++ 全局变量对象/静态变量对象
之所以会在exit
函数后,调用析构函数的原因是,编译器会为全局变量对象/静态变量对象隐式注册其析构函数到atexit
队列里面。
因此我们必须保证业务侧atexit
注册的清理函数在对应的C++
的全局对象/静态全局对象的析构函数注册之后。
这里全局对象/静态全局对象,atexit
注册时机是在main
函数之前,而静态局部对象是在首次执行到定义的时候,去进行注册。
从崩溃堆栈或者我们自己调试来看,这里的posEngine::PosBridgeImpl::~PosBridgeImpl()
析构函数的调用,只有在地图初始化的情况下,调用exit
方法,才会调用这个析构,如果没有调用地图,直接调用exit
方法,并不会调用这个析构函数。
因此我们可以推断这里是一个静态局部对象。
而另外一个被符号化的函数堆栈,经排查这个这个崩溃都是命中人脸识别,因此也咨询了人脸识别的三方,确认确实是人脸识别的库。
因为C++
相关对象的初始化或者C++
相关方法,无法进行hook
,但我们可以确认这两个静态局部对象,都是在对应的地图或者人脸调用之后进行初始化的,因此我们hook
了上层的对象(地图和人脸的相关对象),当对象被初始化之后,延迟一定的时间(时间可以配置)去注册atexit
的清理函数(支持同一个函数多次注册),这样就保证我们注册的exit
清理函数,一定会在C++
静态局部对象的析构函数之前调用。
然后在我们注册的清理函数里面获取当前线程的堆栈,判断线程里面是否包含BackBoardServices
相关库函数,如果包含,则判断是BackBoardServices
调用exit
,这时候直接在exit
的函数里面直接调用_exit(0)
方法。不走
exit(0)
函数的清理流程,直接通过_exit(0)
,退出当前应用。
同时添加降级方案,以及生效的版本范围,目前只对iOS18
及以上的系统生效。
B. 上线结果
上线之后,这个相关崩溃在新版本得到完全治理,同时并没有引起其他稳定性数据的劣化比如(卡顿、abort
等),或者其他反馈问题。
证明该解决方法能有效治理该问题,且无负面影响。
当然最好是能让三方从源头解决问题。
四. 总结
以上主要介绍了针对这个崩溃分析和治理过程的探索和思考,当然最好的解决方法还是让第三方库的提供者,从源代码角度去解决这个问题。
若本文有错误之处或者技术上关于其他类型Crash
的讨论交流的,欢迎评论区留言。