编译
C 语言代码最终成为机器可执行的程序,会经过以下处理:
- 预处理器:将 C 语言代码(da.c)转化成 da.i 文件(gcc –E)
- 编译器:C 语言代码(da.c, wang.c)经过编译器的处理(gcc -0g -S)成为汇编代码(da.s, wang.s)
- 汇编器:汇编代码(da.s, wang.s)经过汇编器的处理(gcc 或 as)成为对象程序(da.o, wang.o)
- 链接器:对象程序(da.o, wang.o)以及所需静态库(lib.a)经过链接器的处理(gcc 或 ld)最终成为计算机可执行的程序
- 加载器:将可执行程序加载到内存并进行执行,loader 和 ld-linux.so
本章的学习重点就是链接器部分
链接基本知识
连接器主要负责做两件事情
- 符号解析 Symbol resolution
我们在代码中会声明变量及函数,之后会调用变量及函数,所有的符号声明都会被保存在符号表(symbol table)中,而符号表会保存在由汇编器生成的 object 文件中(也就是 .o 文件)。符号表实际上是一个结构体数组,每一个元素包含名称、大小和符号的位置。
在 symbol resolution 阶段,链接器会给每个符号应用一个唯一的符号定义,用作寻找对应符号的标志。
- 重定位 Relocation
这一步所做的工作是把原先分开的代码和数据片段汇总成一个文件,会把原先在 .o 文件中的相对位置转换成在可执行程序的绝对位置,并且据此更新对应的引用符号(才能找到新的位置)
符号解析
三种对象文件
所谓的对象文件(Object File)实际上是一个统称,具体来说有以下三种形式:
- 可重定位目标文件 Relocatable object file (.o file)
- 每个 .o 文件都是由对应的 .c 文件通过编译器和汇编器生成,包含代码和数据,可以与其他可重定位目标文件合并创建一个可执行或共享的目标文件
- 可执行目标文件 Executable object file (a.out file)
- 由链接器生成,可以直接通过加载器加载到内存中充当进程执行的文件,包含代码和数据
- 共享目标文件 Shared object file (.so file)
- 在 windows 中被称为 Dynamic Link Libraries(DLLs),是类特殊的可重定位目标文件,可以在链接(静态共享库)时加入目标文件或加载时或运行时(动态共享库)被动态的加载到内存并执行
对象文件格式
上面提到的三种对象文件有统一的格式,即 Executable and Linkable Format(ELF),因为,我们把它们统称为 ELF binaries,具体的文件格式如下
链接器实际上会处理一下不同的符号,对应于代码中不同写法的部分:
- 强符号:有函数体的函数名、带初值的全局变量都是强符号。
- 弱符号:无函数体的函数名、不带初值的全局变量为弱符号。
- 全局符号 Global symbols
- 在当前模块中定义,且可以被其他代码引用的符号,例如非静态 C 函数和非静态全局变量
- 外部符号 External symbols
- 同样是全局符号,但是是在其他模块(也就是其他的源代码)中定义的,但是可以在当前模块中引用
- 本地符号 Local symbols
- 在当前模块中定义,只能被当前模块引用的符号,例如静态函数和静态全局变量
- 注意,Local linker symbol 并不是 local program variables
链接器只知道非静态的全局变量/函数,而对于局部变量一无所知。然后我们来看看局部非静态变量和局部静态变量的区别:
- 局部非静态变量会保存在栈中
- 局部静态变量会保存在 .bss 或 .data 中
那如果两个函数中定义了同名的静态变量会怎么样呢?首先,编译器会在 .data 部分为每一个静态变量进行定义,如果遇到同名,就会在本地的符号表中自动给出唯一的编号
而如果是不同文件的同名全局变量,则会按照强弱符号去判断,然后根据三个规则去处理
因此,得出结论:如果可能,尽量避免使用全局变量
如果一定要用的话,注意下面几点:
- 使用静态变量
- 定义全局变量的时候初始化
- 注意使用 extern 关键字
重定位
重定位就是把不同可重定位对象文件拼成可执行对象文件: