背景
在做方法耗时监控时(基于戴铭老师分享: ming1016.github.io/2017/06/20/…),由于通过汇编hook了objc_msgSend方法。引起了,项目中try catch无法捕获异常导致了crash问题。
测试代码
- (void)abc
{
[NSJSONSerialization JSONObjectWithData:nil options:nil error:nil];
}
- (void)testMethod
{
@try {
[self abc];
} @catch (NSException *exception) {
NSLog(@"abc");
} @finally {
}
NSLog(@"def");
}
crash栈
从crash调用栈分析,我们可以看出,crash原因为:程序没有找到异常的处理代码,调用了objc_terminate方法终止了程序。那么为什么程序没有找到catch块呢?
分析iOS try catch原理
OC代码
// test.m
// class test
- (void)testMethod
{
@try {
NSException *excetion = [[NSException alloc] initWithName:@"AException" reason:@"exceptionReason" userInfo:nil];
@throw excetion;
} @catch (NSException *exception) {
NSLog(@"捕获了异常");
} @finally {
}
}
编译源文件为.cpp文件
通过“clang -rewrite-objc test.m -o test.cpp”将.m文件转化成C++代码。并尝试将大括号对其:
static void _I_test_testMethod(test * self, SEL _cmd) {
{
id volatile _rethrow = 0;
try {
try {
NSException *excetion = ((NSException *(*)(id, SEL, NSExceptionName _Nonnull, NSString * _Nullable, NSDictionary * _Nullable))(void *)objc_msgSend)((id)((NSException *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSException"), sel_registerName("alloc")), sel_registerName("initWithName:reason:userInfo:"), (NSString *)&__NSConstantStringImpl__var_folders_62_g3vh8tcj4lg265_31zsqyp7w0000gn_T_test_96af62_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_62_g3vh8tcj4lg265_31zsqyp7w0000gn_T_test_96af62_mi_1, (NSDictionary * _Nullable)__null);
objc_exception_throw( excetion);
} catch (_objc_exc_NSException *_exception) {
NSException *exception = (NSException*)_exception;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_g3vh8tcj4lg265_31zsqyp7w0000gn_T_test_96af62_mi_2);
}
} catch (id e) {_
rethrow = e;
}
{
struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
}
_fin_force_rethow(_rethrow);
}
}
我们发现代码被编译为objc_exception_throw函数,在Xcode搜索发现objc-exception.h文件中为OC处理异常的一系列方法。
为objc_exception_throw函数打符号断点,查看内部实现
通过汇编窗口,可以看到objc_exception_throw方法调用了__cxa_allocate_exception方法。
结论
iOS的异常处理,使用了一套objc_exception处理函数,函数内部调用了stdlibc++的一套异常处理函数。
libstdc++库中用于处理异常的代码
// 为异常分配空间,存储在线程局部存储中
__cxa_allocate_excetpion
__cxa_throw
__cxa_begin_catch
__cxa_end_catch
// 不能处理,栈恢复
_unwind_resume
解决try catch crash问题
一顿操作猛如虎,上面的原理分析并不能帮助我们解决try catch崩溃的问题。
通过搜索,我们发现有一套CFI伪指令用于描述线程调用栈。而异常处理正是用的CFI来进行stack unwinding(栈展开),查找Landing Pad(在异常处理程序,应该跳转到的位置)。crash的原因找到了!
CFI指令
CFI:call frame information。 通常用在stack unwinding(栈展开)中
CFA: canonical frame address
Landing Pad: 在异常处理程序,应该跳转到的位置
crash解决
需要用到如下的伪指令:
.cfi_startproc
// ...
.cfi_def_cfa w29, 16 // call指令前,SP的值
.cfi_offset w30, -8 // LR寄存器存储在栈中的位置,相对于cfa的偏移
.cfi_offset w29, -16 // FP寄存器村粗在栈中的位置,相对于cfa的偏移
// ...
.cfi_endproc
在hook objc_msgSend的汇编代码中加入上诉伪指令
添加了cfi伪指令后依然crash
问题
添加伪指令后crash栈:
这下更方了。
不过通过打断点可以看到异常已经被成功捕获到了,crash是发生在异常捕获后。
我们知道异常被捕获后,代码会从catch块后继续执行。
我们上面的测试代码:testMethod(try catch)->abc->JSONObjectWithData(内部throw异常),在throw异常后,程序跳转到testMethod的catch块继续执行,abc和JSONObjectWithData后续的代码都不会被执行。abc和JSONObjectWithData方法只有入栈没有出栈。
而我们的汇编代码是通过栈来保存LR寄存器,异常被捕获后入栈和出栈不一致,子程序返回发生错乱,导致某些指令执行失败crash。
static inline uintptr_t popLRRegister()
{
lr_stack *lrStack = pthread_getspecific(s_thread_key_lr);
uintptr_t lr = lrStack->lrs[lrStack->index--];
return lr;
}
void hook_objc_msgSend_before(id self, SEL sel, uintptr_t lr)
{
if (pthread_main_np()) {
push_call_record(object_getClass(self), sel);
}
pushLRRegister(lr);
}
uintptr_t hook_objc_msgSend_after(BOOL is_super)
{
if (pthread_main_np()) {
pop_call_record(is_super);
}
return popLRRegister();
}
.macro CALL_HOOK_BEFORE
BACKUP_REGISTERS
mov x2, lr
bl _hook_objc_msgSend_before
RESTORE_REGISTERS
.endmacro
.macro CALL_HOOK_AFTER
BACKUP_REGISTERS
mov x0, #0x0
bl _hook_objc_msgSend_after
mov lr, x0
RESTORE_REGISTERS
.endmacro
修复
既然我们在hook objc_msgSend的时候已经开辟了栈帧,那我们就没有必要使用线程私有存储来进行LR寄存器的保存和恢复。直接在释放栈帧的时候使用ldr lr, [fp, #0x08]恢复LR寄存器便可。至此try catch崩溃问题被解决掉。
crash解决后,方法耗时监测工具失效
问题
把try catch崩溃问题解决掉后,发现方法耗时监测工具失效,调试发现我们用来记录方法调用的队列(方法调用之前,入队列,方法调用完成之后,出队列)永远不会为空导致监测工具失效。
原因:
其实这个问题的原因和上面提到的异常被捕获后,程序的调用和调出不匹配导致。
解决:
最初想法:
最初是想给方法的调用打tag,在hook_objc_msgSend_before时生成个tag并返回给汇编代码,调用hook_objc_msgSend_after时传递tag进行匹配。但是仔细分析后发现无法通过汇编来进行tag传递,会破坏线程调用栈空间的布局。
换个思路解决:
hook obj_end_catch方法,在异常处理结束后,对我们记录的调用队列和调用栈进行清空。在异常发生,异常被捕获后,仍然可以监测代码执行的方法耗时。
void hook_end_catch(void)
{
origin_objc_end_catch();
if (pthread_main_np()) {
clean();
}
}
void clean(void)
{
if (_mainThreadStack) {
if (_mainThreadStack->stack) {
free(_mainThreadStack->stack);
}
free(_mainThreadStack);
_mainThreadStack = NULL;
}
if (_mainThreadQueue) {
if (_mainThreadQueue->records) {
free(_mainThreadQueue->records);
}
free(_mainThreadQueue);
_mainThreadQueue = NULL;
}
ignore_call_num = 0;
}