本文是<<iOS开发高手课>> 第五篇学习笔记.
首先 简单来说,链接器最主要的作用,就是将符号绑定到地址上。
编译器
iOS 开发使用的是编译器,先使用编译器把代码编译成机器码,然后直接在 CPU 上执行机器码的。之所以不使用解释器来运行代码,是因为苹果公司希望 iPhone 的执行效率更高、运行速度能达到最快。
解释器运行代码的速度不够快是因为解释器会在运行时解释执行代码,获取一段代码后就会将其翻译成目标代码(就是字节码(Bytecode)),然后一句一句地执行目标代码。也就是在运行时才去解析代码,这样就比在运行之前通过编译器生成一份完整的机器码再去执行的效率要低。
但解释器在运行时去执行代码,说明它具有动态性,程序运行后能够随时通过增加和更新代码来改变程序的逻辑。
也就是说,程序跑起来后不用重新启动,就可以看到代码修改后的效果,缩短了调试周期。程序发布后,你还可以随时修复问题或者增加新功能,用户也不用一定要等到发布新版本后才可以升级使用。所以说,使用解释器可以帮我们缩短整个程序的开发周期和功能更新周期。
总结说来
- 采用编译器生成机器码执行的好处是效率高,
- 缺点是调试周期长。解释器执行的好处是编写调试方便,缺点是执行效率低。
关于iOS开发使用编译器可以简单看下 https://www.jianshu.com/p/18740f6ea88f
编译时链接器做了什么?
编译器会对每个文件进行编译,生成 Mach-O(可执行文件);链接器会将项目中的多个 Mach-O 文件合并成一个。
对于 iOS 系统,可执行文件就是 Mach-O文件。
Mach-O 文件里面的内容,主要就是代码和数据:代码是函数的定义;数据是全局变量的定义,包括全局变量的初始值。
不管是代码还是数据,它们的实例都需要由符号将其关联起来。因为 Mach-O 文件里的代码,比如 if、for、while 生成的机器指令序列,要操作的数据会存储在某个地方,变量符号就需要绑定到数据的存储地址。
你写的代码还会引用其他的代码,引用的函数符号也需要绑定到该函数的地址上。
而链接器的作用,就是完成变量、函数符号和其地址绑定这样的任务。而这里我们所说的符号,就可以理解为变量名和函数名。
不做符号绑定的问题
如果地址和符号不做绑定,但机器需要知道你操作的内存地址,你就需要在写代码时给每个指令设好内存地址。就像直接在和不同平台的机器沟通,连编译生成 AST 和 IR 的步骤都省掉了,甚至优化平台相关的代码都需要你自己编写。可读性和可维护性都会很差,修改代码后对地址的维护就会让你崩溃。原因就是代码和内存地址绑定得太早。
除了可读性和可维护性差之外,还会有更多的重复工作。因为,需要针对不同的平台写多份代码,
链接器为什么合并 Mach-O 文件。
项目中文件之间的变量和接口函数都是相互依赖的,所以需要通过链接器将项目中生成的多个 Mach-O 文件的符号和地址绑定起来。
没有这个绑定过程的话,单个文件生成的 Mach-O 文件是无法正常运行起来的。因为,如果运行时碰到调用在其他文件中实现的函数的情况时,就会找不到这个调用函数的地址,从而无法继续执行。
链接器在链接多个目标文件的过程中,会创建一个符号表,用于记录所有已定义的和所有未定义的符号。链接时如果出现相同符号的情况,就会出现“ld: dumplicate symbols”
的错误信息;如果在其他目标文件里没有找到符号,就会提示“Undefined symbols”
的错误信息。
链接器主要工作
- 去项目文件里查找目标代码文件里没有定义的变量。
- 扫描项目中的不同文件,将所有符号定义和引用地址收集起来,并放到全局符号表中。
- 计算合并后长度及位置,生成同类型的段进行合并,建立绑定。
- 对项目中不同文件里的变量进行地址重定位。
一些功能函数随着业务的发展会导致用不到。日长月久,无用的函数越来越多,生成的 Mach-O 文件也就越来越大。这时,链接器在整理函数的符号调用关系时,就可以帮你理清有哪些函数是没被调用的,并自动去除掉。
那这是怎么实现的呢?链接器在整理函数的调用关系时,会以 main 函数为源头,跟随每个引用,并将其标记为 live。跟随完成后,那些未被标记 live 的函数,就是无用函数。然后,可以通过Dead code stripping
配置链接器自去除无用代码的功能。并且,这个开关是默认开启的
动态库链接
在真实的开发中,很多功能都是现成的,不光你能够用,其他 App 也在用,比如 GUI 框架、I/O、网络等。
链接这些共享库到你的 Mach-O 文件,也是通过链接器来完成的。
链接的共用库分为静态库和动态库:
- 静态库是编译时链接的库,需要链接进你的 Mach-O 文件里,如果需要更新就要重新编译一次,无法动态加载和更新;
- 而动态库是运行时链接的库,使用 dyld 实现动态加载。
Mach-O 文件是编译的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。
dlopen 会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen 也可以选择是立刻解析所有引用还是滞后去做。dlopen 打开动态库后返回的是引用的指针,dlsym 的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用。
使用 dyld 加载动态库,有两种方式:有程序启动加载时绑定和符号第一次被用到时绑定。为了减少启动时间,大部分动态库使用的都是符号第一次被用到时再绑定的方式。
加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 Clang Attribute 的 constructor 修饰函数。每个函数、全局变量和类都是通过符号的形式定义和使用的,当把目标文件链接成一个 Mach-O 文件时,链接器在目标文件和动态库之间对符号做解析处理。
网友评论