汇编分析一次系统控件系统栈的crash

系统控件和系统堆栈的crash初看,总以为不好解决,本文通过一步步推导来分析定位,最终找到crash是应用堆栈触发的

一、问题描述

最新线上新版本遇到了一个大规模的crash,也不太好复现,crash堆栈大概如下

0 CoreFoundation 0x00000001819f6d8c ___exceptionPreprocess + 228
1 libobjc.A.dylib 0x0000000180bb05ec objc_exception_throw + 44
2 CoreFoundation 0x00000001819f6c6c -[NSException initWithCoder:]
3 UIKit 0x000000018bfe3134 -[UIPageViewController _validatedViewControllersForTransitionWithViewControllers:animated:] + 588
4 UIKit 0x000000018bfe3cbc -[UIPageViewController _setViewControllers:withCurlOfType:fromLocation:direction:animated:notifyDelegate:completion:] + 568
5 UIKit 0x000000018bfe6da0 -[UIPageViewController _handlePanGesture:] + 292
6 UIKit 0x000000018b7e26e8 -[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 64
7 UIKit 0x000000018bd4f3b4 __UIGestureRecognizerSendTargetActions + 124
8 UIKit 0x000000018b944e38 __UIGestureRecognizerSendActions + 320
9 UIKit 0x000000018b7e1740 -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 764
10 UIKit 0x000000018bd40bd4 __UIGestureEnvironmentUpdate + 1096
11 UIKit 0x000000018b7db4d8 -[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 404
12 UIKit 0x000000018b7db010 -[UIGestureEnvironment _updateGesturesForEvent:window:] + 276
13 UIKit 0x000000018b7da874 -[UIWindow sendEvent:] + 3132

23 UIKit 0x000000018b8d9758 UIApplicationMain + 228
24 mttlite 0x0000000102a843f4 main (main.mm:35)
25 libdyld.dylib 0x000000018134dfc0 _start + 4
The number of view controllers provided (0) doesn't match the number required (2) for the requested transition

二、问题分析

2.1开始

咋一看,这个和UITableView等类似的the number of section after updated(xxx) does not match before(xxx) ... 类似,以为是不是命中了系统的什么bug,但是看了一圈代码也没找到有特殊的逻辑;

再观察所有crash记录,发现出现问题时必有如下handlePanGesture的操作,难道这里有什么系统的bug吗?

而且由于这个问题很难复现到,所以一时就无从下手了。

5 UIKit 0x000000018bfe6da0 -[UIPageViewController _handlePanGesture:] + 292
2.2分析问题

既然复现也不好复现,那就看从crash里找信息吧

3  UIKit                          0x000000018bfe3134 -[UIPageViewController _validatedViewControllersForTransitionWithViewControllers:animated:] +  588

问题发送在[UIPageViewController _validataedViewControllersForTranstionWithViewController:animated:] 函数的段内偏移588的地方,
添加符号断点,找到+588的代码

汇编分析一次系统控件系统栈的crash_第1张图片
请在这里填写图片描述

+588只做了一次赋值操作,所以应该是+584的一次函数调用触发了exception,但是正常路径里又不会走进去+584的代码;

bl 0x18c0452fc

这是一个有返回值的函数调用,返回值由x0寄存器存储;也就是在这个函数执行过程中发生了exception,

继续搜索+584的符号地址0x18c0452fc,发现有若干个地方都会去调用这个方法;一步步修改寄存器值,尝试触发走到这个bl指令去
回溯代码,发现修改+124代码里的w2寄存器值为0就能触发走到+248去,此时就能触发执行bl 0x18c0452fc,代码如下

    0x18fac6f54 <+108>: mov    x23, x0
    0x18fac6f58 <+112>: orr    w20, wzr, #0x1
    0x18fac6f5c <+116>: cbnz   x23, 0x18fac7138          ; <+592>
    0x18fac6f60 <+120>: b      0x18fac70f4               ; <+524>
    0x18fac6f64 <+124>: cbz    w20, 0x18fac6fe0          ; <+248>
    0x18fac6f68 <+128>: adrp   x8, 158039
    
    0x18fac6fe0 <+248>: adrp   x8, 158039
    0x18fac6fe4 <+252>: ldrsw  x26, [x8, #0xc60]
    0x18fac6fe8 <+256>: ldr    x2, [x22, x26]
    0x18fac6fec <+260>: adrp   x8, 158011
    0x18fac6ff0 <+264>: ldr    x1, [x8, #0x360]
    0x18fac6ff4 <+268>: mov    x0, x22
    0x18fac6ff8 <+272>: bl     0x18c0452fc

只要走进去了这个函数,那么就应该会必挂

那接着看下这个函数干了什么?
step into进0x18c0452fc的函数调用,发现如下

->  0x18c0452fc: b      0x1846a8900               ; objc_msgSend
    0x18c045300: b      0x184684ba4               ; __cxa_allocate_exception
    0x18c045304: b      0x184684cf4               ; __cxa_begin_catch
    0x18c045308: b      0x184685ab8               ; __cxa_call_unexpected
    0x18c04530c: b      0x184684d8c               ; __cxa_end_catch
    0x18c045310: b      0x1846a3284               ; objc_lookUpClass
    0x18c045314: b      0x1846a8b00               ; objc_msgSendSuper2
    0x18c045318: b      0x1846b0130               ; objc_autorelease

其中b 0x1846a8900函数调用是做了一次验证,接着就会走向抛exception流程

General Purpose Registers:
        x0 = 0x000000010d28d000
        x1 = 0x000000018ff3a18d  "_validRangeForPresentationOfViewControllersWithSpineLocation:"

从而触发了这个crash

2018-07-09 13:54:18.949648+0800 mttlite[55265:6636239] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'The number of provided view controllers (2) doesn't match the number required (1) for the requested spine location (UIPageViewControllerSpineLocationMin)'
*** First throw call stack:
(0x1854dad8c 0x1846945ec 0x1854dac6c 0x18fac70a8 0x18fac7cbc 0x18facada0 0x18f2c66e8 0x18f8333b4 0x18f428e38 0x18f2c5740 0x18f824bd4 0x18f2bf4d8 0x18f2bf010 0x18f2be874 0x18f2bd1d0 0x18fa9ed1c 0x18faa12c8 0x18faa1628 0x18fa9a368 0x185483404 0x185482ce0 0x18548079c 0x1853a0da8 0x187385020 0x18f3bd758 0x1044ed90c 0x184e31fc0)
libc++abi.dylib: terminating with uncaught exception of type NSException

好的思路已经有了,就是要避免_validataedViewControllersForTranstionWithViewController:animated: 函数触发bl 0x18c0452fc指令的执行;

2.3分析路径

那如何避免bl 0x18c0452fc的执行呢?
那就从头开始过一遍validataedViewControllersForTranstionWithViewController:animated: 函数吧

UIKit`-[UIPageViewController _validatedViewControllersForTransitionWithViewControllers:animated:]:
    0x18fac6ee8 <+0>:   sub    sp, sp, #0x70             ; =0x70 
    0x18fac6eec <+4>:   stp    x26, x25, [sp, #0x20]
    0x18fac6ef0 <+8>:   stp    x24, x23, [sp, #0x30]
    0x18fac6ef4 <+12>:  stp    x22, x21, [sp, #0x40]
    0x18fac6ef8 <+16>:  stp    x20, x19, [sp, #0x50]
    0x18fac6efc <+20>:  stp    x29, x30, [sp, #0x60]
    0x18fac6f00 <+24>:  add    x29, sp, #0x60            ; =0x60 
    0x18fac6f04 <+28>:  mov    x20, x3
    0x18fac6f08 <+32>:  mov    x22, x0
    0x18fac6f0c <+36>:  mov    x0, x2
    0x18fac6f10 <+40>:  bl     0x18c04532c

前面的代码调用好像是为了栈入栈,后面3行,明显是为了进行新的objc_msgSend,像x0寄存器发消息0x18c04532c
去除x0发现,x0是一个数组,返回了所有的viewcontroller,在仔细看下面的代码,都是在跳转,并最终尽量避免跳转到0x18c04532c异常处理去,而最后函数的返回值依旧是x0的原始值,也就是这个函数的本质就是再对x0的参数做校验,如果校验成功,返回x0,否则抛出exception
从crash信息中可以看到,出现异常时x0的个数是0,而不是预期的2,那么x0的值是由谁修改的呢?回溯到上层调用,找到了源头

    0x18fac7ca4 <+544>:  adrp   x8, 158010
    0x18fac7ca8 <+548>:  ldr    x1, [x8, #0x3e0]
    0x18fac7cac <+552>:  orr    w3, wzr, #0x1
    0x18fac7cb0 <+556>:  mov    x0, x25
    0x18fac7cb4 <+560>:  mov    x2, x27
->  0x18fac7cb8 <+564>:  bl     0x18c0452fc
    0x18fac7cbc <+568>:  mov    x29, x29
    0x18fac7cc0 <+572>:  bl     0x18c045338

+564是发消息执行validataedViewControllersForTranstionWithViewController:animated: 函数,x27是实际传入函数的数组,如果x27这个时候为0,则会必然导致crash的发生;
那么x27是谁修改的?继续往上查找发现x27是由函数调用[UIPageViewController _setViewControllers:withCurlOfType: 调进来的

汇编分析一次系统控件系统栈的crash_第2张图片
请在这里填写图片描述

回溯到调用栈[UIPageViewController _handlePanGesture:]里,也就是这里调用[UIPageViewController _setViewControllers:withCurlOfType: 是传入的数组元素个数为0

    0x18facad30 <+180>:  mov    x0, x20
->  0x18facad34 <+184>:  bl     0x18c0452fc
    0x18facad38 <+188>:  mov    x29, x29
    0x18facad3c <+192>:  bl     0x18c045338
    0x18facad40 <+196>:  mov    x19, x0

网上回溯,发现[UIPageViewController _setViewControllers:withCurlOfType: 的参数viewcontrollers是由0x18c0452fc函数调用修改的,找到调用发现,这里做了一个发消息

(lldb) re read -a
General Purpose Registers:
        x0 = 0x000000010e27ec00
        x1 = 0x000000018ff3a65a  "_incomingViewControllersForGestureDrivenCurlInDirection:"
(lldb) po 0x000000010e27ec00

所以问题解决了,执行[UIPageViewController _incomingViewControllersForGestureDrivenCurlInDirection:]返回了实际的viewcontrollers数组,如果这里返回了空数组,那进入[UIPageViewController _incomingViewControllersForGestureDrivenCurlInDirection:]看看吧

2.4定位原因

经过前面的操作,已经初步定位到了是[UIPageViewController _incomingViewControllersForGestureDrivenCurlInDirection:] 该函数最终返回了空数组,且在handlePanGestures:期间,改函数会被调用若干次,那么就看下这个函数到底调用了什么来返回了viewcontrollers数组了?

依旧从返回值开始分析

    0x18faca1c0 <+1048>: mov    x0, x21
    0x18faca1c4 <+1052>: ldp    x29, x30, [sp, #0x60]
    0x18faca1c8 <+1056>: ldp    x20, x19, [sp, #0x50]
    0x18faca1cc <+1060>: ldp    x22, x21, [sp, #0x40]
    0x18faca1d0 <+1064>: ldp    x24, x23, [sp, #0x30]
    0x18faca1d4 <+1068>: ldp    x26, x25, [sp, #0x20]
    0x18faca1d8 <+1072>: ldp    x28, x27, [sp, #0x10]
    0x18faca1dc <+1076>: add    sp, sp, #0x70             ; =0x70 
    0x18faca1e0 <+1080>: b      0x18c04531c

x0的值是由于x21传入的,也就是最终的函数返回值是由x21寄存器决定的,那就分析x21的相关代码,最终找到了如下疑似代码

    0x18fac9ff0 <+584>:  mov    x0, x20
    0x18fac9ff4 <+588>:  mov    x1, x23
    0x18fac9ff8 <+592>:  mov    x2, x24
->  0x18fac9ffc <+596>:  bl     0x18c0452fc
    0x18faca000 <+600>:  mov    x29, x29
    0x18faca004 <+604>:  bl     0x18c045338
    0x18faca008 <+608>:  mov    x22, x0

执行完bl 0x18c0452fc 后就获取到了数组

(lldb) re read -a
General Purpose Registers:
        x0 = 0x000000010e27ec00
        x1 = 0x000000018ff3a510  "_viewControllerAfterViewController:"

即实际的viewControllers是由[UIPageViewController _viewControllerAfterViewController:]函数返回的,好了,问题差不多找到了,最终[UIPageViewController _viewControllerAfterViewController:]是调用了UIPageViewControllerDatasource的

- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController;

该方法返回了nil,问题也找到了,的确业务实现这个datasource时存在先返回nil再重设值的问题。
但是为什么没有必挂呢?
因为这个上层调用[UIPageViewController _incomingViewControllersForGestureDrivenCurlInDirection:]函数会在handlePanGesture时多次调用,而这里是多线程异步的会写setViewControllers:所以只有个别情况才能出现这个crash;

    [self.pageCurlDelegate requestPageViewFor:novelLayerView toNextPage:bToNextPage complete:blockFunc];
    bHaveReturnNil = YES;
    _countPageLoading--;
    
    if(!targetViewController&&bToNextPage)
    {
    //此处存在先返回nil,再异步回调[self.pageViewController setViewControllers:@[targetViewController] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];的问题,如果时序过久,则会触发crash;所以解决办法是永远不返回nil
    }
    return targetViewController;

三、总结

本质上是由于UIPageViewController的dataSource存在异步返回nil的情况,所以导致了该问题不会稳定必现;这种block设计嵌套的逻辑,搞的可能有时返回nil,但立即同步设置非nil的viewcontrollers;但有时因为线程切换导致需要延时调用,一旦超过UIPageViewController的某个条件,就触发了nil的crash了;

你可能感兴趣的:(汇编分析一次系统控件系统栈的crash)