0%

JavaScriptCore 的一些黑魔法

曳杖危楼去。
斗垂天、沧波万顷,月流烟渚。
扫尽浮云风不定,未放扁舟夜渡。
宿雁落、寒芦深处。
怅望关河空吊影,正人间、鼻息鸣鼍鼓。
谁伴我,醉中舞。

十年一梦扬州路。
倚高寒、愁生故国,气吞骄虏。
要斩楼兰三尺剑,遗恨琵琶旧语。
谩暗涩铜华尘土。
唤取谪仙平章看,过苕溪、尚许垂纶否。
风浩荡,欲飞举。

今天是我阳了的第四天,也是正式失业后的第二十天,仅以此词明志,定要守住这“黄沙百战穿金甲,不破楼兰终不还”的魄力!另外关于面试,我觉得它是一次双向选择的过程,而我现在还没想清楚要怎么选择,因为并没有想明白这其中的意义价值,所以没有付出太多行动,被动的挂掉了所有面试。十多年了,这是我休过最长的一次假,人生或许真的有其他活法吧。

关于前几篇的《点灯大师》系列,原本打算一直更新到 Linux 驱动点灯的( 我手上起码有三种 Linux 的开发板 ),后面发生的事情太过魔幻,就耽搁了,按目前的境况而言,估计要无限期的鸽了。这灯终还是没能点到最后呀,不过只要灯点得好,就有机会接触到另外的点灯大师:

这是我离稚晖君最近的一次了,所以截屏纪念了一下,听说他最近也离职了,这也让我失去了更新下一篇《点灯大师》的动力,毕竟我计划下一篇的 SoC 是 Hi3861( 海思的 )。

回归到本篇的主题吧,也算是回归到我的本职工作了,这几年的工作内容越来越偏门,真正在 iOS 平台上有所研究的,也只能说是 JavaScriptCore 用得比较熟了吧 (笑死…)。本篇内容不多,但涉及到的黑魔法,我觉得在做相关基建时,还是挺实用的,毕竟在 iOS 平台上,JS 引擎除了 JSCore,没有其他选择了。

调整 Inspector 的调用线程

为了描述该魔法的使用场景,我们先构建一段测试代码,看测试结果再做解释。先建立一个 NSThread 子类,用于运转一个 RunLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@implementation JSRunThread

- (instancetype)initWithName:(NSString *)name {
NSParameterAssert(name.length);
self = [super init];
self.name = name;

return self;
}

- (void)main {
@autoreleasepool {
CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef noSpinSource = CFRunLoopSourceCreate(NULL, 0, &noSpinCtx);
CFRunLoopAddSource(CFRunLoopGetCurrent(), noSpinSource, kCFRunLoopDefaultMode);
CFRelease(noSpinSource);

while (kCFRunLoopRunStopped !=
CFRunLoopRunInMode(kCFRunLoopDefaultMode, ((NSDate *)[NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)) {
NSAssert(NO, @"not reached assertion");
}
}
}

@end

基于这个 JSRunThread 我们再构建一段测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@interface JSCRunLoopTest () {
JSContext *_context;
JSRunThread *_thread;
}

@end

@implementation JSCRunLoopTest

- (instancetype)init {
self = [super init];
_thread = [[JSRunThread alloc] initWithName:@"js.test.thread"];
[_thread start];

[self performSelector:@selector(_setupJSContext)
onThread:_thread
withObject:nil
waitUntilDone:YES];

return self;
}

- (void)_setupJSContext {
NSAssert([NSThread.currentThread isEqual:_thread], @"");

_context = [[JSContext alloc] initWithVirtualMachine:[JSVirtualMachine new]];
_context.name = @"test js context";

_context[@"testMethod"] = ^(NSString *val) {
NSLog(@"invoke under thread: %@", [NSThread currentThread]);
};

[_context evaluateScript:@"testMethod()"];
}

@end

最终测试的地方类似这么写:

1
2
static JSCRunLoopTest *test; // 需要强引用,否则 JSContext 就被释放了
test = [JSCRunLoopTest new];

代码不复杂,这里也简单解释下,主要就是构建一个 JSContext,并且是从后台线程进行的初始化,然后注入了一个原生方法 testMethod,并在该方法里打印当前线程。后台线程创建 JSVirtualMachine 的原因是将 GC 线程放置到后台,而并不能限制住 testMethod 的入口线程,也就是说,testMethod 里打印的值是不确定的,这取决于我们是从哪个线程进行的 evaluateScript。从实际应用的角度而言,我们通常会限制住 evaluateScript 的线程为 JSVirtualMachine 的初始化线程,那么就能确保 testMethod 的入口线程是单一的,也就是说不会产生多线程的竞争问题。

不过还有例外,当我们打开 Safari 的 Inspector,直接从控制台输入 testMethod 进行调用时,你会发现它的入口线程是主线程。这种差异性会给开发、调试带来一些问题,有可能是死锁,或者运行异常。一些自动化测试框架的原理跟 Safari Inspector 一致,所以也会有同样的问题。

手动去差异性处理线程切换的意义不大,因为这些差异仅产生在测试阶段,线上不应该带上这种不可能触达的代码逻辑,所以就需要使用以下私有 API 了:

1
JS_EXPORT void JSGlobalContextSetDebuggerRunLoop(JSGlobalContextRef ctx, CFRunLoopRef runLoop);

我们将这个 API 直接定义到测试代码的头部,再进行如下调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
JS_EXPORT void JSGlobalContextSetDebuggerRunLoop(JSGlobalContextRef ctx, CFRunLoopRef runLoop);

@interface JSCRunLoopTest () {
JSContext *_context;
JSRunThread *_thread;
}

@end

... 此处省略

- (void)_setupJSContext {
NSAssert([NSThread.currentThread isEqual:_thread], @"");

_context = [[JSContext alloc] initWithVirtualMachine:[JSVirtualMachine new]];
_context.name = @"test js context";

// 重点关注此处的设置
JSGlobalContextSetDebuggerRunLoop(_context.JSGlobalContextRef, CFRunLoopGetCurrent());

...
}

问题就完美解决了,注意这里链接了私有 API 的符号,千万不要这样直接提交 AppStore,被拒的可能性非常大。并且这个一般线上也不会用到,所以可以考虑通过 DEBUG 宏来进行区分。

强制关闭 Safari 的 Inspector

前面说到了 Safari Inspector,这里还引入了另外一个问题,当 Safari Inspector 处于打开的状态时,它会类似的强持有了 JSContext,导致 JSContext 所持有的资源无法被释放。所以,在以往的工作经验中,QA 进行 API 测试时经常会忘记关闭掉 Safari Inspector,从而导致了类似内存泄漏的状况,也导致了一些独占资源的断言失败,从而引发一些不太好解释的缺陷。

JavaScriptCore 并没有提供关闭掉 Safari Inspector 的方法,这里就要使用另外的私有 API 了,当我们明确当前 JSContext 的生命周期已经结束的时候,尝试强制去关闭掉 Safari Inspector。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <JavaScriptCore/JavaScriptCore.h>
#include <dlfcn.h>

static uintptr_t JSCSymbolAddr(const char *symb) {
Dl_info dli;
memset(&dli, 0, sizeof(dli));
dladdr((const void *)JSContextGroupCreate, &dli);

auto handle = dlopen(dli.dli_fname, RTLD_NOLOAD);
auto addr = (uintptr_t)dlsym(handle, symb);
dlclose(handle);

return addr;
}

void ForceCloseSafariInspector(void) {
auto ins = ({
auto addr = JSCSymbolAddr("_ZN9Inspector15RemoteInspector9singletonEv");
if (!addr) return;

((void *(*)(void))addr)();
});

{ // stop remote inspector
auto addr = JSCSymbolAddr("_ZN9Inspector15RemoteInspector4stopEv");
if (!addr) return;

((void (*)(decltype(ins)))addr)(ins);
}

// waitting for remote inspector resources clean
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];

{ // restart remote inspector
auto addr = JSCSymbolAddr("_ZN9Inspector15RemoteInspector5startEv");
if (!addr) return;

((void (*)(decltype(ins)))addr)(ins);
}
}

直接调用 ForceCloseSafariInspector 即可强制关闭掉 Safari Inspector 了,不过这里是重启了 JavaScriptCore 内部的 Inspector Server,所以会导致所有的 Safari Inspector 都会被关闭掉。另外上述代码是 C++ 和 Objective-C 混编的,所以要使用 .mm 文件来承载。同样这里的私有 API 也是不建议直接提交 AppStore 的,并且这里也只是测试时才会有的场景,使用特定的宏进行差异化编译即可。

设定执行耗时的看门狗

通常而言,我们使用 JSContext 主要是为了动态化考虑,也就是所执行的 JavaScript 代码并不太受管控,特别是当我们本身是一个小程序、小游戏框架而言,业务方代码参差不齐,甚至可能有恶意性代码。比如下面这样一个无限死循环的代码:

1
2
3
while (true) {
var a = '123';
}

这会导致 CPU 跑满,从而可能导致主 App 被系统看门狗杀掉。即便不是恶意代码,也有可能无意间引入了类似的死循环,那么为了主 App 的健壮性,面对这样的代码该如何防备呢?那么这里就又是另外的一些私有 API 的使用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef bool (*JSShouldTerminateCallback) (JSContextRef ctx, void* context);

JS_EXPORT void JSContextGroupSetExecutionTimeLimit(JSContextGroupRef group, double limit, JSShouldTerminateCallback callback, void* context);
JS_EXPORT JSStringRef JSContextCreateBacktrace(JSContextRef ctx, unsigned maxStackSize);

static bool JSShouldTerminate(JSContextRef ctx, void *context) {
NSString *bt = ({
JSStringRef x = JSContextCreateBacktrace(ctx, 1024 * 1024 * 4);
(__bridge NSString *)JSStringCopyCFString(kCFAllocatorDefault, x);
});
NSLog(@"JS long time execution backtrace:\n%@", bt);

return true;
}

... 此处省略

JSContextGroupSetExecutionTimeLimit(JSContextGetGroup(_context.JSGlobalContextRef),
3.0,
JSShouldTerminate,
NULL);

... 此处省略

通过以上两个私有 API,我们设定了 JavaScript 执行的时间限制,超过 3 秒的 JavaScript 执行就会被强制结束,并且会将相关的 JavaScript 堆栈输出。此 API 可用于监控,也可用作为 JavaScript 的看门狗。当然,如果想带到线上,还是要考虑下如何隐蔽的使用该私有 API 了,本篇后续会介绍一种相对安全的私有 API 调用方式。

实现 AppStore 应用的远程调试

在 iOS 平台使用 JavaScriptCore 还有一个经常会遇到的问题,当我们的 App 使用非开发者证书进行签名后( 比如企业证书、AppStore证书 ),就无法再使用 Safari Inspector 进行调试了。苹果这样设计是从应用的安全性角度进行考量,无可厚非。不过如果我们应用场景是一个较为开放的平台服务,第三方接入时难免需要进行问题调试和排查,这样就失去了很多便利。

比较常见的做法,是提供类似 vConsole 这种页面级别的调试工具进行日志查看,但总归没有 Inspector 来得好用,那有没有办法再度打开 Inspector 能力呢?答案是肯定的,之所以 Safari 无法再发现我们的 JSContext,是因为 iOS 系统中的 webinspectord 守护进程进行了目标过滤,它会检查目标应用证书及 entitlement,一次常规的 Safari Inspector 到我们 JSContext 连接是这样一个链路:

Safari Inspector <-> webinspectord <-> JSContext

俩俩间通过 RPC/IPC 进行通信,其实 webinspectord 仅仅只是做消息的分发和过滤,最终消息的处理能力还是存在目标 App 中,也就是 JavaScriptCore 框架中。那么我们可以自己构建一条通道,直接将 Inspector 和 JSContext 进行连接,比较适合的是开启 WebSocket 服务,所以最终的链路变成了这样:

Inspector <-> WebSocket Server <-> JSContext

webinspectord 类似,WebSocket Server 主要做的也是消息分发,但由于 Safari Inspector 没法进行 WebSocket 连接,所以这里的 Inspector 使用了 Chrome DevTools。于是 WebSocket Server 这一层又多了一份职责:Safari Inspector 协议和 Chrome DevTools 协议的相互转换,双方协议定义参考下面:

如何让 JSContext 能处理并响应这些调试协议呢?主要依靠的是以下这个私有 API:

1
2
JS_EXPORT Inspector::AugmentableInspectorController* 
JSGlobalContextGetAugmentableInspectorController(JSGlobalContextRef);

这个方法返回了一个 C++ 的抽象类 AugmentableInspectorController 的实例,这个类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class AugmentableInspectorController {
public:
virtual ~AugmentableInspectorController() { }

virtual AugmentableInspectorControllerClient* augmentableInspectorControllerClient() const = 0;
virtual void setAugmentableInspectorControllerClient(AugmentableInspectorControllerClient*) = 0;

virtual const FrontendRouter& frontendRouter() const = 0;
virtual BackendDispatcher& backendDispatcher() = 0;
virtual void registerAlternateAgent(std::unique_ptr<InspectorAgentBase>) = 0;

bool connected() const { return frontendRouter().hasFrontends(); }
};

那么根据 C++ 类的布局,我们其实只要构建相同大小的虚方法表,强转后即可调用到这其中任何一个虚方法。如果这块有所疑问,可以看我之前的这篇 《温习 C++ 的虚函数》 文章。这里我们只要关注 frontendRouterbackendDispatcher 即可,backendDispatcher用来处理协议请求,frontendRouter 用来接收协议处理后的响应以及内部的一些纯通知类协议。

其中 FrontendRouter 核心定义如下:

1
2
3
4
5
6
7
8
9
10
class JS_EXPORT_PRIVATE FrontendRouter : public RefCounted<FrontendRouter> {
public:
...

void connectFrontend(FrontendChannel&);
void disconnectFrontend(FrontendChannel&);
void disconnectAllFrontends();

...
};

这里的 connectFrontend 需要传入一个抽象类 FrontendChannel 实例引用,这个可以理解为类似 iOS 中常见的 Delegate,用观察者模式来监听协议返回和通知。这个 FrontendChannel 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
class FrontendChannel {
public:

enum class ConnectionType {
Remote,
Local
};

virtual ~FrontendChannel() { }
virtual ConnectionType connectionType() const = 0;
virtual void sendMessageToFrontend(const String& message) = 0;
};

同样,我们构建一个虚函数布局相同的实例即可,这里关注的就是 sendMessageToFrontend 方法,该方法是由 JSContext 内部进行回调的。那么如何调用 FrontendRouterconnectFrontend 这个方法呢?毕竟它不是虚函数了,无法从 this 指针进行偏移获得,并且它还是一个实例方法。其实也很简单,我们找到该方法的符号:

_ZN9Inspector14FrontendRouter15connectFrontendERNS_15FrontendChannelE

然后使用前面的 JSCSymbolAddr 方法查询到该符号对应的地址,将地址转换成 C 方法进行调用,根据 iOS 平台的方法调用约定,C++ 实例方法的第一个参数为 this,所以调用方式大致如下:

1
2
3
4
5
6
7
8
9
10
11
auto router = ...; // FrontendRouter 实例通过 AugmentableInspectorController 获取
auto channel = ...; // FrontendChannel 自己构建相同的类布局

auto symbol = "_ZN9Inspector14FrontendRouter15connectFrontendERNS_15FrontendChannelE";
auto addr = JSCSymbolAddr(symbol);

// 强转成函数指针,第一个参数是 this,也就是 FrontendRouter
auto func = (void (*)(decltype(router), decltype(channel)))addr;

// 直接进行调用
func(router, channel);

BackendDispatcher 需要关注的就比较简单了,它定义如下:

1
2
3
4
5
6
7
8
class JS_EXPORT_PRIVATE BackendDispatcher : public RefCounted<BackendDispatcher> {
public:
...

void dispatch(const String& message);

...
};

我们只需要使用这个 dispatch 方法即可,使用方法和前面 connectFrontend 一致,符号转地址再调用。那么完整的链接其实就已经说完了,从代码的角度再总结一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 从 Inspector 发送调试消息 JSContext:
Inspector
-> WebSocket Server
-> JSGlobalContextGetAugmentableInspectorController
-> AugmentableInspectorController.backendDispatcher
-> BackendDispatcher.dispatch // 消息至此处理

// 注册 JSContext 调试消息监听器
JSGlobalContextGetAugmentableInspectorController
-> AugmentableInspectorController.frontendRouter
-> FrontendRouter.connectFrontend

// 从 JSContext 返回消息至 Inspector:
FrontendChannel.sendMessageToFrontend
-> WebSocket Server
-> Inspector

另外再补充一点,这里所有的通讯字符串都是使用了 WTF 中的字符串类型,我们无法直接使用。所以要获取到能使用的字符串,还要构建一个内存布局相同的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class JSCString {
public:
JSCString(NSString *objcStr) {
size_t alloc_size = sizeof(DataView) + objcStr.length;
data_view_ = std::shared_ptr<DataView>((DataView *)malloc(alloc_size), [](DataView *buf) {
free(buf);
});

memset((void *)data_view_.get(), 0, alloc_size);
data_view_->ref_count = 2;
data_view_->length = (unsigned)objcStr.length;
if (@available(iOS 13.0, *)) {
data_view_->datas = (const char *)(&data_view_->hash_flag + 1);
data_view_->hash_flag = 0x4;
} else {
data_view_->datas = (const char *)(&data_view_->datas + 1);
}
memcpy((void *)data_view_->datas, objcStr.UTF8String, objcStr.length);
}

NSString *getObjCString() const {
if (this->data_view_ == nullptr) {
return nil;
}

return [[NSString alloc] initWithBytes:data_view_->datas
length:data_view_->length
encoding:NSUTF8StringEncoding];
}

private:
// WARNING: do not change the member order !!!
struct DataView {
unsigned ref_count;
unsigned length;
const char *datas;
unsigned hash_flag;
};
std::shared_ptr<DataView> data_view_;
}; // class JSCString

那么在实现 FrontendChannel 接收回调时,参数就可以直接使用 这个 JSCString,同理,dispatch 方法直接传入该类实例即可,其它不再赘述。

附:较为安全的 C/C++ 私有 API 的调用方式

本文中所有的私有 API 都是 C/C++ 的,所以这里不讨论 ObjC 私有 API 调用的安全性。对于 C/C++ 私有 API 的调用,本文也提供了两种方式,一种是自己将声明导出,一种是通过符号进行查找 (JSCSymbolAddr)。

导出声明的方式会产生延迟绑定的符号,毕竟链接的是动态库,那么私有 API 就会在相应的符号表里一览无余,都过不了苹果的第一道静态扫描。所以第一种方式仅可用作非生产环境里,一旦提交至 AppStore 审核,大概率会被拒。

第二种使用符号查找地址的方式,那么可以将符号字符串进行混淆存储,避免被静态扫描到,然后再在运行时进行反混淆处理。这样可能相对比较安全,但还不保险,毕竟我们使用了一个非常敏感的函数: dlsym。如果苹果在该函数内部进行了一些追踪,那么还是能直接输出我们使用到的私有 API。为了安全起见,我们就不能使用该函数,可以自己构建一个类似的函数,也就是查阅符号表,获取符号地址,再加上 ASLR 偏移地址,既可以获得真正的私有 API 运行时地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <JavaScriptCore/JavaScriptCore.h>
#include <dlfcn.h>
#include <string>
#include <mach-o/dyld.h>
#include <mach-o/nlist.h>
#include <mach-o/loader.h>

#ifdef __LP64__
typedef struct mach_header_64 mach_header_t;
typedef struct segment_command_64 segment_command_t;
typedef struct section_64 section_t;
typedef struct nlist_64 nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64
#else
typedef struct mach_header mach_header_t;
typedef struct segment_command segment_command_t;
typedef struct section section_t;
typedef struct nlist nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT
#endif


static void _get_jsc_header_and_slide(mach_header_t **header, intptr_t *slide) {
Dl_info dli;
memset(&dli, 0, sizeof(dli));
dladdr((const void *)JSContextGetGlobalObject, &dli);
*header = (mach_header_t *)dli.dli_fbase;

uint32_t c = _dyld_image_count();
for (uint32_t i = 0; i < c; i++) {
if ((uintptr_t)(dli.dli_fbase) == (uintptr_t)_dyld_get_image_header(i)) {
*slide = _dyld_get_image_vmaddr_slide(i);
break;
}
}
}

static void _get_jsc_symb_table(nlist_t **symtab, char **strtab, uint32_t *nsyms, intptr_t *slide) {
mach_header_t *header = nullptr;
_get_jsc_header_and_slide(&header, slide);
if (header == nullptr) {
return;
}

segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;

uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
}
}

if (!symtab_cmd || !linkedit_segment) {
return;
}

uintptr_t linkedit_base = (uintptr_t)*slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
*symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
*strtab = (char *)(linkedit_base + symtab_cmd->stroff);
*nsyms = symtab_cmd->nsyms;
}

static uintptr_t _get_jsc_symb_addr(const char *symb) {
static NSMutableDictionary *_cache = nil;
if (_cache == nil) {
_cache = [[NSMutableDictionary alloc] initWithCapacity:10];
}

NSNumber *cache_value = _cache[@(symb)];
if (cache_value != nil) {
return cache_value.unsignedIntegerValue;
}

nlist_t *symtab = nullptr;
char *strtab = nullptr;
uint32_t nsyms = 0;
intptr_t slide = 0;
_get_jsc_symb_table(&symtab, &strtab, &nsyms, &slide);

if (!symtab || !strtab || !nsyms) {
return 0;
}

for (int i = 0; i < nsyms; i++) {
char *symbol_name = strtab + symtab[i].n_un.n_strx;
if (symbol_name[0] && symbol_name[1] && strcmp(symb, &symbol_name[1]) == 0) {
uintptr_t addr = (uintptr_t)symtab[i].n_value + slide;
cache_value = @(addr);
[_cache setObject:cache_value forKey:@(symb)];

return addr;
}
}

return 0;
}

使用以上代码,现将私有 API 的符号进行混淆存储,然后运行时先反混淆,最终使用 _get_jsc_symb_addr 来获取对应私有 API 的地址即可。

总结

本篇其实比较简单,也算不上什么奇淫技巧,但在某些场景还是会有其实用价值。另外本文中所有的黑魔法,都是我对 WebKit 源码进行阅读后获得的,所以建议大家如果有其他想法,且发现目前 JavaScriptCore / WKWebView 公开的 API 无法满足,那么可以尝试去 WebKit 源码中进行查找。比如如何获得 JSContext 分配的堆内存大小,如何快速验证 WKWebView 崩溃后的兜底逻辑,其实用点心都能找到答案。

另外,三年前刷了一两百道 LeetCode,这一次不想刷了,因为它给不了我所想要的出路。后续我大概率会关注目前 Java/Go 的后端技术发展,毕竟这么多年没关注,都已经诞生了 SpringBoot 这样妖孽化的框架了。