【Linux】:动静态库

朋友们、伙计们,我们又见面了,本期来给大家带来动静态库相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

目录

1. 静态库

1.1 静态库的创建

1.2 静态库的使用

①朴素做法:

②简便做法:

2. 动态库

2.1 动态库的创建

2.2 动态库的使用

① 安装(拷贝)至系统指定目录 

② 使用软链接

③ 通过环境变量查找

④ 更改系统配置文件

2.3 安装外部库

3. 动态库的加载(原理) 

① 库和程序都要加载

② 绝对编址和相对编址 

③ 基于地址空间理解加载


1. 静态库

静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库(静态库不需要加载);静态库的前缀以lib开头中间是库名称以.a为后缀结束。

库中是没有main函数的,我们也不能把main函数写入库中! 

将所有头文件和.o文件打包就叫做一个库。

1.1 静态库的创建

实现一个简单的计算功能的代码:包含 + - *功能的计算。

// 源Add.c
#include "Add.h"

int Add(int x,int y)
{
    return x + y;
}
// 源Sub.c
#include "Sub.h"

int Sub(int x,int y)
{
    return x - y;
}

// 源Mul.c
#include "Mul.h"

int Mul(int x,int y)
{
    return x * y;
}

// 源Test.c
#include "Add.h"
#include "Sub.h"
#include "Mul.h"

int main()
{
    int x = 10, y = 20;
    printf("%d + %d = %d\n", x, y, Add(x, y));
    printf("%d - %d = %d\n", x, y, Sub(x, y));
    printf("%d * %d = %d\n", x, y, Mul(x, y));
    return 0;
}

我们可以对全部的.c源文件一起编译形成一个可执行程序:

但是我们不建议将所有的源文件一起编译成可执行程序,通常先将所有的.c文件预处理、编译、汇编形成.o文件,然后使用哪几个源文件就将哪几个链接在一起(所以在VS2019中可以看到许多的.obj文件)。

gcc -c 源文件:将源文件汇编形成.o二进制目标代码;

为了方便,我们直接使用Makefile来自动形成.o文件和链接.o文件形成可执行。

Makefile文件:

Test:Add.o Sub.o Mul.o Test.o #将所有.o文件链接在一起形成可执行
	gcc -o $@ $^
%.o:%c       #将所有的.c文件汇编形成.o文件
	gcc -c $<

.PHONY:clean
clean:
	rm -f *.o Test

如果我们要形成一个库,只需要将.o文件和头文件保留,然后打包,如果用户有别的程序要使用,先将用户的源代码汇编形成.o文件,然后再和我们库中的.o文件链接即可。

静态库的创建:ar -rc lib库名称.a .o文件...

// 生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o

为了方便,在Makefile中也可以使用生成静态库的脚本:

Makefile文件:

static-lib=libmymath.a 

$(static-lib):Add.o Sub.o Mul.o
	ar -rc $@ $^

%.o:%c      
	gcc -c $<
	
.PHONY:clean
clean:
	rm -f *.o *.a

静态库的本质:将库中的源代码直接翻译成为.o目标二进制文件,然后将其打包! 

1.2 静态库的使用

当我们其他路径的程序要使用我们打包的库该怎么使用呢?

①朴素做法:

  • 先把所有的头文件拷贝到当前目录;
  • 再将打包的库拷贝到当前目录。

此时编译时会发现链接式错误,这是为什么呢?

我们所形成的静态库是第三方库,gcc编译器默认是不认识的,所以要是用指定链接某一个库的选项:

gcc 源文件 -l库名称 -L.

  • -l表示指定要链接哪一个库;
  • -L.表示在当前目录下查找这个库

所以我们要使用静态库需要用到.h和.a; 

  • 我们可以使用ldd 指令可以查看当前可执行程序链接的库:

明明我们已经链接,但是为什么还是没有呢?

因为静态库在使用时已经被拷贝到可执行程序了,所以查不到静态库!

  • 当我们在编译形成可执行程序时,需要用到我们的静态库,那么需不需要加上static呢?

加上-static表示的是在形成可执行时全部链接的是静态库,如果没有静态库,就会报错,不带-static时,形成可执行程序时,能用动态链接的全都用动态链接,但是我们指定链接静态库也是可以的局部性的进行静态链接,其他库正常动态链接,动静态的混杂式链接是可以存在的。

②简便做法:

上述做法对于用户来说有点麻烦,我要使用库,还必须把头文件、.a文件全部拷贝到当前目录,所以为了方便,我们将.a和.h打包在一个文件中(将.h和.a导出),将.a和.h分别存储,在使用时直接将整个文件拷贝即可。

Makefile文件:

static-lib=libmymath.a 

$(static-lib):Add.o Sub.o Mul.o
	ar -rc $@ $^

%.o:%c      
	gcc -c $<

.PHONY:output
output:
	mkdir -p mymath_lib/include
	mkdir -p mymath_lib/lib
	cp -f *.h mymath_lib/include
	cp -f *.a mymath_lib/lib

.PHONY:clean
clean:
	rm -f *.o *.a mymath_lib

  • 接下来在使用的时候只需要将mymath_lib拷贝然后分路径查找使用即可;
  • gcc -I 路径:在编译时新增查找头文件的搜索路径
  • 安装库就是将对应的头文件拷贝到usr/include下,将对应的.a文件拷贝到usr/lib64目录下。

2. 动态库

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

2.1 动态库的创建

还是使用静态库使用的例子。

动态库也是一个库,所以它也是将.o文件打包,但是需要注意的是:在形成.o文件是使用的gcc选项是:gcc -fPIC -c .c文件

fPIC叫做与位置无关码,后续介绍。

在链接.o文件的时候也有所不同,链接.o文件使用的gcc选项是:gcc -shared .o文件...

shared: 表示生成共享库格式

为了方便,直接将动态库的创建由Makefile来自动完成:

Makefile文件:

dy-lib=libmymath.so 

$(dy-lib):Add.o Sub.o Mul.o
	gcc -shared -o $@ $^

%.o:%c      
	gcc -fPIC -c $<

.PHONY:output
output:
	mkdir -p mymath_lib/include
	mkdir -p mymath_lib/lib
	cp -f *.h mymath_lib/include
	cp -f *.so mymath_lib/lib

.PHONY:clean
clean:
	rm -rf *.o *.so mymath_lib

2.2 动态库的使用

我们把.o文件使用动态库的方法打包在了一起,那么该怎么使用动态库呢?

还是和静态库一样,把.h和.so打包在一起拷贝到当前目录,然后指定头文件路径、动态库路径编译可执行程序:

  • 但是当我们运行时,就会发现直接报错:

  • 接下来就介绍4种使用动态库的方法:

① 安装(拷贝)至系统指定目录 

我们系统默认搜索动态库的路径在/usr/lib64,所以我们只需要将我们要使用的动态库拷贝到该目录下即可运行:

cd /usr/lib64
sudo cp -rf 动态库路径 .

用法就和C语言的标准库一模一样,直接使用即可。

② 使用软链接

使用软链接,查找所要使用的动态库:

③ 通过环境变量查找

在我们Linux中环境变量中有一个环境变量用于加载库的:LD_LIBRARY_PATH

我们只需要将动态库的路径添加到它后面,然后在运行程序的时候,我们的动态库就会被自动加载:

④ 更改系统配置文件

环境变量的修改只是基于内存级别的,重新登录之后就恢复默认了,所以要想永久的改变,就需要修改系统环境变量的配置文件,此外还可以修改系统中关于加载动态库的配置文件:

/etc/ld.so.conf.d/

  • 所以直接在该目录下创建一个我们自己的文件,将我们的动态库的路径添加进去即可永久有效:
sudo touch /etc/ld.so.conf.d/ mylibpath
  • 然后打开mylibpath,在里面写入我们动态库所在的路径即可。

对于一组库,当库中同时存在动态库和静态库时,gcc编译器默认使用的是动态库。要使用静态库,需要加上static选项。 

2.3 安装外部库

系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses库)

sudo yum install -y ncurses // 安装ncurses库

安装之后就可以在我们系统默认路径下找到这个动态库:

使用这个库你可以让你的输出出现在终端任意的地方  

  • initscr(); //打开curses模式 进入你的终端
  •  endwin(); //关闭窗口stdscr
  • noecho();  //关闭回显 让键盘输入字符 不显示在终端上
  • echo() // 显示字符
  • cbreak(); // 接受单个字符处理  禁止行缓冲
  • keypad(WINDOW *, bool); //指定窗口 激活功能键  上下左右 F1 F2等
  • start_color();//打开color模式  
  • scroll(); //指定窗口  接受字符 超过一行自动写入下一行
  • getmaxyx(WINDOW *,int winHLineNumber, int winWLineNumber);//获取指定窗口行数 和行宽字符列数
  • newwin(int winHLineNumber, int winWLineNumber, int begin_x, int begin_y); //创建新窗口
  • subwin(WINDOW *,int winHLineNumber, int winWLineNumber, int begin_x, int begin_y)//创建子窗口  参数 1 父窗口   2 行高  3 行宽 4 基于父窗口 那行 起5 基于父窗口那列起
  • delwin(WINDOW *);释放窗口

3. 动态库的加载(原理) 

① 库和程序都要加载

Linux下形成的可执行程序都是ELF格式,这种格式下的可执行程序中有一张表,用于记录我们调用函数时使用到的动态库的入口地址,当我们的可执行运行的时候,就要将该地址和动态库进行链接,这种方式叫做动态链接当可执行被加载至内存时,此时动态库加载的只有一个地址,所以也要把对应动态库加载入内存,这就是为什么我们的代码已经编译行程可执行程序,但是运行的时候不专门链接动态库就运行不了的原因。

① 源代码在经过一系列编译之后形成了可执行程序,可执行程序没有被加载到内存的时候,程序内部是存在地址的!

② 变量名,函数名、符号等,在编译成为二进制文件的时候,变量名和函数名就不存在了,全都变成地址了!

③ 在编译的时候,对代码进行编址,基本遵守的是虚拟地址空间的方案。

④ 虚拟地址空间不仅仅是OS内部的概念,编译器在编译的时候,也要按照这样的规则编译可执行程序,这样子才在加载的时候,进行磁盘文件到内存,再进行映射关系的建立。

⑤ 我们的可执行程序在没有被加载的时候它的代码和数据就已经具有了虚拟地址,这个地址叫做逻辑地址(基地址 + 偏移量)。

⑥ 我们整个代码在编译的时候也采用基地址 + 偏移量的编址方式,只不过基地址默认是0,偏移量的范围[0,FFFFFFFF],这种以0为基地址的编址模式称为平坦模式。

② 绝对编址和相对编址 

这两种编址方式就类似于空间位置中的绝对位置和相对位置;

  • 绝对编址容易受起始位置的影响;
  • 相对编制只需要根据相对偏移量来确定具体地址。

我们的可执行程序如果按照绝对编址的方式,因为可执行程序在内存中加载的位置很可能是不一样的,如果程序的起始地址改变,可执行程序中的某一些函数方法地址就会收到影响;

如果按照相对编址的方式,可执行程序中的函数方法的地址只需要根据可执行程序从开始到现在的偏移量记录下来,所以不管可执行程序未来加载到什么位置,其中的函数地址只需要根据偏移量来确定。

我们在形成动态库的时候使用的选项fPIC叫做与位置无关码:

  • 相对编址适用于库中函数的编址。

所以不管未来库加载到内存的哪个位置,对它里面的函数地址是没有影响的,所以fPIC叫做与位置无关码!

③ 基于地址空间理解加载

可执行程序在要链接静态库时是直接把库中对应的方法拷贝到了自己的可执行程序的代码区中,直接以绝对编址的方式编址到了我自己程序的代码中,所以与静态库无关了,因此静态库不存在加载的问题。

我们编译好的可执行程序是以ELF格式存储在磁盘上的,前面说过对代码的编址模式也是按照虚拟地址空间模式编址的,所以可执行程序在没有被加载的时候也存在自己对应的代码区、数据区......当把数据和代码加载到内存中时,要经过页表的映射,从虚拟到物理的转化,此外也要将使用到的动态库进行链接(动态链接),然后把动态库也加载到内存,既然动态库也要被加载到内存,那么必然也要在虚拟地址空间有映射关系,动态库在被加载时会映射到对应进程的虚拟地址空间的共享区。

库可以在共享区的任意位置加载,并可以正确运行,一旦库加载之后,它的位置就是确定的。

将库加载至内存同时映射到共享区,那么具体该怎么使用呢?

在ELF格式的可执行程序中,会保存所使用到的库名和对应方法的偏移量(库地址:方法偏移量),在将可执行程序加载至内存中时,会将符号替换(库的起始地址替换掉库名称),在程序按照代码逻辑执行的时候,遇到库中的方法,首先根据库的起始地址找到指定的库,然后根据方法偏移量找到具体使用的方法。

  • 在我们代码中自己写的函数在跳转的时候只是在代码内部跳转,但是在调用库函数的时候在整个地址空间内进行跳转。
  • 动态库也叫做共享库,它不会因为使用它的程序数量的增加而不断往内存中加载,只会存在一份。
  • 我们自己的可执行程序会在特定的位置,记录下来自己程序的入口地址来供OS读取;
  • 代码和数据都有自己对应的虚拟地址,当加载到物理内存时,因为这些虚拟地址需要占据空间,所以一定要有对应的物理地址,也就可以在页表中建立起来映射关系;
  • CPU内的指令寄存器,可以根据虚拟地址然后通过页表的映射,从而找到物理内存中的各种指令函数。 

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!         

  • 30
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

stackY、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值