1、动态库和静态库概念
Linux中的库分为动态库和静态库。
静态库(.a):库文件以.a为后缀,程序在编译链接时把库的代码链接到可执行文件中(将需要的库函数拷贝一份到代码中)。程序运行时不需要再跳转到静态库。
动态库(.so):库文件以.so为后缀,程序在运行时才去链接动态库的代码(运行时跳转到动态库中,在动态库中执行库函数)。多个程序共享库的代码。
链接的本质:我们调用库函数时是如何与标准库联系的。
库的名称:去掉前缀lib和后缀’.a/.so’剩下的就是库名称,例如:libc.so就是C库。
gcc/g++,在编译时默认使用动态链接,如果想要生存静态链接,我们要带上-static。
2、库
我们了解了动态库和静态库的相关概念,但是我们还是不理解库是个什么东西。
假设,我们做了一个小程序,只希望提供给用户小程序的功能,不希望暴露我们的源码。我们可以选择给用户提供我们的.o可重定位目标二进制文件(gcc -c 文件)与头文件。让用户使用我们提供的.o文件和.h文件进行链接即可。(在编译时,只需要把源文件编译成.o文件,再将其链接即可形成一个可执行程序,因此我们可以直接提供.o文件)。
文件add.c
1 #include"add.h"
2 int add(int x, int y)
3 {
4 printf("ADD: %d + %d = ?\n",x, y);
5 return x + y;
6 }
文件mul.c
1 #include"sub.h"
2 int sub(int x, int y)
3 {
4 printf("SUB : %d - %d = ?\n",x, y);
5 return x - y;
6 }
文件add.h
1 #pragma once
2 #include<stdio.h>
3 extern int add(int, int);
文件sub.h
1 #pragma once
2 #include<stdio.h>
3 extern int sub(int, int);
文件main.c
1 #include"add.h"
2 #include"sub.c"
3 int main()
4 {
5 int a = add(1, 2);
6 int ret = sub(10, a);
7 return 0;
8 }
运行
我们给用户同时提供.o文件(方法的实现)以及.h文件(方法的声明),用户就可以链接形成可执行程序。
但是如果我们有很多.c文件,难道我们要将所有的.c文件全部编译成.o文件,然后一个一个提供给用户吗?未免太过麻烦。我们可以把编译得到的所有.o文件打包,直接给对方提供一个库文件即可。把多个.o文件打包成一个文件,这个文件就是库。
库的本质就是.o文件的集合。
3、制作静态库
首先,如果写一个库是否需要写main函数?
答案是不需要,因为库是提供给别人使用的,用户自己写的main函数会与库函数起冲突。我们需要在编写库的角度和使用库的角度同时考虑来制作库:
编写库:
3.1 创建Makefile:
1 libmymath.s:add.o sub.o
2 ar -rc $@ $^
3 add.o:add.c
4 gcc -c add.c -o add.o
5 sub.o:sub.c
6 gcc -c sub.c -o sub.o
7 .PHONY:output
8 output:
9 mkdir -p mylib/include
10 mkdir -p mylib/lib
11 cp -f *.a mylib/lib
12 cp -f *.h mylib/include
13 .PHONY:clean
14 clean:
15 rm -f *.o libmymath.a
3.2 打包库
将文件编译为.o文件
or命令:把所有.o文件打包起来,or作用是归档,-rc(replace和create):例如
or -rc libmymath.a add.o sub.o
output:发布。交付库,将库文件.a以及配套的头文件都交给用户。
将mylib打包起来。
此时,用户如果需要我们的库,只需要将mylib.tgz拷贝过去:
cp mylib.tgz .../test
然后解压
tar xzf mylib.tgz
安装本质就是拷贝。
3.3 使用库
文件main.c
1 #include"add.h"
2 #include"sub.h"
3 int main()
4 {
5 printf("1 + 2 = %d",add(1, 2));
6 printf("10 - (1 + 2) = %d",sub(10,3));
7 return 0;
8 }
为什么会找不到头文件?
编译器搜索头文件,默认是在当前目录下搜索,在系统默认指定路径下搜索。虽然此时的mylib在当前路径下,但是头文件太深了(文件不在本层),编译器找不到头文件,因此我们需要给gcc指定路径(-I)。指明在当前路径下mylib目录中查找。
gcc -o mymath main.c -I ./mylib/include
此时出现了新问题——找不到库函数的实现。
我们在形成可执行程序时,库文件要使用,必须知道库所在的路径,而系统中库默认路径为/lib64。因此,我们要告诉gcc,它要链接的库的路径在哪里(-L)。
如果要链接第三方的库,必须去指明库的名称(注意:指明时要去掉前缀和后缀!!!),也就是说,一定要告知是哪一个路径下的哪一个库,即使该路径下只有一个库也要明确告知gcc是哪一个库(我们以前写代码的时候,从未指明库的名称,是因为gcc/g++默认帮我们填写了,因为它们可以识别C/C++自带的库。但是自己写的库或者第三方库必须要写明)。
gcc -o mymath main.c -I ./mylib/include -L ./mylib/lib -l mymath
结果正确!!!
总结
-I 指明头文件的路径
-L 指定库文件目录,可以指定多个文件目录。库目录没有在/lib、/usr/lib、/usr/local/lib中,则必须用-L来指定一个库目录
-l 指定具体的库文件。如果没有指定,则默认去/lib、/usr/lib、/usr/local/lib去找。默认寻找的是动态库,可以指定-static,寻找静态库。
注意
gcc默认是动态链接,对于一个特定的库,究竟是动态链接还是静态链接取决于提供的是动态库还是静态库。
库的安装
将库安装到系统头文件下。
gcc对头文件的默认路径为/usr.include,对于库文件的默认路径是/lib64
sudo cp 头文件(包含路径) /usr/include/
sudo cp 库文件(包含路径) /lib64/
但是,不太推荐将第三方库写入系统默认路径,因为第三方库未经过测试会污染系统内其它文件。
4、制作动态库
首先我们将文件全部编译成.o文件,这里与制作静态库不同的是,需带上-fPIC,形成位置无关码:
gcc -c -fPIC add.c
动态库打包:
gcc -shared -o libmymath.so add.o sub.o
我们试着运行mymath:
为啥运行不了呢(为啥找不到库)?
我们的确已经告诉了gcc:我们的库文件的路径以及库名称,但是我们编译完成后,程序与gcc还有关系吗?(程序是由gcc运行的吗?)显然此时程序与gcc无关。接下来的程序运行是由OS来进行的。
动态库是程序运行时才进行链接的,而程序的运行是OS和shell来执行的,因此OS和shell也需要知道库文件的路径及名称。但是我们自己制作的库并不在系统的默认路径下,因此OS无法找到库,就无法正常执行程序。那么我们要如何让OS找到我们的库呢?
我们可以将库路径添加到环境变量LD_LIBRARY_PATH中。例如:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/Jinger/dir1/mylib/lib/libmymath.so
直接运行:
注意:我们自己定义的环境变量只是本次登录有效,如果想永久有效只能修改环境变量的配置。当然,我们还有其它办法:
配置文件(/etc/ld.so.conf.d/):动态库进行搜索时可以通过自己定义conf文件找到动态库。
建立软链接,直接找到对应的库。
把对应的动态库建立在系统的目录下。
总结
拷贝.so文件到系统共享库(动态库)路径下,默认路径是/usr/lib
更改LD_LRBRARY_PATH
ldconfig配置/etc/ld.so.conf.d/,ldconfig更新
创建软链接
5、动静态库的加载
静态库不需要加载,静态库是将代码直接拷贝到程序中,因此内存中的代码和数据可能会存在多分,造成空间浪费。把静态库代码拷贝到内存中的代码区:
动态库通过fPIC形成位置无关码,采用相对编址的方式,在程序链接时将对应库中的偏移量添加到程序中,库函数在程序运行时加载进来,经过页表,把库映射到虚拟地址空间后(共享区),库就具有了起始地址。通过起始地址和偏移地址,就可以找到要调用的库函数。
系统层面上会维护动态库的起始地址(虽然刚刚加载时不能确定起始地址,因为共享区是由OS分配的,但是加载完毕就不会改变了),直接建立页表与内存的映射,就可以直接跳转访问了。所以动态库加载一次就可以被多个进程共同使用。
动态库相对于静态库更节省内存,静态库由多个程序使用相同的库函数,加载到内存中就会导致内存中有多份重复的库函数代码,而动态库则是多个程序共用一份动态库,不会导致出现重复的库函数代码,就节省了内存空间。