关于iOS的崩溃分析网上有很多文章,有利用xcode
来进行符号化的,有利用symbolicatecrash
工具来进行符号化的,但经常会遇到无法符号化
,或者部分符号化
的情况,那该如何处理呢,今天就来好好分析一下吧
背景知识
先了解几个概念:
-
符号文件
:存储了函数内存地址
和方法名称及其实现文件
等信息的映射关系。 -
DSYM文件
:应用程序打包编译后生成的符号文件。里面只有我们应用程序的符号信息。 -
ASLR机制
:应用程序代码块被加载到内存时会主动加上一个随机的偏移量,防止缓冲区溢出攻击。全称Address space layout randomization
认识崩溃日志
这是一个ios crash log文件,省略部分内部:
{"app_name":"AppCrashTest","timestamp":"2020-08-24 11:17:29.11 +0800","app_version":"8.2.2.4","slice_uuid":"4452a677-4a33-301f-88cb-b44e9f744062","adam_id":0,"build_version":"1","bundleID":"com.mccm.test","share_with_app_devs":false,"is_first_party":false,"bug_type":"109","os_version":"iPhone OS 12.4 (16G77)","incident_id":"50DCADC2-896D-4D0B-889C-0DF39AFDBBA0","name":"AppCrashTest"}
Incident Identifier: 50DCADC2-896D-4D0B-889C-0DF39AFDBBA0
CrashReporter Key: 6afda7a3846fb33ba936d48b9cbbfabf2903c90b
Hardware Model: iPhone9,1
Process: AppCrashTest [4277]
Path: /private/var/containers/Bundle/Application/210704D2-AA1F-468F-9840-679AB05FB371/AppCrashTest.app/AppCrashTest
Identifier: com.mccm.test
Version: 1 (8.2.2.4)
Code Type: ARM-64 (Native)
Role: Non UI
Parent Process: launchd [1]
Coalition: com.mccm.test [1099]
Date/Time: 2020-08-24 11:17:28.9696 +0800
Launch Time: 2020-08-24 11:10:44.1941 +0800
OS Version: iPhone OS 12.4 (16G77)
Baseband Version: 5.70.01
Report Version: 104
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
Application Specific Information:
abort() called
Last Exception Backtrace:
(0x196cde98c 0x195eb79f8 0x196bf8098 0x1976bc088 0x1c391ec78 0x1c39364f4 0x1c39365fc 0x1054f4504 0x104c1cc24 0x196c4fa28 0x196c4f9f4 0x196c4eee8 0x196c4eb94 0x196bc8474 0x196c4e644 0x1976376f4 0x104ae571c 0x104ae41f0 0x19671ca38 0x19671d7d4 0x1966cb008 0x196c7032c 0x196c6b264 0x196c6a7c0 0x198e6b79c 0x1c3735c38 0x104e17adc 0x19672e8e0)
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x000000019687b0dc 0x196858000 + 143580
1 libsystem_pthread.dylib 0x00000001968f4094 0x1968f2000 + 8340
2 libsystem_c.dylib 0x00000001967d3ea8 0x196779000 + 372392
3 libc++abi.dylib 0x0000000195ea0788 0x195e9f000 + 6024
4 libc++abi.dylib 0x0000000195eac858 0x195e9f000 + 55384
5 libc++abi.dylib 0x0000000195eac8c4 0x195e9f000 + 55492
6 libdispatch.dylib 0x000000019671d7e8 0x1966bd000 + 395240
7 libdispatch.dylib 0x00000001966cb008 0x1966bd000 + 57352
8 CoreFoundation 0x0000000196c7032c 0x196bc6000 + 697132
9 CoreFoundation 0x0000000196c6b264 0x196bc6000 + 676452
10 CoreFoundation 0x0000000196c6a7c0 0x196bc6000 + 673728
11 GraphicsServices 0x0000000198e6b79c 0x198e61000 + 42908
12 UIKitCore 0x00000001c3735c38 0x1c2e79000 + 9161784
13 AppCrashTest 0x0000000104e17adc 0x104910000 + 5274332
14 libdyld.dylib 0x000000019672e8e0 0x19672d000 + 6368
......
Thread 3:
0 libsystem_kernel.dylib 0x000000019687aee4 0x196858000 + 143076
1 libsystem_pthread.dylib 0x00000001968f5cf8 0x1968f2000 + 15608
2 libc++.1.dylib 0x0000000195e51128 0x195e49000 + 33064
3 AppCrashTest 0x0000000105a5cc58 0x104910000 + 18140248
4 AppCrashTest 0x0000000105b9d3e0 0x104910000 + 19452896
5 AppCrashTest 0x0000000105b9d1a0 0x104910000 + 19452320
6 AppCrashTest 0x0000000105b9d200 0x104910000 + 19452416
7 AppCrashTest 0x0000000105b873fc 0x104910000 + 19362812
8 AppCrashTest 0x0000000105b87c94 0x104910000 + 19365012
9 libsystem_pthread.dylib 0x00000001968fd2c0 0x1968f2000 + 45760
10 libsystem_pthread.dylib 0x00000001968fd220 0x1968f2000 + 45600
11 libsystem_pthread.dylib 0x0000000196900cdc 0x1968f2000 + 60636
Binary Images:
0x104910000 - 0x10742bfff AppCrashTest arm64 <4452a6774a33301f88cbb44e9f744062> /var/containers/Bundle/Application/210704D2-AA1F-468F-9840-679AB05FB371/AppCrashTest.app/AppCrashTest
0x108310000 - 0x10840bfff Charts arm64 <0eae557f017e31ceade3a43c5a644b92> /var/containers/Bundle/Application/210704D2-AA1F-468F-9840-679AB05FB371/AppCrashTest.app/Frameworks/Charts.framework/Charts
0x1085c0000 - 0x108617fff dyld arm64 <06f3d9add3a233cea57df42b73686817> /usr/lib/dyld
......
线程堆栈解读
第一列:调用序号
第二列:调用方法所属的binary image信息
第三列:调用方法的内存地址
第四列:调用方法所属的image加载首地址+偏移量,和第三列值一样,可以不用关心
Binary Images解读,以AppCrashTest为例
-
0x1048d0000 - 0x1073ebfff
:image被加载到内存后的地址段 -
AppCrashTest
: image信息描述 -
arm64
: 构架信息 -
4452a6774a33301f88cbb44e9f744062
: 该image的唯一id,该id必须和符号文件的uuid匹配上,否则无法进行符号化,libSystem.B.dylib和AppCrashTest都一样。
获取符号文件的uuid: dwarfdump --uuid 符号文件路径
符号化过程分析
符号化的过程其实非常简单,就是拿crash文件中堆栈信息中的方法内存地址
到相应image的符号文件
中找映射的方法描述信息
。
例如我们想符号化这行日志:
AppCrashTest 0x0000000104e17adc 0x104910000 + 5274332
就拿0x0000000104e17adc
这个地址到AppCrashTest这个image对应的符号文件中去找方法描述信息。
想符号化这行日志:
CoreFoundation 0x0000000196c7032c 0x196bc6000 + 697132
就拿0x0000000196c7032c
这个地址到CoreFoundation这个image对应的符号文件中去找方法描述信息。
所以整体crash文件符号化过程就是,逐行读取堆栈信息,然后获取第一行的image信息
和内存地址
,然后到其相应的符号文件
中查找方法名即可。
Last Exception Backtrace是如何符号化的
仔细观察Binary Images列表,你会发现image被加载到内存后的地址段基本是连续的,并且不会有重叠。所以Last Exception Backtrace
中的每个内存地址一定处于某个image地址段中,遍历找到其所在的地址段就能找到其所属image信息
,然后到对应的符号文件
中查找方法描述信息即可。
手动解析
上面的说法只是基于我们的猜测,到底是不是这样的,还需要通过实践来证明。xcode给我提供了一个工具来进行符号化:
xcrun atos -arch -o /Contents/Resources/DWARF/ -l
-arch 所属架构
-o 符号文件地址
-l image被加载到内存的起始地址
其中-l
的值为该行日志所属image被加载到内存中的起始地址
,我们来解析一下thread 0中第13行:
13 AppCrashTest 0x0000000104e17adc 0x104910000 + 5274332
先输入
//注意AppCrashTest 这个image的初始地址为0x104910000
crash % xcrun atos -arch arm64 -o ./AppCrashTest.app.dSYM/Contents/Resources/DWARF/AppCrashTest -l 0x104910000
回车后输入方法的内存地址0x0000000104e17adc
,打印如下:
main (in AppCrashTest) (main.m:13)
再试一下thread 3中这几行:
3 AppCrashTest 0x0000000105a5cc58 0x104910000 + 18140248
4 AppCrashTest 0x0000000105b9d3e0 0x104910000 + 19452896
5 AppCrashTest 0x0000000105b9d1a0 0x104910000 + 19452320
6 AppCrashTest 0x0000000105b9d200 0x104910000 + 19452416
7 AppCrashTest 0x0000000105b873fc 0x104910000 + 19362812
8 AppCrashTest 0x0000000105b87c94 0x104910000 + 19365012
//回车后输入相应的地址
0x0000000105a5cc58
std::__1::cv_status std::__1::condition_variable::wait_until > >(std::__1::unique_lock&, std::__1::chrono::time_point > > const&) (in AppCrashTest) + 124
0x0000000105b9d3e0
std::__1::cv_status std::__1::condition_variable_any::wait_until, std::__1::chrono::steady_clock, std::__1::chrono::duration > >(std::__1::unique_lock&, std::__1::chrono::time_point > > const&) (in AppCrashTest) + 104
0x0000000105b9d1a0
TXCCondition::wait(std::__1::unique_lock&, long) (in AppCrashTest) + 88
0x0000000105b9d200
TXCCondition::wait(long) (in AppCrashTest) + 60
0x0000000105b873fc
__async_log_thread() (in AppCrashTest) + 176
0x0000000105b87c94
void* std::__1::__thread_proxy >, void (*)()> >(void*) (in AppCrashTest) + 44
按此步骤所有属于AppCrashTest image的日志信息都能被符号化出来,那些系统方法的调用该如何符号化呢?
符号化系统库
我们应用程序对应的image模块能够通过atos
符号化,是因为我们有DSYM文件,那系统符号文件如何获取呢?有多种方式,关于系统库的符号文件获取大家可以参考iOS Crash分析必备:符号化系统库方法
由于这个符号文件
的uuid要和我们本次分析的crash
文件中对应image
的uuid
一致才能进行符号化,而市面上的ios手机和操作系统实太多,所以相应的系统符号文件
也是非常多的。不过还好网上已经有人收集了历史版本的符号文件
,链接在这里网上收集的部分系统库符号文件。
如果你用过友盟或者bugly,你会发现在它们崩溃分析中有些系统库的日志也是无法符号化的,那是因为它们对于苹果历史版本的符号文件也收集不全
所幸OS Version: iPhone OS 12.4 (16G77)
在我本机的/Users/xxx/Library/Developer/Xcode/iOS DeviceSupport/
目录下有,接下来我们分析一下thread 0中这几行日志:
0 libsystem_kernel.dylib 0x000000019687b0dc 0x196858000 + 143580
1 libsystem_pthread.dylib 0x00000001968f4094 0x1968f2000 + 8340
2 libsystem_c.dylib 0x00000001967d3ea8 0x196779000 + 372392
3 libc++abi.dylib 0x0000000195ea0788 0x195e9f000 + 6024
4 libc++abi.dylib 0x0000000195eac858 0x195e9f000 + 55384
5 libc++abi.dylib 0x0000000195eac8c4 0x195e9f000 + 55492
6 libdispatch.dylib 0x000000019671d7e8 0x1966bd000 + 395240
7 libdispatch.dylib 0x00000001966cb008 0x1966bd000 + 57352
第0行libsystem_kernel:
dyf@dyfdeMacBook-Pro crash % xcrun atos -arch arm64 -o '/Users/dyf/Library/Developer/Xcode/iOS DeviceSupport/12.4 (16G77)/Symbols/usr/lib/system/libsystem_kernel.dylib' -l 0x196858000
0x000000019687b0dc
__pthread_kill (in libsystem_kernel.dylib) + 8
第1行libsystem_pthread:
dyf@dyfdeMacBook-Pro crash % xcrun atos -arch arm64 -o '/Users/dyf/Library/Developer/Xcode/iOS DeviceSupport/12.4 (16G77)/Symbols/usr/lib/system/libsystem_pthread.dylib' -l 0x1968f2000
0x00000001968f4094
pthread_kill$VARIANT$mp (in libsystem_pthread.dylib) + 380
第6-7行libdispatch:
dyf@dyfdeMacBook-Pro crash % xcrun atos -arch arm64 -o '/Users/dyf/Library/Developer/Xcode/iOS DeviceSupport/12.4 (16G77)/Symbols/usr/lib/system/libdispatch.dylib' -l 0x1966bd000
0x000000019671d7e8
_dispatch_client_callout (in libdispatch.dylib) + 36
0x00000001966cb008
_dispatch_main_queue_callback_4CF$VARIANT$mp (in libdispatch.dylib) + 1068
所以只要能找到相应版本的系统符号文件
我们就能对系统库进行符号化。
关于Slide偏移
前面我们提到ASLR机制
,我们的应用程序被加载时会先生成一个随机值,然后所有的image被加载到内存都会加上这个值,这个随机值就是Slide偏移量
。
在分析Slide之前我们先认识一个新的概念Load Command
,它是应用程序编译完成生成的可执行文件的描述信息。可以通过otool
命令查看,还是用上面的AppCrashTest应用来举例:
otool -l AppCrashTest.app/AppCrashTest
...
//注意,这里只看arm64的
Load command 1
cmd LC_SEGMENT_64
cmdsize 1192
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000002b1c000
fileoff 0
filesize 45203456
maxprot 0x00000005
initprot 0x00000005
nsects 14
flags 0x0
...
信息很多,但我们重点关注的是__TEXT
段,这是代码加载段,vmaddr
为其起始地址,filesize
为整个代码数据占用内存大小。如果没有ASLR机制
程序被加载时,其代码数据被加载到内存应该是从0x100000000
开始占用45203456
个字节。我们再来看一下crash文件中真实的AppCrashTest程序被加载时内存段:
0x104910000 - 0x10742bfff AppCrashTest arm64 <4452a6774a33301f88cbb44e9f744062> /var/containers/Bundle/Application/210704D2-AA1F-468F-9840-679AB05FB371/AppCrashTest.app/AppCrashTest
0x10742bfff - 0x104910000 + 1 = 45203456
运行过程中AppCrashTest的代码是从0x10742bfff
开始占用45203456
字节,直到0x104910000
结束。
所以从Slide = 0x104910000 - 0x100000000 = 0x4910000。整个应用程序的内存地址偏移了0x4910000
个字节。
如何证明呢?
任然需要利用atos
,查看atos
命令的帮助说明:
dyf@dyfdeMacBook-Pro crash % atos --help
Usage: atos [-p pid] [-o executable] [-f file] [-s slide | -l loadAddress] [-arch architecture] [-printHeader] [-fullPath] [address ...]
--fullPath show full path to source file
我们可以看到-l
和-s
是二选一,那这次我们来使用-s
对thread 0中的这行日志进行解析:
AppCrashTest 0x0000000104e17adc 0x104910000 + 5274332
输入命令如下:
xcrun atos -arch arm64 -o AppCrashTest.app.dSYM/Contents/Resources/DWARF/AppCrashTest -s 0x4910000
回车后输入0x0000000104e17adc,打印如下:
main (in AppCrashTest) (main.m:13)
依然能够解析出来。可以看到-s
这种方式来进行符号化比较麻烦,所以大部分情况下用-l
即可。
注意
-s
这种方式只能解析我们应用程序的符号信息,系统库的是无法解析的。对于系统库的符号文件来说是没有slide这个概念的,系统库的__TEXT
的地址段和运行时被加载到内存后的地址段是一致的,这就是动态库的设计思路,为了节约内存
,所有的系统库在内存中都只需要加载一次。
通过对比同一个版本的两们崩溃日志也会发现,我们应用程序的image地址段在两次crash文件中不一样,但系统库的地址段都是一样的。