首先我们要了解iOS使用的是什么编译器?
苹果公司现在使用的编译器是LLVM,相比于 Xcode 5 版本前使用的 GCC,编译速度提高了 3 倍。同时,苹果公司也反过来主导了 LLVM 的发展,让 LLVM 可以针对苹果公司的硬件进行更多的优化。
LLVM是什么?
LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
广义的LLVM其实就是指整个LLVM编译器架构,包括了前端、后端、优化器、众多的库函数以及很多的模块;而狭义的LLVM其实就是聚焦于编译器后端功能(代码生成、代码优化、JIT等)的一系列模块和库。
总结来说,LLVM 是编译器工具链技术的一个集合。而其中的 lld 项目,就是内置链接器。编译器会对每个文件进行编译,生成 Mach-O(可执行文件);链接器会将项目中的多个 Mach-O 文件合并成一个。
LLVM的三个阶段
前端(Frontend)– 优化器(Optimizer)– 后端(Backend)
编译的几个重要过程:
- 写好代码后,LLVM会预处理代码,比如把宏嵌入对应的位置。
- 预处理完后,前端可以使用不同的编译工具对代码文件做词法分析以形成抽象语法树AST(语法树结构上比代码更精简,遍历起来更快,所以使用AST能够更快速地进行静态检查),然后将分析好的代码转换成LLVM的中间表示IR(intermediate representation);
- 中间部分的优化器只对中间表示IR操作,通过一系列的pass对IR做优化。IR 是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。
- 后端负责将优化好的IR解释成对应平台的机器码。
编译时链接器做了什么?
链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?链接过程到底包含了什么内容?为什么要链接?
简单来讲就是随之软件规模越来越大,每个程序被分成了多个模块,这些模块的拼接过程就叫:链接(Linking)。
链接的过程:
- 地址和空间的分配(Address and Storage Alloction)
- 符号决议(Symbol Resolution)Ps:”决议”更倾向于静态链接,而”绑定”更倾向于动态链接。
- 重定位(Relocation)
重定位的过程如下:
假设有个全局变量叫做var,它在目标文件A里面。我们在目标文件B里面要访问这个全局变量。由于在编译目标文件B的时候,编译器并不知道变量var的目标地址,所以编译器在没法确定的情况下,将目标地址设置为0,等待链接器在目标文件A和B连接起来的时候将其修正。这个地址修正的过程被叫做重定位,每个被修正的地方叫一个重定位入口。
链接的过程本质就是要把多个不同的目标文件之间相互”粘到一起”。在链接中目标文件之间互相拼合实际上是目标文件之间对地址的引用,即对函数和变量地址的引用。比如目标文件B要用到目标文件A中的函数“foo”,那么我们就称目标文件A定义(Define)了函数“foo”,称目标文件B引用(Reference)了目标文件A中的函数“foo”。这样的概念同样适用于变量。每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。
在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。
由于链接形式的不同,产生了静态链接和动态链接
静态链接
整个链接过程分为两步:
- 第一步:空间与地址的分配 扫描所有的输入目标文件,并获得它们各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
- 第二步:符号解析与重定位 使用上面第一步中收集到的所有信息,读取输入文件中断的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。
动态链接
动态链接解决了浪费内存和磁盘空间、模块更新困难等问题,要解决空间浪费和更新困难这两个问题的办法就是把程序模块互相分割开来,行程独立的文件,而不再将它他们静态地链接在一起。简单是将,就是不对哪些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。
链接的共用库分为静态库和动态库:静态库是编译时链接的库,需要链接进你的 Mach-O 文件里,如果需要更新就要重新编译一次,无法动态加载和更新;而动态库是运行时链接的库,使用 dyld 就可以实现动态加载。
由于动态共享对象会被多个程序使用,导致它在虚拟地址空间中的位置难以确定。不同模块的目标装载地址如果有相同的,那么同时导入这两个模块就会出问题。如果都不一样也不行,因为可能存在的模块太多了。没有那么多内存。所以动态共享对象需要在装载时重定位。
装载时重定位就是:在链接时,对所有绝对地址的引用不做重定位,而把这一步推迟到装载时再完成。一旦模块装在地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址进行重定位。
Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。
dlopen()函数用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程,它的C原型定义为
/*第一个参数是被加载的路径,如果是路径是绝对路径(以“/”开始的路径),则该函数将会尝试直接打开该动态库;如果是相对路径,那么dlopen()会尝试在以一定的顺序去查找该动态库文件 第二个参数flag表示函数符号的解析方式。 RTLD_LAZY:表示使用延迟绑定,函数第一次被用到时才进行绑定,即PLT机制; RTLD_NOW:表示当模块被加载时即完成所有的函数绑定工作,如果有任何未定义的符号音乐绑定工作没法完成,那么dlopen()就返回错误; RTLD_GLOBAL:可以跟上面两者任意一个一起使用(通过常量的“或”操作),它表示将被加载的模块的全局符号合并到进程的全局符号中,使得以后加载的模块可以使用这些符号。 */
void * dlopen(const char *filename,int flag);
dlsym 函数基本是运行时装载的核心部分,我们可以通过这个函数找到所需要的符号,它的定义如下:
/* 第一个参数是由dlopen()返回的动态库的句柄; 第二个参数即所需要查找的符号的名字,一个以“\0”结尾的C字符串。如果dlsym()找到了相应的符号,则返回该符号的值,没有找到相应的符号,则返回NULL。 */
void * dlsym(void * handle, char * symbol);
dlopen 会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen 也可以选择是立刻解析所有引用还是滞后去做。dlopen 打开动态库后返回的是引用的指针,dlsym 的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用。
使用 dyld 加载动态库,有两种方式:有程序启动加载时绑定和符号第一次被用到时绑定。为了减少启动时间,大部分动态库使用的都是符号第一次被用到时再绑定的方式。
加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 Clang Attribute 的 constructor 修饰函数。
每个函数、全局变量和类都是通过符号的形式定义和使用的,当把目标文件链接成一个 Mach-O 文件时,链接器在目标文件和动态库之间对符号做解析处理。
下面来看dyld的链接过程:
第一步:编写几个文件:
Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
- (void)say;
@end
NS_ASSUME_NONNULL_END
Person.m
#import "Person.h"
@implementation Person
- (void)say{
NSLog(@"hi there again!\n");
}
@end
Sayhi.m
#import "Person.h"
int main(int argc, char *argv[])
{
@autoreleasepool {
Person *person = [[Person alloc] init];
[person say];
return 0;
}
}
第二步:编译文件:
在控制台输入指令编译文件
xcrun clang -c Person.m
xcrun clang -c Sayhi.m
编译完成会生成Person.o 和 Sayhi.o 文件
第三步:将编译后的文件链接起来,这样就可以生成 a.out 可执行文件了:
在控制台输入指令,生成a.out文件(a.out 是编译器的默认名字)。
xcrun clang Sayhi.o Person.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
用 nm 工具看一下 Sayhi.o 文件:
xcrun nm -nm Sayhi.o
(undefined) external _OBJC_CLASS_$_Person
(undefined) external _objc_alloc_init
(undefined) external _objc_autoreleasePoolPop
(undefined) external _objc_autoreleasePoolPush
(undefined) external _objc_msgSend
0000000000000000 (__TEXT,__text) external _main
- OBJC_CLASS$_Person 表示Person的OC符号。
- (undefined) external ,表示未实现非私有。如果是私有的话,就是 non-external
- external _main ,表示 main() 函数,处理 0 地址,记录在 ____TEXT , ____text 区域里。
再来看Person.o文件:
xcrun nm -nm Person.o
(undefined) external _NSLog
(undefined) external _OBJC_CLASS_$_NSObject
(undefined) external _OBJC_METACLASS_$_NSObject
(undefined) external ___CFConstantStringClassReference
(undefined) external __objc_empty_cache
0000000000000000 (__TEXT,__text) non-external -[Person say]
0000000000000060 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Person
00000000000000a8 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Person
00000000000000c8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Person
0000000000000110 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person
0000000000000138 (__DATA,__objc_data) external _OBJC_CLASS_$_Person
a.out文件:
xcrun nm -nm a.out
(undefined) external _NSLog (from Foundation)
(undefined) external _OBJC_CLASS_$_NSObject (from libobjc)
(undefined) external _OBJC_METACLASS_$_NSObject (from libobjc)
(undefined) external ___CFConstantStringClassReference (from CoreFoundation)
(undefined) external __objc_empty_cache (from libobjc)
(undefined) external _objc_alloc_init (from libobjc)
(undefined) external _objc_autoreleasePoolPop (from libobjc)
(undefined) external _objc_autoreleasePoolPush (from libobjc)
(undefined) external _objc_msgSend (from libobjc)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000eb0 (__TEXT,__text) external _main
0000000100000f10 (__TEXT,__text) non-external -[Person say]
00000001000020e0 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person
0000000100002108 (__DATA,__objc_data) external _OBJC_CLASS_$_Person
0000000100002130 (__DATA,__data) non-external __dyld_private
链接器通过动态库解析成符号会记录是通过哪个动态库解析的,路径也会一起记录下来,对比a.out和Person.o, 关注那些undefined 的符号,有了更多信息,就可以知道在哪个动态库能够找到它。
可以通过otool工具查看库的位置:
xcrun otool -L a.out
a.out:
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1673.126.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1673.126.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
未定义的符号需要用的库是libSystem 和 libobjc。
dylib 这种格式,表示是动态链接的,编译的时候不会被编译到执行文件中,在程序执行的时候才 link,这样就不用算到包大小里,而且不更新执行程序就能够更新库。
参考链接:
《程序员的自我修养》
今天的文章iOS 开发 链接器的作用分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/19209.html