Linux系统编程——动静态库

news2024/11/20 3:37:53

目录

一,关于动静态库

1.1 什么是库?

1.2 认识动静态库

1.3 动静态库特征

二,静态库

2.1 制作静态库

2.2 使用静态库 

三,动态库 

3.1 制作动态库

3.2 使用动态库一些问题

3.3 正确使用动态库三种方法 

3.3.1 方法一:添加系统环境变量

3.3.2 方法二:改配置文件

3.3.3 方法三:软链接方案

五,一些问题解答

5.1 为什么gcc编译的时候没有指明过库名也能编译?

5.2 为什么要有库?

5.3 进程是如何访问到库中的所有函数的?


一,关于动静态库

1.1 什么是库?

一个可执行程序,由一堆源文件和一堆头文件经过预处理,编译,汇编,链接四个步骤后得来。

我们先写几个简单的头文件和源文件,如下代码:

mymath.h

#pragma once

extern int Add(int x, int y);

mymath.c

#include "mymath.h"

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

myprint.h

#pragma once 

extern void Printf(int x, int y);

myprint.c

#include <stdio.h>
#include "myprint.h"

void Printf(int a, int b)
{
  printf("hello %d\n", Add(a, b));
}

main.c

int main()
{
  Printf(10, 20);
  return 0;
}
gcc mymain.c mymath.c myprint.c -o my.exe

① 一般来说,程序都是先通过汇编之后形成.o文件,然后就将所有的.o文件进行链接最终形成可执行程序。那么,如果我把我的.h和.o给别人,别人能用吗

②当然是可以的,在Linux操作系统下,只需要cp *.o /home/user/ 把.o和.h拷贝到其它用户目录下,其它用户目录就可以使用了。

③但是一些稍微大一点的项目,.o文件会非常非常多,所以我们得把所有的.o文件打个包,这个打包的过程就叫做形成库的过程,这个打好的包就叫做库

④我们目前见到的库绝大多数其实都是一堆目标文件(.o)的集合,库的文件中并不包含主函数,但是提供了大量的方法以供调用。

1.2 认识动静态库

我们先通过一个最简单的代码来认识库:

#include<stdio.h>
int main()
{
  printf("hello world\n");
  return 0;
}

我们gcc编译后生成test可执行程序,运行后成功在屏幕上打印hello world。

我们用ldd test来查看一个可执行程序所依赖的库文件,如下图:

 

  • 在Linux中,.so为后缀的是动态库,.a为后缀的是静态库
  • 在Windows中,.dll为后缀的是动态库,.lib为后缀的是静态库

 gcc/g++默认是动态链接的,但是我们也可以强制让它变成静态链接,在后面携带一个-static选项即可,如下图:

 我们也可以用file查看文件的详细属性:

1.3 动静态库特征

静态库

静态库是程序在遍历链接时把库的代码原封不动地全部拷贝到源文件中一起编译

优点:使用静态库生成地可执行程序可以直接独立运行,不再需要动态库

缺点:使用静态库生成地可执行程序空间会大很多,就拿上面的图来看,test-s的大小是test的一百多倍。而且这只是包含了一个头文件,当有多个静态程序同时加载相同的库,那么内存会存在大量的重复代码

动态库: 

动态库是在程序运行时才会去链接相应的动态库代码的,多个程序共享使用库的代码。

一个与动态库链接的可执行文件只包含了它用到的函数入口地址的一个表,而不是外部函数所在文件的所有数据。在可执行文件运行前,调用的函数所在的库由操作系统从磁盘上先搞到物理内存中,这个过程称为动态链接。然后动态库在多个程序间共享,节省了内存和磁盘的空间,具体步骤如下图:

  1. 首先,动态库是一个独立的文件,可执行程序也是一个独立的文件,所以动态库和可执行程序是可以分批加载的
  2. 地址空间最下面的代码区,中间一大块是堆栈,而且堆栈相对而生,但是堆栈中间有一大块镂空区域,我们叫做共享区
  3. a.out先加载到内存里面,如果在执行时需要访问库代码,这个时候操作系统就把位于磁盘上的动态库的某一个方法加载到物理内存里,然后通过页表建立映射关系,把该方法的虚拟地址放进进程的共享区中
  4. 然后进程就可以从代码区转到共享区执行完方法再把结果拿回代码区,减少了额外拷贝,只需要改下虚拟地址即可
  5. 这样,一个进程动态链接后物理内存中的该方法就可以继续为其它的调用这个方法的进程服务,这也就是为什么我们两次运行一个程序,第二次运行比第一次快很多,因为第一次加载的数据,第二次就不需要加载了,可以直接用 

优点:能节省磁盘和内存空间,并且多个用到相同动态库的程序同时运行时,库文件会通过进程地址空间进行共享,内存中不会存在重复代码

缺点: 必须依赖动态库,并且动态库的使用比静态库复杂很多,下面的内容会有体会

二,静态库

2.1 制作静态库

就上面的mymath.h和myprintf.h而言,我们可以把它俩打个包,如下图:

ar命令是gnu的归档工具,常用于将目标文件打包为静态库,下面是一些常用选项:

  1. -r(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的
  2. -c(create):建立静态库文件
  3. -t:列出静态库中的文件
  4. -v(verbose):显示库的详细信息 

一个稍微大的项目都有至少5各源文件,我们一个一个打太慢了,所以我们可以用makefile使其自动化:

libmyhello.a:mymath.o myprint.o
	ar -rc libmyhello.a mymath.o myprint.o
mymath.o:mymath.c
	gcc -c mymath.c -o mymath.o
myprint.o: myprint.c
	gcc -c myprint.c -o myprint.o

.PHONY:hello
hello:
	mkdir -p hello/lib
	mkdir -p hello/include
	cp -rf *.h hello/include
	cp -rf *.a hello/lib

.PHONY:clean
clean:
	rm -rf *.o libmyhello.a hello

2.2 使用静态库 

经过上面的步骤后我们形成了静态库,hello目录的include包含我所有的.h文件,lib包含.a文件。

 我们可以先把main.c和hello之外的文件全干掉:

方法一: 

gcc的头文件默认搜索路径是:/usr/include    库文件的默认搜索路径是:/lib64 或者 /usr/lib64

我们可以把我们的文件拷贝到这两个目录下:

sudo cp -rf hello/include/* /usr/include/ 
sudo cp -rf hello/lib/libmyhello.a /lib64

添加了之后,会发现还是编译不过,因为我们自己实现的库还是第三方库,不是系统库也不是C标准库,所以我们需要这样:

gcc main.c -o main -lmyhello

  -l 表示要链接库,然后我们之前的.a文件去掉前面的lib和后面的.a剩下的就是库名

方法二: 

 但是,我们非常不见直接把我们现在写的库文件安装到系统目录下,因为我们目前写的库文件,还没有经过可靠性验证,建议测试完成后直接删掉

sudo rm /usr/include/myprint.h
sudo rm /usr/include/mymath.h
sudo rm /lib64/libmyhello.a

所以我们目前我们要使用我们自己做的静态库,就需要这样:

gcc main.c -I ./hello/include/ -L ./hello/lib/ -lhello

  • -I:指定头文件搜素路径(因为编译器不知道你所包含的头文件在哪里,所以需要指定头文件搜索路径)
  • -L:指定库文件搜索路径(头文件中只有Add函数和Printf函数的声明,并没有实现,所以需要指定链接库文件的搜素路径)
  • -l:指明需要链接库文件路径下的哪一个库(在库文件的lib目录下可能会有大量的库文件,因此我么你需要指明链接库文件路径下的哪一个库)

三,动态库 

3.1 制作动态库

动态库大致和静态库一样,但是又些区别。

动态库是以lib开头,以.so结尾的文件,我们还是以这几个文件为例

然后就是制作动态库,如下图:

 我们先生成.o目标文件,对应-fPIC有下面几个要注意的点:

  1. -fPIC作用于编译阶段,其目的是告诉编译器产生与位置无关的代码,这时产生的代码中没有绝对地址,全都是相对路径,使代码可以被加载器加载到内存的任意位置都可以指向。这也是动态库的特点,它在内存中的位置不是固定的
  2. 如果不加-fPIC选项,则加载.so文件时,其里面的代码端引用的数据对象需要重定位,而重定位会蟹盖代码内容,这就导致每个使用这个.so的进程都会发生写时拷贝,因为每个进程的.so文件代码段和数据段在内存的映射位置不一样

 然后我们可以不用ar命令来生成动态库,只需要在gcc命令加上-shared选项即可。

 当然我们也一般用makefile来搞这个,如下:

.PHONY:all
add:libhello.so libhello.a

libhello.so:mymath_d.o myprint_d.o
	gcc -shared mymath_d.o myprint_d.o -o libhello.so
mymath_d.o:mymath.c
	gcc -c -fPIC mymath.c -o mymath_d.o
myprint_d.o:myprint.c
	gcc -c -fPIC myprint.c -o myprint_d.o

libhello.a:mymath.o myprint.o
	ar -rc libhello.a mymath.o myprint.o
mymath.o:mymath.c
	gcc -c mymath.c -o mymath.o
myprint.o: myprint.c
	gcc -c myprint.c -o myprint.o

.PHONY:output
output:
	mkdir -p output/lib
	mkdir -p output/include
	cp -rf *.h output/include
	cp -rf *.a output/lib
	cp -rf *.so output/lib

.PHONY:clean
clean:
	rm -rf *.o *.a *.so output

先make生成.so文件,在make output生成目录

3.2 使用动态库一些问题

和静态库一样,我们只保留output目录和main.c

然后我们也可以用-I,-L和-l三个选项来生成可执行文件,如下图:

为什么呢,我们ldd a.out可以发现gcc默认使用 /lib64目录下的动态库而不是我们output目录里的那个:

当我们把.so搞到当前目录下发现又能用了。如下图: 

如果我们只有静态库,gcc只能针对该库进行静态链接,如果我强转使用静态库,只能gcc main.c -I output/include -L output/lib -lhello -static  摈弃默认优先使用动态库的原则,而是直接使用静态库的方案。如果动静态库都存在,gcc默认用动态库

问题一:那为什么动态库运行报错了呢?

根据1.2 的动态库原理可知,我们的程序和库是分批加载的,加载程序的时候动态库未加载,所以会报错

问题二:gcc编译的时候我不是已经告诉你库的位置了吗?

这里的“你”指的是gcc,你只是告诉了gcc库的位置,没有告诉可执行程序,所以你需要告诉你的加载器的位置

3.3 正确使用动态库三种方法 

3.3.1 方法一:添加系统环境变量

与动态库有关的环境变量名为:LD_LIBRARY_PATH,其中LD是加载的意思,LIBRARY就是库的意思。

我们可以先查找我们库的路径,然后添加到该环境变量中,如下图:

但是该环境变量有缺点,就是当shell关闭后,环境变量就重置了 

3.3.2 方法二:改配置文件

系统中存在一个默认的动态库搜搜路径:ls /etc/ld.so.conf.d/。我们只需要在这个路径下也创建一个.conf后缀的文件,然后把我们的库文件的路径vim写到里面就可以了,如下图:

(建议测试完就rm掉,因为不建议改配置文件) 

sudo rm /etc/ld.so.conf.d/mylibrary.conf 

3.3.3 方法三:软链接方案

我们ldd a.out发现它默认使用的是动态库,且动态库路径为 /lib64/libhello.so,但是它找不到这个嘛,那么我们就用一个软链接将它那个.so和我们这个.so链接起来就好了,如下图:

sudo ln -s /home/dyk/testdir/output/lib/libhello.so /lib64/libhello.so

 (其实也可以直接把我们的libhello.so拷贝到这个目录下,也能正常运算a.out)

五,一些问题解答

5.1 为什么gcc编译的时候没有指明过库名也能编译?

gcc就是用来编译C与语言的,gcc编译时默认就是找到C标准库,但是我们要链接的是哪一个库,编译器是不知道的,所有需要使用 -l 选项指明链接库文件路径下的具体的库名

5.2 为什么要有库?

站在使用库的人的角度来看,库的存在能大大减少开发的周期,大步幅提高软件本身的质量

站在编写库的人得角度,它得任务就是编写库,只需要关系库得实现,不关心库得使用,能使减少编写库得人的工作量,也能提高库的质量

5.3 进程是如何访问到库中的所有函数的?

每一个动态库被加载到内存,映射到进程的地址空间,映射的位置可能是不一样的,但是因为库里面是相对地址,每一个函数定位采用的是偏移量的方式。

换句话说,只要知道了这个库的相对地址,库的起始地址,就可以通过库的起始地址和虚拟地址 + 函数偏移量,就可以在自己的地址空间中访问库的所有函数了

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

找回xmind文件办法:一切意外均可找回(误删/重启关机等)

我周三编辑完&#xff0c;周四下午评审完用例忘记保存 结果到了快乐星期五&#xff0c;由于是周五我太开心了...早上到公司后觉得电脑卡&#xff0c;直接点了重启啥都没保存啊啊啊啊啊 准备上传测试用例时才想起来我的用例找不见了&#xff01;&#xff01;&#xff01;&…

docker 快速搭建django项目环境(DockerFile)文件基础搭建

首先需要搭建好docker环境&#xff0c;docker环境就不在这里叙述&#xff0c;如果想学在评论区留言小编后期更新由linux系统到docker的安装做一个详细的教程。 下面我们开始今天的重点&#xff1a; 1、第一步&#xff1a;我们在任意&#xff08;linux&#xff09;路径下创建Do…

ARC学习(2)基本编程模型认识(二)

笔者继续来学习一下arc的编程模型的寄存器信息。 1、core寄存器深入 参数寄存器&#xff1a;r0-r7&#xff0c;8个参数&#xff0c;暂存器&#xff1a;r10-r15保存寄存器&#xff1a;r16-r25 调用函数需要保存的寄存器指针寄存器&#xff1a;gp&#xff08;全局指针&#xff09…

HackTheBox-Machines--Nineveh

Nineveh测试过程 1 信息收集 NMAP 端口扫描 80 端口 80端口是服务器的默认页面&#xff0c;无可利用功能点&#xff0c;源代码没有可利用的敏感信息 目录扫描 1.http://10.129.25.123/department 访问/department目录跳转到登录页面&#xff0c;尝试暴力破解&#xff0c;获取…

深入分析 Android Service (四)

文章目录 深入分析 Android Service (四)1. 使用 Messenger 进行通信2. 详细示例&#xff1a;使用 Messenger 进行通信2.1 创建 MessengerService2.2 在 Activity 中绑定服务并发送消息 3. 使用 AIDL 进行进程间通信3.1 定义 AIDL 接口3.2 实现 AIDL 接口3.3 在客户端绑定 AIDL…

【设计模式深度剖析】【7】【结构型】【享元模式】| 以高脚杯重复使用、GUI中的按钮为例说明,并对比Java类库设计加深理解

&#x1f448;️上一篇:外观模式 | 下一篇:结构型设计模式对比&#x1f449;️ 设计模式-专栏&#x1f448;️ 目录 享元模式定义英文原话直译如何理解&#xff1f;字面理解例子&#xff1a;高脚杯的重复使用例子&#xff1a;GUI中的按钮传统方式使用享元模式 4个角色1. …

api网关kong对高频的慢接口进行熔断

一、背景 在生产环境&#xff0c;后端服务的接口响应非常慢&#xff0c;是因为数据库未创建索引导致。 如果QPS低的时候&#xff0c;因为后端服务有6个高配置的节点&#xff0c;虽然接口慢&#xff0c;还未影响到服务的正常运行。 但是&#xff0c;当QPS很高的时候&#xff0c…

.NET IoC 容器(三)Autofac

目录 .NET IoC 容器&#xff08;三&#xff09;AutofacAutofacNuget 安装实现DI定义接口定义实现类依赖注入 注入方式构造函数注入 | 属性注入 | 方法注入注入实现 接口注册重复注册指定参数注册 生命周期默认生命周期单例生命周期每个周期范围一个生命周期 依赖配置Nuget配置文…

07-操作元素(键盘和鼠标事件)

在前面的文章中重点介绍了一些元素的定位方法&#xff0c;定位到元素后&#xff0c;就需要操作元素了。本篇总结了web页面常用的一些操作元素方法&#xff0c;可以统称为行为事件。 一、简单操作 点击按钮&#xff08;鼠标左键&#xff09;&#xff1a;click()清空输入框&…

Linux静态库与动态库加载

了解库&#xff1a; 关于库相比大家之前肯定使用过&#xff0c;比如C/C里面的标准库&#xff0c;STL里面的各种库&#xff0c;我们在调用STL里的容器时都需要使用库&#xff0c;那么库到底是什么呢&#xff1f; 库的本质就是可执行程序的"半成品" 我们先来回顾一下代…

原生APP开发和Flutter开发的比较

原生APP开发和Flutter开发各有优缺点&#xff0c;适用于不同的场景和需求。下面是两者的详细比较&#xff0c;从开发语言、性能、开发效率、维护和更新、社区和支持等多个方面进行分析。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。…

工业安全智勇较量,赛宁网安工业靶场决胜工业网络攻防对抗新战场

2024年1月30日&#xff0c;工信部发布《工业控制系统网络安全防护指南》&#xff08;工信部网安〔2024〕14号&#xff09;&#xff0c;围绕安全管理、技术防护、安全运营、责任落实四方面提出安全防护要求&#xff0c;强调聚焦安全薄弱关键环节&#xff0c;强化技术应对策略&am…

C语言序列化和反序列化--TPL中的API(三)

tpl_map 创建tpl的唯一方法是调用tpl_map()。第一个参数是格式字符串。后面是格式字符串中特定字符所需的参数列表。例如, tpl_node *tn; int i; tn tpl_map( "A(i)", &i );该函数在格式字符串中的项和给定地址的C程序变量之间创建映射。稍后&#xff0c;C变量…

C. Turtle and an Incomplete Sequence

思路&#xff1a;首先如果都是-1的话&#xff0c;我们可以输出1 2循环&#xff0c;否则。 首先处理首尾的连续-1串&#xff0c;这个也很好处理&#xff0c;最后我们处理两个非-1之间的-1串。 对于左边的数a[l]和右边的数a[r]&#xff1a; 如果a[l] > a[r]&#xff0c;那么…

python移动文件

测试1(直接把B文件夹移动到了A里&#xff0c;成为了A的子文件夹) import os import shutil# 移动文件夹,B文件夹在当前目录没有了&#xff0c;跑到了A的子文件里 ## shutil.move(./example1/B/, ./example1/A/)测试2(B文件不动&#xff0c;将B文件里的所有的子文件夹移动到A内…

【算法】模拟算法——提莫攻击(easy)

题解&#xff1a;提莫攻击(模拟算法) 目录 1.题目2.题解3.参考代码4.总结 1.题目 题目链接&#xff1a;LINK 2.题解 举例&#xff1a; 3.参考代码 class Solution { public:int findPoisonedDuration(vector<int>& timeSeries, int duration) {int n timeSeri…

Java设计模式 _行为型模式_访问者模式

一、访问者模式 1、访问者模式 访问者模式&#xff08;Visitor Pattern&#xff09;是一种行为型模式。它允许在不修改已有类结构的情况下&#xff0c;向类中添加新的操作。访问者模式通过将操作封装在一个访问者对象中&#xff0c;使得可以在不改变各个元素类的前提下&#x…

thinkphp6 queue队列的maxTries自定义

前景需求&#xff1a;在我们用队列的时候发现maxtries的个数时255次&#xff0c;这个太影响其他队列任务 我目前使用的thinkphp版本是6.1 第一部定义一个新的类 CustomDataBase&#xff08;我用的mysql数据库存放的队列&#xff09; 重写__make 和createPlainPayload方法 …

《面试笔记》——MySQL终结篇30

三大范式&#xff1f; 第一范式&#xff1a;字段具有原子性&#xff0c;不可再分&#xff08;字段单一职责&#xff09; 第二范式&#xff1a;满足第一范式&#xff0c;每行应该被唯一区分&#xff0c;加一列存放每行的唯一标识符&#xff0c;称为主键&#xff08;都要依赖主…

VB.net进行CAD二次开发(四)

netload不能弹出对话框&#xff0c;参考文献2 参考文献1说明了自定义菜单的问题&#xff0c;用的是cad的系统命令 只要加载了dll&#xff0c;自定义的命令与cad的命令同等地位。 这时&#xff0c;可以将自定义菜单的系统命令替换为自定义命令。 <CommandMethod("Add…