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
后情况会变成什么样呢?
hehe1方法被inline, patch无效, 输出hehe1
那么问题来了! 你愿意牺牲性能换取动态性么?
0x4 参考
- Swift Method Dispatch: http://stackoverflow.com/questions/24014045/does-swift-have-dynamic-dispatch-and-virtual-methods?answertab=votes#tab-top
- Increasing Performance by Reducing Dynamic Dispatch: https://developer.apple.com/swift/blog/?id=27