一、前言
为什么要使用别人的代码?
主要是为了提高程序开发的效率和程序的健壮性。
当别人把功能都实现了,然后我们再基于别人的代码去做二次开发,那么效率当然就提高了。其次,这里基于的别人当然不是随便找的一个人,而特指的是顶尖的工程师,也就是说如果我们的代码出了问题,一般不会去怀疑是别人的库的问题,这也就增加了代码的健壮性。
换而言之,如果我写我的代码,别人写别人的代码,那么在机制上我们的工作就得以分解,假如我是一个写调用、逻辑特别严谨的程序员,假设别人是写库写得很好的一个人,我们两个都有比较完善的工作方式,那么两个人合起来就可以提高代码的效率和健壮性,而不需要两方面都兼顾。
程序的健壮性,更直观的理解是我们在百度搜索时输入乱码,而百度却不会崩溃。
- 动态库:libc.so,libc++.so
- 静态库:libc.a,libc++.a
一般在命名上去掉前缀 lib,去掉 . 和之后的内容,剩下的就是库名,所以这里就是 C 库和 C++ 库。
C/C++ 体系中如何使用别人的功能?
生成可执行的方式有静态链接和动态链接,对应的是静态库和动态库。
比如,张三是一名大一新生,他在写作业时突然有了一个上网的需求,但因为周边环境不熟悉,所以找了学长询问附近网吧的地点,随后就跑去玩了几个小时,然后回来后接着继续写作业,这就叫作动态链接。两年后张三已经是一名大三师兄了,要开始准备找工作了,家里给他买了一台电脑,当他正在学习时想要上网就不用再特意跑去网吧了,而只需要打开自己的电脑就行,也就是说静态链接并没有和外界产生关联。
一般我们写的程序中大部分都是动态链接,因为动态链接中不需要把库中的内容进行过多的拷贝,所以相对而言,它的编译效率较高,这是其一;我们有时候下载一些软件,比如 VS2019,它上面会有一些组件,比如 C++、C# 等,但这些组件并没有在我们的硬盘中,而当我们要安装时,它会帮我们找到这些组件下载,这样有个好处就是我们需要什么就直接下载什么,而不是直接一堆东西直接装在机器上,而自己需要使用的组件却寥寥无几,所以这里就采用动态库的方式实现,这是其二。
当然静态链接也有属于自己的使用场景,一般在服务器上大部分也都是动态链接,不过有时候需要将服务在很多机器上部署,那么单纯的动态链接就可能会出问题,因为动态链接是在程序运行之后才去对应加载到内存中,万一有个库丢失了,那么程序就挂了。所以有时候一些程序它也会采用静态链接,好处就是它不依赖于任何动态库,坏处就是效率比较低。比如静态链接的大小是动态链接的一百倍,要把静态链接这个程序下载下来就只能全部下下来,而动态链接则是边下边用。总的来说,两者各有利弊,没有绝对的好坏之分。
- 动态链接的生成的可执行程序的体积往往比较小,节省资源(磁盘、内存),但是它依赖第三方库,有一定的风险,一旦库丢失,可执行程序不可执行(网吧被查封了)。
- 静态链接虽然生成的可执行程序的体积较大,浪费资源(磁盘、内存),但是它不依赖第三方库,一旦库丢失,可执行程序依旧可以执行(网吧被查封了照样可以玩)。
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
二、静态库的制作打包与使用(不使用 makefile)
1、准备工作
别人要使用这些接口最直观的就是直接给源文件,别人就可以直接在当前目录创建文件,然后直接包含它们就行。但是如果不想让别人看到我们的源文件是如何实现的,而只是告诉别人它们的作用,那么就需要使用库。其实动静态库的属性就是不想暴露自已的源代码,所以可以直接打包给别人。
我们让别人来使用我们的库,前提是别人需要知道我们的库能给他们提供什么方法,这是通过头文件来体现的(头文件可以暴露)。但光有头文件还不行,里面只有声明,还需要有实现,所以就将方法所在的源文件进行编译然后打包到库。
2、生成静态库
(1)gcc -c mymath.c -o mymath.o / gcc -c myprint.c -o myprint.o
先将源文件汇编后生成 .o 文件,它虽然也是二进制文件,但是它还不可以执行,因为还差最后一步链接。所以库就是在编译过程中,在链接的前一步停下来,编译成可被链接的目标文件。
如果只把 .h 和 .o 给别人,别人可以用吗?
可以。
(2)ar -rc libhello.a mymath.o myprint.o
ar (Archive) 是 gnu 归档工具, rc 表示 replace and create,意思就是如果要生成的库中已经包含了对应的 .o 文件,就 replace,否则就 create 。
此时 libhello.a 就是静态库,所以我们将来只需要把 mymath.h,myprint.h,libhello.a 打包交付给其它人即可,而 libhello.a 就由 mymath.h,myprint.h 来说明。这也就是为什么大部分库在提供的时候,一般是提供库文件 + 头文件,也就是为什么在使用 C 语言的时候永远都是 #include <stdio.h>,然后在写 printf 的时候直接调用,最后链接库,本质上就是因为系统在装的时候就把库文件和头文件给装了,而 C 语言的源代码就不需要在系统中了。就相当于把源文件先编译一大部分,不要链接,再把所以编译好的 .o 文件打个包让别人去用,这就是静态库。
提示:libmymath.a 前缀必须是 lib。
(3)查看静态库中的目录列表
- t:列出静态库中的文件
- v:verbose 详细信息
3、使用静态库
李四想用张三写好的 mymath,myprint 代码,但是张三不想让李四看到自己是怎么实现的,所以就将它们打包好变成库 hello,复制给李四,李四人拿到后就可以通过头文件知道了这个库是干嘛的:
需要提前将库拷贝到系统的默认路径下,这个过程叫作库的安装。
所以李四就在 main.c 中使用张三写好的库,随后 gcc main.c -o main,报错说 myprint.h 是没有这个文件或目录的,原因是 myprint.h 既没有在当前路径下,也没有在默认路径下,这里的当前路径是指要和 main.c 在同一级路径下。所以这里 gcc 还要再加一个选项 -I,后面跟上 ./hello,意思就是 gcc 在编译程序,找头文件的话,除了在当前路径下,系统路径下,你也要在 ./dir 下去找。
此时还有报错,但对比上一次报错,显然头文件已经找到了。此时报的是在链接时找不到 addToTarget 方法,所以还 gcc 还要在加上一个选项 -l,后面跟上 ./hello,表明库文件在这个目录下。只不过这里是巧合,头文件和库文件都在 ./hello下。
然后又报错了,原因是指明了库路径,但并没有指明要访问的是这个路径下的哪一个库。如果这个路径下有多个库就需要指明了。所以还要再加一个选项 -l,后面跟上 libhello.a 去掉前后缀,也就是 hello。此时就完成了链接,形成了可执行程序:
(1)-I + 路径
告诉 gcc 除了默认路径以及当前路径,在指定路径下也找一下头文件。而一般 linux 下头文件的默认路径在 /usr/include 下,其中我们就看到了最熟悉的 C 文件 stdio.h。
(2)-L + 路径
告诉 gcc 除了默认路径以及当前路径之外,在指定的路径下也找一下库文件。而一般 linux 下库文件的默认路径在 /lib64 或 /usr/lib64 下, 其中你往下翻,也可以找到比较熟悉的 C 语言的静态库和动态库,libc.a 和 libc.so。
这里需要说一下,实际系统在搜索头和库的时候,除了这些默认路径,还有其它的路径。另外不同 linux 发行版,甚至同一发行版在路径上都可能不太一样,所以具体问题具体对待。
(3)-l + 库名
需要进一步具体说明要链接哪一个库。
4、库搜索路径
- 从左到右搜索 -L 指定的目录。
- 由环境变量指定的目录(LIBRARY_PATH)
- 由系统指定的目录
- /usr/lib
- /usr/local/lib
为什么 C/C++ 在编译的时候,从来没有明显的使用过 -I/L/l 等选项呢?
库文件和头文件在默认路径下 gcc 可以找到。
gcc 编译 C 代码, 默认就应该链接 libc 库。
如果我们自己也不想使用这些选项呢?
头文件和库文件分别拷贝到系统的默认路径下,这个过程就叫做库的安装。我们之前学过环境变量 PATH,然后写了个 Hello World,让它不使用路径就可以执行,其实是把可执行程序拷贝到环境变量的路径下,其实就是安装可执行程序。
不过我们自己写的库叫做第三方库,也就是除了系统,语言之外的库。所以一般也要带上 -l name 表明你要链接的是哪一个库。
这里为什么不需要指定静态链接的方式?
因为当前库中只有静态库,所以这里就算不写 static,也只能用静态的。也就是说,有动态库和静态库时,gcc 编译默认是动态库,但只有静态库的时候就只能是静态库。
三、动态库的制作打包与使用(使用 makefile)
1、准备工作
这里打包动态库依旧不想把源文件暴露出去。
2、生成动态库
- shared:生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
在栈区和堆区中间有一个共享区,一般动态库的代码是映射在这个区域,库文件当然也是一个文件,当然也占磁盘空间。当程序运行,其中需要执行库中的代码时,本质就是进程来执行库中的代码。换而言之,进程一旦运行起来,同时也要把库加载到内存,当然也可以局部加载,然后就把库映射到共享区,此时代码区的代码就可以直接访问共享区中的库。这样做的一个好处就是,如果有多个进程时,可以统一把要使用的一个库从内存映射到自己的共享区,这样就相当于可执行程序是不需要携带库代码的,从而可以有效的节省资源。
如果是静态链接,形成进程后就没有用共享区,此时代码区中就包含了你的代码和库的代码,如果有 10 个 C 代码,每个程序都把库代码都拷贝一份到代码区,此时在内存中就会有 10 份重复的库代码,而实际只需要 1 份即可。所以动态库最典型的特点就是,所有和我们使用同一种库的进程可以把库从内存映射到共享区,以节省内存资源。
其次,有可能 A 进程共享区中只是一部分区域映射到 C 库,B 进程共享区中也只是一部分区域映射到 C 库,但不管最终物理到虚拟地址是如何映射的,可执行程序加载到物理内存的任何位置,最后都一定要保证库中产生的各种代码与我们这个库加载到内存中的位置和映射到共享区的位置是没有关系的,这就叫做产生与位置无关码,如果深入进一步了解就需要学习编译原理中可执行程序的格式。这里可以简单化的理解为,因为库是随时随地可能加载的,它也可能在内存中的任何位置,也可能被映射到共享区的任何区域,所以必须保证库中的代码不会出错。比如有一万行执行代码,因为它本身与位置无关,代码中出现了一个函数调用,它在编译时地址是 0x1234,加载到内存,映射后函数的地址相对于调用方的地址发生了变化,代码就可能执行不起来了,所以必须保证调用目标函数的地址它本身是不会随着程序的加载位置以及映射区域的位置变化而变化的,这就是产生位置无关码。
比如有一条跑道,李四距离终点 80m,张三距离终点 100m,而当终点往前移动 20m 后,此时李四和张三原来距离终点的距离就不对了,但无论终点怎么变,张三距离李四有 20m 是不变的,这就是与位置无关码。
最后再说一下,这个动态库是不会随着本身加载到共享区的任意位置而影响到库中代码地址发生变化,而导致库中的代码不可执行的,所以 gcc 一定要用 fPIC 选项来产生与位置无关码。
3、使用动态库
张三打包好库 output,李四拿到了库:
然后李四写好代码就开始编译,这里如同静态库一样需要使用 -I,-L,-I 选项,然后 ./main 时报错说不能打开共享文件,原因是没有这个文件或目录,为什么呢?刚才不是很明显的把头文件在哪里,库文件在哪里,库文件的名字是什么告诉了 gcc,怎么现在又不认识了?
此时 ldd 确实也是有个库找不到,刚才的选项是给编译器看的,而此时 ./main 时已经和 gcc 没有关系了,所以这里是运行时的问题,所以在运行的时候也要能让系统帮我们找到运行时需要使用的动态库。
那为什么之前动态链接的其它程序可以直接运行呢?因为运行时别人的库可以被找到,它们在默认路径下。这里有一些方案,这里只推荐这一种:这里有一个环境变量 LD_LIBRARY_PATH,在祼机器上是没有这个环境变量,但如果你曾经做过 vim 配置,可能就会有。如果想让你的库被运行起来,需要把库路径导入其中。这里推荐导入绝对路径,导入成功再 ldd,发现这个库已经能找到了。此时就可以 ./main。
如果动静态库同时存在,默认采用的是动态库。
如果动静态库同时存在,但非要使用静态库呢?
-static:摒弃默认优先使用动态库的原则,而是直接使用静态库的方案。
当目录下同时存在动静态库,那么 gcc 在编译时默认就是动态链接,而使用 static 后就是静态链接。
4、运行动态库
- 拷贝 .so 文件到系统共享库路径下,一般指 /usr/lib
- 更改 LD_LIBRARY_PATH
- ldconfig 配置 /etc/ld.so.conf.d/,ldconfig 更新
在动态库操作的时候,需要把动态库所在的路径导入到环境变量 LD_LIBRARY_PATH,但是重新登录时,自己导入的环境变量的路径就没了。(不推荐把变量导入到登录脚本)
若想让它每次在重新登录上的时候不会被重新配置,可以把库的路径导入到 /etc/ld.so.conf.d/ 目录下。这个配置文件是永久生效的,导入成功后需要 ldconfig 刷新,不过也不推荐这种方案,原因是你写的库导入到这个目录下可能也会污染系统本身的环境变量信息。除非将来你需要导入第三方开源的库。
5、使用外部库
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses 库)。
为什么要有库?
- 站在使用库的角度,库的存在可以大大减少我们开发的周期,提高软件本身的质量。
- 站在写库的人的角度,库既简单又安全。