动态链接与装载
复习
- 静态链接:拼成自包含的可执行文件
- 重定位:确定符号的最终地址
- 程序在内存里的样子:代码与数据
TL;DR
- 动态链接把库推迟到运行时才链接
- 可执行文件里只保留对共享库的引用
- 多个程序可以共享同一份库,节省内存
- 代价是运行时需要找到并加载库
正文
静态链接把所有库都“焊”进了可执行文件,代价是体积大、更新麻烦。动态链接换了个时间点:库不在生成可执行文件时链进去,而是等到程序运行时再链接。
推迟到运行时
动态链接的程序里,并不包含库函数的全部代码,只保留一个引用。真正把库“接上”的动作,发生在程序启动(或运行时):操作系统把需要的共享库(比如 .so、.dll 这样的文件)加载进内存,再把程序里的引用指向库中的函数。
换句话说:
- 静态链接:编译时就把库连好,运行时不操心
- 动态链接:编译时只记“我需要它”,运行时才真正接上
省下了什么
动态链接的好处,正好补上静态链接的短板:
- 省空间:库在磁盘上只有一份,程序不必各带一份
- 省内存:多个程序运行时,可以共享内存中同一份库代码
- 更新方便:库修了 bug,换掉库文件,用它的程序就能享受修复,不必重新链接
这些正是它被广泛采用的原因。
代价:运行时依赖
但动态链接也带来了新的麻烦,最典型的就是依赖问题:
- 程序运行时找不到需要的库,就起不来
- 库的版本对不上,可能出现不兼容
- 启动时要花时间加载并解析引用
这也是为什么我们偶尔会遇到“缺少某个动态库文件”“版本不匹配”之类的报错——它们都是“运行时才链接”这份灵活所附带的成本。
之后就是“装载”
动态链接把“链接”推迟到了运行时,而真正把可执行文件放进内存、让它开始运行的动作,叫装载(loading)。加载共享库、把程序映射进内存、跳到入口开始执行……这些属于操作系统的职责。
所以,编译器的故事到这里,已经不可避免地走到了操作系统的门口。下一部分会打开编译器的最后一层——语言运行时;而更完整的“程序如何被装入并运行”,早在操作系统部分已经讲过。 现在,先看看高级语言背后还需要哪些隐藏的服务。
思考题 1
动态链接相比静态链接,省下了什么?
思考题 2
动态链接为什么可能带来“依赖/版本”问题?
小结
知识点
- 动态链接把库的链接推迟到运行时
- 可执行文件只保留对共享库的引用
- 好处是省空间、共享内存、便于更新
- 代价是运行时依赖与版本兼容问题
参考资料
- Wikipedia(zh):动态链接库:运行时才链接的共享库
- Wikipedia(zh):载入器:把程序装入内存并准备执行
思考题答案(仅供参考)
思考题 1
省下了磁盘与内存中的重复:库文件只存一份、多个程序运行时共享同一份库代码,不必各自携带;另外库更新也更方便,替换库文件即可让使用它的程序受益,无需重新链接。
思考题 2
因为链接推迟到了运行时:程序启动时必须能找到所需的共享库,若找不到就无法运行;而且库的版本若与程序期望的不一致,还可能产生不兼容。这些运行时依赖正是动态链接灵活性的代价。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪