关于iOS端的崩溃捕捉的一些原理, 许多三方捕捉底层都是基于KSCrash,有关KSCrash的笔记记录如下,干货满满,不啰嗦。
结构
Installations 安装
Recording 记录
Monitor 监视类型 debug保护
KSCrashReport 报告
Reporting 上报
崩溃执行顺序
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 处理主要崩溃线程崩溃的线程。
知识点
C++ 崩溃
原理
c++崩溃过程
在OSX中,会通过对话框展示异常给用户,但在iOS中,只是重新抛出异常。系统在捕捉到C++异常后,如果能够将此C++异常转换为OC
异常,则抛出OC异常处理机制;如果不能转换,则会立刻调用__cxa_throw
重新抛出异常。
当系统在RunLoop捕捉到的C++异常时,此时的调用堆栈是异常发生时的堆栈,但当系统在不能转换为OC异常时调用__cxa_throw
时,上层捕捉此再抛出的异常获取到的调用堆栈是RunLoop异常处理函数的堆栈,导致原始异常调用堆栈丢失。
C++发生异常调用顺序 __cxa_throw -> NSException
实现方式
- 设置异常函数
- 通过
std::set_terminate
来设置新的程序崩溃接受函数。同时该函数的返回值为上一次设置的函数。保存,后续还原监控以及保留之前监控的时候操作。
- 通过
- 交换
__cxa_throw
方法。- 通过
ksct_swap(handler)
交换方法,并保留之前的__cxa_throw
方法。在调用交换后的方法后保存调用栈,并回调原方法__cxa_throw
。
- 通过
__cxa_throw
往后执行,进入set_terminate
设置的异常处理函数。判断如果检测是OC异常,则什么也不做,让OC异常机制处理;否则获取异常信息。
知识点
ksct_swap的实现方式
static void rebind_symbols_for_image(const struct mach_header *header, intptr_t slide)
找到SEG_DATA_CONST
和SEG_DATA
Segment 遍历这两个段,找到__cxa_throw
并吧原函数的实现存储到KSAddressPair
其中key为dli_fbase,及库的位置。替换__cax_throw
的实现方式__cxa_throw_decorator
。- 通过
__cxa_throw_decorator
,- 调用之前传递过来的handler,即获取当前堆栈。
- 再次获取当前堆栈。通过找到当前调用函数的dli_fbase,找到原方法,并实现。
- 异常再次回调
void CPPExceptionTerminate(void)
- 暂停线程,通过
__cxxabiv1::__cxa_current_exception_type();
获取当前崩溃信息名字。 如果为NSException类型,则不处理。等待NSException捕捉处理 - 如果非NSException类型。或者名字为空。
- 设置线程为非安全异常捕捉(后续通过这个要移除所有监控。防止信号循环调用)
- 获取监视器上下文
- 类型全部类型 char short int 、std::exception& exc等等
- 设置基本信息
- 暂停线程,通过
- 回调
kscm_handleException
进行统一处理 - 回调
onCrash
统一处理
Zombie
原理:
- hook
NSObject
和NSProxy
的dealloc
, 拿到将要释放的对象,通过hash
表,保存Class
类型以及Class Name
。 - 遍历
superClass
, 如果class继承Exception
, 则记录,保存在g_lastDeallocedException
内。存储内容为name,reason,exception对象地址。当程序崩溃的时候,记录在eventContext
的ZombileExpction
内,记录的时候调用并写入。
僵尸对象注意的地方
- KSCrashReport中
- writeNotableRegisters 调用4
- writeNotableStackContents
- 通过SP指针(栈顶指针),获取查找范围lowAddress和HighAddress 遍历调用4
- isNotableAddress 值得注意的地址 会查询僵尸对象保存的地址
- writeMemoryContents
- writeObjCObject
知识点:
获取Class类中ro的信息
- 通过Object_class(id)拿到类,模拟class结构,通过
class->data_NEVER_USE & FAST_DATA_MASK
拿到class_rw_t
, - 通过
class_rw_t
拿到ro_or_rw_ext
, - 判断
ro_or_rw_ext
的最低位是否为1,如果为0则是class_ro_t
,直接返回, - 如果为1则为
class_rw_ext_t
, 通过class_rw_ext_t &= ~0x1UL
, 获取真实地值。在通过class_rw_ext_t->ro
, 拿到ro对象,并返回。
通过指针拿到对象信息
- 判断isa是否为taggerPoint类型, 通过判断指针第1位是否为1判断。
TaggedPotintMask 1<<63
- 如果是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
- 通过FP SP PC LR 寄存器 判断调用链深度。 如果达到最大值,判定为Stack OverFlow 栈溢出。
- 崩溃时检测系统可用运行内存来判断。 task_info()
判断流程
- 开始循环
- 判断是否深度大于maxDepth 否则返回fase
- 当前深度为0 &&
PC寄存器
为空的时候, 赋值PC寄存器。获取当前执行的指针
并nextAddress = PC寄存器 跳转 8 - 如果
LR寄存器
为空 && isPastFramePointer == false 赋值LR寄存器 nextAdress = LR寄存器 跳转 8 - 如果到达初始函数
- 如果为isPastFramePointer == true 则停止循环
- currentFrame.previous 设置为PF指针
- isPastFramePointer设置为true
- 将当前currentFrame.previous拷贝到currentFrame
- 判断currentFrame.previous == 0 || return_adress == 0 停止循环
- nextAddress指向当前当前帧的LR寄存器
- 设置cursor->stackEntry.address的指针为nextAddress 记录当前地址
- 深度+1
信号量解释
SIGABRT
是调用abort()
生成的信号,有可能是NSException也有可能是Mach异常SIGBUS
: 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。比如:char *s = "hello world"; *s = 'H';
SIGSEGV
: 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。比如:给已经release的对象发送消息SegmentationViolation
SIGILL
: 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号SIGPIPE
: 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止SIGTRAP
:由断点指令或其它trap指令产生. 由debugger使用
优点与缺点
优点
- 能够捕获所需要的崩溃
- 项目设计结构清晰。每个类分工明确。
- 针对各种情况比较安全
- 调试模式
p_flag & P_TRACED
unsafe: MachException、Signal、CPP、NSException
- 安全拷贝 vm_read_overwrite()
- 调试模式
缺点
- 判断方式不灵活,需要根据现有oc结构去写
硬代码
OC结构更新,KSCrash就需要同步更新ObjcApple.h
中体现- 各种MASK
问题与解决方式
- TaggerPorint 类型判断
- class中data的获取方式MASK值更新