链接

编译器驱动程序

7.1 编译流程

例如:编译main.c[gcc -Og -o prog main.c]

  1. 运行C预处理器(cpp),将C的源程序翻译为ASCII码的中间文件main.i
  2. 运行C编译器(ccl),将mian.i翻译成ASCII汇编语言文件main.s
  3. 运行汇编器(as),将main.s翻译成可重定位目标文件,main.o
  4. 运行链接器程序(ld),将main.o以及一些必要的系统目标文件结合起来,形成一个可执行目标文件(prog)
运行可执行文件

例如:要运行我们上面生成的prog[./prog] shell调用操作系统中一个叫做加载器(loader)的函数,将可执行文件中的代码和数据复制到内存,然后将控制转移到这个程序的开头

7.2 静态链接

连接器主要有2个任务:

  1. 符号解析:目标文件定义和引用的符号都对应一个函数OR变量。符号解析的目的是将符号引用跟符号定义关联起来
  2. 重定位:编译器合汇编器生成从地址0开始的代码和数据姐。连接器通过把每个符号定义与一个内存地址关联起来,从而重定位这些节。然后修改对这些符号的引用,使他们指向对应的地址

7.3 目标文件

  1. 可重定位目标文件:对应生成的.o 文件
  2. 可执行目标文件:可以直接执行的文件,对.o文件进行连接后,可以直接加载到内存并执行的文件
  3. 共享目标文件:可以在加载或运行时被动态的加载进内存并链接,类似framework

7.4 可重定位目标文件

.O文件,可以用MatchOView打开,或者在命令行使用:来查看 其中:

  1. .symtab:符号表,它存放在程序中定义合引用的函数和全局变量的信息(每个重定向文件中都会有一张符号表,除非特意用STRIP去掉),这个符号表和debug的符号表不同,.symtab符号表不包含局部变量的条目
  2. .debug: 调试符号表,包含了局部变量,类型定义+全局便利,以及原始的C源文件(就是我们的dSYM),只有用-g选项调用编译器驱动程序时,才会生成这张表

7.5 符号和符号表

符号表包含了定义和饮用的所有符号的信息。 富豪中会有一些UNDEF的符号:这些符号表示在本目标模块中饮用,却在其他地方定义的符号(例如:我们引用了.a里面的某个变量方法)

7.6 符号解析

链接的时候,连接器需要解析符号饮用的每个方法+变量,将编译时未确定的符号跟其他可重定位目标文件的符号表关联起来 所以,类似于,下面的代码,可以编译通过,但无法运行,因为链接的时候,无法找到foo的符号

1
2
3
4
5
void foo(void);
int main() {
    foo();
    return 0;
}
7.6.1 连接器如何解析多重定义的全局符号

如果两个文件中都定义了X,一个没赋值,一个赋值了,则系统会取赋值的作为X的值,如果都赋值了,则报错

7.6.2 与静态库链接

静态库以一种成为归档的特殊文件格式存放的,里面是一组连接起来的可重定位目标文件的集合 我们引用.a后,在链接时,链接器只会赋值程序被引用的目标模块

7.6.3 连接器如何使用静态库来解析引用

链接器内部维护了3个集合:E(可重定位目标文件) U(未解析的符号) D(已定义的符号)

  1. 链接器会判断输入是集合还是目标文件,如果是集合就解析除目标文件,然后全部放入E集合
  2. 同时将内部的符号,未解析的放入U,已定义的放入D
  3. 之后扫描出来的目标文件,会先去U里面找,是否有关联自己的符号,如果有,则将该符号移动到D,并将该目标文件放入集合E中,没有任何引用,则直接抛弃。
  4. 当编译器完成扫描后,发现U不为空,则报错。如果为空,则合并E中的目标文件,构建输出可执行文件。 注意:这种算法严重依赖先后顺序,例如:先链接print.a 然后才是main.c 这个时候print.a内部所有的可执行文件都会被抛弃(因为扫描的时候U里面没有符号)。然后就链接失败
7.7 重定位
  1. 重定位节和符号定位:链接器将所有的目标文件中统一类型的节归类到统一个地方,并分配唯一的运行时内存地址
  2. 重定位节中的符号引用:针对每个符号的引用,都使其只想正确的运行时地址
7.7.1 重定位条目

所有的位置未知的引用,都会放在重定位条目中

7.7.3 重定位符号引用

直接根据重定位条目中的信息+特定的算法,就可以算出正确的重定位后的地址。然后做重定位,之后就可以直接加载执行了

7.8 可执行目标文件

7.9 加载可执行目标文件

将程序复制到内存,并通过跳转找到程序的第一条指令或入口点来运行该程序 iOS在解析dSYM的时候,会让你选择是arm64 还是x86。然后分别偏移了0x100000000 0x4000。这是因为在不同的架构中代码段总是从这个地址开始的。

7.10 动态链接共享库

动态链接库,只有在程序运行或加载时才会通过动态链接器将数据copy到程序的内存中(比静态库的优势:1.不用每次更新库,就需要更新程序 2.不用到处copy占用空间)

  1. 依赖动态库的可执行文件在链接的时候,只会加入到动态库的重定位和符号表信息
  2. 之后在应用运行的时候才回去执行动态链接器,加载动态库的代码和数据

7.11 从应用程序中加载和链接共享库

C,JAVA有相应的接口加载指定的动态库,并替换已经加载在内存中的方法.实现不重启服务即可更新动态链接库

7.12 位置无关代码

PIC:主要是因为无论内存在何处加载目标模块,数据段和代码段的距离总是保持不变的。因此,代码段中的任意指令与数据段中的任意变量之间的距离在运行时都是一个常量,而与代码和数据加载的绝对内存位置无关。 延迟绑定:一个程序只会调用共享库中一部分的程序,将函数地址的绑定推迟到实际调用的时候,能够避免动态链接器在加载的时候进行成百上千不需要的重定位。

PIC

由于无论我们在存储器中的何处加载一个目标模块,数据段总是分配为紧随在代码段后面。因此,代码段中任何指令和数据段中的任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对存储位置是无关的。基于上面的事实,编译器在数据段开始的地方创建了一个全局偏移量表(global offset table, GOT)。GOT包含每个被这个目标模块引用的全局数据目标的表目。编译器还为GOT中每个表目生成一个重定位记录。

7.13 库打桩机制

库打桩:可以解惑共享库函数的调用,取而代之执行自己的代码


--EOF--

若无特别说明,本站文章均为原创,转载请保留链接,谢谢