【Linux】动静态库

目录

静态库与动态库

         打包静态库

         使用静态库

         动态库打包

         使用动态库


静态库与动态库

静态库( .a ): 程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
 
动态库( .so ): 程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
 
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
 
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking
 
动态库可以在多个程序间共享,所以 动态链接使得可执行文件更小 ,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
我们为什么要使用别人的代码?
使用顶尖大佬的高质量的代码,可以提高我们的开发效率,和增强代码健壮性
怎样使用别人的代码?
1. 使用别人提供的库
2. 使用开源的代码
库通常分为动态库和静态库,
动态库一般的命名为libc.so
 
静态库一般的命名为libc.a
去掉前缀lib,去掉.之后的内容,剩下的就是库的名字,这里就是c库,生成可执行程序的方式有两种: 动态链接和静态链接
当我们程序编译成功后,我们可以通过ldd命令:查看可执行程序的动态链接关系
在linux当中,默认情况下形成的可执行程序是动态链接的,所以在这里我们看到的是以.so为后缀的。
动态链接和静态链接与前面说的软硬链接类似,动态链接就是把程序所需要的方法所在的地址给程序,当程序需要的时候直接根据地址去寻址所需要的方法。静态链接则是把方法拷贝一个放在程序当中,使用的时候直接在程序内进行调用即可,随时用随时调,不需要去寻找了。
当调用库的频率增加时,就会变得很麻烦,因为会频繁的去根据地址进行寻找。这个时候我们将可执行程序中所需要使用的二进制代码拷贝进我们的可执行当中,这就叫做静态链接。
在我们程序的编译器选择时,绝大多数时候是选择的动态链接。这个时候如果我们必须需要使用静态链接的话,那就必须在gcc后面+static选项:一般为了更好的支持开发,第三方库或者语言库,必须提供两个库,一个叫做静态库,一个叫动态库,方便程序员根据需要进行bin的生成

动态链接的特点:体积小,节省资源(磁盘,内存),但是一旦库丢失,bin不可执行

静态链接的特点:体积大,浪费资源(磁盘,内存),不依赖库,库丢失不影响可执行程序

动静态库的本质是可执行程序的“半成品”。

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

预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件。


编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件。


汇编: 将汇编指令转换成二进制指令,最终形成xxx.o文件。


链接: 将生成的各个xxx.o文件进行链接,最终形成可执行程序。

动态库

动态库是程序在运行的时候才去链接相应的动态库代码的,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。

在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。动态库在多个程序间共享,节省了磁盘空间,操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。其中库被加载到共享区。

我们的可执行程序,编译成功,没有加载运行,二进制代码中也有地址。因为我们直接看代码不用加载运行,我们也可以在我们大脑中运行这个程序。

在编译的时候,编译器会对程序的每一行进行一个编址,也就是虚拟地址(逻辑地址)

他是采用平坦模式 从 0000......0000——FFFF......FFFF进行编址。

比如说下面的程序给main函数编址:

其中使用到了ELF加载器:他的作用主要是将存储在磁盘上的ELF文件读取到内存中。以及设置必要的寄存器值,如程序计数器(PC指针)指向入口点(entry point),以及其他必要的初始化操作。

他有许多应用场景比如:

  1. 操作系统启动:在操作系统启动时,需要加载内核和启动程序,这些程序通常以ELF格式存储。
  2. 应用程序执行:用户或系统服务在执行应用程序时,会通过ELF加载器将应用程序加载到内存中并运行。
  3. 动态库加载:当应用程序依赖于动态库时,ELF加载器会在程序执行过程中动态地加载所需的库文件。

简单理解程序运行:

程序运行首先要创建进程,在创建进程阶段,初始化进程地址空间。

初始化的值哪里来?当然是从可执行程序来。因此虚拟地址空间的概念,不是OS独有的,而是被精心设计过的,要OS,编译器,加载器共同配合完成。

让PC 指针初始化指向程序的入口——main函数的地址。然后加载每一行代码和数据到物理内存,这样就有了物理地址。同时自己的虚拟地址自己也知道,这样就构建了虚拟到物理地址的映射了。

然后去执行PC指针指向的虚拟地址的程序。

通过页表转换为物理地址。然后执行该代码。

然后将PC指针指向下一条指令的虚拟地址。就这样往复的转换。一条条指令就被执行了。

函数调用其实就是在我们地址空间内来回横跳。

这里有一个问题,动态库与可执行文件分别编址,在可执行文件调用动态库的时候,会不会产生地址冲突?

答案是不会的。虽然动态库的虚拟地址可能与可执行文件的相同(不同进程有各自的进程地址空间)。但是动态库加载进去的时候,由于动态库是由平坦模式编址而来,动态库的虚拟地址就可以转换为偏移量。加载进共享区的时候只需要在一段足够的内存中选择一个起点然后依次往后面加载就行了,访问的时候只需要根据起点加偏移量去访问就行了。

动态库由于是共享库,只需要被加载一次,其他程序如何判断有没有被加载呢?

先组织再描述。程序只需要遍历一遍已加载的库就能发现了。

         打包静态库

打包动静态库与使用动静态库

当我们需要将程序给别人使用但是又不能给源代码的时候,我们呢就需要打包动静态库。

比如我们需要打包以下程序代码:

add.c

#include"add.h"

int myadd(int x,int y)

{

        return x+y;

}

add.h

#pragma once

#include<stdio.h>

int myadd(int x,int y);

sub.c

#include"sub.h"

int mysub(int x,int y)

{

        return x-y;

}

sub.h

#pragma once

#include<stdio.h>

int mysub(int x,int y);

我们想让别人能使用我的库,前提是别人需要首先知道我们的库能给别人提供什么方法,通过头文件体现,然后将源文件编译生成目标文件:生成.o文件,这个.o文件可以被别人链接

g++ -c add.c

g++ -c sub.c

然后使用ar 命令 打包生成静态库:

ar命令是gnu的归档工具,常用于将目标文件打包为静态库,下面我们使用ar命令的-r选项和-c选项进行打包。

ar -rc libmylib.a file1.o fil2.o ... filen.o

此外,我们可以用ar命令的-t选项和-v选项查看静态库当中的文件。

-t:列出静态库中的文件。
-v(verbose):显示详细的信息。 

将头文件和生成的静态库组织起来

当我们给别人库的时候需要给两个文件,一个是头文件,一个是库文件。

因此,在这里我们可以将add.h和sub.h这两个头文件放到一个名为include的目录下,将生成的静态库文件libcal.a放到一个名为lib的目录下,然后将这两个目录都放到mathlib下,此时就可以将mathlib给别人使用了。

         使用静态库

方法一:使用选项

代码

#include<iostream>
#include<cstdio>
#include"add.h"
#include"sub.h"

int main()
{
	int x = 20;
	int y = 10;
	int z = my_add(x, y);
	std::cout<<"x + y ="<<z<<std::endl;
	return 0;
}

选项

-I(大i):指定头文件搜索路径。
-L:指定库文件搜索路径。
-l(小L):指明需要链接库文件路径下的哪一个库。

g++ test.cc -I/home/HCC/linux/code14/mathlib/include -L/home/HCC/linux/code14/mathlib/lib -lmycode 

执行结果:

因为编译器不知道你所包含的头文件add.h在哪里,所以需要指定头文件的搜索路径。
因为头文件add.h当中只有my_add函数的声明,并没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。


而在实际中,在库文件的lib目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。


-I,-L,-l这三个选项后面可以加空格,也可以不加空格。

方法二:头文件和库文件拷贝到系统目录下

在编译过程中,编译器会默认在系统路径下去寻找。那么我们同样的可以将头文件和库文件拷贝到系统路径下去。这样我们就不需要用选项指定路径了。

sudo cp mathlib/include/* /usr/include/

sudo cp mathlib/lib/libmycode.a /lib64/

虽然将两个文件放入了系统文件,但是我们仍然需要指定用哪一个库

为什么之前使用g++编译的时候没有指明过库名字?

因为我们使用g++编译的是C++语言,而g++就是用来编译C++程序的。

所以g++编译的时候默认就找的是C++库,但此时我们要链接的是哪一个库编译器是不知道的因此我们还是需要使用-l选项,指明需要链接库文件路径下的哪一个库

扩展知识:
实际上我们拷贝头文件和库文件到系统路径下的过程,就是安装库的过程。但并不推荐将自己写的头文件和库文件拷贝到系统路径下,这样做会对系统文件造成污染。因此我们尽量避免用此种方法。如果非要这样,一般是将大佬写的比较好的库放进去。

         动态库打包

动态库的打包相对于静态库来说有一点点差别,但大致相同,我们还是利用这四个文件进行打包演示:

第一步:让所有源文件生成对应的目标文件

此时用源文件生成目标文件时需要携带-fPIC选项:

-fPIC(position independent code):产生位置无关码。

-fPIC作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的

如果不加-fPIC选项,则加载.so文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的拷贝,并且每个拷贝都不一样,取决于这个.so文件代码段和数据段内存映射的位置。

不加-fPIC编译出来的.so是要在加载时根据加载到的位置再次重定位的,因为它里面的代码BBS位置无关代码。如果该.so文件被多个应用程序共同使用,那么它们必须每个程序维护一份.so的代码副本(因为.so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)。

我们总是用-fPIC来生成.so,但从来不用-fPIC来生成.a。但是.so一样可以不用-fPIC选项进行编译,只是这样的.so必须要在加载到用户程序的地址空间时重定向所有表目。

第二步:使用 -shared 选项 和 gcc / g++ 将对象文件链接成动态库 (.so 文件) 

与生成静态库不同的是,生成动态库时我们不必使用ar命令,我们只需使用gcc的-shared选项即可。

第三步:将头文件和生成的动态库组织起来

         使用动态库

使用该动态库的方法与刚才我们使用静态库的方法一样。

我们既可以使用 -I,-L,-l这三个选项来生成可执行程序,

也可以先将头文件和库文件拷贝到系统目录下。

然后仅使用-l选项指明需要链接的库名字来生成可执行程序。

与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。

我们使用-I,-L,-l这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁。

但是当生成的可执行程序生成后就与编译器没有关系了,后面当可执行程序运行起来后,操作系统就找不到该可执行程序所依赖的动态库。因为静态库是将库加载进可执行文件的。而动态库则是运行的时候根据依赖文件去寻找库。

此时我们用ldd命令查看可执行文件:

我们发现找不到依赖的动态库。

下面解决这个问题,解决问题的方法有3个:

方法一:拷贝.so文件到系统共享库路径下

将 .so 文件拷贝到系统共享库路径下, 一般指 /usr/lib (不推荐把自己写的库放进去,推荐放大神的)

既然系统找不到我们的库文件,那么我们直接将库文件拷贝到系统能找到文件的地方——系统共享的库路径。

方法二:更改LD_LIBRARY_PATH

更改 LD_LIBRARY_PATH 环境变量来指明 .so 文件所在路径。 

LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量当中即可。

此时我们再用ldd命令查看该可执行程序就会发现,系统现在就可以找到该可执行程序所依赖的动态库并且能正常运行程序了。

当然这种方法不是永久的,只是内存级的,当我们下次登陆远程服务器的时候依然不能运行此程序,如果想要永久能运行,则还需要修改环境变量的配置文件 .bashrc 

方法三:配置/etc/ld.so.conf.d/

我们可以通过配置/etc/ld.so.conf.d/的方式解决该问题,/etc/ld.so.conf.d/路径下存放的全部都是以.conf为后缀的配置文件。

而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径。

之后就会在每个路径下查找你所需要的库。我们若是将自己库文件的路径也放到该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件了。

首先将库文件所在目录的路径存入一个以.conf为后缀的文件当中。

echo /home/HCC/linux/code14/mathlib/lib > HCC.conf

然后将该.conf文件拷贝到/etc/ld.so.conf.d/目录下。

此时我们用ldd命令查看可执行程序时,发现系统还是没有找到该可执行程序所依赖的动态库。

这时我们需要使用ldconfig命令将配置文件更新一下,更新之后系统就可以找到该可执行程序所依赖的动态库了。

然后我们就能正常运行程序了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

何陈陈

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

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

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

打赏作者

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

抵扣说明:

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

余额充值