库制作与原理什么是库库是将现有成熟的代码实现复用的操作C库如果没有提供printf每个用户都要自己定义会很麻烦而这类常用的功能就会让全球顶尖的程序员实现一份打包为一个库之后用户使用库即可使用手机用户直接买手机即可没有必要了解手机是如何制造的而库分为两种库静态库.a[Linux] .lib[Windows]动态库.so[Linux] .dll[Windows]静态库静态库的制作动静态库中要不要包含main函数不包含我们动静态库的作用目标是可执行文件与之连接而目标的可执行文件中肯定有main函数所以动静态库内不包含main函数为什么需要静态库小情景来了解作业实现几个库函数zs向ls要这个作业ls就将自己的C库函数代码发给zszs获取了ls的源码需要自己做调用实现usercode老师就查看调用所以zs成功应付此时老师查看zs的函数源码发现和ls一样老师发现看问题这个时候zs希望ls可以帮他解决这个问题此时ls就想到一个办法不给zs源文件将源文件做预处理、编译、汇编形成的同名.o目标文件gcc -c mystdio.c给zs做多文件编译的时候将所有多文件编译为.o最后将所有.o文件链接也可以生成可执行文件此时zs使用.o文件来编译也可以实现这个.o文件的内容为二进制语言将文件内容隐藏了老师也看不懂zs获取到.o文件但是zx不理解如何使用啊ls突然想起来忘记 教zs如何使用ls把.h头文件都发给zszs可以通过头文件使用函数方法所以头文件本质是对源文件的方法说明文档所以库(无论是动还是静)本质都是源文件对应的.o但随着时间的增加库函数的数量越来越多zs要在十多个.o文件一个一个操作太麻烦了把全部的文件都打包———静态库(本质对.o文件打包)如果tar打包也没啥区别啊zs还是要在一堆文件中一个一个操作所以Linux提供了一个方法这个方法可以打包所以的.o文件为一个文件并且gcc可以直接识别所有的.o文件简单说是可以直接使用一个文件不需要解包创建静态库操作ar(archive 归档文件)选项-r: replace-c: create具体命令ar -rc libmyc.a *.o.a静态库本质是一种归档文件不需要使用者解包而用gcc/g直接进行链接即可选项讲解我们将.o文件打包到.a静态库的时候静态库中新增的文件就create添加同名文件更新了就直接replace使用静态库静态库 .a 命名一般 lib- 前缀 .a为后缀 用户识别就是读取中间的数据上面静态库的名字叫myczs现在就获取到ls发送的.a文件zs不会用ls就把.h文件发给zs以此使用gcc -o usercode usercode.o -L. -l myc(-l是做链接库的操作-L是在当前路径下查找库文件)-L是去什么地方找库-l是做找什么库如果我们要连接任何非C/C标准库(包括其他外部或者我们自己写的)都需要使用-L -l链接库的时候不是使用libmyc.a而是使用库的真实名字myc注意gcc/g查找库是默认去/这样就可以直接使用大小.o库函数不需要解包随着时间推移越来越多人找ls要库文件但是ls每次都要说.a库里面函数的使用方法要提供.h文件现在ls要定义标准了ls设置lib文件夹文件夹中有include文件夹和mylib文件夹在include文件夹中存储.h头文件在mylib文件夹存储.a静态库将这个lib文件夹tar为压缩包这个就是我们的安装包了tar czf ib.tgz lib此时zs就直接获取到ls的压缩包解压获得lib文件夹zs就做一系列操作gcc -c usercode.c报错此时头文件不在当前路径下可以使用-I来指定gcc查看头文件的路径gcc -o usercode usercode.c -I ./lib/include -L ./lib/mylib -l myc这样就可以调用成功选项:-I-L-l头文件这样写就不需要-I来指定头文件路径了所以 库也是需要被安装到系统上的gcc -o usercode usercode.c -I ./lib/include -L ./lib/mylib -l myc这个用法gcc默认是找不到库文件Linux下搜索头文件默认是在/usr/include,搜索库文件默认是在/lib64所以我们把自定义的头文件和静态库放到对应的默认位置处我们就可以gcc编译的时候就可以简便很多如何放入拷贝cp lib/include/* /usr/includecp lib/mylib/libmyc.a /lib64我们现在编译就可以直接gcc -o usercode usercode.c -l myc(不用-i 和 -L 但是要-l因为使用的库是自定义的库不是默认的c库)使用我们在计算机上安装库本质是在做拷贝拷贝到指定的路径下之后使用这些库指出调用什么库即可ldd命令是用来查看可执行程序关联什么库gcc是c语言的编译器自然认识c库而我们自定义的库gcc肯定不认识才需要去查找而链接是把查找到的.a静态库中的.o文件内容拷贝到可执行文件中完成链接过程 这就是为什么要用lib文件夹打包为压缩包用户对应接收的库文件可以解压后直接使用也可以将库文件中的头文件和静态库拷贝到对应的默认路径下ldd查看a.out可执行文件发现并没有我们之前链接的静态库文件这是因为静态库的方法实现直接拷贝到了可执行文件中同Makefile创建静态库以及打包动态库动态库的原理和静态库基本一样相比于静态库我们通常把源码打包为动态库是更为常见、常规的操作创建动态库依旧是把.c后缀的文件gcc为同名的.o文件只是动态库这里的操作有些不一样新增了一个选项-fPIC产生位置无关码(position independent code)目前无法理解需要了解后面知识gcc -fPIC -c *.c产生的.o文件继续打包而动态库打包是直接gcc打包的gcc既可以生成可执行程序还可以生成动态库文件gcc -shared -o libmyc.so *.o完全一样肯定不行.o文件没有main函数-shared告诉要生成动态库这样就生成了动态库(也叫做共享库)使用动态库我们获得压缩包打开获得lib文件夹我们和静态库调用的方法来调用gcc -o usercode usercode.c -I ./lib/include -L ./lib/mylib -l myc成功的生成了可执行文件但是调用出错了错误:错误的加载共享库无法打开文件不存在这要如何解释可执行文件不都有了吗怎么不存在而且lib/mylib里面也有.so动态库并且编译的时候还指定路径了我们这里只是让gcc知道了我们的.so动态库在什么地方但是系统并不知道我们自定义的.so动态库在上面地方静态库在链接的时候是直接把库的实现拷贝到可执行程序里可执行程序一旦形成就不再依赖静态库使用系统不知道静态库在哪也没影响但是动态库编译的可执行程序在调用加载的时候还需要找到动态库静态库一旦编译成功就一定能运行系统查找动态库是在/lib64或者/usr/local/lib64下找如何处理这个错误?让系统找到动态库方法1、把.so动态库拷贝到对应的默认路径下系统直接可以找到cp ./lib/mylib/libmyc.so /lib642、在/lib64下定义软连接3、环境变量$LD_LIBRARY_PATHOS在运行程序要查找动态库也会在该环境变量下查找动态库[LD_LIBRARY_PATH经常是空的]设置环境变量export LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:/home/zyx/BtyeNote/file.c/lesson_24/zhangsan/lib/mylib这样也可以直接运行成功系统可以找到.so动态库这里的环境变量是内存级别的并没有配置到磁盘中的配置文件中(bashrc等配置文件)4、在ld.so.conf.d目录下配置路径文件系统除了在默认路径下查找也会有特殊情况去查找其他路径下的动态库所以系统就要求在/etc/ld.so.conf.d路径下以.conf文件存储查找的路径文件名可以随便取内容就是存储查找动态库的路径创建好后输入命令ldconfig重新加载一下结论结论1 如果同时提供动静态库gcc/g默认使用动态库如果一定要使用静态库就在编译的时候添加选项-static前提是有静态库没有静态库不能使用只存在静态库可执行程序对于该库只能静态链接结论2 在Linux系统下默认情况安装的大部分库默认都优先安装的是动态库结论3库 : 应用程序 1 : n所以在应用场景下某些功能大家都要用频率特别高且与应用场景无关的都可以归类到库中所以C/C提供了很多不同功能的库结论4VS不仅仅形成可执行程序也能形成动静态库如何在VS 2022形成动静态库AI了解好技术一定和业务结合的找工作找业务加技术的工作不要纯技术的工作使用外部库-尝试目标文件形成可执行程序实际上就两个步编译链接编译就是把.c/.cpp文件变成.o文件.o文件我们称为目标文件全称叫可重定向目标文件Linux下为.o文件在windows下是.obj文件而我们已经学习过的静态库了库本质也是.o文件所以链接本质上是把.o文件链接这也让我们意识到库的源文件只有一个但是在不同的系统上库会根据不同的平台调整编译过了为什么要把.c文件都编译为.o文件?把.c文件编译为.o文件这个过程相对独立我们有一个文件更新直接用.c编译链接成本太大了其他.c文件要一起编译链接而把每个.c文件编译为.o这也就可以更新对应的.c文件之后和原来的.o文件一起链接成本就减小了而在大型项目开发.c文件不是放在一个文件中的往往有多个模块每个模块放着不同的.c文件而我们的main.c主文件往往会在这些模块外我们要编译链接的时候就把这些模块内的.c文件都做成.a静态库我们在外部让main.c文件与这些静态库链接即将二进制数据拷贝进主函数一个模块一个静态库模块内Makefile建库.o目标文件是一个二进制文件 文件的格式是ELF是对二进制代码的一种封装可执行程序、动静态库、.o目标文件的文件格式都是ELF结论动静态库可执行程序.o文件都是ELF格式的这些文件虽然是二进制数据组成但不是一股脑的写入到文件中而是有自己固定的格式的方便IO访问文件内容会被拆分为不同的模块想可执行程序中有代码块数据块等等这些数据会以一定的格式放入二进制文件中ELF文件重定向文件(.o)可执行文件(.exe)共享目标文件(.so)内核转储这些文件都是ELF文件ELF构成上图就是ELF格式了ELF Header是存储整个ELF文件的属性信息的Program Header Table是存储文件加载的信息Section Header Table 是Section相关表结构文件有自己的代码区数据区通过指令size ELF文件可以查看到这些Section区域的大小text这个Section的大小为2035每个都表示一个Section的名字和大小text、data都占用一个section最常见的节我们现在知道我们可执行程序内部就是由一个一个的节组成在加载的时候由新名词叫做段是虚拟地址空间的段的概念ELF从形成到加载轮廓ELF形成可执行的过程step1将多份C/C源代码翻译成为目标.o文件(.o文件是ELF文件而我们链接需要的动静态库也是ELF文件)step2将多份.o文件section进行合并所以链接的本质就是将ELF文件中相同属性的section合并到一起的形成一个更大的ELF可执行程序所以可执行程序的形成是将我们.o文件与需要链接的动静态库文件的ELF中的相同section属性合并——链接就是合并实际合并是在链接的时进行的但是并不是这么简单的合并也会涉及对库合并此处不做过多追究了解好一个ELF文件的特性我们就可以理解这部分的内容而链接是合并那么链接器就是做ELF合并的工作ELF可执行文件加载一个ELF会有多种不同的Section在加载到内存的时候也会进行Section合并形成segment合并原则相同属性比如可读、可写、可执行、需要加载时申请空间等字符串常量和代码都是只读的我们说字符串常量是存储在虚拟地址空间中的字符常量区但是二者属性相同(都只读)最后还是会和代码合并的加载的时候我们Section有很多类似代码块和只读的数据区加载的时候就合并到一起形成段segment而虚拟地址空间对不同属性段的start和end是根据可执行程序来获取长短的所以虚拟地址空间的内容和编译器和可执行程序有关系虚拟地址空间的属性配置并不是加载到OS中就有的而是在加载之前根据可执行程序计算出来的很明显这个合并方式已经确定了具体合并原则被记录在ELF的程序头表(program Header Table)section之间的合并原则存储在头表加载时就可以快速合并分类为一个整体的segment通过指令readelf -S XXX来读取ELF文件的结点信息即读取section header Table的信息section header Table是一个数组存储section的信息而在ELF中往往会记录section到ELF文件起始的下标偏移量根据偏移量和长度来确定section的范围其中bss是什么变量有全局和局部的局部变量是程序在运行期间创建的全局变量分为已初始化全局变量和未初始化全局变量已初始化全局变量需要让可执行程序把数据记录下来数据记录到data块中还有全局变量是未初始化的未初始化的全局变量默认为0那么还有必要记录到data中吗所以bss是把未初始化的全局变量描述起来加载到磁盘只用记录个数不需要实际的开辟空间而加载到内存根据数量和描述的全局变量在以此开辟空间初始化节省了很多磁盘空间可执行文件加载的时候需要合并section这个操作的方式是需要文件告诉OS的编译角度section Header Table 告诉OS那些section是可以合并的合并为一个大的section而加载角度program Header Table告诉OS哪些已合并的section是可以合并为segment如何读取segmentreadelf -l XXX上图下方的文字是告知OS哪些section是可以合并为一个segment的这样就可以把属性、权限等相似的section合并到一起了这样就形成了一个数据段这样几个数据段就整体做加载而我们之前讲过文件系统的数据存储的基本单位是4KB的块而我们还没涉及到内存管理但可以知道内存存储的基本单位同样是4KB所以文件4KB的数据块可以刚刚好存储到内存的4KB空间所以我们做内存级别的空间申请都是4KB为基本单位及时我们只要1bit也要申请4KB的空间SHT是编译阶段编译器合并section后创建记录的并且制作PSH的而PHT是执行阶段的加载器根据可执行程序的PSH来加载程序进程是如何知道哪些数据是只读的哪些数据是堆区的等OS并不关系这个问题是通过读取ELF的PHT中获取的信息进程的虚拟地址空间本来也是通过PHT创建的这个块是用于记录可执行文件中的名称通过字符串表来记录这邪恶名称可执行文件中的名称不记录字符串而是某个下标通过readelf -s xxx查看符号表ELF Header通过readelf -h xxx来查看OS通过加载器去加载首先要确定这个是一个ELF文件而二进制文件都有magic这样的数据magic数据是随机值但这个值是有特点的OS根据这个magic数据来判断是ELF数据所以图片视频这类文件OS是如何识别的呢就通过magic数据来识别是jpg、mp4格式的文件Size of this header 这个表头的大小Size of program header PHT的大小Number of program header PHT的个数等都记录下来了ELF header 中比较重要的是Entry point address这个是ELF地址偏移量为0的地方开始通过这个偏移量往后读取64字节就可以读取完ELF Header再在这个位置开始往后读取56字节就可以读取完PHT的内容从文件的结尾处往前读64字节就可以读取到SHT的内容理解连接与加载静态链接无论是自己的.o还是静态库中的.o本质都是把.o文件进行连接的过程所以研究静态链接本质就是研究.o文件是如何链接的通过实验了解编译为.o文件gcc -o main.exe * .o编译链接为可执行程序.o文件都要互相链接都用了printf要和C标准库链接exe文件和C标准库是动态链接的但是.o文件一定是合并的形成了exe可执行文件学习工具objdump -d XXXobjdump是对目标文件进行反汇编对比一下.c文件和.s文件汇编文件中callq可能是printf的汇编我们是把.o文件返汇编了这个callp是汇编语句那么左边的一行数字就是汇编指令转化为机器码的数据其中e8就是callq命令的机器码识别到e8就知道要做调用目标地址了而此时并没有链接使用callq调用的函数地址为0未连接就是没目标地址转化为机器码地址为0而链接我认为就是补充这些0地址链接器可以找到目标函数的地址修正对应的跳转地址说白了就是编译hello.c的时候并不知道有printf和run函数的存在因此编译器暂时把这些函数的跳转地址设置为0readelf -s xxx查看符号表查看符号表run和puts(printf底层是puts)都可以找到并且puts是UND是未定义的此时.o文件不知道printfhello.c同理而.o文件怎么连接合并的其实就是hello.c文件中的UND文件去其他.o文件中去查找是否被定义上图就可以找到run函数而puts随后去标准库中查找到puts也可以补全查看main.exe文件的符号表可以知道成功连接上了找到run函数和地址Ndx表示多个section合并这个函数方法最终存放到第14个section而查看结点信息发现第14个section为代码段是否连接成功直接查看main.exe的反汇编我们可以看到run函数和printf函数的调用地址都有了所以静态链接就是库中的.o进行合并和上述过程一样而链接的过程还伴随着地址的修正链接器根据.o中的重定位表找需要被修改的函数全局变量从而修改所以链接过程中会涉及到对.o中外部符号进行地址重定位这就是为什么.o文件也可以叫可重定位目标文件ELF程序是如何加载到内存的一个可执行程序如何没有被加载到内存中该可执行程序有没有地址有地址可执行程序是二进制数据变量名和函数名都会被转化为地址可执行程序中有一个一个的小结.text代码段中会存放函数实现当main函数调用函数需要知道函数在什么地方通过获取代码段的起始位置偏移量来查找函数的位置变量a也是同理获取数据段的起始地址通过偏移量来找到对应的变量数据这样子就可以对可执行程序完成在磁盘上的编制函数名和变量名的地址不冲突从不同段偏移查找的而字符串样式的函数名和变量名就不需要了直接使用起始地址偏移量来查找使用这个就是逻辑地址如果每个segment的开始地址为0呢即所有的可执行程序就是一个segment那么所有segment所有函数变量编址起始偏移量都从0开始也叫做绝对编址使用平坦模式编址我们查看可执行文件的反汇编码可以看到每个代码都会被设置地址这些地址都是从上往下以此增大的平坦模式是OS的设计模式把所有的代码和数据都从0开始编址这类从0开始编址的还是在虚拟地址空间讲过虚拟地址空间不仅仅是进程看待内存的方式我们上面讲的其实就是虚拟地址磁盘上的可执行程序代码和数据编址其实就是虚拟地址的统一编址所有虚拟地址空间不仅仅影响OS还影响编译器编译器在给代码和数据编址的时候就是以其实位置为0开始线性编址的所以说到底是同一个东西在磁盘上的可执行程序中的地址还是逻辑地址但加载到内存中我们就叫虚拟地址空间了也就是在加载之前磁盘中的可执行程序的地址就已经是虚拟地址了即编译好后的可执行程序中函数间互相调用就已经是虚拟地址了所以这里补充了虚拟地址空间知识编译器合并.o文件也是为了统一编址并且补充文件中调用的目标地址(callq 000000)重新理解进程虚拟地址空间在磁盘中的可执行文件本质是二进制文件我们反汇编看到的右边的是汇编语言中间的是二进制机器语言右边就是对应代码在可执行程序中的地址可执行程序在未加载的时候代码的地址本身就是可执行文件的一部分加载可执行程序OS创建进程PCB和mm_struct结构体mm_struct中有一块区域为代码区OS就根据可执行文件中的代码段获取了代码区的起始地址再根据代码区的大小开辟虚拟地址的偏移量而可执行文件的代码二进制数据也要加载呀会有实际的物理地址所以当加载完一行就直接向页表中做虚拟地址和物理地址的映射以此遍历就可以完成页表的映射CPU怎么知道你的可执行程序的起始地址是什么也就是CPU怎么知道从哪里开始执行呢EIP是指令寄存器存储下一个要执行的指令的地址再者ELF有自己ELF Header其中有一个非常重要的字段这个字段是可执行程序的入口地址这个地址是单独被存储在ELF Header中的OS就会将这个地址load加载到CPU的EIP寄存器中以此开始调度进程CPU开始调度进程就会取EIP的地址而EIP存储的是进程的虚拟地址我们通过CR3寄存器(CR3寄存器是可以通过虚拟地址到页表中查找物理地址)根据EIP的虚拟地址获取实际的物理地址进到CPU的地址全部都应该是虚拟地址经过CR3和MMU出来的全部都是物理地址CPU不关心物理地址CPU对应文件的数据和代码的视角和磁盘的视角是一模一样的都是虚拟地址这样OS和编译器就统一了OS认为是虚拟地址空间编译器认为是平坦模式编址绝对编址加载可执行程序虚拟地址有物理地址也加载这样就构建映射用虚拟地址初始化各个区域 入口地址交给CPUCPU通过CR3查找页表获取物理地址以此找到下一个指令虚拟地址以此往复构成循环逻辑地址物理地址虚拟地址可以说是三位一体是同一个概念只是位置不同说法不一样再来看看这个示意图我们之前也讲过mm_struct中每个区域都是由vm_area_struct结构体构成的起始每个vm_area_struct内容是由可执行程序中segment加载提供的因为编译器绝对编址所以这些数据本身就是虚拟地址根据segment的起始结束来创建vm_area_struct这里总结一个这个过程调用可执行程序main.exeOS先创建内核数据结构加载可执行程序(管理信息加载优先EH、SHT、PHT)构建进程PCB虚拟地址空间创建页表映射加载程序代码和数据根据ELF和其中的entry虚拟地址来初始化mm_struct中的vm_area_struct的起始地址此时物理和虚拟地址都有了就映射构建页表而可执行程序在OS是struct filestruct file中由dentryOS就根据dentry来查找inode以此找到i_blocki_block就可以找到磁盘文件中的数据块先文件操作找到文件在进程创建在加载程序我们根据ELF的entry的虚拟地址到反汇编文件中查找可以发现文件的起始地址为_strat而在_start后面会调用_libc_start_main,执行main函数动态库是如何和完美的可执行程序关联的ELF程序是如何转换为进程的(逻辑地址物理地址虚拟地址)虚拟地址空间不需要提静态库和可执行程序的关联因为静态库本身就在可执行程序中程序需要一个库就需要加载一个库文件动态链接的动态库本身就是一个ELF文件加载到物理内存要如何让可执行程序看到库文件呢加载的库文件为ELF文件本来就有自己虚拟和物理地址在页表进行映射将库映射到当前进程的地址空间上进程要调用库函数就从代码区跳到共享区调用调用完再返回代码区静态链接是在代码区里跳转而动态链接是代码区和共享区之间跳转以此完成库函数调用库函数调用1、被进程看到动态库映射到进程的地址空间2、被进程调用在进程的地址空间中进行跳转多个进程需要动态库呢并不影响使用多个进程需要使用动态库只要动态库加载到物理内存进程就可以之间使用内存内的这份动态库进行进程地址映射进程都是向虚拟地址空间共享区进行调用动态库的本质是在OS层面上把公共的代码提取出来代码不会出现重复这也是为什么动态库叫共享库了综上动态库是通过地址空间与程序进行联系动态库是如何加载的动态链接动态链接实际上将链接的整个过程推迟到了程序加载的时候运行程序OS会首先将程序的数据代码连同它用到的一系列的动态库先加载到内存当动态库被加载到内存以后一旦它的内存地址被确认ldd查看一些可执行程序我们看到出来我们需要的C动态库还有一个动态库ld-linux-x86-64.so这个就是ld链接器程序开始的入口是_start这是一个由C运行时库(libc)或链接器(ld)提供的特殊函数具体操作包括:1、设置堆栈:为程序创建初始的堆栈环境2、初始化数据段将程序数据段从初始化数据段复制到相应的内存位置清零未初始化的数据段3、动态链接这是关键一步_start函数调用动态连接器的一系列代码来解析和加载程序所依赖的动态库动态连接器会处理所有的符号解析和重定位确保程序中的函数调用和变量访问能够正常的映射到动态库中的实际地址动态链接器本身和C语言类型文件由关联当重新运行时调用_start函数函数内由相关方法而ld链接器由关联可以直接去解析程序中的动态库依赖并加载这些库到内存中连接器根据环境变量和配置文件这些指定的路径来搜索依赖的动态库4、调用_libc_start_main一旦动态链接成功_start函数调用这个函数这个函数负责执行一些额外的初始化工作5、调用main函数最后_libc_start_main函数会调用程序的main函数此时程序的执行控制权才正式交给用户编写的代码。6、处理main函数的返回值当main函数返回时_libc_start_main会负责处理这个返回值并最终调用_exit函数来终止程序动态库中的相对地址动态库也是ELF我们也理解成为 起始地址(0)偏移量在可执行程序中与动态库映射的位置 动态库方法的绝对编址即偏移量程序与动态库是如何映射起来的进程正常加载代码和数据加载创建并初始化好进程PCBmm_struct页表当程序开始执行_start函数时调用动态链接器对应的方法程序依赖libc.so那么就需要找到C标准库可执行程序能够找到因为ELF会记录程序依赖的动态库以及路径OS就根据文件系统可以找到并打开库文件创建struct file进程根据struct file找到dentry查询到inode再找到i_block以此找到磁盘文件中的数据块以此加载库文件的内容到内存加载的库文件内容其中动态库的虚拟地址可以认为是库中方法的偏移量通过创建vm_area_struct根据动态库大小初始化start和end链入vm_area_struct链表以此映射到进程地址空间进程可以获取到库起始的虚拟地址再加上库中方法的偏移量就可以找到进程地址空间的库方法库文件加载到内存也有自己的物理内存以此做好页表的映射vm_area_struct中还有一个struct file指针是指向对应的库文件页表映射的时候发现缺页中断再分配物理地址做好页表映射程序怎么进行库函数调用在编译阶段可执行程序需要对函数方法编址所有就获取.so动态库内函数方法的偏移量但实际加载库文件映射后起始的虚拟地址会不同此时就需要放回到代码区修改编址即起始地址偏移量!全局偏移表GOT(global offset table)像这样在内存中二次完成地址设置的叫做加载地址重定位能更改代码吗只读的代码不能修改代码所有动态链接采用的做法是在.data(可执行程序或库自己)中专门预留一片区域用来存储函数的跳转地址被叫做全局偏移表.data区域是可读写的所有可以支持动态进行修改在数据区中开辟一个小空间.got用于存储库函数方法的地址偏移量而在代码区调用方法不再是起始虚拟地址偏移量了而是.got 表中偏移地址(第一个就.got0)这个表在加载之前就定义好了可以通过这个表找到库函数偏移量当程序加载的时候库加载和映射成功直接修改.got中libc.so的起始地址因为是数据区所有表中数据可以随时修改程序加载后全局偏移表中的地址就是起始地址 库方法偏移量了。增加表的方式来完成了加载地址重定位解决了代码区只读不能修改的问题.got全局偏移表在编译的时候初始化偏移量在加载库链接成功之后动态修改.got数据区的库的起始虚拟地址代码区不再受影响这就是动态库加载子问题一个进程有独立的一个.got表库是由很多.o文件构成的每个.o文件都以偏移量从0开始编址所有的方法编址都是偏移量库加可以加载到内存的任意位置映射到进程虚拟地址空间根据修改got动态的找到库这种实现方式叫做PIC地址无关代码这就是为什么之前创建动态库指定-fPIC参数的原因 PIC 相对编址 GOT的组合-fPIC 选项文件形成的编址是偏移量且在文件中会形成.got表这个PLT是什么库间依赖库不仅仅和可执行程序由依赖关系库和库之间也会有依赖关系一个库需要调用另一个库那么库之间要如何做到地址无关呢动态库也是ELF文件也有自己的数据区代码区可执行程序也是ELF所有动态库也会有自己的GOT全局偏移表这样就可以做到可执行程序调用库库又调用其他库这也是为什么动态库可执行程序都是ELF的原因而一个可执行程序依赖的库文件可能又成百上千个库方法这样就需要大量的地址重定位这样过于耗时为了进一步降低开销OS就做一些优化比如延迟绑定或者也叫PLT(Procedure Linkage Table)! 将地址重定位这个操作延迟到第一个函数被调用的时候因为绝大多数动态库的函数可能在程序运行期间一次都不会使用可以分批加载可执行程序的内容分批进行映射和修改got表可以AI补充动态链接不仅仅是编译器的工作还有系统层面的工作加载器还要进行二次地址重定位编译器进行初步编址在got表记录库方法偏移量而到程序加载的时候加载器进一步完善增加got表内的库方法的起始地址以此才完成链接