之前听说滴滴的 DynamicCocoa 是基于 JavaScriptCore 搞得, 一直期待看到他们的真正实现, 不过可能后来由于公司机密, 应该不能再开源了
借着最近开始研究 JavaScriptCore 的契机, 我决定利用这一两天所学的 JavaScript 知识, 在业余时间做一个简单的 iOS 动态执行器玩具
题外话 1: 听说滴滴基于 LLVM backend 搞了一套中间语言解释器, 不知道最后用了哪个? 不过 LLVM IR 解释器的话, 嘿嘿, 还是有点意思的
题外话 2: 我研究这个并不是想做 iOS 动态化, 因为 xxxxxxx 我只是纯粹想看看 JavaScriptCore 的一些实现而已
效果
一张 Gif 图想必能最佳得展示我做的玩具, 请各位大佬过目:
前置知识点
在实现我们的执行器前, 我们还是要稍微要了解一下一些前置的知识点
JSWrapper Object
大家都知道, Objective-C 中的诸多类型在 JavaScript 的环境里是不能直接用的, 需要通过 JSValue 进行一层包装, 具体的类型转换如下图展示:
基本上图上的转换都很容易理解, 唯一需要我们注意的是 Wrapper Object 什么是 Wrapper Object 呢?
举个例子:
self.context[@"a"] = [CustomObject new]
上述代码将我们一个自定义类型 CustomObject 的实例以变量名 a 的方式注入到了 JavaScript 的运行环境里但是她是怎么知道我们的定义呢, 又是如何知道我们是否能调用特定的方法?
从默认的角度看, JS 运行环境只会把 OC 中 init 初始化方法以及类的继承关系给同步到 JS 环境中(如果有 JSExport 我们下文说), 然后这个对象会包装给一个 JSWrapperValue 用于 JS 环境中使用而当 JS 环境调用 OC 并且涉及到这个对象的时候, JavaScriptCore 会自动将其解包还原成原始的 OC 对象类型
- - (JSValue *)jsWrapperForObject:(id)object
- {
- JSC::JSObject* jsWrapper = m_cachedJSWrappers.get(object);
- if (jsWrapper)
- return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context];
- // 注意点!!!!!!!!!!!!!!!!!!
- JSValue *wrapper;
- if (class_isMetaClass(object_getClass(object)))
- wrapper = [[self classInfoForClass:(Class)object] constructor];
- else {
- JSObjCClassInfo* classInfo = [self classInfoForClass:[object class]];
- wrapper = [classInfo wrapperForObject:object];
- }
- JSC::ExecState* exec = toJS([m_context JSGlobalContextRef]);
- jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec);
- m_cachedJSWrappers.set(object, jsWrapper);
- return wrapper;
- }
整体分析下, 就是基于一个缓存来判断是否对特定的对象或类型已经构建果 Wrapper Object, 没有的话就进行构建, 构建过程如下:
- JSClassDefinition definition;
- definition = kJSClassDefinitionEmpty;
- definition.className = className;
- m_classRef = JSClassCreate(&definition);
- [self allocateConstructorAndPrototypeWithSuperClassInfo:superClassInfo];
没啥特别的, 就是 OC 对象创建对应的 JS 对象, 类型对类型
OC 类型的继承关系在 JS 里面通过设置 Constructor 和 Prototype 进行构建, 其实就是简单的 JavaScript 原型链继承
JSExport 协议 & JSExportAs
JSExport 协议本质上只是个 Protocol 标记, 用于让 JavaScriptCore 加载那些打上这个特殊标记的类, 用于特定方式的注册及初始化
上文我们提过, 默认情况下, JavaScriptCore 会对象创建一个默认的 Wrapper Object, 但是这个对象除了简单继承关系外, 也就一个按照特殊格式命令的 Constructor 而已:
[NSString stringWithFormat:@"%sConstructor", className]
那如果我们需要将 OC 环境中的方法注入到 JS 环境中, 就需要用到 JSExport 协议了, 这个协议在运行时会按照如下逻辑进行处理, 将方法和属性进行诸如注入:
检查 init 方法簇的方法, 并根据这么合法提供合理的
- __block HashMap<String, Protocol *> initTable;
- Protocol *exportProtocol = getJSExportProtocol();
- for (Class currentClass = cls; currentClass; currentClass = class_getSuperclass(currentClass)) {
- forEachProtocolImplementingProtocol(currentClass, exportProtocol, ^(Protocol *protocol) {
- forEachMethodInProtocol(protocol, YES, YES, ^(SEL selector, const char*) {
- const char* name = sel_getName(selector);
- if (!isInitFamilyMethod(@(name)))
- return;
- initTable.set(name, protocol);
- });
- });
- }
- for (Class currentClass = cls; currentClass; currentClass = class_getSuperclass(currentClass)) {
- __block unsigned numberOfInitsFound = 0;
- __block SEL initMethod = 0;
- __block Protocol *initProtocol = 0;
- __block const char* types = 0;
- forEachMethodInClass(currentClass, ^(Method method) {
- SEL selector = method_getName(method);
- const char* name = sel_getName(selector);
- auto iter = initTable.find(name);
- if (iter == initTable.end())
- return;
- numberOfInitsFound++;
- initMethod = selector;
- initProtocol = iter->value;
- types = method_getTypeEncoding(method);
- });
- if (!numberOfInitsFound)
- continue;
- if (numberOfInitsFound> 1) {
- NSLog(@"ERROR: Class %@ exported more than one init family method via JSExport. Class %@ will not have a callable JavaScript constructor function.", cls, cls);
- break;
- }
- JSObjectRef method = objCCallbackFunctionForInit(context, cls, initProtocol, initMethod, types);
- return [JSValue valueWithJSValueRef:method inContext:context];
- }
注入方法和属性
- Protocol *exportProtocol = getJSExportProtocol();
- forEachProtocolImplementingProtocol(m_class, exportProtocol, ^(Protocol *protocol){
- copyPrototypeProperties(m_context, m_class, protocol, prototype);
- copyMethodsToObject(m_context, m_class, protocol, NO, constructor);
- });
而至于 JSExportAs, 就是做了个简单的名称映射而已, 毕竟 JS 函数传参和 OC 有很大的区别:
- static NSMutableDictionary * createRenameMap(Protocol * protocol, BOOL isInstanceMethod) {
- NSMutableDictionary * renameMap = [[NSMutableDictionary alloc] init];
- forEachMethodInProtocol(protocol, NO, isInstanceMethod, ^(SEL sel, const char * ) {
- NSString * rename = @ (sel_getName(sel));
- NSRange range = [rename rangeOfString: @"__JS_EXPORT_AS__"];
- if (range.location == NSNotFound) return;
- NSString * selector = [rename substringToIndex: range.location];
- NSUInteger begin = range.location + range.length;
- NSUInteger length = [rename length] - begin - 1;
- NSString * name = [rename substringWithRange: (NSRange) {
- begin,
- length
- }];
- renameMap[selector] = name;
- });
- return renameMap;
- }
实现过程
说了那么多基础原理, 下面让我们来看看具体实现流程:
类实例和方法
在我看来, 要实现一个动态化的执行环境, 有三要素是必不可少的:
类 (包括元类) 实例对象以及方法
基于我们上文对于 Wrapper Object 的分析, 我们可以构建特殊类型的 Wrapper Object:
WZJSClassRef, 用于包装 Class, 比如 [NSObject class] 的返回值
WZJSInstanceRef, 用于包装 instance, 比如 [NSObject new] 的返回值
WZJSMethodRef, 用于包装 SEL 比如 -[NSObject alloc]
除了上述三要素, 我们还需要定义一个全局变量, WZGloablObject(大家可以理解为浏览器的 window 对象), 用于拦截顶层的属性访问
按照这个设计, 大家可以自行思考下, 如果是你做, 你会如何继续下面的工作, 文章下周随着代码一起发布吧
Choose 调试
搞过逆向用过 Cycript 的朋友都知道, Cycript 在调试时候有个非常方便的调试功能: Choose 该功能可以快速的帮助我们根据类名在堆上的对象全部查询返回
这么实用的功能必须提供, 我基本上直接照搬了 Cycript 的实现代码很清晰, 基本能够自解释其逻辑核心基本上就是遍历每个 malloc_zone, 然后根据获取的 vmaddress_range 判断获取到的数据其类型是不是我们要的
- // 遍历 zone
- for (unsigned i = 0; i != size; ++i) {
- const malloc_zone_t * zone = reinterpret_cast<const malloc_zone_t *>(zones[i]);
- if (zone == NULL || zone->introspect == NULL)
- continue;
- zone->introspect->enumerator(mach_task_self(), &choice, MALLOC_PTR_IN_USE_RANGE_TYPE, zones[i], &read_memory, &choose_);
- }
- // 检查对象
- for (unsigned i = 0; i <count; ++i) {
- vm_range_t &range = ranges[i];
- void * data = reinterpret_cast<void *>(range.address);
- size_t size = range.size;
- if (size <sizeof(ObjectStruct))
- continue;
- uintptr_t * pointers = reinterpret_cast<uintptr_t *>(data);
- #ifdef __arm64__
- Class isa = (__bridge Class)((void *)(pointers[0] & 0x1fffffff8));
- #else
- Class isa = reinterpret_cast<Class>(pointers[0]);
- #endif
- std::set<Class>::const_iterator result(choice->query_.find(isa));
- if (result == choice->query_.end())
- continue;
- size_t needed = class_getInstanceSize(*result);
- size_t boundary = 496;
- #ifdef __LP64__
- boundary *= 2;
- #endif
- if ((needed <= boundary && (needed + 15) / 16 * 16 != size) || (needed> boundary && (needed + 511) / 512 * 512 != size))
- continue;
- choice->result_.insert((__bridge id)(data));
- }
不过这里一大堆的 511512 的数字构成的公式, 实话说我不是很懂, 有了解的大佬麻烦告知我一下
类型转换
首先我们需要记住, JavaScript 的基础类型如下:
- 字符串
- 数字
- 布尔
- 数组
- 对象
- - Null
- - Undefined
所以我们只要根据对应的进行转换就可以, 如下所示:
JS 字符串 <-> NSString
数字 <-> NSNumber
数组 <-> NSArray
- Null <-> NSNull
- Undefined <-> Void (仅当返回值的时候处理, 否则直接抛出异常)
题外话, JavaScript 里面没有什么整数和浮点数类型区分一说, 所以我们可以无脑将其通过 double 的方式构建 NSNumber
最后再来说下对对象类型的处理:
在 JavaScript, 任何对象都可以简单理解为包含了属性 (方法) 的一个包装体, 如下所示:
var a = {x:10, y:100};
因此, 我们在对类型进行转换的时候, 要特别注意以下几点:
这个对象是不是我们刚刚上文提过的类实例方法, 是的话在其进入到 Objective-C 执行上下文的之前从 JSWrapperObject 中取出来
这个对象是不是特定类型的结构体, 是的话我们将其转换成结构体, 比如 CGRect 之类的, 是的话需要特别转换
是不是可以直接转换成特定类型的对象, 比如 Date <-> NSDate 的转换
最后, 将其可遍历的属性和对应的属性值, 转换到 NSDictionary 之中
当然, 别忘了, 需要注意递归处理
Calling Convention
关于 Calling Convention, 本文就不再赘述, 有兴趣的读者可以参考我和同事一起写的知乎专栏 iOS 调试进阶
简单来重新描述下就是:
一个函数的调用过程中, 函数的参数既可以使用栈传递, 也可以使用寄存器传递, 参数压栈的顺序可以从左到右也可以从右到左, 函数调用后参数从栈弹出这个工作可以由函数调用方完成, 也可以由被调用方完成如果函数的调用方和被调用方 (函数本身) 不遵循统一的约定, 有这么多分歧这个函数调用就没法完成这个双方必须遵守的统一约定就叫做调用惯例(Calling Convention), 调用惯例规定了参数的传递的顺序和方式, 以及栈的维护方式
由于业界已经有知名大佬写的 libffi, 所以我们不需要重复发明轮子, 直接使用即可如果真的要了解具体原理, 也可以参考我的文章, 具体分析 objc_msgSend 的实现流程
其他
为了偷懒, 我直接用 JavaScript 实现了这些的效果其实理论上, 如果我完整的实现编译前端, 构建抽象语法树分析执行上下文, 将 Objective-C 的代码转换成 JavaScript, 那么就能实现动态执行 Objective-C 代码了(当然本质上还是障眼法)
其实更快的方式, 且不能保证完全正确的方式, 就是调用一下 JSPatchConvertor 就好了, 哈哈哈
结语
每篇文章的最后, 请允许我按照惯例吟诗一首致敬我的偶像, 杨萧玉
杨萧玉, 腾讯星, 90 后中属第一
人低调, 还谦虚, iOS 技术无人敌
搞逆向, 他在行, 微信破解太容易
论深度, 特服气, 深入汇编来分析
动态化, 不用提, 想发 patch 就 patch
论收入, 没得比, 一年交税几个亿
我的偶像杨萧玉, 为何你竟如此牛逼?
来源: https://juejin.im/entry/5abfbd64f265da239377351c