目录
静态库与动态库
静态库( .a ): 程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库动态库( .so ): 程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking )动态库可以在多个程序间共享,所以 动态链接使得可执行文件更小 ,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
使用顶尖大佬的高质量的代码,可以提高我们的开发效率,和增强代码健壮性
1. 使用别人提供的库2. 使用开源的代码
动态库一般的命名为libc.so静态库一般的命名为libc.a
动态链接的特点:体积小,节省资源(磁盘,内存),但是一旦库丢失,bin不可执行
静态链接的特点:体积大,浪费资源(磁盘,内存),不依赖库,库丢失不影响可执行程序
从源文件和头文件最终变成一个可执行程序需要经历以下四个步骤:
预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件。
编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件。
汇编: 将汇编指令转换成二进制指令,最终形成xxx.o文件。
链接: 将生成的各个xxx.o文件进行链接,最终形成可执行程序。
动态库
动态库是程序在运行的时候才去链接相应的动态库代码的,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。动态库在多个程序间共享,节省了磁盘空间,操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。其中库被加载到共享区。
我们的可执行程序,编译成功,没有加载运行,二进制代码中也有地址。因为我们直接看代码不用加载运行,我们也可以在我们大脑中运行这个程序。
在编译的时候,编译器会对程序的每一行进行一个编址,也就是虚拟地址(逻辑地址)。
他是采用平坦模式 从 0000......0000——FFFF......FFFF进行编址。
比如说下面的程序给main函数编址:
其中使用到了ELF加载器:他的作用主要是将存储在磁盘上的ELF文件读取到内存中。以及设置必要的寄存器值,如程序计数器(PC指针)指向入口点(entry point),以及其他必要的初始化操作。
他有许多应用场景比如:
- 操作系统启动:在操作系统启动时,需要加载内核和启动程序,这些程序通常以ELF格式存储。
- 应用程序执行:用户或系统服务在执行应用程序时,会通过ELF加载器将应用程序加载到内存中并运行。
- 动态库加载:当应用程序依赖于动态库时,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命令将配置文件更新一下,更新之后系统就可以找到该可执行程序所依赖的动态库了。
然后我们就能正常运行程序了