Crash率是App稳定性的重要指标,一定要千方百计得降低crash率。crash的符号化应该成为每一个iOS开发者的必备技能,俗话说得好,不能符号化crash的开发不是好开发。

crashLog相关介绍

正急需解crash的小伙伴可以直接跳到章节crashlog符号化实战

作为iOS开发工程师,不遇到crash是不可能,而造成crash的原因又是多种多样,包括但不限于读写错误地址,执行非法指令,越权访问等等。一般系统在crash的会记录crash时的线程栈,方便开发者查明crash的原因。

系统在app crash的时候会生成crash log,crash log在真机上的位置一般为:/private/var/mobile/Library/Logs/CrashReporter/。 如下是一个实际的crashlog。crashLog结构解析

// 1: 进程信息
Incident Identifier: 1AB014C6-C27B-41A6-A776-ADEEE093BDBC           // 唯一的ID用来标识该crash
CrashReporter Key:   017e3ec4eb64f18c3e59100a9656b01232afa7d4       // 映射到设备标识的唯一key,如果你发现所有的crash都来自于相同或者相当少的key,那就不必太担心
Hardware Model:      iPhone5,3                                      // 设备类型,对应可参考[model对应机型](https://www.theiphonewiki.com/wiki/Models)
Process:             SampleApp [2764]                                // 应用名字,中括号里是crash时候的pid
Path:                /private/var/mobile/Containers/Bundle/Application/FDEEBADC-71CB-4A64-A80E-AB30486877C9/SampleApp.app/SampleApp // 应用在手机中的路径
Identifier:          com.antfortune.wealthrc                        // 应用Bundle Id
Version:             0.9.0.080302 (0.9.0)                           // 版本号
Code Type:           ARM (Native)                                   // 代码类型
Parent Process:      launchd [1]                                    // 父进程

// 基本信息
Date/Time:           2015-08-05 09:49:56.810 +0800                  // crash发生的时间
Launch Time:         2015-08-05 09:48:54.482 +0800                  // app启动的时间
OS Version:          iOS 8.4 (12H143)                               // iOS版本
Report Version:      105                                        

// 异常信息
Exception Type:  EXC_CRASH (SIGABRT)                                // 异常类型
Exception Codes: 0x0000000000000000, 0x0000000000000000             // 异常码,这里有几个常见的异常码,比如0x8badf00d->watchDog超时;0xdead10cc->deadlock,死循环啦;0xdeadfa11->用户强制退出等等
Triggered by Thread:  18                                            // 异常发生的线程

// 线程堆栈
Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0:

frame号    库名字                          函数调用地址          函数地址起始行数        + 执行到的行数
0         libsystem_kernel.dylib          0x3641f5a4          syscall_thread_switch + 8
。。。。。。。

Thread 18 name:  Dispatch queue: com.apple.root.default-qos         // 这就是crash线程
Thread 18 Crashed:
0   libsystem_kernel.dylib          0x36432df0 __pthread_kill + 8
1   libsystem_pthread.dylib         0x364b1cc2 pthread_kill + 58
2   libsystem_c.dylib               0x363ce904 abort + 72
3   libsystem_malloc.dylib          0x364633aa szone_error + 330
4   libsystem_malloc.dylib          0x36463654 free_list_checksum_botch + 24
5   libsystem_malloc.dylib          0x3645b3fe tiny_malloc_from_free_list + 1022
6   libsystem_malloc.dylib          0x36459eb6 szone_malloc_should_clear + 218
7   libsystem_malloc.dylib          0x3645d3fe malloc_zone_calloc + 90
8   libsystem_malloc.dylib          0x3645d38e calloc + 46
9   CoreFoundation                  0x2746f952 __rehashd + 22
10  CoreFoundation                  0x2738ea08 -[__NSDictionaryM setObject:forKey:] + 696
11  CoreFoundation                  0x27396da8 -[NSMutableDictionary addEntriesFromDictionary:] + 240
12  SampleApp                        0x0014a238 0x29000 + 1184312
13  SampleApp                        0x0014a312 0x29000 + 1184530
14  SampleApp                        0x0014a584 0x29000 + 1185156
15  Foundation                      0x281401f2 _decodeObjectBinary + 3042
16  Foundation                      0x2813f500 _decodeObject + 272
17  Foundation                      0x2813e8f2 +[NSKeyedUnarchiver unarchiveObjectWithData:] + 78
18  SampleApp                        0x0003089e 0x29000 + 30878
19  SampleApp                        0x0033f240 0x29000 + 3236416
20  SampleApp                        0x0033f1d4 0x29000 + 3236308
21  SampleApp                        0x0033cf6e 0x29000 + 3227502
22  SampleApp                        0x0035f92c 0x29000 + 3369260
23  SampleApp                        0x0035f8d4 0x29000 + 3369172
24  SampleApp                        0x0020573a 0x29000 + 1951546
25  SampleApp                        0x00204ea8 0x29000 + 1949352
26  libdispatch.dylib               0x363472e0 _dispatch_call_block_and_release + 8
27  libdispatch.dylib               0x3635137c _dispatch_root_queue_drain + 1384
28  libdispatch.dylib               0x363523be _dispatch_worker_thread3 + 90
29  libsystem_pthread.dylib         0x364aedbe _pthread_wqthread + 666
30  libsystem_pthread.dylib         0x364aeb10 start_wqthread + 4

。。。。。。。

// 寄存器状态
Thread 18 crashed with ARM Thread State (32-bit):
    r0: 0x00000000    r1: 0x00000000      r2: 0x00000000      r3: 0x0000004d
    r4: 0x00000006    r5: 0x0612d000      r6: 0x1574a854      r7: 0x0612c6f0
    r8: 0x00000001    r9: 0x364698b6     r10: 0x01186000     r11: 0x36469ac7
    ip: 0x00000148    sp: 0x0612c6e4      lr: 0x364b1cc7      pc: 0x36432df0 (注意这里和crash的线程正在执行的地址是一样的,因为pc寄存器保存的就是程序执行的位置)
  cpsr: 0x00000010


// 加载的二进制文件
Binary Images:
// SampleApp二进制文件加载的代码段位置是0x29000到0x94fff,其architecture是armv7,其UUID是**路径中的FDEEBADC-71CB-4A64-A80E-AB30486877C9**,注意!这里给我们解析crash log带来了很大的作用。
0x29000 - 0xd94fff SampleApp armv7  <166c5b9ea8cb381b930542bbaf3b979a> /var/mobile/Containers/Bundle/Application/FDEEBADC-71CB-4A64-A80E-AB30486877C9/SampleApp.app/SampleApp

// 以下是app运行所需要的所有的二进制文件
0x1fef8000 - 0x1ff1bfff dyld armv7s  <89c8b5de05ef310e9c399d3abd699990> /usr/lib/dyld
0x25a6c000 - 0x25a74fff AccessibilitySettingsLoader armv7s  <3ec2fd59cf1b3fb3832682498708136a> /System/Library/AccessibilityBundles/AccessibilitySettingsLoader.bundle/AccessibilitySettingsLoader
0x25cbf000 - 0x25dacfff RawCamera armv7s  <33b93bce28763ae881ac6156b1a070fe> /System/Library/CoreServices/RawCamera.bundle/RawCamera
0x25dc3000 - 0x25ed7fff IMGSGX543RC2GLDriver armv7s  <74afd60322403a43b03eadb4f7dfaa7d> /System/Library/Extensions/IMGSGX543RC2GLDriver.bundle/IMGSGX543RC2GLDriver
0x25ee3000 - 0x26050fff AVFoundation armv7s  <194a49546fc93e0d804440a70c033fa4> /System/Library/Frameworks/AVFoundation.framework/AVFoundation
0x26051000 - 0x260b0fff libAVFAudio.dylib armv7s  <1d4dcaa02bd3375397daf4ffa2cf24cf> /System/Library/Frameworks/AVFoundation.framework/libAVFAudio.dylib
0x260ea000 - 0x260eafff Accelerate armv7s  <2c29c5379fe43440b43aff96d73855ae> /System/Library/Frameworks/Accelerate.framework/Accelerate
0x260fb000 - 0x26316fff vImage armv7s  <603d854d418a39559bdfb5ec78b509bd> /System/Library/Frameworks/Accelerate.framework/Frameworks/vImage.framework/vImage
0x26317000 - 0x263fdfff libBLAS.dylib armv7s  <0834c18b8b2435bc9a1a0b9019ab2c7c> /System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/libBLAS.dylib
0x263fe000 - 0x266c2fff libLAPACK.dylib armv7s  <417a0b8d8dd636d499919893aeb6c258> /System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/libLAPACK.dylib
0x266c3000 - 0x266d4fff libLinearAlgebra.dylib armv7s  <ab870bd429bb32808ed43ee4d77b8637> /System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/libLinearAlgebra.dylib
0x266d5000 - 0x26751fff libvDSP.dylib armv7s  <3599c8eb992c302a9573108f9d7cb1b7> /System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/libvDSP.dylib
0x26752000 - 0x26764fff libvMisc.dylib armv7s  <9cc618389310324caa99bba4cee6b56e> /System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/libvMisc.dylib
0x26765000 - 0x26765fff vecLib armv7s  <6f2f59c102323ed0b1559f7523fcf7a5> /System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/vecLib
0x26766000 - 0x2678dfff Accounts armv7s  <ed90e206502e3b8194d1376d49c5ba45> /System/Library/Frameworks/Accounts.framework/Accounts

0x29000之谜

相信仔细看完上面的这个crashlog例子,大家对crashlog就不会感到那么陌生了。我们正要信心满满准备搞定crash的时候,仔细一看,才发现Thread 18给的crash信息在最关键的位置给打了码。这是为什么呢?为什么系统不将这些关键信息公之于众呢?要想搞明白这个问题,就需要弄明白SampleApp这个二进制文件是怎么生成的了。

要想解开0x29000之谜,咱们先从简单的hello程序出发,寻找其中的奥秘。 学过C++的同学都知道,从编写源代码到生成可执行文件,中间需要经过以下几步,并辅以简单的hello world来说明。

源程序     ->              编译           ->                 链接                 -> 可执行文件
hello.cpp                              hello.o                                      hello
(ASCII c program text)                 (Mach-O 64-bit object x86_64)              (Mach-O 64-bit executable x86_64)

编译生成的hello.o是一个object文件,每个object文件都有自己的一张符号表,里面包含了所有外部可见的变量、函数等的标识符符号表),这样在链接的时候,链接器就会根据符号表去解析未解析的符号,否则就会链接失败,报undefined symbol错误。

编译链接过程中符号表可能的存在形式;

  • 可能仅仅存在编译链接过程,完成之后就被丢弃了(大部分发布的二进制文件都是不包含符号表的),
  • 也可能打在二进制文件里面,以便后期再用(比如我们调试的时候),
  • 也有可能用单独的bundle来存符号表(.dSYM文件)

这里说到的符号表就是我们解开0x29000的关键所在,符号表对二进制文件中的指令和源代码中的符号做了映射,使得我们能够从二进制文件的指令中还原我们能看的懂的代码。

光说不练假把式,那我们继续已上面的hello为例,说明其中的关系。按照下面的步骤进行我们的试验。

1、以debug模式编译包

g++ -g hello.cpp           // 生成a.out可执行文件和a.out.dSYM符号表文件

使用nm看a.out的符号表。(nm工具)

0000000100001e0c s GCC_except_table2
0000000100001e4c s GCC_except_table3
0000000100001efc s GCC_except_table5
                 U __Unwind_Resume
                 U __ZNKSt3__16locale9use_facetERNS0_2idE
                 U __ZNKSt3__18ios_base6getlocEv
0000000100001cb0 t __ZNSt3__111char_traitsIcE11eq_int_typeEii
0000000100001cd0 t __ZNSt3__111char_traitsIcE3eofEv
00000001000015d0 t __ZNSt3__111char_traitsIcE6lengthEPKc
                 U __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6__initEmc
                 U __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
                 U __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE3putEc
                 U __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE5flushEv
                 U __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryC1ERS3_
                 U __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryD1Ev
00000001000015f0 t __ZNSt3__116__pad_and_outputIcNS_11char_traitsIcEEEENS_19ostreambuf_iteratorIT_T0_EES6_PKS4_S8_S8_RNS_8ios_baseES4_
0000000100001150 t __ZNSt3__124__put_character_sequenceIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_PKS4_m
                 U __ZNSt3__14coutE
0000000100001040 t __ZNSt3__14endlIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_
                 U __ZNSt3__15ctypeIcE2idE
                 U __ZNSt3__16localeD1Ev
                 U __ZNSt3__18ios_base33__set_badbit_and_consider_rethrowEv
                 U __ZNSt3__18ios_base5clearEj
0000000100000ff0 t __ZNSt3__1lsINS_11char_traitsIcEEEERNS_13basic_ostreamIcT_EES6_PKc
                 U __ZSt9terminatev
0000000100001c90 t ___clang_call_terminate
                 U ___cxa_begin_catch
                 U ___cxa_end_catch
                 U ___gxx_personality_v0
0000000100000000 T __mh_execute_header
0000000100000fa0 T _main
                 U _strlen
                 U dyld_stub_binder

2、使用strip移除符号

cp a.out a.stripped.out         // 拷贝a.out,使用strip生成符号表被缩减过的二进制文件
strip a.stripped.out

使用nm查看a.stripped.out符号表

                 U __Unwind_Resume
                 U __ZNKSt3__16locale9use_facetERNS0_2idE
                 U __ZNKSt3__18ios_base6getlocEv
0000000100001cb0 t __ZNSt3__111char_traitsIcE11eq_int_typeEii
0000000100001cd0 t __ZNSt3__111char_traitsIcE3eofEv
00000001000015d0 t __ZNSt3__111char_traitsIcE6lengthEPKc
                 U __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6__initEmc
                 U __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
                 U __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE3putEc
                 U __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE5flushEv
                 U __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryC1ERS3_
                 U __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryD1Ev
0000000100001150 t __ZNSt3__124__put_character_sequenceIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_PKS4_m
                 U __ZNSt3__14coutE
                 U __ZNSt3__15ctypeIcE2idE
                 U __ZNSt3__16localeD1Ev
                 U __ZNSt3__18ios_base33__set_badbit_and_consider_rethrowEv
                 U __ZNSt3__18ios_base5clearEj
0000000100000ff0 t __ZNSt3__1lsINS_11char_traitsIcEEEERNS_13basic_ostreamIcT_EES6_PKc
                 U __ZSt9terminatev
                 U ___cxa_begin_catch
                 U ___cxa_end_catch
                 U ___gxx_personality_v0
0000000100000000 T __mh_execute_header
                 U _strlen
                 U dyld_stub_binder

仔细观察两者的差别,可以发现strip之后,a.stripped.out文件比a.out小了,其中很多符号被移除了,比如_main,GCC_except_table2等。一般正式发布的二进制都会将将符号表strip,以便减少安装包的大小。

3、使用lldb加载二进制(lldb)

lldb是下一代的高性能调试器,使用lldb可以帮助我们调试和验证。

target create --no-dependents --arch x86_64 a.out   #创建目标a.out
image list                                          #查看加载的二进制文件

 # sample output
 #[  0] D081A049-722D-39D9-A9E8-4B1D79BA0AEB 0x0000000100000000 /Users/xxuser/Program/cpptest/a.out 
 #      /Users/xxuser/Program/cpptest/a.out.dSYM/Contents/Resources/DWARF/a.out

image load --file a.out __TEXT 0                    #加载image到内存中,并且把TEXT段的起始地址设为0

image lookup -a 0xfa0                               #比如我们要查看0000000100000fa0地址处的代码是什么,就可以这样查看,注意这里使用了偏移地址
 # sample output
 # Address: a.out[0x0000000100000fa0]  (a.out.__TEXT.__text + 0)
 #     Summary: a.out`main at hello.cpp:8

是不是很神奇,我们发现,这样我们找出来的符号是main,而a.out的符号表中main对应的地址也正好是0x0000000100000fa0!

好了,以上就是符号化crash的全部原理了,那么说了那么多,相信大家都知道0x29000就是我们hello中的0x0000000100000000了,也就是SampleApp代码段的起始地址,相信大家也知道如果有了符号表,那么所有的真相都将被公之于众。

crashlog符号化实战

上文提及,发布的二进制包有两种情况,一种是包含符号表的,一种是不包含的,下面我们的解析也就分两种,首先是有符号表的,其次是没有符号表的。

二进制中有符号表

有符号表可以直接使用lldb调试器来进行crash的符号化,而需要的文件也就只是二进制和crash文件。就本文开头的crash文件为例。

就解如下12、13行。

Thread 18 Crashed:
0   libsystem_kernel.dylib          0x36432df0 __pthread_kill + 8
1   libsystem_pthread.dylib         0x364b1cc2 pthread_kill + 58
2   libsystem_c.dylib               0x363ce904 abort + 72
3   libsystem_malloc.dylib          0x364633aa szone_error + 330
4   libsystem_malloc.dylib          0x36463654 free_list_checksum_botch + 24
5   libsystem_malloc.dylib          0x3645b3fe tiny_malloc_from_free_list + 1022
6   libsystem_malloc.dylib          0x36459eb6 szone_malloc_should_clear + 218
7   libsystem_malloc.dylib          0x3645d3fe malloc_zone_calloc + 90
8   libsystem_malloc.dylib          0x3645d38e calloc + 46
9   CoreFoundation                  0x2746f952 __rehashd + 22
10  CoreFoundation                  0x2738ea08 -[__NSDictionaryM setObject:forKey:] + 696
11  CoreFoundation                  0x27396da8 -[NSMutableDictionary addEntriesFromDictionary:] + 240
12  SampleApp                        0x0014a238 0x29000 + 1184312
13  SampleApp                        0x0014a312 0x29000 + 1184530
target create --no-dependents --arch armv7 SampleApp.app/SampleApp  // 这里arch需要根据crash log中的二进制文件部分查看
 
 # sample output
 # Current executable set to 'SampleApp.app/SampleApp' (armv7).

image list

 # sample output
 # [  0] 166C5B9E-A8CB-381B-9305-42BBAF3B979A 0x00004000 /Users/xxuser/Desktop/crash/SampleApp.app/SampleApp 
 #     /Users/xxuser/Desktop/crash/SampleApp.app.dSYM/Contents/Resources/DWARF/SampleApp

image load --file SampleApp __TEXT 0
 # sample output 
 # section '__TEXT' loaded at 0x0

image lookup -a 1184312
 # sample output
 # Address: SampleApp[0x00125238]  (SampleApp.__TEXT.__text + 1154248)
 #     Summary: SampleApp`-[AFWBaseModel codableProperties] + 212 at AFWBaseModel.m:151

image lookup -a 1184530
 # sample output
 # Address: SampleApp[0x00125312]  (SampleApp.__TEXT.__text + 1154466)
 # Summary: SampleApp`-[AFWBaseModel setWithCoder:] + 150 at AFWBaseModel.m:163

二进制中没有符号表

没有符号表的话,解析之前需要准备三样东西:

  • crash log(i.e. SampleApp.crash),可以通过Xcode或者itunes导出crash log的导出
  • .app package(i.e. SampleApp.app),可以通过解压ipa包,从Payload中获得,也可以直接从机器上拷贝 /User/Containers/Bundle/Application/9D8A82D6-DFE5-45FD-B994-77C93A294B03/SampleApp.app 真机上的app位置
  • .dSYM package,其包含调试信息(i.e. SampleApp.app.dSYM) /Users/xxuser/Library/Developer/Xcode/DerivedData/SampleApp-eangdqkywlcbatcvllxataoasqrj/Build/Products 可在这个目录下找到

接着需要验证这三样东西是否匹配:(How to Match a Crash Report to a Build)

  • 查看crash log的UUID: Binary Images下的UUID
  • 使用dwarfdump查看binary package的UUID
dwarfdump --uuid SampleApp.app/SampleApp
  • 查看.dSYM的UUID
dwarfdump --uuid SampleApp.app.dSYM

三者的uuid一致的话,就说明准备完毕了,然后将他们放在一个目录下,目录结构如下

ls ./

 #sample output
 # SampleApp.app
 # SampleApp.app.dSYM
 # SampleApp.crash

全部准备工作完毕以后,就可以直接使用xcode提供的工具来符号化了。 1、推荐用法symbolicatecrash

 # 一般symbolicatecrash 都在此目录下,如果不在的话,可以尝试在/Applications目录下搜索 find ./ -name symbolicatecrash 
/Applications/Xcode.app/Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash -v SampleApp.crash 2> symbolicatecrash.log

 # 如果报 “DEVELOPER_DIR” is not defined错误,那就执行
 # export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"

2、atos (atos man page)

xcrun atos -o SampleApp.app.dSYM/Contents/Resources/DWARF/SampleApp -l 0x29000 -arch armv7        # 这些参数都可以在crashlog文件的二进制文件部分获得

 # 然后输入crash地址就可以了

0x0014a238
 # sample output
 # -[AFWBaseModel codableProperties]  (in SampleApp) (AFWBaseModel.m:151)
0x0014a312
 # sample output
 # -[AFWBaseModel setWithCoder:]  (in SampleApp) (AFWBaseModel.m:163)

如果使用了上面的方法还是不能解,那就只能再检查一遍uuid有没有对上、arch有没有写对。

Further Reading