
目录前言Part1. ELF 各段基础认知Part2. ELF 文件整体构成Part3. 目标文件 .o、符号、重定位Part4. 地址的概念逻辑地址、偏移量、虚拟地址Part5. 操作系统加载可执行程序的完整流程Part6. 动态库和动态链接基础Part7. GOT表 延迟绑定Part8. 静态链接 vs 动态链接 再总结Part9. 结语前言Linux下对于动静态链接的过程的了解是非常重要的接下来来跟随小编的视角来看看吧。lets go!!!!!!!!Part1. ELF 各段基础认知我们使用readelf -s xxxx可以读取ELF 文件各个段的信息。ELF 将程序信息划分到不同的 section段中段名作用.text 代码段存放编译后的机器指令函数代码.data 数据段已经初始化的全局变量例如 int a 10;.bss 未初始化全局变量段例如 int a;。操作系统在加载的时候会自动把 .bss 内存全部初始化为0文件本身并不占用磁盘空间.rodata 只读数据段存放字符串常量例如 char *str hello;运行期间不可修改一个重要特性Section 的合并.data 和 .bss 经常会被合并。内存是按页分配一页通常4KB如果每一个section都单独占一页会造成大量内存碎片把权限、属性相同的段合并在一起可以充分利用内存空间减少内存浪费。区分概念•section链接视角目标文件、ELF文件内部的逻辑分段给链接器使用•segment运行/加载视角操作系统加载程序的时候会把多个属性、权限一致的section 打包合并为一个 segment用来映射到进程虚拟内存。程序加载器只认识 segment不关心 section。Part2. ELF 文件整体构成一份完整ELF文件由下面几大部分组成1.ELF HeaderELF头部整个文件的总入口记录各个部分的偏移位置、程序入口地址、文件类型可重定位目标文件/可执行程序/动态库。告诉操作系统这个ELF各个部分在哪里。2.Program Header程序头表描述各个 segment供操作系统加载器使用用来告诉内核哪一段要映射到进程虚拟地址空间、权限是什么、加载到哪里。3.Section Header段头表描述各个 section主要给链接器、objdump、readelf 这类工具使用。4.各个真正的 section.text .data .rodata .bss …存放代码与数据。常用调试工具objdump -d xxx反汇编查看 .text 段里面的汇编指令。Part3. 目标文件 .o、符号、重定位这是整个链接最核心的知识点。当我们编译 main.c得到可重定位目标文件 main.o。在 main.c 里面调用一个外部函数 printf编译器此时并不知道printf的内存地址所以汇编里的 call 指令目标地址暂时填为 0这个符号标记为 UNDundefined未定义符号。静态链接做了两件大事1.符号解析链接器遍历所有 .o 文件、静态库为每一个 UND 未定义符号找到它真正的定义拿到符号对应的地址。比如去别的目标文件或者静态库里面找到 printf 的实现。2.地址重定位拿到真实地址之后回去修改 .text 段里面的 call 指令把原来填的0替换成函数真正的地址。这就是为什么 .o 叫可重定位目标文件它内部很多地址都是待定的等待链接器后期修正。链接完成之后地址全部填好就生成了最终的可执行程序。一句话总结静态链接收集多个目标文件合并段解析符号修正所有待定地址。Part4. 地址的概念逻辑地址、偏移量、虚拟地址偏移量相对地址在 .o 目标文件中我们只关心偏移量函数/变量距离本段头部的偏移。举个例子某个函数距离 .text 段开头偏移 0x200。在可重定位文件阶段我们不知道最终加载到内存的绝对位置但是偏移量是固定不变的。逻辑地址早期一些系统的概念逻辑地址 段基址 段内偏移。在现代Linux下逻辑地址 ≈ 虚拟地址MMU内存管理单元负责把虚拟地址翻译成物理地址。虚拟地址线性地址Linux进程的核心每一个进程都拥有独立完整的虚拟地址空间。最终可执行程序里面写的地址全部都是虚拟地址。当程序被操作系统加载到内存之后CPU最终访问的全部是虚拟地址由MMU配合页表翻译成物理地址。重点CPU从来不直接操作物理地址CPU发出的地址永远是虚拟地址交给MMU完成转换。Part5. 操作系统加载可执行程序的完整流程1. exec 系列系统调用启动程序打开可执行ELF文件通过inode找到磁盘上的文件内容2. 操作系统读取ELF的 Program Header拿到各个 segment 的信息起始虚拟地址、长度、读写执行权限3. 内核为进程创建 mm_struct、vm_area_struct 等内核结构体用来描述进程的虚拟地址空间建立虚拟地址到物理内存的映射页映射不一定立刻把全部数据拷贝进物理内存缺页中断的时候才加载4. 设置程序入口地址ELF header记录的入口一般是 _start不是main5.CPU跳转到入口地址开始执行。_start 是libc提供的入口函数在main函数之前运行初始化栈、初始化环境变量、做动态链接最后调用我们写的 main。main返回之后_start 还会调用exit结束进程。Part6. 动态库和动态链接基础静态链接会把库的代码直接拷贝进可执行文件动态链接不会。• .so 动态库本身也是一份独立的ELF有自己独立的 section / segment• 程序运行的时候动态库不会在编译期合并进exe运行时操作系统的动态链接器ld-linux把动态库加载到进程的虚拟地址空间•多个进程可以共享内存里面同一份动态库节省物理内存。但是这里有一个问题动态库被加载到哪一块虚拟地址编译阶段是无法预先确定的。为了解决这个问题动态库编译的时候必须使用 -fPIC生成位置无关代码。Part7. GOT表 延迟绑定为什么需要GOT动态库里面调用外部函数比如printf编译期不知道这个函数最终的虚拟地址。如果直接修改代码段 .text有两个致命问题1. .text 代码段是只读的运行时不能修改2. 多进程共享同一份so如果修改.text所有进程都会被影响。解决方案把地址存放到一个可写的数据段里面也就是GOT表。代码段里面永远只写固定的偏移去读取GOT表里面保存的函数真实地址。call 偏移量 ; 跳转到PLT段PLT GOT 延迟绑定Lazy BindingLinux默认使用延迟绑定函数第一次被调用的时候才去查找、填充GOT表里面的真实地址而不是程序一启动就解析所有符号。1. 第一次调用库函数GOT里面暂时是占位地址跳转到动态链接器2. 动态链接器去符号表查找这个函数真正的虚拟地址回填到GOT表3. 第二次再调用这个函数直接从GOT拿到已经填好的地址直接调用不再走解析流程。好处程序启动速度更快。很多库函数可能整个程序生命周期根本不会被调用就不需要解析它的地址。补充GOT 存放在 .data 可写段所以运行时可以修改。动态库自己内部也拥有属于自己的GOT表。Part8. 静态链接 vs 动态链接 再总结特性静态链接动态链接链接时机编译链接期一次性完成符号解析重定位符号解析推迟到程序加载运行阶段库代码库的机器码拷贝进可执行文件不拷贝运行时单独加载so重定位链接期一次性完成地址修正运行期依靠GOT表完成地址回填内存每个进程拥有独立一份库代码物理内存中只保存一份多进程共享体积可执行文件体积更大exe很小依赖外部so文件Part9. 结语这篇文章我们深入的知道了动静态链接的相关知识接下来小编还会带来更多Linux相关的知识敬请期待~最后祝大家可以春风得意马蹄疾一日看尽长安花最后的最后要是觉得本文还可以的话可以点点赞关注小编一波谢谢大家~