目录
hello,大家好,今天我们继续学习Linux中的动静态库,我们将从不同的角度来学习如何使用,并如何制作一个可供他人使用的动静态库文件,并试着探究一下动态库加载问题。那我们就开始学习吧!!
对于学习C/C++的同学来说:听到最多的就是标准库,其次就是库函数。但究竟什么是库呢?为什么我们只需要添加一下头文件,就可以使用库中包含的函数了呢?别着急,通过本文,我们都会讲清楚。
简单来说:库是一些可重定向的二进制文件,这些文件在链接时可以与其他的可重定向的二进制文件一起链接形成可执行程序。
一般来说库被分为静态库和动态库,他们是有不同的后缀来进行区分的。
另外对于C/C++来说其库的名称也是有规范要求的,例如在Linux下:一般要求是lib + 库的真实名称 +(版本号)+ .so /.a + (版本号),版本号是可以省略不写的。
例如这两个标准库 :
libstdc++.so.6 真实名称是 c++
libc-2.17.so 真实名称是 c
头文件与库的关系
那么,Linux下的库在什么位置呢?
如上便是我们使用库所要吧包含的所有的头文件。
接下来,我们回答几个问题:
问:1. 我们在使用像vs2019这样的编译器时,要下载并安装开发环境,这其中是在下载什么?
答:安装编译器软件,安装要开发的语言配套的库和头文件。
问:2. 我们在使用编译器,都会有代码补全,但是都需要先包含头文件,这时为什么呢?
答:代码补全是编辑器根据的将用户输入的内容,不断的在被包含的头文件中进行搜索匹配,所以代码补全,功能是依赖头文件而来的!
问:3. 我们在写代码的时候,我们的编辑器怎么知道我们的代码中有语法错误?
答:编译器很复杂,编译器有命令行的模式,还有其他自动化的模式,编辑器或集成开发环境可以在后台不断的调用编译器检查语法问题,从而达到语法检查的效果。
为什么会有库的存在呢?
我认为:库的存在,有效的降低了开发人员的开发成本,提高效率,使我们站在巨人的肩膀上,展望世界。如果没有库给我们提供一些完善的函数,我们想要完成某些功能就需要我们从零开始自己实现某些功能,但有了库的存在,免于我们做大量的无用工作。
- .c为后缀的文件:c语言源代码文件
- .a为后缀的文件:是由目标文件构成的库文件
- .cpp为后缀的文件:是c++源代码文件
- .h为后缀的文件:头文件
- .o为后缀的文件:是编译后的目标文件
- .s为后缀的文件:是汇编语言源代码文件
- .m为后缀的文件:Objective-C原始程序
- .so为后缀的文件:编译后的动态库文件
g++执行的四个过程
1、预处理:条件编译,头文件包含,宏替换的处理,生成.i文件。
2、编译:将预处理后的文件转换成汇编语言,生成.s文件
3、汇编:汇编变为目标代码(机器代码)生成.o的文件。
4、链接:连接目标代码,生成可执行程序。在链接之前,各个头文件都是独立进行编译的。各个头文件编译的过程互不干扰。
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共享,节省了内存和磁盘的空间。
首先,我们形成一个共识:
接着:我们试着实现一个库。
main.c
- #include "my_add.h"
- #include "my_sub.h"
- int main()
- {
- int a=10;
- int b=5;
- int res=my_sub(a,b);
- printf("sub:%d\n",res);
-
- res=my_add(a,b);
- printf("add:%d\n",res);
- }
my_add.c
- #include "my_add.h"
-
- int my_add(int a,int b)
- {
- printf("add.......\n");
- return a+b;
- }
my_add.h
- #include
-
- extern int my_add(int a,int b);
my_add.h
- #include
-
- extern int my_sub(int a,int b);
my_sub.c
- #include "my_sub.h"
- int my_sub(int a,int b)
- {
- printf("sub.......\n");
- return a-b;
- }
但这么多文件,如何进行编译呢?
方法1:
方法二
我们前面说过:直到形成".o"文件,所有文件都是独立进行编译的,互不影响。所以这种方法实际上和第一种方案是一样的。
我们知道库中不存在main函数,所以我们把main.c移出去。
这一次,我们把多余的文件都移出去。我们知道通过".o"文件链接,也可执行代码,所以我们要把main.c生成属于main.c的二进制目标文件。
到现在,有点像库的意思了。如果我们不想给别人源代码,我们给别人提供.o可重定位二进制文件,然后用代码进行链接就行。
未来,我们可以给别人提供.o(方法的实现).h(都有什么方法),别人就可以使用了。
但是,问题又出现了。如果未来链接过程中需要很多个“.o”和“.h”文件,由于“.o”文件都是二进制,如果漏掉其中一个,查找起来非常的麻烦。所以我们尝试将所有的".o"文件打一个包。给对方提供一个库文件即可。所以,库就出现了。 由于在打包工具和方式上的差异,就有了动态库和静态库。
一句话:库的本质就是“.o”文件的集合。
准备工作完成,接下来,正式开始:
为了方便一些,我们创建一个makefile
我们直接把这个归档形成的文件给使用者,使用者就可以使用了吗?不好意思,不可以。
我们为什么可以在Linux下敲C语言代码呢?这是因为系统中有C语言所需的头文件和库文件。
交付库:将形成的归档文件和匹配的头文件都传给别人。
当然,我们也可以将我们的库发布一下:
完整的makefile如下图所示:
- libmymath.a:my_add.o my_sub.o
- ar -rc $@ $^
- my_sub.o:my_sub.c
- gcc -c my_sub.c
- my_add.o:my_add.c
- gcc -c my_add.c
- .PHONY:clean
- clean:
- rm -f libmymath my_add.o my_sub.o
- .PHONY:output
- output:
- mkdir -p wer/include
- mkdir -p wer/lib
- cp -f *.a wer/lib
- cp -f *.h wer/include
经过整理,我们的路径中仅剩:
这时,我们就可以编译代码了
因为这个库是我们自己的库,没在操作系统指定路径下,所以我们需要指明头文件路径和库路径和库名称。其实我们在vs上运行代码也需要指明这些,只不过好多都是默认的,但并不意味着不需要指明。
- -l:链接动态库,只要库名即可(去掉lib以及版本号)·
- -L:链接库所在的路径
- -I(大写):指明头文件的路径。
运行完成。
我们生成的明明是静态库,这里怎么是动态链接呢?
首先:我们形成2个共识
- 我们知道gcc默认使用动态链接(建议选项),当动态库和静态库同时存在时,gcc首选的是动态链接。对于特定的一个库,使用动态链接还是动态链接,取决于提供的是动态库还是静态库。
- 形成一个可执行程序,可能不止依赖一个库,甚至几十个都是有可能的。
由于默认使用的是动态链接,这就意味着:当依赖的库中有一个库是动态链接,整体就是动态链接的。静态链接的库以静态的方式吧代码拷贝过来,但最终整体是动态链接。
在这份代码中,我们还使用了C语言标准库,由于C语言标准库是动态链接,所以我们整体上采用的就是动态链接。
动态库和静态库的制作过程是相似的,但也存在不同之处。
makefile内容如下:
- ibmymath.so:my_add.o my_sub.o
- gcc -shared -o libmymath.so my_add.o my_sub.o
- my_sub.o:my_sub.c
- gcc -fPIC -c my_sub.c
- my_add.o:my_add.c
- gcc -fPIC -c my_add.c
- .PHONY:clean
- clean:
- rm -f libmymath.so my_add.o my_sub.o
- .PHONY:output
- output:
- mkdir -p wer/include
- mkdir -p wer/lib
- cp -f *.so wer/lib
- cp -f *.h wer/include
gcc -fPIC -c my_sub.c:-fPIC的作用是产生与位置无关的码,即产生的不是绝对位置的码,而是具有相对关系的码。方便动态库加载。
接下来,我们就可以将这些无用的文件给移出去了
我们在不加任何选项的情况下进行编译,会报错。这是因为我们的动态库既没有在系统指明路径下,也没有在当前路径下(比代码所在路径深一层)。所以系统会找不到库的位置。所以需要我们在链接时指明:
[user@VM-8-5-centos wer]$ gcc -o mymath main.c -I ./wer/include/ -L ./wer/lib/ -l mymath
然后使用老方法运行这个可执行程序,报错:
这是因为我们刚刚是把库的信息传给了gcc来进行编译。但是运行确实操作系统完成的,况且动态库是在运行时被进行加载的。所以OS和shell也是需要知道库在哪里的。在操作系统中,OS寻找相应的库有指定的默认路径,但是我的库的位置在默认路径里吗?这种情况下,就需要我们指明。所以我们怎么告诉操作系统库在什么位置?有很多做法。
方案1
shell在执行命令时,除了在指定路径下进行搜索,也会在其他地方进行搜索。
在环境变量中进行搜索:
但是这种对环境变量的修改是一次性的,下次登录时,就会还原原来的数据。
方案二
对配置文件进行修改
该文件夹下保存着相关的搜索信息,我们可以创建任意名称的文件,然后将动态库路径写入文件中即可。
方案三
在与可执行程序同一路径下,建立软链接
静态库需要加载吗?不需要,静态库一般不考虑程序加载过程。
那我们使用静态库中的函数,在程序加载到内存时,这些函数的实现方法拷贝到哪里呢?
我们学过,代码在编译的时候内部就存在地址,这个地址是虚拟地址,所以这些实现方法会被拷贝进虚拟地址中的代码块,等待加载到内存中时,也会被拷贝到程序地址空间的代码块,这时,我们调用的静态库中的函数就和我们自己实现的函数完全一样了。所以这些实现方法会一直在代码区中。
采用动态库的程序在使用库中的方法时,会在使用的地方留下一个标记,在程序运行以后进行动态链接时,会将这个标记替换为动态库中的地址。
当一个使用了动态库的进程A运行起来以后在需要动态库a时,操作系统会先在内存中搜寻a,是否存在,如果存在,就直接将a通过页表进行映射进进程A的进程地址空间中的共享区中,如果不存在就会将磁盘中的动态库a加载进入内存,然后再通过页表进行映射。
我们知道被编译好的程序内部是有地址的!动态库内部的地址并不是绝对地址,而是偏移量!(相对地址)
因为不同的进程,运行程度不同,需要使用的第三库是不同的注定了,每一个进程的共享空间中空闲位置也是不确定的!如果采用了绝对编址,在一个进程使用了多个库时就有可能照成地址冲突!
当一个动态库,真正的被映射进地址空间的时候,它的起始地址才能真正确定! 此时动态库中的方法的地址就等于库的地址加上自己在库中的偏移量。通过这种设计方式,动态库在进程的地址空间中,可以随便加载,我们都能够找到库中的方法,也不会与其他库产生冲突了! 这就是与位置无关码。
一般来说可执行程序在生成时,会对多个库进行链接,我们可以使用ldd
命令查看我们的程序链接了那些库,可执行程序在连接时也可以选择部分采用动态库部分采用静态库。
写到这里,就结束了,如果您觉得写的可以的话,麻烦一键三连哦!!