链接

  C 代码的完整编译流程分为四个阶段:预处理、编译优化、汇编以及链接。前三个阶段对代码进行分析、优化和转换,最后一个阶段将程序依赖的所有对象文件进行整合,最终生成可执行的二进制文件。其中,链接根据发生时刻的不同,分为编译时链接加载时链接执行时链接

一、静态链接

  编译时链接最常用的叫法是静态链接,在程序编译的过程中完成链接,比如链接目标文件(.o)、静态库等。

1.静态链接与动态链接

  与静态链接对应的是动态链接,在程序被编译时,我们可以选择使用静态链接还是动态链接。

——静态链接在程序准备执行之前,其正常运行所需的代码就已经成为了可执行文件的一部分

——使用动态链接的程序,编译器在编译时在可执行文件中留下用于占位的槽,在执行的时候相关代码被加载进内存

  动态链接与静态链接最的区别体现在以下四点:

——可执行文件的体积:对于静态链接来说,所有代码在编译的时候都打包进了可执行文件,所以可执行文件体积较大。

——程序执行的效率:对于动态链接来说,依赖代码在启动程序的时候被加载进内存,所以启动效率略低于静态链接。

——程序的可移植性:两者其实是差不多的,只是动态链接的程序在移植的时候需要将相关依赖代码生成的动态库一并打包移走。

——程序更新的难易程度:对于静态链接来说,更改某个功能模块需要编译整个工程,提交上一个可执行程序;但是对于动态链接来说,只需要提交一个功能相关的库。

2.静态链接的处理过程

1.可重定位目标文件的基本结构

  Linux 平台上的 .o 文件是一种叫做可重定位文件的 ELF 文件类型,其内部遵循着和二进制可执行 ELF 文件相同的布局,但不包含与动态视图相关的 Segment 和 Program 头部。与静态链接密切相关的几个 section 如下所示:

——.symtab:符号表,存放程序定义和引用的所有函数与全局变量的信息。

——.rela.text:.text section 的重定位信息表,内部存放着当链接器将当前文件和其他文件进行静态链接组合时,.text 中需要被修改的代码。

——.rela.data:.data section 的重定位信息表,内部存放着当连接器将当前文件和其他文件进行静态链接组合时,.data 中需要被修改的值。

  每个可重定位目标文件都有一个符号表,可以通过 nm 命令将其打印出来:

 

  第一列表示在对应 section 中的偏移量;第二列表示符号类型,T 表示函数,D 表示已初始化的全局变量,U 表示符号是未定义的;第三列表示符号的具体名称。连接器在整合 .o 文件时,为每一个符号找到定义符号的地方,这个过程叫做符号解析,如果不能找到,会抛出 “undefined reference to symbol” 错误。

2.处理过程

——第一步,符号解析。编译器在编译源码时,会为找不到定义的符号生成一个特定的符号表条目,并将找到这些符号定义的任务交给链接器,链接器在信息符号解析时,在全局符号表中搜索进行搜索。如果链接器找到了符号的多个定义,会按照一定的规则进行解析。编译器在编译源代码时,为每一个全局符号指定对应的“强弱”信息,同时将其编码在符号对应的符号条目中。通常来说,函数和已初始化的全局变量为强符号,未初始化的全局变量为弱符号。链接器对符号定义的选择会根据以下规则进行:

  ——一个强符号和多个弱符号同时存在,选择强符号。

  ——多个弱符号同时存在,任选一个弱符号。

  ——多个强符号存在,抛出链接错误。

  符号之所以有强弱之分,是为了在不确定符号是否被显示定义的情况下,链接器仍然可以选择对应的弱类型符号来编译程序。同时,可以使用 __attribute__((weak)),将定义的函数标记为弱类型。

——第二步,重定位

  在符号解析的过程中,链接器为每一个引用的符号都找到了定义。接下来,链接器根据之前收集的所有信息,将多个目标文件内相同的 section 进行合并。同时为这些 section 以及程序运行过程中用到的符号指定运行时 VAS 的地址。链接器会修改 .text 和 .data 这两个 section 中对每个符号的引用信息,以便他们可以指向正确的运行时地址。重定位的主要目标是将各个独立的目标文件内的外部符号的引用地址进行修正,编译阶段不知道这些地址的真实位置,通常用 0 作为地址占位符。

二、动态链接

  如果一个程序依赖于一些功能模块,当它依赖的是静态库时,每次功能模块更新时都需要将应用程序与模块重新链接,这很不方便。如果是动态库的话,只需要将原有的动态库和新的动态库进行替换。同时,如果这些模块是可以被多个进程使用的功能模块,静态链接到可执行文件中会使得可执行文件过大,这些功能模块的副本也会随着多个进程的运行,被多次加载到内存中,浪费内存资源。动态链接有两种,一种是加载时链接,发生在程序代码执行之前;另一种是运行时链接,发生在程序运行的任意时刻。

1.关于动态链接的一些知识点

1.动态库

  动态库是一种叫做 ET_DYN 的 elf 格式文件类型,在 Linux 下以 .so 结尾,使用 gcc name.c -shared -fPIC -o name.so 命令生成动态库,fPIC 参数的含义是位置无关代码。有时会提示找不到动态库,这可能是因为动态库路径没有加入搜索路径中,加入搜索路径的方法有以下几种:

——把动态库拷贝到默认搜索路径中,/usr/lib和/lib。

——在环境变量中加入路径: export LD_LIBRARY_PATH=路径。

——在 /etc/ld.so.conf文件加入路径,运行 ldconfig ,

2.位置无关代码(PIC)

  位置无关代码是一种特殊的机器代码,这种机器代码在使用时,可以被放置在每个进程 VAS 的任意位置,无需链接器对它内部的引用地址进行重定位,可以通过 fPIC 参数指定产生这种代码。通常来说模块(独立的应用程序或共享库)之间的数据引用分为四种方式:

——模块内部的函数调用:通常以 PC-relative 的寻址方式进行,不依赖于进程 VAS 的绝对地址。

——模块内部的数据访问:由于 .data 和 .text 这两个 section 相对位置是固定的,因此也不依赖 VAS 绝对地址。

——模块之间的函数调用:在进程 VAS 中的加载位置不确定,难以真正共享同一份物理内存中的数据,于是 PIC 诞生了,其核心思想是将易变的部分抽离到进程独享的可修改内存中。编译器为此新增一个 section ,叫做全局偏移表

——模块之间的数据访问:同上。

3.全局偏移表

  全局偏移表(Global Offset Table,GOT)是位于每个模块 Data Segment 起始位置处的一个特殊表结构,位于 Data Segment 起始位置处的一个特殊表结构,每个表项中都存放有一个地址信息,这些地址信息对应于变量或函数在 VAS 中的实际地址。模块在编译时,其 Text Segment 和 GOT 之间的距离是可以被计算出来的,编译器利用这一点去引用 GOT 的表项,并为这些表项生成相应的重定位记录。当程序被加载进内存时,动态链接器修正 GOT 中的表项,进一步修正代码中符号对应的实际引用地址。但并非所有的编译器都是通过 GOT 间接引用模块中使用的全局变量

4.过程链接表

2.加载时链接

   加载时链接:动态链接器进行的符号重定位过程发生在程序代码被真正执行之前。动态链接器通过  .dynamic 中记录的信息,来完成对自己的重定位工作。接着通过访问应用程序的 .dynamic 获取程序依赖的所有外部共享库,完成动态链接。

3.运行时链接

   运行时链接:符号的重定位发生在程序的运行过程中。它的链接方式与加载时链接基本一致,但是链接时间被推迟到了程序运行的过程中。通过这种方式,程序可以在需要的时候加载动态库,并在不需要的时候卸载。其主要通过动态链接器提供的四个 API 实现,引用的头文件为 "dlfcn.h"。:

——dlopen:加载动态库。

——dlsym:从打开的共享库中获取某个具体实例。

——dlerror:获取 API 调用过程中产生的错误信息。

——dlclose:卸载动态库。

posted @ 2022-03-16 21:30  一只吃水饺的胡桃夹子  阅读(97)  评论(0编辑  收藏  举报