iOS疑难Crash-iOS18.0+ BackBoardServices exit 崩溃治理

一. 背景

我们司机端AppiOS18系统开始出现了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间进程间通信)回调,重新去激活跟BackBoardServicesXPC的连接,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函数的调用,因此尝试hooklibsystem_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这几个方法没有生效,但我自测的时候,在debugrelease环境都是正常生效的, 。

经我这边搜集到的资料分析,给出的原因如下:

  • 我自测的时候,之所以在debugrelease进行hook系统库的函数,都能支持hook,并执行相关逻辑,主要原因是debugrelease是使用开发证书进行签名,构建的App包含调试权限(get-task-allow),可绕过部分系统保护。
  • AppStore上的线上包,之所以hook的相关代码没有效果,主要原因在于在 iOS 14+ 中引入的指针认证码(PAC)技术对函数指针的完整性验证具有严格的熔断机制。当 Hook 篡改指针触发 PAC 校验失败时,系统不会仅跳过相关代码的执行,而且触发了指令级熔断,将违规线程挂起,后续指令不再继续执行。
  • 这里判断是指令级熔断,而非其他失败处理的原因是司机可以正常使用App,没有反馈闪退和不可用等问题。

PAC 验证失败的处理机制

  1. 指令级熔断(CPU 硬件层) PAC 在 ARM 架构中由 CPU 硬件直接实现。当内核检测到被篡改的指针(如 Hook 修改的 objc_msgSendexit(0 函数指针)时:

    1. CPU 会立即抛出 EXC_BAD_ACCESS 异常(代码 0x8badf00d ),标记为 "指针完整性违规"
    2. 违规线程被强制挂起,后续指令不再执行。
  2. 内核级进程清除

    1. 内核捕获异常后,向违规进程注入 SIGKILL 信号(不可阻塞)。
    2. 进程内存被标记为 "污染状态" ,所有动态库卸载,线程栈销毁。
  3. 用户态响应

    1. 应用瞬间闪退,无崩溃日志(因日志系统未响应)。
    2. 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的讨论交流的,欢迎评论区留言。

相关推荐
2501_916013741 小时前
iOS 加固工具使用经验与 App 安全交付流程的实战分享
android·ios·小程序·https·uni-app·iphone·webview
2501_915106325 小时前
Fiddler 中文版抓包实战 构建标准化调试流程提升团队协作效率
android·ios·小程序·https·uni-app·iphone·webview
iReaShare6 小时前
iPhone 数据擦除软件评测(最新且全面)
ios
iReaShare7 小时前
轻松将文件从 iPhone 传输到 Mac
ios
9144062327 小时前
IOS 18下openURL 失效问题
ios
杂雾无尘11 小时前
swift 基础:关联引用讲解
ios·swift·客户端
毛发浓密的女猴子11 小时前
iOS 基础篇(一): char、int、long、NSInteger类型对比
ios
瓜子三百克14 小时前
SwiftUI 全面介绍与使用指南
ios·swiftui·swift
GitLqr1 天前
数码洞察 | Apple VS DMA、三星新品、Android 16KB Page Size
android·ios·samsung