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

结构

Installations 安装
Recording 记载
Monitor 监督类型 debug维护
KSCrashReport 报告
Reporting 上报

溃散履行顺序

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

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 处理首要溃散线程溃散的线程。

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

知识点

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值更新