📫作者简介:小明java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫
🏆InfoQ签约博主、CSDN专家博主/Java领域优质创作者、阿里云专家/签约博主、华为云专家、51CTO专家🏆
🔥如果此文还不错的话,还请👍关注 、点赞 、收藏三连支持👍一下博主~
一、ELF原理解析
1、ELF格式及其原理
在了解了程序执行本质、程序的内存区域和格式、ELF 文件的3种体现形式、动态链接和静态链接的概念后
我们看看ELF 文件格式到底是什么样子的,如下图所示。
ELF文件可分为两部分来看待,即链接节(linkable section) 和执行段(exectable segment) 。
从链接器的角度看,看到的是一堆 section,也称之为节。
从 CPU 调度执行的角度看,看到的是一堆 segment,也称之为段。还记得前面学习的段寄存器吗?那是将内存分段,这里是将一个可执行程序按功能分如代码段、数据段等。
同Java 语言编译后的 class 文件一样,它也有自己的头部,上图中ELF header 头部的下方存在 program header table,用于告诉 OS 在加载这个可执行文件后,这些段在哪里。
文件末尾的 section header table 则由链接器识别使用,用寻找这个文件的用于链接的节处于文件的哪个位置。
这时,我们可以给出结论,在动态链接器执行时,将忽略掉 program header table,只使用 section header table;而当程序被执行时,将忽略掉 section header table,只使用 program header table,同时一个 segment 段可以由多个 section 节组成。下图描述了 ELF 文件的不同视图。
2、ELF文件内部实现
2.1、ELF header文件头分析
ELF header 开始了解 ELF 文件格式,仍然用 hello world 的例子,用 gcc demo.c 生成使用动态连接的可执行文件。
使用 readelf -h a.out 指令输出 a.out 执行程序的 ELF 头部信息。同 Java 中的 .class 文件一样,ELF 也需要 magic 变量来表明它是一个 ELF 文件,且类别为 ELF64。此外,ELF 还需要使用版本信息和数据信息。这里,只需要关注以下几条信息。
MELF 类型为 EXEC,表明为可执行文件。
Entry point address 程序开始执行点为 0x400440。
program headers 程序头部表在 64byte偏移处。
section headers 节头部表处于 6480byte 偏移处。
此时,链接器或者 OS 就可以通过这些信息在内存中加载、链接、执行这个程序了。
当我们使用 gcc -static demo.c 、 readelf -h a.out 后可以看到,动态链接器的 INTERP 不见了,证明静态链接包含了所有需要的信息,所以不需要动态链接器的参与
2.2、ELF文件header table头部表分析
接下来,看看这个程序的 Program headers table 信息。
使用 readelf -l a.out 命令输出其 Program headers 信息,从下图可以看出以下信息。
PHDR,表明程序头部表的虚拟地址信息(偏移量为0x40,十进制为64byte,即Program headers
地址)。
INTERP,表明程序被 OS 加载到内存中后,必须调用的解释器,它通过链接其他库来满足未解析的符号引用,用于在虚拟地址空间中映射当前程序运行所需的动态链接库的函数,如程序中使用的 printf 函数。
LOAD,表明当前程序文件映射到虚拟地址空间的段,其中保存了常量数据(如字符串)、程序目标代码等。另外,还可以通过后面跟着的权限位来判断是代码段还是数据段,其中RE表明为可读、可执行的段,则为代码段;RW 表明可读、可写的段,则为数据段。
DYNAMIC,表明保存了由动态连接器,即 INTERP 段中指定的解释器使用的信息。
Section to Segment mapping,表明,这些段由 section 节组成。
2.3、ELF文件section节信息分析
接下来,我们来分析程序的section节信息。通过 readelf -S a.out 命令,可得到 section节信息。包含了当前程序的节表信息,包括每个节的大小、类型、虚拟地址信息和偏移量。通过这些信息我们可以在动态链接时组合成相应的段信息。对于每个节的标志位,Key to Flags中已经给出,这里不再赘述。通常我们可以看到以下节信息。
.hash:符号哈希表。
.dynsym、.dynstr:动态链接符号表,动态链接字符串表。
.rel.dyn、.rel.plt:节区中包含了重定位信息。
.init:此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。
.plt:此节区包含过程链接表(procedure linkage table)。
.text、.fini:此节区包含程序的可执行指令。fini 是进程终不止代码的一部分,程序正常退出时,系统将安排执行这里的代码。
.rodata:这些节区包含只读数据,这些数据道通常参与进程映像的只读代码段。
.init_array、.fini_array:进程初始化、退出时时所运行的函数指针数组。
.dynamic:此节区包含动态链接信息。
.got:此节区包含全局偏移表,其与 plt 一起协作完成符号的动态查找。
.data:节区包含初始化了的数据,将出现在程序的内存映像中。
.bss:包含将出现在程序的内存映像中的为初始化数据。根据定义,当开始执行程序时,系统将把这些数据初始化为 0。此节区不占用文件空间。
.comment、.debug_*:符号调试信息。
2.4、ELF文件分析总结
对于ELF 的文件描述和程的组成信息就描述到这里。这里读者只需要对程序组成有个基本印象,为后续的学习铺路,形成计算机思维。
读者可以看到,实际上程序分为3类,即共享目标文件、动态链接库和可执行文件。其中,可执行文件又分为可动态链接的执行文件和静态连接的执行文件。这里只分析了可动态链接的执行文件对比了静态连接的执行文件的程序头部表,看了看段信息,并没有去分析另外两类文件。这里给出了方法和相应的命令。
读者只需要通过本节了解如下信息即可,什么是 ELF,ELF header 的了解,什么是 section 节和 segment 段信息。
本文总结
本文深入浅出,讲解程序的本质(编译的过程),通过C语言代码的例子,分析程序从运行到CPU执行的整个过程和其中流转的原理。通过这个过程,我们开始了解程序的组成(程序所需的内存),程序保存在磁盘、内存,程序在内存中的由那几部分组成,堆区栈区代码段等等。最终通过这个保存,引申出程序的格式(ELF),分析了程序的动态连接、静态链接,什么是 ELF、ELF header 、section 节和 segment 段信息。相信这里读者对程序组成有个基本印象,为后续的学习铺路,形成完整的计算机思维。