预备知识(简单了解)在我们学习C语言的时候就知道main函数是程序的入口程序运行时是从main函数开始往后执行的但真的是这样吗其实在Linux系统里_start才是真正的程序入口它会做一些前置工作比如说根据用户传入的命令行参数来判断返回main()还是返回main(int argc.....)也就是说_start()调用了main函数之后程序才开始执行的。值得一提的是每一条汇编语句在经过汇编再到后边链接之后在可执行文件里大部分都会形成一条指令就是一串二进制序列这条指令由指令长度操作码数据构成全是二进制的只不过一串二进制序列的不同部分表示这个含义。一个可执行文件在OS看来就是一串二进制流将来PC就是根据每条指令前边的指令长度去找到下一条指令的。具体怎么理解还是按照前边博客里写的理解方式这块是编译链接的东西不用搞太深。在CPU内部有个叫指令集的东西程序编译成了二进制之后本质上就是一串指令集指令集的意思就是一套步骤就像起身走到桌前拿水再走回来这一套拿水步骤一样将指令集交给CPU它才能帮我们完成任务。从硬件角度不同OS上跑的程序不能跨平台的本质原因就是每个OS底层的指令集不一样从软件角度就叫系统调用。静态库链接过程静态链接的本质就是把库中相关的代码拷贝到你的程序中。先来证明一下这个事情。1.通过验证我们知道.o文件里如果有函数调用在没有链接之前将里边的二进制转化为汇编语言之后发现其实call函数的时候那个地址它由于不明确就随便搞了一个地址过去。也可以用查符号表的方式去验证这一点具体的指令就是readelf -s 文件名(没链接前查的就是.o文件的符号表)。反正不管用哪种方式总而言之就是没链接前调用函数的地方不知道地址是什么就直接先以全0代替。2.链接之后(可执行文件假设叫main)你再用反汇编去查main就发现两个.o文件被合并了并且之前不知道的地址也被填上了。这个过程就是静态链接就是将多个.o的代码段合并到一个可执行文件里然后进行统一的编址并且会重写之前.o里不明确的函数地址最后就能call地址就代码能跑了。(修改call 00000000地址的过程也叫地址重定位)。静态链接的时候有没有加载过程没有加载的过程它是直接合并多个.o文件到一个文件并且会重写函数地址静态链接的链接过程在进行链接的时候在加载之前就全部完成了。动态库加载过程进程创建的时候是先会去创建内核数据结构(PCB....)虚拟地址空间页表之类的东西然后再去加载程序如今我程序里需要用到动态库比如说C标准动态库那我是先加载库还是先加载程序的代码和数据呢答案是先加载库要加载库就得先找到库库的本质就是.o文件的集合所以库就在磁盘里存着呢根据之前文件系统的知识到磁盘里去找文件的第一步就是要有文件的路径所以先要有库的路径而动态库的路径在上一篇博客里给出了4种方法。至此库被加载到了内存里然后程序的代码和数据也加载了进来虚拟地址被划分好了页表被填充好了程序就可以运行了。动态库被加载到内存里对应的虚拟地址是存在虚拟地址空间里的共享区的在代码区里的程序拿到了比如说printf的虚拟地址之后就会去共享区里找对应函数的虚拟地址然后就调用它。说白了就是动态库的链接就是得跳转到共享区才能找到函数的地址而静态库就直接就在代码区里就直接找到了因为全部合并到一起了。现在情景换成多进程我现在加入说有两个进程都用到了同一个动态库那我比如说A进程在运行的时候已经将库加载到内存里了且A进程由于时间片轮转等各种原因导致还没结束则等到B进程运行的时候就不用去加载这个库第二次了就直接根据库在内存里的物理地址和虚拟地址去填充自己的页表进而映射到虚拟地址空间里的共享区就可以了。此时这个动态库被AB两个进程共享了所以动态库也叫共享库。库里边本质也是代码和数据正是因为它可以共享所以多个进程需要用到的资源只需要在内存中形成一份节约了空间。OS里有非常多的进程用到的重复的不重复的动态库都很多它们都要加载到内存了才能用OS就必须要对这些加载进来的库进行管理先描述再组织具体细节不要深究了比较复杂。动态链接实际上将链接的整个过程推迟到了程序加载的时候什么意思就是上文在说静态库的时候不是查过可执行程序的符号表嘛你会发现符号表已经能够找到call的地址了原因就是动态链接的过程有一步就是重定位地址。动态库则不是动态链接即使形成了可执行文件了里边依赖动态库里的方法依旧是UND(undefined的)你随便找一个可执行文件然后readelf -s查一查它的符号表你就清楚了里边的地址依旧是不知道的链接之后你仅仅只是知道这个程序需要哪些库函数依旧不知道地址。只有当动态库被加载到内存之后才会知道函数的地址。编译器也会有依赖的库(叫/lib64/ld-linux-x86-64.so.2ldd可以查)当程序执行的时候这个库也会加载进来里头有一个函数就是上文说的程序真正的开始_start()函数它会干一件重要的事情就是动态链接最重要的一步就是把上文说的那些在链接的时候不知道地址的函数的地址做重定位(上文说过地址重定位什么意思)。细节补充代码区是怎么找到共享区里的函数的不是链接之后明明库函数的地址依旧是全0吗动态库里是没有main函数的动态库内部包含了大量的方法每一个方法都要有自己的地址跟可执行程序一样库里的编址也是采用相对编址就是逻辑偏移地址平坦模式的0000...0000~FFFF....FFFF编址里边的每一个地址实际表示的是相对起始地址的偏移量。来看下边的一张图mm_struct里每一个区域都有表示该区域起始和结束的变量程序在运行的时候先创建内核数据结构虚拟地址空间页表然后就要加载库了先在共享区创建一块跟库大小一样的区域从而得到库起始的虚拟地址根据起始虚拟地址跟每一个函数的偏移量(当初是以全0作为起始地址来搞接下来的偏移地址的所以库里的地址全都是偏移量都不用计算了)(没加载到内存也能读取磁盘去知道库ELF文件的header的信息从而就知道它的大小就可以分配空间了)直接根据现在给库分配的起始地址本来就有的偏移量就得到每一个函数的地址库的路径我是知道的在运行的时候可以通过环境变量默认路径....方式知道所以就根据路径做路径解析......找到inode里i_block所指向的数据块将库从磁盘加载到内存里加载进来之后就有起始物理地址了根据起始物理地址加偏移量得到每一个库函数的物理地址最后填充页表。有了这种起始地址偏移量的方式就算把库离散加载到内存的各个位置也依旧没问题因为只要知道起始地址偏移量就行。综上我们就知道了OS实现动态链接的过程就是对于在代码区里要调用的库函数来说我知道它的偏移量然后在库加载的时候OS会根据共享区的起始地址偏移量从而去修改代码中call后的地址。动态链接的过程由OS完成它会先加载内核数据结构再加载动态库(只有先加载了库才会先知道库的起始虚拟地址)最后加载代码和数据(边加载边完成动态链接)。细节补充地址重定位就是在修改call后面的内容就是在修改函数地址但call在代码区代码区是只读的啊怎么修改啊动态链接的做法是在ELF的.data节中存放函数的跳转地址叫做全局偏移表got就是.dot和.data是ELF中的两个节它们最后会被合并到program的同一个段里call后编的实际上是got地址表中的偏移地址(就是告诉你got表的地址和函数在got表中的偏移量)got相当于一个数组知道起始地址和偏移量就能找到某一个下标从而找到里边的数据在数据区里存了got里边存着被调用的每一个库函数的偏移地址和对应动态库的起始虚拟地址所谓的地址重定位改的是got表里存的地址。