#链接的过程
一段简单的程序如
printf("hello world");
return 0;
这样的程序都要经过4个步骤:预处理、编译、汇编、链接才能最终运行。
预编译:
1.将所有"#define"删除,并且展开所有宏定义。
2.处理所有条件预编译指令,比如“#if”、“#elif”、“#else”、“#endif”。
3.处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能包含其他文件。
4.删除所有的注释“//”和“/* */”。
5添加行号和文件名标识,比如#“hello.c”2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
6.保留所有的#pragma编译器指令,因为编译器须要是用它们。
经过预编译后的.i文件不包括任何宏定义,因为所有的宏已经被展开,并且包含的文件已经被插入到.i文件中。所以无法判断定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
编译:
就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。
汇编:
汇编器将汇编代码变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来说比较简单,没有复杂语法,也没有语义,也不需要做指令优化,只是根据汇编指令的对照表一一翻译。使用gcc命令从c源代码文件开始,经过预编译、编译和汇编直接输出目标文件:
gcc -c hello.c -o hello.o
链接:
我们需要将一大堆文件链接起来才可以得到"a.out",即最终的可执行文件。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。链接过程主要包括地址和空间分配、符号决议、重定位等步骤。
##可重定位和可执行目标文件
一.目标文件分类
在介绍可重定位目标文件之前我们首先要了解一下什么是目标文件分哪几种。
1.可重定位目标文件
包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
2.可执行目标文件
包含二进制代码和数据,其形式可以被直接复制到内存并执行。
3.共享目标文件
一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态的加载进内存并链接。
二.如何生成可重定位目标文件
使用gcc -c test.c
test.c中的内容:
#include <stdio.h>
int main(){
printf("hello world");
return 0;
}
通过gcc命令:gcc -c test.c 会生成test.o文件及可重定位目标文件
##解读
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191207155851275.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTA1MzA2,size_16,color_FFFFFF,t_70)
2.使用readelf -S查看test.o(显示节区表内的所有信息,包括每个节的属性)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191207155947180.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTA1MzA2,size_16,color_FFFFFF,t_70)
3.使用readelf -h查看test.o(显示在ELF文件头里包含的所有信息)
![](https://img-blog.csdnimg.cn/20191207160027438.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTA1MzA2,size_16,color_FFFFFF,t_70)
4.使用readelf -s查看test.o(显示符号表的信息,包含静态符号表(.symtab)和动态符号表(.dynsym))
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191207160038861.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTA1MzA2,size_16,color_FFFFFF,t_70)
##符号及符号解析
- 符号和符号表
符号的本质:ELF文件中存储的一块空间的首地址
定义的符号(函数,非局部变量)记录在符号表
将对符号的引用存在重定位节
分类:
全局符号:不带static,本模块定义
外部符号:其他模块定义的,在本模块extern 声明
局部符号:带static
符号表:
描述该符号所在的位置(处于哪个节(索引),偏移多少,大小多少,符号类型等
- 链接规则
全局符号的类型(只有全局才能是强符号)
强符号:函数和已经初始化的变量名
弱符号:未初始化的变量名
解析规则(是否链接错误):
强符号只能有一次定义
一次强符号,多次弱符号:按强定义处理
多个弱符号定义:任选其中一个
静态链接:
静态链接对象:多个.o + 静态库(.a包括多个.o)
良好的编码风格:
避免所有函数写在一个.c中:每次修改要重新编译
避免一个.c只写一个函数:增加了链接的负担
静态共享库:多个.o打包成.a
```c
gcc -c my1.c my2.c
ar rcs mylib.a my1.o my2.o
gcc -c main.c
gcc -static -o mypro main.o ./mylib.a
链接的步骤:
符号解析得到集合E,集合D
合并E中所有.o
重定位D中所有符号
##重定位
源文件首先被编译器编译生成目标文件,目标文件种有三段内容:数据段、代码段以及符号表,所有的函数定义被放在了代码段,全局变量的定义放在了数据段,对外部变量的引用放到了符号表。
编译器在将源文件编译生成目标文件时可以确定一下两件事:
定义在该源文件中函数的内存地址
定义在该源文件中全局变量的内存地址
对于可重定位的ELF文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容。对于每个要被重定位的ELF段都有一个对应的重定位表,而一个重定位表往往就是ELF文件中的一个段,所以其实重定位表也可以叫重定位段,我们在这里统一称作重定位表。比如代码段“text”如有要被重定位的地方,那么会有一个相对应叫“, rel text”的段保存了代码段的重定位表;如果代码段“data”有要被重定位的地方,就会有一个相对应叫“ rel. data"”的段保存了数据段的重定位表。我们可以使用 objdump来查看目标文件的重定位表。
##动态链接
1.为什么要动态链接
静态链接有两大缺陷:
①浪费内存和存储空间。
因为各个可执行文件可能会调用相同的库函数及它们所需要的辅助数据结构。假设有两个目标文件Program1.o和Program2.o,都需要和lib.o进行链接形成可执行文件,当用动态链接的时候内存中只需要存在一份lib.so就可以了。
②模块更新困难。
因为当可执行文件中的一个模块更新之后,所有的模块要重新链接才可以使用。
2.动态链接的装载
①固定装载地址
固定装载地址对于动态链接来说,效果不好,因为在多个模块被多个程序使用的情况下很复杂,要人为的去安排动态库的装载地址。
②装载时重定位
静态链接所用到的重定位是链接时重定位,而这里动态链接可以用装载时重定位,当各模块装载至进程虚拟内存中之后,可以对引用到的符号地址进行重定位。
但是这也带来一个问题,就是动态库中的代码部分所有进程都共享一份,否则就失去了动态链接的意义。当动态链接模块被装载映射至虚拟空间后,由于装载时重定位的方法需要修改指令,所以没有办法做到同一份指令被多个进程共享。
为什么要修改指令?——见下面地址无关代码的㈢
③地址无关代码(PIC)(Position-independent Code)
由于装载时重定位的确定,我们希望模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变。
对于共享文件中的地址引用方式可分为4种:
㈠模块内部的函数调用、跳转等
这种情况下,只要用相对偏移指令就可以了,所以不会改变代码。
㈡模块内部的数据访问,比如模块中定义的全局变量、静态变量
模块内部的数据访问依然可以用相对寻址(指令中不能包含数据的绝对地址),虽然现代的体系结构中,数据的相对寻址往往没有相对与当前指令(PC)的寻址方式,但是还是有很多方法得到PC值之后加一个偏移进行对数据的寻址的。
㈢模块间数据访问
因为模块间的数据访问目标地址要等到装载时才能确定。地址无关代码的基本思想就是把跟地址相关的部分放到数据段里面。ELF的做法就是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用
㈣模块间调用、跳转
对于模块间的调用和跳转也可以运用GOT来解决,不同的是GOT中存储的目标函数的地址