【Linux系统IO】六、动静态库

Ⅰ. 前言

​ 我们之前学 gcc 的时候也有接触过一点动静态库的知识,现在要把它单独拿出来讲,主要是因为我们后面肯定在自己开发的时候需要包装自己的库,此时就需要有动静态库的原理知识和使用知识!

​ 一般库名称都是中间部分,也就是去掉前缀和后缀的部分剩下的内容,如:libc.so,去掉前缀 lib,去掉后缀 .so -> c 动态库。

​ 静态库和动态库最本质的区别就是:该库是否被编译进目标(程序)内部。

​ 下面我们一一介绍它们!

​ 在介绍之前我们先来介绍两个我们也曾经讲过的指令:

​ 第一个就是 ldd 指令,它的功能是 显示可执行文件依赖的库 。

【Linux系统IO】六、动静态库

​ 第二个指令就是 file 可执行文件 指令,用于 查看程序是动态还是静态链接。

【Linux系统IO】六、动静态库

Ⅱ. 静态库一、静态库的概念

​ 静态库:这类的函数库通常扩展名为 libxxx.a 或 xxx.lib 。工作原理是程序在编译链接的时候把库的代码链接 (拷贝) 到可执行文件中,变成可执行文件中的一部分,所以 程序运行的时候将不再需要静态库。

​ 这类库在编译的时候会直接整合到目标程序中,所以利用 静态函数库编译成的文件会比较大,这类函数库最大的优点就是编译成功的可执行文件 可以独立运行,而不再需要向外部要求读取函数库的内容;但是从升级难易度来看明显没有优势,如果函数库更新,需要重新编译。

​ 静态链接:链接静态库,每个程序将自己在库中用到的指令代码单独写入自己可执行程序中,程序 运行时无依赖,加载运行速度快,但是程序运行后有可能会有 冗余代码 在内存中。

二、ar指令

​ ar (archiver)命令可以用来 创建、查询、修改库。库是一组单独的文件,里面包含了按照特定的结构组织起来的源文件,原始文件的内容、模式、时间戳、属性、组等属性都保留在库文件中。

​ 下面是命令选项:

代码语言:JavaScript代码运行次数:0运行复制

-d:删除库文件中的成员文件-m:变更成员文件在库文件中的次序-p:显示库文件中的成员文件内容-q:将文件附加在库文件末端-r:将文件插入库文件中-t:显示库文件中所包含的文件-x:从库文件中取出成员文件-a:将文件插入库文件中指定的成员文件之后-b:将文件插入库文件中指定的成员文件之前-c:建立库文件-f:截掉要放入库文件中过长的成员文件名称-i:将文件插入库文件中指定的成员文件之前-o:保留库文件中文件的日期-s:若库文件中包含了对象模式,可利用此参数建立备存文件的符号表-S:不产生符号表-u:只将日期较新文件插入库文件中-v:程序执行时显示详细的信息-V:显示版本信息

下面介绍几个常用的:

参数 r :在库中插入或替换模块。当插入的模块名已经在库中存在,则替换同名的模块。如果若干模块中有一个模块在库中不存在,ar 显示一个错误消息,并不替换其他同名模块。默认的情况下,新的成员增加在库的结尾处,不过也可以使用其他任选项来改变增加的位置。参数 c :创建一个库。不管库是否存在,都将创建。参数 s :创建目标文件索引,这在创建较大的库时能加快时间。(补充:如果不需要创建索引,可改成大写 S 参数;如果 .a 文件缺少索引,可以使用 ranlib 命令添加)参数 t :比如 ar t libxxx.a,表示显示库文件中有哪些目标文件,只显示名称。参数 v :比如 ar tv libxxx.a,表示显示库文件中有哪些目标文件,显示文件名、时间、大小等详细信息。nm -s libxxx.a :显示库文件中的索引表。ranlib libxxx.a :为静态库文件创建索引表。三、静态库的封装

​ 封装库就是将多个 .o 文件打包到一个文件中,所以我们可以使用 gnu 中的归档指令 ar -rc (其中 ar 代表 archiver,rc 选项表示 replace and create)封装一个静态库。

​ 所以下面我们用 makefile 将封装指令使用起来,形成我们的静态库 libmymath.a :

代码语言:javascript代码运行次数:0运行复制

libmymath.a : add.o sub.o   # 使用ar指令封装静态库ar -rc $@ $^      %.o : %.c      # %的作用是匹配目录下的.c文件集合生成.o文件集合,与*号类似,但是%多用于makefile,且使用范围不太一样gcc -c $<cleanclean:rm libmymath.a mylib><figure class=""><img src="https://img.php.cn/upload/article/001/503/042/174488066080163.jpg" alt="【linux系统IO】六、动静态库"></figure><p>​ 这样子还不够,因为我们不仅仅需要将库发给对方,我们还需要将头文件也打包起来发给对方,考虑到如果头文件太多也不好管理的情况,我们最好自己将头文件和库放在一个目录下打包起来,所以我们可以在 makefile 中添加一个伪目标 output,其中我们调用 make output 的时候希望其能创建一个目录将我们需要打包的头文件和库打包到一个目录下:</p>代码语言:javascript<i class="icon-code"></i>代码运行次数:<!-- -->0<svg xmlns="http://www.w3.org/2000/svg" width="16"    style="max-width:90%" viewbox="0 0 16 16" fill="none"><path d="M6.66666 10.9999L10.6667 7.99992L6.66666 4.99992V10.9999ZM7.99999 1.33325C4.31999 1.33325 1.33333 4.31992 1.33333 7.99992C1.33333 11.6799 4.31999 14.6666 7.99999 14.6666C11.68 14.6666 14.6667 11.6799 14.6667 7.99992C14.6667 4.31992 11.68 1.33325 7.99999 1.33325ZM7.99999 13.3333C5.05999 13.3333 2.66666 10.9399 2.66666 7.99992C2.66666 5.05992 5.05999 2.66659 7.99999 2.66659C10.94 2.66659 13.3333 5.05992 13.3333 7.99992C13.3333 10.9399 10.94 13.3333 7.99999 13.3333Z" fill="currentcolor"></path></svg>运行<svg width="16" height="16" viewbox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 15.5V3.5H14.5V15.5H4.5ZM12.5 5.5H6.5V13.5H12.5V5.5ZM9.5 2.5H3.5V12.5H1.5V0.5H11.5V2.5H9.5Z" fill="currentcolor"></path></svg>复制<pre class="prism-token token line-numbers javascript">.PHONY : output      # output作为伪目标进行打包头文件和静态库output:mkdir -p mylib/includemkdir -p mylib/libcp -f *.a mylib/libcp -f *.h mylib/include
【Linux系统IO】六、动静态库

​ 除此之外,如果我们想安装,也就是说这个库和头文件我希望不只是被对方使用,还能在我这个系统上面使用,所以可以安装这些库和头文件,其实本质就是将头文件放到 /usr/include 中,库放到对应的库目录中比如 /lib64 中,所以我们现在也能清楚,安装的本质就是将头文件拷贝到系统的头文件目录下,库拷贝到系统的库目录下!

​ 参考下面代码,这里就不贴调用效果了:

代码语言:javascript代码运行次数:0运行复制

.PHONY:installinstall:sudo cp *.h /usr/include  # 注意一般拷贝到系统目录的时候要sudo一下sudo cp *.a /lib64

​ 最后我们将这个 mylib 目录压缩变成一个包,一般采用 tar 指令进行压缩,现在,我们的软件就已经发布出来了,我们就可以将其打包然后放在网站或者 yum 的资源中供别人进行下载使用了:

【Linux系统IO】六、动静态库

四、静态库的使用

为什么要把静态库的使用单独拎出来说呢,因为其中有很多坑,许多人打包完却因为很多这些坑导致这些库都调不起来,所以我们要好好来讲一下!

​ 下面假设我们在别的目录下的 main.c 中调用该库,在这之前先将这个包解压到 main.c 目录下:

【Linux系统IO】六、动静态库

​ 接下来有了这个 mylib ,我们不就可以直接编译链接 main.c 为可执行文件了吗,下面我们来试试看:

【Linux系统IO】六、动静态库

​ 奇怪,明明我们的库和头文件都有啊,为什么还报错说找不到头文件呢 ❓ ❓ ❓

​ 仔细一想,我们之前在学c语言的时候讲过,如果头文件使用双引号括起来的,那么它首先会到源文件的当前目录下查找,但是我们好像把库和头文件都放在了 mylib 中,深度相对于源文件更深了一点,所以 main.o 在链接的时候就找不到了!

【Linux系统IO】六、动静态库

​ 不仅如此,就算我们等会解决了这个问题还会遇到其它问题,这里就不卖关子了,直接将几个问题的解决方法一次性给出:

​ 当我们链接库时,必须指定库的名称,这是因为同一路径下可能同时存在许多库(头文件不需要指定名称,只需指定路径,因为 main 中指明了我们需要的头文件名称),同时,库需要去掉前缀 lib 和 后缀 .a 或者 .so 才是库真正的名称,也就是在我们编译链接可执行文件的时候需要在 gcc 或者其它指令后面 指定头文件路径、库文件路径、库文件名称(注意要去掉前缀和后缀):

代码语言:javascript代码运行次数:0运行复制

gcc -o main main.c -I./mylib/include -L./mylib/lib -lmymath-I 指定头文件路径:告诉编译器在./xxx路径中找头文件-L 指定库文件路径:告诉编译器在./xxx路径找库-l 指定库文件名:库名称(去掉前缀lib,去掉后缀.so或.a)

​ 其中不管 -I 还是 -L ,其实它们和路径之间是可以不留空格的,一般我们的书写习惯也是不留空格!

【Linux系统IO】六、动静态库

​ 为了方便,我们可以在 makefile 中将这些选项加入:

代码语言:javascript代码运行次数:0运行复制

libmymath.a : add.o sub.o   # 使用ar指令封装静态库ar -rc $@ $^ -I./mylib/include -L./mylib/lib -lmymath%.o : %.cgcc -c $<outputoutput:mkdir mylib libmymath.a><p>​ 平时我们使用编译器提供的库并不需要带这些选项,是因为编译器有自己的环境变量(LIBRARY_PATH),能够找到位于 /lib64 库文件的存放目录和 /usr/include 头文件的存放目录。</p> <p>​ 所以我们可以将我们写的静态库和头文件放入这些目录或其他相关目录下,这就是一般软件的安装过程。但是不推荐,因为放进去会污染标准库(可能不安全)。</p>五、静态链接的一个小问题<p>​ 接下来还有一个问题,我们查看一下我们生成的可执行文件的属性看看:</p> <figure class=""><img src="https://img.php.cn/upload/article/001/503/042/174488066173494.jpg" alt="【Linux系统IO】六、动静态库"></figure><p>​ 这里还存在一个奇怪的地方:main 的依赖库中并看不到 libmymath.a,并且 main 是动态链接的;这是由如下原因造成的:</p> <p>​ 1、gcc 默认使用动态链接(只是建议行为),这是针对动静态库都存在的情况说的,如果只存在静态库,那么 Linux 也只能使用静态链,但是如果存在动态库,即使指明 Static 选项也只会使用动态链接;</p> <p>​ 2、一个可执行程序的形成可能不仅仅只依赖一个库,如果依赖的库中有一部分不只有静态库,有一部分库有动态库,那么形成的可执行程序整体是动态链接的,但其中只有静态库的地方才会进行静态链接;</p> <p>​ 3、这里的现象和第二点一样,main 的形成不仅仅依赖一个库 (使用了 C 语言库函数),且 Linux 中存在 C 语言动态库,所以这里是使用动态链接的,而我们自己的库 libmymath.a 以静态的方式进行链接。</p>Ⅲ. 动态库1、动态库的概念<p>​ 动态库:这类函数库通常名为 libxxx.so 或 xxx.dll 。</p> <p>​ 与静态函数库被整个捕捉到程序中不同,动态函数库在编译的时候,在程序里只有一个 “指向库” 的位置而已,也就是说当 可执行文件需要使用到函数库的机制时,程序才会去读取函数库来使用;也就是说 可执行文件无法单独运行。这样从产品功能升级角度方便升级,只要替换对应动态库即可,不必重新编译整个可执行文件。</p> <p>​ 动态库可以在多个程序间共享,所以动态链接使得 可执行文件更小,节省了磁盘空间。<a style="color:#f60; text-decoration:underline;" title="操作系统" href="https://www.php.cn/zt/16016.html" target="_blank">操作系统</a>采用 虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。</p> <p>​ 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。这种链接方式,是用于解决静态库存在的浪费内存和磁盘空间,以及解决模块更新困难等问题。</p> <p>​ 动态链接生成可执行程序,可执行程序中会记录自己依赖的库列表以及库中的函数地址信息,等到运行程序的时候,由操作系统将库加载到内存中(多个程序可以共享,不需要加载多份相同实例),然后根据库加载后的地址在对每个程序内部用到的库函数的地址进行偏移计算。</p> <p>​ 动态库也叫运行时库,是运行时加载的库,将库中数据加载到内存中后,每个使用了动态库的程序都要根据加载的起始位置计算内部函数以及变量地址,因此动态链接动态库加载及运行速度是不如静态链接的,但是它也有好处,就是多个程序在内存中只需要加载一份动态库就可以共享使用。</p> <figure class=""><hr></figure><p>基于这么一种思想,动态链接具有以下优缺点:</p> <p>优点:</p>节省内存并减少页面交换;库文件与程序文件独立,只要输出接口不变,更换库文件不会对程序文件造成任何影响,因而极大地提高了可维护性和可扩展性;不同编程语言编写的程序只要按照函数调用约定就可以调用同一个库函数;适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。<p>缺点:</p>运行时依赖,所以找不到库文件就会运行失败加载动态库的程序运行速度相对较慢,因为动态库运行时加载,映射到虚拟地址空间后需要重新根据映射起始地址计算函数/变量地址需要对库版本之间的兼容性做出更多处理二、动态库的封装<p>动态库的制作和静态库存在很多相似的地方,但也有不同:</p>动态库汇编 形成 .o 文件需要指定 fPIC 选项,用于 形成位置无关码(与位置无关,库文件可以在内存的任意位置加载,不影响其他程序的关联性)动态库归档不使用 ar 指令,而是 在 gcc 中指定 shard 选项就可以完成归档工作,表示生成共享库形式。<p>​ 同样的,知道封装动态库的知识后,我们将所有 .o 文件进行打包并与头文件合并成目录:</p>代码语言:javascript<i class="icon-code"></i>代码运行次数:<!-- -->0<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewbox="0 0 16 16" fill="none"><path d="M6.66666 10.9999L10.6667 7.99992L6.66666 4.99992V10.9999ZM7.99999 1.33325C4.31999 1.33325 1.33333 4.31992 1.33333 7.99992C1.33333 11.6799 4.31999 14.6666 7.99999 14.6666C11.68 14.6666 14.6667 11.6799 14.6667 7.99992C14.6667 4.31992 11.68 1.33325 7.99999 1.33325ZM7.99999 13.3333C5.05999 13.3333 2.66666 10.9399 2.66666 7.99992C2.66666 5.05992 5.05999 2.66659 7.99999 2.66659C10.94 2.66659 13.3333 5.05992 13.3333 7.99992C13.3333 10.9399 10.94 13.3333 7.99999 13.3333Z" fill="currentcolor"></path></svg>运行<svg width="16" height="16" viewbox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 15.5V3.5H14.5V15.5H4.5ZM12.5 5.5H6.5V13.5H12.5V5.5ZM9.5 2.5H3.5V12.5H1.5V0.5H11.5V2.5H9.5Z" fill="currentcolor"></path></svg>复制<pre class="prism-token token line-numbers javascript">libmymath.so : add.o sub.o   # 加上-shared封装静态库gcc -shared -o $@ $^ %.o : %.c    # %的作用是匹配目录下的.c文件集合生成.o文件集合,与*号类似,但是%多用于makefile,且使用范围不太一样gcc -fPIC -c $<figure class=""><img src="https://img.php.cn/upload/article/001/503/042/174488066285836.jpg" alt="【Linux系统IO】六、动静态库"></figure>三、动态库的使用<p>​ 和静态库一样,我们先将 mylib 目录文件压缩,然后发到使用方那边再解压。接着还是一样,我们在编译链接成可执行文件的时候必须加上三个选项:头文件路径、库文件路径、库文件名! </p><figure class=""><img src="https://img.php.cn/upload/article/001/503/042/174488066289355.jpg" alt="【Linux系统IO】六、动静态库"></figure><p>​ 很奇怪啊,为什么找不到我们写的动态库呢,我们明明已经把头文件路径、库文件路径和库文件名都加上去了啊,为什么还是找不到啊 ❓ ❓ ❓</p><p>​ 其实是因为虽然说我们是告诉了 gcc 我们的头文件路径等,但是当我们编译链接生成可执行文件之后,gcc 可就不管我们了啊,我们执行一个可执行文件,这是和操作系统有关系的,通过加载到内存变成进程从而管理,但是操作系统哪里知道我们告诉了它这些头文件路径等等呢,并且我们的库也不在系统中,所以 操作系统 和 shell 才会找不到!</p><p>​ 所以要执行可执行文件的话我们必须告诉 操作系统 和 shell 关于库的路径!下面介绍四种方法!</p>方案一:更改环境变量LD_LIBRARY_PATH – 短暂性<p>​ 在系统中有个环境变量叫做 LD_LIBRARY_PATH,其中该环境变量中放的就是一些指定的动态链接库的路径,我们可以利用 export 指令将我们要存放的动态链接库的路径添加进去,注意要添加绝对路径! </p><figure class=""><img src="https://img.php.cn/upload/article/001/503/042/174488066236729.jpg" alt="【Linux系统IO】六、动静态库"></figure><p>​ 这里需要注意的是:添加环境变量后,默认只在本次登录有效,下次登录时无效(默认清理登录前一次添加环境变量)。如果想让这个环境变量永久生效,可以把这个环境变量添加到登录相关的启动脚本里,下面两个都行,但是不建议,如果真要改,多开几个终端,防止改了之后登不上 Linux:</p>代码语言:javascript<i class="icon-code"></i>代码运行次数:<!-- -->0<svg xmlns="http://www.w3.org/2000/svg" width="16"    style="max-width:90%" viewbox="0 0 16 16" fill="none"><path d="M6.66666 10.9999L10.6667 7.99992L6.66666 4.99992V10.9999ZM7.99999 1.33325C4.31999 1.33325 1.33333 4.31992 1.33333 7.99992C1.33333 11.6799 4.31999 14.6666 7.99999 14.6666C11.68 14.6666 14.6667 11.6799 14.6667 7.99992C14.6667 4.31992 11.68 1.33325 7.99999 1.33325ZM7.99999 13.3333C5.05999 13.3333 2.66666 10.9399 2.66666 7.99992C2.66666 5.05992 5.05999 2.66659 7.99999 2.66659C10.94 2.66659 13.3333 5.05992 13.3333 7.99992C13.3333 10.9399 10.94 13.3333 7.99999 13.3333Z" fill="currentcolor"></path></svg>运行<svg width="16" height="16" viewbox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 15.5V3.5H14.5V15.5H4.5ZM12.5 5.5H6.5V13.5H12.5V5.5ZM9.5 2.5H3.5V12.5H1.5V0.5H11.5V2.5H9.5Z" fill="currentcolor"></path></svg>复制<pre class="prism-token token line-numbers javascript">vim ~/.bash_profilevim ~/.bashrc

方案二:将动态库和头文件拷贝至对应的系统库路径(/lib64)和头文件路径(/usr/include)下(不推荐)

​ 这个我们上面讲过,这里就不讲了,并且不推荐,因为会污染系统文件池!

方案三:配置文件 –永久性代码语言:javascript代码运行次数:0运行复制

[liren@VM-8-2-centos use_library]$ ll -d /etc/ld.so.conf.d/drwxr-xr-x. 2 root root 4096 Jul 25  2022 /etc/ld.so.conf.d/

​ 在我们系统中存在一个系统搜索动态库的路径配置文件目录 /etc/ld.so.conf.d/ ,我们可以在这个目录创建 .conf 配置文件,向配置文件中添加我们的动态库的 绝对路径 即可!(这个配置文件的名称是可以随便取的)

​ 添加绝对路径后,我们还要使用 ldconfig 指令来更新一下这些配置文件,才能生效!

【Linux系统IO】六、动静态库

方案四:创建软链接

​ 在当前文件路径下建立软链接,注意这个软链接的名称要和动态库的名称一样,因为在寻找库的时候我们指定了用库文件名!

【Linux系统IO】六、动静态库

Ⅳ. 动静态库的加载一、静态库的加载

​ 首先,我们要知道,静态库不需要加载!

​ 这个过程,在磁盘中 main.c 和 lib.c 库,会先在磁盘中形成一段代码,然后对这段代码进行编译,编译的本质就是预处理,编译-查找错误,形成二进制代码,然后进行汇编形成二级制指令。在编译阶段的时候就已经形成了虚拟地址空间。

​ 在虚拟地址空间中,这段代码也就被存入代码区,这个是根据不同区的特性所决定的。当执行这段代码的时候,操作系统就会直接在代码区进行访问。

【Linux系统IO】六、动静态库

二、动态库的加载

​ 动态库加载的过程,在磁盘中有一个 my.exe(可执行)和 lib.so(动态库),在形成可执行之前,编译阶段时,我们用到了 fPIC(产生位置无关码)。

​ 在这个阶段,动态库会将指定的函数地址,写入到可执行文件中。这个地址可以理解成 my_add.c(地址) + 偏移地址。

​ 形成可执行文件之后,磁盘将可执行文件拷贝到内存中,内存通过页表映射到虚拟地址空间的代码区中,当 OS 执行程序时,扫描到 my_add.c 是需要调用动态库的时候,程序会停下来,OS 会再通过函数的地址,然后页表映射去内存到磁盘中找动态库中,找到后拷贝到内存,又通过页表映射到共享区中。OS 再去 共享区 调用该方法,然后向下执行程序。

​ 注意:动态库可以避免静态库内存空间浪费的问题,这是由于如果多个进程链接了同一个动态库,动态库也只需要加载一次。动态库被加载到物理内存中并通过页表映射到某一个进程(假设A进程)的共享区之后,操作系统会记录该动态库在A进程共享区中的地址,当其他进程也需要执行动态库代码时,操作系统会根据记录的地址加上偏移量通过页表跳转到A进程的共享区中执行函数,执行完毕后再跳回到当前进程地址空间的代码段处。

​ 所以 从始至终物理内存中都只有一份动态库代码。

【Linux系统IO】六、动静态库

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享