目录一 引出一个概念---ELF二 ELF格式具体指什么三 ELF可执行文件加载四 理解链接与加载1 静态链接2 知识点总结虚拟地址的生成过程磁盘与内存中程序的地址表现关键认知纠正Linux 系统中的地址特殊性我们上一篇博客还有一个内容点没有讲到库的制作原理和加载原理---ELF我们这篇博客就来介绍这部分一 引出一个概念---ELF我们以前学习源代码写完是需要翻译的---四步预处理编译汇编链接把源文件编译成.o文件和库一起打包并链接库是在链接的时候生效的有些项目是通过C和汇编语言一起写出来的我们把.o文件称为可重定位目标文件本身是不可执行的我们来看一份代码// hello.c #includestdio.h void run(); int main() { printf(hello world!\n); run(); return 0; } // code.c #includestdio.h void run() { printf(running...\n); } // 编译两个源⽂件 $ gcc -c hello.c $ gcc -c code.c $ ls code.c code.o hello.c hello.o我们看到hello.c和code.c这两个程序运行编译后形成.o文件再通过gcc * .o合并形成可执行程序a.out 内部是2进制但不只是二进制内部以特定的规则格式把写好的包含代码和数据的逮捕好的二进制 他们是有固定的格式--叫做ELF.o .a .exe .so的格式都是ELF格式这里的格式指的是文件的内容所遵守的因为大家的格式都相同所以大家才能进行链接形成可执行文件所以ELF是类 Unix 系统Linux、Android、FreeBSD 等中最主流的二进制文件标准格式用于存放可执行程序、目标文件、动态库与核心转储文件理解编译链链接的细节我们需要先了解 ELF 文件。实际上以下四类文件都属于 ELF 文件可重定位文件Relocatable File即.o文件。包含适合与其他目标文件链接用于生成可执行文件或共享目标文件的代码与数据。可执行文件Executable File即可以直接运行的程序文件。共享目标文件Shared Object File即.so文件。内核转储文件core dumps用于存放当前进程的执行上下文由 dump 信号触发生成。一个 ELF 文件主要由四部分组成ELF 头ELF header描述文件的核心特性位于文件开头主要作用是定位文件中的其他结构。程序头表Program header table描述所有有效的段segments及其属性。表中记录了每个段的起始位置、偏移量offset和长度。由于这些段在二进制文件中紧凑排布需要通过程序头表的信息来区分和解析各个段。节头表Section header table包含对各个节sections的描述信息。节SectionELF 文件的基本组成单位用于存放特定类型的数据。ELF 文件的各类信息与数据都存储在不同的节中例如代码节存放可执行指令数据节存放全局变量与静态数据等。最常见的节包括代码节.text用于存储机器指令是程序的主要执行部分。数据节.data用于存储已初始化的全局变量和局部静态变量。二 ELF格式具体指什么ELF格式由四部分组成ELF header是一个结构体变量它可以直接写在磁盘上决定了大小是固定的可以描述整个ELF的格式查看ELF header: 我们要学到一个新命令readdelfreadelf -h 你的ELF文件注意文件经过gcc/g编译之后也还是文件一直是文件类型不变在ELF header中有一个Magic叫做魔数它会生成一段没有意义固定大小彼此认识编译器或......的一段固定序列的随机值你是怎么知道你要读取的文件是ELF格式的操作系统或工具判断一个文件是否为 ELF 格式核心是读取文件开头的魔数读取固定长度魔数先读取文件最开头的 4 个字节这部分属于 e_ident[0..3] 字段。ELF 文件的魔数固定为0x7F 0x45 0x4C 0x46对应 ASCII 字符\x7F E L F如果这 4 个字节完全符合上述序列就判定为 ELF 格式否则为其他文件类型所以魔数是用来在磁盘中判定文件类型的当操作系统读取文件时起始并不知道文件类型如何看待磁盘文件的内容的byte array[n]大数组磁盘上的 ELF 文件就是一个连续的字节数组byte array[file_size]byte array[n]中保存的内容byte array[0 ... 511] → ELF 文件头最重要 byte array[512 ... ...] → 程序头表描述怎么加载进内存 byte array[...] → 代码段 .text byte array[...] → 数据段 .data byte array[...] → 重定位信息、符号表...文件有自己的读写位置就是文件下标怎么知道可执行文件中每个区域开始位置大小都被记录在固定的开头处ELF header 开始位置---偏移量大小---区域的大小Section Header Table就是一个表结构用来描述可执行程序中的一个一个section(数据节保存未来形成的代码和数据所以Section Header Table就是相当于宏观上统一管理section查看具体的section# objdump -S main我们以前所写的代码全局变量类.....编译后都会以一个节一个节的形式存在某一个section中链接的本质就是形成ELF三 ELF可执行文件加载ELF格式是在磁盘中保存的在对程序双击等操作时可执行程序要被加载到内存磁盘在进行IO交互的时候基本单位是4KB但是在读取section时可能不足4KB可能是128字节但是申请了4KB只有128个字节的空间是有效数据如此反复很多空间就被浪费了所以我们可以把section合并起来程序一到内存就变成了进程进程中含有页表页表中记录了可读可写可执行的信息为什么页表中会有这些信息因为每个数据是带有权限信息的例如可读可写.....这些权限信息ELF都写好了可以从ELF中提取这些权限信息我们合并section的原则是什么相同权限的section进行加载时合并合并之后的内容叫做segment---段不用在ELF中存在边合并边加载怎么知道哪些section进行合并Program HeadersTable中把section合并成segment的操作手册四 理解链接与加载1 静态链接静态链接就是在程序运行之前把所有需要的代码和库文件打包合并成一个独立的可执行文件的过程静态链接 链接器ld把多个 .o 可重定位文件 静态库.a/.lib合并成一个完整的、可直接运行的可执行文件所有依赖都打包进去运行时不再需要外部文件代码会被CPU运行但是CPU只认二进制文件所有我们就需要有指令集那什么是指令集呢我们用一个故事来理解我们给刚出生的婴儿说站起来别哭了....这样的指令婴儿是听不懂的但是在我们长时间的说长时间的训练婴儿就能听懂一些简单的指令。如果连续发布四五条认识的指令它完成了把婴儿从什么都不知道到训练到它知道这个过程叫做给小孩写入指令集的过程。婴儿就相当于CPU刚出生就相当于一块刚生成出来的二氧化硅板子我们训练婴儿的过程就相当于向CPU刻录指令集这个过程由CPU的供应商向光片机通过高频率刻录形成电路CPU有了指令集只能识别简单的指令用户可以把指令集做集合形成复杂命令---就能完成具体工作静态函数重定向是静态链接的核心步骤链接器定位到被调用函数的真实地址将指令中预留的占位符地址00000替换为该实际地址完成地址修正最终生成可直接运行的程序。一个程序没有加载到内存时变量或代码会不会有地址一定会有和内存地址的关系后面讲静态链接会整合所有模块将用到的函数代码拷贝到可执行文件中exe文件并把 .o 文件里预留的函数占位地址替换为真实地址。这个过程也正是 .o 文件被称为可重定位目标文件的原因。观念与可执行程序地址问题有如下几种地址1物理地址实际硬件内存的物理存储位置是真实的内存单元编号。2虚拟地址线性地址由操作系统的内存管理单元MMU映射到物理地址的地址程序运行时使用的地址需通过编址方案转换为物理地址。3逻辑地址由基址固定位置 偏移量组成和虚拟地址的区别是虚拟地址不直接知道对应的物理内存位置而逻辑地址通过基址和偏移能定位到程序内部的指令 / 数据虚拟地址就是直接知道200m的位置找到家逻辑地址就是已知一个固定位置(庙偏移量20m在现实生活中处处是地址例如学生老师的编号门牌号....给学生老师编号---本质就是编址一个可执行程序有很多行代码是一个函数代码的集合里面的每一行代码都必须有地址有地址就需编址就需选择编制方案程序编译阶段会用平坦模式编址起始地址设为 0000…结合绝对编址、相对编址两种方法最终生成虚拟地址由编址方案确定可执行程序还没加载到内存时它有没有地址答案是有因为编译阶段已经分配了虚拟地址如call指令会对应目标地址加载时操作系统再将虚拟地址映射到物理地址。CPU 执行程序时会从程序的入口地址Entry Point开始读取指令这个地址是编译阶段确定的虚拟地址并非物理地址。程序运行时操作系统会将这个虚拟起始地址映射到物理内存CPU 通过这个映射后的地址开始取指执行虚拟地址表的形成与数据来源虚拟地址表的形成虚拟地址表页表 / 段表取决于内存管理方式是操作系统在程序加载时创建的。当可执行程序被加载到内存时操作系统会为程序分配物理内存页 / 段同时建立虚拟地址与物理地址的映射关系这个映射关系就存储在虚拟地址表中。表内数据的来源虚拟地址部分来自程序编译时生成的虚拟地址由编译器的编址方案确定物理地址部分来自操作系统为程序分配的物理内存单元由内存管理模块根据内存空闲情况分配Entry point address 写的是程序加载到内存的入口地址”入口地址是程序运行时 CPU 开始执行的第一个指令的虚拟地址由编译器在生成可执行文件时写入文件头如 PE/ELF 文件格式。程序加载时操作系统读取这个入口地址将其映射到物理内存后CPU 就从该地址开始执行程序。CPU进来的是虚拟地址出去的是物理地址2 知识点总结虚拟地址的生成过程Section 合并程序编译、链接阶段编译器会将代码段.text、数据段.data、BSS 段等不同section段进行合并形成连续的指令和数据块这是地址分配的基础。统一编址合并后的可执行程序会进行统一编址包含两种方式绝对编址直接给指令 / 数据分配固定的虚拟地址如起始地址设为 0x00000000相对编址以某个基准点如函数起始地址为参考用偏移量表示地址笔记中 “以一开始作为参照点” 就是这个意思。这一步完成后虚拟地址的雏形就已形成后续加载时仅需映射到物理地址即可。磁盘与内存中程序的地址表现磁盘中可执行程序对应的地址一般以逻辑地址表示基址 偏移此时还未关联物理内存只有虚拟 / 逻辑地址的定义。内存中程序加载到内存后操作系统会立刻为其建立物理地址和虚拟地址的映射CPU 通过虚拟地址访问物理内存。关键认知纠正不要认为 “只有把代码加载到内存才有地址”地址在编译链接阶段就已分配虚拟 / 逻辑地址加载只是完成虚拟地址到物理地址的映射并非地址的首次生成。可执行程序采用同一编址指编译链接时为整个程序分配统一的虚拟地址空间保证指令 / 数据的地址连贯性。Linux 系统中的地址特殊性在 Linux 中虚拟地址和逻辑地址本质是同一个地址只是在不同场景下的叫法不同编译链接阶段称逻辑地址基址 偏移程序运行、内存管理阶段称虚拟地址需 MMU 映射到物理地址。而其他系统如 Windows中逻辑地址和虚拟地址可能有更明确的分层如分段 分页Linux 因以分页管理为主二者概念趋于统一。