iOS dSYM与崩溃日志分析

一. Macho格式解析

准备test.m文件,内容如下

void test() {
    
}
void test_1() {
    
}
int global = 10;
int main(){
    global = 21;
    global = 20;
    test();
    test_1();
    return 0;
}
// 把test.m编译成可执行文件
$ clang test.m -o test
// 查看test可执行文件代码段,可以看到代码段中有 机器码 汇编指令
$ objdump --macho -d test
// 查看test文件所有二进制内容
$ objdump -macho -s test
// 把test.m编译成.o文件
$ clang -c test.m -o test.o
// 查看test.o文件代码段
$ objdump --macho -d test.o
// 查看test.o重定位符号表
$ objdump --macho --reloc test.o
image.png
image.png
e8 后面 00 00 00 00 表示近地址相对位移指令,这个地址加上下面 48:偏移量就是 _test地址,
这个时候_test地址是个虚拟地址,所以00 00 00 00要把真实偏移量填进去,就要把_test放入重定位符号表里
告诉链接器00 00 00 00地址需要重定位
// 查看e指令作用
(lldb) help e
// 查看b8 二进制形式 -f 表示format b表示二进制 x表示16进制 d表示十进制
(lldb) e -f b -- 0xb8
(int) $0 = 0b00000000000000000000000010111000

二. 崩溃日志与dSYM

dSYM文件就是按DWARF格式保存调试信息的文件
DWARF是一种被众多编译器和调试器使用的用于支持源代码级别 调试的调试文件格式
调试信息生成dSYM文件过程

  • 读取debug map
  • 从.o文件中加载__DWARF
  • 重新定位所有地址
  • 最后将全部的DWARF打包成dSYM Bundle
// 把test.m编译成.o文件,并且生成调试信息  -g生成调试信息
$ clang -g -c test.m -o test.o
// 查看.o文件 mach header,其中__DWARF下就是调试信息
$ objdump --macho --private-headers test.o
Section
  sectname __debug_str
   segname __DWARF
      addr 0x0000000000000064
      size 0x0000000000000118
    offset 1372
     align 2^0 (1)
    reloff 0
    nreloc 0
      type S_REGULAR
attributes DEBUG
// 把test.m文件生成可执行文件,并且生成调试信息
$ clang -g test.m -o test
// 查看可执行文件 mach header,发现找不到__DWARF内容,因为 调试信息已经放入了符号表
$ objdump --macho --private-headers test
// 查看符号表,下面列出来的就是调试符号
$ nm -pa test
0000000000000000 - 00 0000    SO /Users/wangning/Documents/资料/2:24/第十节、MachO与lldb/上课代码/1-mach-o分析/
0000000000000000 - 00 0000    SO test.m
00000000603b3ffe - 03 0001   OSO /var/folders/d7/5qn4fnqn0p197t4lkw1bw1p40000gn/T/test-6777ea.o
0000000100003f60 - 01 0000 BNSYM
0000000100003f60 - 01 0000   FUN _test
0000000000000010 - 00 0000   FUN
0000000000000010 - 01 0000 ENSYM
0000000100003f70 - 01 0000 BNSYM
0000000100003f70 - 01 0000   FUN _test_1
0000000000000010 - 00 0000   FUN
0000000000000010 - 01 0000 ENSYM
0000000100003f80 - 01 0000 BNSYM
0000000100003f80 - 01 0000   FUN _main
0000000000000035 - 00 0000   FUN
0000000000000035 - 01 0000 ENSYM
0000000000000000 - 00 0000  GSYM _global
0000000000000000 - 01 0000    SO
// 把test.m文件生成可执行文件,并且生成test.dSYM文件
$ clang -g1 test.m -o test
// 查看dSYM文件内容
$ dwarfdump test.dSYM

创建工程TestInject,ViewController.m文件内容如下

#import "ViewController.h"

@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self test_dwarf];
    });
}

- (void)test_dwarf {
    NSArray *array = @[];
    array[1];
}
@end

运行崩溃,数组越界2021-02-28 15:44:53.003369+0800 TestInject[3353:197728] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 1 beyond bounds for empty NSArray'

Cmd + 空格键 ,输入 控制台 ,打开控制台 可以看到崩溃信息如下
image.png
Build Settings 中将Deployment Postprocessing 设置为Yes,就可以将符号剥离,此时再 Cmd + 空格键 ,输入 控制台 ,打开控制台 可以看到崩溃信息如下
image.png

通过上面两个图对比可以发现,现在变成了地址,为了方便我们查看错误信息,现在该怎么把地址还原成符号?
此时需要借助dSYM文件,上图显示的地址是偏移后的地址,现在需要解决的是把偏移后的地址改为偏移前的地址

// 查看控制台崩溃日志0x107cc0000就是偏移量
Binary Images:
       0x107cc0000 
// 这里的偏移后的地址
3   TestInject                          0x0000000107cc1e70 TestInject + 7792
// 计算偏移前地址   偏移前地址 = 偏移后地址 - 偏移量
(lldb)  e -f x -- 0x0000000107cc1e70 - 0x107cc0000
(long) $0 = 0x0000000000001e70
// 进入运行生成的工程目录Debug-iphonesimulator,就可以看到崩溃符号信息
$ dwarfdump --lookup 0x0000000000001e70 TestInject.app.dSYM

小结
dSYM文件内保存的是真实的虚拟内存地址
我们在运行时调试到的地址实际上是 调试地址 = 虚拟地址 + ASLR
.crash文件可以通过.dSYM文件进行崩溃信息还原,正是通过计算得到真实虚拟内存地址,然后在dSYM文件中找到恢复符号的信息

三. ASLR与dSYM

3.1 根据真正的内存地址排查崩溃符号
工程TestInject,ViewController.m文件内容做如下修改:

#import "ViewController.h"
#import <mach-o/dyld.h>
#import <mach-o/getsect.h>
#import <objc/runtime.h>

@interface ViewController ()
@end

@implementation ViewController
// 获取ASLR
uintptr_t get_slide_address(void) {
    uintptr_t vmaddr_slide = 0;
    // 使用的所有的二进制文件 = ipa + 动态库
    // ASLR 根据Macho二进制文件或者image 做偏移
    for (uint32_t i = 0; i < _dyld_image_count(); i++) {
        // 遍历的是那个image名称
        const char *image_name = (char *)_dyld_get_image_name(i);
        const struct mach_header *header = _dyld_get_image_header(i);
        if (header->filetype == MH_EXECUTE) {
            vmaddr_slide = _dyld_get_image_vmaddr_slide(i);
        }
        NSString *str = [NSString stringWithUTF8String:image_name];
       
        if ([str containsString:@"TestInject"]) {
                   
              NSLog(@"Image name %s at address 0x%llx and ASLR slide 0x%lx.\n", image_name, (mach_vm_address_t)header, vmaddr_slide);
                   break;
          }
    }
    
    // ASLR返回出去
    return (uintptr_t)vmaddr_slide;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self getMethodVMA];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self test_dwarf];
    });
    NSLog(@"123");
}

- (void)test_dwarf {
    NSArray *array = @[];
    array[1];
}

- (void)getMethodVMA {
    // 运行中的地址(偏移)
    IMP imp = (IMP)class_getMethodImplementation(self.class, @selector(test_dwarf));
    unsigned long imppos = (unsigned long)imp;
    unsigned long slide =  get_slide_address();
    // 运行中的地址(偏移) - ASLR = 真正的虚拟内存地址
    unsigned long addr = imppos - slide;
}
@end

Build Settings 中将Debug Information Format 设置为 DWARF with dSYM File

// 运行程序获取运行中的地址
image.png
$ dwarfdump --lookup 0x0000000100001d10 TestInject.app.dSYM/
TestInject.app.dSYM/Contents/Resources/DWARF/TestInject:    file format Mach-O 64-bit x86-64
0x0004a51c: Compile Unit: length = 0x0000037d version = 0x0004 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x0004a89d)

0x0004a527: DW_TAG_compile_unit
              DW_AT_producer    ("Apple clang version 12.0.0 (clang-1200.0.32.28)")
              DW_AT_language    (DW_LANG_ObjC)
              DW_AT_name    ("/Users/wangning/Documents/\350\265\204\346\226\231/2:24/\347\254\254\345\215\201\350\212\202\343\200\201MachO\344\270\216lldb/\344\270\212\350\257\276\344\273\243\347\240\201/3-ASLR\344\270\216dSYM/TestInject/TestInject/ViewController.m")
              DW_AT_LLVM_sysroot    ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk")
              DW_AT_APPLE_sdk   ("iPhoneSimulator14.3.sdk")
              DW_AT_stmt_list   (0x0000b267)
              DW_AT_comp_dir    ("/Users/wangning/Documents/\350\265\204\346\226\231/2:24/\347\254\254\345\215\201\350\212\202\343\200\201MachO\344\270\216lldb/\344\270\212\350\257\276\344\273\243\347\240\201/3-ASLR\344\270\216dSYM/TestInject")
              DW_AT_APPLE_major_runtime_vers    (0x02)
              DW_AT_low_pc  (0x0000000100001a30)
              DW_AT_high_pc (0x0000000100001dd2)

0x0004a6f7:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000100001d10)
                DW_AT_high_pc   (0x0000000100001d77)
                DW_AT_frame_base    (DW_OP_reg6 RBP)
                DW_AT_object_pointer    (0x0004a711)
                DW_AT_name  ("-[ViewController test_dwarf]")
                DW_AT_decl_file ("/Users/wangning/Documents/资料/2:24/第十节、MachO与lldb/上课代码/3-ASLR与dSYM/TestInject/TestInject/ViewController.m")
                DW_AT_decl_line (55)
                DW_AT_prototyped    (true)
Line info: file 'ViewController.m', line 55, column 0, start line 55
// 可以看到具体崩溃文件,以及方法名

3.2 引入framework库打断点跳入库源码
创建TestLibrary工程,使用cocoapods引用TestFramework库

// TestFramework库中只有一个类TestExample
// TestExample.h内容
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TestExample : NSObject
- (void)lg_test:(_Nullable id)e;
@end
NS_ASSUME_NONNULL_END

//// TestExample.m内容
#import "TestExample.h"
#import <UIKit/UIKit.h>
@interface TestExample()
@end
@implementation TestExample
// lldb 使用
// flutter 引擎调试
- (void)lg_test:(id)e {
    NSLog(@"lg_test--");
}
@end

// podspec文件内容做了以下判断
if ENV['Source'] {
  // 引用TestFramework库源码
} else {
 // 引用TestFramework库
}

// 终端执行命令
$ pod install  //引入TestFramework库
$ Env=Source pod install //引入TestFramework库源码
$ Source=1 pod install //引入TestFramework库源码

// 理论上TestFramework库保存完整的调试信息,添加断点是可以进入lg_test方法内
// 接下来验证的是给引用的库添加断点,看能否跳入源码
 TestLibrary工程中viewController.m中引用类TestExample,引用地方打断点,看能不能跳到方法lg_test源码中?
(lldb) br set -r lg_test(.*)  //断点打在TestFramework库方法lg_test中

小结:
提供sdk库的时候,保存完整的调试信息,这样就可以不用提供源码,也能进行调试

四. dyld与插入动态库

如何调试dyld?

  • 如果想调试dyld源代码,需要准备带调试信息的dyld/libdyld.dylib/ libclosured.dylib,与系统做替换,⻛险较大。
  • lldb保留了 一个库列表,避免在按名称设置断点时出现问题,而dyld与libdyld.dylib就在该列表上。
    有两种方式在可以强制在dyld上设置断点:
  1. br set -n dyldbootstrap::start -s dyld // -s 指定在哪个二进制文件中设置断点
  2. set set target.breakpoints-use-platform-avoid-list 0
// 使用第一节test.m文件
(lldb) file test
Current executable set to '/Users/wangning/Documents/资料/2:24/第十节、MachO与lldb/上课代码/1-mach-o分析/test' (x86_64).
// 断点设置失败
(lldb) b dyldbootstrap::start
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
// 使用下面方式断点设置成功
(lldb) br set -n dyldbootstrap::start -s dyld
Breakpoint 2: where = dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*), address = 0x0000000000001062

// 也可以使用这种方式设置断点
// 先禁掉白名单(只在这一次生效),再添加断点
(lldb) set set target.breakpoints-use-platform-avoid-list 0
(lldb) b dyldbootstrap::start
Breakpoint 3: where = dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*), address = 0x0000000000001062
第二种方式无需查看代码、二进制文件,而是通过dyld提供的环境变量来控
制dyld在运行过程中输出有用信息。
1. DYLD_PRINT_APIS:打印dyld内部几乎所有发生的调用;
2. DYLD_PRINT_LIBRARIES:打印在应用程序启动期间正在加载的所有动态库;
3. DYLD_PRINT_WARNINGS:打印dyld运行过程中的辅助信息;
4. DYLD_*_PATH:显示dyld搜索动态库的目录顺序;
5. DYLD_PRINT_ENV:显示dyld初始化的环境变量;
6. DYLD_PRINT_SEGMENTS:打印当前程序的segment信息;
7. DYLD_PRINT_STATISTICS:打印pre-main time;
8. DYLD_PRINT_INITIALIZERS:显示都有initialiser。

// 可以使用上述命令一一调试 =1 表示让命令生效
$ DYLD_PRINT_APIS=1 ./test
_dyld_register_func_for_add_image(0x7fff20368506)
_dyld_register_for_bulk_image_loads(0x7fff2009bcf3)
_dyld_is_memory_immutable(0x7fff200e5d3b, 36)

// lldb中查看dyld调试过程中的信息
$ lldb -file test
(lldb) target create "test"
Current executable set to '/Users/wangning/Documents/资料/2:24/第十节、MachO与lldb/上课代码/1-mach-o分析/test' (x86_64).
(lldb) b main  //main函数中设置断点
Breakpoint 1: where = test`main + 15 at test.m:10:12, address = 0x0000000100003f8f
(lldb) settings set target.env-vars DYLD_PRINT_APIS=YES
(lldb) r
Process 5251 launched: '/Users/wangning/Documents/资料/2:24/第十节、MachO与lldb/上课代码/1-mach-o分析/test' (x86_64)
_dyld_register_func_for_add_image(0x7fff20368506)
_dyld_register_for_bulk_image_loads(0x7fff2009bcf3)
_NSGetExecutablePath(...)
_dyld_is_memory_immutable(0x7fff200e5d3b, 36)
Process 5251 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100003f8f test`main at test.m:10:12
   7    }
   8    int global = 10;
   9    int main(){
-> 10       global = 21;
   11       global = 20;
   12       test();
   13       test_1();
Target 0: (test) stopped.

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

推荐阅读更多精彩内容

  • 了解和分析应用程序崩溃报告 当应用程序崩溃时,将创建崩溃报告,这对于理解导致崩溃的原因非常有用。本文档包含有关如何...
    x1sn0w阅读 2,769评论 0 0
  • iOS逆向学习【1~32】 【chechra1n、Cydia、sshopen、vim、Cycript、adv-cm...
    CYC666阅读 1,978评论 0 5
  • 转自http://www.raywenderlich.com/zh-hans/30818/ios应用崩溃日志揭秘 ...
    RunSnails阅读 4,433评论 2 22
  • iOS 项目打包成功后为 xcarchive 后缀的文件, 里面不但包含了 app 运行所需要的二进制文件和资源文...
    uniapp阅读 3,275评论 0 5
  • 前言 iOS分析定位崩溃问题有很多种方式,但是发布到AppStore的应用如果崩溃了,我们该怎么办呢?通常我们都会...
    xh_0129阅读 396评论 2 2