CSAPP-----链接

版权声明: https://blog.csdn.net/zl6481033/article/details/84990262

本篇目录: 

1、编译器驱动程序

2、静态链接

3、目标文件

4、可重定位目标文件

5、符号和符号表

6、符号解析

7、重定位

8、可执行目标文件

9、加载可执行目标文件

10、动态链接库

11、从应用程序加载和链接共享库

12、位置无关码

13、库打桩机制

14、处理目标文件的工具

15、小结

16、本篇相关实践


 

 

 

本系列文章的观点和图片均来自《深入理解计算机系统第 3 版》仅作为学习使用

         链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程。这个文件可以被加载到内存并执行,链接可以执行于编译时,也就是在源代码被翻译成机器代码时,也可以执行于加载时,也就是程序被加载器加载到内存中并执行,甚至可以执行于运行时,由应用程序执行。在早期,是手动执行,现在基本都是由链接器(Linker)程序自动执行。

        链接器使分离编译成为可能,一个大型程序可以分为更小更好管理的模块,可以独立的修改编译这些模块,掌握链接的知识的好处:(1)理解链接器将帮助你构造大型程序。(2)理解链接器可以帮助你避免一些危险的编程错误。(3)理解链接帮助你理解语言的作用域规则是如何实现的,比如全局变量与局部变量的区别等。(4)理解链接能帮助你理解其他更重要的系统概念。(5)理解链接使你能够利用共享库。

1、编译器驱动程序

       这一部分内容都会以下图的程序作为参考来理解。

        

        

        上图的程序,有两个源文件组成,main.c和sum.c。

        大多数编译系统提供提供编译器驱动程序(compiler driver),它代表用户在需要时调用语言预处理器、编译器、汇编器、链接器。比如在GNU编译系统中需要输入 gcc -Og -oprog main.c sum.c  ,下图概括了驱动程序将上图程序从ASCII源文件翻译成可执行文件时的行为, 首先运行C预处理器(cpp)将C的源程序main.c翻译成一个ASCII的中间文件main.i,接下来驱动程序运行c编译器(cc1)将main.i翻译成一个ASCII汇编语言文件main.s,然后驱动程序运行汇编器(as)将main.s翻译成一个可重定位目标文件main.o,驱动程序经过相同的步骤生成sum.o,最后运行链接器程序ld,将main.o和sum.o以及一些必要的系统目标文件组合起来创建一个可执行文件prog,运行它只需要在shell中输入名字即可。shell会调用操作系统中一个加载器函数将可执行文件prog中代码和数据复制到内存,然后控制转移到这个程序的开头。

2、静态链接

        像linux中ld程序这样的静态链接器,以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载的、运行的可执行文件作为输出,输入的可重定位目标文件由不同的代码和数据节组成,每一节都是一个连续的字节序列。为了构造可执行文件链接器必须完成两个主要任务:

        *符号解析:目标文件定义和引用符号,每个符号都对应一个函数,一个全局变量或一个静态变量符号解析的目的就是将每个符号引用正好和一个符号定义关联起来。

        *重定位:编译器和汇编器生成从地址0开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定义这些节,然后修改所有对这些符号的引用,使得它们指向内存这个位置。链接器使用汇编器产生的重定位条目的详细指令。

3、目标文件

        目标文件有三种形式:

        *可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位的目标文件合并起来,创建一个可执行目标文件。

        *可执行目标文件。包含二进制代码和数据,其形式可以直接被复制到内存执行。

        *共享目标文件。一种特殊类型的可重定位目标文件,可以在加载和运行时被动态的加载进内存并链接。

        编译器和汇编器生成可重定位目标文件,链接器生成可执行目标文件。目标文件是按照特定的目标文件格式来组织的,各个操作系统的目标文件格式都不相同。

4、可重定位目标文件

        下图展示了一个典型的ELF可重定位目标文件的格式。ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(可重定位的、可执行的、可共享的)机器类型(如x86-64),节头部表的文件偏移以及节头部表中条目的大小和数量。

        

        夹在ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含下面几个节:

        .text:已编译的程序的机器代码。

        .rodata:只读数据,比如printf中格式串和开关语句的跳转表。

        .data:已初始化的全局和静态变量a,局部变量在运行时被保存在栈中,既不出现在.data中,也不出现在.bss中。

        .bss:未初始化的全局和静态变量,以及所有被初始化为0的全局变量和静态变量。在目标文件中这个节不占据实际的空间,仅仅是一个占位符,目标文件格式区分已初始化和未初始化变量是为了空间效率,在目标文件中未初始化的变量不需要占据任何实际的磁盘空间。运行时在内存中分配这些变量,初始值为0。

        .symtb:一个符号表,它存放程序中定义和引用的函数和全局变量的信息。

        .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时需要修改这些位置。

       .rel.data:被模块引用通告或定义的所有全局变量的重定位信息,一般而言,任何已初始化的全局变量,如果他的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。

        .debug:一个调试符号表,其条目是程序中定义的局部变量和定义类型,程序中定义和引用的全局变量,以及原始的C源文件,只有以-g选项调用编译器驱动程序时才会用到这张表。

        .line:原始C源程序中的行号和.text节中机器指令之间的映射,以-g选项调用编译器驱动程序时才会用到这张表。

        .strtab:一个字符串表,其中包括.symtab和.debug中的符号i表,以及节头部中的节名字,字符串表就是以null结尾的字符串序列。

5、符号和符号表

        每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息,在链接器的上下文中有三种不同的符号:

        *由模块m定义并能被其他模块引用的全局符号,全局链接器符号对于非静态的c函数和全局变量。

        *由其它模块定义并被模块m引用的全局符号,这些符号称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量。

        *只被模块m定义和引用的局部符号,它们对应于带static属性的C函数和全局变量。这些符号在模块m中任何位置可见,但不能被其他模块引用。

        定义带有C static属性的本地过程变量不是在栈中管理的。相反编译器在.data或.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。假如在同一个模块中,两个函数中都定义了一个局部静态变量x,如下图,那么这种情况下,编译器会向汇编器输出两个不同名字的局部链接器符号。

        

        在c语言中可以利用static属性隐藏变量和函数名字。符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab节中包含了ELF符号表,每个符号表包含了一个条目的数组,下图为每个条目的格式。

        

        字符表中name是字符串表中的字节偏移,指向符号的以NULL结尾的字符串名字。value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移,对于可执行目标文件来说,这个值是一个绝对运行时的地址。size是目标的大小(以字节为单位)type通常要么是数据要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目,所以这些目标的类型也有所不同。bingding字段表示符号是本地的还是全局的。

        符号被分配到目标文件的某个节,由section表示,在section中有三个伪节,其中有COMMON。现代的GCC版本根据以下规则来将可重定位目标文件的符号分配到COMMON和.bss:COMMON中是未初始化的全局变量,.bss重视未初始化的静态变量和初始化为0的全局变量。GNU READELF是一个可以查看目标文件内容的工具。具体分析参考(https://blog.csdn.net/zl6481033/article/details/85317790)。

        重要的理解程序中的变量函数等都放在哪一块。

6、符号解析

        链接器解析符号引用的方法是将每个印度用与他输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。

        定义和引用都在一个模块中,符号解析很简单明了的,每个模块中每个局部符号都有一个定义。

        如果是全局符号引用,编译器遇到一个不是当前模块定义的符号(变量、函数)会假设它是在其他模块定义的,生成一个链接器符号条目表,然后交给链接器,如果链接器在任何输入模块中找不到这个被引用的符号的定义,就输出一条错误信息。如果多个目标文件定义相同名字的全局符号,链接器要么标志一个错误要么以某一种方式选出一个定义。如果是C++/java中,允许重载,那么编译器会将每一个唯一的方法和参数列表组合编码成一个对于链接器来说是唯一的名字,这个过程交重整,相反的过程叫恢复。

        C++和java中,重整策略是兼容的,重整的类名字是由名字中字符的整数数量后面跟原始名字,比如类Foo会被编码成3Foo,方法编码成原始方法名后面加__,再加被重整的类名,再加每个参数的单字母编码,比如Foo::bar(int ,long)会被编码成bar__3Fooil。

    6.1 链接器如何解析多重定义的全局符号

        如果多个模块定义同名的全局符号,linux中会采用以下方法:在编译时,编译器会向汇编器输出每个全局符号,或是强或是弱,汇编器会将这个信息隐含的编码在可重定位目标文件的符号表里,函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。根据强弱符号定义链接器使用下面规则处理多重定义的符号名:

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

        规则2:如果有一个强符号和多个弱符号中任意选一个。

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

        虽然编译器允许这样的操作,但是最好不要这样,当涉及到的模块很多,这样的错误很难区找到,因为编译器不会提示出错。如果你怀疑程序有这类错误,那么可以用 GCC -fno-common 标志这样的选项调用链接器,这个选项会告诉链接器,遇到多重定义的全局符号触发错误。或者使用-Weror,可以把警告变成错误。

        所以,在上一节中编译器会按照一定的规则将符号分配为COMMON和.bss。当编译器翻译某个模块时,遇到一个弱的全局符号,编译器无法判断其他模块是否有这个符号的多重定义,所以会将它分配给COMMON,决定权在链接器。而如果是一个强符号,根据上述规则,必须是唯一的,编译器会将它分配到.bss。类似的,静态符号的构造必须是唯一的,所以编译器会将它们分配到.data或.bss。

    6.2 与静态库的链接

        在前面,链接器读取的是一组可重定位目标文件,并将它们链接起来,生成可执行文件。编译系统都提供一种机制,将所有相关的模块打包成一个单独的文件,称为静态库。可以作为链接器的输入。当链接器构造出一个输出的可执行文件,,它只复制静态库里被应用程序引用的目标模块。

        假设如果没有链接库,那么第一种解决办法直接让编译器辨认对标准函数的调用并生成相对应的代码(pascla就是这样),但是对于C来说不合适,C定义了大量的标准函数,这样会给编译器增加显著的复杂性,而且每次如果修改标准函数会需要一个新的编译器版本第二种解决办法是将所有的标准C函数单独放在一个可重定位目标模块中,之后应用程序员可以把这个模块链接到自己的可执行文件。这种方法大优点是将编译器的实现与标准函数的实现分离开,但是一个很大的缺点是系统的每个执行文件都包含了一份标准函数的集合完整副本那么很明显会占用很大的内存和磁盘空间,还有一个缺点就是如果标准函数需要修改那么库的开发人员需要重新编译整个源文件,使得标准函数的开发和维护变得很复杂。那么还有一种办法,将每个标准函数创建一个独立的可重定位目标文件,把他们存放在大家都知道的目录,但是这样又需要应用程序员需要显式的将合适的目标文件链接到自己的可执行文件中,显然这是一个很耗时并容易出错的事。

        静态库的提出就是为了解决上述缺点。相关函数可以被编译独立的目标模块然后封装成一个单独的静态库文件。然后应用程序可以通过命令行指定单独的文件名字来使用这些在库中定义的函数,比如,使用C标准库和数学库中函数的程序可以使用如下的命令行来编译和链接:

gcc main.c /usr/lib/libm.a /usr/lib/lib.a

        在链接时,链接器只复制被程序引用的目标模块,这就减少了可执行文件在磁盘和内存中的大小。应用程序员只需要包含较少的库文件的名字(实际上C 编译器驱动程序总是传送libc.a给链接器)。在linux系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中,存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件大小和位置,存档文件名由后缀.a标识。

        静态链接测试。(https://blog.csdn.net/zl6481033/article/details/85340096)。

    6.3 链接器如何使用静态库解析引用

        在符号解析阶段,链接器会维护一个可重定位目标文件的集合E(这个集合文件会被合并起来形成可执行文件),一个为解析的符号(即引用了电视未定义的符号集合)U,以及一个在前面文件中已定义的符号集合D。初始情况下都是为空。

        *对于命令行上的每一个输入文件f,链接器会判断f是一个目标文件还是一个存档文件,如果f是一个目标文件,链接器会把f添加到E,修改U和D中来反映f中的符号定义和引用并继续下一个输入文件。

        *如果f是一个存档文件,那么链接器会尝试匹配U中未解析的符号和由存档文件成员定义的符号,如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D反应m中的符号定义和引用。对于存档文件中的所有成员目标文件都依次进行这个过程,直到UD不再变化。此时任何不包含在E中的成员目标都简单的被丢弃,链接器继续处理下一个输入文件。

        *如果链接器完成对命令行上输入文件的扫描之后,U是非空的,那么会输出一个错误并终止。否则会合并和重定位E中的目标文件,构建输出的可执行文件。

        关于库的一般准则是将它们放在命令行的结尾。如果库是相互独立的,那么这些库可以以任何顺序放在命令行结尾处,如果不是相互独立的,那么必须对他们排序,使得每个被存档文件的成员外部引用的符号s,在命令行至少有一个s的定义是在对s的引用之后的。

7、重定位

        一旦链接器完成符号解析这一步,就把代码中每个符号引用和正好一个符号定义(即每一个输入目标模块中的一个符号条目表)关联起来。此时,链接器就知道输入的目标模块中代码节和数据节的确切大小,就可以开始重定位,重定位就是将输入的模块合并,并为每个符号分配运行时地址。重定位由两步组成:

        *重定位节和符号定义:链接器将所有同类型的节合并为同一类型的新的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每一个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一的运行时内存地址。

        *重定位节中的符号引用,这一步,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址,要执行这一步,链接器要依赖可重定位目标模块中重定位条目的数据结构。

    7.1 重定位条目

        当汇编器生成一个目标模块的时候并不知道数据和代码最终会放在内存中的什么位置,也不知道函数全局变量的位置,所以无论何时汇编器遇到对最终位置位置的目标引用会生成一个重定位条目,告诉连接器再将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。

        下图是ELF重定位条目的格式,offset是需要被修改的引用的节偏移。symbol标识被修改引用应该指向的符号,type告知链接器如何修改新的引用,addend是一个有符号的常数,一些类型的重定位要使用他对被修改的值做偏移调整。

        

        ELF定义了32种不同的重定位类型,只关心其中两种:

        *R_x86_64_PC32,重定位一个使用32位PC相对地址的引用。

        *R_x86_64_32,重定位一个使用32位绝对地址的引用。

    7.2 重定位符号引用

        根据对main.o的反汇编代码:

        

        
        main函数引用了两个全局符号,array和sum。为每个引用,汇编器产生一个重定位条目,显示在引用的后面一行,这些重定位条目告诉链接器对sum的引用要使用32位PC相对地址进行重定位,对array的引用要使用32位绝对地址进行重定位。

        1.重定位PC相对引用:在上图第六行,main调用sum函数,sum在sum.o中定义,call指令开始于节偏移0xe的地方,包括一字节的操作码0xe8,后面跟着对目标sum的32位PC相对引用的占位符。相对应的重定位条目r由四个字段组成,

        r.offset=0xf,r.symmbol=sum,r.type=R_X86_64_PC32,r.addend=-4,这些字段告诉链接器修改开始于偏移量0xf处的32位PC引用,这样在运行时会指向sum。假设链接器已经去欸的那个ADDR(s)=ADDR(.text)=0x4004d0和ADDR(r.symblo)=ADDR(sum)=0x4004e8,之后使用重定位算法,先计算出引用的运行时地址(第七行),refaddr=ADDR(S)+r.offset=0x400d0+0xf=0x400df,之后更新引用,使它在在运行时指向sum程序(第八行),*refptr=ADDR(r.symbol)+r.addent-refaddr=0x4004e8-4-0x4004df=0x5。之后对可执行文件进行反汇编,如下图,在运行时,call指令存放在4004e4中,当CPU执行call指令的时候,PC的值为call指令后面的指令的地址,为了执行call,先将此时PC值入栈,再将PC+5,也就是我们上面计算出来的相对地址,之后得到的地址也就是sum的第一条指令的地址。

        

        2.重定位的绝对引用:绝对引用相对简单,上上图中第四行,mov指令将array的地址一个32位立即数复制到寄存器%edi,mov指令开始于节偏移0x9的位置,包括1字节的操作码0xbf,后面跟着对array的32位绝对引用的占用符。包括四个字段: r.offset=0xa,r.symmbol=array,r.type=R_X86_64_32,r.addend=0,这个字段告诉链接器要修改从偏移量0xa开始的绝对引用,这样会在运行时指向array的第一个字节,现在链接器已经确定,ADDR(r.symbol)=ADDR(array)=0x601018,链接器使用链接器算法得到第十三行的引用,在可执行文件中会有如下形式的重定位。

        综合以上两种定位,最终给出可执行文件中已经重定位的.text节和.data节。在加载的时候,加载器会直接把这些节中的字节直接复制到内存,不用再进行修改的执行这些指令。

    

8、可执行目标文件

       在前面介绍了链接器怎么将多个目标文件合并成一个可执行目标文件,从开始的文本文件变成一个二进制文件,并且这个二进制文件包含着加载程序到内存并运行它的所有信息。下图为一个典型的ELF可执行文件中的各类信息。

        

        可执行目标文件的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式。包括程序的入口点,也就是程序运行时的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件的节是相似的,除了这些节已经被重定位到他们最终的运行时内存地址以外。.init节定义了一个函数,叫做_init,程序的初始化代码会调用它,因为可执行文件是完全链接的(已经重定位),所以不再需要.rel节。

        ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片被映射到连续的内存段。程序头部表描述了这种映射关系,下图为可执行文件prog的程序头部表,由objdump -dx prog得到。

        书中图如下:

        从程序头部表,我们会看到根据可执行目标文件的内容初始化两个内存段,第一行和第二行告诉我们第一个段(代码段)有读和执行访问权限,开始于内存地址0x400000处,总共内存大小是0x69c个字节,并且被初始化为可执行文件的头0x69c个字节,其中包括ELF头,程序头部表以及.init、.text和.rodata节。

        第三四行告诉我们第二个段(数据段)有读写访问权限,开始于内存地址0x600df8处,总的内存大小为0x230个字节,并用从目标文件偏移0xdf8处开始的.data节中的0x228个字节初始化,该段中剩下的8个字节对应运行时被初始化为0的.bss数据。

        对于任何段s,链接器必须选择一个起始地址vaddr使得:vaddr mod align =off mod align 这里off是目标文件中段的第一个节的偏移量,align是程序头部中指定的对齐(2^21=0x200000),对于上图中的数据段:vaddr mode align= 0x600df8 mod 0x200000=0xdf8,

以及 off mode align=0xdf8 mod 0x200000 =0xdf8。这里的对齐要求是一种优化,使得当程序执行时,目标文件中的段能够很有效率的传送到内存中。

9、加载可执行目标文件

        要运行可执行目标文件prog,在shell中输入./prog,因为prog不是一个内置的shell指令,所以shell会认为prog是一个可执行目标文件,通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码运行它,任何linux程序都是通过调用execve函数来调用加载器,加载器将可执行文件中的代码和数据从磁盘复制到内存中,然后跳转到程序的第一条指令或入口点来运行该程序,这个将程序从磁盘复制到内存并运行的过程叫加载。

        linux x86-64系统中代码总是从地址0x400000处开始,后面才是数据段,运行时堆在数据段之后,通过调用malloc库往上增长,堆后面的区域是为共享模块保留的,用户栈总是从最大的合法用户地址开始向较小内存地址增长,栈上的区域从2的48次方开始,是为内核中的代码和数据保留的,所谓内核就是操作系统驻留在内存的部分。当链接器运行时,它会创建类似下图的内存映像,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,接下来加载器跳转到程序的入口点,也就是_start函数的地址,该函数定义在libc.so中,他会初始化执行环境,调用用户层的main函数,处理main函数的返回值,并在需要的时候把控制返回给内核。

        

        加载器工作概述:linux每个程序都运行在一个进程的上下文中,有自己的虚拟地址空间,当shell运行一个程序时,父进程生成一个子进程,它是父进程的一个复制,子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆都被初始化为0,通过虚拟地址空间的页映射到可执行文件的页大小的片,新的代码和数据段会被初始化为可执行文件的内容,最后加载器跳转到_start地址,它会调用应用程序的main函数。除了一些头部信息在加载过程中没有任何从磁盘到内存的数据复制,直到CPU引用一个被映射的虚拟页时才会进行复制,此外操作系统利用他的页面调度机制自动将页面从磁盘传送到内存。

10、动态链接库

        前面研究过静态库,它解决了许多关于如何让大量相关函数对应用程序可用的问题。而静态库还是有一些明显的缺点,静态库和所有软件一样需要定期维护和更新,如果应用程序员想要使用一个库的最新版本,他们必须以某种方式了解到该哭的更新情况,然后显示的将他们的程序与更新了的库重新链接。另一个问题是几乎每个C程序都使用标准IO函数,比如printf和scanf,在运行时这些函数代码会被复制到每个运行进程的文本段中,在一个运行上百个进程的典型系统上,这将是对稀缺的内存资源的极大浪费。

       共享库是致力解决静态库缺陷的一个现代创新产物,共享库是一个目标模块,在运行或加载时,可以加载到任意内存地址,并和一个内存中的程序链接起来,这个过程叫做动态链接。有一个叫做动态链接器的程序来执行,共享库也称为共享目标,在linux中常用.so为后缀表示。也就是DLL。

        共享库是以两种不同的方式来共享的,首先是对于任何给定的文件系统中,对于一个库只有一个.so文件,所有引用该哭的可执行目标文件共享这个.so文件中的代码和数据,而不像静态库的内容那样被复制和嵌入到引用他们的可执行文件中。其次,在内存中一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。

       

        上图为一个程序动态链接的过程。

        动态链接测试(https://blog.csdn.net/zl6481033/article/details/85699083)。

11、从应用程序加载和链接共享库

         在之前基本讨论的是应用程序被加载后执行前,动态链接器加载和链接共享库的情景,然而,应用程序还可能在他运行的时候要求动态链接器和链接某个共享库,而无需在编译时将那些库链接到应用中。

        动态链接是一个很强大的技术在现实中有很广泛的应,比如:分发软件,windows应用开发者常常利用共享库来分发如那件更新,他们声称一个共享库的新版本,用户可以下载,并用它代替当前版本,下次运行程序时会自动链接和加载新的共享库。还可以用来构建高性能WEB服务器,其思路是将每个生成动态内容的函数打包在共享库中,当有web浏览器的请求到达时,服务器自动加载和链接适当的函数,直接调用而不是使用fork或execve在子进程的上下文运行函数。也就是无需停止服务就可以更新已存在的函数。

        linux系统为u动态链接器提供了一个简单的接口,允许程序在运行时加载和链接共享库。

        

        dlopen函数加载和链接共享库filename。

        

        dlsym函数的输入是一个指向前面打开了的共享库的句柄和一个symbol名字。如果该符号存在就返回符号的地址,否则返回NULL。

         

        如果没有其他共享库还在使用这个共享库,dlclose函数就卸载该共享库。

        

        dlerror函数返回一个字符串,它描述的是调用前面几个函数时发生的错误。

        运行时调用动态函数库。(https://blog.csdn.net/zl6481033/article/details/85699083)。

12、位置无关码

        为了避免多个进程共享一个程序造成的各种问题,现代系统使他们可以加载到内存的任何位置而无需链接器修改,使用这种方法可以使无限多个进程共享一个共享模块的代码段的单一副本。(每个进程仍然有自己的读写数据块)。

        可以加载但是不需要重定位的代码称为位置无关码(PIC),用户对GCC使用-fpic选项只是GNU编译系统生成PIC代码,共享库的编译总是使用该选项。

13、库打桩机制

        linux链接器有一个很强大的技术,称为库打桩。它允许你截获对共享函数的调用,取而代之执行自己的代码,使用库打桩技术你可以追踪对某个特殊库函数的调用次数,验证和追踪输入输出值。

14、处理目标文件的工具

        在linux下有大量的工具可以帮助你理解和处理目标文件。

        *AR:创建静态库,插入、删除、列出和提取成员。

       *STRINGS:列出一个目标文件中所有的可打印字符串。

       *STRIP:从目标文件中删除符号表信息

       *NM:列出一个目标文件中符号表中定义的符号。

       *SIZE:列出目标文件中节的名字和大小

       *READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息,包含SIZE和NM的功能。

       *OBJDUMP:所有二进制工具之母,能够显示一个目标文件中所有的信息,最大的作用是反汇编.text节中的二进制指令。

       *LDD:列出一个可执行文件在运行时所需要的共享库。

15、小结

        链接器的两个主要任务就是符号解析和重定位,符号解析是将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址并修改对那些目标的引用。

16、本篇相关实践

        (1)readelf分析。(https://blog.csdn.net/zl6481033/article/details/85317790

        (2)静态链接。    (https://blog.csdn.net/zl6481033/article/details/85340096

        (3)动态链接。    (https://blog.csdn.net/zl6481033/article/details/85699083

          

       

    

        

        

        

        

        

        

        

        

    

猜你喜欢

转载自blog.csdn.net/zl6481033/article/details/84990262
今日推荐