更多精彩内容,欢迎观看:
我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash(上)
六、总结根因
通过上述分析推演,iOS 16 键盘 Crash 的根因已查明,即-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法内执行-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
尝试加锁失败后,不return
继续向下执行读写不安全内存以及解锁,导致存在锁失效的情况,使得UIKeyboardTaskQueue
成员变量_deferredTasks
数组在多线程下出现并发添加UIKeyboardTaskEntry
实例而引起野指针,导致最终 Crash。
注:该根因除了导致数组读写异常而 Crash,也可能导致其他变量的状态不一致性,只是不一定表现为 Crash 而已,建议用本文方案修复。
解决方案(App 内置补丁源码)
明确根因后,解决方案就比较明确了,写一个 App 内置补丁代码使得-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法内执行-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
尝试加锁失败后,正常return
即可。补丁方案有两个:
- 重写
-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法。在原汇编基础上新增一条指令,即在bl _objc_msgSend$tryLockWhenReadyForMainThread
后添加一条汇编指令cbz w0, return_label
(return_label
对应源码return
对应的汇编指令地址),如失败则return
。但该方案涉及的原汇编指令较多,有 95 条汇编指令(见下文附件中 iOS 系统汇编),容易踩坑。 - 重写
-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
方法。在该方法内如加锁失败则模拟两次return
,回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]
的上一个函数栈,改造的汇编指令较少,安全性较好,也确认了除-[UIKeyboardTaskQueue continueExecutionOnMainThread]
调用外,无其他方法调用-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
。
最终,支付宝 App 基于稳定性的考虑,采用第 2 种补丁方案修复键盘 Crash。
补丁原理
图 14 修复键盘 Crash 的补丁原理
在-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
实现以下逻辑:
- 如加锁成功,则
return
1 次,返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法的下一条指令继续执行; - 如加锁失败,则模拟
return
2 次,返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]
的函数栈的上一层函数的地址继续执行,也就是模拟了从-[UIKeyboardTaskQueue continueExecutionOnMainThread]
中执行return
操作。
源码return
语句,对应汇编的 4 步:
- 恢复
fp
和lr
寄存器。fp
(也称x29
)记录当前帧的内存地址,lr
(也称x30
)记录从当前函数返回时跳转到哪个地址继续执行。运行时就是通过fp
和lr
寄存器,输出线程的函数栈的。如 Crash 函数栈,或从lldb
的bt
输出的函数栈; - 恢复
callee-saved
寄存器。即x19-x28
的寄存器,try-catch
的实现就涉及该类寄存器,一般按需执行; - 恢复
sp
寄存器。sp
记录当前帧的栈顶地址,,当前函数的局部变量所在的内存地址就在(fp
,sp
]之间; - 执行
ret
指令。执行ret
指令后,pc
就指向lr
寄存器的值,然后继续执行;
本文补丁方案的原理中,tryLock 失败时就是通过:恢复fp
和lr
寄存器 + 恢复callee-saved
寄存器 + 恢复sp
寄存器 + 再次恢复fp
和lr
寄存器 + 再次恢复callee-saved
寄存器 + 再次恢复sp
寄存器 +ret
指令 来模拟在-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
方法内return
2 次直接返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]
的函数栈的上一层函数的。
补丁实现
有两部分组成:
- 重写方法:对应 fix_UIKeyboardTaskQueue.S 文件;
- Hook 入口:对应 fix_UIKeyboardTaskQueue.m 文件;
重写方法
重写-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
方法实现,对应下文附件中补丁源码的 fix_UIKeyboardTaskQueue.S 文件。
图 15 重写 -[UIKeyboardTaskQueue tryLockWhenReadyForMainThread] 方法实现
Hook 入口
借助+ (void)load
方法在 App 启动时执行的特点实现对-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
方法的 Hook,仅在 iOS 16 的 Arm64 架构上生效,对应下文附件中补丁源码的 fix_UIKeyboardTaskQueue.m 文件。
图 16 Hook 入口的代码
方案效果
于 2023.8.25 在支付宝 App 近期版本 10.5.16.6000 上全量开启解决方案的开关后,该版本上的 Crash 日 PV 已经降到 0 了。
图 17 支付宝 App 近期版本 10.5.16.6000 上键盘 Crash 日 PV
同时,支付宝 App 的全量版本(包括所有历史版本)的键盘 Crash 日 PV 下降了近 90%,随着更多用户升级到支付宝 App 最新版本,预计会降到个位数。
图 18 方案上线后键盘 Crash 日 PV 明显下降的趋势图
最终该方案由验收人确认有效,键盘 Crash 已解决,揭榜挑战成功,附上一张挑战成功捷报图收个尾。
图 19 蚂蚁内部的技术英雄榜捷报
附件
1、补丁源码
补丁源码包括两部分:fix_UIKeyboardTaskQueue.S 和 fix_UIKeyboardTaskQueue.m。使用时将该两文件直接内置在 App 中即可,也可在 App 启动时加开关控制 Hook 入口的时机。
//// fix_UIKeyboardTaskQueue.S// fix_UIKeyboardTaskQueue//// Created by Alipay on 2023/8/10.// Copyright © 2023 Alipay. All rights reserved.///**原实现-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]:ldr x0, [x0, #0x10]mov x2, #0x0b "_objc_msgSend$tryLockWhenCondition:"*/// 重写实现.section__TEXT,__cstring,cstring_literalstryLockWhenCondition.str: .asciz"tryLockWhenCondition:".text.align4.global_fix_UIKeyboardTaskQueue_tryLockWhenReadyForMainThread.cfi_startproc_fix_UIKeyboardTaskQueue_tryLockWhenReadyForMainThread: stpx20, x19, [sp, #-0x20]!stpx29, x30, [sp, #0x10] addx29, sp, #0x10movx19, x0 ; selfadrpx0, tryLockWhenCondition.str@PAGEaddx0, x0, tryLockWhenCondition.str@PAGEOFFbl_sel_registerName ; @selector(tryLockWhenCondition:) movx1, x0ldrx0, [x19, #0x10] ; _lockmovx2, #0x0bl_objc_msgSend ; -[_locktryLockWhenCondition:0] ldpx29, x30, [sp, #0x10] ; 恢复fp和lrldpx20, x19, [sp], #0x20 ; 恢复callee-saved寄存器、并恢复spcbzx0, 1f// 如tryLock成功,则继续执行-[UIKeyboardTaskQueue continueExecutionOnMainThread]的指令ret// 如tryLock失败,则模拟从-[UIKeyboardTaskQueue continueExecutionOnMainThread] return,不再继续执行1: ldpx29, x30, [sp, #0x20] ; 恢复fp和lrldpx20, x19, [sp, #0x10] ; 恢复callee-saved寄存器addsp, sp, #0x30 ; 恢复spautibsp ; AuthenticateInstructionaddressret.cfi_endproc
//// fix_UIKeyboardTaskQueue.m// fix_UIKeyboardTaskQueue//// Created by Alipay on 2023/9/4.//@interfacefix_UIKeyboardTaskQueue : NSObject@end@implementationfix_UIKeyboardTaskQueue+ (void)load { externBOOLfix_UIKeyboardTaskQueue_tryLockWhenReadyForMainThread(idself, SELselector); if (@available(iOS16.0, *)) { NSString*systemVersion= [[UIDevicecurrentDevice] systemVersion]; NSArray*verInfos= [systemVersioncomponentsSeparatedByString:@"."]; NSUIntegercount= [verInfoscount]; if (count>=2) { if ([verInfos[0] isEqualToString:@"16"]) { class_replaceMethod(objc_getClass("UIKeyboardTaskQueue"), sel_getUid("tryLockWhenReadyForMainThread"), (IMP)fix_UIKeyboardTaskQueue_tryLockWhenReadyForMainThread, "B16@0:8"); } } } } @end
2、Demo 关键源码
//// ViewController.m// UIKeyboardTaskQueueDemo//// Created by Alipay on 2023/8/30.//// #import <DebugKit/DebugKit.h>@interfaceViewController () @end@implementationViewController { NSMutableArray*_tasks; NSMutableArray*_deferredTasks; NSConditionLock*_lock; } - (void)viewDidLoad { [superviewDidLoad]; // 输出UIKeyboardTaskQueue的所有实例方法和类方法// dk_print_all_methods_of_class("UIKeyboardTaskQueue");// 输出UIKeyboardTaskQueue的所有property// dk_print_all_properties("UIKeyboardTaskQueue");// 输出UIKeyboardTaskQueue的所有ivars// dk_print_class_all_ivars("UIKeyboardTaskQueue");UITextView*textView= [[UITextViewalloc] initWithFrame:self.view.bounds]; [self.viewaddSubview:textView]; [selftest_crash]; [selftest_ok]; } - (void)unlock { [_lockunlockWithCondition:0]; } - (void)lock { [_locklock]; } - (BOOL)tryLock { return [_locktryLockWhenCondition:0]; } - (void)addEntry_ok { [selflock]; [_deferredTasksaddObject:[[NSObjectalloc] init]]; NSLog(@"add, %lu", _deferredTasks.count); [selfunlock]; } - (void)removeEntry_crash { [selftryLock]; if (_deferredTasks.count) { [_tasksaddObject:[_deferredTasksobjectAtIndex:0]]; if (_deferredTasks.count) { [_deferredTasksremoveObjectAtIndex:0]; NSLog(@"remove, %lu", _deferredTasks.count); // NSLog(@"%@, %lu -[_deferredTasks removeObjectAtIndex:0]", [NSThread currentThread], _deferredTasks.count); } } [selfunlock]; } - (void)removeEntry_ok { if (![selftryLock]) return; if (_deferredTasks.count) { [_tasksaddObject:[_deferredTasksobjectAtIndex:0]]; if (_deferredTasks.count) { [_deferredTasksremoveObjectAtIndex:0]; NSLog(@"remove, %lu", _deferredTasks.count); // NSLog(@"%@, %lu -[_deferredTasks removeObjectAtIndex:0]", [NSThread currentThread], _deferredTasks.count); } } [selfunlock]; } - (void)test_crash { // init_tasks= [NSMutableArrayarray]; _deferredTasks= [NSMutableArrayarray]; _lock= [[NSConditionLockalloc] initWithCondition:0]; for (inti=0; i<10000; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [selfaddEntry_ok]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // dispatch_async(dispatch_get_main_queue(), ^{ [selfremoveEntry_crash]; // }); }); } } - (void)test_ok { // init_tasks= [NSMutableArrayarray]; _deferredTasks= [NSMutableArrayarray]; _lock= [[NSConditionLockalloc] initWithCondition:0]; for (inti=0; i<1000; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [selfaddEntry_ok]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [selfremoveEntry_ok]; }); } } @end
3、脚本源码
TEXT_FILE="$1"; CLASS_NAME="$2"; cat"$TEXT_FILE"|tr'\n''&'|sed's/&\-\[/\n\-\[/g'|grep"^\-\[$CLASS_NAME "|tr'&''\n';
4、iOS 系统汇编(关键方法)
将 iOS 16.6 的 iPhone 12 Pro Max(Hardware Mode: iPhone13 4)设备连接到 Xcode 后,按如下操作可获取到 UIKeyboardTaskQueue 类的实现汇编,即UIKitCore_20G75_arm64e_TEXT.txt 文件。
otool-s__TEXT__text-v~/Library/Developer/Xcode/iOS\DeviceSupport/16.6\\(20G75\)\arm64e/Symbols/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore>~/Desktop/UIKitCore_20G75_arm64e_TEXT.txt./fetch_class_text_from_all.sh~/Desktop/UIKitCore_20G75_arm64e_TEXT.txtUIKeyboardTaskQueue>~/Desktop/UIKeyboardTaskQueue_20G75_arm64e_TEXT.txt
-[UIKeyboardTaskQueuecontinueExecutionOnMainThread]: 0000000189466fd0pacibsp0000000189466fd4subsp, sp, #0x300000000189466fd8stpx20, x19, [sp, #0x10] 0000000189466fdcstpx29, x30, [sp, #0x20] 0000000189466fe0addx29, sp, #0x200000000189466fe4movx19, x00000000189466fe8bl0x18c9df5e00000000189466feccmpw0, #0x10000000189466ff0b.ne0x1894670240000000189466ff4movx0, x190000000189466ff8bl_objc_msgSend$tryLockWhenReadyForMainThread0000000189466ffcldrx8, [x19, #0x28] 0000000189467000cbzx8, 0x1894670580000000189467004ldrx8, [x19, #0x30] 0000000189467008cbzx8, 0x1894670b4000000018946700cbl0x18c9df2f00000000189467010strx0, [sp, #0x8] 0000000189467014ldrx8, [x19, #0x30] 0000000189467018strxzr, [x19, #0x30] 000000018946701cbl0x18c9df1500000000189467020b0x1894670ac0000000189467024adrpx8, -26465 ; 0x182d060000000000189467028addx2, x8, #0xe19000000018946702cmovx0, x190000000189467030movx3, #0x00000000189467034movw4, #0x00000000189467038ldpx29, x30, [sp, #0x20] 000000018946703cldpx20, x19, [sp, #0x10] 0000000189467040addsp, sp, #0x300000000189467044autibsp0000000189467048eorx16, x30, x30, lsl#1000000018946704ctbzx16, #0x3e, 0x1894670540000000189467050brk#0xc4710000000189467054b"_objc_msgSend$performSelectorOnMainThread:withObject:waitUntilDone:"0000000189467058ldrx0, [x19, #0x18] 000000018946705cbl_objc_msgSend$count0000000189467060cbzx0, 0x1894670b80000000189467064adrpx8, 333019 ; 0x1da9420000000000189467068ldrx0, [x8, #0x500] 000000018946706cbl0x18c9dee300000000189467070movx2, x190000000189467074bl"_objc_msgSend$initWithExecutionQueue:"0000000189467078movx20, x0000000018946707cmovx0, x190000000189467080movx2, x200000000189467084bl"_objc_msgSend$setExecutionContext:"0000000189467088bl0x18c9df0a0000000018946708cldrx0, [x19, #0x18] 0000000189467090movx2, #0x00000000189467094bl"_objc_msgSend$objectAtIndex:"0000000189467098bl0x18c9deec0000000018946709cstrx0, [sp, #0x8] 00000001894670a0ldrx0, [x19, #0x18] 00000001894670a4movx2, #0x000000001894670a8bl"_objc_msgSend$removeObjectAtIndex:"00000001894670acldrx0, [sp, #0x8] 00000001894670b0b0x1894670b800000001894670b4movx0, #0x000000001894670b8strx0, [sp, #0x8] 00000001894670bcbl_objc_msgSend$originatingStack00000001894670c0bl0x18c9deec000000001894670c4movx20, x000000001894670c8movx0, x1900000001894670ccmovx2, x2000000001894670d0bl"_objc_msgSend$setActiveOriginator:"00000001894670d4bl0x18c9df0a000000001894670d8movx0, x1900000001894670dcbl_objc_msgSend$unlock00000001894670e0ldrx1, [sp, #0x8] 00000001894670e4ldrbw20, [x19, #0x8] 00000001894670e8movw8, #0x100000001894670ecstrbw8, [x19, #0x8] 00000001894670f0ldrx2, [x19, #0x28] 00000001894670f4cbzx1, 0x18946710800000001894670f8movx0, x100000001894670fcbl"_objc_msgSend$execute:"0000000189467100ldrx1, [sp, #0x8] 0000000189467104b0x18946710c0000000189467108cbzx2, 0x189467130000000018946710cstrbw20, [x19, #0x8] 0000000189467110ldpx29, x30, [sp, #0x20] 0000000189467114ldpx20, x19, [sp, #0x10] 0000000189467118addsp, sp, #0x30000000018946711cautibsp0000000189467120eorx16, x30, x30, lsl#10000000189467124tbzx16, #0x3e, 0x18946712c0000000189467128brk#0xc471000000018946712cb0x18c9df0600000000189467130ldrx0, [x19, #0x20] 0000000189467134bl_objc_msgSend$count0000000189467138ldrx1, [sp, #0x8] 000000018946713ccbzx0, 0x18946710c0000000189467140movx0, x190000000189467144bl_objc_msgSend$performDeferredTaskIfIdle0000000189467148b0x189467100-[UIKeyboardTaskQueuetryLockWhenReadyForMainThread]: 0000000189467738ldrx0, [x0, #0x10] 000000018946773cmovx2, #0x00000000189467740b"_objc_msgSend$tryLockWhenCondition:"-[UIKeyboardTaskQueueperformDeferredTaskIfIdle]: 0000000189c814b4pacibsp0000000189c814b8stpx20, x19, [sp, #-0x20]!0000000189c814bcstpx29, x30, [sp, #0x10] 0000000189c814c0addx29, sp, #0x100000000189c814c4movx19, x00000000189c814c8bl_objc_msgSend$lock0000000189c814ccmovx0, x190000000189c814d0bl_objc_msgSend$promoteDeferredTaskIfIdle0000000189c814d4movx0, x190000000189c814d8bl_objc_msgSend$unlock0000000189c814dcmovx0, x190000000189c814e0ldpx29, x30, [sp, #0x10] 0000000189c814e4ldpx20, x19, [sp], #0x200000000189c814e8autibsp0000000189c814eceorx16, x30, x30, lsl#10000000189c814f0tbzx16, #0x3e, 0x189c814f80000000189c814f4brk#0xc4710000000189c814f8b_objc_msgSend$continueExecutionOnMainThread-[UIKeyboardTaskQueuepromoteDeferredTaskIfIdle]: 0000000189c814fcpacibsp0000000189c81500subsp, sp, #0x300000000189c81504stpx20, x19, [sp, #0x10] 0000000189c81508stpx29, x30, [sp, #0x20] 0000000189c8150caddx29, sp, #0x200000000189c81510ldrx8, [x0, #0x28] 0000000189c81514cbzx8, 0x189c815280000000189c81518ldpx29, x30, [sp, #0x20] 0000000189c8151cldpx20, x19, [sp, #0x10] 0000000189c81520addsp, sp, #0x300000000189c81524retab0000000189c81528movx19, x00000000189c8152cldrx0, [x0, #0x20] 0000000189c81530bl_objc_msgSend$count0000000189c81534cbzx0, 0x189c815180000000189c81538ldrx0, [x19, #0x20] 0000000189c8153cmovx2, #0x00000000189c81540bl"_objc_msgSend$objectAtIndex:"0000000189c81544bl0x18c9deec00000000189c81548movx2, x00000000189c8154cstrx0, [sp, #0x8] 0000000189c81550ldrx0, [x19, #0x18] 0000000189c81554bl"_objc_msgSend$addObject:"0000000189c81558ldrx0, [x19, #0x20] 0000000189c8155cmovx2, #0x00000000189c81560bl"_objc_msgSend$removeObjectAtIndex:"0000000189c81564ldrx0, [sp, #0x8] 0000000189c81568ldpx29, x30, [sp, #0x20] 0000000189c8156cldpx20, x19, [sp, #0x10] 0000000189c81570addsp, sp, #0x300000000189c81574autibsp0000000189c81578eorx16, x30, x30, lsl#10000000189c8157ctbzx16, #0x3e, 0x189c815840000000189c81580brk#0xc4710000000189c81584b0x18c9df050-[UIKeyboardTaskQueueaddDeferredTask:]: 0000000189c815fcpacibsp0000000189c81600subsp, sp, #0x300000000189c81604stpx20, x19, [sp, #0x10] 0000000189c81608stpx29, x30, [sp, #0x20] 0000000189c8160caddx29, sp, #0x200000000189c81610movx19, x00000000189c81614bl0x18c9df2000000000189c81618movx20, x00000000189c8161cmovx0, x190000000189c81620bl_objc_msgSend$lock0000000189c81624adrpx8, 330945 ; 0x1da9420000000000189c81628ldrx0, [x8, #0x510] 0000000189c8162cbl0x18c9dee300000000189c81630movx2, x200000000189c81634bl"_objc_msgSend$initWithTask:"0000000189c81638strx0, [sp, #0x8] 0000000189c8163cbl0x18c9df0a00000000189c81640ldrx0, [x19, #0x20] 0000000189c81644ldrx2, [sp, #0x8] 0000000189c81648bl"_objc_msgSend$addObject:"0000000189c8164cmovx0, x190000000189c81650bl_objc_msgSend$unlock0000000189c81654movx0, x190000000189c81658bl_objc_msgSend$continueExecutionOnMainThread0000000189c8165cldrx0, [sp, #0x8] 0000000189c81660ldpx29, x30, [sp, #0x20] 0000000189c81664ldpx20, x19, [sp, #0x10] 0000000189c81668addsp, sp, #0x300000000189c8166cautibsp0000000189c81670eorx16, x30, x30, lsl#10000000189c81674tbzx16, #0x3e, 0x189c8167c0000000189c81678brk#0xc4710000000189c8167cb0x18c9df050
🔗 相关链接
[2] Arm64 汇编指令集说明:https://documentation-service.arm.com/static/6023d5512cb3723f20208db2
[3] NSConditionLock 条件状态锁:https://developer.apple.com/documentation/foundation/nsconditionlock/