05-链接器:符号是怎么绑定到地址上的?

一、知识点

1.1编译器和解释器

iOS编写的代码是使用编译器将代码编译成机器码,直接在CPU上运行机器码。像Java是先使用编译器将代码编译成字节码,再通过解释器将字节码解释为不同平台的机器码。

  • 编译器优点是执行效率高,缺点是调试周期长。
  • 解释器优点是方便调试,缺点是执行效率低。

1.2 iOS的编译器

iOS现在的编译器集合叫做LLVM,其内置编译器为lld。编译器将每个文件都编译成Mach-O(可执行文件),链接器将多个Mach-O合并成一个。
编译的过程:

  • LLVM预处理代码,比如将宏嵌入到对应的位置。
  • LLVM对代码进行词法分析和语法分析,生成AST(抽象语法树)。AST结构更简单,遍历更快,而且可以快速生成IR。
  • 最后AST生成IR(一种更接近于机器码的语言),IR可以生成多份适合不同平台的机器码。对于iOS来说,IR生成的机器码是Mach-O。

1.3 链接器的功能

  • 将变量、函数符号和其地址绑定起来
  • 将多个Mach-O文件合成一个

1.4 动态库链接

静态库是编译时链接的库,会链接到Mach-O文件中。动态库是运行时链接的库,使用dyld实现动态加载。
dyld做了哪些事:

  • 先执行 Mach-O 文件,根据 Mach-O 文件里 undefined符号加载对应的动态库,系统会设置一个共享缓存来解决递归依赖问题。
  • 加载后,将 undefined 的符号绑定到动态库里对应的地址上。
  • 最后再处理 +load 方法,main 函数返回后运行 static terminator。

二、课后作业

在 App 运行时通过 dlopen 和 dlsym 链接加载 bundle 里的动态库。

实现思路

1.制作一个简单的包含动态库的bundle文件
2.在运行时通过dlopen函数打开对应的动态库可执行文件,通过dlsym函数找到对应符号的函数进行调用。

2.1 如何制作动态库和bundle文件

这一步就不做过多解释了,网上很多对应的文章。这里我还是以戴铭老师对应专题里的例子文件来做。Boy.m文件代码如下:

#import "Boy.h"

@implementation Boy

void mytest(int a) {
    NSLog(@"%s --- parama = %d",__func__,a);
}

- (void)say {
    NSLog(@"%s",__func__);
}

@end

示例工程结构如下图(其中Test.framework即是Boy文件编译的动态库):


工程结构

2.2 运行时加载动态库

1. 先找到动态库中可执行文件的路径

// 包含动态库的bundle路径
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"dyld" ofType:@"bundle"];
// framework的路径
NSString *frameworkPath = [[NSBundle bundleWithPath:bundlePath] pathForResource:@"Test" ofType:@"framework"];
// 动态库中可执行文件的真实路径
NSString *dyldFilePath = [frameworkPath stringByAppendingString:@"/Test"];

如下图所示,framework中的Test才是动态库的可执行文件


动态库可执行文件

2.打开动态库

// 以指定模式打开指定的动态链接库文件,并返回一个句柄
// 不同的模式的详细介绍见dlopen百度百科
void *handle = dlopen([dyldFilePath UTF8String], RTLD_LAZY);
// handle == null 表示打开动态库失败,dlerror能获取到失败的信息
if (!handle) {
    NSLog(@"dlopen error == %s",dlerror());
}
NSLog(@"dlopen = %s",handle);

3.通过符号找到函数并执行

// 定义函数
void(*pMytest)(int);
// 通过"mytest"符号找到其对应的函数
pMytest = dlsym(handle, "mytest");
// pMytest == null 表示没有找到符号对应的地址,dlerror能获取到失败的信息
if (!pMytest) {
    NSLog(@"dlsym error == %s",dlerror());
}else {
    // 调用函数
    pMytest(5);
}

4.通过runtime调用OC方法

// runtime调用oc方法
[self runtimeCallMethod];
// 可以试试把该方法的调用放到dlopen之前,看看有什么区别
- (void)runtimeCallMethod {
    Class Boy = NSClassFromString(@"Boy");
    id boy = [[Boy alloc] init];
    
    SEL boySaySel = NSSelectorFromString(@"say");
    
    [boy performSelector:boySaySel withObject:nil afterDelay:0];
}

5.控制台打印结果

2019-05-05 22:20:28.939416+0800 05-加载bundle中的动态库[95027:7738314] dlopen = \M-`\M-I=\^O\^A
2019-05-05 22:20:28.939622+0800 05-加载bundle中的动态库[95027:7738314] dlsym = UH\M^I\M-eH\M^C\M-l\^PH\M^M\^E\M-y\^A
2019-05-05 22:20:28.939726+0800 05-加载bundle中的动态库[95027:7738314] mytest --- parama = 5
2019-05-05 22:20:28.956245+0800 05-加载bundle中的动态库[95027:7738314] -[Boy say]

已经能成功调用私有动态库中的C函数和OC方法了。

最后附上完整代码

更多详细内容,请移步至戴铭老师的专栏

你可能感兴趣的:(05-链接器:符号是怎么绑定到地址上的?)