[iOS]原生swift的hotpatch可行性初探

0x0 引子

最近在iOS群里面看到某应用因为Hotpatch审核被拒绝, 如果Hotpatch全面被封禁, 那还不如全切swift, 又能提高性能, 又能减少编码中犯的错误. 仔细想想如果swift也有办法被Hotpatch, 不就更加完美了?
Hotpatch是无法被全面封禁的, 可爱的程序猿们总能有应对的办法

0x1 swift的方法调用方式

swift有四种方法调用方式:

  • inline method
  • static dispatch
  • dynamic dispatch
  • message send

inline method会在编译期间将被调用的方法直接内联到调用方法的方法体里面, 这种状况下方法调用将没有任何开销.

示例代码如下:

public class aclass {
    public func hehe() {
        clock()
        hehe1()
    }
    
    public func hehe1() {
        clock()
    }
}

编译后的汇编代码是:

    0x1000e9af8 <+84>:  bl     0x1000ea6c4               ; symbol stub for: clock
    0x1000e9afc <+88>:  bl     0x1000ea6c4               ; symbol stub for: clock

可以看出汇编代码超级简洁, 就只剩两次对clock方法的调用.

static dispatch会通过汇编指令bl跳转到被调用方法所在的地址, 地址在编译期就已经决定. 但因为多了一次bl指令, 会有少量的时间消耗.
对代码做不inline处理:

public class aclass {
    public func hehe() {
        clock()
        hehe1()
    }
    
    @inline(never) public func hehe1() {
        clock()
    }
}

编译后的汇编代码是:

    0x100051b18 <+124>: bl     0x1000526ac               ; symbol stub for: clock
    0x100051b1c <+128>: bl     0x100052100               ; function signature specialization <Arg[0] = Dead> of testswift.aclass.hehe1 () -> () at ViewController.swift:40
; hehe1 方法的实现
    0x100052100 <+0>: b      0x1000526ac                 ; symbol stub for: clock

这里比面上多出来一条bl指令, 跳转到hehe1方法所在地址.

dynamic dispatch会生成一张类的方法表(在数据段), 在调用时通过ldr指令从类表取出方法所在的地址到寄存器, 再跳转到方法所在的地址. 这种方式需要3条指令及1次内存访问, 所耗费的时间更长.

代码同第一段代码, 把工程Build Settings的Swift Comipler - Code Generation的Optimization Level调为None, 反编译代码如下:

    0x1000c1ad8 <+28>: ldr    x8, [x30]             ; 从对象中取出class元数据指针
    0x1000c1adc <+32>: ldr    x8, [x8, #88]         ; 从class元数据指针的偏移量88位置取出hehe1方法的地址放入x8
    0x1000c1ae8 <+44>: blr    x8                    ; 跳转到x8中保存的地址处的代码, 也就是hehe1方法
; hehe1 方法的实现
    0x1000c1b08 <+16>: bl     0x1000c2704           ; symbol stub for: clock

注: 此段代码汇编代码经过精简, 代码的解释见注释

message send就是objc_msgSend的流程, 这里暂不多做介绍.

0x2 patch尝试

inline的代码明显没戏, 连独立的方法都没有了, 更没有方法调用.

static的代码也没戏, 调用的方法地址在编译期就决定好了, 无法动态的做改变(代码在__TEXT段, 代码加载到内存后无写权限, 无法更改).

message用oc那一套方案就可以了.

dynamic的代码中, 是通过加载class的元数据中存储的方法地址进行方法调用, 而class元数据位于__DATA段, __DATA段是可以读写的. 那不就意味着采用dynamic方式, 把class的元数据给篡改掉就可以patch了? 我们试试!

先来一段原始代码:

public class aclass {
    public func hehe() {
        hehe1()
    }
    
    public func hehe1() {
        print("hehe1")
    }
}

按照正常的执行流程, 我们会在调试窗口看到:

hehe1

我想把hehe1这个方法patch到另一个方法的实现(一个c方法):

void patched_hehe1() {
    printf("patched_hehe1");
}

从之前提到的dynamic调用方式的修改class元数据的思路展开说, 我们首先要获取到aclass的class元数据的地址, 再获取到hehe1方法指针在元数据中的偏移量, 再获取patched_hehe1方法的指针, 再塞到class元数据中hehe1方法对应的位置.

开干!

下面实现了patch_hehe1方法, 将hehe1方法给patch成patched_hehe1方法:

void patched_hehe1() {
    printf("patched_hehe1");
}

void write_memory(void **ptr, void *value) {
    *ptr = value;
}

void *get_patch_method_address() {
    return &patched_hehe1;
}

void *get_class_meta_address() {
    Class aclass = NSClassFromString(@"testswift.aclass");
    return (__bridge void *)aclass;
}

long get_method_offset(void *class) {
    void * raw_method_address = dlsym(RTLD_DEFAULT, "_TFC9testswift6aclass5hehe1fT_T_");
    for (long i=0; i<1024; i++) {
        if (*(long *)(class+i) == (long)raw_method_address) {
            return i;
        }
    }
    return -1;
}

void patch_hehe1() {
    void *method_address = get_patch_method_address();  // 获取patched_hehe1方法的地址
    void *class_meta_address = get_class_meta_address();  // 获取aclass metadata的地址
    long offset = get_method_offset(class_meta_address);  // 获取偏移量
    write_memory(class_meta_address+offset, method_address); // 篡改metadata中的方法指针
}

调用patch_hehe1方法后, aclass的hehe1方法就被patch掉了! 运行程序看调试窗口的结果:

patched_hehe1

patch成功!

0x3 局限性

在前面提到, 为了实现让swift走dynamic dispatch, 将编译选项中的优化级别设为了None. 那如果将优化级别恢复为Fast后情况会变成什么样呢?

[iOS]原生swift的hotpatch可行性初探

hehe1方法被inline, patch无效, 输出hehe1

那么问题来了! 你愿意牺牲性能换取动态性么?

0x4 参考

  1. Swift Method Dispatch: http://*.com/questions/24014045/does-swift-have-dynamic-dispatch-and-virtual-methods?answertab=votes#tab-top
  2. Increasing Performance by Reducing Dynamic Dispatch: https://developer.apple.com/swift/blog/?id=27
上一篇:python 回溯法 记录


下一篇:jQuery dataTables 网格