链接器一般采用两步链接的方法:
(1)空间与地址分配:扫描所有输入的目标文件,然后将符号表中所有的符号定义与符号引用收集起来放到全局部符号表中。获取输入目标文件段的长度,并将它们合并,计算出输出文件中各个段合并后的长度与位置,然后建立映射关系。
(2)符号解析与重定位:读取输入文件中段的数据、重定位信息,进行符号解析与重定位、调整代码中的地址。
1.静态链接
1.1空间与地址分配
1.1.1相似段合并
对于输入的多个目标文件,链接器一般采用“相似段合并”的方法将相同性质的段合并到一起,如下图:
1.1.2符号地址的确定
当合并相似段之后,链接器开始计算各个符号的虚拟地址,由于各个符号在段内的相对位置是固定的,所以链接器只需要给每一个符号加上一个偏移量,使得它们能够调整到正确的虚拟地址上。
1.2符号解析与重定位
1.2.1符号解析
目标文件的复习部分:符号与符号表
①符号:
每个可从定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息,在链接器的上下文中,有三种不同的符号:
- 由模块m定义并能在其他模块引用的全局符号
- 模块m引用的其他模块的所定义的全局符号
- 模块m定义的只能够在当前模块使用的符号。它们对应于带static属性的C函数和局部变量
②符号表:
符号表是一个Elf64_Symbol结构体类型的数组,Elf64_Symbol结构体类型的定义如下:
typedef struct
{
int name;//字符串表中的字节偏移
char type:4,//通常要么是数据,要么是函数
binding:4;//表示符号是本地的还是全局的
char reserved;
short section;//每个符号被分配到目标文件的哪个节
long value;//符号的地址,对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移
long size;//是目标的大小
}Elf64_Symbol;
(1)链接器解析多重定义的全局符号
规则:
- 不允许出现多个同名的强符号
- 当出现同名的强符号和弱符号的时候,选择强符号
- 如果多个弱符号同名,那么从这些弱符号中选择所占字节较大的那个符号,如果多个弱符号所占字节大小都相同,会任意选择一个
(2)如何使用静态库来解析引用
①相关基本概念:
- 静态库:
相关函数可以被编译为独立的目标模块,再封装成一个单独的库文件,然后应用程序通过在命令行上指定单独的文件名字来使用这些在库中定义的函数。 - 存档文件:
静态库以一种称为存档的特殊文件格式存放在磁盘中,存档文件是一组连接起来的可重定位目标文件的集合。 - 需要注意的是:
在链接时,链接器将只复制被程序引用的目标模块。
②链接器如何使用静态库来解析引用?
在符号的解析阶段,链接器从左到右按照它们在编译器驱动程序命令上出现的顺序来扫描可重定位目标文件和存档文件,在扫描的过程中,链接器维护着三个集合:
- 集合E:可重定位目标文件集合
- 集合U:未解析的符号集合
- 集合D:在前面输入文件中已定义的集合
对命令行上的每一个输入文件f,链接器都会判断f是一个目标文件还是一个存档文件。
- 如果是目标文件:链接器会把f添加到集合E中,然后修改U,D来反应f中的符号定义和引用,并继续下一个输入文件。
- 如果是存档文件:链接器会尝试匹配U中未解析的符号和存档文件成员m中定义的符号,如果由匹配的,就会将这个成员m添加到E中,并且链接器修改U和D来反应n中的符号定义和引用,对于存档文件中那些没有匹配上的成员则丢弃。
- 如果链接器完成对命令行上的输入文件扫描后,U非空,链接器就会输出一个错误并终止,表名还有符号没有解析。
1.2.2重定位
(1)重定位表
重定位表用来保存与重定位相关的信息。它在ELF文件中有一个或者多个。
比如:代码段有要被重定位的地方,那马就会有一个“.rel.data”的段保存数据段的重定位表。
对于32位的Intel x86系列的处理器来说,重定位表是一个Elf32_Rel结构的数组,每一个数组元素对应一个重定位入口。
typedef struct
{
Elf32_Addr r_offset;//重定位入口的偏移
Elf32_Word r_info;//重定位入口的类型和符号
}Elf32_Rel;
(2)如何进行重定位
通过objdump来查看目标文件的重定位表,例如:
上面的R_386_PC32是绝对寻址修正,R_386_PC32是相对寻址修正。
①绝对寻址修正:修正后的地址为该符号的实际地址。修正方法:S+A
②相对寻址修正:修正后的地址为符号距离被修正位置的地址差。修正方法:S+A-P
其中:
A:被存在被修正位置的值
P:被修正的位置(该值可以通过r_offset计算得到)
S:符号的实际地址。
2.动态链接
共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程称为动态链接。是由一个叫做动态链接器的程序来执行的。
2.1动态链接共享库
共享库两种不同的共享方式:
①一个库只有一个.so文件,所有引用该库的可执行目标文件共享这个.so文件中的代码和数据。
②在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。
动态链接共享库的基本思路:
当创建可执行文件时,静态的执行一些链接,然后在程序加载时,动态完成链接过程。
例如:(假定prog21里面需要动态链接libc.so,libvector.so)
当加载器加载和运行可执行文件prog21时,加载部分链接的可执行文件prog21。接着,当prog21包含一个.interp节,这一节包含动态链接器的路径名,动态链接器本身就是一个共享目标,然后加载和运行这个动态链接器,最后动态链接器通过执行下面的重定位完成链接任务:
- 重定位libc.so的文本和数据到某个内存段
- 重定位libvector.so的文本和数据到另一个内存段
- 重定位prog21中所有对由libc.so和libvector.so定义的符号和引用
最后,动态链接器将控制传递给应用程序。
2.2从应用程序中如何加载和连接共享库
Linux系统为动态链接器提供了接口,使得应用程序在运行时加载和链接共享库。
①void *dlopen(const chae *filename,int flag);
此函数加载和链接共享库filename
flag参数必须要么包括RTLD_NOW,该标志告诉链接器立即解析对外部符号的引用,RTLD_LAZY标志链接器推迟符号解析直到执行来自库中的代码。
②void *dlsym(void* handle,char* symbol);
此函数输入的是一个子项前面已经打开了的共享库的句柄和一个symbol符号,如果symbol这个符号存在,就返回符号的地址,否则返回NULL
③int dlclose(void* handle)
此函数卸载该共享库
④const char* dlerror(void)
此函数返回一个字符串,他描述的是调用dlopen、dlsym、dlclose函数时发生的最近的错误,如果没有错误发生返回NULL。
3.如何加载可执行目标文件
当加载器运行时,它创建内存映像,在程序头部表的引导下加载器将可执行文件的片复制到代码段和数据段,接下来加载器跳转到程序的入口点_start函数的地址,此函数调用系统启动函数_ _libc_start_main,它会初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。