程序的编译、链接与装载

2023-11-04

《程序员的自我修养-链接装载与库》是一本值得推荐的书,主要介绍系统软件的运行机制和原理,涉及在Windows和Linux两个系统平台上,一个应用程序在编译、链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现,C/C++运行库的工作原理,以及操作系统提供的系统服务是如何被调用的。
本文主要对书中涉及Linux中程序的变异、链接、装载等核心部分内容进行整理,方便查看。

编译过程

许多IDE和编译器将编译和链接的过程合并在一起,称为构建(Build),使用起来非常方便。但只有深入理解其中的机制,才能看清许多问题的本质,正确解决问题。
一般的编译过程可以分解为4个步骤,预处理,编译,汇编和链接:

  • 预编译:处理源代码中的以”#”开始的预编译指令,如”#include”、”#define”等。
  • 编译:把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件,是程序构建的核心部分,也是最复杂的部分之一。
  • 汇编:将汇编代码根据指令对照表转变成机器可以执行的指令,一个汇编语句一般对应一条机器指令。
  • 链接:将多个目标文件综合起来形成一个可执行文件。

而对于第2步,编译由编译器完成器,编译器是将高级语言翻译成机器语言的一个工具,其具体步骤包括:

  • 词法分析:将源代码程序输入扫描器,将源代码字符序列分割成一系列记号(Token)。
  • 语法分析:对产生的记号使用上下文无关语法进行语法分析,产生语法树。
  • 语义分析:进行静态语义分析,通常包括声明和类型的匹配,类型的转换。
  • 中间语言生成:使用源代码优化器将语法树转换成中间代码并进行源码级的优化。
  • 目标代码生成:使用代码生成器将中间代码转成依赖于具体机器的目标机器代码。
  • 目标代码优化:使用目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用位移替代乘法、删除多余指令等。

如果一个源代码文件中有变量或函数等符号定义在其他模块,那么编译后得到的目标代码中,该符号的地址并没有确定下来,因为编译器不知道到哪里去找这些符号,事实上这些变量和函数的最终地址要在链接的时候才能确定。现代的编译器只是将一个源代码编译成一个未链接的目标文件,最终由链接器将这些目标文件链接起来形成可执行文件。

目标文件格式

编译器编译源代码后生成的文件称为目标文件,事实上,目标文件是按照可执行文件的格式存储的,二者结构只是稍有不同。Linux下的目标文件和可执行文件可以看成一种类型的文件,统称为ELF文件,一般有以下几类:

  • 可重定位文件:如.o文件,包含代码和数据,可以被链接成可执行文件或共享目标文件,静态链接库属于这一类。
  • 可执行文件:如/bin/bash文件,包含了可以直接执行的程序,一般没有扩展名。
  • 共享目标文件:如.so文件,包含代码和数据,可以跟其他可重定位文件和共享目标文件链接产生新的目标文件,也可以跟可执行文件结合作为进程映像的一部分。

目标文件由许多段组成,其中主要的段包括:

  • 代码段(.text):保存编译后得到的指令数据。
  • 数据段(.data):保存已经初始化的全局静态变量和局部静态变量。
  • 只读数据段(.rodata):保存只读变量和字符串常量,有些编译器会把字符串常量放到”.data”段。
  • BSS段(.bss):保存未初始化的全局变量和局部静态变量。

除了这几个常用的段之外,ELF可能包含其他的段,保存与程序相关的信息,如:

  • .comment 编译器版本信息
  • .debug 调试信息
  • .dynamic 动态链接信息
  • .hash 符号哈希表
  • .line 调试时的行号表,源代码行号与编译后指令的对应表
  • .note 额外的比编译器信息
  • .strtab String Table,字符串表,存储用到的各种字符串
  • .symtab Symbol Table,符号表
  • .shstrtab Section String Table,段名表
  • .plt 动态链接跳转表
  • .got 动态链接全局入口表
  • .init 程序初始化代码段
  • .fini 程序终结代码段

ELF目标文件的总体结构如下图所示,其中省去了一些繁琐的结果,把最终的提出出来。

ELF Header
.text
.data
.rodata
.comment
.shstrtab
Section Table
.symtab
.rel.text

以下选取较为重要的进行介绍。
ELF文件头(ELF Header):保存描述整个文件的基本属性,如ELF魔数、文件机器字节长度、数据存储格式等。

段表(Section Header Table):保存各个段的基本属性,是除了文件头之最重要的结构。节选样例内容如下:

[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[1] .text PROGBITS 00000000 000034 00005b 00 AX 0 0 4

其表示的意义为,下标为1的段是.text段,类型是程序段(PROGBITS包括代码段和数据段),加载地址为0,在文件中的偏移量是0×34,长度为0x5b,项的长度为0(表示该段不包含固定大小的项),标志AX表示该段要分配空间及可以被执行,链接信息的两个0没有意义(不是与链接相关的段),最后的4表示段地址对齐为2^4=16字节。

重定位表:链接器在处理目标文件的时候,需要对目标文件中某些部位进行重定位,即代码段和数据段中那些绝对地址的引用位置,这些重定位信息记录在重定位表里。每个需要重定位的代码段或数据段都会有一个相应的重定位表,如.rel.text是针对”.text”段的重定位表,”.rel.data”是针对”.data”段的重定位表。

字符串表:ELF文件中用到很多字符串,如段名、变量名,因为字符串的长度不固定,用固定的结构来表示它比较困难,一般把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。一般字符串表在ELF中以段的形式保存,常见的有.strtab(字符串表,String Table)和.shstrtab(段表字符串表,Section Header String Table),前者保存如符号名字等普通字符串,后者保存如段名等段表中用到的字符串。

符号表:函数和变量统称为符号,其名称称为符号名。链接过程中关键的部分就是符号的管理,每一个目标文件都会有一个相应的符号表,记录了目标文件用到的所有符号,每个符号有一个对应的符号值,一般为符号的地址。一个样例如下:

Num Value Size Type Bind Vis Ndx Name
13 0000001b 64 FUNC GLOBAL DEFAULT 1 main

其意义如下:下标为13的符号的符号值为0x1b,大小为64字节,类型为函数,绑定信息为全局符号,VIS可以忽略,Ndx表示其所在段的下标为1(通过上一个样例可知,该段为.text段),符号名称为main。如果Ndx下标一项为UND(undefine),则表示该符号在其他模块定义,以后需要重定位。

调试信息:目标文件里可能保存有调试信息,如在GCC编译时加上”-g”参数,会生成许多以”.debug”开头的段。

静态链接

几个目标文件进行链接时,每个目标文件都有其自身的代码段、数据段等,链接器需要将它们各个段的合并到输出文件中,具体有两种合并方法:

  • 按序叠加:将输入的目标文件按照次序叠加起来。
  • 相似段合并:将相同性质的段合并到一起,比如将所有输入文件的”.text”合并到输出文件的”.text”段,接着是”.data”段、”.bss”段等。

第一种方法会产生很多零散的段,而且每个段有一定的地址和空间对齐要求,会造成内存空间大量的内部碎片。所以现在的链接器空间分配基本采用第二种方法,而且一般采用一种称为两部链接的方法:

  1. 空间与地址分配。扫描所有输入的目标文件,获得他们各个段的长度、属性和位置,收集它们符号表中所有的符号定义和符号引用,统一放到一个全局符号表中。此时,链接器可以获得所有输入目标文件的段长度,将他们合并,计算出输出文件中各个段合并后的长度与位置并建立映射关系。
  2. 符号解析与重定位。使用上面收集到的信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。

经过第一步后,输入文件中的各个段在链接后的虚拟地址已经确定了,链接器开始计算各个符号的虚拟地址。各个符号在段内的相对地址是固定的,链接器只需要给他们加上一个偏移量,调整到正确的虚拟地址即可。

ELF中每个需要重定位的段都有一个对应的重定位表,也称为重定位段。重定位表中每个需要重定位的地方叫一个重定位入口,包含:

  • 重定位入口的偏移:对于可重定位文件来说,偏移指该重定位入口所要修正的位置的第一个字节相对于该段的起始偏移。
  • 重定位入口的类型和符号:低8位表示重定位入口的类型,高24位表示重定位入口的符号在符号表的下标。

不同的处理器指令对于地址的格式和方式都不一样,对于每一个重定位入口,根据其重定位类型使用对应的指令修正方式修改其指令地址,完成重定位过程。

可执行文件的装载

32位硬件平台上进程的虚拟地址空间的地址为0到2^32-1:0×00000000~0xFFFFFFFF,即通常说的4GB虚拟空间大小。在Linux操作系统下,4GB被划分成两部分,操作系统本身占用了0xC00000000到0xFFFFFFFF共1GB的空间,剩下的从0×00000000到0xBFFFFFFFF共3GB的空间留给进程使用。
可执行文件只有被装载到内存以后才能运行,最简单的办法是把所有的指令和数据全部装入内存,但这可能需要大量的内存,为了更有效地利用内存,根据程序运行的局部性原理,我们可以把程序中最常用的部分驻留内存,将不太常用的数据放在磁盘中,即动态装入。

现在大部分操作系统采用的是页映射的方法进行程序装载。页映射并不是一下把程序的所有数据和指令都装入内存,而是将内存和所有磁盘中的数据和指令按照”页(Page)”为单位划分成若干个页,以后所有的装载和操作的单位就是页。目前一般的页大小为4K=4096字节。装载管理器负责控制程序的装载问题,当运行到的某条指令不在内存的时候,会将该指令所在的页装载到内存中的一个地方,然后继续程序的运行。如果内存中已经没有位置,装载管理器会根据一定的算法放弃某个正在使用的页,并用新的页来替代,然后程序可以继续运行。

可执行文件中包含代码段、数据段、BSS段等一系列的段,其中很多段都要映射进进程的虚拟地址空间。当段的数量增加时,会产生空间浪费问题。因为ELF文件被映射时是以系统的页长度为单位进行的,一个段映射的长度应为页长度的整数倍,如果不是,那么多余部分也将占用一个页,从而产生内存浪费。
实际上操作系统并不关心可执行文件各个段所包含的实际内容,它只关心一些跟装载有关的问题,最主要的是段的权限(可读、可写、可执行)。ELF中,段的权限组合可以分成三类:

  • 以代码段为代表的权限为可读可执行的段。
  • 以数据段和BSS段为代表的权限为可读可写的段。
  • 以只读数据段为代表的权限为只读的段。

于是,对于相同权限的段,可以把它们合并到一起当做一个段进行映射,这样可以把原先的多个段当做一个整体进行映射,明显地减少页面内部碎片,节省内存空间。这个称为”Segment”,表示一个或多个属性类似的”Section”,可以认为”Section”是链接时的概念,”Segment”是装载时的概念。链接器会把属性相似的”Section”放在一起,然后系统会按照这些”Section”组成的”Segment”来映射并装载可执行文件。

进程的虚拟地址空间中除了被用来映射可执行文件的各个”Segment”之外,还有包括栈(Stack)和堆(Heap)的空间,一个进程中的栈和堆在也是以虚拟内存区域(VMA, Virtual Memrory Area)的形式存在。操作系统通过给进程空间划分出一个个的VMA来管理进程的虚拟空间,基本原则是将相同权限属性的、有相同映像文件的映射成一个VMA,一个进程基本可以分为如下几种VMA区域:

  • 代码VMA,权限只读,可执行,有映像文件。
  • 数据VMA,权限可读写,可执行,有映像文件。
  • 堆VMA,权限可读写,可执行,无映像文件,匿名,可向上扩展。
  • 栈VMA,权限可读写,不可执行,无映像文件,匿名,可向下扩展。

其常见的分布情况如下图所示:

OS
STACK VMA
HEAP VMA
DATA VMA
CODE VMA

动态链接

静态链接允许不同程序开发者相对独立地开发和测试自己的程序模块,促进程序开发的效率,但其也有相应的缺点:

  • 浪费内存和磁盘空间。在多进程操作系统下,每个程序内部都保留了公用的库函数及其他数量可观的库函数及辅助数据结构,浪费大量空间。
  • 程序开发和发布困难。一个程序如果使用了很多第三方的静态库,那么程序中一旦有任何库的更新,整个程序就要重新链接并重新发布给客户,非常不方便。

动态链接可以解决空间浪费和更新困难的问题,其不对那些组成程序的目标文件进行链接,而是等到程序运行时才进行链接。使用了动态链接之后,当我们运行一个程序时,系统会首先加载该程序依赖的其他的目标文件,如果其他目标文件还有依赖,系统会按照同样方法将它们全部加载到内存。当所需要的所有目标文件加载完毕之后,如果依赖关系满足,系统开始进行链接工作,包括符号解析及地址重定位等。完成之后,系统把控制权交回给原程序,程序开始运行。此时如果运行第二个程序,它依赖于一个已经加载过的目标文件,则系统不需要重新加载目标文件,而只要将它们连接起来即可。

动态链接可以解决共享的目标文件存在多个副本浪费磁盘和内存空间的问题,因为同一个目标文件在内存中只保存一份。另外,当一个程序所依赖的库升级之后,只需要将简单地用新的库将旧的覆盖掉,无需将所有的程序再重新链接一遍,当程序下次运行时,新版本的库会被自动加载到内存并链接起来,程序仍然可以正常运行,并且完成了升级过程。

对于静态链接的可执行文件来说,整个进程只有一个文件要被映射,那就是可执行文件本身。但是对于动态链接来说,除了可执行文件本身,还有它所依赖的共享目标文件,此时,它们都是被操作系统用同样的方法映射进进程的虚拟地址空间,只是它们占用的虚拟地址和长度不同。另外,动态链接器也和普通共享对象一样被映射到进程的地址空间。系统开始运行程序之前,会把控制权交给动态链接器,由它完成所有的动态链接工作,然后再把控制权交回给程序,程序就开始执行。

装载时重定位

动态链接的共享对象在被装载时,其在进程虚拟地址空间的位置是不确定的,为了使共享对象能够在任意地址装载,可以参考静态链接时的重定位(Link Time Relocation)思想,在链接时对所有的绝对地址的引用不做重定位,把这一步推迟到装载时再完成。一旦模块装载完毕,其地址就确定了,即目标地址确定,系统就对程序中所有的绝对地址引用进行重定位。这种装载时重定位(Load Time Relocation)又称为基址重置(Rebasing)。

但是动态链接模块被装载映射至虚拟空间后,指令部分是在多个进程之间共享的,由于装载时重定位的方法需要修改指令,所以没有办法做到同一份指令被多个进程共享,因为指令被重定位之后对于每个进程来讲是不同的。当然,动态链接库中的可修改的数据部分对于不同的进程来说有多个副本,所以它们可以采用装载时重定位的方法来解决。

地址无关代码

装载时重定位导致指令部分无法在多个进程之间共享,失去了动态链接节省内存的一大优势。为了程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,可以把指令中那些需要改变的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变了,而数据部分可以在每个进程中拥有一个副本。这种方案称为地址无关代码(PIC, Position-independent Code)技术。
我们把共享对象模块中的地址引用按照是否扩模块分成模块内部引用和模块外部引用,按照不用的引用方式分成指令引用和数据引用,然后把得到的4种情况分别进行处理:

  • 模块内部调用或跳转。因为被调用的函数和调用者处于同一个模块,相对位置固定,而现代的系统对于模块内部的跳转、函数调用可以采用相对地址调用或者给予寄存器的相对调用,所以这种指令不需要重定位,其是地址无关的。
  • 模块内部数据访问。显然指令不能包含数据的绝对地址,那么只有进行相对寻址。因为一个模块前面一半是若干个页的代码,然后是若干个也的数据,这些页之间的相对位置是固定的,即任何一条指令与它所需要访问的模块颞部数据之间的相对位置是固定的,那么只需要相对当前指令加上固定的偏移量就可以访问模块内部数据了。现代的体系结构中,数据的相对寻址往往没有相对当前指令地址(PC)的寻址方式,ELF中使用了巧妙的办法获取当前的PC值,然后再加上一个偏移量达到访问相应变量的目的。
  • 模块间数据访问。模块间的数据访问目标地址要等到装载时才能确定,这些变量的地址跟模块的装载地址相关。ELF在数据段里建立一个指向这些变量的指针数组,称为全局偏移表(GOT, Global Offset Table),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用。当指令需要一个其他模块的变量时,程序会先找到GOT,然后根据GOT中变量对应的项找到该变量的目标地址。每个变量对应一个4字节的地址,链接器在装载模块的时候会查找每个变量所在的地址,然后填充GOT的各个项,以确保每个指针所指向的地址都正确。由于GOT本身放在数据段,它可以在被模块装载时修改,并且每个进程都可以有独立的副本,相互不受影响。
  • 模块间调用、跳转。采用上述类似的方法,不同的是,GOT中相应保存的是目标函数的地址,当模块需要调用目标函数时,可以通过GOT中的项进行间接跳转。调用一个函数时,先得到当前指令地址PC,然后加上一个偏移得到函数地址在GOT中的偏移,然后进行间接调用。

于是,四种地址引用方式在理论上都实现了地址无关性。

数据段地址无关性

以上的方法能够保证共享对象中代码部分地址无关,但数据部分并不是地址无关的,比如:

static int a;
static int* p = &a;

指针p的地址是绝对地址,指向变量a,但a的地址会随着共享对象的装载地址改变而变。
数据段在每个进程都有一份独立的副本,并不担心被进程改变,于是可以选择装载时重定位的方法来解决数据段中绝对地址引用的问题。对于共享对象来说,如果数据段中有绝对地址的引用,那么编译器和链接器会产生一个重定位表,这个表中包含了”R_386_RELATIVE”类型的重定位入口来解决上述问题。当动态链接器装载共享对象时,如果发现共享对象上有这样的重定位入口,就会对该共享对象进行重定位。
其实对代码段也可以使用装载时重定位而不是地址无关代码的方法,它有以下特点:
代码段不是地址无关,不能被多个进程共享,失去了节省内存的有点。
运行速度比地址无关代码的共享对象块,因为它省去了地址无关代码中每次访问全局数据和函数时都要做一次计算当前地址以及间接地址寻址的过程。

动态链接相关结构

动态链接下可执行文件的装载与静态链接下基本一样,首先操作系统会读取可执行文件的头部,检查文件的合法性,然后从头部中的”Program Header”中读取每个”Segment”的虚拟地址、文件地址和属性,并将它们映射到进程虚拟空间的相应位置,这些步骤跟前面的静态链接情况下的装载基本无异。在静态链接情况下,操作系统接着就可以把控制权交给可执行文件的入口地址,然后程序开始执行。但在动态链接情况下,操作系统会先启动一个动态链接器,动态链接器得到控制权后,开始执行一系列自身的初始化操作,然后根据当前的环境参数,开始对可执行文件进行动态链接工作。当所有动态链接工作完成以后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始正式执行。

动态链接涉及到的段主要如下:

  • “.interp”段。在Linux中,操作系统在对可执行文件进行加载时,会寻找装载该可执行文件需要的相应的动态链接器,即”.interp”段指定的路径的共享对象。
  • “.dynamic”段。动态链接ELF中最重要的结构,保存了动态链接器需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。”.dynamic”段保存的信息类似于ELF文件头,只是ELF文件头保存的是静态链接相关的内容,这里换成动态链接所使用的相应信息。
  • 动态符号表。ELF中专门保存符号信息的段为”.dynsym”。类似于”.symtab”,但”.dynsym”只保存与动态链接相关的符号,而”.symtab”则保存了所有的符号,包括”.synsyms”中的符号。同样地,动态符号表也需要一些辅助的表,如保存符号名的字符串表,静态链接时叫符号字符串表”.strtab”,在这里就是动态符号字符串表”.dynstr”(Dynamic String Table)。为了加快动态链接下程序符号查找的过程,往往还有扶着的符号哈希表”.hash”。动态链接符号表的结构与静态链接的符号表几乎一样,可以简单地将导入函数看做是对其他目标文件函数的引用,把导出函数看做是在本目标文件定义的函数即可。
  • 动态链接重定位表。动态链接下,可执行文件一旦依赖于其他共享对象,它的代码或数据中就会有对于导入符号的引用,这些导入符号的地址在运行时才确定,所以需要在运行时将这些导入符号的引用修正,即需要重定位。如果共享对象不是以PIC编译的,那么它需要在装载是被重定位;如果它是PIC编译的,虽然代码段不需要重定位,但是数据段还包含了绝对地址的引用,其绝对地址被分离出来成了GOT,而GOT是数据段的一部分,需要重定位。
    装载时重定位跟静态链接中的目标文件重定位十分相似。静态链接中,目标文件里包含专门用于重定位信息的重定位表,如”.rel.txt”表示代码段的重定位表,”.rel.data”表示数据段的重定位表。类似地,动态链接中,重定位表分别为”.rel.dyn”和”.rel.plt”,前者是对数据引用的修正,修正的位置位于”.got”以及数据段,后者是对于函数引用的修正,修正的位置位于”.got.plt”。

动态链接的步骤

动态链接的步骤基本上分为3步:启动动态链接器本身,然后是装载所有需要的共享对象,最后是重定位和初始化。

  1. 动态链接器自举。普通共享对象文件的重定位工作由动态链接器完成,动态链接器本身本身不可以依赖于其他共享对象,其重定位工作由其自身完成,这需要动态链接器在启动时有一段非常精巧的代码可以完成这项艰巨的工作而同时不能用到全局和静态变量,甚至不能调用函数,这种具有一定限制的启动代码称为自举(Bootstrap)。
    动态链接器获得控制权后,自举代码开始执行。自举代码首先找到自己的GOT,而GOT的第一个入口即是”.dynamic”段的偏移地址,由此找到了动态链接器本身的”.dynamic”段。通过”.dynamic”的信息,自举代码可以获得动态链接器本身的重定位表和符号表,从而得到动态链接器本身的重定位入口,先将他们全部重定位,然后动态链接器代码可以使用自己的全局变量和静态变量。
  2. 装载共享对象。自举完成后,动态链接器将可执行文件盒链接器本身的符号表合并到一个全局符号表中,然后开始寻找可执行文件依赖的共享对象。通过”.dynamic”段中类型的入口是DT_NEEDED的项,链接器可以列出可执行文件所依赖的所有共享对象,将他们的名字放入一个装载集合中。然后从集合中取出一个共享对象的名字,找到相应的文件后打开,读取相应的ELF文件头”.dynamic”段,然后将它相应的代码段和数据段映射到进程空间。如果这个ELF共享对象还依赖其他共享对象,则将所依赖的共享对象的名字放入装载集合中。如此循环把所有依赖对象都装载进内存为止。如果把依赖关系看做一个图的话,装载过程就是图的遍历过程,可以使用广度优先或深度优先搜索的顺序进行编译。
  3. 重定位和初始化。上述步骤完成后,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将他们的GOT/PLT中的每个需要重定位的位置进行修正。因为此时动态链接器已经拥有了进程的全局符号表,所以这个修正过程比较容易,和前面的地址重定位原理基本相同。
    重定位完成后,如果共享对象有”.init”段,那么动态链接器会执行”.init”段的代码,用来实现共享对象特有的初始化过程,比如共享对象中C++的全局/静态对象的构造。相应地,如果有”.finit”段,当进程退出时会执行”.finit”段中的代码,比如类似的C++全局对象的析构。而进程的可执行文件本身的的”.init”和”.finit”段不是由动态链接器执行,而是有运行库的初始化部分代码负责执行。

重定位和初始化后,准备工作宣告完成,所需要的共享对象也都已经装载并且链接完成,这是动态链接器就如释重负,将进程的控制权交给程序的入口并开始执行。

显式运行时链接

动态链接还有一种更加灵活的模块加载方式,称为显式运行时链接(Explicit Run-time Linking),也叫运行时加载。就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。一般的共享对象不需要进行任何修改就可以进行运行时加载,称为动态装载库(Dynamic Loading Library)。动态库的装载通过以下一系列的动态链接器API完成:

  • dlopen:打开一个动态库,加载到进程的地址空间,完成初始化过程。
  • dysm:通过指定的动态库句柄找到制定的符号的地址。
  • dlerror:每次调用dlopen()、dlsym()或dlclose()以后,可以调用dlerror()来判断上一次调用是否成功。
  • dlclose:将一个已经加载的模块卸载。系统会维持一个加载引用计数器,每次使用dlopen()加载时,计数器加一;每次使用dlclose()卸载时,计数器减一。当计数器减到0时,模块才真正地卸载。

下面是一个简单的例子,这个程序将数学库模块用运行时加载的方法加载到进程中,然后获取sin()函数符号地址,调用sin()并且返回结果。

#include <stdio.h>
#include <dlfcn.h>
int mian ()
{
	void *handle;
	double (*func)(double);
	char *error;
	
	handle = dlopen(argv[1], RTLD_NOW);
	if (handle == NULL)
	{
		printf("Open library %s error: %s\n", argv[1], dlerror());
		return -1;
	}
	
	func = dlsym(handle, "sin");
	if ( (error = dlerror()) != NULL)
	{
		printf("Symbol sin not found: %s\n", error);
		goto exit_runso;
	}
	
	printf("%f\n", func(3.1415926/2));
	
	exit_runso:
	dlclose(handle);
}

编译运行结果如下:

$gcc -o RunSoSimple RunSoSimple.c -ldl
$./RunSoSimple /lib/libm-2.6.1.so
1.000000
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

程序的编译、链接与装载 的相关文章

  • 2022中山大学计算机技术专硕考研初试、复试经验帖

    2022年中山大学计算机技术专硕考研初试 复试经验帖 个人简介 推荐几个我自己感觉对考研非常有帮助的小助手吧 可以帮助节省时间 考研时间规划总览 初试篇 数学 英语 政治 408 复试篇 如果觉得有帮助的话可以点个收藏后续会修改和增加内容
  • 雷军发布会刚结束,就能写出上万字原创文章!

    前言 看完雷军演讲会之后你有没有看到过很多文章 成千上万个字的原创文章 瞬间就出现了 这是一个一个字敲的吗 当然不是 是AI 话不多说直接上教程 把雷军的演讲整理到笔记中 可以是md格式 word格式等等 复制粘贴即可 打开网站 smart
  • chatgpt赋能python:Python中如何取出列表中的数字

    Python中如何取出列表中的数字 在Python编程中 经常需要从一个包含数字和其他类型数据的列表中仅取出数字元素 这可以通过几种不同的方法来实现 下面将介绍其中常用的几种方法 1 使用循环遍历 第一种方法是使用循环遍历列表 并检查每个元
  • 两款免费、好用的数据库连接工具

    一 Navicate Navicat是一套快速 可靠的数据库管理工具 专为简化数据库的管理及降低系统管理成本而设 它的设计符合数据库管理员 开发人员及中小企业的需要 Navicat 是以直觉化的图形用户界面而建的 让你可以以安全并且简单的方
  • 人工智能之深度学习-初始环境搭建(安装Anaconda3和TensorFlow2步骤详解)

    Python微信订餐小程序课程视频 https edu csdn net course detail 36074 Python实战量化交易理财系统 https edu csdn net course detail 35475 前言 本篇文章
  • linux 查询服务器的配置信息

    linux下看配置 可没有windows那么直观 你只能一个一个查看 一 cpu root srv more proc cpuinfo grep model name root srv grep model name proc cpuinf
  • chatgpt赋能python:Python中如何写π

    Python中如何写 在Python中 写 Pi 即圆周率 可能是一个小小的挑战 但是 这个问题的答案相对比较简单 在本文中 我们将介绍如何在Python中计算 以及如何使用Python的数学库 math库 介绍 是一个十分重要的数学常数
  • secure boot 是什么

    一 secure boot 是什么 secure boot 中文叫安全启动 是电脑行业成员共同开发的一种安全机制 用于确保电脑只能启动原装系统 如果电脑重装了其他系统 那么这个系统是启动不了的 说白了 就是垄断 所以安装完其他系统 必须关闭
  • [ Shell ] 通过 Shell 脚本导出 CDL 网表

    Python微信订餐小程序课程视频 https edu csdn net course detail 36074 Python实战量化交易理财系统 https edu csdn net course detail 35475 https b
  • 设计模式之命令模式

    优质资源分享 学习路线指引 点击解锁 知识定位 人群定位 Python实战微信订餐小程序 进阶级 本课程是python flask 微信小程序的完美结合 从项目搭建到腾讯云部署上线 打造一个全栈订餐系统 Python量化交易实战 入门级 手
  • chatgpt赋能python:Python如何获取微信聊天记录:详细教程

    Python如何获取微信聊天记录 详细教程 在当前的数字时代 如何快速 便捷地获取信息是困扰每个人的问题 随着移动互联网的发展 微信成为了人们交流沟通的主要工具之一 在这样的背景下 如何获取微信聊天记录成为了一项非常重要的技能 在某些场合中
  • 总结:对Java内存模型JMM的理解

    JMM规定了线程的工作内存和主内存的交互关系 以及线程之间的可见性和程序的执行顺序 一方面 要为程序员提供足够强的内存可见性保证 另一方面 对编译器和处理器的限制要尽可能地放松 JMM对程序员屏蔽了CPU以及OS内存的使用问题 能够使程序在
  • verilog之状态机详细解释(二)

    一 有限状态机设计的一般步骤 1 逻辑抽象 得出状态转换图 就是把给出的一个实际逻辑关系表示为时序逻辑函数 可以用状态转换表来描述 也可以用状态转换图来描述 这就需要 分析给定的逻辑问题 确定输入变量 输出变量以及电路的状态数 通常是取原因
  • 【Vue】从0-1全系列Vue教程带你启航!

    hello 我是小索奇 Vue js 已经备好文章 预更新哈 涵盖 Vue2 和 Vue3 涵盖代码示例 拓展内容 图解 疑难解答 让大家用最通俗的方式来学会 Vue 本系列会考虑到新手 会由浅入深 即使你是老玩家 也可以收藏备用 其中有很
  • chatgpt赋能python:如何用Python实现抢购?

    如何用Python实现抢购 Python是一种灵活多样的编程语言 可以用它来完成各种任务 其中之一就是抢购 在电商大促销的节日 抢购商品通常需要竞争非常激烈 但是使用Python编写抢购脚本可以让您获得更高的成功率 以下是一些建议 通过Py
  • 【CSDN】删除文章后,浏览量会减少吗?了解软删除和硬删除

    hello 我是小索奇 如果你也在博客写作的话 有没有考虑到一个问题 文章删除后 浏览量会减少吗 下面就给大家阐述一下 当在CSDN中删除已发布的文章后 该文章的浏览量统计会有以下情况 软删除状态的文章 浏览量统计会保留 不会下降或者重置
  • chatgpt赋能python:Python金额计算

    Python金额计算 Python是一种高级编程语言 因其易于阅读 简单 灵活和易于学习而广受欢迎 它还具有强大的数学和计算功能 因此 它是一种非常流行的用于金额计算的工具 在本文中 将介绍如何使用Python进行金额计算 包括不同的货币格
  • chatgpt赋能python:用Python对图片进行分类

    用Python对图片进行分类 在如今的数字时代 图片分类是一个越来越常见的任务 特别是在搜索引擎优化中 图片分类可以让搜索引擎更容易地找到特定类型的图片 并在相关的搜索中以更高的排名显示它们 在本文中 我们将介绍如何使用Python来分类图
  • 网络安全竞赛:硬件防火墙与软件防火墙,谁胜谁负?

    文章目录 前言 1 性能 2 安全性 3 灵活性 4 成本 5 管理和维护 如何入门学习网络安全 黑客 帮助
  • 牛客字符串

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 前言 一 pandas是什么 二 使用步骤 1 引入库 2 读入数据 总结 前言 提示 这里可以添加本文要记录的大概内容 例如 随着人工智能的不断发展 机器学习这门

随机推荐

  • 使用fastreport4经验小谈

    1 fastreport 属性窗口如何找回来 打开报表 frxreport 查看 view gt 选项 options gt 恢复默认值 restore defaults 2 我们查看报表输出时 从第二页不能清晰的看出订单数据是那个客户的数
  • MyEclipse添加反编译插件jadClipse--通用--超好+部署文档,傻瓜版

    MyEclipse添加反编译插件jadClipse 通用 超好 部署文档 傻瓜版 下载地址 https download csdn net download u014246526 10406292 效果图如下
  • Github Copilot 的补强工具Github Copilot Labs的常用功能介绍

    一 什么是Github Copilot Labs Github Copilot Labs是由GitHub推出的一款基于人工智能技术的代码协作工具 旨在协助开发者更加快速 高效地编写代码 该工具使用了机器学习技术 通过学习大量的开源代码和编写
  • Linux下Hadoop的介绍

    hadoop官网 http hadoop apache org 一 初始Hadoop 1 Hadoop是什么 Hadoop是Apache开源组织的一个分布式计算框架 可以在大量廉价硬件设备组成的集群上运行应用程序 并为应用程序提供一组稳定可
  • 最大间隙问题

    问题描述 最大间隙问题 给定n个实数 求这n个数在实轴上相邻2个数之间的最大差值 设计解最大间隙问题的线性时间算法 算法分析 问题很简单 而且描述本身就暗示了一种自然的求解方法 即先对元素排序 然后逐个求相邻元素的间距 这种解法的复杂度为O
  • Linux CentOS 修改MySQL安装目录

    安装MySQL 使用yum和Mysql官方源下载 安装方法参见 点此查看 Mysql 5 7源 修改Yum源 参照这里 修改安装目录 说明 仅针对还没有数据库数据的情况 有数据的请谨慎操作 虽然其实步骤似乎差不多 安装后的MySQL默认路径
  • linux开启rdp服务,让windows电脑mstsc远程,linux rdesktop远程windows机器

    windows 远程 linux桌面系统 windows7 CentOS release 6 9 1 安装 yum install xrdp 2 启动服务 service xrdp start 3 服务加入开机启动项 chkconfig x
  • MRTK-Unity学习记录

    TextMeshPro生成中文字体 Window TextMeshPro Font Asset Creator 1 Source Font File C盘 Windows Fonts 选择需要的字体文件 2 Atlas Resolution
  • Linux下安装Nginx

    一 什么是Nginx Nginx engine x 是一个高性能的 HTTP和反向代理服务器 也是一个 IMAP POP3 SMTP 服务器 正向代理 反向代理 很多大网站都是使用nginx做反向代理 应用非常广泛 Nginx是一款高性能的
  • Spring Boot 中的静态资源是什么,如何使用

    Spring Boot 中的静态资源是什么 如何使用 在 Web 应用程序中 静态资源通常是指不会动态生成的文件 例如图片 CSS JavaScript 文件等 Spring Boot 提供了一种简单的方式来处理这些静态资源 让我们可以更加
  • SpringMVC手写-核心逻辑

    文章目录 注解解析过程DispatcherServlet web xml Controller 自定义注解 从网上看到了SpringMVC实现的最简单版本 大致体现了核心逻辑 1 实现HttpServlet实现web请求访问 2 在实际处理
  • 批量获取力扣做题量工具

    这是一个批量统计国内版LeetCode做题量的工具 写这个小工具的起因是我们实验室准备每周统计大家leetcode的做题量 我们实验室十几个人 如果一人一个人看 太费时间了 作为高效程序员这不是我们做事的风格 于此就有了这个小工具 对于平常
  • matlab 基于密度的聚类算法,基于密度DBSCAN的聚类算法

    聚类算法概念 聚类分析又称群分析 它是研究 样品或指标 分类问题的一种统计分析方法 同时也是数据挖掘的一个重要算法 聚类 Cluster 分析是由若干模式 Pattern 组成的 通常 模式是一个度量 Measurement 的向量 或者是
  • mysqldump使用方法(MySQL数据库的备份与恢复)

    mysqldump使用方法 MySQL数据库的备份与恢复 mysqldump help 1 mysqldump的几种常用方法 1 导出整个数据库 包括数据库中的数据 mysqldump u username p dbname gt dbna
  • TCP三次握手

    三次握手指的是TCP协议建立连接的过程 当客户端请求与服务器建立TCP连接时 必须要经过三次握手才能真正建立连接 三次握手详细过程 第一次握手 客户端向服务器发送 SYN 报文 请求建立连接 其中 SYN 标志位被置为 1 同时客户端随机选
  • eclipse在java环境基础上配置C++环境(MinGW安装包+详细步骤)

    前言 在eclipse开发java的基础上 又不想再下一个c 的编辑器 如何实现java与c 的转换 博主整了两天 踩了好多坑 整理出来帮助大家 第一步 在eclipse里下载CDT 打开以后需要一点时间 搜索CDT 等待下载完成 补充说明
  • mac在pytorch环境下装transformer并成功实例运行

    主要是想成功运行下面这个安装步骤 https huggingface co transformers installation html installing from source 一 进入环境 conda activate pytorc
  • RuntimeError: cublas runtime error : unknown error at C:/w/b/win…cu:225

    报错 分析原因 GPU不够用 把数组改小就可以正常运行 或者改为在CPU上运行
  • Think in Java 复用类(第7章) 读书笔记

    第7章 复用类 1 组合语法 在新类中产生现有类的对象 由于新类是由现有类的对象组成的 所以这种方法称为组合 2 继承语法 按照现有类的类型来创建新类 无需改变现有类的形式 采用现有类的形式并在其中添加新代码 除非已经明确从其他类中继承 否
  • 程序的编译、链接与装载

    程序员的自我修养 链接装载与库 是一本值得推荐的书 主要介绍系统软件的运行机制和原理 涉及在Windows和Linux两个系统平台上 一个应用程序在编译 链接和运行时刻所发生的各种事项 包括 代码指令是如何保存的 库文件如何与应用程序代码静