动态库和静态库

静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库

动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码

静态库

静态库是一组目标文件(.o 或.obj)的集合,它在程序编译链接阶段被整合到可执行文件中,就像把一些工具(函数、类等实现)放在一个工具箱(静态库)里,当需要构建一个产品(可执行程序)时,就把这个工具箱里有用的工具拿出来组装到产品中

安装库文件本质上就是将头文件和库文件添加到对应的目录中

静态库制作

先将源文件编译成 .o 文件,g++ -c xxx.c然后使用 ar rcs xxx.a xxx.o 命令将.o 文件打包成静态库,ar是gnu归档工具,rc表示(replace and create),这个过程中头文件和源文件不需要在同一个目录下

静态库的使用:在要使用的源文件中 include 库的头文件,然后正常使用

带有静态库文件的编译

g++ -o 最终可执行文件名 xxx.cpp -I(不需要加空格,参数是要在哪个目录下寻找头文件,例:当前目录就是 -I. ,math/include 目录就是-I/math/include) -L(可以不用加空格,参数是要在哪个目录下寻找库文件,例:当前目录就是 -I. ,math/lib 目录就是-L/math/lib)-l(库文件名,不需要在中间加空格,假设库文件名为 libmath.a 默认省略会 lib 和 .a 后缀,如-lmath)

如:g++ -o test test.cpp -I ./mymath/include -L ./mymath/lib -lmath

当使用 GCC 的-static选项进行编译链接时,会尽量使用静态库进行链接,而不是动态库

由于 Linux 默认会使用动态库链接,因此编译时应加上 -static 选项来使用静态库

静态库的优点

提高开发效率:将通用的代码封装进静态库后,可以在多个不同的项目复用;

代码标准化和质量保证:当代码封装进静态库时,这些代码通常经过了充分的测试和优化;

可预测性能表现:静态库在编译时就被链接到可执行文件中,这使得程序在运行时的性能更加可预测;静态库的代码和数据在内存中的布局相对固定,这有助于优化程序的性能

运行时的独立性:静态库使程序在运行时不依赖于外部库文件的存在,也就是说当编译完成后,就可以独立运行而不需要外部库了

代码隐藏:静态库可以帮助隐藏代码的实现细节,可以将加密算法封装成静态库,只提供接口声明,用户可以使用加密功能,但无法看到加密算法的具体实现,从而保护了公司的核心技术。

静态库的缺点

物理内存浪费:当多个程序使用同一个静态库时,会造成代码段重复

硬盘空间浪费:静态库在编译链接阶段会被完整地复制到可执行文件中。这意味着如果多个程序使用了相同的静态库,每个可执行文件都会包含一份静态库的副本

更新困难:当静态库的代码需要更新(如修复错误或者添加新功能)时,所有使用该静态库的可执行文件都需要重新编译。这是因为静态库的代码已经被整合到可执行文件中,更新静态库相当于更新可执行文件的一部分代码

缺乏动态性:静态库在编译时就确定了其内容,程序运行时无法动态地加载或卸载不同版本的静态库。

动态库

动态库是一种可在程序运行时被加载和链接的库,与静态库不同,动态库的代码不会在编译时被复制到可执行文件中,而是在程序运行过程中,当需要调用动态库中的函数或访问其中的数据时,操作系统才将其加载到内存中

  • 例如,想象一个图书馆(操作系统),静态库就像是把一些书籍(代码)复印后直接装订到每一本需要的杂志(可执行文件)里;而动态库则是把书籍放在图书馆的书架上,当杂志(可执行文件)的某一页(程序运行过程中的某个调用点)需要参考这些书籍中的内容时,再从书架(操作系统的内存)上把书取出来查看。

动态库的制作

g++ -fPIC -c xxx.c生成.o文件

fPIC:产生位置无关码(position independent code)

g++ -shared -o xxx.so xxx.o

带有动态库文件的编译

g++ -o 最终可执行文件名 xxx.cpp -I(不需要加空格,参数是要在哪个目录下寻找头文件,例:当前目录就是 -I.,math 目录就是 -Imath) -L(不需要加空格,参数是要在哪个目录下寻找库,例:当前目录就是 -L.,math 目录就是 -Lmath) -l(库文件名,不需要在中间加空格,假设库文件名为 libmath.so 默认省略会 lib 和 .so 后缀,-lmath)

编译成功后二进制文件不能直接运行,因为当运行调用了动态库可执行文件时,程序会默认从/lib 和/usr/lib 目录下去找该动态库,若自己制作的动态库没有在这两个目录下,将会找不到动态库而报错

调用动态库的方式
  1. 将动态库放到/lib/usr/lib目录下
  2. 在 /lib 或 usr/lib 目录下建立和动态库库之间的软连接
  3. 修改环境变量 LD_LIBRARY_PATHexport LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/user/mylib(临时生效,只在当前终端会话有效)
  4. 修改.bashrc 配置文件,让环境变量永久生效
  5. 在 /etc/ld.so.conf.d 目录下新增动态库搜索的配置文件将自己的库文件的路径添加到该配置文件中,如mylib.conf,然后使用 ldconfig命令让配置生效即可

动态库的优点

节省内存和磁盘空间:

  • 内存共享机制:动态库在内存中可以被多个程序共享。当多个应用程序同时使用同一个动态库时,操作系统只会将该动态库加载到内存中一次
  • 减小可执行文件体积:由于动态库的代码不在编译时嵌入可执行文件,可执行文件的体积相对较小。这使得在磁盘存储上更加节省空间,特别是对于大型软件系统或者需要存储大量程序的设备而言,能够节省可观的磁盘资源

易于更新和维护:

  • 动态更新功能:动态库可以在程序运行过程中进行更新,而不需要重新编译整个程序软件开发者可以通过更新动态库来修复程序中的漏洞或者添加新的功能
  • 方便版本管理:在一个复杂的软件系统中,使用动态库可以更好地进行版本管理。不同版本的动态库可以共存,并且可以根据程序的需求灵活地选择使用

增强软件的灵活性和扩展性

  • 动态加载特性:程序可以在运行时根据需要动态地加载和卸载动态库。这使得软件能够根据不同的运行场景和用户需求灵活地扩展功能
  • 支持分布式开发和第三方集成:动态库有利于分布式开发,不同的开发团队可以独立地开发动态库和主程序,然后将它们集成在一起。同时,也方便了第三方开发者为软件提供扩展功能

动态库的缺点

运行时依赖和潜在的兼容性问题

  • 运行时依赖要求:程序运行时依赖于动态库的存在和正确版本。如果动态库丢失、损坏或者版本不兼容,程序将无法正常运行
  • 操作系统和硬件差异导致的兼容性问题:不同的操作系统版本或者硬件平台可能对动态库的支持存在差异。这可能导致在某些环境下动态库无法正常工作

性能开销(相对静态库)

  • 动态加载和链接的时间成本:与静态库在编译时就完成链接不同,动态库需要在程序运行时进行加载和链接操作。这会带来一定的时间开销,尤其是在程序启动阶段或者频繁加载不同动态库的情况下
  • 间接寻址和额外的系统调用开销:由于动态库中的函数地址在运行时才确定,每次调用动态库中的函数可能会涉及到间接寻址和额外的系统调用,这在一定程度上会降低程序的运行效率

动态库的链接过程

一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码

在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)

程序运行时会创建一个页表,用于当前进程的内存和物理内存之间进行转换,页表项中包含了该虚拟页面对应的物理页框号(如果该页面已经加载到物理内存中)、页面的访问权限(如可读、可写、可执行等)、页面是否在物理内存中的标志(存在位)等信息

动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,通过页表将内存中的动态库映射到每一个用到该动态库的进程中堆栈之间的共享区,当程序调用动态库中的代码时,会直接通过页表查找到内存中动态库的地址,节省了内存和磁盘空间

动态库被加载进内存时,动态链接器解析库函数的逻辑地址,操作系统会生成描述文件来对库文件进行管理,程序因此可以通过描述文件,通过库文件的名称依次遍历链表,来判断库文件是否被加载

  • 处理器根据虚拟地址执行指令或访问数据:
    • 若目标页已加载到物理内存(命中),通过 MMU 转换为物理地址并访问
    • 若目标页未加载(缺页,Page Fault):
      1. 触发缺页中断,操作系统从磁盘交换分区(Swap)将对应页加载到物理内存
      2. 更新页表映射,重新执行指令

mm_struct 中成员变量的初始值是由运行的程序决定的

平坦模式

优点:简化了内存访问的方式,进程可以使用简单的线性地址来访问内存,而不需要考虑多个段的基址和界限等复杂因素

虚拟内存支持:对于每个进程来说,它看到的是一个平坦的虚拟内存空间。当进程访问内存时,操作系统的内存管理单元(MMU)会将这个虚拟地址转换为实际的物理地址

在 Linux 中的可执行程序是 ELF 格式的,其 ELF 格式的文件头中就包含了代码段、数据段等的虚拟地址范围等信息。可执行程序编译后,会变成很多汇编语句,每一条汇编语句都会有自己的地址,这些地址是按照从全 0 到全 F 进行编址的,这些地址都是虚拟地址,这种编址方式就叫做平坦模式。当进程被创建并加载可执行程序时,ELF 的文件中的信息会被用来初始化mm_struct中对应的成员变量,以确定进程地址空间中代码和数据的布局

逻辑地址是在编译链接完成后直接生成的,而不是运行时生成的,链接完成后会在可执行文件中生成 main 函数的入口地址,加载器将库和可执行文件从磁盘加载到内存中的虚拟内存空间,对于静态链接的程序,其逻辑地址直接对应虚拟地址,对于动态库,其逻辑地址需通过基址 + 逻辑偏移量计算为虚拟地址,虚拟地址最终通过内存管理单元(MMU)和页表映射到物理地址

在程序启动时,动态库被加载到进程的虚拟内存空间中,并没有加载到物理内存中,当首次访问的时候才会通过缺页中断将其加载到物理内存中,库中的代码和数据的地址是相对于库文件起始位置的偏移量,称为逻辑地址此时逻辑地址与最终运行时的虚拟地址无关,当动态库被映射到进程的虚拟内存时,操作系统会为其分配一个基址,库中每个逻辑地址会被转换为虚拟地址 = 基址 + 逻辑地址偏移量,而静态库的逻辑地址在编译时直接合并到程序的逻辑地址空间(成为程序自身的绝对地址),无需基址偏移,每个进程独立加载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值