文章目录
- 前言
- Q&A
- 概念
- Q:使用静态库和使用动态库的程序有什么区别?
- Q:什么是静态链接/动态链接?
- 使用与制作
- Q:如何制作动静态库?
- Q:如何使用第三方库?
- Q:程序加载时,链接器查找动态库文件的顺序?
- 原理
- Q:怎么从进程地址空间的角度理解动态库?
前言
本篇博客梳理关于动静态库相关的Q&A,这是Linux复习中的一部分。若读者也在复习这块知识,或者正在学习这块知识,可以通过这些Q&A检测自己的知识掌握情况。此外,思维导图已经更新至我的gitee,Q&A之外的体系梳理还请移步思维导图。
Q&A
概念
Q:使用静态库和使用动态库的程序有什么区别?
A:
- 静态库(.a):程序编译时会将静态库的二进制代码拷贝到程序代码中,运行时不再需要链接外部静态库
- 动态库(.so):程序编译完成,运行时才会去链接外部的动态库,不需要将动态库拷贝到程序的代码中
Q:什么是静态链接/动态链接?
A:
- 静态链接:首先要明白的是:main函数所在的obj文件可以和其他obj文件一起编译,生成可执行文件。而静态库中有多个obj文件,将这些obj文件与main函数所在的obj文件一起编译,这个过程就叫做静态链接。静态链接需要编译除自己之外的其他obj文件,生成的可执行文件会占用更多的空间
- 动态链接:使用动态库的可执行文件含有一张动态符号表,表中包含了需要用到的函数符号。动态库也有一张动态符号表,包含了动态库的函数符号。程序运行时,系统将动态库从磁盘加载到内存。同时,动态链接器根据符号表将程序与动态库之间建立绑定。一个动态库可以绑定多个程序的动态符号表,也就是可以被多个程序使用。使用动态库的程序,编译生成的可执行文件将占用更少的空间
关于动态链接器的链接过程:符号解析 + 重定位
- 动态链接器根据程序中的符号引用和动态库中的符号定义生成一张符号表,用来记录程序中每个符号在内存中的地址
- 动态链接器遍历程序的重定位条目,根据符号表中的地址信息,修改程序中符号的引用,使它们指向正确的内存位置
- 动态链接器调用动态库的初始化函数,完成必要的初始化工作
- 动态链接器将控制权交给程序的入口点,使程序开始执行
使用与制作
Q:如何制作动静态库?
A:
- 动态库的制作:必须使用位置无关的obj文件
- 编译obj文件时:gcc -fPIC -c (源文件名) -o (生成的obj文件名)
- 生成动态库时:gcc -shared (源obj文件…) -o (生成的动态库名)
- 静态库的制作:使用位置有关的obj文件
- 使用ar -rc (要生成的静态库名称) (要归档的obj文件)指令归档一个静态库
- 最后创建一个总目录,其中有两个子目录,一个放头文件,一个放obj文件
Q:如何使用第三方库?
A:首先,无论是动态库还是静态库,源程序都需要包含头文件(函数的声明)。这样做可以使得程序编译成功,但是无法进行链接。
- 静态库:编译器在链接时,会在/usr/lib64这个默认路径下搜索源程序需要链接的库,在/usr/include这个路径下搜索需要使用的头文件。通常我们会用到标准库,所以编译器会默认在/usr/include和/usr/lib64这两个路径下查找头文件与标准库并且链接标准库。若源程序需要使用第三方库,就需要告知编译器:头文件所在路径、需要链接的库及其所在路径
- 若第三方库和第三方头文件已经添加到/usr/lib64和/usr/include目录下,那么编译源文件时,只要带上-l 选项,告知编译器要链接的库。比如
gcc test.c -l myfirst -o test
- 要注意的是:一般库文件名称都含有"lib"的前导和".so"或".a" 的后缀。比如libmyfirst.so和libmyfirst.a,使用-l选项时,需要去除"lib"前导与".*"的后缀。比如libmyfirst.so写为myfirst
- 若第三方库和第三方头文件没有添加到默认路径下,那么就需要使用-I (头文件所在路径)和-L (库文件所在路径)告知编译器库文件与头文件所在的位置。比如
gcc test.c -L ../lib-static/lib -I ../lib-static/include -l myfirst
库文件所在路径 头文件所在路径 要链接的库
- 动态库:同样,使用动态库有两种方法,一种是简单的向默认路径添加库文件与头文件,编译时只使用-l选项。另一种使用-L、-I、-l三个选项进行编译。注意:对于后者,源程序依然可以编译成功,但是程序无法执行,一旦执行就会崩溃,原因是:
- 虽然编译器知道源程序依赖的库文件所在路径,编译器根据库文件生成符号表。但是程序加载时,操作系统需要进行动态链接 ,这个过程需要加载动态库到内存并修改符号表,使其指向正确的内存位置。然而进行动态链接的动态链接器不知道动态库所在的位置(磁盘中的位置),动态链接的过程无法进行。解决方法是:
- 采用第一种方法,将动态库文件添加到默认路径下
- 向环境变量LD_LIBRARY_PATH导入动态库路径(用export设置)。动态链接时,链接器除了会在默认路径下查找库文件,还会解析LD_LIBRARY_PATH中的路径,查找变量中的路径。不过系统一旦重新,环境变量重置,需要重新配置才能使程序正确运行
- 修改系统配置文件。/etc/ld.conf.so.d/目录下存储了系统的配置文件,每个配置文件存储了一个路径,将库文件所在的绝对路径为内容创建我们的配置文件,并添加到该路径下。添加完成使用sudo ldconfig指令,加载配置文件即可
- 与第一种方式类似,向默认路径下以绝对路径的方式添加软连接。使用指令
sudo ln -s /home/xx/xxx/lib-dynamic/libmyfirst.so /usr/lib64/libmyfirst.so
库文件的绝对路径 需要在系统中创建的软链接文件
Q:程序加载时,链接器查找动态库文件的顺序?
A:动态链接器会根据以下顺序查找动态库文件,若都无法找到,动态加载失败,程序将崩溃
- 在环境变量LD_LIBRARY_PATH中存储的指定路径中查找,使用export设置
- 在/etc/ld.conf.so.d/目录下根据配置文件存储的路径查找,使用ldconfig使配置生效
- 默认的/usr/lib64路径
其实一开始,动态链接器会根据程序硬编码的RPATH或RUNPATH选项指定的路径查找。不过我们一般都不会设置这两个选项,因为这会降低程序的可移植性和灵活性,所以我没有在上面提及。
原理
Q:怎么从进程地址空间的角度理解动态库?
A:加载程序时,系统就要为其分配进程地址空间,堆区往上栈区往下之间有一个区域:共享区,共享区有很多作用,其中一个是:存储动态库函数的映射地址。映射地址只是虚拟地址,由于动态库函数的地址是在程序运行时才确定的,而共享区在程序加载时就需要进行创建。所以操作系统会在动态库函数地址确定之后修改页表,使共享区的映射地址指向正确的动态库函数地址。
运行使用动态库的程序的主要过程:
- 系统将程序对应的可执行文件和需要使用的动态库加载到内存中
- 系统创建为可执行文件创建进程,建立进程地址空间,页表。同时调用动态链接器,进行符号解析和重定位
- 当系统为动态库函数分配好了空间,系统将修改进程的页表,使共享区的动态库函数映射地址指向正确的地址
- 当进程调用动态库函数时,会跳转到共享区,获取函数的映射地址,经过页表的转换找到函数并且执行。函数执行完,程序会从共享区跳转回原来的区域,继续往下执行代码