背景
库其实就是一段二进制代码,加上一些头文件。 使用库无非就两种情况:
- 提供服务,但是不希望别人看到源码。
- 减少工程编译时间。
使用库的时候只需要 Link 一下,不会浪费编译时间。Link 的方式有两种,静态和动态。于是就有了动态库和静态库。
静态库
静态库即静态链接库(Mac : xx.a)。静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。它的好处是,编译完成后,目标程序没有外部依赖,可以直接运行。但是上面也说了,它会把代码复制到目标程序,这无疑会增加程序体积。
![](https://img.haomeiwen.com/i2301467/9455d780865775a0.png)
动态库
动态库即动态链接库(Mac : xx.dylib/ xx.tbd)。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。
动态库的优点是,因为不需要拷贝到目标程序中,所以不会影响目标程序的体积。同时,编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。动态库带来的问题主要是,动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境。
问题来了?
- 动态库在运行的时候才进行加载,那么目标程序是如何映射动态库的?
Talk is cheap ,Show me the code
新建工程MachO ,其中ViewController 代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"Hello world");
}
接着用otool 命令简单看一下,MachO 可执行文件用到的动态库。
tool -L MachO
输出结果如图所示:
![](https://img.haomeiwen.com/i2301467/048cbd40c83becbc.png)
可以看到,这么简单的一个程序,里面用到了5个动态库。这5个动态库,基本上所有App 都会用到。来看一下整个MachO 的结构吧。使用MachOView,看一下我们的MachO 文件。
![](https://img.haomeiwen.com/i2301467/ec67c9bdcaaef5b6.png)
对Mach-O 文件不熟悉的可以查阅:基础 。这里只关注Dynamic load info,它 结构是这样的:
struct dyld_info_command {
uint32_t cmd; /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
uint32_t cmdsize; /* sizeof(struct dyld_info_command) */
uint32_t rebase_off;
uint32_t rebase_size;
uint32_t bind_off;
uint32_t bind_size;
uint32_t weak_bind_off;
uint32_t weak_bind_size;
uint32_t lazy_bind_off;
uint32_t lazy_bind_size;
uint32_t export_off;
uint32_t export_size;
};
- FYI,可以在命令行,通过xcrun dyldinfo -opcodes MachO 查看dymanic load info
几个重要的参数:
dymanic load info | |
---|---|
Rebase info | 重定向数据 |
Binding info | 进行动态绑定依赖的dyld的函数 |
Lazy Binding info | 需要从动态库加载的函数符号 |
Export info | 对外开放的函数 |
rebase修复的是指向当前镜像内部的资源指针(mach-o每次加载到内存中不是固定的地址)
binding就是将这个二进制调用的外部符号进行绑定的过程。 比如我们objc代码中需要使用到NSObject, 即符号OBJC_CLASS$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起。
lazyBinding就是在加载动态库的时候不会立即binding, 当时当第一次调用这个方法的时候再实施binding。 做到的方法也很简单: 通过dyld_stub_binder 这个符号来做。 lazy binding的方法第一次会调用到dyld_stub_binder, 然后dyld_stub_binder负责找到真实的方法,并且将地址bind到桩上,下一次就不用再bind了。
weakBinding针对弱符号进行绑定的过程, 在c语言中,函数、初始化的全局变量、static变量是强符号,未初始化的全局变量是弱符号。 weakBinding这一步会把AllImages中所有含有弱符号的映像合并成一个列表,再进行binding. 所以这一步放在最后,等加载完成后在做操作。
动态链接库的加载步骤具体分为5步:
- load dylibs image 读取库镜像文件
- Rebase image
- Bind image
- Objc setup
- initializers
可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。 rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改。bind在其后进行,查询符号表,来指向跨镜像的资源。
![](https://img.haomeiwen.com/i2301467/597c87b3b252c249.png)
在Rebase Info 中 :
11: 高位0x10表示设置立即数类型,低位0x01表示立即数类型为指针
22: 表示REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB + 2 重定向到数据段2。结合上面的信息,就是重定向到数据段2,该段数据信息为一个指针。
![](https://img.haomeiwen.com/i2301467/17dfdf4cf7a303eb.png)
Lazy Binding 中的Bind_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB + 2(截图所示)。可以看到指针指向_NSLog .
那么程序是如何调用动态库中的NSLog 。使用Hopper 来开看一下程序执行到ViewDidLoad ,究竟做了什么。下面是ViewDidLoad 的汇编代码截图:
![](https://img.haomeiwen.com/i2301467/76250336aeab241f.png)
整个跳转的流程,大概如下:
1) x0 寄存器保存着参数:Hello world,存在在:0x100008000 + 0x60。0x100008000其实是dyld_stub_binder。
跳转到imp___stubs__NSLog
0000000100001762 bl imp___stubs__NSLog
segment _TEXT section _text
2) imp___stubs__NSLog 是一段汇编指令,地址是在stubs区,该区存放的是二进制文件中未定义符号的占位符。
编译器生成代码时会创建对符号桩区的调用,链接器在运行时解决对桩的这些调用。
链接器解决方案是在被调用的地址处,放置一条br指令。br指令将控制权转交给真实的函数体。
代码执行最后跳转到0x100008010 。
imp___stubs__NSLog:
0000000100006ba4 nop ; CODE XREF=-[ViewController viewDidLoad]+76
0000000100006ba8 ldr x16, #0x100008010
0000000100006bac br x16
segment _TEXT section _stubs
3)0x100008010 指向数据段__la_symbol_ptr
_NSLog_ptr:
0000000100008010 dq _NSLog ; DATA XREF=imp___stubs__NSLog+4
segment _DATA section _la_symbol_ptr
4)
0000000100010000 db 0x00 ; '.' | segment External Symbols . in Foundation.frame/Foundation
section name | |
---|---|
__la_symbol_ptr | lazy symbol pointers |
__nl_symbol_ptr | Non lazy symbol pointers |
__stubs | 描述了Indirect Symbols和lazy符号的对应关系。 |
__stubs_helper | 这其实是整个lazy绑定的起始点,dyld_stub_binding_helper函数位于其起始处。 |
__stubs
![](https://img.haomeiwen.com/i2301467/3bd508407294fd78.png)
__la_symbol_ptr
![](https://img.haomeiwen.com/i2301467/83d61a09cd6b6435.png)
__nl_symbol_ptr
![](https://img.haomeiwen.com/i2301467/bf44464e8866ece9.png)
在__stubs , __la_symbol_ptr ,中都可以看到_NSLog 符号。
- imp___stubs__NSLog 在__stubs 相当于占位符,imp___stubs__NSLog 的地址是0000000100006ba4 ,它执行一段汇编代码,目的是跳转到__la_symbol_ptr 的0000000100008010 地址跳转到。 最后通过0000000100008010 ,定位到NSLog 函数。
以上是个人理解,大家互相学习,有问题请多指教
Ref:
DYLD Detailed
网友评论