第3章:iOS崩溃日志采集:Mach异常与信号机制、崩溃捕获实现、符号化配置

3.1 iOS崩溃的两层机制:Mach异常与Unix信号

iOS / macOS 的底层内核是 XNU(X is Not Unix),它采取了双层次异常架构:Mach异常 位于内核态,Unix信号 位于用户态。崩溃发生在 App 进程中时,首先触发的是 Mach 层异常,如果 Mach 层未被处理,内核会将其转换为对应的 Unix 信号抛向用户进程。

层级 机制名称 触发场景 捕获优势
内核态 Mach 异常 EXC_BAD_ACCESS / EXC_BAD_INSTRUCTION / EXC_ARITHMETIC 最早捕获点,可获取更完整的线程栈
用户态 Unix 信号 SIGSEGV / SIGABRT / SIGBUS / SIGTRAP 兼容 POSIX 标准,被第三方库广泛使用
设计原则: 崩溃采集 SDK 通常优先注册 Mach 异常端口,作为备份再注册 signal handler。Mach 异常处理失败时回退到信号处理,保证捕获率接近 100%。

3.2 崩溃捕获的实现流程

实现一个最小的崩溃捕获模块需要以下 5 个步骤:

  1. 创建 Mach 异常端口: 使用 mach_port_allocate 申请一个接收异常的端口。
  2. 设置任务异常处理: 调用 task_set_exception_ports,将该端口绑定到当前任务(mach_task_self()),指定关注的异常掩码(EXC_MASK_ALL)。
  3. 启动异常处理线程: 在子线程中循环调用 mach_msg 接收异常消息,提取线程状态、寄存器信息。
  4. 安装信号处理器: 使用 signal(SIGSEGV, handler) 等方式注册传统信号处理函数,作为 Mach 异常未处理情况下的兜底。
  5. 保存崩溃上下文: 在 Handler 中将捕获到的 ucontext 或 thread_state 序列化为 JSON,写入沙盒缓存文件,等待下次启动上报。
核心代码示例(C 函数,注册 Mach 异常端口):
// 创建异常端口
mach_port_t exception_port;
kern_return_t kr = mach_port_allocate(mach_task_self(),
                                      MACH_PORT_RIGHT_RECEIVE,
                                      &exception_port);
if (kr != KERN_SUCCESS) return kr;

// 将端口注册为当前任务的异常处理端口
kr = task_set_exception_ports(mach_task_self(),
                              EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION,
                              exception_port,
                              EXCEPTION_DEFAULT,
                              THREAD_STATE_NONE);
if (kr != KERN_SUCCESS) return kr;

// 在独立线程中等待异常消息
mach_msg_header_t *msg = ...;
kr = mach_msg(msg, MACH_RCV_MSG, 0, sizeof(msg),
              exception_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
// 获取发生异常的线程状态
thread_state_t state;
mach_msg_type_number_t state_count = ARM_THREAD_STATE64_COUNT;
thread_get_state(exception_thread, ARM_THREAD_STATE64,
                 (thread_state_t)&state, &state_count);

3.3 符号化配置:从空虚地址到可读符号

崩溃捕获模块收集到的是 空虚地址(Slide Address) 和 偏移量。要转换成人类可读的函数名 + 行号,必须依赖 dSYM 符号文件(DWARF with DSYM Bundle)。

符号化流程:

  • 查找 dSYM: 根据 Bundle ID、UUID 匹配对应版本的 .dSYM 包。
  • 计算基址: 从崩溃日志或捕获上下文获取 App 的 ASLR Slide(虚拟地址偏移)。
  • 解析地址: 使用 atos 工具或 dwarfdump 将地址解析出符号。
常见错误: dSYM 与 Arch 不匹配(例如 Release 模式未生成 dSYM 或上传了错误的 UUID)。符号化结果全部变成 <redacted> 或缺失行号,通常是 dSYM 缺失或配置路径错误。

3.4 符号化自动化与原生集成

推荐在 Xcode Build Phases 或 CI/CD 管道 中自动化上传 dSYM。下面是主流崩溃平台所需的配置步骤:

平台 / SDK 脚本语言 关键 API 或命令
Bugly(腾讯) Python buglySymboliOS.py -i <dSYM> -o <output>
Firebase Crashlytics Bash + Run Script ${PODS_ROOT}/FirebaseCrashlytics/upload-symbols + -gsp
Sentry(自建或 SaaS) Python / Bash sentry-cli upload-dif --include-sources *.dSYM
自建符号化 Swift / C atos -arch arm64 -o App.dSYM/... -l slide_addr addr
命令行示例:手动符号化单个崩溃地址
# 获取二进制的 load address(从崩溃日志或 dyld_info 获取)
load_addr=0x100000000
# 崩溃地址(来自捕获日志)
crash_addr=0x100123abc
# 计算真实偏移
offset=$((crash_addr - load_addr))
# 使用 atos 解析
atos -arch arm64 -o MyApp.dSYM/Contents/Resources/DWARF/MyApp \
     -l 0x0 $offset

3.5 全局捕获流程与设计结构

下图展示了从 Mach 异常 / Unix 信号发生,到捕获模块、符号化、上报的完整链路:

iOS崩溃日志采集全链路结构图 iOS 崩溃日志采集全链路 应用层 App Mach 异常 (EXC_xxx) 内核态异常端口 Unix 信号 (SIGSEGV等) 用户态 signal handler 崩溃捕获模块(Handler) 获取 thread_state / 堆栈地址 JSON 序列化 + 沙盒缓存 下次启动上传 符号化引擎 / atos / dwarfdump 加载 dSYM → 空虚地址 → 符号 + 行号 可读崩溃报告 说明: 优先捕获Mach异常 失败时回退信号处理

3.6 常见错误与稳定性优化

  • Mach 端口泄漏: 每次 crash 后未回收 mach_port_t,导致进程重启后端口耗尽。应复用单例端口,不要每崩溃一次就新建一个。
  • 符号化失败: 超过 95% 的原因是没有在打包时保留或上传 dSYM。在 Xcode 中设置 DEBUG_INFORMATION_FORMAT = dwarf-with-dsym,并确认 Build Phase 中调用了上传脚本。
  • 信号处理器重入: 在 signal handler 中调用非 async-signal-safe 的函数(如 malloc、printf、ObjC 消息发送)会导致死锁。建议使用 write() 直接写入预分配缓冲区。
  • 性能优化: 不要在捕获处理线程中进行网络上报、文件解压等耗时操作。捕获线程应只做最轻量的数据保存,上报任务延迟到应用下次启动时执行。
调试提示: 手动触发崩溃测试捕获是否生效时,务必在真机上进行(模拟器行为与真机不完全一致),且需在 Release 模式下验证 dSYM 符号化是否正常。关闭 Xcode Debugger 以避免调试器抢先处理异常。

3.7 部署检查清单

# 检查项 操作
1 dSYM 生成 确认 Build Settings → DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
2 异常端口注册 在 +load 或 UIApplicationMain 前完成注册
3 信号处理器 注册 SIGSEGV / SIGABRT / SIGBUS / SIGTRAP 四种信号
4 符号化脚本 CI 将 upload-symbols 脚本加入 CI/CD 流程
5 沙盒写入权限 确认 crash 缓存路径在 NSCachesDirectory 下