动态链接及其部分实现细节
Posted taocr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态链接及其部分实现细节相关的知识,希望对你有一定的参考价值。
一、动态链接的好处
前面说了静态链接的流程,提到了静态链接与动态链接的不同之处以及各自的优势:静态链接的优势在于其优秀的可移植性,但是相对应的其所占空间大小也很大,且还有在对程序的更新、维护方面也有着问题。
动态链接则消除了这方面的问题,即使得空间不再浪费,更新一个程序也变得不再麻烦。
浪费内存问题的解决
假设有两个程序a和b,如果两个都依赖于Libc.o这个模块,那么当我在系统中同时执行这两个程序时,静态链接的情况下就会在内存中产生两个Libc.o的副本,典型的浪费了空间
而动态链接将链接的过程推迟到了运行时就解决了这个问题,运行前先将可执行文件装入内存,然后查询所需要依赖的目标文件是否已存在于内存,如存在就不必再装入,如不存在就将其装入,直到所有依赖的目标文件装入完成,随即开始进行链接的过程
已发布程序更新、维护问题的解决
上面的方法不仅解决了空间浪费的问题,对于程序更新维护的问题也一并解决了。
例如a程序中使用的Lib.o由其他人提供,当此人对其进行了更新了以后,那么使用静态链接的情况下,a程序的开发者需要拿到最新的Lib.o,进行重新链接后将新的a程序发布给用户,这种情况下程序中任何一个小地方的改动都会导致程序重新下载,而对于动态链接来说,用户只需要下载最新的Lib.o就可以了,只要保证调用的函数接口不变,就可以直接在运行时链接。
动态链接的基本思想即将链接的过程推迟进行,在运行时加载至内存后再进行链接。
二、简单了解动态链接
/*a.c*/
#include "Lib.h"
int main()
foobar(1);
return 0;
/*b.c*/
#include "Lib.h"
int main()
foobar(2);
return 0;
/*Lib.c*/
#include <stdio.h>
void foobar(int i)
printf("This message from Lib.so %d\\n",i);
/*Lib.h*/
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif
上面是两个简单程序a和b,都依赖于Lib.o这个目标文件,Lib.h是需要包含头文件
动态链接流程
首先将Lib.c编译成一个共享对象文件
gcc -fPIC -shared -o Lib.so Lib.c /*-fPIC表示产生地址无关代码,-shared表示产生共享对象,关于-fPIC在后面进行解释*/
然后生成了Lib.so这个共享对象,之后分别编译连接a.c和b.c
gcc -o a a.c ./Lib.so
gcc -o b b.c ./Lib.so
从而生成a和b两个ELF文件
链接过程中的疑问
总结一下上面动态链接的流程
这里产生一个问题:Lib.so也参与了a.c文件的链接过程,但是前面说过动态链接的基本思想是将链接过程推迟到加载后再进行链接,这里貌似冲突了?
之前解释过编译的过程,在a.c文件的编译过程中foobar()函数编译器是不知道其地址的,于是将这个问题留给链接器处理。
这里链接器就对foobar()确定其性质,如果它在静态链接库中就使用静态链接的规则进行重定位,而如果foobar()位于动态共享对象中的话,就将这个符号标记为一个动态链接的符号,不进行重定位,留到加载时再进行。
这里参与链接过程的Lib.so实际上提供了符号信息,即用于说明foobar是一个定义在Lib.so中的动态符号,于是链接器不对其进行重定位,留待加载时进行。
动态链接程序的地址空间分布
对于一个静态链接文件,整个进程只需要映射一个文件即ELF文件本身,但是对于动态链接,除了可执行文件本身,还需要映射它所依赖的共享目标文件,那么进程地址空间如何分布?
由于需要查看虚拟地址空间分布,我们在前面Lib.c的基础上进行修改
/*Lib.c*/
#include <stdio.h>
void foobar(int i)
printf("This message from Lib.so %d\\n",i);
sleep(-1);
将其编译成为共享对象文件(.so),利用动态链接的特性,我们不需要再去对原本的a.c及b.c进行链接,可以直接执行
我们可以看到整个虚拟地址空间中有着多个文件的映射,其中a是我们链接生成的ELF文件,Lib.so是我们定义的共享对象文件,不过还有两个文件,其中libc-2.19.so是动态链接形式的C语言运行库,另一个ld-2.19.so则是动态链接器。
从中不难发现动态链接与静态链接的链接器不同,动态链接的扩展名也为.so,分明也是一个共享对象,与静态链接不同,当系统开始运行a程序时,首先会将控制权交给动态链接器,当完成所有动态链接工作后再把控制权还给a程序,进而执行程序。
分别看a文件与Lib.so文件的装载属性
可以看到与静态链接文件相同,其中有两个装载的Segment,整个a文件的起始装载地址为0x08048000,与静态链接文件不同之处在于其多了一些Segment,这是为了实现动态链接而所需要的部分
接着来看Lib.so的装载属性
除了文件类型与普通程序不同以外,其他几乎与普通程序一样,不过还有一点不同,.so文件的装载地址从0x00000000开始,这个地址很明显是无效的,而且从前面查看进程的虚拟地址空间分布中可以看出Lib.so文件的实际装载地址也不是0x00000000,于是得出一个结论,共享对象的最终装载地址在编译时并不确定。
将刚刚看到的进程名为4853的执行a程序的进程终止,然后再执行一次./a &,并查看其虚拟地址分布,比较两者的不同
将刚刚看到的进程名为4853的执行a程序的进程终止,然后再执行一次./a &,并查看其虚拟地址分布,比较两者的不同
可以看到这次的进程号为5839,同时除了a文件以外,其他3个共享对象文件的映射位置都已经与之前那次不同。
因此可以知道,共享对象是在装载时,装载器根据当前地址空间的空闲情况为其动态分配地址空间的。
三、动态链接细节
1、装载时重定位与地址无关代码
前面说过,共享对象在装载时的地址不是指定好的,其实是装载器根据当前地址空间的空闲情况为其动态分配的,那么为什么要在任意地址空间为其分配呢?
动态链接的情况下,不同的模块装载地址一样是不行的。对于一个单个程序,我们可以指定各个模块的地址,但是对于某个模块被多个程序使用,或者是多个模块被多个程序使用,那么就会产生冲突的情况,比如1个人指定A模块为0x1000-0x2000,另一个人不使用B模块,而且指定B模块地址为0x1000-0x2000,那么很明显,A与B两个模块无法同时存在,任何人不能再同一个程序内使用模块A与B。
另外,此种情况下升级共享库也成了很大的问题,首先共享库必须保持其中全局函数与变量地址不变,因为链接时已经绑定了这些地址,如果改变就要重新链接,而且由于被分配的地址空间肯定有限制,所以对于共享库的升级,其大小不能增大太多,否则就会超过被分配空间。
因此共享对象必须要在任意地址加载,那么就不能假设自己在进程虚拟地址空间中的位置。
为了完成上面所说的共享对象在任意地址加载,那么如何解决共享对象地址的问题呢?程序运行时,指令所调用的函数地址或者变量地址都必须是确定的,对于共享对象这种任意地址加载的情况下,如果解决呢?
装载时重定位
首先运用前面在静态链接中所学到的重定位的概念,那么稍加改变,在链接的过程中仍然不对程序中使用到的动态链接符号进行重定位,推迟到装载时再完成,即一旦模块装载地址确定,就对程序中所有绝对地址引用进行重定位。这样的方式叫做装载时重定位,而之前在静态链接中所提到的重定位叫做链接时重定位。
但是此种情况下还是有个问题,我们所希望的共享对象在内存中后可以被多个进程共享,即只要装载共享对象一次即可不像静态链接情况下需要多次装载(这里的装载共享对象一次是指物理内存中装载了共享对象),但是对于不同进程来说,物理内存中的共享对象在在各自的虚拟地址动检还是需要各自进行映射,同时各自映射的地址也不同,这就造成了共享对象的变量及一些函数的地址在虚拟地址空间中的不同。对于装载时重定位来说,只要重定位就必须要改变指令中的具体地址,特别是绝对地址,势必造成各自进程中的共享对象的代码段不同,那么要使其能够正常运行,就必须有各自的副本,这就失去了和静态链接相比能够节省内存的优势,所以这种方法是不太适合的。
于是引出了第二种能够让共享对象任意地址加载的方法。
地址无关代码
对于地址无关代码,需要分情况来讨论,共享对象模块中的地址引用可以按照是否跨模块分为两类:模块内与模块外,按照引用方式的不同可分为指令引用与数据访问,指令引用与数据访问实际上就是我们在静态链接中两种重定位入口的区别(指令引用使用相对地址引用而数据访问采用绝对地址引用)
以一个例子来实际的理解地址无关代码技术:
static int a;
extern int b;
extern void ext();
void bar()
a = 1; /*模块内部数据访问,类型b*/
b = 2; /*模块外部数据访问,类型d*/
void foo()
bar(); /*模块内部函数调用,类型a*/
ext(); /*模块外部函数调用,类型c*/
gcc -shared -fPIC -o pic.so pic.c
可以得到由上面的源文件得出的共享对象文件。
a、模块内调用
由于被调用函数与调用者位于同一个模块,于是相对位置固定,所以根本不要用到地址无关代码的技术,直接进行相对地址引用,不需要重定位。
图中其实地址位于5a3的语句即为调用bar()函数的语句,不过可以看到和想象中有些不同,调用的是bar@plt函数,关于这个牵扯到了延迟绑定(PLT)的内容,后面会说明。
b、模块内数据访问
对于模块内部的数据访问,使用的是绝对地址,因为前面说过指令中不能直接包含数据的绝对地址,所以我们要将其改为相对地址。
一个模块前面若干页是代码,后面若干页是数据,这些页之间的相对位置固定,那么就简单了,任何一个指令与其所需要访问的模块内部数据之间的相对位置固定,于是相对于当前命令加上偏移量即可。
图中的白底部分即访问模块内部变量a,并赋值为1的具体实现,其中第一句调用了__x86.get_pc_thunk.cx函数,那么这个函数是干什么的呢?
这就是这个函数的具体实现,可以看到只是将堆栈的栈顶指针esp的值赋值给ecx寄存器,那么目的是什么呢?
当处理器执行call指令后,下一条指令的地址会被压到栈顶,而esp即指向栈顶,于是这个函数能够将下条指令的地址存入ecx寄存器。
那么我们继续看下面的语句,将ecx寄存器中的值加上0x1a8d,我们可以计算一下:下一条指令地址+0x1a8d=0x0573+0x1a8d=0x2000,于是此时ecx寄存器中存储的地址应是0x2000,看看这个地址位于哪里
.got.plt段(延迟绑定中会讲到此段的具体作用)的起始地址就是0x2000,当然这是还没有装载时的地址,如果装载的话上面计算的地址都要加上共享对象装载的起始地址的,于是上面的两句实际上找到了.got.plt段的具体位置。
最后在这个地址的基础上加上了偏移量0x24,于是比对上一张图我们可以看到,实际上找到了.bss段中,而对于没有初始化的全局变量,确实存放于.bss段中。
ELF使用模块的实际装载地址+下条指令的地址+偏移量获得.got.plt段的地址,再根据具体变量与.got.plt段起始地址的偏移量找到数据变量。
于是模块内的调用与数据访问都是用相对地址完成。
c、模块外部数据访问
由于共享对象的地址要到装载时才能确定,所以共享对象模块间的数据访问也需要等到装载时才能够决定,所以比较麻烦。
这里是模块外部的数据访问,无法计算模块与模块间的偏移量,那么相对地址引用就无法使用,必须牵扯到绝对地址的引用。此种情况下,要使地址代码无关,基本思想就是把跟地址相关的部分放到数据段中由于数据段每个进程都有各自的副本,所以不会影响到代码段的多进程共享。于是ELF在数据段中建立一个指向这些变量的指针数组,即全局偏移表(Global Offset Table),代码需要引用此全局变量时,通过GOT中相对应的项间接引用即可。
比如指令要访问b,就会先找到GOT,根据其中变量所对应的项找到目标地址,每个变量对应一个4字节的地址(32位)。装载模块时链接器会查找每个变量所在地址,充填GOT中的项。
图中是例子中bar( )函数对模块外部变量b进行数据访问并赋值2的语句,前面说过eax寄存器的值为.got.plt段的起始地址,此时第一句在此地址基础上减去偏移量0x14,实际上找到了0x1fec的位置
从图中可以看到,0x1efc的位于.got段中,且应为第二项,于是找到了变量b的绝对地址,从而给变量b赋值。
d、模块外部函数调用
上面那种情况理解后,这种就很好理解,就是在GOT中符号所对应的并不再是变量地址,而是函数的入口地址,从而通过GOT中找到相应的项然后找到相应的入口地址,从而跳转执行
上面就是地址无关代码所使用的技术,可以看到通过这种方法我们解决了共享对象模块内以及其与其他模块之间的数据访问与函数调用问题,于是我们得以实现多个进程同时共享一个装载完成的共享对象的目的。
e、额外情况
下面考虑一种情况:
如果在一个叫做module.c文件中使用了一个定义于其它模块(但不是共享对象)的整型变量global,那么如何声明?
extern int global;
如果是共享对象模块中定义的整型变量呢?
extern int global;
这里应该已经发现了一个问题,这种情况下,链接器无法分辨这个变量到底是定义在哪里了,是使用了PIC的共享对象模块呢还是没有使用PIC技术的主模块中呢?前者需要在装载后才能够知道具体地址,那么在链接时候就无法重定位出正确地址,而后者则在链接时就可重定位正确地址。对于这种不知道的情况,链接器会在.bss段中定义一个global变量的副本,于是问题出现,.bss段中有个副本,而共享对象模块中还有其定义的那个部分,一个变量出现在了两个位置上,这在实际运行过程中是肯定不行的,那么如何处理?
解决的办法就是统一所有使用这个变量的指令都指向同一个位置,这个位置是.bss段中的那个副本。共享对象编译时,默认将所有定义于模块内部的全局变量当做定义在其他模块的全局变量,于是就像前面所说PIC中的类型三,通过GOT来进行数据的访问。在装载时,会对主模块中的变量进行判断,如果某个全局变量有副本,那么就把GOT中的相应项指向这个副本,于是得以使这个变量的位置统一,如果这个变量在共享对象中进行了初始化,那么就将这个初始化的值也放入副本中;如果没有这个变量的副本,那么就自然指向了共享对象模块内部的那个唯一的副本。
2、延迟绑定(PLT)
动态链接的确比起静态链接来说有许多优势,节省内存、更易更新维护等,但是它也因此付出了一定的代价,使得ELF程序在静态链接下摇臂动态链接稍微快些,根据前面所讲,这些代价来自于两方面:1、动态链接在装载后进行链接工作;2、动态链接对于全局和静态的数据访问要进行复杂的GOT定位,然后进行间接寻址,比静态链接麻烦的多。那么如何进行优化?
其实我认为这种性能优化的中心思想和动态链接的基本思想差不多,根本来说就是推迟,不需要马上用的函数就推迟对其的链接,这种方法就叫做延迟绑定(PLT),即当函数第一次被用到时才进行绑定(符号查找、重定位等操作),如果用不到就不进行绑定。
由于有了上面的技术,所以可以推断出程序开始执行时,模块间函数的调用都没有进行绑定,而是等到需要使用时才会由动态链接器负责绑定,于是大大加快了程序的启动速度。
那么延迟绑定如何实现?
前面说过,对于调用外部模块的函数时,由于地址无关代码的机制,我们需要通过GOT中相应的项来进行间接跳转,而PLT为了实现延迟绑定,在此基础上又加了一层间接跳转。
因为实际实验中的代码跟原理上有点不同,所以这里先说一下原理,之后再来看实际的情况
bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dl_runtime_resolve
这是一个bar@plt的实现,即在GOT的基础上多加的一层间接跳转的具体代码。
首先第一个jmp语句是一个间接跳转,跳转的具体位置即后面标识的地方,这里就是GOT中保存bar()的相应的项,如果链接器已经初始化了该项(填入了正确的地址),那么就会直接跳转到bar()函数执行,但是为了实现PLT,链接器在初始化的阶段实际填的地址是下一条指令的地址,于是这条相当于执行下一句。当然,等到实现了绑定后,这里就实际上完成了跳转执行bar()函数的功能;
第二句push,相当于bar这个符号在重定位表“.rel.plt”中的下标,即函数的编号。这里考虑下,如果我们需要使用一个函数来绑定函数地址,那么我们需要哪些参数?答案是需要知道这个地址绑定发生在哪个模块中的哪个函数,这里的n实际上就是将哪个函数的参数进行压栈;
第三句push,上面已经压入了发生绑定的函数编号,下面就是模块了,于是压入模块ID;
最后跳转_dl_runtime_resolve,这个函数就是用来根据模块ID及函数编号进行绑定的函数,于是完成了绑定的功能。接下来第二次跳转函数的地址,就可以进而执行具体函数的内容,返回时根据堆栈中保存的EIP直接返回到调用者,不再继续执行bar@plt第二条开始的代码。
上面就是PLT间接调用的基本流程
接下来来看实际情况
图中即foo函数的反汇编代码,其中白底的两行代表调用bar( )及ext( )的语句。
先看上面的两句,其实这里很熟悉,前面讲地址无关代码时这里已经讲过,即执行白底两句时ebx寄存器中存储的地址是0x2000(.got.plt地址),实际的运用中,ELF将GOT拆分成了.got和.got.plt两部分,区经济中.got用来保存全局变量的地址,而.got.plt用于保存函数引用的地址,于是很明显这里调用函数,必须在.got.plt中找寻具体项从而找到具体地址。
这是第一句call 400 <bar@plt>
真实跳转的位置,可以看到第一句跳转到了.got.plt的第四项的位置(32位系统4字节是一项),根据前面的原理,这里的跳转的应该是bar( )的具体项从而具体找到了bar( )的地址,不过这是第一次执行,还没有绑定,于是跳转下一句;
第二句push $0x0,根据前面原理中,这里压入的应该是在重定位表“.rel.plt”中bar这个符号的下标,即表中第一个重定位入口(通过readelf -r 文件名
可以看到具体表中情况);
第三句跳转一个函数,这是因为在一个模块中,对于模块id肯定相同,且所要运行的函数的绑定地址也是相同的,那么就可以写成一个函数节省空间。
具体的函数中,第一句将模块id压入,第二句跳转到具体执行绑定所用的函数。
不过这里压入的模块id时0x4(%ebx),跳转的地址为0x8(%ebx),这是为什么?这里牵扯到了.got.plt的特殊之处,它的前三项有着特殊意义:
1、第一项保存“.dynamic”段的地址,这个段用于描述本模块汇总的动态链接相关信息
2、第二项保存本模块ID
3、第三项保存_dl_runtime_resolve( )的地址
这样就能懂了吧,压入的模块id是找到了.got.plt中的第二项中的模块ID,而跳转的地址是找到了.got.plt中第三项中保存的地址。
于是经过上面的步骤,PLT的延迟绑定技术得以实现,使得动态链接的性能得以提高。另外.plt段也是一个地址无关代码,所以可以跟代码段合并成一个可读可执行的Segment装载。
3、动态链接的一些相关结构(.so文件)
先描述一遍动态链接ELF文件的执行过程:首先操作系统读取可执行文件的头部,检查文件合法性,然后从头部的“Program Header”中读取每个“Segment”的虚拟地址、文件地址及属性,并且进行映射。
以上的步骤都和静态链接没有什么区别。
接下来静态链接的话,操作系统接着把控制权交给ELF文件的入口地址,然后程序开始执行,但是动态链接和静态链接不同。动态链接的情况下,操作系统先启动一个动态链接器,即ld.so文件,动态链接器实际上也是一个共享对象,操作系统也通过映射的方式将它加载到进程的地址空间中,在加载完动态链接器后,将控制权交给动态链接器的入口地址,之后动态链接器开始执行自己的初始化操作,再对可执行文件进行动态链接工作,在所有的动态链接工作完成以后,动态链接器将控制权转交到ELF文件的入口地址,程序得以执行。
.interp段
上面的过程中,动态链接器也是一个共享对象,需要被映射到内存中,那么如何知道哪个是所需要使用的动态链接器呢?
答案是.interp段
通过objdump可以查看.interp段的具体内容
可以看到.interp段中的内容实际上就是保存了一个字符串,即动态链接器的路径,在Linux下ELF文件所需动态链接器的路径几乎都是这个路径,不过实际查看可以发现这是个软链接文件,其指向了具体的动态链接器。Linux下动态链接器是Glibc的一部分,即属于系统库,于是在更新时会改变这个软连接文件的内容,使其指向最新的链接器,而对于可执行文件本身,并不需要区修改.interp中的动态链接器路径。
.dynamic段
对于动态链接的ELF文件来说,其中用于动态链接的段并不止.interp这个用于指明动态链接器的段,还有.dynamic及.dynsym段等,其中最重要的结构应该是.dynamic段,它保存了动态链接器所需要的基本信息,依赖于哪些共享对象、动态链接符号表位置、动态链接重定位表位置、共享对象初始化代码地址等等。
.dynamic段的结构就是多个结构体,结构体定义在/usr/include/elf.h文件中
可以看到对于32位与64位分别尤其自己的结构,这个结构有一个类型值加上一个附加的数值或指针组成,对于不同的类型,后面的数值或指针有着不同的意义,elf.h文件中也有记录了所有的类型
图中只记录了一部分的类型,可以看到其中有着许多种类型,比如DT_NEEDED表示需要的库的名称、DT_SYMTAB表示动态链接符号表.dynsym的地址、DT_STRTAB表示动态链接字符串表.dynastr的地址等
我们可以直接对.dynamic段进行查看
根据上面的这些信息可以看出.dynamic段中保存的信息比较像ELF文件头,只是我们前面看到的ELF文件头中保存的是静态链接时相关的内容,这里换成了动态链接下使用的相应信息。
动态符号表
在前面的静态链接中曾经介绍过在静态链接的目标文件及ELF文件中存在着一个段叫做符号表.symtab
图中即一个静态链接文件的符号表,里面保存了所有关于该文件的符号的定义及引用,即所有关于符号方面的信息都存储于其中。
动态链接也有着这样一次相似的段,称做动态符号表.dynsym。但是与.symtab不同,.dynsym中只存储与动态链接的符号,而对于模块内部的符号等则不保存,因此动态链接的模块一般拥有.dynsym和.symtab两个表,其中.symtab保存了所有符号包括了.dynsym中保存的符号。
图中可以看到动态符号表.dymsym只有14条符号,而符号表.symtab中有着57条符号信息,实际查看中确实包含了所有的动态符号表中的所有信息。
不管对于.symtab与.dynsym来说都需要一些辅助表,比如保存符号名的字符串表:分别对应的是.strtab与.dynstr,与此同时,在动态链接的情况下,需要在程序运行时查找符号,为了加快速度,还有辅助的符号哈希表,在静态链接中并无此段。
动态链接重定位表
动态链接将链接的过程推迟到装载时再进行,说起链接的过程,其中最重要的就是重定位的过程,对于共享对象来说需要重定位就是因为它调用了其他模块中定义的符号,在编译的时候这些符号的地址未知。静态链接中这些未知地址在最终链接时修正,但在动态链接中在装载后进行链接重定位。
对于一个动态链接,如果不使用前面说的PIC技术(地址无关代码),在装载时肯定是需要重定位;那么使用了PIC技术的呢?共享对象的代码段是不需要进行重定位了,但是别忘了地址无关代码中位于数据段中的GOT部分,那并不是地址无关的,因此还是需要重定位。
在静态链接中有专门用于表示重定位信息的重定位表,即代码段重定位表.rel.text及数据段重定位表.rel.data,而在动态链接中既然也需要重定位那么当然也有重定位表,即.rel.dyn及.rel.plt,其中.rel.dyn是对数据引用的修正,用于修正.got及数据段;.rel.plt是对函数引用的修正,用于修正.got.plt。
图中即一个共享对象的两个重定位表。
重点来看重定位表中各重定位入口的类型,前面的静态链接接触过两种类型即R_386_32及R_386_PC32,这里我们看到有三种类型:R_386_RELATIVE、R_386_GLOB_DAT、R_386_JUMP_SLOT,其实静态链接所接触的两种重定位入口都是比较复杂的,这里的三个都是很简单的。
其中R_386_GLOB_DAT和R_386_JUMP_SLOT两个重定位入口,它们表示被修正的位置只需直接填入符号的地址,而对于R_386_RELATIVE的类型,其重定位的方法就是将此重定位入口相对于起始地址(共享对象起始地址未装载前为0)的偏移加上装载后的具体起始地址
动态链接进程堆栈初始化信息
在静态链接曾经说过,进程初始化时,堆栈中保存了进程执行环境(即环境变量)以及命令行参数等信息,动态链接也有这些信息,但是动态链接仍然包含其他信息。
对于一个动态链接来说,操作系统映射完ELF文件中的Segment并且将所有需要的共享对象全部装载并映射完成后,需要把控制权交给动态链接器,由其进行链接,然后再将控制权交给ELF文件的入口地址。那么作为一个动态链接器,至少要知道一些信息,比如可执行文件的Segment数、每个Segment属性以及程序的入口地址(链接完成后动态链接器需要知道这个地址从而交出控制权)等等,这些信息都由操作系统交给动态链接器,保存在了进程的堆栈中,因此对于动态链接器来说,堆栈中保存了命令行参数信息、进程执行环境以及辅助信息(即上面说的动态链接器所需要知道的信息)。
命令行参数以及进程执行环境在静态链接中已经说明了如何存储,那么辅助信息呢?
图中就是一条辅助信息的结构体,位于elf.h文件中因此可以看出辅助信息存储时就是以这种一条条这样结构体形式存储的,其中a_type字段是指信息的类型,具体的类型的信息也有具体记录
这里就是所有关于a_type的类型,可以看到其中AT_ENTRY类型就表示了程序的入口地址,AT_BASE表示动态链接器自己的装载地址等等。
这些信息都存储在环境变量指针的后面,通过上面的观察可以简单对动态链接的堆栈初始化情况做一个简图:
以上是关于动态链接及其部分实现细节的主要内容,如果未能解决你的问题,请参考以下文章