IOS个人开发业务方案iOS 开发工具

iOS 的崩溃捕获-堆栈符号化-崩溃分析

2018-09-14  本文已影响85人  midmirror

一、获取 Crash、dSYM 文件

获取到的 .ips 改后缀为 .crash 即可

二、Crash 符号化(Symbolicating crash logs)

symbols 和 Symbolicate

symbols 就是函数名或变量名。符号化的过程就是把 crash log 中的内存地址转化为相应的函数调用关系。

一般来说,debug 模式构建的 app 会把符号表存储在编译好的 binary 信息中,而 release 模式构建的app会把符号表存储在 dSYM 文件中以节省体积。

系统库符号化文件

每当 Xcode 连接一台从未在当前电脑调试过的 iOS 版本的设备时,都会花一段时间把手机的系统库符号化文件自动导入到 ~/Library/Developer/Xcode/iOS DeviceSupport,这个过程叫 Processing symbol files。每个系统版本的 symbols 文件约占 2GB,所以这个文件夹会占用不少磁盘空间。但是,最好将这些内容备份到外置硬盘,需要符号化的时候再重新拷贝回来,而不是使用清理工具清理掉。因为,系统符号化文件的获取没有那么容易。

系统库符号文件不是通用的,而是对应crash所在设备的系统版本和CPU型号的。获取系统符号化文件的两大方式就是通过真机,或者通过各版本 Xcode 附带,苹果官方没有提供任何下载方式。有技术员总结了搜集方式,并给出了 github 下载方式,可查看附录。

通过 Xcode 符号化

需要3个文件,放在同一目录下

操作过程:Xcode -> Devices and Simulators -> 选中设备 -> View Device Logs

然后把 .crash文件 拖到 Device Logs 或者选择下面的import导入.crash文件。这样你就可以看到crash的详细log了。

通过命令行工具 symbolicatecrash 符号化
# 找到 symbolicatecrash 工具并拷贝出来
find /Applications/Xcode.app -name symbolicatecrash -type f
# 会返回几个路径,拷贝其中一个
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

# 引入环境变量
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
# 符号解析
./symbolicatecrash appName.crash .dSYM文件路径 > appName.log
./symbolicatecrash appName.crash appName.app > appName.log
# 将符号化的 crash log 保存在 appName.log 中
./symbolicatecrash appName.crash appName.app > appName.log
通过命令行工具 atos 符号化

有多个 .app、.dSYM、.crash 的时候很好用。用于符号化单个地址(可使用脚本批量化)。

每一个可执行程序都有一个build UUID来唯一标识(每次 build 都不同)。Crash日志包含发生crash的这个应用(app)的 build UUID以及crash发生的时候,应用加载的所有库文件的[build UUID]。

# 获取 crash 文件的 UUID
grep "appName armv" *crash
# 或者
grep --after-context=2 "Binary Images:" *crash

# 获取 app 的 UUID
xcrun dwarfdump --uuid appName.app/appName
# 获取 dSYM 的 UUID
xcrun dwarfdump --uuid appName.dSYM

# 对比 app 和 crash 的 UUID 进行匹配

# 用 atos 命令来符号化某个特定模块加载地址 (3种方式都可以)
# 0x4000 是模块的加载地址(必须是DWARF文件地址,而不是dSYM地址,dSYM只是一个bundle)
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x4000 -arch armv7
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -arch armv7
xcrun atos -o appName.app/appName -arch armv7

# 另外,应用内 获取 UUID 的方法

#import <mach-o/ldsyms.h>
NSString *executableUUID() {
    const uint8_t *command = (const uint8_t *)(&_mh_execute_header + 1);
    for (uint32_t idx = 0; idx < _mh_execute_header.ncmds; ++idx) {
        if (((const struct load_command *)command)->cmd == LC_UUID) {
            command += sizeof(struct load_command);
            return [NSString stringWithFormat:@"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
                    command[0], command[1], command[2], command[3],
                    command[4], command[5],
                    command[6], command[7],
                    command[8], command[9],
                    command[10], command[11], command[12], command[13], command[14], command[15]];
        } else {
            command += ((const struct load_command *)command)->cmdsize;
        }
    }
    return nil;
}

# 通过 iTunes Connect 网站来下载 dSYM 的话,对下载下来的每个 dSYM 文件都执行一次
xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/[...]/BCSymbolMaps [UUID].dSYM

示例:

# 有两行未符号化的 crash log
* 3 appName 0x000f462a 0x4000 + 984618 
* 4 appName **0x00352aee** 0x4000 + 3468014

# 1. 执行
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x4000 -arch armv7
# 2. 然后输入 0x00352aee
# 3. 符号化结果:
-[UIScrollView(UITouch) touchesEnded:withEvent:] (in appName) (UIScrollView+UITouch.h:26)
注意:

三、Crash 文件结构

1. Process Information(进程信息)
Incident Idnetifier 崩溃报告的唯一标识符,不同的Crash
CrashReporter Key 设备的 id(不是 uuid)。通常同一个设备上同一版本的 app 发生Crash时,该值都是一样的。
Hardware Model 设备类型
Process 进程名称[进程 id],进程通常是 app 名字
Path 可执行程序的位置
Identifier com.companyName.appName
Version app 版本号
Code Type CPU 架构
Parent Process 父进程,iOS中App通常都是单进程的,一般父进程都是 launchd
2. Basic Information(基本信息)
Date/Time Crash发生的时间,可读的字符串
OS Version 系统版本(build 号)
Report Version Crash日志的格式,目前基本上都是104,不同的version里面包含的字段可能有不同
3. Exception(异常)
Exception Type 异常类型
Exception Subtype: 异常子类型
Crashed Thread 发生异常的线程号
Exception Information 额外诊断信息

从macOS Sierra, iOS 10, watchOS 3, 和 tvOS 10开始,额外诊断信息,包括:

  1. 应用的具体信息:在进程被终止前捕捉到的框架错误信息

  2. 内核信息:关于代码签名问题的细节

  3. Dyld (动态链接库)错误信息:被动态链接器提交的错误信息

# 一段因为找不到链接库而导致进程被终止的Crash Report的摘录
Dyld Error Message:

Dyld Message: Library not loaded: @rpath/MyCustomFramework.framework/MyCustomFramework

 Referenced from: /private/var/containers/Bundle/Application/CD9DB546-A449-41A4-A08B-87E57EE11354/TheElements.app/TheElements  
 Reason: no suitable image found.
 
 # 一段因为没能快速加载初始view controller而导致进程被终止的Crash Report的摘录
Application Specific Information:
com.example.apple-samplecode.TheElements failed to scene-create after 19.81s (launch took 0.19s of total time limit 20.00s)
Elapsed total CPU time (seconds): 7.690 (user 7.690, system 0.000), 19% CPU
Elapsed application CPU time (seconds): 0.697, 2% CPU
4. Thread Backtrace(线程回溯)

Crash 发生时的线程的调用栈,没有符号化前是内存地址。

5. Thread State(线程状态)

Crash 发生时的寄存器状态。在你读一个 Crash Report 的时候,了解线程状态并非必须,但是如果你想更好地了解crash的细节,这会起一些帮助,这需要一些处理器硬件只是和汇编知识的储备。

LLDB与汇编调试-提高你的调试效率

6. Binary Images(二进制映像)

Crash 发生时 app 可执行文件、加载的所有系统库和第三方库。

 # app 可执行文件 Elephant
 0x104e80000 -        0x107b2bfff +Elephant arm64  <38c058044caa34818a83d88981986fad> /var/containers/Bundle/Application/5694FC83-018E-46E7-B060-A008867D3C9D/Elephant.app/Elephant
 
 # WCDB 可执行文件。b512f6d343e73a0db1bcb499d2597c8a 是 WCDB 的 UUID
 # 符号化时 dsym 的 UUID 需要与之匹配才能符号化
 0x10b724000 -        0x10b86ffff  WCDB arm64  <b512f6d343e73a0db1bcb499d2597c8a> /private/var/containers/Bundle/Application/5694FC83-018E-46E7-B060-A008867D3C9D/Elephant.app/Frameworks/WCDB.framework/WCDB

四、Crash 的类型

4.1 两类主要的 Crash

引发崩溃的代码本质上就两类,

一类是 c/c++ 语言层面的错误,比如野指针,除零,内存访问异常等等(相对复杂)

对于前者,无论是 iOS 还是 Android 系统,其底层都是 unix 或者是类 unix 系统,都可以通过信号机制来获取 signal 或者是 sigaction (但是只能捕捉有限的几种类型),设置一个回调函数。

  • Watchdog 超时、用户强制退出、低内存终止等,系统抛出Unix信号,没有任何的错误堆栈信息

另一类是未捕获异常 Uncaught Exception(相对简单)

iOS 下面最常见的就是 Objective-C 的NSException(@throw 抛出),可以使用NSUncaughtExceptionHandler catch 住防止崩溃。

  • 如数组越界,给对象发送了无法识别的消息(selector方法没有实现,对象调用方法出错)等,系统抛出一个NSException对象,对象中有出错的堆栈,描述了出错的代码位置、类名和方法名
4.1.1 Bus Error

在检测顺序上,先检测 SIGBUS,再检测 SIGSEGV。

SIGBUS 地址被放到地址总线之后,检测出地址不对齐,发出异常信号,

SIGSEGV 地址已经放到地址总线上后,在后续流程中检测出内存违法访问,发出异常信号。

4.1.2 其他异常类型

崩溃(准确的说是程序异常终止)是程序接收到未处理信号的结果。

未处理信号有三个来源:内核、其他进程和应用本身。导致崩溃最常见的两个信号如下:

在 Objective-C 异常中,导致异常抛出最常见的原因是应用向对象发送了未实现的方法选择器(比如拼写错误,对象混淆或者向已经释放的对象发送消息)。

4.2 Low Memory Report 低内存报告

Low Memory Termination

跟一般的Crash结构不太一样,通常有Free pages,Wired Pages,Purgeable pages,largest process 组成,同时会列出当前时刻系统运行所有进程的信息。

Low Memory Report 与其它 Crash Report 不同,它没有堆栈信息,所以不需要符号化。一个低内存 Report的Header会和 Crash Report 的header有些类似。紧接着Header的时各个字段的系统级别的内存统计信息。记录下页大小(Page Size)字段。每一个进程的内存占用大小是根据内存的页的数量来 Report的。一个低内存 Report最重要的部分是进程表格。这个表格列出了所有的运行进程,包括系统在生成低内存 Report时的守护进程。如果一个进程被”遗弃”了,会在[原因]一列附上具体的原因。一个进程可能被遗弃的原因有:

当你发现一个低内存crash,与其去担心哪一部分的代码出现问题,还不如仔细审视一下自己的内存使用习惯和针对低内存告警(low-memory warning)的处理措施。Locating Memory Issues in Your App 列出了如何使用Leaks Instrument工具来检查内存泄漏,和如何使用Allocations Instrument的Mark Heap 功能来避免内存浪费。 Memory Usage Performance Guidelines 讨论了如何处理接受到低内存告警的问题,以及如何高效使用内存。当然,也推荐你去看下2010年的WWDC中的 Advanced Memory Analysis with Instruments 那一章节。

重要:Leaks和Allocation工具不能检测所有的内存使用情况。你需要和VM Tracker工具一起运行(包含在Allocation工具里)来查看你的内存运行。默认VM Tracker是不可用的。如果想通过VM Tracker来profile你的应用,点击instrument工具,选中”Automatic Snapshotting”标签或者手动点击”Snapshot Now”按钮。

五、Crash 的捕获

5.0 Last Exception Backtrace

若程序因 NSException 而 Crash,系统日志中的 Last Exception Backtrace 信息是完整准确的,不会受应用层的 Crash 统计服务影响,可作为排查问题的参考线索。如果 Last Exception Backtrace,只包含16进制信息的日志,必须进行符号化来获取有价值的堆栈信息

# 未符号化的异常堆栈
Last Exception Backtrace:

(0x18eee41c0 0x18d91c55c 0x18eee3e88 0x18f8ea1a0 0x195013fe4 0x1951acf20 0x18ee03dc4 0x1951ab8f4 0x195458128 0x19545fa20 0x19545fc7c 0x19545ff70 0x194de4594 0x194e94e8c 0x194f47d8c 0x194f39b40 0x194ca92ac 0x18ee917dc 0x18ee8f40c 0x18ee8f89c 0x18edbe048 0x19083f198 0x194d21bd0 0x194d1c908 0x1000ad45c 0x18dda05b8)

5.1 处理未捕获异常(uncaught exceptions)

Demo :https://github.com/xcysuccess/iOSCrashUncaught

有两种方式可以捕获那些会导致崩溃的未捕获状态。

注意:signal 要在没有附加 debugger 的环境下获取,否则会被 debugger 优先拦截。UncaughtExceptionHandler可以在调试状态下捕获

抓取 NSException
// 安装 Objective-C 异常处理器和信号处理的代码如下:
void InstallUncaughtExceptionHandler() {
    NSSetUncaughtExceptionHandler(&MyUncaughtExceptionHandler);
    signal(SIGABRT, SignalHandler);
    signal(SIGILL, SignalHandler);
    signal(SIGSEGV, SignalHandler);
    signal(SIGFPE, SignalHandler);
    signal(SIGBUS, SignalHandler);
    signal(SIGPIPE, SignalHandler);
}
// 对于异常和信号的响应会在 MyUncaughtExceptionHandler 和 SignalHandler 中实现。在样例程序中,以上二者的处理方式相同。

void MyUncaughtExceptionHandler(NSException *exception) {
    NSString *ret = [NSString stringWithFormat:@"异常名称:\n%@\n\n异常原因:\n%@\n\n出错堆栈内容:\n%@\n",exception.name, exception.reason, exception.callStackSymbols];
    // 将捕获到的 exception 细节上传到后台
}
抓取 Signal

signal信号是Unix系统中的,是一种异步通知机制.信号传递给进程后,在没有处理函数的情况下,程序可以指定三种行为:

  1. 忽略该信号,但是对于信号SIGKILLSIGSTOP不可忽略
  2. 使用默认的处理函数SIG_DFL(即 signal(sig, SIG_DFL);),大多数信号的默认动作是终止进程
  3. 捕获信号,执行用户定义的函数

有两个特殊的常量:

还有两个常用的函数

// UNIX系统中常用的信号有以下几种:
SIGABRT--程序中止命令中止信号 
SIGBUS--程序内存字节未对齐中止信号
SIGFPE--程序浮点异常信号
SIGILL--程序非法指令信号
SIGSEGV--程序无效内存中止信号
SIGTERM--程序kill中止信号
SIGKILL--程序结束接收中止信号 
    
SIGALRM--程序超时信号 
SIGHUP--程序终端中止信号
SIGINT--程序键盘中断信号 
SIGSTOP--程序键盘中止信号  
SIGPIPE--程序Socket发送失败中止信号

// 抓取的是以下几种
static int Beacon_errorSignals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
    SIGTERM,
    SIGKILL,
};
for (int i = 0; i < Beacon_errorSignalsNum; i++) {
    signal(Beacon_errorSignals[i], &mysighandler);
}
// 抓取信号的处理函数
void mysighandler(int sig) {
    void* callstack[128];
    NSString* name ;
    int i, frames = backtrace(callstack, 128);
    for (i = 0; i < Beacon_errorSignalsNum; i++) {
        if (Beacon_errorSignals[i] == sig ) {
            name = [Beacon_errorSignalNames[i] copy];
            break;
        }
    }
    char** strs = backtrace_symbols(callstack, frames);
    NSMutableString* exceptionStr = [[NSMutableString alloc]initWithFormat:@"异常名称:\n%@\n\n出错堆栈内容:\n",name];
    for (i =0; i <frames; i++) {
        [exceptionStr appendFormat:@"%s\n",strs[i]];
    }
    free(strs);
}

// 在应用崩溃后,保持运行状态而不退出,让响应更加友好
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

while (!dismissed) {
    for (NSString *mode in (__bridge NSArray *)allModes) {
        CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);
    }
}

CFRelease(allModes);

这里只处理最常见的信号,但是,你可以为自己的程序添加所需的所有异常信号。

注意,有两种异常是不能捕获的:SIGKILL和SIGSTOP。它们会终止或者暂停应用。(SIGKILL是命令行函数kill -9发出的,SIGSTOP是键入Control-Z发出的)。

如果你发现本应该被捕捉的异常并没有被捕捉到,请确定您没有在building应用或者library时添加了-no_compact_unwind标签。

64位 iOS 用了zero-cost的异常实现机制。在zero-cost系统里,每一个函数都有一个额外的数据,它会描述如果一个异常在跨函数范围内实现,该如何展开相应的堆栈信息。如果一个异常发生在多个堆栈但是没有可展开的数据,那么异常处理函数自然无法跟踪并记录。也许在堆栈很上层的地方有异常处理函数,但是如果那里没有一个片段的可展开信息,没办法从发生异常的地方到那里。指定了-no_compact_unwind标签表明你那些代码没有可展开信息,所以你不能跨越函数抛出异常(也就是说无法通过别的函数捕捉当前函数的异常)。

5.2 Xcode 提供的调试工具

都在 Edit Scheme -> Diagnostics(诊断) 依次可以找到

Runtime Sanitization
Memory Management
Analyze(静态代码分析)

不是那么准确,但是会发现一些问题

可以发现编译中的 warning,内存泄漏隐患,甚至还可以检查出逻辑上的问题;所以在自测阶段一定要解决Analyze发现的问题,可以避免出现严重的bug。

主要分析以下四种问题:

# 内存泄漏隐患
Potential(潜在) Leak of an object allocated on line ……
# 数据赋值隐患
The left operand of …… is a garbage value;
# 对象引用隐患
Reference-Counted object is used after it is released;
Profile(就是运行 Instrument)

真正运行程序,对程序进行内存分析(查看内存分配情况、内存泄露)

优点:分析非常准确,如果发现有提示内存泄露,基本可以断定代码问题

缺点:分析效率低(真正运行了一段代码,才能对该代码进行内存分析)

六、附录

上一篇 下一篇

猜你喜欢

热点阅读