学习笔记之编译流程

编译流程笔记 《程序员的自我修养》

静态连接

空间与地址分配

对于链接器来说,链接过程就是将几个输入目标文件加工后合并成一个输出文件。链接的方式有两种:

  1. 按序叠加:将输入的目标文件按照次序叠加起来,直接将各个目标文件依次合并。这样会导致的问题就是在有很多输入文件的情况下,输出文件将会有很多零散的段。十分浪费空间,因为每个段都需要有一定的地址和空间对齐。
  2. 相似段合并:将相似性质的段合并到一起。

链接器为目标文件分配地址和空间所说的地址和空间是有两个含义:

  1. 在输出的可执行文件中的空间。
  2. 在装载后的虚拟地址中的虚拟地址空间。

对于有实际数据的段,如data和text,在文件中和虚拟地址空间中都要分配空间,对于bss来说,分配空间的意义只局限于虚拟地址空间,因为在文件中并没有内容。一般我们关心的空间分配只关注于虚拟地址空间的分配。

目前的连接器都采用相似段合并的方式,使用这种方式有两步:

  1. 空间与地址分配:扫描所有的输入目标文件,获取它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表
  2. 符号解析与重定位:读取输入文件中段的数据、重定位信息,并且进行符号解析和重定位、调整代码中的地址。

例如,使用ld链接器将a.0和b.o链接起来:ld a.o b.o -e main -o ab

  • -e main:表示将main函数作为程序入口,ld链接器默认的程序入口是_start。
  • -o ab表示链接输出文件名为ab,默认是a.out.

在linux中,elf可执行文件默认从地址0x08048000开始分配。

符号解析与重定位

在ELF文件中,有个叫做重定位表的结构专门用来保存在这些与重定位相关的信息,重定位表在ELF文件中往往是一个或多个段。对于可重定位的ELF文件来说,必须包含重定位表,用来描述如何修改相应的段里的内容。对于每个要被重定位的ELF段都有一个对应的重定位表,而一个重定位表往往就是ELF文件中的一个段,所以起始重定位表也可以叫做重定位段。

比如代码段.text中有需要重定位的地方,那么就会有一个段叫做.rel.text的段保存了代码段的重定位表。可以使用objdump来查看目标文件的重定位表。objdump -r a.o

每个要被重定位的地方叫一个重定位入口,重定位入口的偏移表示该入口在要被重定位的段中位置。

装载与进程

进程的建立

一个进程拥有一个独立的虚拟地址空间。创建一个进程,然后装载相应的可执行文件并且执行,在有虚拟存储的情况下,需要做三件事。

  1. 创建一个独立的虚拟地址空间。
  2. 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
  3. 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。

创建虚拟地址空间是创建映射函数所需要的相应的数据结构,这一步是建立虚拟地址空间和物理内存的映射关系。

读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系,是建立虚拟地址空间与可执行文件的映射关系。因为当发生缺页错误的时候,操作系统将从物理内存中分配一个物理页,然后将缺少的页从磁盘中读取到内存中,再设置缺页的虚拟页和物理页之间的映射关系。所以当发生缺页错误的时候,需要知道所需要的页在可执行文件中的那个位置。这个就是虚拟空间和可执行文件之间的映射关系。

linux将进程虚拟地址空间中的一个段叫做虚拟内存区域(VMA),例如操作系统创建进程后,会在进程相应的数据结构中设置一个.text段的VMA。

将CPU指令寄存器设置成可执行文件入口,启动运行,是操作系统通过设置CPU的指令寄存器将控制权转交给集成,由此进程开始执行。

segment是从装载的角度重新划分了ELF的各个段。在将目标文件链成可执行文件的时候是,链接器会尽量把相同权限属性的段分配在同一空间。比如可读可执行的段都放在一起。在ELF文件中这些属性相似、又连在一起的段叫做一个segment。section是指elf文件的各个段。elf文件经过链接过后,将相同属性的section组合在一起,成为一个segment。section是从链接的角度来看,segment是从装载的角度。描述section属性的结构叫做段表,描述segment的结构叫做程序头,主要描述ELF文件该如何被操作系统映射到进程的虚拟空间。

堆和栈

VMA除了用来映射可执行文件中的各个segment以外,还可以有其他的作用,可以通过VMA来对进程的地址空间进行管理。进程的堆和栈在虚拟空间中也是以VMA的形式存在的。可以通过/proc来查看进程的虚拟地址空间分布。

内核装载ELF过程

在用户层面,bash进程首先会调用==fork()系统调用创建一个新的进程,然后新的进程调用execve()==执行指定的ELF文件,原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。

int execve(const char *filename, char *const argv[], char *const envp[]),三个参数分别表示了被执行的程序文件名、执行参数以及环境变量。Glibc对execvp()系统调用进行了封装,提供了execl()、execlp()、execle()、execv()、execvp()等5个API。

在内核中execve()系统调用相应的入口是sys_execve(),sys_execve()进行一些参数的检查复制之后,调用do_execve()。do_execve()会先查找被执行的文件,如果找到文件,则读取文件的前128 个字节,用来判断文件的格式。

当do_execve()读取了这128个直接的文件头部之后,然后调用search_binary_handle()取搜索和匹配合适的可执行文件装载处理过程。ELF文件的装载处理过程叫做load_elf_binary(),可执行文件的装载处理过程叫做load_aout_binary()。load_elf_binary()函数的主要处理步骤是:

  1. 检查ELF文件格式的有效性,比如魔数、程序头表中segment的数量。
  2. 寻找动态链接的“.interp”段,设置动态链接器路径。
  3. 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。
  4. 初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址。
  5. 将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_entry所指的地址。

动态链接

动态链接就是不对哪些组成程序的目标文件进行链接,等到程序要运行的时候才进行链接,把链接过程推迟到了运行时再进行,这就是动态链接

动态链接的的定就是程序在运行时可以动态的选择加载各种程序模块,这个可以用来制作程序的插件。程序与动态库之间的链接工作是由动态链接器完成的,并不是静态链接器ld。动态链接就是把链接这个过程从本来的程序装载前被推迟到了装载的时候。

在静态链接的时候,整个程序最终只有一个可执行文件,但是在动态链接下,一个程序被分成了若干个文件,有可执行文件和程序所依赖的共享对象,可以将这些部分称为模块,即动态链接下的可执行文件和共享文件都可以看做是程序的一个模块。

装载时重定位是解决动态模块中有绝对地址引用的办法之一,但是有个缺点就是指令部分没办法在多个进程之间共享。可以把指令中需要修改的部分分离出来,和数据部分放在一起,这样指令部分就可以保持不变,数据部分可以在每一个进程拥有一个副本,这种解决方法被称为地址无关代码(PIC,Position-independent Code)。

共享对象模块中的地址引用按照是否为跨模块可以分成两类:模块内部引用模块外部引用。按照不同的引用方式可以分为指令引用和数据访问。所以一共有四种情况:

  1. 模块内部的函数调用、跳转。
  2. 模块内部的数据访问,比如模块中定义的全局变量、静态变量。
  3. 模块外部的函数调用、跳转。
  4. 模块外部的数据访问,比如其他模块定义的全局变量。

地址无关代码

模块内部调用

调用者和被调用则处于同一个模块,相对位置是固定的,可以直接使用相对地址调用或者是基于基础漆的相对调用,不需要重定位。

模块内部数据访问

模块内部的数据访问,指令中不能直接包含数据的绝对地址,唯一的办法就是相对寻址。一个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页之间的相对位置是固定的,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据。

模块间数据访问

模块间的数据访问比模块内部稍微麻烦一点,因为模块间的数据访问目标地址要等到装载时才决定。使得代码地址无关,基本的思想就是把地址相关的部分放到数据段里面建立一个指向这些变量的指针数据,也称为全局偏移表(GOT,global offset table),当代码需要引用该全局变量的时候,可以通过GOT中相对应的项间接引用。

当指令要访问变量时,程序会先找到GOT,然后根据GOT中变量所对应的项找到变量的目标地址。每个变量都对应一个四个字节的地址,链接器在装载模块的时候会查找每个变量的地址,然后填充GOT中的各个项,以确保每个指针所指向的地址正确。GOT因为是放在数据段的,所以可以在装载的时候被修改,并且每个进程都可以有独立的副本,相互不受影响。

模块间调用和转换

对于模块间的调用和跳转,也可以使用GOT的方式来解决,GOT中相应的项保存的目标函数地址。

小结

模块内部的指令跳转和调用使用相对跳转和调用,模块内部的数据访问使用相对地址访问。

模块外部的指令跳转和调用使用的是间接跳转和调用(GOT),模块外部的数据访问使用的是间接访问(GOT)。

-fpic和-fPIC

使用GCC产生地址无关代码只需要使用“-fPIC”参数即可,GCC还提供了另外一个参数“-fpic”。这两个参数从功能上来讲是完全一样的,都是让GCC产生地址无关代码。唯一的区别就是“-fpic”产生的代码相对较小,“-fPIC”产生的代码相对较大。但是“-fpic”在一些硬件平台上有一些限制,比如全局符号的数量或者代码的长度等,“-fPIC”则没有这样的限制。

区分一个so是否是pic的,可以使用readelf命令查看文件中是否有TEXTREL段。PIC的so是不会代码段重定位表,TEXTREL表示的是代码段重定位表地址。

共享模块的全局变量问题

当一个模块引用了一个定义在共享对象的全局变量的时候,编译器编译模块的时候无法判断这个全局变量是定义在同一个模块中的其他目标文件还是定义在另一个共享对象中,即无法判断是不是跨模块的调用。

当模块是可执行文件中的一部分的时候,程序主模块的代码并不是地址无关代码,不会使用PIC机制,所以这个全局变量就使用的普通的数据访问方式。可执行文件在运行时并不进行代码重定位,所以变量的地址必须在链接过程中确定下来。为了能够是链接过程正常,链接器会在创建可执行文件时,在.bss段创建一个global变量的副本。所以就会导致变量同时存在多个位置中。

ELF共享库在编译的时候,默认都把定义在模块内部的全局变量当做定义在其他模块的全局变量,通过GOT来实现变量的访问。当共享模块被装载时,如果某个全局变量,在可执行文件中有副本那么动态链接器会把GOT中相应的地址指向该副本,这样该变量在运行时就会只有一个实例。如果该全局变量没有在程序主模块中有副本,则GOT中的相应地址就会指向模块内部的该变量副本。

如果一个共享对象lib.so中定义了一个全局变量G,而进程A和进程B都使用了lib.so,那么当进程A改变这个全局变量G的值时,进程B中的G会受到影响吗?

不会,应为当lib.so被两个进程加载时,他的数据段部分在每个进程中都有独立的副本,共享对象中的全局变量实际上和定义在程序内部的全局变量没有什么区别。任何一个进程访问的只是自己的那个副本,而不会影响到其他进程。

如果改成同一个进程中的线程A和线程B,同一个进程中的不同线程访问的就是相同的进程地址空间,也就是同一个lib.so的副本。

那么是否有与上面的相反的需求呢?

有的,多进程共享全局变量又叫做共享数据段,多个线程访问不同的全局变量副本又叫做线程私有存储。

延迟绑定PLT

静态链接比动态链接快大约1%-5%,主要是因为动态链接对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址。对于模块间的调用也要先定位GOT,然后再进行间接跳转,所以程序的运行速度减慢。

在动态链接下,程序模块之间包含了大量的函数引用,在程序开始执行之前,动态链接会消耗不少时间用于解决模块之间的函数引用的符号查找以及重定位。但是在一个程序运行过程中,可能很多函数在程序执行完都不会被用到。所以ELF采用了一种叫做延迟绑定的做法,基本思想就是当函数第一次被用到时才进行绑定(符号查找、重定位),如果没有用到就不进行绑定。

动态链接相关结构

可执行文件的装载首先操作系统会读取可执行文件的头部,检查文件的合法性,然后从头部中的Program Header中读取每个Segment的虚拟地址、文件地址和属性,并将它们映射到进程虚拟地址空间的相应位置。在静态链接的情况下,操作系统接着就可以把控制权转交给可执行文件的入口地址,然后程序开始执行。在动态链接的情况下,操作系统还不能在装载完可执行文件之后就把控制权交给可执行文件,应为可执行文件还有很多的以来的共享对象。可执行文件中的很多外部符号的引用还是处于无效地址的状态,所以操作系统会先启动一个动态链接器。

在linux下,动态链接器ld.so实际上是一个共享对象,操作系统同样通过映射的方式将他加载到进程的地址空间中。操作系统在加载完动态链接器后就将控制权交给动态链接器的入口地址。当动态链接器取得控制权后,开始执行一系列自身的初始化操作,然后根据当前的环境参数,开始对可执行文件进行动态链接工作。当所有动态链接工作完成后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始执行。

.interp段

interp段是解释器interpreter的缩写,.interp的内容很简单,里面保存的就是一个字符串,这个字符串就是可执行文件所需要的动态链接器的路径。在linux系统下,所有的可执行文件所需要的动态链接器的路径几乎都是“/lib/ld-linux.so.2”,linux.so.2是一个软连接,操作系统在对可执行文件进行加载的时候回去找装载该可执行文件所需要的相应的动态链接器。

.dynamic段

.dynamic段保存了动态链接器所需要的基本信息,比如依赖于哪些共享独享、动态链接符号表的位置、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址。

.dynamic段保存的信息有点像ELF文件头,只是ELF文件头中保存的是静态链接时的相关内容,即静态链接时的符号表、重定位表。.dynamic就是保存的动态链接所使用的相应信息,可以看做是动态链接下ELF文件的文件头。

动态符号表

在静态链接中,有一个专门的段叫做符号表.symtab,里面保存了所有关于该目标文件的符号的定义和引用。在动态链接中,为了表示动态链接这些模块之间的符号导入和导出关系,ELF专门有一个叫做动态符号表(Dynamic Symbol Table)的段来保存这些信息,这个段的名字通常叫做.dynsym。与“.symtab”不同的是“.dynsym”只保存了与动态链接相关的符号,对于模块内部的符号则不保存。很多时候动态链接的模块同时拥有“.dynsym”和".symtab"两个表,“.symtab”保存了所有的符号,包括“.dynsym”的符号。

动态符号表也需要一些符号的表,比如保存符号名的字符串表。静态链接时叫做符号字符串表“.strtab”,在动态符号表中就动态符号字符串表“.dynstr”。由于需要在程序运行时查找符号,为加快符号的查找过程,往往还有辅助的符号哈希表“.hash”。

动态链接重定位表

在动态链接下,无论是可执行文件还是共享对象,一旦有了依赖对象,就会有导入的符号,那么代码或者数据中就会有对导入符号的引用。在编译时这些导入符号的地址未知,在静态链接中,这些未知的地址引用在最终链接时被修正。在动态链接中,导入符号的地址在运行时才确定,所以需要在运行时将这些导入符号的引用修正,即需要重定位。

不论共享对象是否是使用PIC的方式编译,都需要在装载的时候重定位。使用PIC方式编译的共享对象,代码段不需要重定位,但是数据段还包含了绝对地址的引用,因为代码段中绝对地址相关的部分被分离出来,变成了GOT,而GOT实际上是数据段的一部分。除了GOT以外,数据段还可能包含绝对地址引用。

静态链接中目标文件的重定位是在静态链接时完成的,而共享对象的重定位是在装载时完成的。在静态链接中,目标文件里面包含了专门用于表示重定位信息的重定位表,比如“.rel.text”表示是代码段的重定位表,“.rel.data”表示的是数据段的重定位表。

在动态链接的文件中,也有类似的重定位表“.rel.dyn”和“.rel,plt”,分别当当于“.rel.text”和“.rel.data”。“.rel.dyn”是对数据引用的修正,修正的位置位于“.got”以及数据段,而“.rel.plt”是对函数的修正,修正的位置位于“.got.plt”。

动态链接的步骤

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

动态链接器自举:对于普通的共享对象来说,重定位工作是动态链接器做的,依赖的其他共享对象也是动态链接器负责链接和装载。但是对于动态链接器本身来说,不可以依赖任何共享对象,并且需要的全局变量和静态变量的重定位工作是由自己完成。动态链接器在启动时有一段代码可以完成重定位且不用到任何的全局变量和静态变量。这种有一定限制条件的启动代码称为自举

当操作系统将控制权交给动态链接器的时候,动态链接器的自举代码就开始执行。自举代码首先找到自己的GOT,GOT的第一个入口保存的是.dynamic段的偏移地址,这样就得到了动态链接器本身的重定位表和符号表。

装载共享对象

动态链接器完成自举后,将可执行文件和链接器本身的符号表都合并到一个符号表当中,称为全局符号表(Global Symbol Table)。然后链接器开始寻找可执行文件所依赖的共享对象,在“.dynamic”中。链接器可以列出可执行文件所依赖的共享对象,并将这些共享对象放在一个装载集合中。然后链接器依次从装载集合中取出一个共享对象的名字,并找到相应的文件后打开这个文件,读取相应的ELF文件头和“.dynamic”段,然后将对应的代码段和数据段映射到进程空间中。

一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖的现象称为全局符号介入(Global Symbol Interpose)。linux规定当一个符号需要被加入到全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。

重定位和初始化

当装载完成后,链接器开始遍历可执行文件和每个共享对象的重定位表,将所有的GOT/PLT中的每个需要重定位的位置进行修正。因为已经有了全局符号表,所以这个修正过程比较容易。

重定位完成后,如果某个共享对象有“.init”段,那么动态链接器会执行“.init”段中的代码,用以实现共享对象特有的初始化过程。

如果进程的可执行文件也有“.init”段,那么动态链接器不会执行它,因为可执行文件中的“.init”段和“.finit”段由程序初始化部分代码执行。

当重定位和初始化完成后,动态链接器就会把控制权交给程序的入口,并开始执行。

动态链接器的实现

动态链接器的路径是/lib/ld-linux.so.2,但是这个实际上是一个软连接,真正的动态链接器是/lib/ld-x.y.z.so。动态链接器是一个非常特殊的共享对象,它不仅是共享对象还是个可执行的程序,可以直接在命令行下执行。

显式运行时连接

支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接,也叫做运行时加载。动态库的装载由一系列动态链接器提供的API:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)、关闭动态库(dlclose),程序可以通过这几个API对动态库操作。

dlopen

用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程。函数原型为:void * dlopen(const char *filename, int flag);

第一个参数是动态库的路径,如果路径是绝对路径则会直接打开。如果是相对路径的话,那么dlopen()会尝试以一定的顺序取尝试查找该动态库文件。

  1. 查找有环境变量LD_LIBRARY_PATH指定的一系列目录。
  2. 查找由/etc/ld.so.cache里面所指向的共享库路径。
  3. /lib、/usr/lib

第二个参数flag表示函数符号的解析方式,RTLD_LAZY表示延迟绑定,当函数第一次用到时才进行绑定。RTLD_NOW表示当模块被加载时即完成所有的函数绑定工作,如果有任何未定义的符号引用的绑定工作未完成,dlopen()就会返回错误。

dlsym

找到动态库中需要的符号,函数原型为:void * dlsym(void *handle, char * symbol)

第一个参数是dlopen()返回的动态库的句柄,第二个参数是要查找的符号的名字,一个以“\0”结尾的字符串。如果dlsym找到了相应的符号,则返回符号的值,如果没有找到相应的符号,则返回NULL。如果查找的符号是个函数,那么返回的是函数的地址,如果查找的符号是个变量,那么返回的是变量的地址,如果查找的符号是一个常量,那么返回的是常量的值。

如果常量的值刚好的0,则怎么判断是否找到了符号?

就要用到dlerror()来判断了。

dlerror

如果找到了就会返回NULL,没有找到返回NULL。dlerror返回值类型是char*,表示上一次调用是否成功。

dlclose

将一个已经加载的模块卸载,系统会维持一个加载引用计数器,每次使用dlopen()加载某模块的时候,相应计数器加一。每次使用dlclose卸载某模块的时候,相应的计数器减一。只有当计数器值减到0时,模块才会被真正的卸载。卸载的过程是执行“.finit”段代码,然后将相应的符号从符号表中去除,取消进程空间和模块的映射关系,然后关闭模块文件。

共享库

linux库对共享库的命名规则是:libname.so.x.y.z。最前面使用前缀lib,中间是库的名字,后缀是.so,最后面跟着的是版本号,x是主版本号,y是次版本号,z是发布版本号。

共享库的系统路径

  1. /lib:这个位置主要存放了系统最关键和基础的共享库,比如动态链接器、c语言运行库、数学库等。主要是在/bin和/sbin路径下要用到的库。
  2. /usr/lib:主要保存的是一些非系统运行时所需要的关键性的共享库,主要是一些开发时要用到的共享库,这个共享库不会被用户的程序或者shell脚本直接用到。
  3. /usr/local/lib:用来放置一些和操作系统本身并不十分相关的库,主要是一些第三方的应用程序的库。

共享库的查找过程

linux中有ldconfig程序,这个程序的作用就是为共享库目录下的各个共享库创、删除或者更新相应的符号连接。

环境变量

LD_LIBRARY_PATH

linux系统中,LD_LIBRARY_PATH是一个由若干个路径组成的环境变量,每个路径之间由冒号隔开,默认情况下,LD_LIBRARY_PATH为空。如果我们为某个进程设置了LD_LIBRARY_PATH,那么进程在启动的时候,动态连接器在查找共享库时,会首先查找由LD_LIBRARY_PATH指定的目录

linux中除了使用LD_LIBRARY_PATH=/xxx来指定外,还可以在直接运行动态链接库来启动程序,/lib/lib-linux.so.2 -library-path /home/user /bin/ls。

动态链接器会按照下列的顺序依次装载或查找共享对象。

  1. 由环境变量LD_LIBRARY_PATH指定的路径。
  2. 由路径缓存文件/etc/ld.so.cache指定的路径。
  3. 默认共享库目录,先/usr/lib,然后/lib。

LD_LIBRARY_PATH也会影响GCC编译时查找库的路径,它里面包含的目录相当于链接时GCC的”-L “参数。

LD_PRELOAD

LD_PRELOAD可以指定预先装载的一些共享库甚至是目标文件。在LD_PRELOAD里面指定的文件会在动态链接器按照固定规则搜索共享库之前装载,比LD_LIBRARY_PATH里面所指定的目录中的共享库还要优先。无论程序是否依赖于它们,LD_PRELOAD里面指定的共享库或目标文件都会被装载。

LE_PRELOAD里面指定的共享库或者目标文件中的全局符号就会覆盖后面加载的全局同名福符号。

LD_DEBUG

LD_DEBUG可以打开动态连接的调试功能,当我们设置这个变量的时候,动态链接器会在运行时打印出各种有用的信息。LD_DEBUG可以设置files(装载过程)、bindings(显示动态连接的符号绑定过程) 、libs(显示共享库的查找过程)、versions(显示符号的版本依赖关系)、reloc(显示重定位过程)、symbol(显示符号表查找过程)、statistics(显示动态链接过程中的各种统计信息)、all(显示所有信息) 、help(显示上面的各种可选值的帮助信息)。

共享库的创建

创建共享库需要在使用GCC编译的时候添加两个参数,-shared和-fPIC。-shared表示输出结果是共享库类型。-fPIC表示使用地址无关代码技术来输出文件。还有一个参数就是“-WI”,这个参数可以将指定的参数传递给链接器。

默认情况下,链接器在产生可执行文件时,只会将那些链接时被其他共享模块引用到的符号放到符号表中,这样可以减少动态符号表的大小。所以在使用dlopen函数动态加载某个模块时,该动态模块需要反向引用主模块的符号,可能主模块的某些符号因为在连接是没有被其他共享模块引用,而没有放到动态符号中,导致反向引用失败。为了防止这种情况出现,ld提供了一个“-export-dynamic”的参数,可以将全局符号导出到动态符号表。也可以在GCC中使用“-Wl,-eport-dynamic” 将该参数传递给链接器。

清除符号信息

对于发布版本来说,调试信息用处不大,可以使用strip工具,清除调共享库或可执行文件的所有符号和调试信息。去掉符号信息的版本只有原来的一半或者不足一半。

也可以在ld的时候使用“-s”或者“-S”参数,使得链接器生成输出文件时就不产生符号信息。“-s”和“-S”的区别就是:“-S”消除调试符号信息,而“-s“消除所有符号信息。也可以在GCC中通过”-Wl,-s“和”-Wl,-S“给ld传递这两个参数。

共享库的安装

创建共享库后,需要将它安装在系统中,以便于各种程序都可以共享它。最简单的方法就是将共享库复制到给某个标准的共享库目录,如/lib,/usr/lib等,然后运行ldconfig。

将文件拷贝到系统库路径需要root权限,没有root权限还可以使用ldconfig -n shared-libary-directory来指定目录。在编译时,也需要指定共享库的位置,GCC提供了两个参数“-L”和“-l”,分别用于指定共享库搜索目录和共享库的路径。

共享库构造和析构函数

如果想要共享库在装载的进行一些初始化工作,GCC提供了一种共享库的构造函数,只要在函数声明时上__attribute__((constructor)) 的属性,即指定该函数为共享库构造函数,拥有这种属性的函数会在共享库加载时被执行,即在程序的main函数之前被执行。如果使用dlopen打开共享库,共享库构造函数在dlopen函数返回之前执行。

同样的,析构函数可以在函数声明时加上__attribute((destructor))的属性,这种函数会在main()函数执行完后执行或者调用exit()时执行。如果是在运行时加载的,当使用dlclose()来卸载共享库时,析构函数将会在dlclose()返回之前执行。

猜你喜欢

转载自blog.csdn.net/qq_41323475/article/details/127856519