iOS开发中,解决Crash相信是开发者最为头疼的问题了,特别是对于已上线的应用,对其Crash的跟踪和修复显得尤其重要.

通常来讲iOS系统中Crash分为两种,未捕获的Objective-C异常和Mach异常

Objective-C异常

在OC层面(iOS库、第三方库出现错误抛出)的异常称为OC异常。
比如:

1
2
NSArray * array= @[@“s",@“x",@“m"];
[array objectAtIndex:4];

OC异常可以用try-catch抓住:

1
2
3
4
5
6
@try {
NSArray * array= @[@“s",@“x",@“m"];
[array objectAtIndex:4];
} @catch (NSException *exception) {
NSLog(@"%@",exception);
}

使用此方法可以抓到当前抛出的异常并阻止程序崩溃,输出如下:

1
-[__NSArrayI objectAtIndex:]: index 4 beyond bounds [0 .. 2]

常见的OC异常

以下内容来自文章《iOS开发质量的那些事》

iOS开发中常见的异常包括以下几种:

1
2
3
4
5
NSInvalidArgumentException
NSRangeException
NSGenericException
NSInternalInconsistencyException
NSFileHandleOperationException

NSInvalidArgumentException

非法参数异常(NSInvalidArgumentException)是 Objective - C 代码最常出现的错误,所以平时在写代码的时候,需要多加注意,加强对参数的检查,避免传入非法参数导致异常,其中尤以nil参数为甚。

1. 集合数据的参数传递

比如NSMutableArray, NSMutableDictionary的数据操作
(1) NSDictionary不能删除nil的key
(2) NSDictionary不能添加nil的对象
(3) 不能插入nil的对象
(4) 其他一些nil参数

1
2
3
4
NSString *key = nil;
NSString *value = @"Hello";
NSMutableDictionary *mDic = [[NSMutableDictionary alloc] init];
[mDic setObject:value forKey:key];

运行后控制台输出日志:

1
2
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '*** -[__NSDictionaryM setObject:forKey:]: key cannot be nil'

2. 其他一些API的使用

APP一般都会有网络操作,免不了使用网络相关接口,比如NSURL的初始化,不能传入nil的http地址:

3. 未实现的方法

(1) .h文件里函数名,却忘了修改.m文件里对应的函数名
(2) 使用第三方库时,没有添加”-ObjC” flag
(3) MRC时,大部分情况下是因为对象被提前release了,在你心里不希望他release的情况下,指针还在,对象已经不在了。

NSRangeException

越界异常(NSRangeException)也是比较常出现的异常,有如下几种类型:

1. 数组最大下标处理错误

比如数组长度count, index的下标范围[0, count -1], 在开发时,可能index的最大值超过数组的范围;

2. 下标的值是其他变量赋值

这样会有很大的不确定性, 可能是一个很大的整数值

3. 使用空数组

如果一个数组刚刚初始化,还是空的,就对它进行相关操作

1
2
3
NSArray *array = @[@0, @1, @2];
NSUInteger index = 3;
NSNumber *value = [array objectAtIndex:index];

运行后控制台输出日志:

1
2
*** Terminating app due to uncaught exception 'NSRangeException', 
reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]'

所以,为了避免NSRangeException的发生,必须对传入的index参数进行合法性检查,是否在集合数据的个数范围内。

NSGenericException

NSGenericException这个异常最容易出现在foreach操作中,在for in循环中如果修改所遍历的数组,无论你是add或remove,都会出错 “for in”,它的内部遍历使用了类似 Iterator进行迭代遍历,一旦元素变动,之前的元素全部被失效,所以在foreach的循环当中,最好不要去进行元素的修改动作,若需要修改,循环改为for遍历,由于内部机制不同,不会产生修改后结果失效的问题。

1
2
3
4
NSMutableArray *mArray = [NSMutableArray arrayWithArray:@[@0, @1, @2]];
for (NSNumber *num in mArray) {
[mArray addObject:@3];
}

运行后控制台输出日志:

1
2
*** Terminating app due to uncaught exception 'NSGenericException', 
reason: '*** Collection <__NSArrayM: 0x600000c08660> was mutated while being enumerated.'

NSInternalInconsistencyException

不一致导致出现的异常.
比如NSDictionary当做NSMutableDictionary来使用,从他们内部的机理来说,就会产生一些错误

1
2
NSMutableDictionary *info = method return to NSDictionary type;
[info setObject:@“sxm" forKey:@"name"];

比如xib界面使用或者约束设置不当

NSFileHandleOperationException

处理文件时的一些异常,最常见的还是存储空间不足的问题,比如应用频繁的保存文档,缓存资料或者处理比较大的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [cacheDir stringByAppendingPathComponent:@"1.txt"];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSString *str1 = @"Hello1";
NSData *data1 = [str1 dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:data1 attributes:nil];
}

NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
[fileHandle seekToEndOfFile];
NSString *str2 = @"Hello2";
NSData *data2 = [str2 dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle writeData:data2];
[fileHandle closeFile];

运行后控制台输出日志:

1
2
*** Terminating app due to uncaught exception 'NSFileHandleOperationException', 
reason: '*** -[NSConcreteFileHandle writeData:]: Bad file descriptor'

所以在文件处理里,需要考虑到手机存储空间的问题。

NSMallocException

这也是内存不足的问题,无法分配足够的内存空间

1
2
3
NSMutableData *mData = [[NSMutableData alloc] initWithCapacity:1];
NSUInteger len = 1844674407370955161;
[mData increaseLengthBy:len];

运行后控制台输出日志:

1
2
*** Terminating app due to uncaught exception 'NSMallocException', 
reason: 'Failed to grow buffer'

OC异常的抓取和分析

在debug环境下,OC异常导致崩溃时Xcode控制台会输出完整的异常信息,比如:

1
Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘this is reason description’

包括Exception的类型、原因和发生异常的完整堆栈。

这些信息一般来说都足够详细,足够我们轻易地找到异常的位置并进行修复。

非debug环境下,可以通过注册 NSUncaughtExceptionHandler 捕获异常信息。虽然无法阻止APP崩溃,但是可以获取异常信息并进行收集,下次启动APP时进行上报,方便开发者进行错误跟踪及修复,这就是常用Crash收集工具所做的事情。

1
2
3
4
5
6
7
8
9
void InstallUncaughtExceptionHandler(void) {
NSSetUncaughtExceptionHandler(&handleUncaughtException);
}

void handleUncaughtException(NSException *exception) {
NSString * crashInfo = [NSString stringWithFormat:@"Exception name:%@\nException reason:%@\nException stack:%@",[exception name], [exception reason], [exception callStackSymbols]];
//存储异常信息.

}

Mach Exception

Mach异常是指最底层的内核级异常。

最常见的Mach异常:

EXC_BAD_ACCESS (Bad Memory Access)

这种内存访问异常分为访问非法地址(SIGBUS信号)和访问了被回收掉的内存(SIGSEGV信号),实际开发中遇到的错误通常令人莫名其妙,往往需要大量时间来排查,非常头疼。

EXC_BAD_ACCESS

这种异常后面通常带有code来帮助我们判断到底是什么错误,比如EXC_I386_GPFLT指访问了一块已经不属于你的内存。

一些其他的Mach异常:

EXC_BAD_INSTRUCTION运行了非法的指令,往往是运行指令的参数不对(0或者nil的参数)
EXC_RESOURCE程序资源上限(cpu占用过高或者内存不足)。
EXC_GUARD一些C函数访问错误导致的异常。

0x00000020 奇怪异常集合,常见的是由于主线程阻塞看门狗杀死了APP.

Unix Signal Exception

从Mach异常最终会转化成Unix信号投递到出错的线程

1. OC异常并不是真正的异常,但是当一个OC异常被抛出到最外层还没被捕获,程序会强行发送SIGABRT信号中断程序。

2. Mach异常没有比较便利的捕获方式,既然它最终会转化成信号,我们也可以通过捕获信号,来捕获 Crash 事件。

iOS提供了signal方法来注册一个处理函数,在处理函数中,使用execinfo中的 backtrace_symbols取出汇编层程序的堆栈信息。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
+ (void)signalRegister {
InstallMySignalRegister(SIGABRT);
InstallMySignalRegister(SIGBUS);
InstallMySignalRegister(SIGFPE);
InstallMySignalRegister(SIGILL);
InstallMySignalRegister(SIGPIPE);
InstallMySignalRegister(SIGSEGV);
InstallMySignalRegister(SIGSYS);
InstallMySignalRegister(SIGTRAP);
}

static void InstallMySignalRegister(int signal) {
// Register Signal
struct sigaction action;
action.sa_sigaction = handleSignalException;
action.sa_flags = SA_NODEFER | SA_SIGINFO;
sigemptyset(&action.sa_mask);
sigaction(signal, &action, 0);
}

void handleSignalException(int signal) {
NSMutableString *mstr = [[NSMutableString alloc] init];
[mstr appendString:@"Signal Exception:\n"];
[mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]];
[mstr appendString:@"Call Stack:\n"];

// 这里过滤掉第一行日志
// 因为注册了信号崩溃回调方法,系统会来调用,将记录在调用堆栈上,因此此行日志需要过滤掉
for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) {
NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
[mstr appendString:[str stringByAppendingString:@"\n"]];
}

[mstr appendString:@"threadInfo:\n"];
[mstr appendString:[[NSThread currentThread] description]];

// 保存崩溃日志到沙盒cache目录
[XXXCrashReporter saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"];

}

测试过程中可以通过方法来模拟信号:

1
raise(SIGPIPE);

下面是一些常用信号代表的含义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(1)  SIGHUP
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
(2) SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
(3) SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
(6) SIGABRT
调用abort函数生成的信号。
(7) SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
(8) SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
(9) SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
(11) SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
(13) SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

除此之外, 如果有些系统触发的信号你不需要处理,你可以通过代码来忽略这些信号, 当发生这些信号异常时, 应用将不会crash.

1
signal(SIGPIPE, SIG_IGN);

Crash日志收集

Crash日志收集方式

1.苹果Crash收集服务

新版iTunes Connect已不能看到APP的crash日志,只能在XCode 中Window->Organizer->Crashes可以看到crash日志。
当程序运行Crash的时候,系统会把运行的最后时刻的运行信息记录下来,存储到一个文件中,也就是我们所说的Crash文件,但收集crash功能需要用户设置->隐私->诊断与用量->诊断与用量数据选择自动发送,并与开发者共享,由于不是所有用户都会把这个功能打开,所以并不能保证收集到所有的Crash信息,推荐指数三颗星。

2.自行实现Crash收集及上报框架

实现原理上面已详细描述,适合人手充足,技术储备足够的团队使用。

3.第三方crash收集服务

腾讯bugly、友盟等Crash收集服务比较完善,作为开发者省心省力,适合个人或者对隐私性要求不高的团队使用。

Crash日志收集的冲突

在我们自己研发 Crash 收集框架之前,最早肯定都会接入腾讯 Bugly、友盟等第三方日志框架来进行崩溃的收集和分析。如果多个 Crash 收集框架存在时,往往会存在冲突。
不管是对于 Signal 捕获还是 NSException 捕获都会存在 handler 覆盖的问题,正确的做法应该是先判断是否有前者已经注册了 handler,如果有则应该把这个 handler 保存下来,在自己处理完自己的 handler 之后,再把这个 handler 抛出去,供前面的注册者处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static SignalHandler previousABRTSignalHandler = NULL;
static SignalHandler previousBUSSignalHandler = NULL;
static SignalHandler previousFPESignalHandler = NULL;
static SignalHandler previousILLSignalHandler = NULL;
static SignalHandler previousPIPESignalHandler = NULL;
static SignalHandler previousSEGVSignalHandler = NULL;
static SignalHandler previousSYSSignalHandler = NULL;
static SignalHandler previousTRAPSignalHandler = NULL;


+ (void)backupOriginalHandler {
struct sigaction old_action_abrt;
sigaction(SIGABRT, NULL, &old_action_abrt);
if (old_action_abrt.sa_sigaction) {
previousABRTSignalHandler = old_action_abrt.sa_sigaction;
}

struct sigaction old_action_bus;
sigaction(SIGBUS, NULL, &old_action_bus);
if (old_action_bus.sa_sigaction) {
previousBUSSignalHandler = old_action_bus.sa_sigaction;
}

struct sigaction old_action_fpe;
sigaction(SIGFPE, NULL, &old_action_fpe);
if (old_action_fpe.sa_sigaction) {
previousFPESignalHandler = old_action_fpe.sa_sigaction;
}

struct sigaction old_action_ill;
sigaction(SIGILL, NULL, &old_action_ill);
if (old_action_ill.sa_sigaction) {
previousILLSignalHandler = old_action_ill.sa_sigaction;
}

struct sigaction old_action_pipe;
sigaction(SIGPIPE, NULL, &old_action_pipe);
if (old_action_pipe.sa_sigaction) {
previousPIPESignalHandler = old_action_pipe.sa_sigaction;
}

struct sigaction old_action_segv;
sigaction(SIGSEGV, NULL, &old_action_segv);
if (old_action_segv.sa_sigaction) {
previousSEGVSignalHandler = old_action_segv.sa_sigaction;
}

struct sigaction old_action_sys;
sigaction(SIGSYS, NULL, &old_action_sys);
if (old_action_sys.sa_sigaction) {
previousSYSSignalHandler = old_action_sys.sa_sigaction;
}

struct sigaction old_action_trap;
sigaction(SIGTRAP, NULL, &old_action_trap);
if (old_action_trap.sa_sigaction) {
previousTRAPSignalHandler = old_action_trap.sa_sigaction;
}
}

然后将自己的异常处理模块稍作修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void handleSignalException(int signal, siginfo_t* info, void* context) {
NSMutableString *mstr = [[NSMutableString alloc] init];
[mstr appendString:@"Signal Exception:\n"];
[mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]];
[mstr appendString:@"Call Stack:\n"];

// 这里过滤掉第一行日志
// 因为注册了信号崩溃回调方法,系统会来调用,将记录在调用堆栈上,因此此行日志需要过滤掉
for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) {
NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
[mstr appendString:[str stringByAppendingString:@"\n"]];
}

[mstr appendString:@"threadInfo:\n"];
[mstr appendString:[[NSThread currentThread] description]];

// 保存崩溃日志到沙盒cache目录
[XXXCrashReporter saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"];

MyClearSignalRigister();

// 调用之前崩溃的回调函数
previousSignalHandler(signal, info, context);
}

上面的是一个处理 Signal handler 冲突的大概代码思路,下面是 NSException handler 的处理思路,两者大同小异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler;

static void LDAPMUncaughtExceptionHandler(NSException *exception) {
// 获取堆栈,收集堆栈
// ......
// 处理前者注册的 handler
if (previousUncaughtExceptionHandler) {
previousUncaughtExceptionHandler(exception);
}
}

+ (void)installExceptionHandler {
previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&LDAPMUncaughtExceptionHandler);
}

  • 两种异常共存的问题
    由于我们既捕获了Objective-C异常,又捕获了Mach异常,那么当发生Objective-C异常的时候就会出现两份Crash日志。

一份是通过NSSetUncaughtExceptionHandler设置异常处理函数生成的日志,另一份是通过捕获Unix信号产生的日志。这两份日志中,通过Unix信号捕获的日志是无法定位问题的,因此我们只需要NSSetUncaughtExceptionHandler中异常处理函数生成的日志即可。

那该怎么做才能阻止生成捕获Unix信号的日志呢?在DoraemonKit中采取的方式是在Objective-C异常捕获到Crash之后,主动调用exit(0)或者kill(getpid(), SIGKILL)等方式让程序退出。

堆栈符号解析

分为两种情景讨论:

Crash收集上报的堆栈信息解析

这种信息一般还原度比较高,基本上都能给出崩溃的具体定位信息,一些需要解析堆栈符号的解决方法如下:

异常信息有三种类型:

1.已标记错误位置的:

1
test 0x000000010bfddd8c -[ViewController viewDidLoad] + 8588
  • 这种信息已经很明确了,不用解析

2.有模块地址的情况:

1
test 0x00000001018157dc 0x100064000 + 24844252

以上面为例子,从左到右依次是:
二进制库名(test),调用方法的地址(0x00000001018157dc),模块地址(0x100064000)+偏移地址(24844252)

3.无模块地址的情况:

1
test 0x00000001018157dc test + 24844252

解析堆栈信息
dSYM符号表获取,xcode->window->organizer->右键你的应用 show finder->右键.xcarchive 显示包内容->dSYMs->test.app.dYSM

然后使用atos命令来符号化某个特定模块加载地址

1
atos [-arch 架构名] [-o 符号表] [-l 模块地址] [方法地址]

使用终端,进到test.app.dYSM所在目录

一.如果是有模块地址的情况,运行:
1
atos -arch arm64 -o test.app.dSYM/Contents/Resources/DWARF/test -l 0x100064000 0x00000001018157dc
二.如果是无模块地址的情况
  • 1.先将偏移地址转为16进制:

    1
    24844252 = 0x17B17DC
  • 2.然后用方法的地址-偏移地址,得到的就是模块地址

    1
    0x00000001018157dc - 0x17B17DC = 0x100064000
  • 3.最后运行:

1
atos -arch arm64 -o test.app.dSYM/Contents/Resources/DWARF/test -l 0x100064000 0x00000001018157dc

苹果自带Crash日志上报的堆栈信息解析

有四种常见的方法:

  • symbolicatecrash
  • mac 下的 atos 工具
  • linux 下的 atos 的替代品 atosl
  • 通过 dSYM 文件提取地址和符号的对应关系,进行符号还原

以上方案都有对应的应用场景,对于线上的 Crash 堆栈符号还原,主要采用的还是后三种方案。atos 和 atosl 的使用方法很类似,以下是 atos 的一个示例。

1
2
3
4
atos -o MonitorExample 0x0000000100062ac4  ARM-64 -l 0x100058000

// 还原结果
-[GYRootViewController tableView:cellForRowAtIndexPath:] (in GYMonitorExample) (GYRootViewController.m:41)

但是 atos 是Mac上一个工具,需要使用 Mac 或者黑苹果来进行解析工作,如果由后台来做解析工作,往往需要一套基于 Linux 的解析方案,这个时候可以选择 atosl,但是这个库已经有多年没有更新了, atosl 好像不太支持 arm64 架构,所以我们放弃了该方案。

最终使用了第四个方案,提取 dSYM 的符号表,可以自己研发工具,也可以直接使用 bugly 和友盟的工具,下面是提取出来的符号表。第一列是起始内存地址,第二列是结束地址,第三列是对应的函数名、文件名以及行号。

1
2
3
4
5
6
7
8
9
10
a840    a854    -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:41
a854 a858 -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a858 a87c -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a87c a894 -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a894 a8a0 -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
aa3c aa80 -[GYFilePreviewViewController initWithFilePath:] GYRootViewController.m:21
aa80 aaa8 -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:23
aaa8 aab8 -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:23
aab8 aabc -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:24
aabc aac8 -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:24

因为程序每次启动基地址都会变化,所以上面提到的地址是相对偏移地址,在我们获取到崩溃堆栈地址后,可以根据堆栈中的偏移地址来与符号表中的地址来做匹配,进而找到堆栈所对应的函数符号。比如下面的第四行,偏移为 43072 转换为十六进制就是 a840,用 a840 去上面的符号表中找对应关系,会发现对应着

1
2
3
4
5
6
-[GYRootViewController tableView:cellForRowAtIndexPath:],基于这种方式,就可以将堆栈地址完全还原为函数符号啦。

0 libsystem_kernel.dylib 0x0000000186cfd314 0x186cde000 + 127764
1 Foundation 0x00000001887f5590 0x1886ec000 + 1086864
2 GYMonitorExample 0x00000001000da4ac 0x1000d0000 + 42156
3 GYMonitorExample 0x00000001000da840 0x1000d0000 + 43072

符号文件格式

OC中编译输出的dSYM文件是dwarf格式的, dwraf文件的格式说明