Linux第九讲:动静态库
1.静态库的制作 && 什么是库
无论是ubunto操作系统,还是centos操作系统,都有对应的静态库和动态库,下面我们通过静态库的制作来了解一下什么是库:
如果需要链接的.h文件不在当前路径下呢?我们该如何进行链接?:
系统只能在特定的路径进行查找,我们当然可以将自己实现的库,保存在自动查找的路径上:
我们进行一个小总结:
gcc main.c -I 头⽂件路径 -L 库⽂件路径 -l mymath
-L: 指定库路径
-I: 指定头文件搜索路径
-l: 指定库名
1.1静态库生成
libmyc.a:mystdio.o mystring.o
ar -rc $@ $^
mystdio.o:mystdio.c
gcc -c $<
mystring.o:mystring.c
gcc -c $<
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mylib
cp -f *.h lib/include
cp -f *.a lib/mylib
tar czf lib.tgz lib
.PHONY:clean
clean:
rm -rf *.o libmyc.a lib lib.tgz
这样,当别人想要使用自己实现的库进行链接时,我们make output一下,就可以将我们的lib目录或者是打包的文件发送,我们使用tar -xzvf archive.tgz就可以进行解包了:
//解包操作
tar -xzvf lib.tgz
//静态库链接操作
gcc -o usercode usercode.c -I lib/include/ -L ./lib/mylib/ -lmyc
然后就可以形成可执行文件了!
2.动态库
2.1动态库生成
libmyc.so:mystdio.o mystring.o
gcc -shared -o $@ $^
mystdio.o:mystdio.c
gcc -fPIC -c $<
mystring.o:mystring.c
gcc -fPIC -c $<
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mylib
cp -f *.h lib/include
cp -f *.so lib/mylib
tar czf lib.tgz lib
.PHONY:clean
clean:
rm -rf *.o libmyc.so lib lib.tgz
动态库的生成与静态库的生成有所不同,我们需要注意一下。这时我们何上面那样,进行打包,解包,然后gcc进行链接,这时出现了问题:
那么这就出现问题了:我已经将链接哪个库,在哪里找到该库等信息全都告诉你了,为什么还是执行不了呢?为什么静态库没有这个问题呢?
我们可以使用ldd指令查看可执行文件的链接状态,可以看到,确实是找不到目标动态库了
2.2静态链接和动态链接的区别
原因是:
1.静态库:程序在编译链接时就已经把库的代码和数据拷贝到了可执行文件中,所以可执行程序的执行就不需要静态库的存在了
2.动态库:程序在执行时才去链接动态库的代码和数据
3.而且,你是将路径告诉了gcc,gcc知道了从哪里获取库,获取哪一个库,但是系统不知道呀!执行可执行文件,是系统执行的呀!
2.3解决策略
那么动态库究竟要怎么进行链接呢?:
2.3.1将我们写的动态库拷贝至系统
sudo cp ./lib/mylib/libmyc.so /lib64/
系统会自动在/lib64/路径下查找链接需要的动态库,当我们执行拷贝操作之后,就能够直接执行可执行文件了!:
2.3.2建立软链接
sudo ln -fs /home/test/LinuxClass/class15/zhangsan/lib/mylib/libmyc.so /lib64/libmyc.so
建立软连接,也可以找到需要链接的动态库,这个方法也是比较常用的方法
//取消建立链接
sudo unlink /lib64/libmyc.so
2.3.3LD_LIBRARY_PATH
操作系统运行程序,要查找动态库,也会在该环境变量中查找动态库:
//将我们的动态库的路径导入到该环境变量中
export LD_LIBRARY_PATH=/home/test/LinuxClass/class15/zhangsan/lib/mylib:$LD_LIBRARY_PATH
$LD_LIBRARY_PATH:保留原有的LD_LIBRARY_PATH值,以便在添加新路径的同时,不覆盖已有的路径。
这样就可以直接执行了:
需要注意的是:该环境变量在每次退出之后都会清空,因为它是内存级别的环境变量,所以我们可以将路径写到配置文件中,保证可以永久使用该动态库:
2.3.4ldconfig方案:配置/etc/ld.so.conf.d/
这个比较麻烦,我们只了解即可:
2.4几个结论输出
1.gcc/g++默认使用的是动态库,当我们既有静态库、又有动态库时,默认进行动态库链接,如果没有进行配置,就会出现报错!如果我们非要静态链接,在代码后面加上-static即可,但是一旦加上了-static,就只能进行静态链接,也就是说,如果只有动态库存在的情况下,加上-static,就不考虑动态链接的情况了
2.在Linux系统下,我们默认install、yum等操作,默认都优先安装动态库!
3.可能会有多个应用程序使用同一个库,也就是库:应用程序 = 1:n
4.vs在下载的过程中,想要编译C/C++代码,我们需要按照教程点击C++桌面开发等内容进行下载,其实下载的就是C/C++的标准库!所以说vs既可以形成可执行程序,也就是exe文件,也可以形成动静态库!只是需要配置罢了!感兴趣的可以询问大模型
3.使用外部库 – 感兴趣尝试
使用指南: link
我们首先需要安装ncurses,这是一个图形库,可以在Linux下展示一些有趣的图形界面:
// 安装
// Centos
$ sudo yum install -y ncurses-devel
// ubuntu
$ sudo apt install -y libncurses-dev
4.目标文件
.o/.obj文件被称为目标文件,其实叫做可重定位目标文件,它可以与其它文件进行链接,生成可执行程序
而为什么要将.c文件先生成.o文件之后,再将所有的.o文件与库进行链接,才能生成可执行程序呢?为什么不是将.c文件直接与库进行链接呢?:
1.先将.c文件编译为.o文件,这样当一份.c文件做出修改之后,只需要将这一份的.c文件再次编译成.o文件,再进行链接,就可以生成可执行程序了。对此,编译器其实做了很多隐藏的任务,当我们只对一份源文件进行修改,make之后,其实只有修改的那一份源文件进行了编译!
2.源文件并不是ELF格式,下面我们会讲什么是ELF文件格式
5.ELF文件
我们先说一下常见的ELF文件:
1.可重定向文件:.o文件
2.可执行文件:.exe文件
3.共享库文件:.so .a文件
ELF文件其实就是将源文件以一定的格式放入到二进制文件中
5.1ELF文件格式
ELF文件的格式如下:
我们现在只需要了解:
1.ELF文件,对于相同属性的代码和数据,放在一个section(节)中
2.代码节(.txt):保存机器指令,是程序的主要执行部分
3.数据节(.data):保存已初始化的全局变量和静态局部变量
5.2ELF从形成到加载轮廓
5.2.1ELF形成可执行
.o文件和库文件都是ELF文件,那么形成可执行文件的具体操作是什么?:
更多的细节操作我们不追究
5.2.2ELF可执行文件加载
我们先对ELF做出一个详细的了解:
所以ELF可执行文件怎么加载到内存的,我们已经简单了解了,下面会详细讲解加载到内存的全部操作的,我们先来看一些问题:
1.为什么要将section合并为segment:
1.减少页面碎片,提高内存使用效率:
磁盘是以块(4kb)为单位存储的,即使是1字节的数据,也需要一个块来存储,内存也是这样,也是按照块来存储的。假设.text部分占用4097个字节,一个页面可以存储4096个字节,所以.text部分就会占用两个页面,.init部分占用512字节,也就是1个页面,当进行合并时,.text部分会和.init部分合并,从而使页面使用率提高,减少了页面碎片。
2.我们怎么理解程序头表和节头表呢?我们从两个不同的视角来看:
1.链接视角 – 对应头节表(Section Header Table):
头节表中存储的是对节的描述,包括不同的节的地址,当进行链接操作时,需要知道各种的节地址,才能够执行链接操作
2.执行视角 – 对应程序头表(Program header table)
程序头表中存储的是所有的段对应的属性,当进程执行时,必须要告诉操作系统,如何完成内存的初始化,这时需要程序头表
3.symtable节 && Magic
5.2.3地址和偏移量之间的关系
6.理解链接与加载
有了上面的基础知识储备之后,我们看一下动静态库的链接和加载操作:
6.1静态链接
静态库的链接,其实就是将.o文件进行连接的过程,所以我们只需要研究.o文件的链接即可:
所以,.o文件被称为可重定向目标文件,其实就是在链接的过程中,会进行重定向操作!
6.2ELF加载与进程地址空间
6.2.1虚拟地址/逻辑地址
一个ELF文件,在汇编过程,就已经为可执行程序进行编址了,对于可执行程序来说,它包含代码段、数据段,其实也就是不同的segment,进程创建时,mm_struct和vm_area_struct的初始化数据就从Program Header Table中来
6.2.2重新理解虚拟地址空间
之前我们讲虚拟地址空间,知道了PCB、mm_struct、vm_area_struct,但是并没有深入到磁盘级别,我们今天重新理解一下:
所以,我们就知道了可执行程序执行的详细过程了!
6.3动态链接与动态库加载
6.3.1进程如何看待动态库 && 共享库
我们知道了进程如何看待可执行文件,但是动态库也是ELF文件,那么是如何看待动态库的,以及引入共享库的概念:
6.3.2动态链接
静态链接的缺陷在于:会将进程运行所需要的各种库,合并成一个独立的可执行文件,它不需要额外的依赖就可以运行。但是生成的文件体积大,当多个进程运行时,会造成多个相同库同时存在的情况。
而动态链接只需要在进程运行时存在一份库文件就可以实现进程的运行了!
6.3.2.1我们的可执行程序被编译器动了手脚
当我们反汇编我们的可执行程序后,会看到程序的真正入口 – _start:
0000000000400440 <_start>:
400440: 31 ed xor %ebp,%ebp
400442: 49 89 d1 mov %rdx,%r9
400445: 5e pop %rsi
400446: 48 89 e2 mov %rsp,%rdx
400449: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40044d: 50 push %rax
40044e: 54 push %rsp
40044f: 49 c7 c0 d0 05 40 00 mov $0x4005d0,%r8
400456: 48 c7 c1 60 05 40 00 mov $0x400560,%rcx
40045d: 48 c7 c7 3d 05 40 00 mov $0x40053d,%rdi
400464: e8 b7 ff ff ff callq 400420 <__libc_start_main@plt>
400469: f4 hlt
40046a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
在_start函数中,会执行一系列的初始化操作:
1.设置堆栈,为程序创建一个初始的堆栈环境
2.初始化数据段:将程序的数据段从初始化数据段复制到相应的内存位置,并清零未初始化的数据段
3.动态链接:这是今天要讲述的重要部分,要知道这个:_start函数会通过动态链接器提供的方法来解析和加载进程所依赖的动态库,处理所有的符号解析,让我们能够正确访问到库中的函数
4.调用__libc_start_main函数:动态链接完成之后,会调用该函数,负责执行一些额外的初始化工作,如信号处理等
5.main函数的调用:然后才会调用我们的main函数
6.处理main函数的返回值:当main函数返回时,__libc_start_main函数会处理该返回值,并最终调用_exit函数来终止进程
我们可以看一下动态链接器:
6.3.2.2动态库中的相对地址
动态库也是ELF,我们也可以理解为:起始地址(0) + 偏移量:
6.3.2.3程序如何和库进行映射
6.3.2.4程序怎么进行库函数的调用
上面对代码区进行修改的方法称为加载地址重定位,需要二次完成地址设置
6.3.2.5全局偏移量表(GOT)
在.data中,会被预留一块区域,用于存放函数的跳转地址,叫做全局偏移量表GOT,而.data区域是可读写的
6.3.2.6库间依赖 && plt(延迟绑定)
不仅仅只有可执行程序会调用库,库间也会进行库的调用,也就是库间依赖,而库也是ELF格式,也有.got,从而实现对其它库的调用!
但是库间链接需要对大量的函数进行重定位,比较耗时,所以就进行了延迟绑定(plt)的优化策略,思路是不一次性进行所有函数的重定位操作了,因为有的函数可能需要多次使用,从而进行多次的重定位操作,优化为在函数第一次执行时进行重定位操作,并且更新got表,再次进行函数调用时,只需要查找got表即可!:
7.总结
1.静态链接的出现,提高了程序的模块化水平,比如在企业中,各组完成的项目可以独立地进行测试,最后通过静态链接,就可以生成最终的可执行文件
2.静态链接会将编译产生的所有目标文件,和各种库合并,生成一个独立的可执行文件,其中我们会修改模块间函数的跳转地址,这叫做编译重定位
3.动态链接实际上是将链接的过程推迟到了程序加载的时候,当进程运行时,会将动态库的代码和数据加载到内存中,但是无论被加载到哪里,都要被映射到进程对应的地址空间,然后通过.got进行调用