目录
动静态库的使用与制作
在开始之前,我们先来简略为大家介绍一下库的起始~
而这时候小强想要抄小明的作业,但怕老师发现又不能把源代码都抄下来,所以小明就只把汇编后的.o文件发给小强,让他自己去和main函数进行链接~
小强最终只需要一个库文件+一堆头文件就可以在自己的Main函数中实现与小明作业同样的效果了
为什么要有库呢?
隐藏源代码
提高开发效率
我们先把前段时间制作的仿c接口函数拿过来进行测试~
再添加一个加法函数~
最后我们在到main函数中实现需求:创建一个名为log.txt的文件与打印一下10+20的结果
#include "mymath.h" #include "mystdio.h" #include <stdio.h> int main() { int res = myAdd(10, 20); printf("%d+%d=%d\n", 10, 20, res); myFILE *fp = my_fopen("log.txt", "w"); if(fp == NULL) return 1; return 0; }
开始汇编,准备链接形成可执行文件
生成可执行文件,运行时实现加法效果与创建文件效果~
现在就相当于小明的作业已经完成了,接下来要处理的就是进行库的封装打包给小强~
我们把所有的.o文件都封装到静态库中~
重新整理后,我们到小强那实验库的使用~
注意我们在小强这里生成可执行文件时要用-l来指定链接我们自定义的库,因为gcc只认识c库但不认识自定义库,然后用-L表明当前库的路径,因为一般都是在库文件中寻找库的而非当前路径~
运行后发现小强在没有写核心源代码的情况下,仅靠小明给的静态库与头文件然后经过自己的main函数表明需求后成功实现需求~
接下来我们再来尝试封装一下动态库~
生成动态库需要以下几点条件:
- shared: 表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
重复之前的步骤把动态库交给小强
运行进程后实现需求~ 在只有main函数的情况下利用动态库与别人的头文件
最后我们再来搞一个发布版本,连.h也给打包到一个文件中~
再来搞个压缩包
我们再次来到小强这里,这一次只需要传给小强一个压缩包即可,解压后就可以得到包含.h的文件与动态库的文件~
这里出现了新的状况~找不到头文件了~所以我们在生成.o文件时要用-I告诉编译器在其指定的路径下搜索头文件~这也是老问题了,只会在默认头文件库中搜索~
后面-L就是告诉.o文件在链接时的库在哪里,-l就是链接的自定义库是哪一个~
可这样最后还是会失败~
这里又涉及到了另外一个知识点~
与静态库不同的是动态库不仅仅是在编译的时候告诉库的路径,头文件的路径,在运行时也要做同样的事情~编译是gcc处理的,而运行是OS处理的,二者无关联,所以需要重新说明路径~
而静态库只需要在编译时标明路径即可~
所以我们需要做的就是把库安装到系统中(/lib64),这样即可以支持编译也支持运行~
所谓把库安装到系统中,本质就是把对应的文件拷贝到系统指定的目录中~
第一种方式:直接将库拷贝到系统中~(推荐)
第二种方式:修改环境变量:系统运行时,动态库查找的辅助路径,我们只需要将不在其默认搜索下的库路径拷贝进去即可~
第三种方式:通过软链接(推荐)
第四种方式:改配置文件
创建好配置文件后往里面写入动态库路径即可~配置/etc/ld.so.conf.d/,ldconfig更新
最后成功处理~
拓展:
理解动态库的加载
最开始来看正常的代码与数据都是和进程空间内的代码等区域建立映射关系的~
而库文件中的数据与代码则是会和进程空间内的共享区建立映射关系~
代码执行期间需要用到库的方法时就会开始加载进内存里让cpu进行处理~而cpu就从共享区中找到库~这也反映出库函数的调用仍旧是在进程空间内进行的~
如果这时候有另一个进程需要调度库函数,那只需要到其共享区查到即可。在第一次需要库方法时该库就已经存储在物理内存中了,其他进程调用只需要查看其共享区(库会在新进程的页表中建立映射)~
所以在所有系统进程中公共的代码与数据只需要存一份就够了~
那谁来辨别哪些库加载了,哪些库又没加载呢?——OS会帮我们操心~
那系统中是否会存在许多已加载的库呢?——会的,而且OS会管理好这些库——先描述,再组织~
编址与可执行程序
提问:如果我们的可执行程序还没有加载进内存中,那么这个程序有没有地址呢?
test.s在没运行前是有地址的~
可执行程序在没有被加载进内存前就已经按照类别(权限,属性等)被划分为各个区域~
诸如正文区域,未初始化数据区域等等~
另外有一点需要注意的是,我们进程地址空间的很多地址数据都是从可执行程序中来的~
可执行程序是有地址的,所以我们要对可执行区域进行编址~
所以在可执行程序被加载时cpu是怎么知道代码的入口地址的呢?就是通过这里的信息知道的~
那么我们对可执行程序编址有哪种方式呢?
在可持续程序被加载的时候是需要为可执行程序创建进程的地址空间的,而这些地址空间的本质就是代码的起始地址及结束地址这些也称虚拟地址,那这些数据又是从哪里来给地址空间初始化的呢?
我们是怎么明确知道正文代码等各个区域的起始地址,结束地址的呢?是谁给它们的?毕竟它们本质就是个结构体,是谁初始化这些结构体给它们起始地址,结束地址的数据的?
可执行程序的代码就是代码区,可是不同程序的代码量是不同的,注定了正文代码区域不同,再来看整个地址空间代码量越大的程序不是意味着其虚拟地址更多吗?页表互相映射也多~
那这么多的数据都是哪里来的呢?是谁初始化这些结构体给它们起始地址,结束地址的数据的?
所以再提一次我们前面的话:进程地址空间的很多地址数据都是从可执行程序中来的~
这种跨度说明编译器跟系统还有关系吗?
绝对编址从0开始编址,那它的范围又是多少呢?【0,4GB】
这种线性的地址方式不刚好符合初始化进程地址空间结构体的起始地址以及结束地址吗?
所以可执行程序区域是天然带有了虚拟地址,在加载进内存得到进程地址空间时候也正是通过这些虚拟地址来初始化地址空间里面的起始与结束地址的~以及页表的初始化~
所以虚拟地址空间是由编译器与操作系统相互配合而形成的~
虽然我们都是按照绝对地址来理解的,但为了兼容以前的相对地址问题,我们把整个可执行区域都当成一个区域,这样起始地址+偏移量也可以当作是相对地址~只不过起始地址为0~这样概念上就可以兼容以前的相对地址问题。
其实也不要猛纠结这两种编址方式,反正都大差不差作为地址即可~
理解动态库的连接和加载问题
一般程序的加载
在进程创建前要把地址空间,页表预初始化好,到了加载时候页表再同步建立映射~
代码行也是属于数据,所以加载进内存的时候也会物理地址~这样代码被加载进内存的时候就有两种地址了——物理地址和虚拟地址~然后在页表处建立映射~
那么程序运行的时候怎么知道从哪开始呢?总不能从最开始的绝对地址一个个往下读吧?
在表头有入口地址,然后搭配各个寄存器之间的合作实现可执行程序中代码的正确运行~
就这样获取虚拟地址,读取指令就可以到物理内存在通过物理地址找到代码,最终读取完所有代码运行程序~
动态库的加载
动态库编址不采用绝对编址的方式,在可执行程序中调用动态库方法时其虚拟地址为动态库的起始地址+该方法距离起始地址的偏移量~
所谓动态库的链接其实就是把该库的起始地址与其方法的偏移量填入可执行程序区域中~
OS会对加载的库进行管理,在它眼中库名就是代表其物理内存中的起始地址~
一般都是把库名与其_start虚拟地址联系起来,当拿到指令识别到库名(_start)时就会转到共享区里去通过虚拟地址找到在物理内存中的共享库代码数据,然后通过偏移量去读取该库中某个方法的代段~