关于KSCrash的一些整理(干货满满)

4,067 阅读7分钟

关于iOS端的崩溃捕捉的一些原理, 许多三方捕捉底层都是基于KSCrash,有关KSCrash的笔记记录如下,干货满满,不啰嗦。

结构

Installations 安装
Recording 记录
Monitor 监视类型 debug保护
KSCrashReport 报告
Reporting 上报

崩溃执行顺序

16587199082192.jpg

Singnal捕捉

原理

通过int sigaction(int, const struct sigaction * __restrict,struct sigaction * __restrict);函数来注册signal的捕捉,三个参数代表的意义是: 第一个参数int: 是指signal。 第二个参数const struct sigaction * __restrict 是当前操作的回调方法以及栈空间。 第三个参数const struct sigaction * __restrict 是之前操作的回调方法。

struct sigaction action = {{0}};
action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#if KSCRASH_HOST_APPLE && defined(__LP64__)
    action.sa_flags |= SA_64REGSET;
#endif 
sigemptyset(&action.sa_mask);
action.sa_sigaction = &handleSignal;


sigaction(fatalSignals[i], &action, &g_previousSignalHandlers[i])

崩溃捕捉后需要记录崩溃地址,从信号量的参数上来获取

    static void handleSignal(int sigNum, siginfo_t* signalInfo, void* userContext) {
        crashContext->faultAddress = (uintptr_t)signalInfo->si_addr;
        crashContext->signal.userContext = userContext;
        crashContext->signal.signum = signalInfo->si_signo;
        crashContext->signal.sigcode = signalInfo->si_code;
    }

知识点

sigaction()signal()的优势是: 当出现递归错误的时候,sigaction有自己的栈空间去处理错误。

Mach捕捉

原理

两个线程处理崩溃 1 主要崩溃线程 2 处理主要崩溃线程崩溃的线程。

Mach.png

知识点

C++ 崩溃

原理

c++崩溃过程

在OSX中,会通过对话框展示异常给用户,但在iOS中,只是重新抛出异常。系统在捕捉到C++异常后,如果能够将此C++异常转换为OC异常,则抛出OC异常处理机制;如果不能转换,则会立刻调用__cxa_throw重新抛出异常。

当系统在RunLoop捕捉到的C++异常时,此时的调用堆栈是异常发生时的堆栈,但当系统在不能转换为OC异常时调用__cxa_throw时,上层捕捉此再抛出的异常获取到的调用堆栈是RunLoop异常处理函数的堆栈,导致原始异常调用堆栈丢失。

C++发生异常调用顺序 __cxa_throw -> NSException

实现方式

  1. 设置异常函数
    1. 通过std::set_terminate来设置新的程序崩溃接受函数。同时该函数的返回值为上一次设置的函数。保存,后续还原监控以及保留之前监控的时候操作。
  2. 交换__cxa_throw方法。
    1. 通过ksct_swap(handler)交换方法,并保留之前的__cxa_throw方法。在调用交换后的方法后保存调用栈,并回调原方法__cxa_throw
  3. __cxa_throw往后执行,进入set_terminate设置的异常处理函数。判断如果检测是OC异常,则什么也不做,让OC异常机制处理;否则获取异常信息。

知识点

ksct_swap的实现方式

  1. static void rebind_symbols_for_image(const struct mach_header *header, intptr_t slide)找到SEG_DATA_CONSTSEG_DATA Segment 遍历这两个段,找到__cxa_throw 并吧原函数的实现存储到KSAddressPair 其中key为dli_fbase,及库的位置。替换__cax_throw的实现方式__cxa_throw_decorator
  2. 通过__cxa_throw_decorator,
    1. 调用之前传递过来的handler,即获取当前堆栈。
    2. 再次获取当前堆栈。通过找到当前调用函数的dli_fbase,找到原方法,并实现。
  3. 异常再次回调void CPPExceptionTerminate(void)
    1. 暂停线程,通过__cxxabiv1::__cxa_current_exception_type();获取当前崩溃信息名字。 如果为NSException类型,则不处理。等待NSException捕捉处理
    2. 如果非NSException类型。或者名字为空。
      1. 设置线程为非安全异常捕捉(后续通过这个要移除所有监控。防止信号循环调用)
      2. 获取监视器上下文
      3. 类型全部类型 char short int 、std::exception& exc等等
      4. 设置基本信息
  4. 回调kscm_handleException进行统一处理
  5. 回调onCrash统一处理

Zombie

原理:

  1. hook NSObjectNSProxydealloc, 拿到将要释放的对象,通过hash表,保存Class类型以及Class Name
  2. 遍历superClass, 如果class继承Exception, 则记录,保存在g_lastDeallocedException 内。存储内容为name,reason,exception对象地址。当程序崩溃的时候,记录在eventContextZombileExpction内,记录的时候调用并写入。

僵尸对象注意的地方

  1. KSCrashReport中
  2. writeNotableRegisters 调用4
  3. writeNotableStackContents
    1. 通过SP指针(栈顶指针),获取查找范围lowAddress和HighAddress 遍历调用4
  4. isNotableAddress 值得注意的地址 会查询僵尸对象保存的地址
  5. writeMemoryContents
  6. writeObjCObject

知识点:

获取Class类中ro的信息

  1. 通过Object_class(id)拿到类,模拟class结构,通过class->data_NEVER_USE & FAST_DATA_MASK拿到class_rw_t,
  2. 通过class_rw_t拿到ro_or_rw_ext
  3. 判断ro_or_rw_ext的最低位是否为1,如果为0则是class_ro_t,直接返回,
  4. 如果为1则为class_rw_ext_t, 通过class_rw_ext_t &= ~0x1UL, 获取真实地值。在通过class_rw_ext_t->ro, 拿到ro对象,并返回。

通过指针拿到对象信息

  1. 判断isa是否为taggerPoint类型, 通过判断指针第1位是否为1判断。TaggedPotintMask 1<<63
  2. 如果是taggedPoint,判断taggedPoint具体类型 通过(pointer >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK),TAG_SLOT_SHIFT为60 即获取后三位,判断后三位的值,来确定具体类型;类型如下所示:
    enum
     {
     // 60-bit payloads
     OBJC_TAG_NSAtom            = 0, 
     OBJC_TAG_1                 = 1, 
     OBJC_TAG_NSString          = 2, 
     OBJC_TAG_NSNumber          = 3, 
     OBJC_TAG_NSIndexPath       = 4, 
     OBJC_TAG_NSManagedObjectID = 5, 
     OBJC_TAG_NSDate            = 6,
     };
    
    通过去除掉_OBJC_TAG_EXT_PAYLOAD_LSHIFT或者_OBJC_TAG_PAYLOAD_LSHIFT的偏移量,KSCrash为低4位。

安全赋值

防止权限问题 通过vm_read_overwrite()判断拷贝的字节是否与应拷贝的字节是否相等。如果相等,则没有读取权限的问题。 如果不相等,则证明有些数据不可读取。就不该继续操作了。 同时也可以用vm_read_overwrite()进行安全拷贝。防止权限问题。

vm_size_t bytesCopied = 0;
vm_read_overwrite(mach_task_self(),(vm_address_t)src,(vm_size_t)byteCount,(vm_address_t)dst,&bytesCopied)

CFStringRef

判断是否是unicode

(str->base._cfinfo[CF_INFO_BITS] & __kCFIsUnicodeMask) == __kCFIsUnicode;

OOM判断 out of memnory

  1. 通过FP SP PC LR 寄存器 判断调用链深度。 如果达到最大值,判定为Stack OverFlow 栈溢出。
  2. 崩溃时检测系统可用运行内存来判断。 task_info()

判断流程

  1. 开始循环
  2. 判断是否深度大于maxDepth 否则返回fase
  3. 当前深度为0 && PC寄存器为空的时候, 赋值PC寄存器。获取当前执行的指针 并nextAddress = PC寄存器 跳转 8
  4. 如果LR寄存器为空 && isPastFramePointer == false 赋值LR寄存器 nextAdress = LR寄存器 跳转 8
  5. 如果到达初始函数
    1. 如果为isPastFramePointer == true 则停止循环
    2. currentFrame.previous 设置为PF指针
    3. isPastFramePointer设置为true
  6. 将当前currentFrame.previous拷贝到currentFrame
  7. 判断currentFrame.previous == 0 || return_adress == 0 停止循环
  8. nextAddress指向当前当前帧的LR寄存器
  9. 设置cursor->stackEntry.address的指针为nextAddress 记录当前地址
  10. 深度+1

信号量解释

  1. SIGABRT是调用abort()生成的信号,有可能是NSException也有可能是Mach异常
  2. SIGBUS: 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。比如:
    char *s = "hello world";
    *s = 'H';
    
  3. SIGSEGV: 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。比如:给已经release的对象发送消息 SegmentationViolation
  4. SIGILL: 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号
  5. SIGPIPE: 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止
  6. SIGTRAP:由断点指令或其它trap指令产生. 由debugger使用

优点与缺点

优点

  1. 能够捕获所需要的崩溃
  2. 项目设计结构清晰。每个类分工明确。
  3. 针对各种情况比较安全
    1. 调试模式 p_flag & P_TRACED
      1. unsafe: MachException、Signal、CPP、NSException
    2. 安全拷贝 vm_read_overwrite()

缺点

  1. 判断方式不灵活,需要根据现有oc结构去写硬代码 OC结构更新,KSCrash就需要同步更新
    1. ObjcApple.h中体现
    2. 各种MASK

问题与解决方式

  1. TaggerPorint 类型判断
  2. class中data的获取方式MASK值更新