链接器、静态库、符号解析与重定位、动态链接共享库

  一、编译器产生未解决符号表、导出符号表、地址重定向表

    ①未解决符号表:编译器在编译一个编译单元时,会将编译单元中只声明未定义的全局符号放入未解决符号表(声明为extern的全局对象或者是仅声明的全局函数,声明为extern作用于函数没有实际意义)。这些未解决符号的地址,在编译时地址会置为空即0x00000,代表为解决。(如果是函数则只有调用指令call,操作数为空,call num()被编译为e8 00 00 00 00,变量同理)

    注意:这里的全局与静态static不同

//以下符号均被放入为解决符号表
extern int x;
extern int foo();   //作用等同于
static int y; //属于静态变量,不属于全局变量
int foo(); int main(){..}

    ②导出符号表:一个编译单元内被定义为全局变量或全局函数的符号会被放入导出符号表。

int x=0int foo(){
return 0;
};
int main(){...};

 

    ③地址重定向表(重定位条目):这个条目告诉链接器在将目标文件合并成可执行文件时如何修改这个应用。格式如下:

typedef struct{
long offset;   //需要被修改的引用的节偏移
long type:32,      //重定位类型(如何修改新的引用)
       symbol:32    //被修改引用应该指向的符号,即对应符号表地址
long addend;      //一个符号常数,一些类型的重定位需要它对被修改引用的值做偏移} Elf64_Rela;

    过程:链接器通过重定位条目,将目标文件的引用符号的地址修改为运行时地址。具体的修改方法有很多种,例如重定位PC相对引用(type为R_X86_64_PC32)、重定位绝对引

     

 

  二、链接器的作用:

  1.符号解析:将每个编译单元的未解决符号表中的符号与其他编译单元中导出符号表中的符号相互关联。

    ①多重定义的全局符号分类:

    全局符号分为强符号和弱符号。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号,另外共享库(标准库)中定义的函数或符号一般是弱符号例如operator new函数,可以由用户进行重写。

    ②确定多重定义的全局符号规则:

    规则1:不允许多个同名的强符号。

    规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。

    规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。

  注意:①C++中区分变量只通过名字区分,不同类型的同名变量会被当成多重定义。区分函数则只通过函数名和参数,不同返回类型的函数被视为多重定义。(C++通过name mangling来编译重载函数)

     ②内置类型的全局变量会被编译器默认初始化为0,所以会被当成强符号。

  2.重定位:合并输入模块,并为每个符号分配运行时地址。

  

  三、静态库

  1、静态库:相关函数被编译为独立的目标模块,然后封装成一个独立的静态库文件(linux系统中静态库以存档archive的文件格式储存.a文件,window中以.lib文件储存。这样可以将标准库函数的编译和程序的编译分离,减少编译时间和内存占用。

  2、静态库的解析引用:

  ①原理:当链接器需要构造一个可执行的目标文件时,他只复制静态库里被应用程序应用的目标模块。

  ②过程:(以linux为例)链接器会维护一个未解决符号集合U,可重定位目标文件E,已定义符号集合D(导出符号表的集合)

gcc -static -o prog2c main.o ./libvector.a     //以可重定向目标文件和库libvector.a生成可执行文件prog2c

  1):对于链接器上的每个输入文件,链接器会判断输入文件f是目标文件还是存档文件。如果输入文件f是目标文件,则链接器将输入文件f的未解决符号表映射到集合U,把导出符号表映射到D,把文件添加到E。然后继续输入下一个文件。

  2):如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m的导出符号表中有一个符号来解析U中的一个引用,那么将m添加到E中,并且链接器修改U和D来反映m中的导出符号表和未解决符号表。对存档中的所有成员都进行此过程,直到U和D都不在发生变化,此时,任何包含在E中的成员文件都被简单的丢弃,然后处理下一个输入文件。

  3):当扫描完所有输入文件后,如果U是非空的则会输出一个错误。否则会合并和重定位E中的目标文件,构建输出的可执行文献。

  注意:gcc命令的目标文件和库的顺序很重要,因为库的解析过程是用库自己的导出符号表去匹配链接器中的U,而不会将库的导出符号表加入到D中,所以如果库.a在.o文件之前,则可能会链接失败

 

  四、重定位

  1.过程:

  ①重定位节和符号定义(合并输入模块):这一步中,链接器将所有相同类型的节合并成同一类型的新的聚合节,即将所有目标文件合并到可执行文件(代码段合并到代码段,数据段合并到数据段),这样所有目标文件的符号都到了可执行文件。然后链接器将运行时内存地址付给新的聚合节,付给输入模块定义的每个节以及每个符号。这样程序中的每条指令和全局变量都有一个唯一的运行时内存地址。

  ②重定位节中的符号引用:这一步中,链接器通过重定位条目来修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行地址。

 

参考:https://blog.csdn.net/qq_38350702/article/details/107646058

 

  五、动态链接共享库

  允许多个正在运行的进程共享内存中相同的库代码

  1.位置无关代码PIC:静态链接使用库函数时要把目标代码复制,占用内存,使用库定义的全局变量时,要在代码段进行重定位,每个进程的运行时地址都不同所以每个进程运行时不能共享代码段,所以要使用位置无关的代码。

  ①PIC数据引用:无论目标模块被加载器加载到内存的哪个位置,代码段与数据段的距离总是保持不变。所以代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量(取决于运行时数据被放在数据段的哪个位置)。

  所以编译器在数据段开始的地方创建一个全局偏移量表(GOT)。在GOT中,每个被这个目标模块引用的全局数据都有一个8字节条目。并且为每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使其包含目标的正确的绝对地址。而代码段中引用目标变量的地方则只需在编译期改成 (0xoffset)%rip (rip存放当前指令地址,即当前指令地址+当前指令与GOT中对应条目地址的偏移)。

                                

 

  ②PIC函数调用:

    PIC函数调用通过过程链接表PLT和全局偏移表GOT来进行延迟绑定,借此来避免赋值目标函数到模块(这样会修改代码段)。

  延迟绑定:将过程地址的绑定(函数名和地址相对应)推迟到第一次调用该过程时。

  过程链接表(PLT):PLT是一个数组,属于代码段的一部分,其中每个条目是16字节代码,PLT[0]是一个特殊条目,他跳转到动态链接器中。每个被可执行程序调用的库函数都有他自己的PLT条目。每个条目都负责调用一个具体的函数。

  全局偏移量表(GOT):GOT是一个数组,属于数据段的一部分,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。

  过程:①在目标函数被调用时会首先调用其在PLT中的对应条目。

 

     ②第一次进入PLT对应条目时会直节跳转到GOT中对应的条目,而每个GOT条目初始时都只想它对应的PLT指令条目的第二条指令(PLT一个条目有很多指令),这个跳转只是简单的把控制传回PLT对应条目中的下一条指令。

     ③在PLT对应条目中剩余的指令会把ID压入栈中,然后跳转到PLT[0]

     ④PLT[0]通过GOT[1]间接的把动态链接器的一个参数压入栈中,然后通过GOT中的条目间接跳转进动态链接器。动态连接器用两个栈条目来确定被调用函数的运行时位置,然后重写第二步中跳转的GOT条目(这样下次调用次函数时,这个条目不会跳转到PLT而是直接跳转到对应函数),然后再把控制传递到被调用函数。

posted @ 2021-09-23 23:05  放不下的小女孩  阅读(1007)  评论(0编辑  收藏  举报