第一课:崩溃日志初探

大家好,我是你们的讲师。今天咱们聊聊崩溃日志。

说实话,我刚入行那会儿,看到崩溃日志就头疼。满屏的十六进制地址,一堆看不懂的线程信息,感觉像在看天书。后来我才明白,这东西其实是逆向工程师的宝藏。

什么是崩溃日志?

崩溃日志,说白了就是App挂掉时系统留下的“遗书”。它会记录下死因、死亡时间、死前最后一刻在干什么。

举个例子,你写了个iOS应用,用户一打开就闪退。系统会生成一个.crash文件,里面写着:

  • 什么设备(iPhone 14 Pro)
  • 什么系统版本(iOS 17.2)
  • 什么时间崩溃的
  • 崩溃时CPU在跑哪段代码

Android那边类似,会生成一个tombstone文件或者Java堆栈。嗯,名字挺吓人的,但内容其实差不多。

核心要点:崩溃日志是系统在App异常终止时自动生成的诊断文件。它记录了崩溃时的线程状态、寄存器值、调用栈等信息。

为什么需要符号化?

这个问题我问过很多新人。他们觉得崩溃日志里那些十六进制地址挺酷的,像黑客帝国。但实际工作中,这些地址屁用没有。

你想想看,日志里写着:

0x1045e3a4c 0x1045e2000 + 0x1a4c

这能看出啥?啥也看不出来。但符号化之后,它会变成:

-[MyViewController viewDidLoad] + 124

这下就清楚了——原来是MyViewController的viewDidLoad方法里第124行出事了。

我在项目中遇到过最坑的一次:客户反馈App在某个页面必崩,但我们的测试机死活复现不了。后来拿到用户的崩溃日志,一看符号化后的堆栈,发现是某个第三方SDK在特定机型上内存泄漏。如果没有符号化,我可能还在傻乎乎地翻自己的代码。

我的习惯:每次发版前,我都会把对应的dSYM文件(iOS)或mapping文件(Android)归档保存。别问我为什么,问就是吃过亏。

iOS与Android崩溃日志的异同

这两个平台的崩溃日志,就像北京和上海的煎饼果子——都是煎饼,但做法不一样。

对比维度 iOS Android
文件格式 .crash 或 .ips tombstone 或 Java堆栈
符号化工具 atos, symbolicatecrash ndk-stack, ReTrace
符号文件 dSYM (Debug Symbol) mapping.txt (ProGuard/R8)
崩溃类型 EXC_BAD_ACCESS, SIGSEGV 等 NullPointerException, SIGABRT 等
获取方式 Xcode Organizer, 设备日志 adb logcat, Google Play Console

这里有几个关键区别,我重点说一下:

iOS崩溃日志的特点

iOS的崩溃日志,我个人觉得更“优雅”一些。它会把所有线程的堆栈都列出来,包括主线程和后台线程。而且每个线程都有完整的调用栈,从系统库到你的代码,一层层都给你扒干净。

举个例子,iOS日志里你会看到这样的结构:

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libobjc.A.dylib                0x1a3e4f000 objc_msgSend + 16
1   UIKitCore                      0x1b8c2a000 -[UIView(UIViewGeometry) setFrame:] + 64
2   MyApp                          0x1045e3a4c 0x1045e2000 + 0x1a4c
3   MyApp                          0x1045e3b00 0x1045e2000 + 0x1b00

看到没?第2行和第3行是咱们自己的代码,前面是十六进制地址。符号化之后,这些地址就会变成方法名和行号。

注意:iOS的崩溃日志里,二进制模块的加载地址(0x1045e2000)每次启动都可能不同。所以你不能直接拿地址去查,必须用符号化工具。

Android崩溃日志的特点

Android这边就复杂一些。它有两种崩溃:

  • Java崩溃:发生在Java/Kotlin层,堆栈清晰,直接能看到类名和方法名
  • Native崩溃:发生在C/C++层,跟iOS一样,也是一堆十六进制地址

我曾经调试过一个Android Native崩溃,日志里全是:

#00 pc 0x0001a4c  /system/lib64/libc.so (abort+160)
#01 pc 0x0002b00  /system/lib64/libc.so (__assert2+20)
#02 pc 0x003c4a0  /vendor/lib64/egl/libGLESv2_adreno.so

嗯,看着就头疼。但用ndk-stack符号化之后,就能看到具体是哪个so文件里的哪个函数出了问题。

避坑指南:我曾经因为没保存ProGuard的mapping文件,导致线上崩溃堆栈全是a.b.c()这种混淆后的名字。从那以后,我每次发版都会把mapping文件备份到三个地方——本地、Git、云盘。

符号化的本质

说了这么多,符号化到底在干什么?

说白了,就是把内存地址翻译成人类能读懂的代码位置。这个过程需要两个东西:

  1. 崩溃日志本身——里面有地址信息
  2. 符号文件——里面有地址和代码的对应关系

iOS的dSYM文件,Android的mapping.txt,都是干这个用的。它们就像一本字典,告诉你0x1a4c对应的是viewDidLoad方法的第124行。

你可能会问:为什么系统不直接生成符号化的日志?

原因很简单——安全。如果App Store分发的应用里直接包含符号信息,那逆向工程就太容易了。所以苹果和谷歌都选择把符号文件留在开发者手里,只有拿到dSYM或mapping文件的人才能看懂崩溃日志。

小结

这一课咱们聊了:

  • 崩溃日志是App挂掉时的诊断记录
  • 符号化是把地址翻译成代码位置的过程
  • iOS和Android的日志格式不同,但原理相通
  • 保存符号文件是基本功,别偷懒

下一课,我会带大家手把手符号化一个真实的iOS崩溃日志。到时候你们会发现,这东西其实没那么神秘。

记住一句话:崩溃日志是逆向工程师最好的朋友。别怕它,读懂它,你就能解决90%的线上问题。