Categories
程式開發

海神平台iOS端崩溃日志解析踩坑之旅


开始这篇文章的时候,我内心是拒绝的,毕竟 Google 搜索“iOS Crash解析”,内容不要太多。

自己其实也是阅文无数,但是每每到了动手解析一个Crash日志的时候,往往还要再去翻书签。“纸上得来终觉浅,绝知此事要躬行”。这次我们换个思路,以前都是讲完原理讲应用,这次我们讲海神平台在Crash解析功能开发过程中遇到的问题以及解决方案,争取大家看完以后不用存书签。

1. 如何获取Crash日志

元数据获取的话题永不过时。

常见的日志获取方式有以下几种:

  1. 将崩溃设备连接到Xcode导出Crash日志,如果有符号表,则Crash日志直接被解析
  2. 依赖三方应用,类似于iTools、iMazing等,可以导出Crash日志
  3. 通过iTunes Connection获取,此项需要用户在客户端授权
  4. 使用imobiledevice套件进行导出,这种方式在自动化测试中广泛应用
  5. 使用bugly、Fabric等商业平台收集,这种方式在发布环境使用较多
  6. 使用开源Crash框架收集上报,这种方式在发布环境使用较多

前4种方案在开发测试阶段非常有效,但是应用发布之后却比较无力,因为开发同学既无法获取崩溃设备,也无法保证用户授权。

方案5,当前商业平台都没有数据导出接口,所以无法获取崩溃日志。

海神平台的定位是已发布应用的崩溃日志收集平台,所以最终依赖KSCrash在发布阶段获取客户端Crash日志。

1.1 KSCrash与上下文

KSCrash是著名且有效的崩溃日志收集框架,提供抓取多种类型崩溃上下文,包括:mach、signal、C++ Exception、OC Exception等的能力,并支持多种渠道数据上报。详细内容可以参考Github。

KSCrash抓取的上下文默认组织为JSON格式,同时框架提供了类和方法用于将JSON转换为Apple Format。下面是一个JSON格式的例子:

{
    "report": {
        "id": "2BF8D28A-A2A5-4076-952C-F8EFB4A42456",
        "process_name": "LJBaseCrashReporter_Example",
        "timestamp": 1569731896698234,
        ...
    },
    "binary_images": [
        {
            "image_addr": 4377903104,
            "image_vmaddr": 4294967296,
            "image_size": 2818048,
            "name": "/var/containers/Bundle/Application/92937E4A-DFA9-4BAF-9780-2F8796A1A6C7/LJBaseCrashReporter_Example.app/LJBaseCrashReporter_Example",
            "uuid": "FE056305-553D-3ED3-AB93-7AAB60BDE692",
            "cpu_type": 16777228,
            "cpu_subtype": 0,
            "major_version": 0,
            "minor_version": 0,
            "revision_version": 0
        },
        ...
    ],
    "system": {
        "system_name": "iOS",
        "system_version": "12.4",
        "machine": "iPhone8,2",
        "model": "N66mAP",
        "CFBundleIdentifier": "com.lianjia.LJBaseCrashReporter",
        ...
    },
    "crash": {
        "error": {
            "mach": {
                "exception": 1,
                "exception_name": "EXC_BAD_ACCESS",
                "code": 1,
                "code_name": "KERN_INVALID_ADDRESS",
                "subcode": 8
            },
            "signal": {
                "signal": 11,
                "name": "SIGSEGV",
                "code": 0,
                "code_name": "SEGV_NOOP"
            },
            "address": 1,
            "type": "mach"
        },
        "threads": [
            {
                "backtrace": {
                    "contents": [
                        {
                            "object_name": "LJBaseCrashReporter_Example",
                            "object_addr": 4377903104,
                            "symbol_name": "-[LJCrashDebugMachsController tableView:didSelectRowAtIndexPath:]",
                            "symbol_addr": 4378464516,
                            "instruction_addr": 4378464680
                        },
                        ...
                    ],
                    "skipped": 0
                },
            }
        ]
    }
}

上面的例子只保留了JSON Format数据的框架结构,原始数据大约在200k左右,包含全部线程信息以及全部二进制文件信息。

显而易见,JSON Format对于Server端处理非常友好,而Apple Format对于开发人员阅读非常友好。所以海神平台选择了两全其美的方案:

  • 海神客户端上报JSON Format数据,海神平台后端数据流转也保持对象结构
  • 交叉编译Apple Format工具用于支持下载文件格式化

1.2 同步上传遇到的问题

时效性是监控系统最靓的🏷

海神希望能够最快知晓用户设备上发生了什么,所以客户端目标始终是同步上传Crash日志。

iOS的thread和runloop是个好话题。当工程师熟练的创建NSURLConnection的时候,一定要感谢是runloop在背后默默的支持网络请求。

但是当应用崩溃时,包括main thread在内的所有线程runloop都会退出,所以无论是async还是sync的方式,NSURLConnection都无法发送网络请求。

当然,NSURLConnection已经是废弃的网络框架,现在提到OC网络框架时主要指NSURLSession(https://developer.apple.com/documentation/foundation/nsurlsession)。

NSURLSession是OC新一代网络框架,目标是取代NSURLConnection。NSURLSession由系统网络进程管理网络请求,实现统一的安全性、连接、带宽和能源等管理。不过糟糕的是NSURLSession文档非常少,导致很多具体的技术细节无从知晓。

不过,通过 控制台 连接iOS设备,可以看到NSURLSession的守护进程,并且能够看到该进程简单的状态信息。

nsurlsessiond   nsurlsessiond   Application  entered foreground    17:30:18.758723 +0800

NSURLSessionConfiguration是NSURLSession的配置类,通过方法:

-backgroundSessionConfigurationWithIdentifier:可以创建后台会话(Background Session)。

详见:https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration?language=objc

Use this method to initialize a configuration object suitable for transferring data files while the app runs in the background. A session configured with this object hands control of the transfers over to the system, which handles the transfers in a separate process. In iOS, this configuration makes it possible for transfers to continue even when the app itself is suspended or terminated.

Background Session能够保证应用终止后完成数据传输,似乎解决了崩溃后无法发送网络请求的问题。

Unlike data tasks, you can use upload tasks to upload content in the background.

NSURLSessionUploadTask是NSURLSession的上传任务(uploadTask)类,有两种方式创建uploadTask,在崩溃发生时同步上传。

详见:https://developer.apple.com/documentation/foundation/nsurlsessionuploadtask?language=objc

  • -uploadTaskWithStreamedRequest:
  • -uploadTaskWithRequest:fromData:

A URL request object that provides the URL, cache policy, request type, and so on. The body stream and body data in this request object are ignored.

但是实际测试发现,文档和代码有差别…上面的说明似乎没有生效。

于是我们很容易就有了下面的代码:

NSURL *URL = [NSURL URLWithString:url];
NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:URL];
requestM.HTTPMethod = @"POST";
[requestM setHTTPBody:body];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:NSUUID.UUID.UUIDString];
NSURLSession *sharedInstance = [NSURLSession sessionWithConfiguration:configuration];
NSURLSessionUploadTask *task = [sharedInstance uploadTaskWithStreamedRequest:requestM];
[task resume];

上面的代码在iOS 12.4上运行的非常良好。从控制台可以看到完整的进程间通信过程:

LJBaseCrashReporter_Example CFNetwork           background session setup will wait for reply: session  with identifier    17:18:50.724156 +0800
nsurlsessiond               nsurlsessiond       Creating session with identifier:  for bundle id:     17:18:50.726110 +0800
nsurlsessiond               nsurlsessiond       Client  is a SpringBoard application   17:18:50.726390 +0800
nsurlsessiond               nsurlsessiond       Session <>.<> using resource timeout: 604800.000000, request timeout: 60.000000 allowsCellularAccess: 1, allowsExpensiveAccess: 1 _sourceApplicationBundleIdentifier: (null), _sourceApplicationSecondaryIdentifier: (null)   17:18:50.740688 +0800
LJBaseCrashReporter_Example CFNetwork           background session setup reply received: session  with identifier     17:18:50.742934 +0800
nsurlsessiond               nsurlsessiond       Task . uploadTaskWithRequest:  fromFile: (null)   17:18:50.746283 +0800
nsurlsessiond               nsurlsessiond       Current discretionary status for  is non-discretionary 17:18:50.748662 +0800
nsurlsessiond               CFNetwork           Task . is for <>.<>.  17:18:50.748973 +0800
nsurlsessiond               nsurlsessiond       Task . enqueueing  17:18:50.749067 +0800
LJBaseCrashReporter_Example CFNetwork           Task . resuming, QOS(0x15) 17:18:50.749642 +0800
LJBaseCrashReporter_Example libnetwork.dylib    Create activity   17:18:50.749979 +0800
LJBaseCrashReporter_Example libnetwork.dylib    Activated     17:18:50.752752 +0800
LJBaseCrashReporter_Example CFNetwork           [Telemetry]: Activity  on Task . was not selected for reporting  17:18:50.753120 +0800

Anything Perfect! But 上面的例子在 系统兼容性 的测试上无法通过。

在较低版本的iOS系统上(实在是找不齐系统版本😂),已知会出现三种类型的异常:

类型一: iOS 9.x、iOS 10.x等系统在应用崩溃后创建Background Session导致进程卡死无法退出

这种情况发生时,如果手动退出应用的话,仍然可以通过Xcode获取Crash日志。忽略掉无关代码之后,导致进程卡死的调用栈如下:

0   libsystem_kernel.dylib          semaphore_wait_trap + 8
1   libdispatch.dylib               _dispatch_semaphore_wait_slow + 244
2   CFNetwork                       -[__NSURLBackgroundSession setupBackgroundSession] + 540
3   CFNetwork                       -[__NSURLBackgroundSession initWithConfiguration:delegate:delegateQueue:] + 412
4   CFNetwork                       +[NSURLSession sessionWithConfiguration:delegate:delegateQueue:] + 560

...

11  LJBaseCrashReporter_Example     handleExceptions + 1769296 (KSCrashMonitor_MachException.c:363)
12  libsystem_pthread.dylib         _pthread_body + 156
13  libsystem_pthread.dylib         _pthread_body + 0
14  libsystem_pthread.dylib         thread_start + 4

如果熟悉GCD的话,那么对于semaphore_wait_trap一定很熟悉,因为无论是dispatch_once还是dispatch_semphore内部都使用了陷阱模式来实现线程wait。

从堆栈可以确定,-[__NSURLBackgroundSession setupBackgroundSession]在等待一个“响应”,并根据这个响应退出陷阱模式,而这个“响应”永远都没有到来。

Google上并没有多少关于NSURLSession跨进程通信的说明,简书上有一篇Blog(https://www.jianshu.com/p/4b51c85c82b3)描述了一个socket通讯管道破裂导致崩溃的场景,允许我们大胆猜测NSURLSession大概也因为类似的原因导致永远无法从陷阱模式退出。

类型二: iOS 11.x等系统在Swift应用崩溃后创建Background Session导致进程卡死无法退出

虽然现象是相同的,但是背后的原因却并不相同。此类场景下导致进程卡死的调用栈如下:

0   libsystem_kernel.dylib          0x000000020bb6cf2c __psynch_mutexwait + 8
1   libsystem_pthread.dylib         0x000000020bbe8a84 _pthread_mutex_firstfit_lock_wait + 92
2   libsystem_pthread.dylib         0x000000020bbe89f4 _pthread_mutex_firstfit_lock_slow$VARIANT$mp + 272
3   libdyld.dylib                   0x000000020ba23760 dyldGlobalLockAcquire+ 14176 () + 20
4   dyld                            0x0000000107850a40 dlopen_internal + 296
5   libdyld.dylib                   0x000000020ba24908 dlopen + 176
6   CFNetwork                       0x000000020c64b9fc initMKBDeviceUnlockedSinceBoot+ 883196 () + 44
7   CFNetwork                       0x000000020c589eb4 -[__NSURLBackgroundSession setupBackgroundSession] + 76
8   CFNetwork                       0x000000020c5965c4 -[__NSURLBackgroundSession initWithConfiguration:delegate:delegateQueue:] + 492
9   CFNetwork                       0x000000020c57e314 +[NSURLSession sessionWithConfiguration:delegate:delegateQueue:] + 644
...

18  CoreFoundation                  0x000000020bfd05b8 __handleUncaughtException + 692
19  libobjc.A.dylib                 0x000000020b1aadf4 _objc_terminate+ 24052 () + 112
20  open_dev                        0x0000000104d92404 0x1048b0000 + 5121028
21  libc++abi.dylib                 0x000000020b19f838 std::__terminate(void (*)+ 55352 ()) + 16
22  libc++abi.dylib                 0x000000020b19f434 __cxa_rethrow + 144
23  libobjc.A.dylib                 0x000000020b1aabc8 objc_exception_rethrow + 44
24  CoreFoundation                  0x000000020bf5c11c CFRunLoopRunSpecific + 544
25  GraphicsServices                0x000000020e15c79c GSEventRunModal + 104
26  UIKitCore                       0x0000000238506978 UIApplicationMain + 212
27  open_dev                        0x0000000104935258 main + 545368
28  libdyld.dylib                   0x000000020ba218e0 start + 4

Swift依赖的网络框架和OC似乎并不相同,导致在崩溃时还需要调用libdyld.dylib来启动额外的库以支持Background Session。与上一节不同的是,这类卡死是因为pthread_mutex无法释放导致的。

类型三: iOS8.x Background Session无效

贝壳现在最低支持iOS 9,所以这里就不写了😝😝😝

1.3 海神的同步上传方案

由于存在以上各种问题,海神客户端放弃了基于OC Runtime的网络框架,使用Standard C实现同步网络请求。

站在巨人的肩膀上总是更轻松,海神对curl进行裁剪和移植,作为客户端同步上传的网络基础库。实现的代码如下:

curl_global_init(CURL_GLOBAL_ALL);

CURL *curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POST, 1);

//设置请求头
struct curl_slist *headers = curl_slist_append(NULL, "Content-Encoding:gzip");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

//设置请求体
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_size);

curl_easy_perform(curl);

2. 如何获取系统符号表

系统符号表和项目符号表类似,都是记录可执行文件的符号信息。通常我们将设备连接到Xcode后,Xcode会自动从设备中导出系统符号表,也就是显示“Preparing debugger support for Device”的过程。在Mac上iOS系统符号表默认的存储路径:

~/Library/Developer/Xcode/iOS DeviceSupport

海神平台创建之初,目标是支持iOS 8.0开始所有发布版本的日志解析。我们都知道:

Crash原始日志 + 项目符号表 + 系统符号表 = Crash解析日志

项目符号表可以通过持续集成平台获取到,Crash原始日志在上一节中也已实现,剩下的工作就是找到系统符号表。

2.1 iOS System Symbols项目

系统符号表和iOS系统是一一对应的。标准的系统版本号包括发布版本和Build版本,同一个版本号又包含多个架构类型。以iOS 12.1为例:

Build Version Arch
16B92 arm64、arm64e
16B93 arm64、arm64e

基本上所有公司都没有收集系统符号表的传统,贝壳也是😞。由于测试机数量有限,而iOS系统又持续升级,再加上很多“清理Mac存储空间”的坑爹文章,导致海神平台缺失不少版本和架构的符号表。

iOS-System-Symbols是Zuikyo发起的系统符号表收集项目(具体见GitHub),基本集齐了7.0-12.x的所有版本。项目还在持续更新,感谢Zuikyo的工作,海神平台当前就是使用这套系统符号表作为基础。

2.2 系统符号表的及时性

iOS新版本通常会有新功能和优化体验,而且随着更新习惯养成和网速的不断提升,更新成本也越来越低。Apple官方数据表明,iOS用户越来越愿意升级新版本。

iOS-System-Symbols项目更新总是在新版本发布一段时间之后,完全依赖此项目会导致新版本系统产生Crash时,海神平台还没有该系统符号表。于是解析流程被中断,Crash数量和率都出现异常。

是时候开始自动监控iOS新版本并自动生成和部署系统符号表了!

为什么连接设备能够导出系统符号表?其实系统符号表和iOS固件是密不可分的,Apple没有提供下载系统符号表的方式,但是我们完全可以通过固件提取指定版本的符号表。

The iPhone Wiki(https://www.theiphonewiki.com/)由著名的黑客geohot创建,用于收集有关iOS操作系统(及其变体,tvOS和watchOS)以及运行该软件的设备(iPhone,iPod touch,iPad,Apple TV和Apple Watch)的所有公共知识。此项目中Firmware(https://www.theiphonewiki.com/wiki/Firmware)类别包括了几乎所有固件,并且具有非常好的时效性。

The iPhone Wiki一定要多逛一逛🏷

仍然以iOS 12.1为例下载iPhone 5s和iPhone XS Max的固件。之所以下载两个固件文件,是因为iOS 12.1支持arm64和arm64e两种架构。当iOS系统支持多种架构类型时,需要下载全部架构的固件分别提取系统符号表,然后合并成生成多架构系统符号表。下载好的固件:

  • iPhone_4.0_64bit_12.1_16B92_Restore.ipsw
  • iPhone11,4,iPhone11,6_12.1_16B92_Restore.ipsw

虽然文件后缀为“.ipsw”,但其本质上就是一个压缩文件,可以通过修改后缀为“.zip”,然后直接解压缩得到文件目录:

海神平台iOS端崩溃日志解析踩坑之旅 1

多个.dmg文件中,占用空间最大的包含需要的系统库。iOS 10之前的版本此文件是经过加密的,所以诞生了很多VFDecrypt工具,但是之后的版本不再加密。加载映像以后得到文件目录:

海神平台iOS端崩溃日志解析踩坑之旅 2

打开/System/Library/Caches/com.apple.dyld/目录:

海神平台iOS端崩溃日志解析踩坑之旅 3

dyld_shared_cache_xxx文件是所有系统库的压缩包,其中xxx表示具体架构。想要打开cache文件需要dyld的解压支持。

dyld是MachO文件的开源加载库,在iOS中主要作用于应用main函数之前的加载流程。dyld在Github上的仓库比较老,我个人更喜欢在Apple Open Source下载iOS和OSX的开源代码。 强烈建议

使用551.x版本(https://opensource.apple.com/tarballs/dyld/dyld-551.4.tar.gz),尽管最新版本是655.x,但其需要的编译条件比较多,想要编译成功略费劲。解压得到文件目录:

海神平台iOS端崩溃日志解析踩坑之旅 4

在launch-cache目录下,能够找到dsc_extractor.cpp、dsc_iterator.cpp,这两个文件就是解压cache的工具库。打开dsc_extractor.cpp,将“#if 0”修改为“#if 1”,然后使用clang就可以将源码编译为二进制文件,命令如下:

clang dsc_extractor.cpp dsc_iterator.cpp -lc++ -o dsc_extractor

得到解压工具后,就可以开始解压dyld_shared_cache_xxx文件了。dsc_extractor接收两个参数,第一个是待解压文件地址,第二个是解压目标目录。命令如下:

./dsc_extractor dyld_shared_cache_arm64 arm64
./dsc_extractor dyld_shared_cache_arm64e arm64e

输出:

...
dyld_shared_cache_extract_dylibs_progress() => 0

得到两种结构的符号表之后,就要开始合并。合并是通过lipo实现的,lipo是一个OS X中处理通用可执行文件的工具,支持查看架构,拆分、合并文件。命令如下:

lipo -create arm64/xx/xxx arm64e/xx/xxx -output '12.1 (16B92)/xx/xxx'

xx/xxx表示xx目录下的xxx文件。遍历文件目录,各种语言提供的工具比较多,这里就不再赘述。如果想在OS X上查看文件夹下完整目录,可以试试tree。命令如下:

brew install tree
tree xxx

合并好的文件可以通过lipo查看架构,命令如下:

lipo -info '12.1 (16B92)/xx/xxx'

输出:

Architectures in the fat file: /12.1 (16B92)/xx/xxx are: arm64 arm64e

最后一步需要调整目录结构。可以查看Xcode导出的系统符号表的结构:

海神平台iOS端崩溃日志解析踩坑之旅 5

在System和usr的外层增加目录Symbols,系统符号表就做好了。

海神平台当前是通过Python定时脚本抓取和分析固件信息。发现新版本系统后,自动下载、解压、合并和上传系统符号表。

3. symbolicatecrash解析Crash日志的问题

3.1 symbolicatecrash是什么

symbolicatecrash是Xcode提供的傻瓜式解析脚本,使用Perl语言开发,用于将Apple Format原始日志中的地址解析成可读符号。

新版本的Xcode,symbolicatecrash文件位置为:

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

如果不能确定symbolicatecrash的位置,可以使用以下命令查找:

find /Applications/Xcode.app -name symbolicatecrash -type f

输出:

/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

一般我们选择使用SharedFrameworks相关目录下的脚本。symbolicatecrash的运行依赖Xcode工具链,所以需要在Shell环境提供DEVELOPER_DIR:

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer

友情提示一下:做饭需要米,解析Crash需要符号表。所以请先准备好系统符号表和项目符号表。

symbolicatecrash要求系统符号表放在指定位置:

/Users/LiXiangYu/Library/Developer/Xcode/iOS DeviceSupport

项目符号表放在什么位置就比较随意了,不过 建议 和Crash日志放在同一目录下。

现在可以开始解析Crash日志了。symbolicatecrash只需要原始Crash日志的路径即可实现解析,不过解析内容会被打印在控制台,通过重定向可以很方便的输出到文件中。

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash ~/Desktop/origin.crash > ~/Desktop/symboled.crash

如果你和你的Shell是 好朋友 的话,上面的命令会短很多:

symbolicatecrash ~/Desktop/origin.crash > ~/Desktop/symboled.crash

3.2 symbolicatecrash的错误

行至上一小节,简直完美对不对?但理想是丰满的,现实是骨感的。解析的时候,很可能会遇到这种错误:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/size: /Users/LiXiangYu/Library/Developer/Xcode/iOS DeviceSupport/*/Symbols/System/Library/Frameworks/*.framework/* (for architecture *)  truncated or malformed object (dataoff field of LC_SEGMENT_SPLIT_INFO command 12 extends past the end of the file)

还有这种错误:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/objdump: /Users/LiXiangYu/Library/Developer/Xcode/iOS DeviceSupport/*/Symbols/System/Library/Frameworks/*.framework/*: truncated or malformed object (dataoff field of LC_SEGMENT_SPLIT_INFO command 14 extends past the end of the file)

And More…

系统符号表有问题?!祖传的系统符号表怎么说不行就不行了呢?明明测试机连上Xcode用的好好的呢?

虽然错误信息非常多,但是所有的错误信息都包含了同一个关键字:LC_SEGMENT_SPLIT_INFO。如果熟悉MachO文件格式,一定会想到这是可执行文件中的一个加载命令(Load Command)。

通过MachOView分别打开有问题的符号表和相同版本没有问题的符号表,查看MachO的文件结构。

有问题时:

海神平台iOS端崩溃日志解析踩坑之旅 6

无问题时:

海神平台iOS端崩溃日志解析踩坑之旅 7

可以看到有问题的符号表中确实多一个LC_SEGMENT_SPLIT_INFO加载命令:

海神平台iOS端崩溃日志解析踩坑之旅 8

我们再查看MachO的头文件信息:

海神平台iOS端崩溃日志解析踩坑之旅 9

会发现LC_SEGMENT_SPLIT_INFO中指定的Data Offset远大于Fat Header中的Offset + Size。如果在MachoView中尝试打开:

海神平台iOS端崩溃日志解析踩坑之旅 10

MachoView会直接崩溃。

大概清楚错误的原因了。既然信息是symbolicatecrash打印出来的,那就再看下它的实现,验证下我们的猜测。Perl是不可能学的,这辈子都不可能学的。幸好这并不影响接下来的分析。

symbolicatecrash大概有1.5k行,虽然文件比较长,但是逻辑很清晰,可以很清楚的看到symbolicatecrash如何一步一步实现Crash日志解析。这里不再详细描述解析的过程,但有几个关键点需要列举:

  • 无需指定项目符号表的位置,因为内部使用 mdfind 进行全局文件查找,需要校验符号表
  • Apple Format文件通过字符串操作,分解为各种元数据用于查询
  • 聚合相同堆栈,优化解析速度
  • 核心是使用 atos 进行地址到符号的解析

fetch_symbolled_binaries 函数中,我们发现了很有意思的东西:

#  13T5280f: My crash logs aren't symbolicating
# System libraries were not being symbolicated because /usr/bin/size is always failing.
# That's  /usr/bin/size doesn't like LC_SEGMENT_SPLIT_INFO command 12
#
# Until that's fixed, just hope for the best and assume no sliding. I've been informed that since
# this scripts always deals with post-mortem crash files instead of running processes, sliding shouldn't
# happen in practice. Nevertheless, we should probably add this sanity check back in once we 21604022
# gets resolved.

$real_base = $$lib{base}

# call to size failed.  Don't use this image in symbolication; don't die
# delete $$images{$b};
#print STDERR "Error in symbol file for $symboln"; # and log it
# next;

rdar://problem/21604022 已经不可访问,所以历史原因亦无从追溯了。但是从备注中基本可以确定,这个是Apple留下的一个bug…

后续Google也没有找到更多有价值的内容,问题似乎无解了,但是这没有阻止海神的脚步。通过前面分析symbolicatecrash的实现,对于Crash的解析我们还是得到了一些启发。

3.3 海神的解析方案

3.3.1 更快的查找速度

作为平台后端,每次通过mdfind来查找文件是不能接受的。海神平台后端直接存储了系统符号表文件路径,通过符号表UUID查找路径实现直接命中符号文件。

校验符号表操作,对于symbolicatecrash这种独立工具来讲是合理的,保证每次解析流程的准确性。但是对于海神平台就不必要了,因为符号表会稳定存储在目标机器上,不会出现“变质”情况,这样又节约一些时间。

3.3.2 聚合JSON格式数据

其实我并不理解symbolicatecrash为什么使用字符串操作这种Stupid的方式来解析Crash日志,也许又是因为什么“历史原因”吧。┓( ´∀` )┏

和Perl语言调用系统工具相同,Java调用系统工具也需要进行跨进程通信。由于创建进程会消耗大量系统资源,所以减少解析次数对提高解析速度非常有效。

在上面1.1小节【KSCrash与上下文】中已经讨论过,海神客户端与Server端通过JSON格式传输数据。使用JSON格式主要是便于Java直接将数据映射为对象,而对象方便进行相同库不同符号地址的聚合。

下面是一个栈帧的组成结构:

{
    "object_name": "LJBaseCrashReporter_Example",
    "object_addr": 4377903104,
    "symbol_addr": 4378464516,
    "instruction_addr": 4378464680
}
  • object_name:

    可执行文件名称

  • object_addr:

    可执行文件加载地址

  • symbol_addr:

    函数地址

  • instruction_addr:

    调用地址

3.3.3 atos解析

可以发现,symbolicatecrash的大部分工作已经被优化掉或者由Java层承担,核心的解析操作就可以通过调用atos命令来实现了。

atos是OS X系统的地址符号化工具,它接收可执行文件路径、可执行文件加载地址和符号地址,需要特别注意,地址值需要转换为十六进制。命令如下:

atos -o 符号文件地址 -arch 架构 -l 可执行文件加载地址 符号地址1 符号地址2 ...

输出:

符号信息1
符号信息2
...

以3.3.2小节【聚合JSON格式数据】中的样例栈帧为例:

atos -o LJBaseCrashReporter_Example.app.dSYM/Contents/Resources/DWARF/LJBaseCrashReporter_Example -arch arm64 -l 0x104F18000 0x104FA11A8

输出:

-[LJCrashDebugMachsController tableView:didSelectRowAtIndexPath:] (in LJBaseCrashReporter_Example) (LJCrashDebugMachsController.m:135)

获取的输出中包括符号、库、文件和行号。系统库和商业平台SDK经常会阉割符号表,导致atos输出信息不完整,拆分时需要兼容。

4. 总结

以上问题解决后,海神平台从获取到解析Crash日志的流程就基本完成了。

海神平台目前作为贝壳移动端基础设施之一,已经融进了贝壳的整个监控体系。随着公司业务的快速发展和新业务的出现,稳定性建设方向也出现了新的监控诉求。为此,海神也在通过不断的迭代来覆盖这些新场景,为业务的稳定发展提供有力保障。

本文转载自公众号贝壳产品技术(ID:beikeTC)。

原文链接

https://mp.weixin.qq.com/s/8rQE_bnSsswd-wTNcOevFg