7

UIKit解剖(-)逆向UITableViewController分析Bug

 3 years ago
source link: http://satanwoo.github.io/2017/08/06/UITableViewController/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

UIKit解剖(-)逆向UITableViewController分析Bug

之前在做XXXSDK的时候,我hook的UITableViewsetDelegate:方法。整个SDK在接入手淘、天猫以及闲鱼等其他App的时候都没啥问题。

上周,UC的同学突然找到说,给我说了如下图所示的问题:

商业保密,不显示了

卧槽,这下我就懵逼了,看样子是把整个rowHeight给Hook坏了,那这是为什么呢?

从开发UITableView的正向角度来说:我们一般都需要给其提供一个必选的UITableViewDataSource和一个可选的UITableViewDelegate,其中,涉及到高度的是如下这个API:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

有人说可以直接通过tableview.rowHeight设置高度,但是对于不同cell不同高度的动态需求,但是这里我们暂不提这种分支情况。

通过UC同学的协助,我们发现了如下输出:

delegate.png?raw=true

通过输出不难发现,是最后的delegate被从对应的UIViewController改成了一个乱七八糟没实现对应heightForRowAtIndexPath方法的对象。

为什么会这样呢?

通过如下图所示的调用栈,

stack.png?raw=true

调用栈最下层是UC同学的代码;

self.tableview = [[xxxTableView alloc] init]

调用栈最上层是我们的一层防护性hook,其代码如下:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Swizzle([UIScrollView class], @selector(init), @selector(swizzled_init));
    });
}

- (instancetype)swizzled_init
{
    id obj = [self swizzled_init];
    UIScrollView *scrollView = (UIScrollView *)obj;
    if (!scrollView.delegate) {
        //scrollView.delegate = [UIScrollViewDelegateDummyStub sharedStub];
    }
    return obj;
}

这段代码是什么作用呢?

我们之前提了UITableViewDelegate不是必需,因此,为了能够抓去所有UITableView的代码,我们会提供一个内置的默认delegate(当时的实现存在bug,没有实现heightForRowAtIndexPath方法)

而且,为了防止我们的delegate覆盖了有delegate的情况,我们还特地做了!scroll.delegate的判断。

按照我们的预期设想,存在两种时间顺序情况:

  1. 我们的init先执行,此时肯定会进入我们设置默认的逻辑;然后当外部代码调用tableview.delegate = xxx的时候,会把我们这个替换掉,不会影响正常的逻辑。
  2. 我们的init后执行(比如某些子类覆盖的情况),那这样的话,当子类已经设置好delegate后,压根不会进入我们的设置逻辑。

然而,就是这一小段看起来无错的代码导致了UC的App出现了文章开头的Bug。

逆向分析UITableViewController

基于10.2的UIKit,我们通过汇编来分析-[UITableViewController setTableView:]的流程:

     // 保存寄存器
->  0x18c84c640 <+0>:   stp    x26, x25, [sp, #-0x50]!
    0x18c84c644 <+4>:   stp    x24, x23, [sp, #0x10]
    0x18c84c648 <+8>:   stp    x22, x21, [sp, #0x20]
    0x18c84c64c <+12>:  stp    x20, x19, [sp, #0x30]
    0x18c84c650 <+16>:  stp    x29, x30, [sp, #0x40]

    // 获取原先UITableViewController的旧tableView
    0x18c84c654 <+20>:  add    x29, sp, #0x40            ; =0x40 
    0x18c84c658 <+24>:  mov    x20, x0
    0x18c84c65c <+28>:  mov    x0, x2
    0x18c84c660 <+32>:  bl     0x1851c8090               ; objc_retain
    0x18c84c664 <+36>:  mov    x19, x0
    0x18c84c668 <+40>:  adrp   x8, 124100
    0x18c84c66c <+44>:  ldr    x1, [x8, #0xd78]
    0x18c84c670 <+48>:  mov    x0, x20
    0x18c84c674 <+52>:  bl     0x1851c2f60               ; objc_msgSend
    0x18c84c678 <+56>:  mov    x29, x29
    0x18c84c67c <+60>:  bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue

    // 比较旧的tableview和新的tableview
    0x18c84c680 <+64>:  mov    x21, x0
    0x18c84c684 <+68>:  cmp    x21, x19

    // 如果两个tableView一致,直接返回
    0x18c84c688 <+72>:  b.eq   0x18c84c7d4               ; <+404>

    // 获取旧的tableView的datasource
    0x18c84c68c <+76>:  adrp   x8, 124074
    0x18c84c690 <+80>:  ldr    x23, [x8, #0x2e0]
    0x18c84c694 <+84>:  mov    x0, x21
    0x18c84c698 <+88>:  mov    x1, x23
    0x18c84c69c <+92>:  bl     0x1851c2f60               ; objc_msgSend
    0x18c84c6a0 <+96>:  mov    x29, x29
    0x18c84c6a4 <+100>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue


    // 从self的成员对象便宜792处取出UIFilteredDataSource
    0x18c84c6a8 <+104>: mov    x22, x0
    0x18c84c6ac <+108>: adrp   x8, 124145
    0x18c84c6b0 <+112>: ldrsw  x8, [x8, #0x7ac]
    0x18c84c6b4 <+116>: ldr    x8, [x20, x8]
    0x18c84c6b8 <+120>: cmp    x22, x20
    0x18c84c6bc <+124>: ccmp   x22, x8, #0x4, ne

    // 如果不一致,把旧的tableview的datasource 置为nil
    0x18c84c6c0 <+128>: b.ne   0x18c84c6d8               ; <+152>
    0x18c84c6c4 <+132>: adrp   x8, 124073
    0x18c84c6c8 <+136>: ldr    x1, [x8, #0x3c0]
    0x18c84c6cc <+140>: mov    x2, #0x0
    0x18c84c6d0 <+144>: mov    x0, x21
    0x18c84c6d4 <+148>: bl     0x1851c2f60               ; objc_msgSend

    // 获取旧的tableview的delegate
    0x18c84c6d8 <+152>: adrp   x8, 124074
    0x18c84c6dc <+156>: ldr    x24, [x8, #0x7d8]
    0x18c84c6e0 <+160>: mov    x0, x21
    0x18c84c6e4 <+164>: mov    x1, x24
    0x18c84c6e8 <+168>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c6ec <+172>: mov    x29, x29
    0x18c84c6f0 <+176>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue
        0x18c84c6f4 <+180>: mov    x25, x0
    0x18c84c6f8 <+184>: bl     0x1851c8150               ; objc_release

    // 判断旧的delegate是不是当前的UITableViewController
    0x18c84c6fc <+188>: cmp    x25, x20
    0x18c84c700 <+192>: b.ne   0x18c84c718               ; <+216>
    0x18c84c704 <+196>: adrp   x8, 124073
    0x18c84c708 <+200>: ldr    x1, [x8, #0x3c8]

    // 如果不是,就把旧的tableview的delegate置为nil
    0x18c84c70c <+204>: mov    x2, #0x0
    0x18c84c710 <+208>: mov    x0, x21
    0x18c84c714 <+212>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c718 <+216>: adrp   x8, 124080
    0x18c84c71c <+220>: ldr    x1, [x8, #0xe80]
    0x18c84c720 <+224>: mov    x0, x21
    0x18c84c724 <+228>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c728 <+232>: mov    x29, x29
    0x18c84c72c <+236>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue
    0x18c84c730 <+240>: mov    x25, x0

    // 将uitableviewcontroller的tableview通过setView:置为新的
    0x18c84c734 <+244>: adrp   x8, 124076
    0x18c84c738 <+248>: ldr    x1, [x8, #0x4b0]
    0x18c84c73c <+252>: mov    x0, x20
    0x18c84c740 <+256>: mov    x2, x19
    0x18c84c744 <+260>: bl     0x1851c2f60               ; objc_msgSend

    // 新的tableview的datasource判断是不是为空,为空通过_applyDefaultDataSourceToTable将其UIFilteredDataSource
    0x18c84c748 <+264>: adrp   x8, 124080
    0x18c84c74c <+268>: ldr    x1, [x8, #0x810]
    0x18c84c750 <+272>: mov    x0, x19
    0x18c84c754 <+276>: mov    x2, x25
    0x18c84c758 <+280>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c75c <+284>: mov    x0, x19
    0x18c84c760 <+288>: mov    x1, x23
    0x18c84c764 <+292>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c768 <+296>: mov    x29, x29
    0x18c84c76c <+300>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue
    0x18c84c770 <+304>: mov    x23, x0
    0x18c84c774 <+308>: bl     0x1851c8150               ; objc_release
    0x18c84c778 <+312>: cbnz   x23, 0x18c84c790          ; <+336>
    0x18c84c77c <+316>: adrp   x8, 124100
    0x18c84c780 <+320>: ldr    x1, [x8, #0xd80]
    0x18c84c784 <+324>: mov    x0, x20
    0x18c84c788 <+328>: mov    x2, x19
    0x18c84c78c <+332>: bl     0x1851c2f60               ; objc_msgSend

    // 新的tableview的delegaate判断是不是为空,为空通过将delegate置为self(即当前的UITableViewController)
    0x18c84c790 <+336>: mov    x0, x19
    0x18c84c794 <+340>: mov    x1, x24
    0x18c84c798 <+344>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c79c <+348>: mov    x29, x29
    0x18c84c7a0 <+352>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue
    0x18c84c7a4 <+356>: mov    x23, x0
    0x18c84c7a8 <+360>: bl     0x1851c8150               ; objc_release


    0x18c84c7ac <+364>: cbnz   x23, 0x18c84c7c4          ; <+388>
    0x18c84c7b0 <+368>: adrp   x8, 124073
    0x18c84c7b4 <+372>: ldr    x1, [x8, #0x3c8]
    0x18c84c7b8 <+376>: mov    x0, x19
    0x18c84c7bc <+380>: mov    x2, x20
    0x18c84c7c0 <+384>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c7c4 <+388>: mov    x0, x25
    0x18c84c7c8 <+392>: bl     0x1851c8150               ; objc_release
    0x18c84c7cc <+396>: mov    x0, x22
    0x18c84c7d0 <+400>: bl     0x1851c8150               ; objc_release
    0x18c84c7d4 <+404>: mov    x0, x21
    0x18c84c7d8 <+408>: bl     0x1851c8150               ; objc_release

    // 恢复寄存器

    0x18c84c7dc <+412>: mov    x0, x19
    0x18c84c7e0 <+416>: ldp    x29, x30, [sp, #0x40]
    0x18c84c7e4 <+420>: ldp    x20, x19, [sp, #0x30]
    0x18c84c7e8 <+424>: ldp    x22, x21, [sp, #0x20]
    0x18c84c7ec <+428>: ldp    x24, x23, [sp, #0x10]
    0x18c84c7f0 <+432>: ldp    x26, x25, [sp], #0x50
    0x18c84c7f4 <+436>: b      0x1851c8150               ; objc_release
  • 一看到adrp, ldr的搭配,基本可以确定是取某个方法进行调用。
  • 看到一大堆的bl objc_retainbl objc_release,不用管,反正都是ARC帮我们自动插入的。
  • 可以看出,当传入给UITableViewController的tableView含有dataSourcedelegate,UITableViewController都不会对其进行处理;否则会进行一个默认的设置。

我自己理解后转写的伪代码如下:

UITableView *oldTableView = [self __existingTableView]; 

if (oldTableView == xxxtableView) {
    return 
} else {
    id oldDataSource = [oldTableView dataSource];
    // x21 被赋值成了oldTableView, x22 oldDataSource

    // 取UITableViewController 792偏移的成员变量 filteredDataSource
    id filteredDataSource = [self _filteredDataSource];

    if (oldDataSource != filteredDataSource) 
    {

    } else {
        [oldTableView setDataSource:nil];
    }

    id oldDelegate = [oldTableView delegate];
    // x25 被赋值了oldDelegate

    if (oldeDelegate != self)
    {
        goto //
    } else {
        [oldTableView setDelegate:nil];
    }

    id oldRefreshControl = [oldTableView _refreshControl];
    // x25 被赋值了oldRefreshControl

    [self setView:xxtableView];
    [xxxtableView _setRefreshControl:oldRefreshControl];

    id newDataSource = [xxxtableview dataSource];
    if (!newDataSource) {
        [self _applyDefaultDataSourceToTable:xxxTableView];
    }

    id newDelegate = [xxxtableview delegate];
    if (!newDelegate) {
        [xxxTableView setDelegate:self];
    }
}

通过上面对汇编和伪代码的理解,我们可以很轻易的得出结论:当我们处于第一种情形的实现,我们将tableview.delegate设置成了我们的stub。因为不为空,所以UITableViewController默认不会对其进行处理,而由于我们当时没有提供stub对于heightForRowAtIndexPath的实现,导致出现了UC的bug。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK