[toc]
概述
静态库、动态库的本质区别。关键在于 编译和链接。
不搞清楚这俩东西,就只能知道静态库是运行前加载的,动态库是运行时或者运行后加载的,不知所以然。
带星号的标题最好看下
当然细节也不用太过深究,只要知道大概这些模块都干了啥就行了。
所有图都取自于 CMU CSAPP 的PPT。
https://www.bilibili.com/video/BV1iW411d7hd?p=13
编译、链接、运行过程
下面两个.c 是怎么最终成为可执行程序并被运行的?
- 第一步:
编译器分别编译两个.c,这里的编译器包括预处理器、C编译器、汇编器,最终生成 main.o sum.o,叫做可重定位目标文件 (relocatable object file)
.o 是怎么从 .c来的这里不扩展。详情看 CSAPP 7.1
第二步:
链接器被调用,把main.o 和 sum.o 以及一些必要的系统目标文件组合起来,创建一个可执行目标文件 prog.out第三步:
加载器被调用,把刚刚生成的可执行文件 prog.out 的 code 和 data 复制到内存,就可以运行了。
链接器作用 可重定位目标文件 xxx.o 是什么
简单理解:.o 继承了 .c 想做的所有功能,并且携带符号位置信息,可以帮助链接器最后把这些 .o 整合起来,做成一个可执行程序。
换句话说,静态库、动态库,其实就是这些.o 的集合体
把你的.c 编成 .o 后,它里面保存了所有的 全局变量的结构、text代码、data数据、bss数据、对外暴露的符号表(所有不加static的全局变量和函数) 、debug信息、重定位信息。
符号表的作用是:编译器告诉链接器,你在链接的时候,一定要记得把当前的这些代码和数据,给确定好位置,因为汇编器自己不知道,在程序运行时,这个符号会储存在内存的哪里。
重定位是个非常难懂的过程,我汇编已经全忘了,详情请自己看书。
链接符号 *
全局符号
对于当前的模块A,内部定义的,不是static标记的 全局变量 或者 函数名称外部符号
对于当前模块A,不属于内部定义的,其他模块定义的全局符号局部符号
对于当前模块A,被static标记的 全局变量 或者 函数名称。由于加了static,所以这些东西不会被暴露给其他文件。局部符号不是局部变量
因为局部变量是程序在运行时生成在栈里面的,链接器感知不到。而我们上面提到的所有东西,都是放在 data 和 bss 里面的。
并且要记住,static变量即使是在一个花括号的函数中,被定义的,它也会保存在 data 或者 bss里面,而不是栈里面。
同名全局变量引发的 “强弱符号” 巨坑
在我们的代码仓中,一般禁止出现同名全局变量,所以这个问题不会出现。但是有必要了解它的原理。
链接器如何解决同名符号?
强符号:
被赋值初值的全局变量或者函数,是强符号弱符号:
没有被赋值初值的全局变量或者函数,是弱符号符号解析规则
1、同名符号间,强符号唯一,否则报错
2、同时有强弱符号,选择强符号
3、只有多个弱符号,随机选择一个
链接器处理强弱符号时,可能发生的问题 *
-
两个不同模块都定义了 x 全局变量。一个赋值了,一个没有。那么没赋值的那个模块会使用另一个x,很难定位。
-
更恶心的是如果类型都不同,那么可能你只写了int,最后别人用double去取数值,越界。
静态库 static library
静态、动态库就是 .o文件的集合体
为什么要建立静态库
写了个hello world.c,运行的时候得用 C 标准函数吧,如果没有这个库,怎么调用人家的函数?
简单粗暴点,直接把 C 标准函数都给整到一个 亿行代码的.c,然后出 .o ,最后把你的 .o 和人家的一块链接起来,就能跑了。
好像也能跑,但是一个C标准函数编完了,最后大小是5MB,你的每一个进程,都得带着 C标准.o 编译吧,这样当进程多起来的时候,就造成了内存的极大浪费。
如果能做一个库,然后把 C标准的几百的 .c 都分别编成 几百个 .o 做成一个库。这个库里.o们,可以允许你自取所需,不要的直接扔掉,那效率就大大提升了。
静态库的 自取所需 是怎么实现的? *
链接器帮助实现,链接器在整合文件的时候。其实就是:
正常遍历你输入的所有 .o,CMakefile里面输入的那些 .c ,都会被按照顺序遍历
链接器会用一个 待办事项集合U,来保存所有的未被解析的符号。
用 目标文件集合E 来保存将会被整合为 可执行文件的 .o
用 已办完事项集合D 来保存所有的已经被解析的符号。
每当一个文件被遍历,首先判断,他是你想要编译的目标文件 .o,还是库文件 .a
如果是目标文件 .o,就把它放到E里面,然后更新 U 和 D。
如果是 库文件 .a,就尝试去匹配,看看这个文件里面有没有U里面缺的符号。
如果有,就把这些 待办事项 清理掉。同时把这个 .a中的 .o 扔到E里面。更新 U D
如果没有,说明这个 .o 是个废物,直接丢了就完事了。
如果遍历结束后,待办事项集合不为空,那么就报错
用下图来举例,如果我把 add.c 和 mult.c 做成了一个 libvector.a,但是 main.c 却没有调用 mult功能,那么链接器根本不会鸟 mult.o,会直接丢弃。
就像这样,mult.o 根本不会被取出来
自建一个库 由于链接顺序 产生的问题 *
链接顺序有问题,会导致链接错误。
举个最简单的例子:
如果你想链接 helloworld.o 和 printf.o ,如果链接器先遍历到 printf.o ,会直接把这玩意丢了,因为它的待办事项里面没有 printf
后面遍历到 helloworld.o 时,它会把 printf 加入到待办事项里面,但是printf.o已经没得了,待办事项 不为空,连接错误。
所以在CMakefile里面排顺序的时候要注意,否则就会出现明明都能找到定义,但是链接错误的问题。
动态库 shared library
为什么要用动态库? 静态、动态比较 *
静态库还是不够省内存
在使用静态库的时候,假设我们有上百个进程都用了 printf,那么 printf.o 就会被链接进这上百个进程,也是浪费。
而动态库能做到,让这些进程,全都共享使用一个 printf.o 。怎么做到的我不知道。但是就是能做到。静态库更改,就得重编整个可执行文件
静态库链接的时候,最基本的就是整合 .o 嘛,最后出一个可执行文件。
如果你静态库改了,那么就得从新整个走一遍,再把它链接起来。一定程度的解耦?
好多仓,之间有编译依赖顺序。其中 A仓 依赖 B仓,而当前 B仓给的对接件是 libB.a
问题来了:当你的B仓改变的时候(涉及到了A仓调用的东西),你想让A仓生效,那就要去先编译 B,再去带着 B 的东西去编译 A,就麻烦了一点。如果把 B改成出 .so,那就搞好 so 被加载的顺序就成了。静态库的优点是加载快
一般选用静态库,都是为了提高性能。
动态库可以在程序运行的 load-time 或者 run-time 去链接这个库
怎么做到的呢?
先用 .o集合 去做出一个 .so (使用 gcc 的 -shared),在第一次链接,出可执行程序这个阶段,需要把这个so里面符号表给导进去,但是不导内容。
这样子这个可执行程序就知道,将来我 起始或者运行 的时候,这个符号会被某个so给定义,我先不用管。
最终再动态的被调用去load 各个so。
不知道为啥就没办法截成一张图