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