在过往的学习中,对于库是既熟悉又陌生。其实在写C/C++代码时,一定会用到C/C++的标准库。通常库和头文件会一起使用,头文件我们会显式的包含,库一般是看不到的。头文件提供方法的声明,库提供方法的实现,二者结合发挥作用。
库的本质是已编译的二进制代码文件,所以在开发者向用户提供服务时,可以将源代码打包成库发送给用户,而不是直接给用户源代码,保证了代码安全。
库分为静态库和动态库两种形式:
.lib
(Windows)或.a
(Linux)。.dll
(Windows)或.so
(Linux)。Linux中,静态库的文件后缀是.a
,动态库的文件后缀是.so
,且都有前缀lib
。所以,静态库libmymath.a
,实际库名称只有mymath
。
前面说了,库的本质是已编译的二进制代码文件,那么在编译过程中,什么时候生成二进制代码文件呢?复习一下
在汇编过程之后,编译器会将汇编代码文件转换成二进制代码文件,即.o
为后缀的文件。而库的制作方法就是:先将多个实现方法的源代码文件转化为二进制代码文件,然后链接起来,形成库。
在Linux环境下,简单制作一个包含加法(myadd)和减法(mysub)的数学库(mymath),分别使用静态库和动态库:
myadd.h
,用以声明myadd
函数#pragma once
#include
int myadd(int x,int y);
mysub.h
,以声明mysub
函数#pragma once
#include
int mysub(int x,int y);
//myadd.c
#include "myadd.h"
int myadd(int x,int y)
{
return x+y;
}
//myadd.c
#include "mysub.h"
int mysub(int x,int y)
{
return x-y;
}
用gcc编译myadd.c
和myadd.c
,加上-c
选项,以生成.o
二进制文件。
[ckf@VM-8-3-centos blog]$ gcc -c myadd.c
[ckf@VM-8-3-centos blog]$ gcc -c mysub.c
[ckf@VM-8-3-centos blog]$ ll
total 24
-rw-rw-r-- 1 ckf ckf 63 Jul 6 16:05 myadd.c
-rw-rw-r-- 1 ckf ckf 58 Jul 6 16:05 myadd.h
-rw-rw-r-- 1 ckf ckf 1240 Jul 6 16:15 myadd.o
-rw-rw-r-- 1 ckf ckf 43 Jul 6 16:03 mysub.c
-rw-rw-r-- 1 ckf ckf 58 Jul 6 16:05 mysub.h
-rw-rw-r-- 1 ckf ckf 1240 Jul 6 16:15 mysub.o
ar -rc [libname.a] [filename.o]
生成静态库(将.o
文件链接起来)
前面与制作动静态库前三步相同,直到编译生成二进制代码文件时有所不同。
用gcc编译myadd.c
和myadd.c
,加上-c
选项,以生成.o
二进制文件。除了-c
选项,还要加上与位置无关码-fPIC
,这是制作动态库的关键。
[ckf@VM-8-3-centos blog]$ gcc -c -fPIC myadd.c -o myadd-d.o
[ckf@VM-8-3-centos blog]$ gcc -c -fPIC mysub.c -o mysub-d.o
[ckf@VM-8-3-centos blog]$ ll
total 36
-rw-rw-r-- 1 ckf ckf 2692 Jul 6 16:23 libmymath.a
-rw-rw-r-- 1 ckf ckf 63 Jul 6 16:05 myadd.c
-rw-rw-r-- 1 ckf ckf 1240 Jul 6 16:29 myadd-d.o
-rw-rw-r-- 1 ckf ckf 58 Jul 6 16:05 myadd.h
-rw-rw-r-- 1 ckf ckf 43 Jul 6 16:03 mysub.c
-rw-rw-r-- 1 ckf ckf 1240 Jul 6 16:29 mysub-d.o
-rw-rw-r-- 1 ckf ckf 58 Jul 6 16:05 mysub.h
test.c
程序,其中调用了myadd
和mysub
方法。#include "myadd.h"
#include "mysub.h"
int main()
{
int x = 10;
int y = 20;
printf("x+y = %d\n",myadd(x,y));
printf("x-y = %d\n",mysub(x,y));
return 0;
}
lib
目录中,头文件都存到include
目录中。如下:[ckf@VM-8-3-centos blog]$ ll
total 12
drwxrwxr-x 2 ckf ckf 4096 Jul 6 16:59 include
drwxrwxr-x 2 ckf ckf 4096 Jul 6 16:59 lib
-rw-rw-r-- 1 ckf ckf 180 Jul 6 16:58 test.c
[ckf@VM-8-3-centos blog]$ tree
.
|-- include
| |-- myadd.h
| `-- mysub.h
|-- lib
| |-- libmymath.a
| `-- libmymath.so
`-- test.c
gcc编译程序时,需要告知编译器以下信息。(括号内是使用的gcc选项)
头文件所在目录路径(-I [dirpath]
)
库所在目录路径(-L [dirpath]
)
库的名称(-l [libname]
)注意这里的libname是去掉前后缀的真实名称。
也可以将头文件和库拷贝到系统默认搜索路径下,就不用专门指明头文件目录路径和库目录路径了。但库的名称是一定要指定的,否则编译器无法确定你要用哪个库。
得到可执行文件test
,分别用file
和ldd
指令获取该文件相关信息。发现test
是动态链接,但是我们自己写的libmymath.so
却没有找到。这不是互相矛盾了吗?下面要理解一些细节。
⭕理解一
动态库是运行时链接,换句话说,程序运行时要能够找到动态库,继而再链接上。当一个程序启动后,变成进程,便受OS管理,我们上面的gcc命令虽然带了
-L
和-l
选项,指明了库的所在目录路径和名称,但这只是告诉编译器,并没有告诉OS。所以当程序运行起来后,OS无法将其与动态库链接上,这里自然也就找不到我们自己写的libmymath.so
。所以,此时的test
是无法运行的,系统会告诉你找不到动态库libmymath.so
。
[ckf@VM-8-3-centos blog]$ ./test
./test: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory
如何告知OS程序所用动态库在哪?三种方法
修改环境变量LD_LIBRARY_PATH,这是系统查询动态库的默认目录路径
[ckf@VM-8-3-centos blog]$ echo $LD_LIBRARY_PATH
:/home/ckf/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
[ckf@VM-8-3-centos blog]$ pwd
/home/ckf/NewBeginning/lesson5_lib/blog
[ckf@VM-8-3-centos blog]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ckf/NewBeginning/lesson5_lib/blog/lib/
[ckf@VM-8-3-centos blog]$ echo $LD_LIBRARY_PATH
:/home/ckf/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/ckf/NewBeginning/lesson5_lib/blog:/home/ckf/NewBeginning/lesson5_lib/blog/lib/
[ckf@VM-8-3-centos blog]$ ./test #程序可运行
x+y = 30
x-y = -10
[ckf@VM-8-3-centos blog]$ ldd test
linux-vdso.so.1 => (0x00007ffcd5531000)
libmymath.so => /home/ckf/NewBeginning/lesson5_lib/blog/lib/libmymath.so (0x00007fbfac5fa000)
libc.so.6 => /lib64/libc.so.6 (0x00007fbfac22c000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbfac7fc000)
但这种方法不常用,因为环境变量只服务于当前会话,下次重启系统就丢失了。
随意地在系统lib目录下写入文件是不安全的,因此这个方法也不是最优解。
配置文件
在Linux系统中,/etc/ld.so.conf.d
目录是用于配置动态链接器的文件目录。可以通过在/etc/ld.so.conf.d
目录中创建或编辑配置文件来添加、修改或删除动态库搜索路径。然后,使用ldconfig
命令来刷新动态链接器的缓存,以使配置生效。
[ckf@VM-8-3-centos blog]$ ll
total 24
drwxrwxr-x 2 ckf ckf 4096 Jul 6 16:59 include
drwxrwxr-x 2 ckf ckf 4096 Jul 6 16:59 lib
-rwxrwxr-x 1 ckf ckf 8432 Jul 6 19:50 test
-rw-rw-r-- 1 ckf ckf 180 Jul 6 17:42 test.c
[ckf@VM-8-3-centos blog]$ ./test #程序不可运行
./test: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory
[ckf@VM-8-3-centos blog]$ ls /etc/ld.so.conf.d/
bind-export-x86_64.conf dyninst-x86_64.conf kernel-3.10.0-1160.71.1.el7.x86_64.conf mariadb-x86_64.conf
[ckf@VM-8-3-centos blog]$ sudo vim /etc/ld.so.conf.d/test.conf #创建一个新的配置文件,并导入动态库的绝对路径
[ckf@VM-8-3-centos blog]$ sudo cat /etc/ld.so.conf.d/test.conf
/home/ckf/NewBeginning/lesson5_lib/blog/lib
[ckf@VM-8-3-centos blog]$ sudo ldconfig
[ckf@VM-8-3-centos blog]$ ./test #程序可运行
x+y = 30
x-y = -10
[ckf@VM-8-3-centos blog]$ ldd test
linux-vdso.so.1 => (0x00007fff4fbd1000)
libmymath.so => /home/ckf/NewBeginning/lesson5_lib/blog/lib/libmymath.so (0x00007f50ca280000)
libc.so.6 => /lib64/libc.so.6 (0x00007f50c9eb2000)
/lib64/ld-linux-x86-64.so.2 (0x00007f50ca482000)
⭕理解二
[ckf@VM-8-3-centos blog]$ gcc test.c -o test -I include -L lib -l mymath
对于动静态库,编译器默认将文件链接动态库。上面这个指令,头文件找到后,会到lib
目录寻找mymath
(去掉前缀后缀)库。此时就两种情况:
lib
中只包含libmymath.so
或者同时包含libmymath.a
和libmymath.so
(即同名动静态库),编译器会默认使用动态库,将动态库与test
进行链接。此时有分两种情况:若OS能找到libmymath.so
(理解一中的三种方式),则test
能够运行,否则运行不了。lib
中只包含libmymath.a
,不包含libmymath.so
。此时编译器找不到动态库,只能链接静态库libmymath.a
。但编译器还是会做一些优化处理,能动态链接的库就还是动态链接(比如C标准库就依然是动态链接),所以可执行程序test
还是一个动态链接的文件,有点“动静结合”的意思。补充情况:只有用户在编译时主动给出-static
选项,编译出来的可执行文件才是纯静态链接。
三种情况生成的可执行文件的大小对比:
-rwxrwxr-x 1 ckf ckf 8432 Jul 6 20:14 test-d #情况1
-rwxrwxr-x 1 ckf ckf 8480 Jul 6 20:14 test-u #情况2
-rwxrwxr-x 1 ckf ckf 861336 Jul 6 20:14 test-s #补充情况
⭕动态库的加载和进程的地址空间密切相关。关键所在:进程运行时,将动态库加载到自己的共享区中,使得每个进程都能独立使用动态库,互不干扰。
编译形成的可执行文件中,有一套逻辑地址,服务于调用函数和一些变量,这些地址一般都是当前文件中的某个地址。而在链接动态库的可执行文件中,调用函数的逻辑地址被编译为动态库当中的某个地址,OS能到指定的动态库中找到所调用函数。如图(test是可执行程序,mymath是动态库,test调用了mymath中的myadd方法)
进程开始运行,当CPU读取到虚拟内存中myadd的地址,经过页表的映射,发现对应地址在物理空间中的二进制指令是调用mymath的1234地址处。OS开始寻找mymath动态库,找不到则提示用户无法找到对应的库,找到了,就会有以下过程:将对应的动态库二进制可执行文件load到内存中,再load到对应进程的共享区中。此时进程就可以在自己的mm_struct中独立地调用myadd。
那么,要是一个进程链接了多个动态库,会是什么样的情形呢?我们需要先回到可执行文件中逻辑地址的概念,逻辑地址有绝对编址和相对编址两种形式。而我们上面讨论的形式都是绝对编址。绝对编址使用固定的内存地址来访问数据,每个内存位置都有唯一的地址,在32位机器下,绝对编址的范围是0x00000000~0xffffffff
。所以,若动态库中采用绝对编址的方式,那么在其load进内存之后,需要根据其在内存中的位置修改绝对地址,十分麻烦。
因此,动态库中其实使用的是相对编址,简单理解就是将绝对的地址变成相对的偏移量,默认参考点为0。这就是我们在编译形成库的
.o
文件时,加上了-fPIC
位置无关码的原因。所以,当一个进程使用了多个动态库时,将多个库load到内存中,由OS统一管理并记录每个库的起始位置。此时调用动态库中的函数call的地址是:对应库的起始位置+函数的偏移量。这样一来就不用再去修改库中的地址了,况且OS也可以根据偏移量判断要load库的哪一部分,而不是将整个库load到内存中。一旦一个动态库被load内存(指一个进程维护的虚拟内存)中,下次该进程再调用该库中的方法,就直接从自己的内存中跳转并调用了。
Ending