title: 打印堆栈信息,保存崩溃现场
date: 2017-01-31 13:11:57
tags: [iOS]

程序崩溃的时候需要上报崩溃堆栈,以及每个线程的堆栈信息。方便查找bug

如何捕获崩溃

NSSetUncaughtExceptionHandler

注册 catchCrash 异常。

1
2
3
4
5
6
7
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions  
{

InstallUncaughtExceptionHandler();//信号量截断
//注册消息处理函数的处理方法
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
}

捕获异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void uncaughtExceptionHandler(NSException *exception)  
{
// 异常的堆栈信息
NSArray *stackArray = [exception callStackSymbols];
// 出现异常的原因
NSString *reason = [exception reason];
// 异常名称
NSString *name = [exception name];
NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
NSLog(@"%@", exceptionInfo);

NSMutableArray *tmpArr = [NSMutableArray arrayWithArray:stackArray];
[tmpArr insertObject:reason atIndex:0];

//保存到本地 -- 当然你可以在下次启动的时候,上传这个log
[exceptionInfo writeToFile:[NSString stringWithFormat:@"%@/Documents/error.log",NSHomeDirectory()] atomically:YES encoding:NSUTF8StringEncoding error:nil];
}

signal

NSSetUncaughtExceptionHandler 用来做异常处理,但功能非常有限.这仅仅是捕获一般的OC异常信息,对于Signal异常信号我们仍然无法捕获到。

而引起崩溃的大多数原因如:内存访问错误,重复释放等错误就无能为力了,因为这种错误它抛出的是Signal,所以必须要专门做Signal处理。

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
//注意: SignalHandler不要在debug环境下测试。因为系统的debug会优先去拦截。
void InstallUncaughtExceptionHandler()
{

// signal的类型在signal.h的头文件里有详细的列表。
//调用abort函数生成的信号。
signal(SIGABRT, signalHandler);
//执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
signal(SIGILL, signalHandler);
//试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据. signal(SIGSEGV, signalHandler);
//在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
signal(SIGFPE, signalHandler);
//非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
signal(SIGBUS, signalHandler);
//管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
signal(SIGPIPE, signalHandler);
}

void signalHandler(int signal)
{
NSMutableString *mstr = [[NSMutableString alloc] init];
[mstr appendString:@"Stack:\n"];
void* callback[128];
int i, frames = backtrace(callstack,128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i < frames; ++i){
[mstr appendFormat:@"%s\n",strs[i]];
}
NSLog(@"%@",mstr);
}

崩溃后获取线程的信息

首先是要通过 task_threads 取到所有的线程

1
2
3
4
5
thread_act_array_t threads; //int 组成的数组比如 thread[1] = 5635
mach_msg_type_number_t thread_count = 0; //mach_msg_type_number_t 是 int 类型
const task_t this_task = mach_task_self(); //int
//根据当前 task 获取所有线程
kern_return_t kr = task_threads(this_task, &threads, &thread_count);

遍历时通过 thread_info 获取各个线程的详细信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SMThreadInfoStruct threadInfoSt = {0};
thread_info_data_t threadInfo;
thread_basic_info_t threadBasicInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
if (thread_info((thread_act_t)thread, THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
threadBasicInfo = (thread_basic_info_t)threadInfo;
if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) {
threadInfoSt.cpuUsage = threadBasicInfo->cpu_usage / 10;
threadInfoSt.userTime = threadBasicInfo->system_time.microseconds;
}
}
uintptr_t buffer[100];
int i = 0;
NSMutableString *reStr = [NSMutableString stringWithFormat:@"Stack of thread: %u:\n CPU used: %.1f percent\n user time: %d second\n", thread, threadInfoSt.cpuUsage, threadInfoSt.userTime];

可以通过 thread_get_state 得到 machine context 里面包含了线程栈里所有的栈指针。

1
2
3
4
_STRUCT_MCONTEXT machineContext; //线程栈里所有的栈指针
//通过 thread_get_state 获取完整的 machineContext 信息,包含 thread 状态信息
mach_msg_type_number_t state_count = smThreadStateCountByCPU();
kern_return_t kr = thread_get_state(thread, smThreadStateByCPU(), (thread_state_t)&machineContext.__ss, &state_count);

创建一个栈结构体用来保存栈的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//为通用回溯设计结构支持栈地址由小到大,地址里存储上个栈指针的地址
typedef struct SMStackFrame {
const struct SMStackFrame *const previous;
const uintptr_t return_address;
} SMStackFrame;
SMStackFrame stackFrame = {0};
//通过栈基址指针获取当前栈帧地址
const uintptr_t framePointer = smMachStackBasePointerByCPU(&machineContext);
if (framePointer == 0 || smMemCopySafely((void *)framePointer, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) {
return @"Fail frame pointer";
}
for (; i < 32; i++) {
buffer[i] = stackFrame.return_address;
if (buffer[i] == 0 || stackFrame.previous == 0 || smMemCopySafely(stackFrame.previous, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) {
break;
}
}

符号化

符号化主要思想就是通过栈指针地址减去 Slide 地址得到 ASLR 偏移量,通过这个偏移量可以在 __LINKEDIT segment 查找到字符串和符号表的位置。具体代码实现如下:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
info->dli_fname = NULL;
info->dli_fbase = NULL;
info->dli_sname = NULL;
info->dli_saddr = NULL;
//根据地址获取是哪个 image
const uint32_t idx = smDyldImageIndexFromAddress(address);
if (idx == UINT_MAX) {
return false;
}
/*
Header
------------------
Load commands
Segment command 1 -------------|
Segment command 2 |
------------------ |
Data |
Section 1 data |segment 1 <----|
Section 2 data | <----|
Section 3 data | <----|
Section 4 data |segment 2
Section 5 data |
... |
Section n data |
*/
/*----------Mach Header---------*/
//根据 image 的序号获取 mach_header
const struct mach_header* machHeader = _dyld_get_image_header(idx);
//返回 image_index 索引的 image 的虚拟内存地址 slide 的数量,如果 image_index 超出范围返回0
//动态链接器加载 image 时,image 必须映射到未占用地址的进程的虚拟地址空间。动态链接器通过添加一个值到 image 的基地址来实现,这个值是虚拟内存 slide 数量
const uintptr_t imageVMAddressSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
/*-----------ASLR 的偏移量---------*/
//https://en.wikipedia.org/wiki/Address_space_layout_randomization
const uintptr_t addressWithSlide = address - imageVMAddressSlide;
//根据 Image 的 Index 来获取 segment 的基地址
//段定义Mach-O文件中的字节范围以及动态链接器加载应用程序时这些字节映射到虚拟内存中的地址和内存保护属性。 因此,段总是虚拟内存页对齐。 片段包含零个或多个节。
const uintptr_t segmentBase = smSegmentBaseOfImageIndex(idx) + imageVMAddressSlide;
if (segmentBase == 0) {
return false;
}
//
info->dli_fname = _dyld_get_image_name(idx);
info->dli_fbase = (void*)machHeader;
/*--------------Mach Segment-------------*/
//地址最匹配的symbol
const nlistByCPU* bestMatch = NULL;
uintptr_t bestDistance = ULONG_MAX;
uintptr_t cmdPointer = smCmdFirstPointerFromMachHeader(machHeader);
if (cmdPointer == 0) {
return false;
}
//遍历每个 segment 判断目标地址是否落在该 segment 包含的范围里
for (uint32_t iCmd = 0; iCmd < machHeader->ncmds; iCmd++) {
const struct load_command* loadCmd = (struct load_command*)cmdPointer;
/*----------目标 Image 的符号表----------*/
//Segment 除了 __TEXT 和 __DATA 外还有 __LINKEDIT segment,它里面包含动态链接器的使用的原始数据,比如符号,字符串和重定位表项。
//LC_SYMTAB 描述了 __LINKEDIT segment 内查找字符串和符号表的位置
if (loadCmd->cmd == LC_SYMTAB) {
//获取字符串和符号表的虚拟内存偏移量。
const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPointer;
const nlistByCPU* symbolTable = (nlistByCPU*)(segmentBase + symtabCmd->symoff);
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;

for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
//如果 n_value 是0,symbol 指向外部对象
if (symbolTable[iSym].n_value != 0) {
//给定的偏移量是文件偏移量,减去 __LINKEDIT segment 的文件偏移量获得字符串和符号表的虚拟内存偏移量
uintptr_t symbolBase = symbolTable[iSym].n_value;
uintptr_t currentDistance = addressWithSlide - symbolBase;
//寻找最小的距离 bestDistance,因为 addressWithSlide 是某个方法的指令地址,要大于这个方法的入口。
//离 addressWithSlide 越近的函数入口越匹配
if ((addressWithSlide >= symbolBase) && (currentDistance <= bestDistance)) {
bestMatch = symbolTable + iSym;
bestDistance = currentDistance;
}
}
}
if (bestMatch != NULL) {
//将虚拟内存偏移量添加到 __LINKEDIT segment 的虚拟内存地址可以提供字符串和符号表的内存 address。
info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddressSlide);
info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
if (*info->dli_sname == '_') {
info->dli_sname++;
}
//所有的 symbols 的已经被处理好了
if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
info->dli_sname = NULL;
}
break;
}
}
cmdPointer += loadCmd->cmdsize;
}