链接(二)
参考文献:
[1]Randal E.Bryant、David R. O’Hallaron.Computer Systems A Programmer’s Perspective Third Edition 北京:机械工业出版社,2017.4;
[2]蒋本珊.计算机组成原理(第3版)[M].北京:清华大学出版社,2013;
目录:
1、静态链接库与动态链接库:
2、编译使用动态链接库:
3、编译使用静态链接库:
4、链接器解析引用:
5、重定位条目:
6、ELF可执行目标文件中的各类信息:
7、加载器:
8、静态链接库和动态链接库的优缺点:
9、总结:
10、头文件和库文件的区别:
1、静态链接库与动态链接库:
(1)Windows下的静态链接库:.lib
动态链接库:.dll
(2)Linux下的静态链接库:.a
动态链接库:.so
(3)在Linux系统中,静态链接库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。
2、编译使用动态链接库:
源程序示例test.c:
图2.1 示例test.c
要转换为动态链接库示例printname.c
图2.2 示例printname.c
(1)命令:gcc -c test.c:通过编译获得test.o文件
图2.3 命令行截图1
(2)命令:gcc -shared printname.c -o libprint.dll:将printname.c编译为动态库,编译完成后可以在当前目录下发现多了文件libprint.dll
图2.4 命令行截图2
(3)命令:gcc -o testpro test.o ./libprint.dll:告诉编译器要使用libprint的动态链接库。其中./libprint.dll也可以采用-L. -lprint来替换,-L.参数告诉链接器在当前目录下查找libprint.dll
图2.5 命令行截图3
(4)命令:start testpro.exe查看运行结果
图2.6 命令行截图4
(5)当删除了dll文件后,再运行testpro.exe程序,出现如下错误结果:
图2.7 错误结果截图
3、编译使用静态链接库:
(1)命令:gcc -c printname.c:首先编译静态链接库文件。
图3.1 命令行截图5
(2)命令:ar crv libprintname.lib printname.o:ar把目标文件归档,参数c表示在创建库的过程不警告,参数r表示如果原库不存在,则创建一个新库,如果原库存在,则用新的模块替换原来的模块。参数v表示be verbose(是冗长的)。
图3.2 命令行截图6
(3)命令:gcc -c test.c编译源文件
图3.3 命令行截图7
(4)命令:gcc -static -o testpro test.o ./libprintname.lib:-static参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行文件,它可以加载到内存并运行,在加载时无须更进一步的链接。
图3.4 命令行截图8
(5)当删除了.lib文件后,再运行testpro.exe程序,发现依旧可以运行。
图3.5 命令行截图9
4、链接器解析引用:
(1)在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。驱动程序自动将命令行中所有的.c文件翻译为.o文件。在这次扫描中,链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号集合U(引用了但是尚未定义的符号),以及一个在前面输入文件中已定义的符号集合D。
(2)如果库不是相互独立的,那么必须对它们排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义是在对s的引用之后的。
(3)如果需要满足依赖要求,可以在命令行上重复库。
(4)示例:
a和b表示当前目录中的目标模块或者静态库,而a->b表示a依赖于b,也就是说b定义了一个被a引用的符号。对于下面每种场景,请给出最小的命令行(即一个含有最少数量的目标文件和库参数的命令),使得静态链接器能解析所有的符号引用。
A)p.o->libx.a
gcc p.o libx.a
B)p.o->libx.a->liby.a
gcc p.o libx.a liby.a
C)p.o->libx.a->liby.a且liby.a->libx.a->p.o
gcc p.o libx.a liby.a libx.a
5、重定位条目:
(1)使用命令objdump -dx test.o:查看反汇编代码:
图5.1 命令行截图10
寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
重定位条目r由四个字段组成:
①r.offset = 0x3e,offset是需要被修改的引用的节偏移。
②r.symbol = _printName,symbol标识被修改引用应该指向的符号。
③r.type = DISP32,type告知链接器如何修改新的引用。
④r.addend = 0,addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。此处没有显示即为0
(2)重定位PC相对引用:
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr);
s:字节数组,存放每个节。
ADDR(r.symbol):每个节和每个符号的运行时地址。
refaddr:引用的运行时地址。
(3)重定位绝对引用:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
(4)示例:
考虑目标文件m.o中对swap函数的调用。
图5.2 示例题目截图1
图5.3 示例题目截图2
9: e8 00 00 00 00 callq e <main+0xe> swap()
它的重定位条目如下:
r.offset = 0xa
r.symbol = swap
r.type = R_X86_64_PC32
r.addend = -4
现在假设链接器将m.o中的.text重定位到地址0x4004d0,将swap重定位到地址0x4004e8。那么callq指令对swap的重定位引用的值是什么?
答:R_X86_64_PC32是重定位一个使用32位PC相对地址的引用。
ADDR(s) = ADDR(.text) = 0x4004d0,
ADDR(r.symbol) = ADDR(swap) = 0x4004e8,
r.offset = 0xa,
计算引用的运行时地址:
refaddr = ADDR(s) + r.offset = 0x4004d0 + 0xa = 0x4004da,
然后修改此引用:
*refptr = 0x4004e8 + (-4) - 0x4004da = 0xa
所以重定位引用的值是0xa。
4004d9:e8 0a 00 00 callq 4004e8 <swap>
6、ELF可执行目标文件中的各类信息:
(2).init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。
(3)段头部表:将连续的文件节映射到运行时内存段。
(4)节头部表:描述目标文件的节。
7、加载器:
(1)shell简述:shell是系统的用户界面,是一个命令解释器,提供了用户与内核之间进行交互操作的一种接口,它接收用户输入的命令并把它送入内核中去执行。
(2)加载:任何Linux程序都可以通过调用execve函数来调用加载器,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载。
(3)加载器的工作原理:
Linux系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制。子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
8、静态链接库和动态链接库的优缺点:
(1)静态链接库的优点:
①代码装载速度快,执行速度略比动态链接库快;
②只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱(DLL HELL,导出类的DLL在维护和修改时如增加成员变量、修改导出类的基类等操作所可能导致的意想不到的后果)等问题。
(2)动态链接库的优点:
①程序复用的重要方式。
②节省内存并减少页面交换;
③ DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
④不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
⑤适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
(3)不足之处:
①使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
②使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;速度比静态链接慢。当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件,统统去除,这在早期Windows中很常见。
9、总结:
(1)链接器的两个主要任务是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。
(2)静态链接器是由像GCC这样的编译驱动程序调用的。它们将多个可重定位目标文件合并成一个单独的可执行文件。多个目标文件可以定义相同的符号,而链接器用用来悄悄地解析这些多重定义的规则可能在用户程序中引入微秒的错误。
(3)多个目标文件可以被连接到一个单独的静态库中。链接器用库来解析其他目标模块中的符号引用。许多链接器通过从左到右的顺序扫描来解析符号引用,这是一个引起链接时错误的来源。
(4)加载器将可执行文件的内容映射到内存中,并运行这个程序。链接器还可能生成部分链接的可执行目标文件,这样的文件中有对定义在共享库中的例程和数据的未解析的引用。在加载时,加载器将部分链接的可执行文件映射到内存,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接任务。
10、头文件和库文件的区别:
(1)头文件是文本文件,库文件是二进制文件;
(2)头文件在编译中使用,库文件在链接中使用;
(3)头文件中是函数或定义的声明,以及少量内联函数的使用,一般不包括非静态函数实现,库文件中包含函数的实现;
(4)头文件是手动编写的,库文件是编译生成的。