linux静态库与动态库整理

简化版本

静态库:

  • 制作:
gcc -g -c ./*.c -I ../include/
ar crs libmod.a *.o
  • 使用
gcc -g main.c -L ./ -lmod -I ../include/

动态库:

  • 制作:
gcc -g -c -fPIC -Wall ./*.c -I ../include/
gcc -g -fPIC -shared -o libmod.so ./*.o

or
gcc -g -fPIC -Wall ./*.c -I ../include/ -shared -o libmod.so

  • 使用
gcc -g main.c -L ./ -lmod -I ../include/

注意,要将库放入可以被系统搜索到的位置,详情见下面的动态库使用


测试代码

下面是测试使用的代码

目录结构

├── include                ## 库头文件
│   └── mod.h
├── source                 ## 库文件
│   ├── add.c
│   ├── div.c
│   ├── mul.c
│   └── sub.c
└── test                   ## 测试代码
    └── main.c

库 头文件

库是一个非常简单的例子 就是加减乘除,但是这里我们重点是制作库 而不是里面的函数。

mod.h

int add(int a,int b);
int sub(int a,int b);
int mul(int a,int b);
int div(int a,int b);

add.c

#include "mod.h"

int add(int a,int b){
        return a+b;
}

sub.c

#include "mod.h"

int sub(int a,int b){
        return a-b;
}

mul.c

#include "mod.h"

int mul(int a,int b){
        return a*b;
}

div.c

#include "mod.h"

int div(int a,int b){
        return a/b;
}

test.c

#include "mod.h"
#include <stdio.h>
int main(){
        printf("hello world\n");
        printf("a + b = %d\n",add(1,3));
        printf("a - b = %d\n",sub(1,3));
        printf("a * b = %d\n",mul(1,3));
        printf("a / b = %d\n",div(4,3));
        return 0;
}

静态库

静态库简介

静态库被称为归档文件,是UNIX系统提供的第一种库。
实际上就是简单的一个普通的目标文件的集合,一般来说习惯用“.a”作为文件的后缀。
可以用ar这个程序来产生静态函数库文件。Ar是archiver的缩写。

  • 可以将一组进程被用到的目标文件整合为单独的库文件,这样就可以使用它构建程序时,无需重新编译原来的源代码。
  • 链接命令变得更为简单,只需要指定静态库名称即可。链接器知道如何搜索静态库并将可执行程序需要的对象抽取出来。

静态库函数允许程序员把程序link起来而不用重新编译代码,节省了重新编译代码的时间。不过,在今天这么快速的计算机面前,一般的程序的重新编译也花费不了多少时间,所以这个优势已经不是像它以前那么明显了。静态函数库对开发者来说还是很有用的,例如你想把自己提供的函数给别人使用,但是又想对函数的源代码进行保密,你就可以给别人提供一个静态函数库文件。理论上说,使用ELF格式的静态库函数生成的代码可以比使用共享函数库(或者动态函数库)的程序运行速度上快一些,大概1-5%

静态库优劣

  • 优点:
    1. 隐藏代码内部细节,不需要暴露源码
    2. 无需重复编译静态库源码
    3. 无需外部链接,静态库会被编译进可执行程序内部,编译程序完成后,静态库可以删除
  • 缺点:
    1. 静态库编译进可执行程序内部,可执行程序占用更多的磁盘空间
    2. 运行后的可执行程序,相同的静态库在代码段各自有一份,造成内存空间浪费
    3. 升级静态库,需要重新编译所有的可执行程序,无法模块化升级。

静态库命名规则

libname.a

  • lib: 固定名称
  • name: 库名
  • .a :固定后缀

静态库制作

静态库的制作十分容易(相比动态库),仅需要以下指令。

  1. 将源码程序编译为.o文件

cd到source目录下,执行以下语句

gcc -g -c ./*.c -I ../include/
  • -g :添加调试信息
  • -c :编译为.o文件
  • -I : 指定头文件目录
  1. 将.o文件打包为静态库
ar crs libmod.a *.o
  • ar : 静态库打包命令
  • crs:可选参数
    • c 不在必须创建库的时候给出警告
    • r 替换归档文件中已有的文件或加入新文件
    • s 创建归档索引 (cf. ranlib)

后面跟库名称,以及.o文件

静态库使用

静态库的使用也很简单,只需要编译的时候注意一下链接就好。
cd 到test目录下

gcc -g main.c -L ../static/ -lmod -I ../include/
  • L 指定库目录
  • l(小L) 指定库名
  • I(大i) 指定头文件目录

静态库注意

关于静态库需不需要头文件的问题?

不一定需要。创建一个库一般处于以下两种目的:

  1. 把一些相关的代码,打包成一个库,发布给其它的人用。
    这中情况是最常见的情况,如写 C 语言用到 libgcc。在这种情况下,你除了提供库文件:静态库[ windows 下 .lib,linux .a];动态库:[Windows 下 .dll,Linux 下 .so] 之外,必须提供头文件。头文件是你这个库里面提供了那些接口可以供外界使用。 如果没有头文件,其他人无法使用,因为不知道函数方法的原型!

  2. 在为某些软件项目写插件,而这些项目软件是公司内部的;或者说自己相对熟悉可接触的,即然是可以直接得知可能用到的函数方法的原型(函数名,参数列表,返回值等)的;就没有必要单独列出头文件,直接作为库使用也是可以的;很多大的项目,都是模块化设计,留有一些特定的接口,方便定制。当程序运行时,会动态加载指定目录下的动态库,运行时调用动态库里面约定好的方法。这种情况无需提供头文件,但要按照特定的约定来实现这个库。

总之:
当调用方还不知道不清楚函数原型的时候:动态库中的函数方法的原型(函数名,参数,返回值等)的情况下;

  • 代码编写时候

调用方是不知道如何使用该库的,所以是需要头文件帮助,来编写调用代码的;尤其是用到了头文件中声明的类;类型和相关变量,相关函数;
然后库文件存在就可以直接调用.

  • 代码编译时

如果是静态调用;无论是静态库或动态库,都是需要库的头文件参加编译的;
如果是动态加载动态库(dlopen/load等方法),则不需要头文件,只需要库文件.前提是调用方知道函数名和参数列表,返回值等信息,方可正确调用;

  • 代码运行时

运行时,无论静态库还是 动态库,都不需要头文件;

参考链接:https://blog.csdn.net/weixin_42073232/article/details/110531589

静态库相关命令

ar命令详情见man手册和help

  • 查看库内模块
liuwh@liuwh-PC ~/D/l/source> ar tv libmod.a
  • 替换库内模块
liuwh@liuwh-PC ~/D/l/source> ar r libmod.a add.o
  • 删除库内模块
liuwh@liuwh-PC ~/D/l/source> ar d libmod.a mul.o
  • 追加库内模块
liuwh@liuwh-PC ~/D/l/source> ar q libmod.a mul.o 

动态库

动态库简介

简单说,动态库是为了解决静态库的痛点出现的。

动态库的关键思想是,动态库被所有需要这些模块的程序共享,动态库不会像静态库一样被复制到链接的可执行程序中,相反,当第一个需要动态库中的模块的程序启动时(通常第一次加载动态库的程序会慢一点,后面的程序启动时不需要加载会快一些),库的副本才会被加载到内存中。

当后续进程需要该动态库中的模块时,可以直接使用已经加载到内存中的库的副本(放心,不会造成内存冲突,对于库内的变量都有各自的地址)。这样的好处是可执行程序需要的磁盘空间和虚拟内存空间减少了。

ps:为什么我会说模块呢?因为假如一个动态库有三个.o文件组成,那么只有在调用其中某个.o文件时才回去找该文件内的符号地址,不会一次性将三个模块都加载进来。(这是根据资料推测的,有待证实 😄 )

动态库优劣

  • 优点:
    1. 隐藏代码内部细节,不需要暴露源码
    2. 无需重复编译源码
    3. 可执行程序变得更小,节省磁盘空间,运行时内存使用更小
    4. 更新动态库不需要重新编译可执行程序,因为链接不变,甚至可以做到热更新(插拔),可以做到模块化更新
  • 缺点:
    1. 与静态库相比,不可删除库
    2. 动态库比静态库编译更加复杂
    3. 动态库编译时必须使用位置独立的代码,在大多数架构上带来性能开销(需要使用额外的寄存器)
    4. 运行时必须执行符号重定位,重定位造成时间浪费。(符号重定位是为了将动态库中的符号(变量或函数)的引用修改为在虚拟内存中的实际位置)

动态库命名规则

与静态库不同的是,由于是动态加载库,为了便于更新所以要有一套命名规范。
linux下动态库命名规范都是lib开头 .so结尾,加载动态库或静态库时,可不写lib和.so

在这里插入图片描述
真实名称在这里插入图片描述

  • 主版本号:共享库每个不兼容的版本通过唯一的主要版本标识来区分,依赖于旧的库版本的程序,如果使用新的版本号的库,需要进行相应修改并且重新编译链接。
  • 次版本号:主要是表示进行了增量升级,修改包括新增符号,并且保持原有符号不变。高的次版本号,向后兼容主版本号相同,但是次版本号较低的库文件 [[#3]]。一般情况下,次版本号是奇数为开发版,偶数为稳定版。
  • 发行/修订版本号(可选):主要是进行bug的修正和性能的改进等。由于不添加新的接口,因此主版本、次版本号相同但是发布版本不同的库完全兼容

so-name

so- name包含相应的真实名称中的主版本号,但不包括次版本号和发布/修订版本号。
一个主版本对应一个so-name,因此so-name的命名形式为libname.so.主版本号

在编译动态库时,可以添加编译选项增加so-name名称

gcc -g -fPIC -Wall ./*.c -I ../include/ -shared -Wl,-soname,libmod.so.1 -o libmod.so.1.1

ps: -soname后面接soname名称l-soname选项前要加-Wl选项,Wl选项后的内容会作为参数传递给链接器

查看动态库的soname

liuwh@liuwh-PC ~/D/l/share> readelf -d libmod.so.1.1.1 

Dynamic section at offset 0x2e40 contains 22 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libmod.so.1]  //可以看到动态库的soname是libmod.so.1
 0x000000000000000c (INIT)               0x1000
 .......
 .......
 0x0000000000000000 (NULL)               0x0

加载该动态库的可执行程序上,也有一个soname的标签。

liuwh@liuwh-PC ~/D/l/share> readelf -d a.out 

Dynamic section at offset 0x2e10 contains 25 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[libmod.so.1]
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
 0x000000000000000c (INIT)               0x401000
 0x000000000000000d (FINI)               0x4012b4
 .......
 .......
 0x0000000000000000 (NULL)               0x0
liuwh@liuwh-PC ~/D/l/share> 

而一般来说会创建一个软连接指向该动态库,也就是说,当可执行程序去链接动态库时候,链接的其实是soname指向的动态库。

lrwxrwxrwx 1 liuwh liuwh    15 1124 10:05 libmod.so.1 -> libmod.so.1.1.1*
-rwxr-xr-x 1 liuwh liuwh 17824 1124 10:04 libmod.so.1.1.1*

那么,当次版本号需要更新的动态库可以直接放入原动态库位置,只要将libmod.so.1指向的动态库位置变更一下即可,其他可执行程序都不需要修改链接了,完美!

但是如果有的程序必须要用低的次版本的库怎么办呢?那就在链接时直接指定真实的库就行吧,不要指定soname了。指定方式见下文。

如果需要手动维护每一个库的次版本更新问题,那么可太傻了,有一个命令叫做ldconfig,当系统中安装或更新一个共享库时,需要运行这个工具,它会遍历默认所有共享库目录,比如/lib,/usr/lib等,然后更新所有的软链接,使它们指向最新共享库。

链接名称

按理来说,有了soname已经可以解决动态库更新的问题了,但是 总有一些时候,更新的库不兼容之前的版本,那么这个版本的库就是主版本更新了。
像是这样情况,soname已经无法解决这个问题,所以引入了第三层,链接名称。

链接名称指向最新的soname,这样当主版本更替时,应用程序仍然可以找到动态库。

lrwxrwxrwx 1 liuwh liuwh    11 1124 10:05 libmod.so -> libmod.so.1*
lrwxrwxrwx 1 liuwh liuwh    15 1124 10:05 libmod.so.1 -> libmod.so.1.1.1*
-rwxr-xr-x 1 liuwh liuwh 17824 1124 10:04 libmod.so.1.1.1*

当必须使用不同主版本时,和soname一样,链接时指定低版本的soname链接即可,指定方式见下文。

动态库制作

首先我们仍然使用最开始的目录结构

  1. cd到source目录下 执行如下代码,将.c文件编译为.o文件
gcc -g -c -fPIC -Wall ./*.c -I ../include/
  • -g :添加调试信息
  • -c :编译为.o文件
  • -I : 指定头文件目录
  • -fPIC:指定编译器生成位置独立的代码.
  • -Wall: 编译后显示所有警告.
  1. 创建动态库
gcc -g -fPIC -shared -o libmod.so ./*.o 
  • -shared:表明产生共享库(动态库)
  • -o:指定输出名称,见命名规则

其实上面的两句话可以合成一句话

gcc -g  -fPIC -Wall ./*.c -I ../include/ -shared -o libmod.so

下图是共享库被加载进程序的过程,可以看一下在这里插入图片描述

在这里插入图片描述

动态库使用

  1. cd 到test目录下执行
gcc -g main.c -L ../source/ -lmod -I ../include/
  1. 查看生成了a.out文件,运行
liuwh@liuwh-PC ~/D/l/test> ./a.out 
./a.out: error while loading shared libraries: libmod.so.1: cannot open shared object file: No such file or directory

what?! 找不到动态库?
当然找不到了,看一下linux系统搜索动态库的路径以及顺序。

  1. 编译目标代码时指定的动态库搜索路径,指定了dt_rpath没有指定dt_runpath情况下搜索rpath目录;
  2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径,注意如果可执行程序制定了suid或sgid,就会忽略该变量,为了防止用户欺骗动态链接器加载一个同名的私有库;
  3. DT_RUNPATH指定的目录
  4. 配置文件/etc/ld.so.conf中指定的动态库搜索路径;
  5. 默认的动态库搜索路径/lib;
  6. 默认的动态库搜索路径/usr/lib。

看到上面的顺序 就知道必须通过这六种方式让动态链接器找到相应的库。

说一下推荐的方式,个人最不推荐方式2,除非测试,否则不应该使用该方法。
方式5和6都有可能和系统库冲突,但是确认没有冲突的情况下可以。
1,3,4都可以自己指定路径,其中4的话需要修改ldconfig的配置文件。
第一种方式可以创建类似windows的发布程序,详情见附录里的rpath

个人推荐在不影响系统情况下,应该在/etc/ld.so.conf/中配置/usr/local/lib/“动态库名” 目录,然后执行ldconfig,或者放在/usr/local/lib下也是可以接受的

动态库注意

关于链接动态库,有以下几个点需要注意,我只给出关键词,因为内容过于庞大,我暂时没有能力整理出来

rpath

静态编译

ldconfig的配置文件

动态库相关命令

ldd

objdump

readelf

nm

ldconfig

gcc

ld

共享库高级特性

预加载共享库

LD_PRELOAD,是个环境变量,用于动态库的加载,动态库加载的优先级最高,一般情况下,其加载顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib。
参考链接:https://blog.csdn.net/m0_37806112/article/details/80560235

动态加载库(dlopen等)

参考链接 : https://zhuanlan.zhihu.com/p/463608159

控制符号可见性

Linux动态库的导出控制

不想看网页版本:

attribute ((visibility(“default”))) 默认导出

attribute ((visibility(“hidden”))) 隐藏

g++ -shared -o test.so -fPIC -fvisibility=hidden so.cpp
添加该编译选项,-fvisibility=hidden设置默认导出符号为隐藏

链接器版本脚本

简单讲可以创建一个链接器(ld)的脚本,控制链接器的行为。

可实现功能有:控制符号可见性,符号版本化等

这一章我大体看懂想做的事情,但是觉得暂时用不上,所以就不深入研究了。

初始化与终止函数

动态库连接时的初始化函数:https://blog.csdn.net/qq_37061368/article/details/117709236

监控动态链接器 LD_DEBUG

一句话简介:监控动态链接器的操作,出于安全考虑,该功能对于suid,sgid进程无效

liuwh@liuwh-PC ~/Desktop> LD_DEBUG=help date

Valid options for the LD_DEBUG environment variable are:

  libs        display library search paths
  reloc       display relocation processing
  files       display progress for input file
  symbols     display symbol table processing
  bindings    display information about symbol binding
  versions    display version dependencies
  scopes      display scope information
  all         all previous options combined
  statistics  display relocation statistics
  unused      determined unused DSOs
  help        display this help message and exit

To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.


LD_DEBUG环境变量的有效选项包括:

libs       显示库搜索路径
reloc      显示重定位处理
files      显示输入文件的进度
symbols    显示符号表处理
bindings   显示有关符号绑定的信息
versions   显示版本相关性
scopes     显示范围信息
all        所有的选项组合
statistics 信息显示重新定位统计信息
unused     确定未使用的DSO
help       显示此帮助消息并退出

可以使用LD_DEBUG_OUTPUT环境变量指定文件名,将调试输出定向到文件而不是标准输出。

静态库 VS 动态库

简单讲,动态库解决了静态库的大部分痛点,但是自身也会有相应的问题出现,总的来说利大于弊。

动态库静态库
是否需要依赖否,无需外部依赖,所有源码都在库内
编译体积
更新库是否需要重新编译程序
内存中是否共享否,每个进程都有一份,浪费内存空间
运行速度稍慢相同代码情况下,比动态库快1%~5%(理论)
编译程序后,库是否可以删除不可,动态加载可以
运行时是否需要链接需要不需要

附录

位置独立的代码

在这里插入图片描述

https://blog.csdn.net/happythanago/article/details/53463517

gcc -Wall

https://blog.csdn.net/u014044624/article/details/115221092

库兼容条件

满足以下条件时,表明修改的库向前兼容

  • 库中所有公共方法和变量语义保持不变,对全局变量及返回参数产生的影响不变(修复bug,提升性能)
  • 没有删除公共api中的函数及变量,增加公共api或变量
  • 每个函数中分配的结构及返回的结构不变,库中导出的公共结构不变。

关于库中导出的公共结构,有一个特例,就是提前在公共结构中放入一些填充字段,这种情况下可以做到兼容。

ldconfig

更新动态库soname,同时可以将动态库放入ldconfig可以搜索到的路径下,这种情况下动态库也能被加载成功。

https://blog.csdn.net/winycg/article/details/80572735

rpath

RPATH & $ORIGIN实战: https://juejin.cn/post/6899366375987511303

静态编译

static link:关于gcc连接静态库的几种方式: https://cloud.tencent.com/developer/article/1433457

验证动态库搜索路径

https://www.cnblogs.com/AndyJee/p/3835092.html

glibc编译测试

如何使用新的glibc来编译自己的程序

动态库冲突

linux c解决多个第三方so动态库包含不同版本openssl造成的符号冲突: https://blog.csdn.net/found/article/details/105263450

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值