基本环境 Linux 32

本文主要介绍编译流程中的链接流程以及链接相关的一些文件,同时会涉及到一部分加载的内容。

一、编译流程

文本 sample.c 文件 --|预编译|--> 文本 sample.i 文件
文本 sample.i 文件 --|编译|--> 文本 sample.s 文件
文本 sample.s 文件 --|汇编|--> 二进制 sample.o 文件
二进制 sample.o 文件 --|链接|--> 二进制 sample 文件

二、链接流程

在 C 源文件中定义的全局变量和全局函数,在编译中称为符号,编译器并不关心局部变量或局部函数的名字,因为局部变量的创建和回收是通过用户栈进行管理的。而已初始化的全局变量需要在编译时就分配好空间,确定好地址。
链接流程主要做了两件事,一是对符号进行解析,所谓符号解析,就是将符号的声明和符号的定义联系在一起;二是进行符号引用的重定位,即确定符号引用的运行时地址。

2.1 输入文件

链接的输入文件是一种后缀为 .o 的二进制文件。称为可重定位的目标文件。
Linux 系统中可重定位目标文件是一种 ELF 格式的文件(ELF,Executable and Linkage Format)。文件中的信息通过一些节组织在一起。

2.1.1 ELF 文件结构

ELF 文件中一般都会有以下内容:

  • ELF 头 包含文件类型,文件大小,节头部表的偏移位置等信息
  • .text 所有机器指令都存储在此处
  • .rodata 存放只读,例如格式化字符串和switch跳转表
  • .data 存放已经初始化的全局符号
  • .bss 存放尚未初始化的全局符号
  • 节头部表 记录文件中包含的各个节的基本信息

2.1.2 可重定位目标文件结构

汇编生成的可重定位目标文件是无法执行的,因为在 text 节或 data 节中存在某些外部符号的引用还未确定运行时地址。这些外部符号在其他目标文件中定义,需要借助链接过程进行确定。因此在可重定位目标文件中,有两个节对还未确定地址的符号引用进行了描述。

  • .symtab 存放符号表
  • .ref.text 指示机器指令中需要重定位的符号
  • .ref.data 指示全局符号中需要重定位的符号

2.2 符号解析

汇编器在生成可重定位目标文件时,会在目标文件中生成一个符号表。符号表指明了文件中有哪些符号,符号是全局符号还是本地符号,符号的定义所在的节,以及在节中的相对位置。有些符号在本模块中引用,但是在其他模块中定义,汇编器无法确定这种外部符号的定义位置,而需要借助链接器进行符号解析步骤得到。链接器根据传入的各个目标文件完成符号解析后,所有模块中的符号引用都能与某个模块中的符号表条目对应起来。

2.3 重定位

可重定位目标文件中记录的位置都是一些相对位置,因为一个可执行文件往往需要多个可重定位目标文件生成,链接器在生成可执行文件的过程中需要将输入的所有可重定位目标文件中的各个节进行重新聚合,并为每个节确定运行时地址。这个时候所有指令和符号定义的运行时位置都确定了。

三、加载流程

链接器将所需要的各个可重定位目标文件组织在一起,经过符号解析和重定位之后,生成一个可执行目标文件。

3.1 可执行目标文件

可执行目标文件同样是一个 ELF 文件。其特有的节包括:

  • 段头部表 将文件节映射到运行时存储器段 为加载提供便利
  • .init 用于指示程序计数器的值

3.2 加载

加载一个可执行文件时,应用通过 execve 系统调用访问常驻内存的加载器 Loader ,加载器将可执行文件中的数据和指令拷贝到存储器中,并跳转到程序的入口点 entry point

四、静态链接与动态链接

按照前面的链接流程,得到的可执行文件是可以直接执行而不需要依赖于其他文件。用起来很方便,但是有个问题,每次生成一个可执行文件都需要把相关目标文件都复制一份,十分浪费空间。
动态链接可以很大程度上改善这个问题。

4.1 静态库

在上面的链接流程中,我们每次链接都需要将所有相关的可重定位目标文件依次作为参数传入链接器,对于大型项目来说,这会是一个很繁琐且很容易出错的步骤。静态库是基于此背景下提出的一个解决方案。静态库就像是一个集合包,把许多重定位目标文件都聚合在一起作为一个整体。链接的时候,只需要将静态库整体作为参数传入即可。链接器会从静态库中取出生成可执行文件所需要的可重定位目标模块。

静态库可以通过 AR 工具生成

$ gcc -c add.c mul.c
$ ar rcs libcal.a add.o mul.o

生成的 libcal.a 就是一个静态库文件,里面包含了 add.o 和 mul.o 中的两个目标模块。

4.2 动态库

动态库也称作可共享目标文件,同样是一种 ELF 文件,并且是可重定位目标文件的一种特殊形式。
动态库之所以称为共享目标文件,有两个原因,其一是动态库作为文件存储在磁盘中时,不同的应用都共享其数据和代码。其二是动态库在内存中,不同应用共享同一片存储区域。

$ gcc -shared -fPIC -o libcal.so add.c mul.c
  • shared 生成共享目标文件
  • fPIC 生成与位置无关的代码

4.3 动态链接流程

动态链接的方式有两种,一种是加载时链接,一种是运行时链接。对于加载时链接,需要在编译时就知道所需要的动态库的信息,并在编译时将动态库的重定位信息和符号表存入可执行目标文件,并在可执行文件中新增一个 .interp 节指示动态库的路径。在加载时,加载器调用动态链接器 ld-linux.so 将实际的动态库一并载入存储器。因此加载时动态链接在编译和加载时都需要用到动态库。
对于运行时链接,则编译时和加载时都与静态链接一致,而在程序中调用相应的动态库加载代码。

4.3.1 加载时动态链接

$ gcc -o p main.c ./libcal.so

4.3.2 运行时动态链接

#include <dlfcn.h>

// 打开动态库
// @param filename 共享库文件位置
// @param flag 
//                  - RTLD_NOW 立马解析动态库的外部符号
//                  - RTLD_LAZY 不解析动态库中的外部符号,等到代码执行时再解析
//                  - RTLD_GLOBAL 将符号的解析结果存在全局空间,供其他链接使用。如果可执行文件编译时带有 -rdynamic,那么其全局符号也可用
// @return 成功返回句柄,失败返回 NULL
void *dlopen(const char *filename, int flag);

// 获取动态库中某符号的地址
// @param handle 共享库句柄
// @param symbol 符号名
// @return 成功返回符号的地址,失败返回 NULL
void *dlsym(void *handle,char *symbol);

// 关闭动态库
// 如果该动态库没有在别处使用,那么此调用将卸载动态库
// @param handle 共享库句柄
// @return 成功返回 0 ,失败返回 -1
int dlclose(void *handle);

// 获取错误信息
// 如果有报错返回报错字符串,如果没有报错,那么返回 NULL
const char *dlerror(void);

tangyikejun
259 声望36 粉丝

引用和评论

0 条评论