一、静态链接库
1.出现背景
上个世纪60年代,计算机的应用领域开始从简单的求解数学方程扩大到银行、航空、政府等部门的商业数据处理,美国的SAGE防空系统和阿波罗登月计划等软件规模空前的大型的项目也开始出现。
但当时软件开发还处于“手工作坊”阶段,没有高级语言,没有链接器,操作系统也只具备简单的管理硬件和加载程序功能。
同样,当时也没有共享库,程序员们直接编写汇编代码,遇到需要复用代码的时候,直接就把代码逻辑“复制粘贴”过来。可想而知,这种情况下,系统中会出现同一段逻辑代码的多个相似版本,这不仅导致了冗余,还带来了维护的难题。
这个时期是软件复杂度的“爆炸式”增长期,也是计算机科学走向成熟的关键转折点。这个时期,催生了“软件工程”,带来了结构化编程的兴起,还催生了C语言、链接器和静态库等关键技术。
2.静态链接和静态库
静态库解决了代码复用的问题。静态库文件,是将所有相关的函数编译成独立的目标模块,再用ar命令封装而成的一个单独的文件。
在Linux系统上,它以lib开头,扩展名是.a。在/usr/lib目录下,可以找到一些静态库文件:
gcc/x86_64-linux-gnu/13/libasan.a
gcc/x86_64-linux-gnu/13/libgomp.a
gcc/x86_64-linux-gnu/13/libstdc++.a
x86_64-linux-gnu/libm.a
x86_64-linux-gnu/libcrypt.a
x86_64-linux-gnu/libc.a
举例来说:
libc.a封装了所有程序都可用的标准I/O、字符串操作和整数数学函数;
libm.a封装了浮点数学函数;
libgomp.a封装了支持openmp和openacc编程导语的并行编程操作;
libcrypt.a封装了加解密操作。
可以试着使用ar x libc.a命令,抽取一下libc.a包含的.o文件看下:
在实际的软件开发里,工程师们也可以对自己的系统进行拆解,开发项目适用的.a。
在链接时,链接器只复制被程序引用的目标模块,这样可以减少可执行文件在磁盘和内存中的大小。
举例来说(例子来自《深入理解计算机系统》第7章,链接):
1.有一个提供了向量的加和乘操作的静态库libvector.a
2.有个应用main2.c,需要调用向量加操作完成自己的功能,然后给用户打印相关的提示信息。
3.Main2.c通过翻译器转换成可重定位的目标文件main2.o
4.当链接器ld运行时,它判定main2.o运用了addvec.o定义的向量加符号,所以,复制addvec.o到可执行文件prog2。因为涉及到打印,还会复制printf.o等模块。
5.因为不涉及到向量乘,所以链接器ld不会复制multvec.o到可执行文件。
有一个前辈把静态链接库比喻成一些粘合在一起的拼图块。当链接器发现应用程序需要静态链接库里面的拼图块的时候,就从拼图块中拿下一块自己想要的,组装完成应用程序功能拼图。这十分形象。
那么上图中“可重定位的目标文件”和“完全链接的可执行目标文件”有什么区别呢?
(图片来自《深入理解计算机系统》第7章)
我们可以看到还没链接的“可重定位的目标文件”还是比较散装的,地址从0开始,存在一些未定义符号的空洞;
而经过了链接器处理得到的”完全链接的可执行目标文件”,则拥有了真实的虚拟地址,同时sum等未定义符号的空洞也得到了填充。
二、动态链接库
1.出现背景
虽然静态链接库解决了代码复用性的问题,但是它也有明显的缺点。
首先,它需要定期的维护和更新。
因为静态链接库是在链接时将库中的目标文件复制到程序中,生成可执行的目标文件的,所以,如果库更新了,程序员需要了解库的更新情况,然后重新编译链接,生成新的目标程序。
其次,它浪费了存储空间。
静态链接库是将库中的目标文件复制到程序中生成的。对于基础函数,如打印等,每个程序都要用,因此它会被复制到很多程序中,这样在存储时浪费了磁盘空间,另外在运行时也对内存形成了浪费。
因此,在上世纪80年代出现了动态链接库技术。1984 年 SunOS 4.0 推出 ELF 与 ld.so 的完整实现,首次提出PIC(位置无关代码)、延迟绑定(lazy binding),1985 年 AT&T System V R3 把 ELF+动态库规格化,随后 BSD、Solaris、Linux 全线跟进。1987年windows也引入了dll。其后动态链接库技术一直占据主导地位。 2. 动态链接和动态库
动态链接库也叫共享库(shared library),共享目标(shared object),在linux系统上,是以lib开头,.so为扩展名的文件。比如:
bfd-plugins/liblto_plugin.so
gcc/x86_64-linux-gnu/13/libgcc_s.so
gcc/x86_64-linux-gnu/13/libstdc++.so
gcc/x86_64-linux-gnu/13/libtsan.so
gcc/x86_64-linux-gnu/13/libgomp.so
python3/dist-packages/netplan/_netplan_cffi.abi3.so
python3/dist-packages/_dbus_glib_bindings.cpython-312-x86_64-linux-gnu.so
python3/dist-packages/gi/_gi.cpython-312-x86_64-linux-gnu.so
x86_64-linux-gnu/libm.so
x86_64-linux-gnu/libthread_db.so
x86_64-linux-gnu/libmvec.so
动态链接库可以在运行或者加载时,被加载到任意内存地址,并和一个在内存中的程序链接起来。这个过程由动态链接器执行,被称为动态链接。
动态链接库的共享性体现在两个地方:
1.一个库只有一个so,所有引用该库的程序共享这个so中的代码和数据
这点和静态库被嵌入到到每个应用程序不同。因此它能节约磁盘空间。
2.在内存中,一个动态库的的.text节的一个副本可以被不同的正在运行的进程共享。因此它能节约物理内存。
使用动态链接库还能获得ABI(Application Binary Interface)的好处。在其升级时不需要像静态库一样显式的编译链接一下,因此它的函数库的版本升级会更容易。
Linux系统还允许运行时打桩机制,在程序执行时,可用一个库文件取代另一个库文件,以提高运行性能,或者是获得更多的调试信息。
动态链接是一种JIT(Just-In-Time)过程。经过ld在生成部分链接的可执行目标文件时,并没有把任何代码和数据节复制进目标文件,而是复制了一些重定位和符号表信息。
加载器加载和运行可执行文件时:
1.通过.interp节发现动态链接器的路径名
2.加载和运行动态链接器
动态链接器执行下面的重定位完成链接任务:
1.重定位各so的文本和数据到各个内存段
2.重定位可执行文件中所有对so定义的符号的引用
然后应用程序从动态链接器处接过控制权,开始执行程序。
在使用gcc编译生成so时,需要使用-shared -fpic选项;
linux还提供了dlopen、dlsym、dlclose函数,支持在程序运行时动态加载和使用共享库。使用它们,可支持插件系统、延迟加载、运行时才确定的库依赖、动态替换功能模块等场景。
三、总结
技术演进常由问题驱动。早期软件开发中,代码复用面临的严峻挑战催生了静态链接库技术;而动态链接库的出现,正是为了弥补静态链接在内存使用和更新维护方面的局限性。两者相继诞生,共同推动计算机技术的演进与软件工程效率的提升。
尽管动态链接已成为当前主流方案,静态链接在诸多特定场景中仍不可替代。例如在嵌入式系统、安全关键领域和封闭运行环境中,静态链接所提供的“无依赖、零外部调用”的独立运行能力,依然是系统稳定与安全的重要基石。
事实上,静态链接与动态链接并非相互替代,而是互补共存的技术路径。前者以独立性见长,保障程序的封装性与可靠性;后者以共享性为核心,提升资源利用与部署灵活性。它们共同构成了软件链接技术的双重支柱,在不同的应用场景中发挥着不可替代的作用,持续支撑着现代软件系统的高效与稳定演进。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。