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

简介: ### 0x0 引子 之前在对swift hotpatch的原理做一个简单的介绍和简单的示例, 但基础的原理分析并不能确定真实的可行性. 为此想通过这篇文章来做一个更复杂的例子. ### 0x1 先来一个简单的例子 来一个例子, 实现用js patch swift的方法, 功能包括: - 在js中通过类名/方法名/替换的方法, 来替换swift的方法 - 在js中通过方法名来调用原有

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, 或者一个自定义的汇编实现来解决.
试验性的代码比较挫, 各位看官见谅!

目录
相关文章
|
iOS开发
[iOS]原生swift的hotpatch可行性初探
### 0x0 引子 最近在iOS群里面看到某应用因为Hotpatch审核被拒绝, 如果Hotpatch全面被封禁, 那还不如全切swift, 又能提高性能, 又能减少编码中犯的错误. 仔细想想如果swift也有办法被Hotpatch, 不就更加完美了? *Hotpatch是无法被全面封禁的, 可爱的程序猿们总能有应对的办法* ### 0x1 swift的方法调用方式 swift有
5134 0
|
27天前
|
API 数据安全/隐私保护 iOS开发
利用uni-app 开发的iOS app 发布到App Store全流程
利用uni-app 开发的iOS app 发布到App Store全流程
83 3
|
3月前
|
存储 iOS开发
iOS 开发,如何进行应用的本地化(Localization)?
iOS 开发,如何进行应用的本地化(Localization)?
122 2
|
3月前
|
存储 数据建模 数据库
IOS开发数据存储:什么是 UserDefaults?有哪些替代方案?
IOS开发数据存储:什么是 UserDefaults?有哪些替代方案?
38 0
|
3月前
|
安全 编译器 Swift
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
90 2
|
3月前
|
API 开发工具 iOS开发
iOS 开发高效率工具包:10 大必备工具
iOS 开发高效率工具包:10 大必备工具
42 1
|
3月前
|
API 数据安全/隐私保护 iOS开发
利用uni-app 开发的iOS app 发布到App Store全流程
利用uni-app 开发的iOS app 发布到App Store全流程
52 1
|
2天前
|
API 定位技术 iOS开发
IOS开发基础知识:什么是 Cocoa Touch?它在 iOS 开发中的作用是什么?
【4月更文挑战第18天】**Cocoa Touch** 是iOS和Mac OS X应用的核心框架,包含面向对象库、运行时系统和触摸优化工具。它提供Mac验证的开发模式,强调触控接口和性能,涵盖3D图形、音频、网络及设备访问API,如相机和GPS。是构建高效iOS应用的基础,对开发者至关重要。
8 0
|
17天前
|
开发工具 Swift iOS开发
利用SwiftUI构建动态用户界面:iOS开发新范式
【4月更文挑战第3天】 随着苹果不断推进其软件开发工具的边界,SwiftUI作为一种新兴的编程框架,已经逐渐成为iOS开发者的新宠。不同于传统的UIKit,SwiftUI通过声明式语法和强大的功能组合,为创建动态且响应式的用户界面提供了一种更加简洁高效的方式。本文将深入探讨如何利用SwiftUI技术构建具有高度自定义能力和响应性的用户界面,并展示其在现代iOS应用开发中的优势和潜力。
|
2月前
|
监控 API Swift
用Swift开发iOS平台上的上网行为管理监控软件
在当今数字化时代,随着智能手机的普及,人们对于网络的依赖日益增加。然而,对于一些特定场景,如家庭、学校或者企业,对于iOS设备上的网络行为进行管理和监控显得尤为重要。为了满足这一需求,我们可以利用Swift语言开发一款iOS平台上的上网行为管理监控软件。
181 2

相关课程

更多