崩溃捕获

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
// 1.捕获一些异常导致的崩溃
NSSetUncaughtExceptionHandler(&HandleException);
// 2.捕获非异常情况,通过signal传递出来的崩溃
signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);
 
 
- (void)handleException:(NSException *)exception
{
    NSString *message = [NSString stringWithFormat:@"崩溃原因如下:\n%@\n%@",
                         [exception reason],
                         [[exception userInfo] objectForKey:kCaughtExceptionStackInfoKey]];
    NSLog(@"%@",message);
     
    UIAlertController *alertVc = [UIAlertController alertControllerWithTitle:@"程序崩溃了" message:@"如果你能让程序起死回生,那你的决定是?" preferredStyle:UIAlertControllerStyleAlert];
     
   // __weak typeof(self) weakSelf = self;
     
    UIAlertAction *action = [UIAlertAction actionWithTitle:@"崩就蹦吧" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        //weakSelf->ignore = YES;
        self->ignore = YES;
    }];
     
     
    UIAlertAction *action1 = [UIAlertAction actionWithTitle:@"起死回生" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        
    }];
     
    [alertVc addAction:action];
    [alertVc addAction:action1];
     
    UIViewController *rootVC = [CrashHandler currentFrontViewController];
     
    [rootVC presentViewController:alertVc animated:YES completion:nil];
     
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
     
    while (!ignore) {
        for (NSString *mode in (__bridge NSArray *)allModes) {
            CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
        }
    }
     
    CFRelease(allModes);
     
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
     
    if ([[exception name] isEqual:kSignalExceptionName]) {
        kill(getpid(), [[[exception userInfo] objectForKey:kSignalKey] intValue]);
    } else {
        [exception raise];
    }
}
 
void HandleException(NSException *exception)
{
    // 获取异常的堆栈信息
    NSArray *callStack = [exception callStackSymbols]; /// NSThread.callStackSymbols    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    [userInfo setObject:callStack forKey:kCaughtExceptionStackInfoKey];
     
    CrashHandler *crashObject = [CrashHandler sharedInstance];
    NSException *customException = [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo];
    [crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
 
void SignalHandler(int signal)
{
    // 这种情况的崩溃信息,就另某他法来捕获吧
    NSArray *callStack = [CrashHandler backtrace];
    NSLog(@"信号捕获崩溃,堆栈信息:%@",callStack);
     
    CrashHandler *crashObject = [CrashHandler sharedInstance];
    NSException *customException = [NSException exceptionWithName:kSignalExceptionName
                                                           reason:[NSString stringWithFormat:NSLocalizedString(@"Signal %d was raised.", nil),signal]
                                                         userInfo:@{kSignalKey:[NSNumber numberWithInt:signal]}];
     
    [crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}

 

获取各线程调用堆栈

 

1. 获取所有线程

1
2
3
4
thread_act_array_t threads; mach_msg_type_number_t thread_count = 0; const task_t this_task = mach_task_self();
kern_return_t kr = task_threads(this_task, &threads, &thread_count);
if(kr != KERN_SUCCESS) {
  return @"Fail to get information of all threads"; <br>}

2. 获取对应线程最顶层函数,以及esp、ebp指针

1
2
3
4
5
6
7
8
9
10
11
12
13
kern_return_t thread_get_state (
    thread_act_t target_act, //目标线程,通过task_threads接口来获取
    thread_state_flavor_t flavor, //线程状态类型,如 [ARM/x86]_THREAD_STATE64
    thread_state_t old_state, //线程状态信息,可获取线程调用栈寄存器信息
    mach_msg_type_number_t *old_stateCnt //线程状态信息成员数目
);
 
bool fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE,      
        (thread_state_t)&machineContext->__ss, &state_count);
    return (kr == KERN_SUCCESS);
}

3. 根据地址定位镜像

每个镜像文件都对应一个地址范围,且每个镜像文件都是Mach-O格式的文件。

上一步根据栈帧指针得到的是方法的地址,首先需要判断该方法属于哪个镜像文 件。

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
uint64_t count = _dyld_image_count();//image数量
const struct mach_header *header = _dyld_get_image_header(index);//image mach-o header
const char *name = _dyld_get_image_name(index);//image name <br>uint64_t slide = _dyld_get_image_vmaddr_slide(index);//ALSR偏移地址static uint32_t imageIndexContainingAddress(const uintptr_t address)
{
    const uint32_t imageCount = _dyld_image_count(); <br>    const struct mach_header* header = 0;
    for(uint32_t iImg = 0; iImg < imageCount; iImg++) {<br>        /// 遍历images
        header = _dyld_get_image_header(iImg); // image mach-o header        if(header != NULL)
        {
            // Look for a segment command with this address within its range.
            uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg); // ALSR偏移地址
            uintptr_t cmdPtr = firstCmdAfterHeader(header);            if(cmdPtr == 0) { continue; }
            for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {<br>          /// 遍历查找mach-0下的load command 类型为SEGMENT, 找到image地址在进程中的内存
                const struct load_command* loadCmd = (struct load_command*)cmdPtr;
                if(loadCmd->cmd == LC_SEGMENT) {
                    const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
                    if(addressWSlide >= segCmd->vmaddr
                       && addressWSlide < segCmd->vmaddr + segCmd->vmsize)
                    {
                        return iImg;
                    }
                } else if(loadCmd->cmd == LC_SEGMENT_64) {
                    const struct segment_command_64* segCmd = (struct segment_command_64*)cmdPtr;
                    if(addressWSlide >= segCmd->vmaddr
                       && addressWSlide < segCmd->vmaddr + segCmd->vmsize)
                    {
                        return iImg;
                    }
               }
               cmdPtr += loadCmd->cmdsize;
           }
       }
    }
    return UINT_MAX;
}           
   // 参数是函数的地址,返回地址是对应镜像的索引

  

4. 根据地址、镜像文件,定位符号

镜像本身也是Mach-O格式的文件。获取到镜像文件后,就可以读取该镜像文件, 获的其符号表信息、字符串表信息(包含符号数量、符号大小、字符串数量、字符 串大小)。
1
2
3
4
5
6
7
8
9
//基址 = 偏移量 + _LINKEDIT段虚拟地址 - _LINKEDIT段文件偏移地址
uintptr_t linkeditBase = (uintptr_t)slide + linkeditSegment->vmaddr - linkeditSegment->fileoff;
//符号表的地址 = 基址 + 符号表偏移量
const nlist_t *symbolTable = (nlist_t *)(linkeditBase + symtabCmd- >symoff);
//字符串表的地址 = 基址 + 字符串表偏移量
char *stringTab = (char *)(linkeditBase + symtabCmd->stroff); //符号数量
uint32_t symNum = symtabCmd->nsyms;<br><br>// If n_value is 0, the symbol refers to an external object.
// This image has been stripped. The name is meaningless, and
// almost certainly resolves to "_mh_execute_header"
  /// 上述查找符号是获取的真正的符号内存地址和函 数名,而通过函数调用栈获取的是函数内部执行指令的地址,不过该地址与真正的 函数地址偏离不大,因此可以通过遍历符号的内存地址与调用栈函数地址比较得到 离符号内存地址最近的最佳匹配符号,即是当前调用栈的符号。
const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
const uintptr_t addressWithSlide = address - imageVMAddrSlide;//address为调用栈内存地址
//遍历符号需找最佳匹配符号
for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {

   if(symbolTable[iSym].n_value != 0) {

      uintptr_t symbolBase = symbolTable[iSym].n_value;//获取符号的 内存地址(函数指针)

      uintptr_t currentDistance = addressWithSlide - symbolBase;

     if((addressWithSlide >= symbolBase) &&
        (currentDistance <= bestDistance))
     {

        bestMatch = symbolTable + iSym;//最佳匹配符号地址

        bestDistance = currentDistance;//调用栈内存地址与当前符号内存地址距离

     }

  }

}

if(bestMatch != NULL) {

   info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);

   if(bestMatch->n_desc == 16) {

      info->dli_sname = NULL;

  }

  else

  {

    info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);

   if(*info->dli_sname == '_') {

     info->dli_sname++; }

   }

}

 

crash和解决方案:

  • unrecognized selector sent to instance 方法找不到
  • 数组越界,插入空值
  • [NSDictionary initWithObjects:forKeys:]使用此方法初始化字典时,objects和keys的数量不一致时
  • NSMutableDictionary,setObject:forKey:或者removeObjectForKey:时,key为nil
  • setValue:forUndefinedKey:,使用KVC对对象进行存取值时传入错误的key或者对不可变字典进行赋值
  • NSUserDefaults 存储时key为nil
  • 对字符串操作时,传递的下标超出范围,判断是否存在前缀,后缀子串时,子串为空
  • 使用C字符串初始化字符串时,传入null
  • 对可变集合或字符串使用copy修饰并进行修改操作
  • 在空间未添加到父元素上之前,就使用autoLayout进行布局
  • KVO在对象销毁时,没有移除KVO或者多次移除KVO
  • 野指针访问
  • 死锁

1-9都可以利用Runtime进行拦截,然后进行一些逻辑处理,防止crash

NSInvalidArgumentException
向容器加入nil,引起的崩溃。

SIGSEGV

访问没有被开辟的内存或者已经被释放的内存。

NSRangeException

数组越界,字符串截取越界

SIGABRT

异常 这是一个让程序终止的标识,会在断言、app内部、操作系统用终止方法抛出。

SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。

SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。

SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

SIGCHLD
子进程结束时, 父进程会收到这个信号。

crash一般产生自 iOS 的微内核 Mach,然后在 BSD 层转换成 UNIX SIGABRT 信号,以标准 POSIX 信号的形式提供给用户。

NSSetUncaughtExceptionHandler 函数用于设置异常处理的回调函数,在程序终止前的最后一刻会调用我们设置的回调函数(Handler),进行崩溃日志的记录

大部分第三方 SDK 也是通过这种方式来收集异常的,当我们通过 NSSetUncaughtExceptionHandler 设置异常处理函数时,会覆盖其它 SDK 设置的回调函数,导致它们无法上报统计,反之,也可能出现我们设置的回调函数被其他人覆盖。

我们可以在调用 NSSetUncaughtExceptionHandler 注册异常处理函数之前,先通过 NSGetUncaughtExceptionHandler 拿到已有的异常处理函数并保存下来。然后在我们自己的处理函数执行之后,再调用之前保存的处理函数就可以了。

 
posted @   程石亮  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示