【Linux】动静态库
慕雪年华

在最初学习GCC的使用的时候,提到了动态、静态库的创建办法。今天就让我们来详细了解一番,它们之间究竟有何不同吧!

演示所用系统:centos7.6

[TOC]

1.动态库和静态库

先来了解一下动态库和静态库的基本概念吧!

  • 静态库.a 程序编译链接的时候,把静态库的代码连接到自己的可执行程序中,程序运行的时候将不再需要静态库
  • 动态库.so 程序在运行的时候才去链接动态库的代码,多个程序共享库的代码

2.生成

测试所用代码 👉 点我

我写好了两个头文件和两个源文件,为了减少博客篇幅,此处只贴出.c的函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
//myMath.c
#include"myMath.h"
int Add(int a,int b)
{
return a+b;
}

//myPrint.c
#include "myPrint.h"
void Print(const char* msg)
{
printf("time: %d, msg: %s\n",(unsigned int)time(NULL),msg);
}

2.1 静态库

生成静态库所用命令为ar -rc,对应的完整make操作如下

1
2
3
4
5
6
libMytest.a:myMath.o myPrint.o
ar -rc libMytest.a myMath.o myPrint.o
myMath.o:myMath.c
gcc -c myMath.c -o myMath.o
myPrint.o:myPrint.c
gcc -c myPrint.c -o myPrint.o

生成好静态库后,我们可以用ar -tv命令来查看该库的目录列表

1
2
3
4
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库]$ ar -tv libmytest.a
rw-rw-r-- 1001/1001 1240 Nov 3 09:28 2022 myMath.o
rw-rw-r-- 1001/1001 1632 Nov 3 09:28 2022 myPrint.o
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库]$

2.2 动态库

动态库的生成无需额外的命令,只需要在gcc编译的时候,指定-shared即可

同时,依赖的.o文件也需要用-fPIC来编译

1
2
-fPIC 与位置无关码,和动态库的特性有关
-shared 代表需要编译一个动态库

其make操作如下

1
2
3
4
5
6
libmytest.so:myMath.o myPrint.o
gcc -shared -o libmytest.so myMath.o myPrint.o
myMath.o:myMath.c
gcc -fPIC -c myMath.c -o myMath.o
myPrint.o:myPrint.c
gcc -fPIC -c myPrint.c -o myPrint.o

2.3 一并发布

这里我写了一个更加完整的makefile,可以同时编译生成动静态库,并将其打包到一个指定的文件夹内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.PHONY:all
all:libmytest.so libmytest.a

.PHONY:lib
lib:
mkdir -p lib-static/lib
mkdir -p lib-static/include
cp *.a lib-static/lib
cp *.h lib-static/include
mkdir -p lib-dynamic/lib
mkdir -p lib-dynamic/include
cp *.so lib-dynamic/lib
cp *.h lib-dynamic/include

libmytest.so:myMath.o myPrint.o
gcc -shared -o libmytest.so myMath.o myPrint.o
myMath.o:myMath.c
gcc -fPIC -c myMath.c -o myMath.o
myPrint.o:myPrint.c
gcc -fPIC -c myPrint.c -o myPrint.o

libmytest.a:myMath.o myPrint.o
ar -rc libmytest.a myMath.o myPrint.o
myMath_s.o:myMath.c
gcc -c myMath.c -o myMath_s.o
myPrint_s.o:myPrint.c
gcc -c myPrint.c -o myPrint_s.o

.PHONY:clean
clean:
rm -rf *.o *.a *.so lib-static lib-dynamic

3.使用

1
2
3
4
5
6
7
8
9
10
#include "myPrint.h"
#include "myMath.h"
#include "stdio.h"

int main()
{
printf("ret %d\n",Add(1,2));
Print("这是一个测试");
return 0;
}

当我们使用了动静态库后,就没有办法直接编译这个可执行程序了

1
2
3
4
5
6
muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ gcc test.c
/tmp/ccKHwYHv.o: In function `main':
test.c:(.text+0xf): undefined reference to `Add'
test.c:(.text+0x2a): undefined reference to `Print'
collect2: error: ld returned 1 exit status
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$

这是因为,gcc没办法找到我们对应的头文件

  • ""是在当前路径下找
  • <>是在库目录下面找

因为我们的头文件既不在当前路径,也不在系统的库中,所以gcc就没有办法找到头文件和函数声明

3.1 静态库

链接静态库的方法如下

1
gcc test.c -L../lib-static/lib/ -I../lib-static/include/ -lmytest -o test
  • -L选项后带的是库的路径
  • -I选择后带的是头文件的搜索路径
  • -l(小写的L)选项带的是库的名字,需要去掉库文件名前面的lib和后缀.a
  • -o test代表生成可执行文件名为test
1
2
3
4
5
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ gcc test.c -L../lib-static/lib/ -I ../lib-static/include/ -lmytest -o test
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ./test
ret 3
time: 1667441311, msg: 这是一个测试
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$

特点

静态库的特点便是,其库的实现已经被编译链接进入了可执行程序,即便我们将库给删除,也不影响可执行程序的运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库]$ make clean
rm -rf *.o *.a *.so lib-static lib-dynamic
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库]$ ll
total 24
-rw-rw-r-- 1 muxue muxue 702 Nov 3 09:28 makefile
-rw-rw-r-- 1 muxue muxue 60 Nov 3 08:52 myMath.c
-rw-rw-r-- 1 muxue muxue 35 Nov 3 08:51 myMath.h
-rw-rw-r-- 1 muxue muxue 117 Nov 3 09:01 myPrint.c
-rw-rw-r-- 1 muxue muxue 77 Nov 3 09:01 myPrint.h
drwxrwxr-x 2 muxue muxue 4096 Nov 3 09:50 test
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库]$ ./test/./test
ret 3
time: 1667440486, msg: 这是一个测试
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库]$

如果我们把自己的库的实现丢入了系统的库目录下(一般是/lib64/)编译的时候就不需要带-L选项了,只需要用-l指定库名即可

1
gcc test.c -lmytest

但是将自己的库丢入系统库路径下的操作并不推荐,就和你将自己的可执行程序丢入/usr/bin路径里面一样,会污染系统的环境


3.2 动态库

动态库和静态库链接的基本方式是一样的

1
gcc test.c -L../lib-dynamic/lib/ -I ../lib-dynamic/include/ -lmytest -o testd

这里选项的含义和上面完全一致,不同的是运行的时候

1
2
3
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ./testd
./testd: error while loading shared libraries: libmytest.so: cannot open shared object file: No such file or directory
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$

直接运行,你会发现报错了!这个报错的大概意思就是找不到动态库文件

ldd命令

使用ldd命令查看testd可执行文件的动态库结构,会发现我们自己的库是没有找到的

1
2
3
4
5
6
7
8
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ldd testd
linux-vdso.so.1 => (0x00007ffd051fe000)
/$LIB/libonion.so => /lib64/libonion.so (0x00007f7de6d19000)
libmytest.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f7de6832000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f7de662e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7de6c00000)
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$

这是因为,动态库的特点便是运行的时候也需要指定!这是一个动态链接的过程!

动态链接

动态库需要执行动态链接:在可执行程序开始运行之前,外部函数的机器码由操作系统从磁盘上的该动态库复制到内存中

刚刚我们的指定只是告诉了gcc编译器库路径在哪儿,但是可执行程序运行的时候并不知道!

那么如何让可执行程序找到我们的动态库呢?

  • 将动态库拷贝到系统的/lib64文件夹中
  • 通过修改环境变量的方式,类似于PATH,可执行程序运行的时候,会自动到LD_LIBRARY_PATH里面找动态库
  • 修改系统配置文件

3.3 找到动态库

3.3.1 环境变量LD_LIBRARY_PATH

和修改PATH的环境变量一样,我们可以通过修改环境变量的方式增加动态库的查找路径

1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/muxue/git/linux/code/22-11-02_动静态库/lib-dynamic/lib/

修改了之后的环境变量如下

1
LD_LIBRARY_PATH=:/home/muxue/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/muxue/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/muxue/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/muxue/git/linux/code/22-11-02_动静态库/lib-dynamic/lib/

再次运行./testd 成功执行!

1
2
3
4
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/muxue/git/linux/code/22-11-02_动静态库/lib-dynamic/lib/
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ./testd
ret 3
time: 1667443224, msg: 这是一个测试

修改配置文件的办法,便是将该路径永久写入环境变量(修改环境变量的操作只对当前bash有效)这里就不演示辣!

1
2
3
4
5
6
7
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ldd testd
linux-vdso.so.1 => (0x00007ffde04ea000)
/$LIB/libonion.so => /lib64/libonion.so (0x00007fe9000bc000)
libmytest.so => /home/muxue/git/linux/code/22-11-02_动静态库/lib-dynamic/lib/libmytest.so (0x00007fe8ffda1000)
libc.so.6 => /lib64/libc.so.6 (0x00007fe8ff9d3000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fe8ff7cf000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe8fffa3000)

ldd命令的结果也显示出了我们自己写的动态库的路径

3.3.2 /etc/ld.so.conf.d

除了修改环境变量,我们还可以修改/etc/ld.so.conf.d下的文件

1
2
3
4
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ls /etc/ld.so.conf.d
bind-export-x86_64.conf kernel-3.10.0-1160.62.1.el7.x86_64.conf kernel-3.10.0-1160.76.1.el7.x86_64.conf
dyninst-x86_64.conf kernel-3.10.0-1160.71.1.el7.x86_64.conf mariadb-x86_64.conf
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$

这里的操作非常简单,我们只需要在该目录下新增一个.conf文件,并在里面写入动态库的绝对路径即可!

1
2
3
4
5
6
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ls /etc/ld.so.conf.d
bind-export-x86_64.conf kernel-3.10.0-1160.62.1.el7.x86_64.conf kernel-3.10.0-1160.76.1.el7.x86_64.conf mytest.conf
dyninst-x86_64.conf kernel-3.10.0-1160.71.1.el7.x86_64.conf mariadb-x86_64.conf
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ cat /etc/ld.so.conf.d/mytest.conf
/home/muxue/git/linux/code/22-11-02_动静态库/lib-dynamic/lib/
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$

设置了之后,第一次运行,还是显示找不到动态库

1
2
3
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ./testd
./testd: error while loading shared libraries: libmytest.so: cannot open shared object file: No such file or directory
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$

我们只需要执行下面的命令让配文件生效,就OK了!

1
sudo ldconfig #子用户权限不够,需要加sudo

执行完该命令后,可执行程序也能成功运行了1

1
2
3
4
5
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ sudo ldconfig
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$ ./testd
ret 3
time: 1667448942, msg: 这是一个测试
[muxue@bt-7274:~/git/linux/code/22-11-02_动静态库/test]$

测试完毕之后,建议将配置文件删除,并重新加载动态库配置文件

1
2
sudo rm /etc/ld.so.conf.d/mytest.conf
sudo ldconfig

这样做是避免污染

3.3.3 在lib64下创建一个软连接

1
ln -s /home/muxue/git/linux/code/22-11-02_动静态库/lib-dynamic/lib/libmytest.so /lib64/libmytest.so

创建软连接的方式和将我们的文件复制进去本质是一样的,只不过软连接只是一个快捷方式,如果我们把源给删了,软连接也会失效

这部分就不做演示了


4.优劣

4.1 静态库

静态库编译之后的可执行程序可以脱离静态库运行,也不需要知道库的路径。

即便这个库被删除,也丝毫不影响我们的可执行程序

4.2 动态库

动态库的代码只需要一份,所有的可执行程序便都可以使用

在运行期间,动态库可以被多个进程所共享。但前提是,可执行程序需要知道该动态库的路径,以便将其加载到内存中(或者找到它在内存中的位置)

image

这样就保证了多个进程同时使用同一个库,节省了内存的消耗,也节省了磁盘空间

image

这里动态库的可执行文件大小,小于静态库的可执行文件

因为测试的代码不多,所以差距尚不明显

5.动态库-fPIC的作用

参考https://blog.csdn.net/itworld123/article/details/117587091

1
gcc -fPIC -c myMath.c -o myMath.o

fPIC 的全称是 Position Independent Code, 用于生成位置无关代码

什么是位置无关代码?

个人理解是代码无绝对跳转,跳转都为相对跳转

如果我们的静态库中,不使用其他库的代码(比如stdio.h

1
2
3
4
int fuc(int a)
{
return ++a;
}

这时候,就可以再编译的时候不带-fPIC否则会报错

1
2
/usr/bin/ld: /tmp/ccCViivC.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC
/tmp/ccCViivC.o: could not read symbols: Bad value

但显然,这种情况是非常少见的,所以我们一般编译动态库的时候,都需要带上这个参数,来实现真正意义上的动态库编译

  • 加 fPIC 选项生成的动态库,显然是位置无关的,这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能正确执行。通常的方法是获取指令指针的值,加上一个偏移得到全局变量 / 函数的地址。
  • 加 fPIC 选项的源文件对于它引用的函数头文件编写有很宽松的尺度。比如只需要包含个声明的函数的头文件,即使没有相应的 C 文件来实现,编译成 so 库照样可以通过。
  • 对于不加 fPIC,则加载 so 文件时,需要对代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个 .so 文件代码段的进程在内核里都会生成这个 .so 文件代码段的 copy。每个 copy 都不一样,取决于这个 .so 文件代码段和数据段内存映射的位置。这种方式更消耗内存,优点是加载速度可能会快一丢丢,弊大于利

结语

动静态库的基本认识到这里就OVER辣,大家也可以去尝试下载一些第三方的库来使用,比如在树莓派上最常用的wiringPi库,还有C++的boost库等等

1
sudo yum install -y boost-devel

有什么问题,可以在评论区提出哦!

image