静态链接对象:
多个可重定位目标模块 + 静态库(标准库、自定义库)
静态库
在类Unix系统中,静态库文件采用一种称为存档档案(archive)的特殊文件格式,使用.a后缀。
使用静态库,可增强链接器功能,使其能通过查找一个或多个库文件中定义的符号来解析符号
在构建可执行文件时,只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标模块,并且只把用到的模块从库中拷贝出来
使用ar命令创建库libc.a:
$ ar rcs libc.a atoi.o printf.o ... random.o
r:在库中插入模块(替换)。当插入的模块名已经在库中存在,则替换同名的模块。
c:创建一个库。不管库是否存在,都将创建。
s:写入一个目标文件索引到库中,或者更新一个存在的目标文件索引。
Archiver(归档器)允许增量更新,只要重新编译需修改的源码并将其.o文件替换到静态库中。
libc.a ( C标准库 )包含I/O、存储分配、信号处理、字符串处理、时间和日期、随机数生成、定点整数算术运算等1392个目标文件,大约8MB
libm.a (the C math library)包含浮点数算术运算(如sin, cos, tan, log, exp, sqrt, …)等401 个目标文件,大约 1 MB。
$ gcc –static –o myproc main.o ./mylib.a //在gcc命令行中无需明显指定C标准库libc.a(默认库)
调用关系:main→myfunc1→printf
E 将被合并以组成可执行文件的所有目标文件集合
U 当前所有未解析的引用符号的集合
D 当前所有定义符号的集合
开始E、U、D为空,首先扫描main.o,把它加入E,同时把myfun1加入U,main加入D。接着扫描到mylib.a,将U中所有符号(本例中为myfunc1)与mylib.a中所有目标模块(myproc1.o和myproc2.o )依次匹配,发现在myproc1.o中定义了myfunc1 ,故myproc1.o加入E,myfunc1从U转移到D。在myproc1.o中发现还有未解析符号printf,将其加到U。不断在mylib.a的各模块上进行迭代以匹配U中的符号,直到U、D都不再变化。此时U中只有一个未解析符号printf,而D中有main和myfunc1。因为模块myproc2.o没有被加入E中,因而它被丢弃。
接着,扫描默认的库文件libc.a,发现其目标模块printf.o定义了printf,于是printf也从U移到D,并将printf.o加入E,同时把它定义的所有符号加入D,而所有未解析符号加入U。
如果扫描到最后,U中还有未被解析的符号,则发生错误
总结:处理完libc.a时,U一定是空的。
被链接模块应按调用顺序指定!
解析后,E中有main.o、myproc1.o、printf.o及其调用的模块;D中有main、myproc1、printf及其引用的符号
静态库有一些缺点:
– 库函数(如printf)被包含在每个运行进程的代码段中,对于并发运行上百个进程的系统,造成极大的主存资源浪费
– 库函数(如printf)被合并在可执行目标中,磁盘上存放着数千个可执行文件,造成磁盘空间的极大浪费
– 程序员需关注是否有函数库的新版本出现,并须定期下载、重新编译和链接,更新困难、使用不便
解决方案: Shared Libraries (共享库)
– 是一个目标文件,包含有代码和数据
– 从程序中分离出来,磁盘和内存中都只有一个备份。
– 可以动态地在装入时或运行时被加载并链接。
– 共享库升级时,被自动加载到内存和程序动态链接。
– Window称其为动态链接库(Dynamic Link Libraries,.dll文件)
– Linux称其为动态共享对象( Dynamic Shared Objects, .so文件)
共享库可分模块、独立、用不同编程语言进行开发,效率高。
第三方开发的共享库可作为程序插件,使程序功能易于扩展。
自定义一个动态共享库文件:
gcc –c myproc1.c myproc2.c //生成可重定位文件 gcc –shared –fPIC –o mylib.so myproc1.o myproc2.o
-fPIC指示生成位置无关代码(PIC,Position Independent Code)的缩写:
1)保证共享库代码的位置可以是不确定的
2)即使共享库代码的长度发生变化,也不会影响调用它的程序
要实现动态链接,必须生成PIC代码
引入PIC后,链接器无需修改代码即可将共享库加载到任意地址运行
动态链接可以按以下两种方式进行:
-
第一次加载并运行时进行 (load-time linking).
Linux通常由动态链接器(ld-linux.so)自动处理
标准C库 (libc.so) 通常按这种方式动态被链接
gcc –c main.c gcc –o myproc main.o ./mylib.so //libc.so无需明显指出
当调用关系为main→myfunc1→printf时,有:
通过静态链接器生成的仅仅是磁盘中的部分链接的可执行目标文件。最终生成的完全链接的可执行目标是在内存中的。
加载 myproc 时,加载器发现在其程序头表中有 .interp 段,其中包含了动态链接器路径名 ld-linux.so,因而加载器根据指定路径加载并启动动态链接器运行。动态链接器完成相应的重定位工作后,再把控制权交给myproc,启动其第一条指令执行。
-
在已经开始运行后进行(run-time linking)
分发软件包、构建高性能Web服务器等时常有。
可通过动态链接器接口提供的函数在运行时进行动态链接
类UNIX系统中的动态链接器接口定义了相应的函数,如dlopen, dlsym, dlerror, dlclose等,其头文件为dlfcn.h
#include <stdio.h> #include <dlfcn.h> //若调用dlopen 、dlsym 和dlclose 时发生出错,则出错信息可通过调用dlerror 函数获得。 int main(){ void *handle; void (*myfunc1)(); char *error; /* 通过dlopen函数加载和链接包含函数myfunc1()的共享库文件mylib.so,出错返回NULL,正确返回指向共享库文件句柄的指针 */ /* PTLD——LAZY用来指示链接器对共享库中外部符号的解析时间推迟到执行库中代码时 */ handle = dlopen("./mylib.so", RTLD_LAZY); if (!handle) { fprintf(stderr, "%s ", dlerror()); //若调用dlopen、dlsym、dlclose时发生出错,则出错信息可调用dlerror获得 exit(1); } /* 通过dlsym获得共享库中所需函数,返回一个指向函数myfunc1()的指针myfunc1*/ /* 若共享库中不存在指定符合则返回NULL*/ /* 第一个参数是指向共享库的文件句柄,第二个参数用来标识指定符号的字符串,即后面将要使用的函数的函数名*/ myfunc1 = dlsym(handle, "myfunc1"); if ((error = dlerror()) != NULL) { fprintf(stderr, "%s ", error); exit(1); } /* 现在可以像调用其他函数一样调用函数myfunc1(),函数对应代码首地址由dlsyin函数返回 */ myfunc1(); /* 关闭(卸载)共享库文件,成功返回0,否则返回-1*/ if (dlclose(handle) < 0) { fprintf(stderr, "%s ", dlerror()); exit(1); } return 0; }
要编译该程序并生成可执行文件myproc, 通常用以下GCC 命令:
gcc -rdynamic -o myproc main.c -ldl
选项-rdynamic 指示链接器在链接时使用共享库中的函数
选项-ldl说明采用动态链接器接口中的dlopen 、dlsym 等函数进行运行时的动态链接。
引用情况有四种,要生成PIC代码,主要解决后两种
(1) 模块内的过程调用、跳转,采用PC相对偏移寻址
调用或跳转源与目的地都在同一个模块,相对位置固定,只要用相对偏移寻址即可。
无需动态链接器进行重定位。
(2) 模块内数据访问,如模块内的全局变量和静态变量
.data节与.text节之间的相对位置确定,任何引用局部符号的指令与该符号之间的距离是一个常数。
如图中引用a处到.txt节结束处距离为0x118c,a到.data节开始处距离为0x28
变量a与引用a的指令之间的距离为常数,调用__get_pc后,call指令的返回地址,即call指令下一条指令的地址(当前栈顶)被置ECX。若模块被加载到0x9000000,则a的访问地址为:0x9000000+0x34c+0x118c(指令与.data间距离)+0x28(a在.data节中偏移)
(3) 模块外的数据访问,如外部变量的访问
引用其他模块的全局变量,无法确定相对距离
在.data节开始处设置一个指针数组(全局偏移表, GOT),指针可指向一个全局变量
GOT与引用数据的指令之间相对距离固定
编译器为GOT每一项生成一个重定位项(如.rel节…)
call指令的值为0,执行下一条指令:
当前栈顶(call指令下一条指令的地址)置EBX,加0x1180再置EAX,指向GOT中的表项。
加载时,动态链接器对GOT中各项进行重定位,填入所引用的地址(如&b)
PIC有两个缺陷:多用4条指令;
多了GOT(Global Offset Table),故需多用一个寄存器(如EBX),易造成寄存器溢出
(4) 模块外的过程调用、跳转
类似于(3),在GOT中加一个项(指针),用于指向目标函数的首地址(如&ext)
动态加载时,填入目标函数的首地址
多用三条指令并额外多用一个寄存器(如EBX)
可用延迟绑定(lazy binding)技术减少指令条数:不在加载时重定位,而延迟到第一次函数调用时,需要用GOT和PLT(Procedure linkage Table, 过程链接表)
例如调用模块外的ext()函数时:
GOT是.data节一部分,开始三项固定,含义如下:
-
GOT[0]为.dynamic节首址,该节中包含动态链接器所需要的基本信息,如符号表位置、重定位表位置等;
-
GOT[1]为动态链接器的标识信息
-
GOT[2]为动态链接器延迟绑定代码的入口地址
-
调用的共享库函数都有GOT项,如GOT[3]对应ext
PLT是.text节一部分,结构数组,每项16B,除PLT[0]外,其余项各对应一个共享库函数,如PLT[1]对应ext
如图,调用ext()函数的过程可概括为:
(1)执行0x804845b内的call指令,偏移量为ec fe ff ff。来到位于0x804834c的PT[1]
(2)执行PT[1]的第一条指令,jump到位于0x8049590的GOT[3]内的地址0x8048352,也就是下一条指令的地址。
(3)用ID=0标识ext()函数,将0x0压栈
(4)jump到位于804833c的PT[0]
(5)将动态链接器标识信息所在位置GOT[1]0x8049588压栈
(6)jump到位于0x804958c的GOT[2]内的地址——动态链接器延迟绑定代码的入口地址
延时绑定代码根据GOT[1]和ID确定ext地址填入GOT[3],并转ext执行,
以后调用ext,GOT[3]内已经是ext()真正的首地址。只要多执行一条jump指令而不是多3条指令。