0x0 引子
之前在<原生swift的hotpatch可行性初探>对swift hotpatch的原理做一个简单的介绍和简单的示例, 但基础的原理分析并不能确定真实的可行性. 为此想通过这篇文章来做一个更复杂的例子.
0x1 先来一个简单的例子
来一个例子, 实现用js patch swift的方法, 功能包括:
- 在js中通过类名/方法名/替换的方法, 来替换swift的方法
- 在js中通过方法名来调用原有的swift方法
swift代码:
public class ViewController: UIViewController {
override public func viewDidLoad() {
super.viewDidLoad()
patch_init() // patch 初始化及完成patch操作
aclass().hehe() // 调用aclass的hehe方法, hehe方法里面会调用hehe1
}
}
public class aclass {
public func hehe() {
hehe1()
}
public func hehe1() {
print("hehe1")
}
}
!!!请注意, 前方高能, 请务必阅读开头提到的上一篇文章!!!
patch方法的代码:
static JSContext *jsContext = nil;
static JSValue *jsFunction = nil;
void patched() {
[jsFunction callWithArguments:nil];
}
void patch_init() {
jsContext = [[JSContext alloc] init];
jsContext[@"log"] = ^(NSString *message) {
NSLog(@"%@", message);
};
jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) {
void *class = (__bridge void *)objc_getClass(className.UTF8String);
void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String);
if (!class || !raw_method_address) {
NSLog(@"class or method note found!");
return;
}
long offset = 0;
for (long i=0; i<1024; i++) {
if (*(long *)(class+i) == (long)raw_method_address) {
offset = i;
break;
}
}
if (!offset) return;
jsFunction = func;
*(void **)(class+offset) = &patched;
};
jsContext[@"call"] = ^(NSString *methodName){
void (*raw_method_address)() = dlsym(RTLD_DEFAULT, methodName.UTF8String);
if (raw_method_address) {
raw_method_address();
}
};
[jsContext evaluateScript:@"\
function callback(){\
log('patched hehe1');\
log('calling raw method:');\
call('_TFC9testswift6aclass5hehe1fT_T_');\
}\
patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fT_T_', callback);\
"];
}
代码的最后一部分的js代码里面, 将testswift.aclass
的_TFC9testswift6aclass5hehe1fT_T_
方法替换为了js写的callback
方法, 而callback
方法里面又调用了原始的_TFC9testswift6aclass5hehe1fT_T_
方法.
运行结果:
2016-09-09 16:44:49.639 testswift[1725:677144] patched hehe1
2016-09-09 16:44:49.640 testswift[1725:677144] calling raw method:
hehe1
符合预期!
0x2 复杂一点
刚刚的两个方法里面没有参数, 也没有返回值, 实际上本身的复杂度就有了一定程度的降低, 在来点复杂的吧, 带上参数吧.
上代码, swift改成这样, 把hehe1加上参数String:
public class aclass {
public func hehe() {
hehe1("hehe")
}
public func hehe1(str: String) {
print(str)
}
}
那么问题来了, 这下就涉及到patch本身使用的语言及swift之间类型转换了.看了一下swift的短String的数据结构, 发现直接就是char*
. 不过在传String值的时候, 实际上传了三个参数, char/int/void , 其中int是string长度, void*是留给objc的内存管理用的.
修改patch实现如下:
static JSContext *jsContext = nil;
static JSValue *jsFunction = nil;
void patched() {
[jsFunction callWithArguments:@[@"patched"]];
}
void patch_init() {
jsContext = [[JSContext alloc] init];
jsContext[@"log"] = ^(NSString *message) {
NSLog(@"%@", message);
};
jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) {
void *class = (__bridge void *)objc_getClass(className.UTF8String);
void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String);
if (!class || !raw_method_address) {
NSLog(@"class or method note found!");
return;
}
long offset = 0;
for (long i=0; i<1024; i++) {
if (*(long *)(class+i) == (long)raw_method_address) {
offset = i;
break;
}
}
if (!offset) return;
jsFunction = func;
*(void **)(class+offset) = &patched;
};
jsContext[@"call"] = ^(NSString *methodName, NSString *parameter){
void (*raw_method_address)(char *, long, long) = dlsym(RTLD_DEFAULT, methodName.UTF8String);
if (raw_method_address) {
raw_method_address(parameter.UTF8String, strlen(parameter.UTF8String), 0);
}
};
[jsContext setExceptionHandler:^(JSContext *ctx, JSValue *v) {
NSLog(@"error: %@, %@", ctx.exception);
}];
[jsContext evaluateScript:@"\
function callback(str){\
log('calling: patched hehe1');\
log('parameter: ' + str);\
log('calling raw method:');\
call('_TFC9testswift6aclass5hehe1fSST_', str);\
}\
patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fSST_', callback);\
"];
}
跑一下结果:
calling: patched hehe1
parameter: patched
calling raw method:
patched
patch成功.
改动主要在于:
- c方法
patched
里面调用js的callback增加了一个参数. - js方法
call
里面改变了调用原始方法的参数列表
0x3 再复杂一点
再搞复杂一点, 我们搞一个返回值, 并且返回值是一个原始的swift类, 我们在hook里面对swift类做一个修改!
swift代码:
public class aclass {
public var p: UInt64 = 0xaaaaaaaaaaaaaaaa
public func hehe() {
let a = hehe1()
print(a.p)
}
public func hehe1() -> aclass {
return self
}
}
正常情况下我们将看到输出12297829382473034410
, 也就是0xaaaaaaaaaaaaaaaa
的十进制. 我要把它改成1
!
修改patch的实现:
static JSContext *jsContext = nil;
static JSValue *jsFunction = nil;
static void* selfHolder = NULL;
void *patched(void *self) {
selfHolder = self;
JSValue *ret = [jsFunction callWithArguments:nil];
return (void *)[[ret toNumber] longLongValue];
}
void patch_init() {
jsContext = [[JSContext alloc] init];
jsContext[@"log"] = ^(NSString *message) {
NSLog(@"%@", message);
};
jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) {
void *class = (__bridge void *)objc_getClass(className.UTF8String);
void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String);
if (!class || !raw_method_address) {
NSLog(@"class or method note found!");
return;
}
long offset = 0;
for (long i=0; i<1024; i++) {
if (*(long *)(class+i) == (long)raw_method_address) {
offset = i;
break;
}
}
if (!offset) return;
jsFunction = func;
*(void **)(class+offset) = &patched;
};
jsContext[@"call"] = (NSNumber*)^(NSString *methodName, NSString *parameter){
__block void *ret = 0;
void*(*raw_method_address)(void *) = dlsym(RTLD_DEFAULT, methodName.UTF8String);
if (raw_method_address) {
ret = raw_method_address(selfHolder);
}
return [[NSNumber alloc] initWithLong:(long)ret];
};
jsContext[@"memory_write"] = ^(NSNumber *ptr, NSNumber *off, NSNumber *val) {
long long pointer = [ptr longLongValue];
long offset = [off longValue];
long long value = [val longLongValue];
*(long *)(pointer+offset) = value;
};
[jsContext setExceptionHandler:^(JSContext *ctx, JSValue *v) {
NSLog(@"error: %@, %@", ctx, ctx.exception);
}];
[jsContext evaluateScript:@"\
function callback(){\
log('calling: patched hehe1');\
log('calling raw method:');\
var ret = call('_TFC9testswift6aclass5hehe1fT_S0_');\
memory_write(ret, 16, 1);\
return ret;\
}\
patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fT_S0_', callback);\
"];
}
跑一下结果:
2016-09-09 19:10:31.138 testswift[2040:722528] calling: patched hehe1
2016-09-09 19:10:31.139 testswift[2040:722528] calling raw method:
1
请注意左下角的小1
, 修改成功!
这里主要增加了memory_write方法来写内存数据, 用于修改对象的属性. 这里的offset 16可以通过工具来计算. 由于在hehe1中用到了self, swift方法也隐含了对self的传递, 所以在调用原有方法的之前保存了一下self指针, 在调原有方法的时候用.
0x4 小结
上面的patch的方法, 都具有一定的特定性, 无法满足所有需求, 但已经为可行性做了一个更强力的证明.
在改进通用性和易用性后, swift hotpatch的雏形就出来了.
另外在调用原始方法时, 因为不确定参数表, 需要使用libffi, 或者一个自定义的汇编实现来解决.
试验性的代码比较挫, 各位看官见谅!