
-
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
-
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
-
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
-
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
-
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
my_add.h:
cpp
#pragma once
#include<stdio.h>
extern int Add(int a,int b);
my_sub.h:
cpp
#pragma once
#include<stdio.h>
extern int Sub(int a,int b);
my_add.c:
cpp
#include"my_add.h"
int Add(int a,int b)
{
printf("enter Add Func, %d + %d = ?\n",a,b);
return a+b;
}
my_sub.c:
cpp
#include"my_sub.h"
int Sub(int a,int b)
{
printf("enter Sub Func, %d - %d = ?\n",a,b);
return a+b;
}
main.c:
cpp
#include "my_add.h"
#include "my_sub.h"
int main()
{
int a = 10;
int b = 20;
int res = Sub(a, b);
printf("result: ", res);
res = Add(a, b);
printf("result: ", res);
return 0;
}

bash
gcc -o mymath main.c my_add.c my_sub.c
三个源文件单独编译成.o文件,再链接。
在编译的时候,我们直接gcc -o生成可执行程序。其实还可以这样:

bash
gcc -c main.c
gcc -c 会形成同名.o文件。
bash
gcc -o mymath main.o my_add.o my_sub.o
直接将所有的.o文件链接。
我们知道,将所有的.o文件链接起来,就可以形成可执行文件。那么我们先不谈论库,如果我们想让别人可以使用我们的代码。但是我们不想给对方提供源代码,只给对方提供我们的.o可重定位目标二进制文件,让对方用对方的代码进行链接就行。

我们把自己的源文件形成的.o文件以及.h头文件给对方,对方需要链接形成可执行程序的时候,只需要链接上这两个.o文件。可以看到这样做是可以的。
那么如果我们有两个.o文件,对方就要链接两个.o文件,如果有一万个.o文件呢?对方就要链接一万个.o文件,对方在写命令的时候就太麻烦了。
所以,我们就尝试着将所有的".o"文件,打一个包,给对方提供一个库文件即可!多个.o文件形成一个文件,形成的这个文件就叫库文件。由于打包工具和打包方式的不同,库文件又分为了动态库和静态库。
库的本质:就是.o文件的集合。
静态库和静态链接
将所有的源文件生成同名的.o文件之后,使用ar命令再将所有的.o文件打个包即可。归档。
makefile:
bash
libmymath.a:my_add.o my_sub.o
ar -rc $@ $^
my_add.o:my_add.c
gcc -c my_add.c -o my_add.o
my_sub.o:my_sub.c
gcc -c my_sub.c -o my_sub.o
.PHONY:clean
clean:
rm -f *.o libmymath.a

形成了一个libmymath.a的库文件,lib为前缀,库文件名为mymath,后缀.a代表静态库。
形成了一个库文件之后,怎么去用呢?直接将库文件拷贝给对方用户可以吗?不可以。
我们为什么能在Linux下编写C语言代码呢?

原因是Linux系统中已经提供了C语言的头文件和库文件。
所以,除了要把.o文件给用户还要将头文件给他,因为用户要使用头文件里面的方法。这也是为什么写C语言的时候,都是先"#include<stdio.h>"包含头文件,因为写C语言代码时,或多或少都要使用到它库里面的方法。
未来给别人交付库的时候:库文件.a .so + 匹配的头文件都要给别人。
所以,作为一款小小的软件,我们怎么让别人使用我们的库呢?来看一下我们怎么使用的:
makeifle:
bash
libmymath.a:my_add.o my_sub.o
ar -rc $@ $^
my_add.o:my_add.c
gcc -c my_add.c -o my_add.o
my_sub.o:my_sub.c
gcc -c my_sub.c -o my_sub.o
.PHONY:output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.a mylib/lib
cp -f *.h mylib/inlcude
.PHONY:clean
clean:
rm -f *.o libmymath.a
ar是gnu归档工具,rc表示(replace and create) 。


至此,就把库(软件)发布出去了,怎么让别人使用呢?

将软件压缩打个包,放进yum源当中,未来别人就能使用yum来下载。假设这里有一个人要使用我们的软件,我们将压缩包拷贝给他,就相当于别人下载了软件。

用户下载之后再进行解压。 解压完之后通常要安装,所谓的安装,本质就是将可执行程序拷贝到系统指定可以找到的目录下,后面再演示。

可以看到使用者已经拿到这个库了。那怎么使用这个库呢?

报错了,头文件找不到。怎么回事呢?头文件明明不是有吗?
原因是gcc也是一款编译器。编译器在搜索头文件时默认1.在当前目录下搜索 2.在系统默认指定路径下搜索。这里的头文件和main.c不在同级路径下,所以编译器在当前目录下找不到,在系统默认指定路径下也找不到。
使用 -I 选项给gcc指定头文件搜索路径,告知编译器,头文件在 -I 选项后面的指定路径中。

还报错,链接错误。头文件已经找到了,但是库文件还没有找到。也要告知编译器库文件在哪个指定的路径。那写C语言的时候,怎么指定库文件和头文件的路径呢?是因为库文件和头文件在系统默认指定路径下。
使用 -L 选项给gcc指定库文件搜索路径,告知编译器,库文件在 -I 选项后面的指定路径中。

还是编译不过。为啥呢?如果你要链接库,必须指明库名称!明确告知gcc编译器,要链接的库文件是在这个指定路径下的是哪一个库文件。一个路径下可能有很多库文件。
头文件只需要指定路径就可以,因为源文件main.c中已经告知了编译器要包含哪些头文件。但是库文件不仅要指明路径,还必须要指明库名称。
我们写C/C++代码时,从来没有指明过库名称。是因为,gcc/g++就是专门编译C/C++语言的,会默认给你填一个库名称。如果写的C/C++代码没有使用第三方库,使用的是C/C++提供的标准库,那么gcc/g++默认就能清楚要链接的是哪一个库,名称也能知道。但是如果使用了第三方库,就要指明库名称!gcc/g++能默认识别C/C++自带的库,对于第三方库要指明库名称。
使用 -l 选项指定库文件名称。

注意:库文件名为去掉前缀和后缀的部分!
可以看到只使用这个库,就能形成可执行文件,并能正常使用。静态库删掉,程序照样可以运行。
bash
gcc -o mymath main.c -I ./mylib/include -L ./mylib/lib -l mymath

可以看到,ldd查看mymath依赖的共享库的时候,并没有我们自己制作的静态库;file查看mymaht类型的时候,看到的还是静态链接。
对于只链接一个库来说。gcc默认是动态链接(建议行为),对应特定的一个库,究竟是动态链接还是静态链接,取决于提供的是动态库还是静态库,如果同时提供了动态库和静态库,那么取决于gcc的选择!
形成一个可执行程序,可能不仅仅只依赖一个库。假设可执行程序依赖五十个库,三十个是动态库,二十个是静态库,gcc每链接一个库的时候,还是按照只链接一个库的做法来做的。但是只要有一个动态库,gcc就是动态链接的。
下面我们将库安装到系统默认指定路径下。



这就叫做安装,其实也就是拷贝。
既然安装了,那是不是直接就可以gcc形成可执行程序了?

还是链接报错。但是gcc还是找不到要链接的是lib64下的哪一个库,lib64下有很多库。我们的库是第三方库,gcc只能识别默认的C语言的库。

所以,还要指定库名称。

这样,就把库安装到了系统默认指定路径,并可以直接链接形成可执行文件。
下面进行卸载。


从系统路径下将库文件和头文件删除就叫卸载。

动态库与动态链接
makefile:
bash
libmymath.a:my_add.o my_sub.o
ar -rc $@ $^
my_add.o:my_add.c
gcc -c my_add.c -o my_add.o
my_sub.o:my_sub.c
gcc -c my_sub.c -o my_sub.o
.PHONY:output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.a mylib/lib
cp -f *.h mylib/include
.PHONY:clean
clean:
rm -rf *.o libmymath.a mylib

形成动态库,更静态库类似,首先形成.o文件。多加了一个-fPIC选项。

再将所有的.o文件进行打包。静态库使用ar命令将.o文件进行归档。动态库使用gcc命令+shared选项进行归档。

shared选项表示形成共享库,而不是可执行程序。
- shared: 表示生成共享库格式。
- fPIC:产生位置无关码(position independent code)。
- 库名规则:libxxx.so。

此时,就形成了一个动态库。下面开始使用,同样,要给别人提供头文件和库文件。


可以将mylib压缩成压缩包,别人使用的时候再进行解压压缩。跟静态库一样。这里为了演示不再进行压缩了。

已经将头文件路径,库文件路径,库文件名都告知编译器了,为什么报错,共享库找不到呢?

此时,能看到我们的动态库,但是"not found"找不到。
这就是动态库与静态库的不同了。
指定的头文件路径、库文件路径、库文件名,是告诉了gcc编译器。当程序编译完时,就和gcc没关系了。程序编译完,就要运行,要把程序加载到内存变成进程,然后被OS调度。所以,程序运行起来,OS和shell也是要知道库在哪里的。OS和shell加载库的时候,一般也是在系统路径lib64目录下,但是我们的库没有在系统路径下,OS无法找到!
那么我们怎么告诉OS库在哪呢?有很多做法。
库搜索路径
从左到右搜索-L指定的目录。
由环境变量指定的目录 (LIBRARY_PATH)
由系统指定的目录
/usr/lib
/usr/local/lib
方式一:
- 可以将头文件库文件安装到lib64系统默认路径下,和静态库的做法一样。这里不赘述了。
方式二:
- OS和shell除了要在系统默认路径下搜索库,也会在环境变量LD_LIBRARY_PATH里搜索。

所以,我们可以将库添加到这个环境变量里。


此时,LD_LIBRARY_PATH环境变量就包含了我们的库路径。

链接的时候,就能找到我们的库了,也能运行了。
但是这种将库路径导入环境变量的做法有一个问题。只在本次登录有效。我们关闭终端重新登录:

如果要让环境变量永久有效,就要去修改环境变量的相关配置文件。太麻烦这里就不做演示了。
方式三:
- 配置/etc/ld.so.conf.d/

我们要做的非常简单,只需要将动态库的路径复制到任意配置文件中然后更新一下动态库路径的缓存即可。


还找不到的原因是,没有更新动态库路径缓存,ldconfig更新一下就可以使用了。

我们关闭终端,重新登录,执行可执行程序的时候,依旧可以找到动态库。

所以,这个路径是永久有效的。

将其删除,就找不到动态库了。
还有一种永久有效并且简单的做法。
方式四:
- 建立软链接

在当前目录下,建立了一个动态库的软链接。

搜索动态库路径的时候,默认就能在当前目录下搜索。
如果不想在当前目录下建立软链接,就想在系统里直接运行。我们可以将链接建立到系统路径下。

此时,就在系统的默认路径下建立了动态库的软链接,这个链接指向了我们的动态库路径。

我们关闭终端,重新登录,执行可执行程序的时候,依旧可以找到动态库。

也是永久有效的。
动静态库的加载
程序经过编译链接形成可执行程序就可以运行了。
静态库需要加载吗?不需要。当程序需要链接静态库时,实际是把静态库拷贝到程序里,如果有多个程序都需要使用这个静态库,每个程序都会拷贝一份静态库,那么就会造成空间的浪费。把静态库拷贝到程序的哪个区域呢?
代码段。程序在编译的时候,就已经以虚拟地址空间的方式编译好了。程序在没有加载到内存的时候,就已经有了自己的代码段、数据区。
那静态库只能映射到程序的代码段上吗?是的。

在链接阶段,静态库中的代码会被完整地复制到可执行文件中。当多个程序都使用了同一个静态库时,每个可执行文件都包含了该静态库的一份完整拷贝。在运行时,这些程序各自占用内存空间来加载自身包含的静态库代码,空间浪费。

当多个程序都使用同一个动态库时,系统会将动态库的代码映射到这些程序的地址空间中,使得多个程序可以共享同一份动态库的代码在内存中的实例。