大家在下载软件的时候是不是经常会遇到 dll
或者是 lib
文件,如果不是程序员的话,对这些文件恐怕无感,但是对于程序员来说, 这些文件才是一个软件的根本,是程序员自己真真实实敲出来的代码(只不过经过编译变成了二进制文件并且封装成库),为什么会这样说,下面你就知道了。
由于 windows
和 linux
的平台不同(主要是编译器、汇编器和连接器的不同),因此二者库的二进制是不兼容的),本文仅仅介绍Linux
系统下的库。
1. 什么是「库」
库就是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。
库有两种:静态库(.a
、.lib
)和动态库(.so
、.dll
)。 Windows
上对应的是.lib
和 .dll
, linux
上对应的是.a
和 .so
。
本文主要讲解 Linux
下的静态库和动态库的创建和使用。
2. gcc 编译出的 .o 文件
我们知道我们所说的程序编译其实包含着四个步骤,分别是:「预处理」、「编译」、「汇编」 和 「链接」。具体的每一步骤完成的任务就不多说了,百度总是一大堆,这里给出我在网上找到的一张图,感觉很有说服力:
这里我们只说两个 gcc
命令 :
gcc -c xxx.c -o xxx.o
:这个命令就是将我们编写的源文件xxx.c
(.c文件或者是.cpp
文件) 编译为目标文件xxx.o
(.o文件)。gcc xxx1.o xxx2.o -o xxx.out
:将上面生成的多个.o
文件链接生成一个可执行文件xxx.out
。不加-o
选项默认生成的可执行文件名为a.out
。
3. 准备测试代码
首先准备好测试代码:我们这里使用 VSCode
编写三个文件 :hello.h
、hello.c
和 main.c
文件。
文件的结构也是我们最熟悉的,文件结构如下:
|-- hello.c
|-- main.c
在 hello.h
这个头文件中我们只声明函数 printHel
而不定义,而是在 hello.c
源文件中进行定义, 而在 main.c
中调用该函数。
三个程序文件代码如下:
hello.h
头文件包含的是函数的声明:
hello.h
#ifndef HELLO_H_
#define HELLO_H_
void printHel();
#endif
hello.c
包含的是头文件的中声明函数的定义:
hello.c
#include "hello.h"
#include <stdio.h>
void printHel() {
printf("Hello!");
}
main.c
文件中调用了该函数:
/main.c./
#include <stdio.h>
#include "hello.h"
int main() {
printHel();
return 0;
}
至此,我们的测试代码就准备好了,虽然很简单,但是还是能说明问题的,下面继续。
处理思路:
我们有几种方法编译这几个文件生成最终的可执行文件:
1) 通过编译多个源文件,直接将目标代码合成一个.o文件。
2) 通过创建静态链接库 libhel.a
,使得main函数调用 printHel
函数时可调用静态链接库。
3)通过创建动态链接库 libhel.so
,使得main函数调用 printHel
函数时可调用静态链接库。
4. 编译多个源文件
-
编译
hello.c
:# gcc -c hello.c -o hello.o
会生成
hello.o
目标文件。 -
同理编译
main.c
:# gcc -c main.c -o main.o
会生成
main.o
目标文件。 -
将两个
.o
文件链接成为一个.o
文件并生成可执行文件:# gcc hello.o main.o -o execuate
会生成
execuate
这个文件。
执行可执行文件:# ./execuate Hello!
至此,我们的编译工作就算是完成了,但是如果工程很大的话,有多个头文件和源文件的话,这样编译很麻烦,而且产生的可执行文件很大,会造成空间浪费。最好的方法就是将 hello.c
编译为库,其中的函数就成为了库函数。这和 Math
库没有什么区别。
下面我们会仅此呢个实操,将源文件编译成静态库和动态库。
5. 编译为静态库
有了上面的基础之后,我们分别介绍一下什么是静态库,什么是动态库,下面先介绍静态库。
之所以称为 「静态库」,是因为在链接阶段,会将汇编生成的目标文件 .o
与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟 .o
文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o
文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。
静态库特点总结:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
5.1 库的命名
在 Linux
下静态库的名字一般是 libxxx.a
。其中 xxx
是该库的名字,后缀是 .a
。
创建静态库
5.2.1 首先编译 hello.c 产生目标文件
# gcc -c hello.c -o hello.o
然后查看一下果然多了一个 hello.o
文件:
# ls
hello.c hello.h hello.o main.c
5.2.2 使用 ar 打包工具将 hello.o 创建为静态库
ar -cr libhel.a hello.o
参数解析:
-c
:表示创建-r
: 表示replace
,如果存在则用新生成的进行替换。
再查看目录的话就会多出来一个名为libhel.a
的文件,这个就是生成的静态库。
# ls
hello.c hello.h hello.o libhel.a main.c
5.3 使用静态库
在使用静态库之前还是来熟悉一下几个 gcc
的选项参数:
-L
:-L
选项指定额外的函数库的搜索路径,如果不指定的话,则只会搜索gcc
的默认路径,比如有:/usr/lib
等,不会搜索到我们刚才创建的静态库。-l
:-l
选项是指在连接时候指定连接的函数库名字。比如这里就是函数库名 :-lhel
。
注意:-l 后面不需要完成的名字,而是去掉了前面的
lib
前缀以及.a
后缀名。
# gcc main.c -L. -lhel -o execuate
至此那个可执行文件:
./execuate
Hello!
6. 编译为动态库
动态库的创建和使用和上面的静态库使用差不多,只不过个别的命令的变化.
- 由目标文件
.o
生成动态库:
# gcc -shared -fPIC -o libmyhello.so hello.o
然后就会生成 libmyhello.so
动态库。在这李我们可以看出一个区别就是创建动态库不需要 ar
等打包工具,直接由 gcc
编译器就可创建。
2. 使用动态库
使用的命令和使用静态库是一样的:
# gcc main.c -L. -lmyhello -o hhh
上述命令生成可执行文件 hhh
。
- 执行生成的可执行文件
./hhh error while loading shared libraries: libmyhello.so: cannot open shared object file: No such file or directory
错误提示,找不到动态库文件libmyhello.so。程序在运行时,会查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。有多种方法可以解决。
最简单的就是将文件 libmyhello.so
复制到目录 /usr/lib
中,因为 /usr/lib
是 gcc
编译器查找库的默认路径之一:
$ sudo cp libmyhello.so /usr/lib
然后执行:
./hhh
Hello!
参考资料:
【1】https://www.zhihu.com/question/20484931
【2】https://www.cnblogs.com/yanzi-meng/p/9045064.html
2020/8/09 成都