Linux之动静态库

本文详细解释了静态库和动态库的区别,包括它们在编译和运行时的工作原理,以及如何生成和使用静态库和动态库。还讨论了位置无关码、共享库和ELF格式在动态链接中的作用。
摘要由CSDN通过智能技术生成

动静态库

我们知道C语言库中的代码想要使用必须要包含头文件, 头文件中有库函数的声明, 而定义在库文件中.

介绍 

静态库是指程序在编译链接的时候把库的代码链接到可执行文件中, 程序运行的时候将不再需要
态库, 这个被链接的库就是静态库,  在Linux下静态库的后缀名一般为.a, Windows下为.lib

动态库与之相反, 程序在运行的时候才去链接动态库的代码, 多个程序共享使用库的代码.
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表, 而不是外部函数所在目标文件的整个机器码, 在Linux下动态库一般后缀名为“.so”,Windows下为.dll

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

  • 动态库可以在多个程序间共享, 所以动态链接使得可执行文件更小, 节省了磁盘空间. 操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用, 节省了内存和磁盘空间。

静态库的生成和使用

1. 生成可执行文件

先写两个简单的加减函数:

Add.h

#pragma once
 
 
extern int Add(int,int);

Add.c 

#include "Add.h"
 
int Add(int x,int y)
{
    printf("entrt Add func,%d+%d=?\n",x,y);
    return x+y;
}

Sub.h 

#pragma once
 
 
extern int Sub(int, int);

Sub.c 

#include "Sub.h"
int Sub(int x, int y)
{
    printf("entrt Add func,%d-%d=?\n",x,y);
    return x-y;
}

这两个加减函数就是我们想生成的静态库, 而下面的main函数仅仅是测试, 不能包含在静态库中. 

main.c 

#include "Add.h"
#include "Sub.h"
 
int main()
{
    int a = 10;
    int b = 20;
 
    int ret = Sub(a,b);
    printf("Sub result: %d\n", ret);
    ret=Add(a, b);
    printf("Add result: %d\n", ret);
}

头文件对应的查找是在当前目录下或者系统的指定目录下, 所以编译的时候不需要带头文件:

gcc -o Test Add.c Sub.c main.c

在编译多文件项目时, 一般都建议先将 源文件.c 编译成 .o文件,  然后将所有的 .o文件 链接为可执行文件 ,这样做的好处是我们现在只形成一个可执行文件, 而如果一次想执行很多可执行文件, 而它们用到的源代码都是不同的, 所以只需要把不同的 .o 文件组合链接即可:

makefile:

目前我们是先把所有.c源文件编译成 .o文件 然后和 main.o 链接形成可执行文件, 现在我不想给用户提供一整套源文件, 只想提供所有源文件的.o, 然后不管main文件是什么只需要生成自己的可执行文件, 然后和 .o文件 链接即可.

但是如果依赖的.o文件太多, 写起来很麻烦, 所以就想把所有的 .o文件 打包在一起, 所以就有了库.

2. 生成静态库

指令: ar + -rc + [将生成的静态库名称] + [需要集合的.o文件] (静态库名称的前缀是lib后缀是.a,中间部分就是库的名字)

例如: ar -rc libmymath.a my_add.o my_sub.o

ar是GNU归档工具, rc表示 replace and create , 如果存在就替换, 不存在就创建.

 makefile:

所以静态库本质就是将库中的源代码直接翻译成.o目标二进制文件, 然后打包. 

 3.用库生成可执行程序

当前目录下只有一个 main.c, 依赖的文件全都在test里. 

然后把所有的头文件和刚才生成的库拷贝进来, 但是直接编译会链接报错:

 因为我们写的库叫作第三方库, gcc默认是不认识的, 即使是在同一个目录下.

所以在生成可执行文件时, 有几个细节需要特别注意, 需要在编译时加几个选项:

  • -小写l 指定要链接的库的名称
  •  -L 指定要链接的库的路径
  • -大写i 指定头文件路径
  • 如果链接第三方库, 必须指明库名称, 比如libmymath.a 需要去掉前缀和后缀 --> mymath

不正规的做法: 把头文件库文件全拷贝到当前目录下, 所以-L为"." -l为"mymath", 不用加-大写i:

上面的方式太冗杂, .h和.a全都在一起, 正规的做法是把头文件库文件分别按目录安排好:

 把这个mymanthlib变成压缩包, 然后拷贝到main.c目录下, 再解压, 模拟了从网络上下载的动作 :

为了更好地演示, 新建了一个main目录, 把main.c拷贝进去, 然后经过一系列操作将mymathlib拷贝解压到main.c目录下:

修改一下-L和-大写i选项, -l选项依然是库名称:

4. 安装与卸载 

操作系统链接库需要对应目录, 一般 Linux 系统把/usr/lib和/lib64两个目录作为默认的库搜索路径, 使用这两个目录中的库时不需要进行设置搜索路径即可直接使用.

如果我们直接把刚才写的 头文件 和 .a文件 分别拷贝到这两个目录下, 生成可执行程序就只需要指明需要使用的库.

gcc main.c -l mymath

库和头文件拷贝到默认路径下就是我们常说的安装库, 而把默认路径库和头文件删除, 这个过程就叫做卸载库.

此外还有几个问题需要说明:
1. ldd选项查看的是动态库, 静态库不会显示:

 2. gcc静态链接选项是 -static, gcc时的 -static 选项是所有的库必须全部用静态链接, 如果要链接的库没有静态链接直接报错; 而gcc不加-static默认是动态链接, 形成的可执行文件能动态链接就动态链接, 不能就是静态链接, 所以可以出现动静态库混杂式地链接.


动态库的生成和使用

1. 动态库的生成

动态库的生成不使用ar命令, 而是在gcc命令的-c和-o中分别添加两个选项:

  • -fPIC: 产生位置无关码(position independent code)
  • -shared: 表示生成共享库格式

将.c文件编译为.o文件, 不仅要-c选项, 还要加上 -FPLC 的生成路径无关码.

将.o文件链接生成可执行程序, 不仅要-o选项, 还要加上 -shared 表示生成共享库.  

make 并 make output之后, 将目录拷贝到main目录下, (改名是为了防止和之前的静态库冲突).

然后用动态库生成可执行程序, 但是当我们运行它时却会报错, 找不到.so文件, 为什么? 我们不是给gcc传递了-lmymath去指定要链接的库名吗?

因为-lmymath选项是传递给gcc编译器的, gcc编译形成了可执行文件后, 程序编译工作就结束了, 但动态库的链接是在程序运行中的, 而程序运行的过程和gcc无关, 程序运行是操作系统来管理, 所以需要操作系统能找到动态库. 

总结就是: 只要动态库文件没有在系统路径下, 操作系统就无法找到.

2. 解决动态库的动态搜索问题: 

方法一: 最简单粗暴的方式是直接将库拷贝到默认搜索的路径

 这也是安装其他第三方库最推荐的做法, 编译时直接带上-l选项即可. (安装自己的库就不推荐这种做法了)

卸载:  

 方法二: 在系统的默认路径中添加该库文件的软链接

在此之前, 如果我们在当前目录建立一个软链接, 发现也是可以运行的, 这说明静态库默认不会在当前目录下寻找, 动态库默认会在当前目录下寻找:

现在在系统的默认路径中添加该库文件的软链接, 注意要用绝对路径:

方法三: 可以将库路径添加到环境变量LD_LIBRARY_PATH中:

但是改变环境变量只是改变内存的内容, 关机重启内存中的内容就都不存在了.(具体参考环境变量章节)

方法四: 更改系统关于动态库的配置文件 

1. 进入系统目录


2. 在当前配置文件下新建文件


3.进入Test.conf添加动态库路径: sudo vim test_mymath.conf, 在文件里添加库路径:


4. 更新动态路径缓存: sudo ldconfig

3. 同一组库, 提供动静态两种, gcc默认使用动态库

现在提供一个动静态库都有的mymathlib_mix:

 同一组库, 提供动静态两种, gcc默认使用动态库.

4. 使用外部库--ncurses库

(1) 什么是ncurses库

ncurses库 (new curses)是一套编程库, 它提供了一系列的函数以便使用者调用它们去生成基于文本的用户界面.

(2) 安装

使用yum安装:

sudo yum install ncurses-devel

(3) 测试 

复制一段别人写好的源码测试一下:  

【Linux】基于Ncurse图形库的贪吃蛇(C语言)_ncurses库-CSDN博客


 动态库的加载

接下来要更深入地谈一谈动态库在内存中的加载.

上面提到编译动态库的时候需要加 -fPIC(位置无关码), 那位置无关码是什么呢?

1. 谈PIC之前, 首先来谈一谈可执行程序, Linux下的可执行程序格式-----ELF格式(Executable and Linkable Format)

  • ELF中包含了比如我们熟悉的代码段, 只读数据区, 已初始化全局数据区, 未初始化全局数据区, 静态库在源代码翻译的过程中就直接被拷贝到代码段了, 可执行程序怎么加载, 静态库就怎么加载. 动态库并不是, 第4点会介绍.
  • 此外还有文件头其中包含了文件的基本信息, 如文件类型、目标体系结构、入口点地址(main)等. 文件头通常位于文件的起始位置, 并且大小为固定的字节数, 还有程序头表, 节表. 
  • 动态链接相关的是ELF中有一张符号表: 符号表通常用于动态链接, 调试等, 符号表存储了程序中的符号信息, 如函数名, 变量名等, 比如会表示printf : xx.so address, 通过符号表能知道函数在哪些库里面, 可以理解成把库里用到的函数的地址(其实是位置无关码)填进符号表, 所以将可执行程序与库链接起来, 这个动作就叫动态链接

链接的库要被加载进内存:

可执行程序是要被加载进内存的, 但是此时内存中没有具体的库函数的实现, 只有函数的地址, 所以必须在使用依赖的库函数之前要先把库加载进内存. 所以对于动态链接的程序, 不仅程序自己要加载, 所链接的库也要被加载

2. 程序没有被加载, 程序内部有地址吗?

有, 程序中的变量名, 函数名编译成为二进制之后, 符号就变成了地址.

既然有了地址在代码中如何编址呢?

编译的时候, 对代码进行编址, 基本遵循虚拟地址空间的做法, 所以虚拟地址空间不仅仅是操作系统的概念, 因为编译器进行编译的时候, 要按照这样的规则编译可执行程序, 这样才能在加载的时候, 进行从磁盘文件到内存的映射. (所以其实在可执行程序还没有被加载进内存之前, 代码和数据就已经具备了一种虚拟地址的概念). 在磁盘中, 这种地址一般称为逻辑地址, 逻辑地址的取值范围是[0~0xFFFFFFFF], 因为Linux中对可执行程序的编址方式平坦模式.

平坦模式

平坦模式: 平坦模式下, 段基址设置为0, 段限长设置为最大值(4GB或64TB), 从而使得程序可以直接使用线性地址进行内存访问, 比如(32位下, 0x00000000到0xFFFFFFFF,共4GB), 而不需要进行基址和偏移量的计算(x86架构中的最初的工作模式---实模式). 可以理解成段基址为0, 偏移量为0xFFFFFFFF(32位)计算得到的.

3. 绝对编制和相对编址

位置无关码

如果所有的动态链接库都使用固定的基地址(绝对编制), 那么当多个动态链接库同时加载到内存时, 可能会出现地址冲突的情况, 因为要加载的库很多, 无法确定哪一个库就应该被加载在哪个地方, 所以操作系统会在加载动态链接库时选择一个空闲的基地址, 而库函数只需要记录位置无关码, 也就是库函数距离库基地址的偏移量, 通过基地址+偏移量的方式去访问库函数.

绝对编址比较适合固定内存布局, 如代码区、数据区等在程序运行时是固定的内存布局(平坦模式), 相对编制比较适合位置无关性动态内存的情景, 比如分配库函数地址.

4. 动态库也要被加载进内存

 4.1 库被加载之后, 要被映射到指定使用了库的进程的虚拟地址空间的共享区部分:

但是这些库被映射到共享区位置的地址并不是固定的, 我们需要让库在共享区的任意位置都能被任意加载. (第3点也提到过)

4.2 有了位置无关码, 可执行程序中就不需要把库/库中的函数加载进可执行程序, 而是在符号表中记录所使用到的库的位置无关码即可:

 所以一旦库被加载之后, 库的位置就确定, 库中函数的位置也就确定了, 此时动态库中的函数与普通的函数没有区别, 调用函数本质都是在地址空间内做跳转, 只不过普通函数执行时做跳转的距离短(因为都在正文代码段里), 而库函数执行所做的跳转距离长(库函数在共享区).

 4.3 共享库

当执行其它可执行程序时, 也用到之前已经被加载进内存的库, 这个库在整个系统里只会存在一份, 所以动态库也叫共享库. 但是如果被100个可执行程序都会使用一个静态库, 那么系统里就会存在100份这样的库, 很浪费空间.

4.4 可执行程序的执行

 先从ELF文件中entry找到main函数的地址加载进CPU, CPU去页表寻找对应的物理地址, 程序开始执行:

 call到下一个方法时, 再把这个地址加载进CPU, CPU继续去页表寻找虚拟地址:

 程序还没有正式运行时, 页表的映射关系已经建立好, CPU先将程序入口地址通过页表映射找到物理地址开始执行, 读取下一条指令时, 继续使用call的虚拟地址, 再经过页表映射去找物理地址... 所以CPU进行访存时, 都需要经过页表实现虚拟地址转物理地址的过程.

 总结:

所以一个可执行程序要运行, 在编译时就已经处理好了虚拟地址, 加载的时候可执行程序内部的自己实现的代码和数据地址不变, 所以CPU读取代码的指令用到的地址都是虚拟地址, 而我们的代码和数据加载进内存后都有自己的物理地址, 所以 我们的代码和数据既包含虚拟地址又包含物理地址, 将页表提前构建好之后, CPU的指令寄存器就不断地进行 读取指令找到虚拟地址, 转化虚拟地址, 执行代码, 读取指令...的循环, 整个程序的运转就开始了.

地址空间的问题是从编译器就开始考虑了!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值