Linux基础IO(终)——动静态库和进程地址空间

1、静态库

在Linux中,libXXX.a表示的是静态库,libXXX.so表示的是动态库,静态库对应静态链接,动态库对应动态链接。库的名字就是lib和.之间的XXX。
如果我们想把自己写的方法给别人有,有两种方式:
1、直接把源文件给他。2、把我们的源代码想办法打包成库,通过库+.h头文件,别人就可以使用。

1.1、制作静态库

首先,我们站在库的制作者角度来看,先制作一个静态库。
在这里插入图片描述
首先创建mymath.h和mymath.c两个文件,在mymath.h中声明方法,在mymath.c中实现。由于除法函数被除数可能为0,所以设置了一个错误码errno。

下面使用make/makefile实现打包静态库并直接发布:
在这里插入图片描述在这里插入图片描述

打包成静态库的命令为:ar -rc,另外gcc默认带-c选项会将mymath.c经过预处理、编译、汇编形成mymath.o文件,所以不需要带-o。
打包成静态库还是动态库都需要先形成.o文件,所以C语言的库本质上就是将很多个.o文件打包成静态库或动态库。将我们的源文件形成.o文件跟库链接就形成了可执行程序。
静态库对应静态链接就是将库函数的代码拷贝到我的可执行程序中,动态库对应动态链接,当我们调用库函数需要跳转到库中去执行。


1.2、使用静态库

在这里插入图片描述
在main函数中调用div函数,输出结果和myerrno。
在这里插入图片描述
当前源文件和mylib目录处于同一个目录下,mylib里面有include和lib目录,include包含了头文件,lib包含了库文件。

下面我们使用gcc编译:
在这里插入图片描述
报错:找不到对应的头文件。
这是因为编译器默认会在系统的路径/usr/include和当前源文件所在的路径下查找头文件,但是这两个地方都没有我们的头文件,所以gcc找不到。

我们可以给gcc -I带大写i的选项指名头文件路径。这样gcc就会在系统路径/usr/include、当前目录,和我们指明的路径中查找头文件。
在这里插入图片描述
再次编译还是报错,但是这时候不是报头文件找不到的错误,说明头文件的问题已经解决了。
现在报的是链接时的错误,找不到静态库,跟头文件相似,gcc默认会在/lib64下(centos系统)或/lib/x86_64-linux-gnu(ubuntu系统)下查找,再在当前目录下查找,但是这两个地方都没有我们的静态库。

所以我们可以给gcc -L指名库文件所在路径:
在这里插入图片描述
我们发现还是报错,这是因为你虽然指明了库的路径,但是你没有指明哪个库,因为该路径下可能存在多个库文件,所以你还要告诉gcc编译器对应库文件的名字。

给gcc -l选项指明你要链接的库文件的名字:
在这里插入图片描述
最终形成了可执行程序a.out,运行结果也没有问题。
在这里插入图片描述
我们之前说过,gcc默认采用动态链接的方式,通过ldd查看也可以看到它动态链接了C标准库。而对于我们写的库,采用的是静态链接的方式。所以gcc默认采用动态链接,如果没有动态库就用静态链接。换句话说:有动用动,没动用静。

那为什么头文件不需要指明呢?因为源文件里面include的时候你已经指明头文件了。
那为什么C语言提供的库函数不需要带-l呢?它就是不需要,系统调用接口和C库函数都不需要。而以后我们只要用第三方库,那么使用gcc必定要带-l。

C语言提供了一个errno的全局变量,当我们调用库函数失败出错就会设置对应的errno错误码,然后我们就可以将错误码转换成错误信息,所以我们就可以知道出错的原因。


如果我觉得使用gcc带-I和-L麻烦,那么我们可以把将头文件拷贝到/usr/include目录下,将库文件拷贝到/lib64(centos)或/lib/x86_64-linux-gnu(ubuntu系统)下。这样使用gcc就不需要带-I和-L了。
在这里插入图片描述
这是在做什么?这不就是在安装库吗

拷贝是一种方式,我们还可以建立软链接:
在这里插入图片描述
在这里插入图片描述

1、以后使用第三方库的时候,必定要带gcc -l小写L。
2、如果系统中只提供静态链接,gcc只能对该库进行静态链接。
3、如果系统中需要链接多个库,gcc可以连接多个库。在后面继续添加-l(小写L)


2、动态库

2.1、制作动态库

在这里插入图片描述

在这里插入图片描述
创建mylog.h和mylog.c,实现Log函数打印日志信息。
创建myprint.h和myprint.c,实现Print函数打印字符串信息。

下面先看如何形成动态库:
在这里插入图片描述
1、gcc -fPIC -c 形成.o文件。
2、gcc -shared 形成动态库。

要形成动态库,也是需要先有.o文件,我们使用gcc -c选项先形成.o文件,但是需要带-fPIC。
fPIC:产生位置无关码,这个我们后面再讲解。

然后给gcc带上-shared选项表明要形成动态库。
并且我们注意到动态库文件带有可执行权限,这是因为可执行程序调用库函数需要跳转到动态库中执行,那么动态库就必须加载到内存中,而加载到内存就是程序运行起来,所以具有可执行权限。

下面演示使用make/makefile打包动静态库并直接发布:
在这里插入图片描述
在这里插入图片描述


2.2、使用动态库

在这里插入图片描述

下面演示动静态库同时使用,动态库也需要指明头文件和库文件路径,并且也需要指明动态库的名称。
在这里插入图片描述
如上,多个库文件就在后面加-l和名称即可。

下面去掉静态库,只使用动态库:
在这里插入图片描述
形成可执行程序后我们运行,发现报错:找不到动态库mymethod。使用ldd查看可执行程序,发现找不到动态库。
这是为什么呢?上面gcc不是带-l指明动态库的名字了吗?
上面的gcc告诉的是编译器,这里是系统,你告诉了编译器跟系统没有关系,所以你还得告诉系统。


2.3、解决加载找不到动态库的方法

1、将动态库拷贝到/lib64(centos)或/lib/x86_64-linux-gnu(ubuntu)目录下。
在这里插入图片描述


2、在系统默认的库路径/lib64(centos)或/lib/x86_64-linux-gnu(ubuntu)下建立软链接。
在这里插入图片描述


3、将自己库所在的路径添加到系统环境变量LD_LIBRARY_PATH中
环境变量LD_LIBRARY_PATH专门用来搜索用户自定义库路径。
在这里插入图片描述

但是重登之后就会失效,所以可以在.bash_profile(centos)或.profile(ubuntu)配置,这样每次登录都可以生效:
在这里插入图片描述
在这里插入图片描述


4、在/etc/ld.so.conf.d目录下建立自己的动态库路径配置文件,然后ldconfig重新加载配置文件即可。
在这里插入图片描述

实际情况我们用的库都是别人成熟的库,都采用直接安装到系统的方式。也就是直接拷贝到系统路径下。

动态库在进程运行的时候,是要被加载的(静态库不需要)。
常见的动态库被所有的可执行程序动态链接,那么这些可执行程序都要使用动态库,所以动态库是共享库。


2.4、动态库是怎么被加载的

在这里插入图片描述
程序运行起来操作系统要创建对应的PCB、进程地址空间,页表。然后将程序1.exe的代码和数据加载到内存中,虚拟地址空间通过页表映射到物理内存。现在在进程的代码中调用了库函数,那么就需要跳转到库中执行,所以动态库3.so需要加载到内存中,然后同样通过页表进行映射,通过地址空间的共享区映射到物理内存,所以动态库是加载到共享区的。然后在进程地址空间的正文代码遇到库函数,直接跳转到共享区去执行库函数代码,执行完之后再返回正文代码处继续向后执行。
那么系统中可能存在多个进程使用动态库3.so,比如程序2.exe也加载到内存中然后系统也创建PCB、地址空间、页表,然后实现虚拟地址到物理地址的映射。下面这个进程也调用库函数,那么还需要在内存中再开一块空间然后加载3.so吗?并不需要,如果有十个进程那这样我就要加载十份动态库,这样浪费内存。只需要加载一份动态库到内存中即可。然后同样下面这个进程通过虚拟地址空间的共享区映射到物理内存。下面这个进程要执行库函数同样跳转到共享区去执行,执行完再返回正文代码。
所以动态库是一个共享库。
结论:建立映射后,从此往后执行的任何代码,都是在我们的进程地址空间中进行执行!

另外,系统在运行中一定会存在多个动态库,所以操作系统也要管理这些动态库——先描述,再组织。系统中所有动态库加载的情况,OS非常清楚。

那么C语言的动态库libc.so不是有个全局变量errno吗?现在如果两个进程调用库函数都出错了,errno被设置,那么两个进程会互相影响吗?——并不会,进程间具有独立性,所以必定会发生写时拷贝,两个进程各自私有一份,不会相互影响。
从哪可以验证呢?从之前的缓冲区问题可以验证,C语言的缓冲区是C库FILE结构提供的,而我们调用C库函数用的是动态链接,所以C动态库必须加载到内存中,然后映射到进程地址空间的共享区。fork之后,父子进程刷新缓冲区到文件的页缓冲区就发生了写时拷贝,父子各自一份,所以最后写入到文件中的printf、fprintf、fwirte有两份。


3、进程地址空间第二讲

3.1、程序没有加载前的地址(程序)

在这里插入图片描述

首先,程序在被编译好之后,内部有地址的概念吗?
是有的,程序被编译好后,会形成一条一条的指令,每条指令都有对应地址,并且里面的地址已经是进程地址空间这样的形式了。所以程序编译好后里面的地址就是虚拟地址,但是此时程序还在磁盘上,所以我们称为逻辑地址。类似上图。


3.2、程序加载后的地址(进程)

在这里插入图片描述
之前我们说过可执行程序是有格式的——ELF,可执行程序有表头,表头里面有可执行程序的入口地址。所以操作系统先创建PCB、进程地址空间、页表,然后将可执行程序的entry:入口地址加载到CPU内的eip/pc寄存器,这样CPU就知道可执行程序的入口地址了。然后CPU要开始执行,通过地址空间找到页表,发现页表是空的,然后触发缺页中断,将可执行程序加载到内存中,可执行程序里的虚拟地址直接拿过来填到页表左边,同时加载到内存也有物理地址,对应的物理地址直接填到页表的右侧。这样页表就映射好了,然后通过页表找到物理地址,然后读取指令,执行对应的指令。并且CPU能知道每条指令的长度,执行完该条指令直接将pc里面的地址加上该指令的长度就是下一条指令的虚拟地址,然后再到地址空间、页表然后映射到物理内存,获取下一条指令的内容。

在这里插入图片描述
假设现在要调用某个函数,转换成汇编就是call某个地址,比如获取到call 4这条指令,那么也就是说CPU内读取到的指令,可能有数据也可能有地址。如果是地址的话,这个地址也一定是虚拟地址。 然后获取到这个虚拟地址,通过地址空间到页表,找到物理内存,继续获取指令向下执行。然后这样整个过程就转起来了。


3.3、动态库的地址

首先对于4GB的空间,从0x00000000到0xFFFFFFFF,其中某个地址为0x11223344,我们称这个地址为绝对地址。
假设你现在在跑道上跑步,这个跑道0-100米,然后你同学给你打电话,问你现在在哪,你说我在跑道上的30米处,这个就是绝对地址。然后又有人给你打电话,问你在哪里,跑道上有棵树,你说我在树右边的10米处,这就是相对地址。
在这里插入图片描述

引入问题:
程序1.exe代码和数据加载到内存,通过页表映射,然后执行代码,现在我要调用C的库函数printf了,并且我们说过可执行程序编译好就已经有地址了,这个地址是逻辑地址。那么假设编译好后可执行程序对应printf的地址为0x1122,那么执行库函数,在进程地址空间中,需要从正文代码跳转到共享区中去执行,然后再返回正文代码继续向后执行。现在问题是printf的地址为0x1122,也就是说我必须保证加载的动态库libc.so它的printf方法必须在0x1122这个地址处,但是我可能不只加载libc.so,可能还加载liba.so、libb.so、libd.so、libe.so,而且它们谁先被加载谁后加载也是不确定的,可能先加载的动态库把0x1122这个地址给占了,那么就导致libc.so中的printf无法加载到0x1122这个地址。

关键问题是:动态库具体映射到哪里?动态库要被加载到地址空间中的固定位置是不可能的。所以库可以在虚拟地址空间的任意位置进行加载
解决办法是:让库内部的函数不要采用绝对编址,只表示每个函数在库中的偏移量即可。

现在动态库你可以在进程地址空间中的共享区任意位置加载,你爱加载在哪就加载在哪,只要把你加载的起始地址记下来。因为操作系统对于动态库也要进行管理,管理就要先描述再组织,所以就能知道库的起始地址。那么现在我调用了printf,我就通过库的起始地址加上库函数在库中的偏移量就可以找到库函数。假设libc.so加载到库中的起始地址start为0x90000,可执行程序编译好后printf的地址为0x1122,那么就让libc.so库的printf函数加载到共享区的时候,printf要在起始地址偏移量为0x1122处即可。那么我调用就通过0x90000起始地址 + 0x1122偏移量,就可以找到库函数,执行完库函数再返回。

-fPIC:产生位置无关码:意思就是直接用偏移量对库中的函数进行编址。

静态库为什么不谈加载?——因为静态库是直接代码拷贝到我的可执行程序中,所以不需要加载。
静态库为什么不谈与位置无关?——因为代码拷贝进来我就可以直接采用绝对编址的方式了,没必要再用相对编址。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值