如何以正确的姿势解析iOS崩溃堆栈日志, 补充符号表内缺少的地址

这篇文章其实只讲述一个问题, 那就是我们捕获到crash日志之后 再到 送去解析, 在这中间到底还要做什么处理.

目前市面上流通的crash分析SDK有很多, 比如腾讯bugly, 网易云捕, 等等, 用起来也很简单: 引入到工程, 初始化, 设好回调, 就OK了. 然后当我们在后台看到上报的日志后, 只要上传一下符号表, 就可以轻松的解析出问题发生的代码了
随便贴个例子:

Application Specific Information:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'

Last Exception Backtrace:
0   CoreFoundation                      __exceptionPreprocess  +228
1   libobjc.A.dylib                     objc_exception_throw  +56
2   MyProject                                 ____ZN12_GLOBAL__N_118makeOptionalGetterIxEEP11objc_objectm_block_invoke /Users/dingjielu/Documents/Netease/avg-client-ios/Pods/Realm/include/RLMObject_Private.hpp:0 +0
3   MyProject                                 -[HomeViewController tableView:cellForRowAtIndexPath:] /路径/马赛克.m:300 +16
4   UIKitCore                           -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:]  +684
5   UIKitCore                           -[UITableView _createPreparedCellForGlobalRow:willDisplay:]  +80

我们可以很轻松的定位出如下结论: 问题就是出在 HomeViewController tableView:cellForRowAtIndexPath, 300行附近.

用别人的SDK确实很爽很香,

但是, 当我们有一些额外的需求的时候, 就不得不去手动上报了. (比如商业原因不能用竞品SDK, 自家的又垃圾, 又比如需要更精准的定位某几个业务场景的crash日志)

于是开发同学就接到了自己手动捕获并且解析 崩溃日志 的需求了.
然后就去查资料, 发现大部分资料都这么说: 捕获分为NSException捕获, Singal捕获, 我们要做的就是搜集调用堆栈, 然后进行符号化, 这句好似公式一样的话, 存在于各种文档, 资料内.
当我们按照前辈的经验(其实就是抄了一段捕获日志的代码), 兴致BoBo的接入捕获之后, 发现我们上报得到的日志都是这样的

crash_name: 信号量崩溃捕获
 
crash_reason: signal 6 was raised
 
0   MyProject                                 0x0000000104f4b1f4 MyProject + 2535924
1   MyProject                                 0x0000000104f4a1ac MyProject + 2531756
2   libsystem_platform.dylib            0x0000000182308b48 _sigtramp + 36
3   libsystem_pthread.dylib             0x000000018230e288 <redacted> + 376
4   libsystem_c.dylib                     0x00000001820dbd0c abort + 140
5   libc++abi.dylib                     0x00000001818772c8 __cxa_bad_cast + 0
6   libc++abi.dylib                     0x0000000181877470 <redacted> + 0
7   libobjc.A.dylib                     0x00000001818a08d4 <redacted> + 124
8   libc++abi.dylib                     0x000000018189137c <redacted> + 16
9   libc++abi.dylib                     0x0000000181890f78 __cxa_rethrow + 144
10  libobjc.A.dylib                     0x00000001818a07ac objc_exception_rethrow + 44
11  CoreFoundation                      0x00000001825ace18 CFRunLoopRunSpecific + 664
12  GraphicsServices                    0x0000000184592020 GSEventRunModal + 100
13  UIKit                               0x000000018c5cc758 UIApplicationMain + 236
14  MyProject                                 0x0000000104d2618c MyProject + 287116
15  libdyld.dylib                       0x000000018203dfc0 <redacted> + 4

这个时候, 你去解析, 就会发现, 根本解析不了, 为什么?
比如说我们用atos去解析, 解析出来会发现, 什么有用的信息都没产生

我们来看下atos的调用方式:

atos -o [dsym file path] -l [Load Address] -arch [arch type] [Stack Address]

参数解释:
dsym file path: 很好理解, 符号表地址
arch type: 架构类型 (armv7, arm64等)
Stack Address: 崩溃栈
Load Address: 崩溃对象的实际加载地址

我们再来看别人文章内贴出的教学崩溃日志例子. 一般是长这个样子:

Exception Type:  SIGTRAP
Exception Codes: #0 at 0x1fd758624
Crashed Thread:  0

Thread 0 Crashed:
0   CoreFoundation                      CFRelease  +124
1   MyProject                                 0x00000001028ad018 0x1025a4000 + 3182616
2   MyProject                                 0x00000001028accf4 0x1025a4000 + 3181812
3   MyProject                                 0x00000001028ac104 0x1025a4000 + 3178756
4   MyProject                                 0x00000001028acb68 0x1025a4000 + 3181416
5   MyProject                                 0x0000000102930d84 0x1025a4000 + 3722628
6   MyProject                                 0x00000001029306e0 0x1025a4000 + 3720928
7   MyProject                                 0x0000000102938c88 0x1025a4000 + 3755144
8   libdispatch.dylib                   _dispatch_call_block_and_release  +24
9   libdispatch.dylib                   _dispatch_client_callout  +16
10  libdispatch.dylib                   _dispatch_main_queue_callback_4CF$VARIANT$mp  +1068

我们随便拿出一行:

1   MyProject                                 0x00000001028ad018 0x1025a4000 + 3182616

这一行的参数是这样的
MyProject: 回溯对象
0x00000001028ad018: 崩溃栈
0x1025a4000: 崩溃对象的实际加载地址
+3182616 函数偏移值

而我们上面获取到的崩溃信息是这样的:

MyProject                                 0x0000000104f4b1f4 MyProject + 2535924

很明显, 第三列参数, 也就是崩溃对象的实际加载地址看不到, 只看到了这个对象的名字!

所以, 问题的本质就是, 我们捕获崩溃时候光去获取backtrace是不够的, 还需要获取崩溃对象的实际物理加载地址

而实质上, 这个参数需要我们去获取崩溃发生时候的 BinaryImage信息, 然后去找到对应的地址替换进去才可以

获取Binary Image中的关键信息后. 大概会得到这样的东西:

Binary Images:
       0x1025a4000 -        0x103f2ffff +MyProject arm64  <0171fc9962a932eeb17c78e8b2ab5191> /var/containers/Bundle/Application/D37F848C-3DB9-44D4-AC89-0069E1AD6CAA/MyProject.app/MyProject
       0x10464c000 -        0x104657fff  libobjc-trampolines.dylib arm64  <065bd8006d513c358dc14e2a8ff1ba31> /usr/lib/libobjc-trampolines.dylib
       0x1fc9d7000 -        0x1fc9d8fff  libSystem.B.dylib arm64  <8a05d5f48f0a376abe6bd1caf4fc8138> /usr/lib/libSystem.B.dylib
       0x1fc9d9000 -        0x1fca2efff  libc++.1.dylib arm64  <6a272068f00d37a984e331ba58e1c3c4> /usr/lib/libc++.1.dylib
       0x1fca2f000 -        0x1fca41fff  libc++abi.dylib arm64  <d4920e50788d3be6bccf8f550bc6d491> /usr/lib/libc++abi.dylib
       0x1fca42000 -        0x1fd1c9fff  libobjc.A.dylib arm64  <1167a03d9f853f34a96fd96818ad77b5> /usr/lib/libobjc.A.dylib
       0x1fd1ca000 -        0x1fd1cefff  libcache.dylib arm64  <bd876f73c3ff39459113c6fa56d15e3d> /usr/lib/system/libcache.dylib
       0x1fd1cf000 -        0x1fd1dafff  libcommonCrypto.dylib arm64  <b6d7765a17a93ff4a095ee57139d16c5> /usr/lib/system/libcommonCrypto.dylib
       0x1fd1db000 -        0x1fd1dffff  libcompiler_rt.dylib arm64  <644550d26c693e95affb4ce0b8c5c7a6> /usr/lib/system/libcompiler_rt.dylib

可以看到, 第一栏就是我们需要的 MyProject 实际的地址.
我们可以通过如下方法获取 Binary Image的必要信息:

+ (NSArray *)loadBinayImages {
    NSMutableArray *arr = [[NSMutableArray alloc] init];
    for (uint32_t i = 0; i < _dyld_image_count(); i++) {
        uint64_t vmbase = 0;
        uint64_t vmslide = 0;
        uint64_t vmsize = 0;
        
        uint64_t loadAddress = 0;
        uint64_t loadEndAddress = 0;
        NSString *imageName = @"";
        NSString *uuid;
        
        const struct mach_header *header = _dyld_get_image_header(i);
        const char *name = _dyld_get_image_name(i);
        vmslide = (i);
        imageName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
        BOOL is64bit = header->magic == MH_MAGIC_64 || header->magic == MH_CIGAM_64;
        uintptr_t cursor = (uintptr_t)header + (is64bit ? sizeof(struct mach_header_64) : sizeof(struct mach_header));
        struct load_command *loadCommand = NULL;
        for (uint32_t i = 0; i < header->ncmds; i++, cursor += loadCommand->cmdsize) {
            loadCommand = (struct load_command *)cursor;
            if(loadCommand->cmd == LC_SEGMENT) {
                const struct segment_command* segmentCommand = (struct segment_command*)loadCommand;
                if (strcmp(segmentCommand->segname, SEG_TEXT) == 0) {
                    vmsize = segmentCommand->vmsize;
                    vmbase = segmentCommand->vmaddr;
                }
            } else if(loadCommand->cmd == LC_SEGMENT_64) {
                const struct segment_command_64* segmentCommand = (struct segment_command_64*)loadCommand;
                 if (strcmp(segmentCommand->segname, SEG_TEXT) == 0) {
                    vmsize = segmentCommand->vmsize;
                    vmbase = (uintptr_t)(segmentCommand->vmaddr);
                }
            }
            else if (loadCommand->cmd == LC_UUID) {
                const struct uuid_command *uuidCommand = (const struct uuid_command *)loadCommand;
                NSString *uuidString = [[[NSUUID alloc] initWithUUIDBytes:uuidCommand->uuid] UUIDString];
                uuid = [[uuidString stringByReplacingOccurrencesOfString:@"-" withString:@""] lowercaseString];
            }
        }
        
        loadAddress = vmbase + vmslide;
        loadEndAddress = loadAddress + vmsize - 1;
        
        NSString *str = [NSString stringWithFormat:@"%llx - %llx %@ + %@",loadAddress, loadEndAddress, imageName, uuid];
        [arr addObject:str];
    }
    return arr;
}

到这里. 我们就拿到loadAddress 和 loadEndAddress了, 至于具体怎么解析, 之后再出文章解释吧.
参考文章:
iOS实现Crash捕获与堆栈符号化, ps:这篇文章写的确实比较详细

我是火球猫, 谢谢观看我的文章, 有问题的话就留言吧.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容