操作系统:动静态库

目录

 

1.动静态库

 1.1.如何制作一个库

1.2.静态库的使用和管理

1.3.安装和使用库

1.4.动态库 

1.4.1.动态库的实现

1.4.2.动态库与静态库的区别 

1.4.3.共享动态库给系统的方法

2.动态链接

2.1.操作系统层面的动态链接


1.动静态库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静 态库
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文 件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个 过程称为动态链接(dynamic linking)
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚 拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

 1.1.如何制作一个库

库(Library)是一种预先编写好的可重用代码集合,它包含了一组函数、类、数据结构或其他可执行代码的集合。库的目的是为了提供一些常用的功能,以便开发人员可以在自己的程序中直接调用这些功能,而不需要从头开始编写代码。

在我们实际开发的场景下,当我们写好一个库时,我们往往是将源代码(.c文件)经过编译后的文件(.o文件)和头文件一起打包给库的使用者,也就是我们写好的库是不包含源代码的。

文件编译

一堆源文件和头文件最终变成一个可执行程序需要经历以下四个步骤:

  • 预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件。
  • 编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件。
  • 汇编: 将汇编指令转换成二进制指令,最终形成xxx.o文件。
  • 链接: 将生成的各个xxx.o文件进行链接,最终形成可执行程序。

库中包含着.h文件和.o文件 

接下来我们写一个demo并把它打包成库

我们以实现计算器这个库

一般来说我们要编译这个test.c这个可执行文件,需要将三个.c文件同时编译使用

gcc test.c add.c sub.c

但是这样子就需要将源代码给到别人,而开发中的源代码一般是闭源的,所以我们需要先将这几个实现.h文件的.c文件编译成.o文件

gcc -c add.c sub.c    // 生成对应的.o文件

那么我们现在开始创建库的雏形,只给.h文件和.o文件(这个图add.c应该为add.h)

用户在使用时通过先将自己的测试文件转换为.o文件然后再一起编译这几个.o文件

gcc test.o add.o sub.o

假设在实际开发中,有10000个.o文件那这样子编译会不会容易漏掉,所以这时候我们需要打包文件,生成静态库

// 通过这条指令我们打包了一个静态库mycal
ar -rc libmycal.a *.o

如图所示我们就打包好了一个静态库

一般来说我们通常通过makefile来打包好静态库

static-lib=libmycal.a

$(static-lib):add.o sub.o
	ar -rc $@ $^
  
%.o:%.c
	gcc -c $<

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

1.2.静态库的使用和管理

我们在1.1.中制作了一个mycal的静态库,那么从库的使用角度出发我们怎么来使用这个库呢?

首先当我们写了一个库给他人使用时,此时作为第三方库,gcc无法认识这个库,需要链接指定的库

这时我们需要连接上这个静态库,输入链接指令

//  gcc 需要编译的文件名 -l库名称 -L 库所在的路径
gcc test.c -lmycal -L.

注意这里的 -l 直接连接对应的库名称,且库名称需要去掉 lib 和 .a

这样我们就完成了静态库的使用了!!!

值得注意的是:一个可执行文件,可以实现动静态库的混合链接,如何连接时加入 -static 选项,表示只使用该文件编译时需要的静态库。


我们在lib文件夹中发现.h文件和.a库放在一起,在实际开发场景中,可能存在大量的这些文件,为了便于管理,我们通常是将他们分组存放后,然后打包挂到云端提供给他人使用

回到最初的起点,我们开始准备打包

这里是makefile内的代码块

static-lib=libmycal.a

$(static-lib):add.o sub.o
	ar -rc $@ $^
  
%.o:%.c
	gcc -c $<

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

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

接着我们make编译一下

这时就回到了我们之前讲的需要管理的场景,我们在makefile里写了一个脚本output

形成了一个目录,我们查看一下

这样子就实现了我们对这个库进行管理了

tar czf mycal_lib.tgz mycal_lib

最后的最后我们打包好这个文件就可以挂载服务器端让他人下载使用了

1.3.安装和使用库

书接上文,假设我们从网上下载一个mycal_lib这个库,我们需要如何加载进我们的库环境中呢?

首先需要解压这个包,获得库,接着就是将库安装到开发环境中!!!(本质上是拷贝)

安装开发环境的本质就是将第三方库提供的.h、.a文件分别放到系统中的指定位置,一般是拷贝到对应的include、lib文件夹里

那么当我们运行时,gcc默认就会在系统的开发环境中来寻找我们download的库和头文件了,使用时就只需要连接需要的库即可

gcc test.c -lmycal


如果我们不安装到系统里,我们需要怎么使用这个库呢?

 这时我们需要输入指令来寻址(因为头文件和库文件在mycal_lib中,gcc找不到)

// -I 新增头文件搜索路径 -l为连接的库名称  -L 新增库文件的搜索路径
gcc test.c -I mycal_lib/include -lmycal -L mycal_lib/lib

 那么这样子我们就完成了,对下载库使用的学习了

1.4.动态库 

因为动态库的制作和静态库的制作思路一致都是形成.o文件然后打包,但是会有一些细节上的不同,接下来我们来实现一下动态库


1.4.1.动态库的实现

首先是编译文件时和打包指令不同

// 编译成.o文件时需要添加 -fPIC
gcc -fPIC -c add.c sub.c
// 打包成库时用gcc指令
gcc -shared -o libmycal.so *.o

我们只需要修改一下makefile就也可以通过脚本实现之前讲的内容

dy-lib=libmycal.so

$(dy-lib):add.o sub.o
	gcc -shared -o $@ $^
  
%.o:%.c
	gcc -fPIC -c $<

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

.PHONY:clean
clean:
	rm -f *.o *.so output 

实现的部分我们不再赘述了


1.4.2.动态库与静态库的区别 

那我们通过使用静态库的方式来调用一下这个动态库

我们发现就算我们声明了路径,链接了调用的库,最后可以正常生成这个a.out文件但是我们运行时发现 -> 报错:加载共享库,无法打开这个共享库文件

这里我们解释一下: 静态库在编译后,可执行程序和静态库打包在一起相当于加载进了整个文件中,运行期间不需要找到静态库。而运行动态库是和可执行程序分离的,所以需要将动态库加载到内存中。

可执行程序 和 动态库 同时存在于内存中才可以实现动态库!!!

我们发现这时系统找不到动态库的位置,因为我们之前只给了test.c寻找到了动态库的位置,但是系统还是不知道动态库在哪里,所以需要我们把动态库的位置共享给系统。

1.4.3.共享动态库给系统的方法

  • 将头文件和库文件添加到系统默认的区域,也就是拷贝到相应的include和lib
// 拷贝库中的所有.h文件到系统默认的include中
sudo cp mycal_lib/include/*.h /usr/include/
// 拷贝.so库文件
sudo cp mycal_lib/lib/*.so /lib64/

  • 建立软连接,动态库会在pwd中寻找库
ln -s mycal_lib/lib/libmycal.so libmycal.so

动态库在运行时会查找当前路径是否能找到这个动态库,所以我们可以建立软连接来实现!

这时候我们可以思考一下:我们能不能把这个软连接安装到系统里面,而不是安装整个整个库呢?也是可以的!


  • 添加至环境变量LD_LIBRARY_PATH中(内存级别,退出shell会被清空)
// 通过pwd找到动态库所在的路径添加到环境变量里
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/Czh_Linux/test/mycal_lib/lib


  • 更改系统的配置文件(不会因为重启shell而被清空)

系统中存在一些管理动态库的配置文件

// 文件的路径
/etc/ld.so.conf.d/

接着我们就需要写入进这个目录里面,在里面创建一个自己的动态库配置文件

// 需要sudo权限或者root
touch /etc/ld.so.conf.d/my_so.conf

接着在这个my_so.conf文件里面写入你库的地址,进入动态库的地址用pwd打印出地址然后拷贝进这个文件中。

// 直接拷贝这一段话到你创建的.conf文件中
/home/Czh_Linux/test/mycal_lib/lib

这时已经完成了写入动态库信息到配置文件中!!!

// 如果没有生效就需要刷新一下
sudo ldconfig

2.动态链接

在Linux中可执行文件一般以ELF的格式出现,符号表中存放着调用某些库的方法的地址,那也就是当可执行文件加载进内存时,进入内存的只有文件中对应库方法的地址,而没有库中的实现,也就对应了我们之前没有将动态库加载进内存中会出现报错!

所以进行动态链接的过程就是将对应库的方法的地址存放在可执行文件的符号表中,再配合着将对应库加载进内存中!


我们接着思考,当程序加载进内存中时是以什么形式出现的呢?从汇编语言中我们就能看出大部分的函数变量加载进内存时,最终是以“地址”的形式加载进内存的。那么这个问题引申下去就是如何对代码进行编址呢?

在编译器中,也是通过“虚拟地址空间”编址,虚拟地址空间可以看成是一种编译标准

文件通过编译后在形成进程加载进内存之前,代码和数据就已经具有了自己的地址(虚拟地址),大多数情况叫做处于磁盘文件的“逻辑地址”,核心是:基地址 + 偏移量

一般情况下:设定基地址为0,然后偏移量来进行定位区分(Linux的平坦模式)


在计算机中对程序进行编址时,一般是两种方式:绝对编址(以基地址为标准) 和 相对编址(以其他个体地址为标准) 

库当中形成地址一般是通过相对编址,记录函数间的偏移量

假如:Entry的main函数地址为0x1234,进入main函数后遇到的第一个地址为0x1236那么他们的偏移量固定为(0x1236 -0x1234)

未来库在内存中的任意位置加载,那么库内部函数的相对编址始终不变,所以无论是加载在内存的某个区域,只要找到Entry我们都能通过不变的“地址”来使用库中的所有的函数。

这就是 fPIC 与位置无关码

2.1.操作系统层面的动态链接

我们知道了ELF文件(编译后的代码)也实现了类似操作系统中的虚拟地址空间,也就是操作系统和磁盘通过这个虚拟地址空间进行代码的交互,又动态库在内存中会映射在pcb的虚拟内存的共享区内,这样子就穿起来了操作系统和磁盘内的ELF可执行文件(这个不就是进程运行文件的动态链接吗)


但是我们感觉还是有一点抽象,究竟代码的地址和进程的地址是如何联系起来的呢?

我们之前强调了相对编址这个概念,在ELF的符号表中存储着各个调用库的函数的地址(只要知道main函数入口地址),我们可以计算出来他们相对的偏移量,如果main为a,printf为b,那么偏移量就为b-a,这是从库角度出发。当我们从文件的代码区出发,运行代码的本质就是地址的跳转,但是代码区相当于只存储了代码的声明,实现在库中,所以我们就需要代码区进行到某个函数时跳转到对应共享区的部分,通过偏移量,映射到内存中代码的实现! 


接着我们再换一个角度,从进程的调度中出发 

CPU在进行进程调度时也是通过访问虚拟地址来实现的,也就是在我们所说的代码区和共享区进行跳转通过CPU中的指令寄存器。我们知道代码在内存中拥有自己的物理地址,而CPU指令寄存器读入代码虚拟地址通过页表映射找到物理地址,因此CPU可以实现某一行代码的功能。

  1. 操作系统可以读取编译后的ELF程序的入口Entry地址(虚拟地址),这个地址被操作系统用于进程pcb对应的正文代码的main函数的入口,接下来操作系统将这个入口地址加载到指令寄存器中。
  2. 从main函数还是执行,一步一步调用不同的代码,读取对应的虚拟地址,然后通过页表映射到物理内存,实现库方法和代码的运行,接着继续跳转回CPU,读取下一条虚拟地址
  3. 如此往复这个循环,最终完成我们进程的调度

找代码是通过物理地址,代码之间跳转通过虚拟地址。每个程序加载进物理内存中,每个变量、函数都有自己的物理地址和虚拟地址。

核心:

  1. 只要获得起始地址再配合偏移量就能实现库中的任何方法
  2. 通过虚拟地址映射到物理地址来实现代码的定位
  3. 操作系统和编译器用同样的虚拟地址空间标准,实现CPU进行虚拟地址之间的转化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/512500.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Rust---复合数据类型之元组

目录 元组的使用输出结果 元组的使用 fn main() {// 创建一个元组let my_tuple : (i32, &str, f64) (10, "hello", 3.14);// 打印元组中的元素println!("{:?}", my_tuple);// 访问元组中的元素let first_element my_tuple.0; // 访问第一个元素let…

openldap(一):简介和安装

目录 1 OpenLDAP简介1.1 LDAP介绍1、什么LDAP2、为什么要使用LDAP3、LDAP 的特点4、LDAP常用关键字5、LDAP的objectClass6、LADP使用场景 1.2 OpenLDAP介绍1、什么OpenLDAP2、OpenLDAP特点3、OpenLDAP的组件 2 OpenLDAP安装3 简单使用3.1 创建用户1、创建ou2、创建Group 3、创建…

来成都的国际数字影像产业园,开启文创产业园之旅

走进位于成都金牛区福堤路的国际数字影像产业园&#xff0c;仿佛置身于一个充满创意与活力的场域。这里是成都数字产业的聚集地&#xff0c;汇聚了上百家数字媒体相关企业&#xff0c;为成都文创产业注入了新的活力。在这里&#xff0c;你可以感受到浓厚的创新氛围&#xff0c;…

10.图像高斯滤波的原理与FPGA实现思路

1.概念 高斯分布 图像滤波之高斯滤波介绍 图像处理算法|高斯滤波   高斯滤波(Gaussian filter)包含很多种&#xff0c;包括低通、高通、带通等&#xff0c;在图像上说的高斯滤波通常是指的高斯模糊(Gaussian Blur)&#xff0c;是一种高斯低通滤波。通常这个算法也可以用来模…

[mmu/cache]-ARM cache的学习笔记-一篇就够了

快速链接: 【精选】ARMv8/ARMv9架构入门到精通-[目录] &#x1f448;&#x1f448;&#x1f448; 应用场景——什么时候需要刷cache 1、在不同硬件之间共享数据时 场景&#xff1a;CPU往src地址处写入了一串数据&#xff0c;然后交给Crypto硬件进行加解密处理&#xff0c;加解…

LabVIEW电力设备在线监测系统

LabVIEW电力设备在线监测系统 在电力行业中&#xff0c;变电站的稳定运行对于保障电力系统的安全性和可靠性至关重要。开发了一种基于LabVIEW软件开发的变电站电力设备在线监测系统&#xff0c;实时监控变电站内部的电力设备状态&#xff0c;确保电力传输的高效与安全。通过对…

vue广告悬浮框,页面来回移动,鼠标放上停止,离开移动

1.dom <div class"popup-dialog" id"popupDialog" mouseover"onMmouseover" mouseout"onMouseout"><p>vue广告悬浮</p></div>2.js mounted() {this.initPopup();},beforeDestory() {if (this.times) {clearIn…

IDEA无法连接虚拟机中的Redis的解决方案,无法连接Jedis,无法ping通虚拟机的解决方案

首先&#xff0c;笔者先说明一下自身的情况&#xff0c;怎么连接都连不上&#xff0c;网上的教程全部都看了一遍&#xff0c;基本上没用得上的&#xff0c;这篇文章里面的解决方案包括了笔者能在网上找到了最全面的办法总结&#xff0c;最后终于是连上了 目录 一.连接Jedis出错…

类似微信的以文搜图功能实现

通过PaddleOCR识别图片中的文字&#xff0c;将识别结果报存到es中&#xff0c;利用es查询语句返回结果图片。 技术逻辑 PaddleOCR部署、es部署创建mapping将PaddleOCR识别结果保存至es通过查询&#xff0c;返回结果 前期准备 PaddleOCR、es部署请参考https://blog.csdn.net…

基于springboot实现海滨体育馆管理系统项目【项目源码+论文说明】

基于springboot实现海滨体育馆管理系统演示 摘要 本基于Spring Boot的海滨体育馆管理系统设计目标是实现海滨体育馆的信息化管理&#xff0c;提高管理效率&#xff0c;使得海滨体育馆管理工作规范化、高效化。 本文重点阐述了海滨体育馆管理系统的开发过程&#xff0c;以实际…

Python | Leetcode Python题解之第7题整数反转

题目&#xff1a; 题解&#xff1a; def reverse_better(self, x: int) -> int:y, res abs(x), 0# 则其数值范围为 [−2^31, 2^31 − 1]boundry (1<<31) -1 if x>0 else 1<<31while y ! 0:res res*10 y%10if res > boundry :return 0y //10return re…

【鹅厂摸鱼日记(一)】(工作篇)认识八大技术架构

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:重生之我在鹅厂摸鱼⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多知识   &#x1f51d;&#x1f51d; 认识八大架构 1. 前言2. 架构简介&…

Vue插槽(Slots)深入解析

插槽内容与出口​ 在之前的章节中&#xff0c;我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props&#xff0c;但组件要如何接收模板内容呢&#xff1f;在某些场景中&#xff0c;我们可能想要为子组件传递一些模板片段&#xff0c;让子组件在它们的组件中渲染这些片…

cpp第六次作业

1.导图&#xff1a; 2.源码&#xff1a; #include <iostream> using namespace std; template <typename T> class stack{T data[10];int top; public:stack():top(-1){};void add_data(T data1){if(top9){return;}top;data[top]data1;}void del_data(){if(top-1…

玩机进阶教程-----高通9008线刷XML脚本修改备份 檫除的操作步骤解析

在高通9008官方固件中我们可以看到刷写需要的脚本rawprogram0.xml和辅助脚本patch0.xml&#xff0c;脚本的作用在于将固件内各个分区对应写入手机内。根据分区地址段。然后判断脚本中那些分区不写入。以下步骤将分析emmc字库为例来讲解如何将默认刷入脚本修改为备份 檫除脚本。…

Matlab实验:离散时间信号与系统的时域分析

01.代码的主要内容 02.代码效果图 获取代码请关注MATLAB科研小白的个人公众号&#xff08;即文章下方二维码&#xff09;&#xff0c;并回复MATLAB课程设计&#xff1b;本公众号致力于解决找代码难&#xff0c;写代码怵。各位有什么急需的代码&#xff0c;欢迎后台留言~不定时更…

C++的并发世界(三)——线程对象生命周期

0.案例代码 先看下面一个例子&#xff1a; #include <iostream> #include <thread>void ThreadMain() {std::cout << "begin sub thread:" << std::this_thread::get_id()<<std::endl;for (int i 0; i < 10; i){std::cout <&…

程序员为什么要一直写 bug ?

程序员并不是故意写bug的&#xff0c;bug的产生通常是由多种因素导致的&#xff0c;例如&#xff1a; 需求理解不足&#xff1a;如果程序员没有完全理解项目的需求或者功能要求&#xff0c;可能会在编程过程中遗漏一些重要的细节&#xff0c;导致bug的产生。编程经验不足&…

人脸、指纹、刷卡、密码、远程,一文速懂不同功能门禁系统怎么选?

门禁系统顾名思义就是对出入口通道进行管制的系统&#xff0c;它是在传统的门锁基础上发展而来。常见的门禁系统包括&#xff1a;密码识别门禁系统、刷卡识别门禁系统、生物识别门禁系统以及线上远程开门系统等。 在选择门禁系统时&#xff0c;需要根据不同的场景和需求&#x…

Nativefier - 将网页变为软件

Nativefier 是一款命令行工具&#xff0c;可以轻松地为任何网站创建 "桌面应用程序"&#xff0c;而无需大费周章。应用程序由 Electron&#xff08;内核使用 Chromium&#xff09;封装成操作系统可执行文件&#xff08;.app、.exe 等&#xff09;&#xff0c;可在 Wi…