1.为了构造可执行文件,链接器必须完成两个主要任务:
(1)符号解析:目的是将每个符号引用正好和一个符号定义关联起来
(2)重定位:编译期和汇编器生成从地址0开始的代码和数据节,通过把每个符号定义与一个内存位置关联起来,修改所有对这些符号的引用,使得它们指向这个内存位置
2.目标文件(*.o):一个以文件形式存放在磁盘中的字节序列,是按照特定的目标文件格式来组织的,Linux下为ELF,格式如下图所示:
3.ELF可重定位目标文件
如图所示,夹在ELF头和节头部表之间的都是节(section),主要有:
(1).text:已编译程序的机器代码(就是经过预处理->编译->汇编生成的代码的机器码)
(2).data:已初始化的全局和静态变量
(3).bss:未初始化的全局和静态变量。区分已初始化和未初始化主要是为了节省空间
(4).symtab:符号表,存放在程序中定义和引用的函数以及全局变量的信息
(5).rel.text/.rel.data:一个.text/.data中位置的列表,包含重定位信息(因为生成的目标文件是从地址0开始的,需要在运行时根据此列表修改为真实的地址)
3.符号表的类型
(1)全局符号:由模块m定义并被模块m引用的全局符号
(2)外部符号:由其他模块定义并被模块m引用的全局符号
(3)局部符号:只被模块m定义和引用的局部符号(c中为static变量和函数,要与局部变量区分开)
注意,.symtab中不包含对应于本地非静态程序变量的任何符号(局部变量的符号),这些符号在运行时在栈中被管理。有个例外,就是本地静态变量不在栈中管理,而是在.data或.bss为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号
4.符号表的条目(entry)
即.symtab节在目标文件中是如何被组织的,下面用代码来说明其格式:
typedef struct { int name; /*字节偏移,指向符号的字符串名字*/ char type:4; /*类型(数据或函数)*/ char binding:4; /*本地或者全局*/ char reserved; short secotion; /*被分配到目标文件的哪个节中*/ long value; /*距定义目标的节的起始位置的偏移*/ long size; /*大小*/ }
用GNU READELF程序读取某个目标文件,结果如下:
5.符号解析
链接器解析符号引用的方法是将对每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来:
(1)局部符号:对于和引用定义在相同模块中的局部符号的引用,符号解析简单明了,编译器只允许每个模块中每个局部符号有一个定义
(2)全局符号:当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,会假设该符号在其他模块中定义,生成一个链接器符号表条目,并把它交给链接器处理(如sum)
在此阶段,链接器从左到右按照各个文件(*.o:目标文件,*.a:存档文件)在命令行上出现的顺序来扫描可重定位目标文件和存档文件。在这次扫描中,链接器维护一个可重定位目标文件的集合E,一个未解析的符号集合U,以及一个在前面输入文件中已定义的符号集合D。初始时,E、U、D均为空:
(1)对于命令行的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件(可以看做是目标文件的集合)。如果是目标文件,那么链接器把f添加到E中,修改U和D来反映f中的符号定义和引用(对于上图来说,会根据.symtab文件中的条目,将main和array放入D,将sum放入U),并继续下一个输入文件
(2)如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m(是一个目标文件)定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且修改U和D来反映m中的符号定义和引用。对存档文件中所有的成员目标文件都依次进行这个过程,知道U和D都不在发生变化。此时,任何不包含在E中的成员目标文件都简单地被抛弃,并继续处理下一个输入文件
(3)如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。否则,它会合并和重定位E中的目标文件,构建输出的可执行文件
原文:https://www.cnblogs.com/cedriccheng/p/9974864.html