[iOS]编译与链接基本概念
其实一张图就能很明了
上图
这篇博客主要用来作我的学习笔记
所以咱们精简一些叙述
首先是补课一下一些基本概念
GCC、GNU到底啥意思?
还是不太够
有人说那个LLMV和clang是什么意思你也没说啊
来!
LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本
Clang是一个C++编写、基于LLVM、发布于LLVM
BSD许可证下的C/C++/Objective-C/Objective-C++编译器。比起GCC,Clang编译速度快、占用内存小、非常方便进行二次开发
咱们写的代码到可执行文件可以分解为 4 个步骤,分别是预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking)
编译与链接的过程
预处理
首先是源代码文件 main.c 和相关的头文件如 stdio.h 等被预处理器预处理成一个 .i 文件
第一步预处理的过程相当于如下命令(-E 表示只进行预处理)
$gcc –E main.c –o main.i
预处理过程主要处理那些源代码文件中的以"#“开始的预处理指令。比如”#include"、"#define"等,主要处理规则如下:
- 将所有的"#define"删除,并且展开所有的宏定义
处理所有条件预处理指令,比如"#if"、“#ifdef”、“#elif”、“#else”、“#endif” - 处理"#include"预处理指令,将被包含的文件插入到该预处理指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件
- 删除所有的注释"//“和”/* */"
- 添加行号和文件名标识,比如 #2 “main.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号
- 保留所有的 #pragma 编译器指令,因为编译器须要使用它们
经过预处理后的 .i 文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到 .i 文件中
所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预处理后的文件来确定问题。
编译
编译就是把预处理生成的文件进行一系列词法分析、语法分析、语义分析、优化,生成相应的汇编代码文件
这个过程是整个程序构建的核心部分,也是最复杂的部分之一
$ gcc -S hello.i -o hello.s
现在版本的GCC把预编译和编译两个步骤合并成了一个步骤,使用一个叫cc1的程序来完成
该程序位于/usr/lib/gcc/x86_64-linux-gnu/4.8/
,我们可以直接调用cc1来完成它
$ /usr/lib/gcc/x86_64-linux-gnu/4.8/cc1 hello.c
事实上,对于不同的语言,预编译与编译的程序是不同的,如下所示:
C:cc1
C++:cc1plus
Objective-C:cc1obj
Fortran:f771
Java:jc1
GCC是对这些后台程序的封装,它会根据不同的参数来调用预编译程序cc1、汇编器as、链接器ld
汇编
汇编就是将汇编代码转换成机器可以执行的指令
每一个汇编语句几乎都对应一条机器指令
汇编过程相对于编译比较简单,其没有复杂的语法、语义,也无需做指令优化
只是根据汇编指令和机器指令的对照表进行翻译。
$ gcc -c hello.s -o hello.o
UNIX环境下主要有三种类型的目标文件:
(1)可重定位文件
其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
(2)共享的目标文件
这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个 目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
(3)可执行文件
它包含了一个可以被操作系统创建一个进程来执行之的文件。汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作
链接
编译和汇编后得到的是 .o 的二进制文件,但是不能独立允许。链接是编译的最后一步,会将之前编译好的 .o文件,系统库的 .o 文件和库文件彼此相连接,把某个目标文件中引用的符号同另一个文件中的定义链接起来,将所有编译好的单元组成一个可执行文件
$ gcc main.o -o main
链接过程主要包括地址和空间分配(Address and Storage Allocation)、**符号决议(Symbol Resolution)和重定位(Relocation)**等。每个模块的源代码文件经过编译器编译成目标文件(Object File,一般拓展名为.o或.obj),目标文件和库(Library)一起链接形成最终可执行文件。
每个模块都是单独编译,编译器编译hello.c时并不知道引用的函数的地址,所以暂时把调用该函数的目标地址搁置,等待最后链接时由链接器将这些指令的目标地址修正。链接器在链接时根据所引用的符号,自动去相应的模块查找该符号的地址,然后将a.c模块中所有引用到该符号的指令重新修正,让其目标地址为真正的符号的地址
上面这种地址修正的过程叫做重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relacation Entry)
链接可分为静态链接和动态链接
静态链接在编译阶段就将库文件的所有代码加到可执行文件中,因此生成的程序体积更大,其后缀名一般为 .a
- 代码装载速度比动态库快,执行效率也略高。
- 不依赖于外部库安装环境,部署方便。
静态库链接顺序:gcc 链接库的顺序是从右到左的,假设 main.cpp 依赖 liba.a,liba.a依赖libb.a,则链接顺序为 g++ main.cpp liba.a libb.a,如果修改顺序就会链接报错
动态链接在编译链接时并不会把库文件的代码加到可执行文件中,而是在运行时加载所需的动态库,后缀名一般为 .so
- 生成的可执行程序更小。
- 共享库是通过mmap映射的方式实现文件共享,多进程运行时更加节省内存。
- 库文件修改时,可执行文件不需要重新编译,只需要重启即可。
动态库和静态库
编译方式
静态库是在编译时将库的代码打包到可执行程序中,因此生成的可执行程序包含了所有用到的库函数的代码。这样,当程序被调用时,需要使用哪些库函数就直接从可执行文件中取出来使用。因为代码打包进了可执行程序中,因此静态库的生成通常需要在代码的编译阶段进行。
动态库则是在运行时动态加载到程序中的,因此生成的可执行文件并不包含库函数的实现代码,而只是引用了动态库的接口。当程序调用到该库函数时,操作系统会将该函数从动态库文件中加载到内存中供程序运行使用。这样一来,程序的可执行文件会比静态库生成的可执行文件小很多。因为代码加载是在程序运行时进行的,所以动态库的链接通常是在程序运行之前进行。
内存使用方式
由于静态库的代码被打包进了可执行程序中,所以在程序运行时,静态库中的代码被复制到了程序使用的内存中,并一直驻留在内存中使用,因此不需要占用额外的内存空间。、
而动态库的代码在程序运行时才会被加载到内存中,因此动态库的代码实现被复制进内存,会占用额外的内存空间。但是与静态库相比,动态库的内存使用方式具有更好的空间和性能优势,因为多个程序可以共享同一个动态库,而不需要重复加载相同的库文件,从而减少了系统的内存占用。
更新和维护方式
静态库的代码被打包成可执行程序的一部分,因此静态库的更新和维护需要重新进行编译和部署,才能让所有使用了该静态库的程序都能够得到更新的代码。
动态库可以独立于程序进行更新,因为动态库作为一个单独的文件存在于系统中,可以被多个程序共享。因此,当需要更新动态库时,只需要替换掉旧的动态库文件,不需要重新编译和部署所有使用了该动态库的程序。
因此,如果需要多个程序共享同一个库,或者需要较少的内存占用,则使用动态库可能更为合适。如果需要保持部署和更新的稳定性,则静态库可能更为适合
dyld(the dynamic link editor)
查资料一直看到,所以什么是framework
framework是一种文件的打包方式,把头文件,二进制文件,资源文件封装在一起,方便管理和分发。所以动态库和静态库的格式都有.framework
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分
在系统内核做好程序准备工作之后,交由dyld负责余下的工作
它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节
dyld
另外 对于dyld的加载过程有一篇很优雅的博客
dyld详解
dyld的主要功能包含一些load、bind、link之类的对库进行的操作
dyld分为几个版本,dyld、dyld2、dyld3,目前主要使用的是dyld2和dyld3
iOS-底层原理 15:dyld发展史
macOS Dyld Process
dyld 1.0(1996-2004)
dyld 1包含在NeXTStep 3.3中,在此之前的NeXT使用静态二进制数据。作用并不是很大,
dyld 1是在系统广泛使用C++动态库之前编写的,由于C++有许多特性,例如其初始化器的工作,在静态环境工作良好,但是在动态环境中可能会降低性能。因此大型的C++动态库会导致dyld需要完成大量的工作,速度变慢
在发布macOS 10.0和Cheetah前,还增加了一个特性,即Prebinding预绑定
。我们可以使用Prebinding技术为系统中的所有dylib和应用程序找到固定的地址。dyld将会加载这些地址的所有内容。如果加载成功,将会编辑所有dylib和程序的二进制数据,来获得所有预计算。当下次需要将所有数据放入相同地址时就不需要进行额外操作了,将大大的提高速度。但是这也意味着每次启动都需要编辑这些二进制数据,至少从安全性来说,这种方式并不友好
dyld 2(2004-2017)
2004年在macOS Tiger中推出了dyld 2
dyld 2是dyld 1完全重写的版本,可以正确支持C++初始化器语义,同时扩展了mach-o格式并更新dyld。从而获得了高效率C++库的支持。
dyld 2具有完成的dlopen和dlsym(主要用于动态加载库和调用函数)实现,且具有正确的语义,因此弃用了旧版的API
- dlopen:打开一个库,获取句柄
- dlsym:在打开的库中查找符号的值
- dlclose:关闭句柄。
- dlerror:返回一个描述最后一次调用dlopen、dlsym,或 dlclose 的错误信息的字符串。
dyld的设计目标是提升启动速度。因此仅进行有限的健全性检查。主要是因为以前的恶意程序比较少
同时dyld也有一些安全问题,因此对一些功能进行了改进,来提高dyld在平台上的安全性
在 dyld 2 中,对 dyld 进行了改进和优化,以提升性能、增强安全性等。之前提到由于启动速度大幅提升,可以减少 Prebinding 的工作量,且仅在软件更新时编辑系统库,而 dyld 2 就是为了实现这些优化以及其他相关的优化功能而产生的。它具有一些新特性,如对 C++初始化器语义的正确支持、ASLR(地址空间配置随机加载)、codesigning(代码签名)、share cache(共享代码)等,这些特性有助于提高系统的性能、安全性和稳定性。通过 dyld 2 的优化,进一步改善了应用程序的启动速度和系统的整体性能
dyld 2工作流程
- 解析mach-o头部
- 查找依赖库
- 映射mach-o文件,放入地址空间中
- 执行符号查找
- 使用ASLR进行rebase和bind绑定
- 运行所有初始化器
- 执行main函数
dyld 3(2017-至今)
dyld 3是2017年WWDC推出的全新的动态链接器,它完全改变了动态链接的概念,且将成为大多数macOS系统程序的默认设置。2017 Apple OS平台上的所有系统程序都会默认使用dyld 3.
dyld 3最早是在2017年的iOS 11中引入,主要用来优化系统库。
而在iOS 13系统中,iOS全面采用新的dyld 3来替代之前的dyld 2,因为dyld 3完全兼容dyld 2,其API接口也是一样的,所以,在大部分情况下,开发者并不需要做额外的适配就能平滑过渡
性能:想要尽可能的提高启动速度
安全性:在dyld 2中增加了安全特性,但是很难跟随现实情形,虽然做了很多工作,但是难以实现这个目标
可靠性和可测试性:为此Apple发布了很多不错的测试框架,例如XCTest,但是这些测试框架依赖于动态链接器的底层功能,然后将测试框架的库插入进程中,所以不能用于测试现有的dyld代码,且难以测试安全性和性能水平
dyld 3工作流程
- 进程外:将dyld 2中的mach-o头部解析、符号查找移到了进程外执行,且将其执行结果放入启动闭包,存储到磁盘中
- 进程内:验证启动闭包正确性,并映射dylib,执行main函数
- 启动闭包缓存服务