程序执行前经历了啥
当我们编写了一个程序,你想法设法让他运行,在你的编辑器编译运行。
编译是啥,运行前经历了啥。
用一张图就是这样表示的。
预处理器cpp:
gcc -E a.c -o a.i
假设看一个程序没有include:
加入加了include
所以预处理器cpp就是对这个程序进行预处理,可以展开头文件或者宏替换还有去掉注释,有时候头文件里面有条件编译,
也可以翻译成一个.i文件。
编译器cc1:
编译阶段是检查语法,生成汇编:gcc -S a.i -o a.s
将预处理过的.i文件搞成了汇编程序,至于里面进行了怎样的复杂过程,怎样优化,我也不是很清楚,有时间研究,只要知道它是用来翻译成汇编程序就行了。
注意上面的两个文件都是ASCII文件
汇编器as:
汇编代码转换机器码 gcc -c a.s -o a.o
这个汇编器之后就是一个可重定位的目标程序了,等会儿看看啥样子
链接器:
链接过程将多个目标文以及所需的库文件链接成最终的可执行文件
gcc a.o -o a
今天就重点来初识一下链接器所做的链接过程
静态链接
静态链接就是根据一系列可重定位的目标文件和命令行的参数,来生成一个已经完全链接的,而且可以加载运行的可执行文件的过程
不知道什么意思没有关系,我们一步一步来梳理就行。
这个静态链接由一个静态链接器完成,主要执行两个过程。
一个是符号解析
,一个是重定位
。
又懵了,什么是符号,解析他干嘛,重定位又是啥?
还是一步一步来
首先要知道啥时符号吧
符号:不说了,上图吧。
似乎明白了,不就是一些符号啥的嘛,好像没啥了不起的。先有个印象
符号解析这个过程先不说,先来看看那个我们之前所说的可重定位的目标文件。
它是经汇编程序翻译过来的二进制文件。
首先要知道Linux系统使用可执行可链接格式,即ELF。
可重定位文件分成了一节节的内容,即符合ELF格式的一种二进制文件。
先来上一份代码吧
int count = 0;
int boy = 1;
int k;
void main(){
static int p = 1;
count += p;
}
我们使用readelf -S b.o
查看一下内容
我们需要重点观察.bss,.data,.text.symtab
我们通过图片可以知道它是一节节的组成的。
我们重点在于符号表.symtab
使用readelf -s b.o
(这里是小s)可以查看符号表的内容
.symtab:
就是通过readelf -s
> 命令弄出来的,上面这张图,value对应的在对应节的偏移量,Size大小,Type类型,Bind是本地还是全局还是弱符号,Ndx对应于ELF文件(可重定位目标文件)的哪一节。
.bss:
对应上面图片Ndx项的4,用于存放未被初始化的全局变量和静态C变量,但是有个情况,如果你指定了一个为初值为0的变量,就像上面的count,它是一个初始化为0的全局变量,但是它被分配在了.bss字段。还需要注意的就是,如果一个静态变量被分配空间的话,它有个特殊的,它只适合本地模块,也就是说只需要在本地保证唯一就行了,就算其他模块有同名的强符号或者弱符号,本地模块只会引用本地的这个静态变量,除非你指定了用外部模块的。对于bss更细微的定义就是,它一般只保存未初始化的静态变量和初始化位0的全局变量,静态变量,而未初始化的全局变量暂时在符号表中定义为COMMON,就像上面的k一样。
.data:
对应上面图片的3,用于存放已经初始化的全局变量和静态C变量,但是初始值为0的没有分配在这个地方,和上面一样。
.text:
存放是编译后的机器代码
我们了解了符号和符号表,就可以来进行符号解析了
符号解析
就是将某个模块的符号引用与这个符号在另一个模块的定义相关联
写了两个程序
我们把他们编译成可重定位的文件
有三种集合,链接器进行符号解析时,需要创建维护这三个集合。
- E:合并在一起的所有目标文件(还未重定位)
- U:没有解析的符号(定义符号和引用符号还没有被建立联系)
- D:定义符号的集合
当链接器接收到了一个可重定位文件
看看它是不是库文件
库文件:将所有没有解析的符号(U)与库文件匹配,如果匹配上了,就将匹配的模块放入E中,然后将这个U中的符号放到D中,知道U和D不在变化,库文件剩下的全不要了。
非库文件:就是可重定位的目标文件,先把它放到E中,分别将未解析的符号和定义的符号放到U和D中,如果D中有未解析符号的定义,那么就可以将U中的那个未解析的符号和D中定义的相关联。
比如如果链接上面的两个可重定位文件
虽然说符号建立的关联,但是仅仅建立了管理而已,我们根本不知道符号对应地址在哪里。
你看这些全是0,根本不知道对应的符号地址在哪里,你要它怎么去执行命令,这不是为难它吗?
所以我们还需要一个过程,就是重定位
重定位
重定位就是为每个符号分配对应的地址
- 合并相同类型的节,然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。
- 然后对定义符号进行重定位
- 对引用符号进行重定位,那么每个被引用符号的位置,都为改成对应分配的地址
你看将两个可重定位文件重定位后:
所有的地址都变了。
相对寻址
对于一个重定位条目,里面保存了一个数据结构的信息
第一就是给定重定位偏移量offset,还有就是修正值addend,还有重定位寻址类型(相对,绝对)
还有就是重定位的符号r.symbol
计算机会给我们的信息就是每个节的信息(比如.text代码节,记作ADDR(s)),然后还有重定位符号地址(ADDR(r.symbol))
我们先根据重定位偏移量找到我们需要重定位的地址address = ADDR(s)+offset
然后我们要根据重定位符号地址和修正值,来更新重定位的引用地址
则ADDR(r.symbol) +addent-address就填充进我们要重定位的引用地址值
绝对寻址:
绝对寻址比较粗暴简单,直接对符号地址加以修正 ADDR(r.symbol+addend)即可