【Linux系统】—— 动静态库
1「ldd 」指令
通过学习(【Linux系统】—— 编译器 gcc/g++ 的使用) 我们知道,我们写的代码编译器最终还要链接C标准库,但我们凭什么说文件链接过后将真的链接库了呢?
这里我们介绍一个指令:「ldd 」
- 「ldd 」:查看可执行程序依赖那些库
其他库我们不用管,这里我们重点介绍一下libc.so.6
这个库
我们平时写的代码中写了 printf 函数,但 printf 函数是我们自己写的吗?我们实现它了吗?很显然并没有,我们只是在自己的代码中调用了 printf,但是要形成可执行, printf 函数要有实现,这个实现在哪呢?实现在系统,在C标准库中,所以我们自己写的 C程序 是需要依赖 C标准库 的。
C标准库就叫做libc.so.6
在 ubunto 中,C标准库路径为/lib/x86_64-linux-gnu/libc.so.6
也就是说未来我们执行自己的代码时,执行到 printf 函数语句,它要跑到库中去执行,执行完后再返回。
下面我们再深入了解一下库的概念
2 库的理论知识
2.1 动态库与静态库
库本质是一套方法或者数据集,为我们开发提供最基本的保证(基本接口,功能,加速我们二次开发)。
比如我现在要打印 “hello world” 我现在有库,可以 5 秒钟写完,可如果没有库的话,我光写 printf 就要花两小时,还要求懂一些计算机的原理。我们直接用 printf 就相当于二次开发
在任何平台下,库分为两类:动态库与静态库
- 动态库,在Linux下对应的文件格式一般都是
.so
为后缀;在 Windows 中一般是以.dll
为后缀- 静态库,在Linux下对应的文件格式一般都是
.a
为后缀;在 Windows 中一般是以.lib
为后缀
2.2 动态链接与静态链接
Linux 中 C语言 的动态库叫:libc.so
(后面可能还会带 .2.17、.6等,这是库的版本)
这里介绍一下 Linux 中库的命名规则:库的真实名字是要去掉前缀
和去掉 . 后面的后缀
如:libc.so 去掉前缀 lib 再去掉后缀 .so.6,剩下的 c 就是该库的真实名字。
又比如:我将来写了一个库:xyz,这个库将来命名时,必须为 libxyz.so / libxyz.a
库的内部会实现方法,我们自己写的 C/C++ 程序会使用库中的方法。将自己的程序与库连接起来,会形成一个可执行程序。链接中最重要的一步就是让我们自己写的程序能在库中找到对应的方法。
动态库的特点是:执行目标方法时(比如执行 printf 函数),需要跳转到库中执行,执行完了再返回自己的程序继续向后运行。这种链接方式我们称之为动态链接。
所谓动态链接就是未来程序执行时,让我的程序能找到库中方法的地址
。比如我们的程序用了 printf 函数,该函数会被替换成一个函数地址,程序运行到 printf 函数时,就会跳转至该地址处执行 printf 的方法。所以动态链接只会在地址上产生关联。
Linux 中 C语言 的静态库叫:libc.a
同样,libc.a
去掉前缀和后缀后,C语言静态库的真实名字是:c
我们知道了什么是动态链接,那静态链接是什么呢?
静态链接非常简单粗暴,就是直接将库中的方法拷贝到自己的程序中,这样执行该方法就不再需要地址,直接在自己的程序中执行即可。
2.3 形象例子理解动静态链接
比如小明是一位准高中生,小明比较喜欢打游戏,可你考上的高中是不允许学生带电子设备的。于是小明找到学长,问学长学校附近有没有可以上网的地方,学长告诉小明学校附近有一个小黑屋网咖,并告诉他地址:学校东门左拐 300 米,小明心里就默默记住了。
学校开学了,小明心里默默计划着早上上完课吃完饭就去那个小黑屋网咖打游戏,下午回来再继续上课。12:30 时,小明有上网的需求,可是学校并没有提供上网的资源。这时,小明想起来了学长提供的小黑屋网咖的地址,便出东门左拐 300 米,叫网咖老板开了 100 号机器,用网咖提供的100号机器开始上网了。14:30 时,要上课了,小明便回到了学校继续上课。
这个例子中,小明就相当于程序;学校相当于内存;在小明还没开学时,问学长取那里上网,学长给了我一个地址,这个过程叫做动态链接。动态链接的过程和运行时没有关系的,他是属于编译期间做的
,所以动态链接只是把我需要访问的资源的目标地址写到我的程序里
。而小黑屋网咖我们称之为动态库
当小明去上学,这个过程称之为加载过程。计划表上什么时候上语文、什么时候上数学课……,这个过程叫要执行的代码。当程序执行到 12:30 时,要跳转至小黑屋网咖上网,这个过程我们称之为库调用。当小明上完网返回学校的时候,这个过程叫做库函数返回。
所以动态链接是什么时候链的,是程序编译的时候,还没有加载的时候就已经链接好了。动态链接只是把我们要访问的库资源的地址写到我们的程序当中
,当我加载之后,程序就能动态找到这个库,程序就能运行了。
因为学校附近就这么一个网咖,因此学校所有有上网需求的同学都会跳转至该网咖执行上网需求。所以我们把小黑屋网咖这个动态库又称为共享库。
后来,学校发现许多同学上课不见人而是去小黑屋网咖上网,学校就向有关部门举报小黑屋网咖,说它是非法经营。警察蜀黍就过来了,小黑屋网咖就被取缔了。一旦这个小黑屋网咖被取缔了,那么所有有上网需求的同学的程序都无法正确执行了(我没办法上网了,每心情上课了)。所以共享库又一个特点:被多人共享,一旦缺失会导致所以对应的程序都无法执行
别急,故事还没完
因为小黑屋网咖没了,没法上网,小明的学习欲望也就降低,期末考试也由年级第一降到了年级第三。小明老爸问原因,小明说是没法上网,无法再往上查找资料导致的。
于是放寒假时,小明老爸就去找学校领导开路灯,毕竟年级第一嘛,还找到了小黑屋网咖的老板,问了 100 号机器的相关配置,自己组装配了一台与 100 号机器一模一样的。开学后,小明就再也不用去网咖了,直接抱着组装好的电脑在自己宿舍上网。即小明这个程序执行代码时,再也不用网咖这个共享库了,网咖的有无对小明没有任何影响。
后来有小明这个破例,学校被迫接受同学可以上网,其他同学再暑假时也去找网吧老板问配置,自己组装了电脑,新学期开学,再也没有人需要再去网咖了,每个人都可以在宿舍上网。
我们将上述链接方式称为静态链接
静态链接最直接的表现就是将我们程序中使用的库方法拷贝给我自己。静态库只有在链接的时候有用
,一旦形成可执行静态库可以不再需要
动态库相当于小黑屋网咖,而静态库有点像电脑仓库,我们需要时,直接去仓库把电脑拷贝过来就行了。
2.4 动静态库的对比
- 动态库形成的可执行程序体积一定比静态库形成的小
- 为什么呢?这很好理解,还是上面的例子,动态库小明只需要人去就行;而静态库小明还要搬个电脑过去
- 可执行程序对静态库的依赖比较小,而动态库不能缺失
- 程序运行需要加载到内存,静态链接会在内存中出现大量的重复代码。 比如我调用了 100 次 printf 函数,那么静态链接后,可执行程序就会出现 100 份 printf 的实现方法。重复的代码本质上就是对内存资源的一种浪费
- 动态链接,比较节省内存和磁盘资源
3 实践验证
3.1 默认动态链接
我们创建 test.c 文件,写下代码:
用 gcc 编译后成可执行程序后,我们再用「ldd 」
指令看一下
可以看到,当前可执行程序依赖的库是libc.so.6
,是动态库。所以 gcc 在使用时,默认是动态链接的
3.2 「file」指令
「file」指令可以用来查看文件的具体信息
3.3 强制静态链接
如果想要形成可执行是静态链接,那么用 gcc 编译时要加上 「-static」 命令行选项
但静态链接的前提:系统中有C语言的静态库。
许多系统默认是不装静态库的。这也说明了系统更愿意你去编动态链接的程序
安装指令如下:
- centos:sudo yum install -y glibc-static
- ubunto:sudo apt install -y glibc-static
我们用「ldd」和「file」看下新形成的可执行程序
再来对比一下动态链接和静态链接形成的可执行程序大小:
体积扩大了50倍,而且我们只用了一个 printf 而已。
4 进一步理解动静态库
C语言的动静态库,包括我们自己的可执行程序,本质上全都是文件。
对于静态链接的可执行,静态链接完成后,我们将对应的方法拷贝到自己的程序中,这个静态库我们就不需要了,我们的程序就直接加载到了内存。静态链接的可执行程序所占用的内存会很大,相信大家没有问题。
我们重点来看看动态库
当加载我们的程序,我们的程序会被加载到内存中,当这还不够,其实动态库也要加载到内存中。
所以将来执行我们的代码,跳转的时候是在内存中跳转,再返回继续执行
可能有小伙伴会觉得动态链接也不过如此,也是要占用内存。
其实不是这样的,当有多个可执行都用到了该动态库,只要首次这个动态库加载了,未来要执行其他可执行时,就只需要加载可执行文件即可,无需在加载库。当这些可执行想要使用库中的方法时,就可以直接跳转到同一个库中进行访问了。
所以动态库的本质是:把所有程序中公共的代码在内存中保证只出现一份。
如果是静态程序,有多个可执行都使用了 printf 函数,那么每一份可执行中都有 printf 函数的实现方法。出现了多份重复的代码,不就是对内存资源的一种浪费吗?因为代码时只读的,为什么还要在内存中出现多份呢?
5 技术上理解库
首先我们创建一个文件 main.c 和一个目录 lib 来实验
lib 目录中创建 4 个文件:func1.h、 func1.c 、func2.h 、func2.c
文件内容如下
现在 main.c 文件要编成可执行,有一种方法就是将 lib 中方法的实现放在 mian.c 的同一个目录下,然后一起编译成可执行
这里,所有的源文件放在一起形成了一个可执行程序 code,能编译通过的原因是头文件在当前路径下能直接找到。自此就实现了多文件的编译。
可是写出 func1.c 和 func2.c 的人不想将自己的源文件暴露出来,只想提供头文件怎么办呢?
在C语言中,我们往往喜欢将多个 .c 源文件编译成对应的 .o 文件,再将所有的 .o 文件再 链接 起来
有了这点,我们可以将 func1.c 和 func2.c 只编成 .o
文件
注:「-c」 将 .c 文件编译成同名 .o
我再只给你提供.h头文件
和 .o文件
,不再给你源文件,反正 .o
文件是二进制文件你也看不懂。
而 main.c 想要编成可执行,要先将 main.c 编成 main.o
再将三个 .o
文件给链接
起来
这样我给你提供方法的同时,还能将我的具体实现给隐藏起来。
现在我们的源文件只有两个,如果将来有 200 个,2000 个呢?那这样别人要用是不是要链接 2000 个.o
文件呢?万一丢了一两个怎么办?
所以库的本质就是一个被打包起来的 .o,只不过库是将多个.o打包形成一个 .so 或 .a。
打包有打包的命令,这点我们后面在学习
好啦,本期关于动静态库的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在 Linux 的学习路上一起进步!