【Linux】动静态库


1. 认识库

在过往的学习中,对于库是既熟悉又陌生。其实在写C/C++代码时,一定会用到C/C++的标准库。通常库和头文件会一起使用,头文件我们会显式的包含,库一般是看不到的。头文件提供方法的声明,库提供方法的实现,二者结合发挥作用。

为什么要有库

  1. 代码重用: 库使开发者能够写一次代码,并在需要时在不同的项目中重复使用。这大大提高了开发效率,避免了重复编写相同的功能和算法。
  2. 功能扩展: 库为编程语言添加了额外的功能和特性。通过使用库,开发者可以通过调用库中提供的函数和类来实现特定功能,而无需从头开始编写复杂的代码。
  3. 提高开发效率: 库中的函数和类经过了测试和优化,可以直接使用,减少了开发者开发和测试新功能的工作量。它们通常是经过优化和高度可靠的实现,可以提供更好的性能和可靠性。
  4. 标准化和一致性: 库可以提供一致的编程接口,使开发者能够使用相同的方式访问和使用不同的功能。这有助于降低学习曲线,并促进团队合作,因为开发者可以共享和理解使用相同库的代码。
  5. 社区贡献:库经常由开源社区开发和维护,这意味着有很多开发者可以贡献代码、修复错误并改进库的功能。这样的协作有助于共同推动和改进编程语言的生态系统。
  6. 平台独立性:库可以提供平台独立性,使得开发者能够在不同的操作系统和硬件平台上使用相同的代码。这样,开发者可以编写一次代码,然后在不同的平台上部署和运行。

库的本质是已编译的二进制代码文件,所以在开发者向用户提供服务时,可以将源代码打包成库发送给用户,而不是直接给用户源代码,保证了代码安全。

库的分类

💭库分为静态库和动态库两种形式:

  1. 静态库Static Library):静态库是在编译时被链接到应用程序中的代码的副本。 当编译应用程序时,静态库的代码被复制到最终的可执行文件中,使得应用程序可以独立运行,不需要依赖外部的库文件。静态库的扩展名通常为.lib(Windows)或.a(Linux)。
  2. 动态库Dynamic Library):动态库是在运行时由操作系统动态加载和链接的代码库。 相比静态库,动态库的代码不会被复制到应用程序中,而是在程序启动时由操作系统加载到内存中(具体加载过程后面详谈)。因此使用动态库的文件通常会比使用静态库的文件小。多个应用程序可以共享同一个动态库,从而节省内存空间。动态库的扩展名通常为.dll(Windows)或.so(Linux)。

💡Linux中,静态库的文件后缀是.a,动态库的文件后缀是.so,且都有前缀lib。所以,静态库libmymath.a,实际库名称只有mymath


2. 制作动静态库

前面说了,库的本质是已编译的二进制代码文件,那么在编译过程中,什么时候生成二进制代码文件呢?复习一下

在这里插入图片描述

🔎在汇编过程之后,编译器会将汇编代码文件转换成二进制代码文件,即.o为后缀的文件。而库的制作方法就是:先将多个实现方法的源代码文件转化为二进制代码文件,然后链接起来,形成库。

在Linux环境下,简单制作一个包含加法(myadd)和减法(mysub)的数学库(mymath),分别使用静态库和动态库:

静态库

  1. 先写一个头文件myadd.h,用以声明myadd函数
#pragma once

#include <stdio.h>

int myadd(int x,int y);
  1. 同理,写一个头文件mysub.h,以声明mysub函数
#pragma once

#include <stdio.h>

int mysub(int x,int y);
  1. 有了声明就要有实现,分别在两个源文件中实现加法函数(myadd)和减法函数(mysub)
//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;
}
  1. 用gcc编译myadd.cmyadd.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
    
  2. ar -rc [libname.a] [filename.o]生成静态库(将.o文件链接起来)

动态库

前面与制作动静态库前三步相同,直到编译生成二进制代码文件时有所不同。

  1. 用gcc编译myadd.cmyadd.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
    
  2. 同理也需要将.o文件链接起来,但是动态库不需要使用特殊的指令,直接用gcc编译器链接即可(加上-shared选项)
    在这里插入图片描述

3. 使用库的方法

  • 写一个test.c程序,其中调用了myaddmysub方法。
#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选项)

    1. 头文件所在目录路径-I [dirpath]

    2. 库所在目录路径-L [dirpath]

    3. 库的名称-l [libname]注意这里的libname是去掉前后缀的真实名称。

也可以将头文件和库拷贝到系统默认搜索路径下,就不用专门指明头文件目录路径和库目录路径了。但库的名称是一定要指定的,否则编译器无法确定你要用哪个库。

在这里插入图片描述

得到可执行文件test,分别用fileldd指令获取该文件相关信息。发现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程序所用动态库在哪?三种方法

  1. 修改环境变量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)
    

    但这种方法不常用,因为环境变量只服务于当前会话,下次重启系统就丢失了。

  2. 在系统默认的库目录下,创建第三方动态库的软链接
    在这里插入图片描述

    随意地在系统lib目录下写入文件是不安全的,因此这个方法也不是最优解。

  3. 配置文件

    在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(去掉前缀后缀)库。此时就两种情况:

  1. lib中只包含libmymath.so或者同时包含libmymath.alibmymath.so(即同名动静态库),编译器会默认使用动态库,将动态库与test进行链接。此时有分两种情况:若OS能找到libmymath.so(理解一中的三种方式),则test能够运行,否则运行不了。
  2. 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 #补充情况

4. 理解动态库的加载

动态库的加载和进程的地址空间密切相关。关键所在:进程运行时,将动态库加载到自己的共享区中,使得每个进程都能独立使用动态库,互不干扰。

  1. 编译形成的可执行文件中,有一套逻辑地址,服务于调用函数和一些变量,这些地址一般都是当前文件中的某个地址。而在链接动态库的可执行文件中,调用函数的逻辑地址被编译为动态库当中的某个地址,OS能到指定的动态库中找到所调用函数。如图(test是可执行程序,mymath是动态库,test调用了mymath中的myadd方法
    在这里插入图片描述

    这个搞清楚了,接下来就是test启动运行,形成进程,加载进程控制块等过程,不多废话,直接放图
    在这里插入图片描述

  2. 进程开始运行,当CPU读取到虚拟内存中myadd的地址,经过页表的映射,发现对应地址在物理空间中的二进制指令是调用mymath的1234地址处。OS开始寻找mymath动态库,找不到则提示用户无法找到对应的库,找到了,就会有以下过程:将对应的动态库二进制可执行文件load到内存中,再load到对应进程的共享区中。此时进程就可以在自己的mm_struct中独立地调用myadd。
    在这里插入图片描述

  3. 那么,要是一个进程链接了多个动态库,会是什么样的情形呢?我们需要先回到可执行文件中逻辑地址的概念,逻辑地址有绝对编址和相对编址两种形式。而我们上面讨论的形式都是绝对编址。绝对编址使用固定的内存地址来访问数据,每个内存位置都有唯一的地址,在32位机器下,绝对编址的范围是0x00000000~0xffffffff。所以,若动态库中采用绝对编址的方式,那么在其load进内存之后,需要根据其在内存中的位置修改绝对地址,十分麻烦。

    因此,动态库中其实使用的是相对编址,简单理解就是将绝对的地址变成相对的偏移量,默认参考点为0。这就是我们在编译形成库的.o文件时,加上了-fPIC位置无关码的原因。所以,当一个进程使用了多个动态库时,将多个库load到内存中,由OS统一管理并记录每个库的起始位置。此时调用动态库中的函数call的地址是:对应库的起始位置+函数的偏移量。这样一来就不用再去修改库中的地址了,况且OS也可以根据偏移量判断要load库的哪一部分,而不是将整个库load到内存中。一旦一个动态库被load内存(指一个进程维护的虚拟内存)中,下次该进程再调用该库中的方法,就直接从自己的内存中跳转并调用了。

在这里插入图片描述


Ending

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值