linux系统的ELF文件解析(一)

前言

我们常说CPU只认识0和1,操作系统运行程序的过程就是在读取程序包中的机器码并执行的过程。可执行程序的文件确实主要由机器码组成,但是这些保存在磁盘中的的0、1机器码并不是杂乱的随机分布的,而是按照固定的规则来进行排列分布的。

这其实也不难理解,操作系统要执行程序,就需要从磁盘中读取程序文件,但是假如程序和数据都是杂乱的,那么操作系统将无法理解所读取的机器码的含义,也不知道该从哪里执行。所以操作系统会规定可执行文件的存储格式,比如文件开头的固定部分内容代表什么,包含什么信息。有了这种约定,操作系统不管面对什么程序,都可以先尝试从程序的头部读取固定部分的信息从而了解整个程序的情况,进而执行程序。

每个操作系统都会定义自己平台的可执行文件的格式,比如windows平台的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),本质上他俩是类似的,而且不光是可执行文件按照这种格式,其引用的动态链接库(windows的dll,linux的.so)静态链接库(windows 的.lib,linux的.a)也同样按照这种格式组织文件。

今天我们就以linux平台为例,解析一下ELF文件。

ELF文件的基本格式

文件头

操作系统中,大多数类型的文件都有文件头,文件头一般用于标识该文件的一些重要信息,ELF文件也不例外,ELF的header部分提供了该文件的重要结构表的地址以及程序入口地址,为此linux定义了header的数据结构:

arduino 复制代码
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Off;

#define EI_NIDENT 16

typedef struct {
    unsigned char e_ident[EI_NIDENT];  // 最开头是16个字节的e_ident, 其中包含用以表示ELF文件的字符,以及其他一些与机器无关的信息。开头的4个字节值固定不变,为0x7f和ELF三个字符。
    uint16_t      e_type;  // 该文件的类型 2字节
    uint16_t      e_machine;  // 该程序需要的体系架构 2字节
    uint32_t      e_version;  // 文件的版本 4字节
    Elf64_Addr   e_entry;  // 程序的入口地址 8字节
    Elf64_Off      e_phoff;  // Program header table 在文件中的偏移量 8字节
    Elf64_Off      e_shoff;  // Section header table 在文件中的偏移量 8字节
    uint32_t      e_flags;  // 对IA32而言,此项为0。 4字节
    uint16_t      e_ehsize;  // 表示ELF header大小 2字节
    uint16_t      e_phentsize;  // 表示Program header table中每一个条目的大小 2字节
    uint16_t      e_phnum;  // 表示Program header table中有多少个条目 2字节
    uint16_t      e_shentsize;  // 表示Section header table中的每一个条目的大小 2字节
    uint16_t      e_shnum;  // 表示Section header table中有多少个条目 2字节
    uint16_t      e_shstrndx;  // 包含节名称的字符串是第几个节 2字节
} Elf64_Ehdr;

下面我们以一段代码为例

arduino 复制代码
// main.c
#include<stdio.h>


void sayWords(){
printf("hello owrld from C \n");
}

 int main(){
    sayWords();

    return 0;
 }

把它编译为linux系统下可执行文件

less 复制代码
$ gcc -o main.o main.c
// 然后执行mian.o
$ ./main.o

接着我们使用readelf工具来读取这个可执行文件

less 复制代码
readelf -h main.o
 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060  # 程序入口地址
  Start of program headers:          64 (bytes into file)  #PHT的地址
  Start of section headers:          14008 (bytes into file)  # SHT的地址
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes) # 对应的尺寸
  Number of program headers:         13
  Size of section headers:           64 (bytes)  
  Number of section headers:         31 
  Section header string table index: 30  # shstrtab段的所在sht的数组下标

我们参照上文的header的数据结构可以知道开头的这16个字节对应e_ident这个字符数组,这其中的前四个字节称之为ELF文件的魔数,接下来的一个字节表示ELF文件的是32位还是64位,第六个字节表示当前文件的字节序,是大端还是小端,第七个字节表示ELF的版本,后面的字节暂时没有定义,用0填充

类似的示意图

其余比较重要的的信息有程序的入口地址(也就是代码段的地址),以及SHT(Section header table)和PHT(Program Header Table),他们分别表示程序在编译链接后的程序的组织方式以及操作系统在内存中运行程序时的组织方式。

Section header table

section header table是程序编译链接之后按照功能逻辑组织程序代码数据的方式。

我们知道c/cpp源代码代码再被编译之后生成中间文件,中间文件会分成不同的部分,有一些属于代码逻辑,有些是数据,有些是符号....等等这些不同性质的数据一般会分门别类的放置,因此出现代码段,数据段,符号段,

而且c/cpp的代码都是文件单独编译,然后通过链接的方式把一个个编译后的中间文件链接成一个可执行程序。那么这个链接的过程很重要的一步就是把不同的中间文件里的数据合并,而合并的方式就是把他们内部的同类型的段进行合并。

而为了能够在一个地方很方便的找到这些段,定义一个section header table也是很合理的事情。

同样的,我们使用readelf工具的命令也可以查看sht的结构

less 复制代码
$ readelf -S main.o

There are 31 section headers, starting at offset 0x36b8:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        0000000000000318 000318 00001c 00   A  0   0  1
  [ 2] .note.gnu.property NOTE           0000000000000338 000338 000030 00   A  0   0  8
  [ 3] .note.gnu.build-id NOTE           0000000000000368 000368 000024 00   A  0   0  4
  [ 4] .note.ABI-tag     NOTE            000000000000038c 00038c 000020 00   A  0   0  4
  [ 5] .gnu.hash         GNU_HASH        00000000000003b0 0003b0 000024 00   A  6   0  8
  [ 6] .dynsym           DYNSYM          00000000000003d8 0003d8 0000a8 18   A  7   1  8
  [ 7] .dynstr           STRTAB          0000000000000480 000480 00008d 00   A  0   0  1
  [ 8] .gnu.version      VERSYM          000000000000050e 00050e 00000e 02   A  6   0  2
  [ 9] .gnu.version_r    VERNEED         0000000000000520 000520 000030 00   A  7   1  8
  [10] .rela.dyn         RELA            0000000000000550 000550 0000c0 18   A  6   0  8
  [11] .rela.plt         RELA            0000000000000610 000610 000018 18  AI  6  24  8
  [12] .init             PROGBITS        0000000000001000 001000 00001b 00  AX  0   0  4
  [13] .plt              PROGBITS        0000000000001020 001020 000020 10  AX  0   0 16
  [14] .plt.got          PROGBITS        0000000000001040 001040 000010 10  AX  0   0 16
  [15] .plt.sec          PROGBITS        0000000000001050 001050 000010 10  AX  0   0 16
  [16] .text             PROGBITS        0000000000001060 001060 00011c 00  AX  0   0 16
  [17] .fini             PROGBITS        000000000000117c 00117c 00000d 00  AX  0   0  4
  [18] .rodata           PROGBITS        0000000000002000 002000 000018 00   A  0   0  4
  [19] .eh_frame_hdr     PROGBITS        0000000000002018 002018 00003c 00   A  0   0  4
  [20] .eh_frame         PROGBITS        0000000000002058 002058 0000cc 00   A  0   0  8
  [21] .init_array       INIT_ARRAY      0000000000003db8 002db8 000008 08  WA  0   0  8
  [22] .fini_array       FINI_ARRAY      0000000000003dc0 002dc0 000008 08  WA  0   0  8
  [23] .dynamic          DYNAMIC         0000000000003dc8 002dc8 0001f0 10  WA  7   0  8
  [24] .got              PROGBITS        0000000000003fb8 002fb8 000048 08  WA  0   0  8
  [25] .data             PROGBITS        0000000000004000 003000 000010 00  WA  0   0  8
  [26] .bss              NOBITS          0000000000004010 003010 000008 00  WA  0   0  1
  [27] .comment          PROGBITS        0000000000000000 003010 00002b 01  MS  0   0  1
  [28] .symtab           SYMTAB          0000000000000000 003040 000378 18     29  18  8
  [29] .strtab           STRTAB          0000000000000000 0033b8 0001e3 00      0   0  1
  [30] .shstrtab         STRTAB          0000000000000000 00359b 00011a 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  R (retain), l (large), p (processor specific)

sht是一个大小为31的数组,数组的每个元素都是一个固定大小的结构体(结构体你可以理解为一个对象),结构体的定义如下

ini 复制代码
typedef struct elf64_shdr {
	Elf64_Word sh_name;  #段名
	Elf64_Word sh_type;  #段的类型
	Elf64_Xword sh_flags; #段的标志位 W:该段可写  A:该段需要申请内存空间  X:该段可被执行
	Elf64_Addr sh_addr;  #段的地址
	Elf64_Off sh_offset; #内存中的虚拟地址
	Elf64_Xword sh_size;  #文件中的偏移
	Elf64_Word sh_link;  #段链接信息
	Elf64_Word sh_info;
	Elf64_Xword sh_addralign; # 段地址对齐
	Elf64_Xword sh_entsize;    #段大小(针对固定大小)
} Elf64_Shdr;

从这个数组组成的段表中我们也可以看到ELF头信息中的程序入口地址就是代码段的地址,而Start of section headers也确实指向对应的sht的地址。

需要解释一下的是,程序在运行时往往是基于某一个基地址开始运行,然后后续的执行和跳转的地址都是在基地址的基础上加上偏移(offset),而在编译链接之后,编译器链接器并不知道程序未来会在哪里运行,因此一般会把基地址假设为0,那么此时的偏移就可以被认为是当前地址。

Program Header Table

操作系统通过内存映射的方式读入可执行程序,为了节约内存空间往往不是把整个程序读入到内存中,而是通过内存分页(把整块的内存划分了一个个细小的块),一页一页的载入程序的部分内容。假设单页的大小是4kb,那么载入程序部分内容时,比如载入代码段,往往希望代码段的大小能正好时页大小的整数倍,这样能达到最好的内存使用效率,但现实很难做到,假设载入的代码段时4100byte(4kb多4个字节),因为超过了一个页的大小,所以系统必须给程序分两个页,也就是8kb,这就产生了几乎整页的浪费。假设此时又载入了一个init段,大小是512byte,则又需要分一个页,这就导致不到5kb的内容占据了3个页(12kb)的大小。

于是自然就产生了一个合理的设计,那就是尽可能的把程序的section都归纳到一起载入,于是就需要对之前的section重新分类,section是根据功能属性来区分的,这次则是根据section的权限来区分:把读写权限相同的段视作一个新的部分(我们称作segment)。

于是像我们举例的text段和init段,可以被视作一个段(segment)载入内存的话,那么他们的大小是4612byte,只需要占用两个页的大小即可。

我们可以使用如下命令查看PHT

python 复制代码
$ readelf -l main.o


Elf file type is DYN (Shared object file)
Entry point 0x1060
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R   0x8
  INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R   0x1000
  LOAD           0x001000 0x0000000000001000 0x0000000000001000 0x000189 0x000189 R E 0x1000
  LOAD           0x002000 0x0000000000002000 0x0000000000002000 0x000124 0x000124 R   0x1000
  LOAD           0x002db8 0x0000000000003db8 0x0000000000003db8 0x000258 0x000260 RW  0x1000
  DYNAMIC        0x002dc8 0x0000000000003dc8 0x0000000000003dc8 0x0001f0 0x0001f0 RW  0x8
  NOTE           0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R   0x8
  NOTE           0x000368 0x0000000000000368 0x0000000000000368 0x000044 0x000044 R   0x4
  GNU_PROPERTY   0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R   0x8
  GNU_EH_FRAME   0x002018 0x0000000000002018 0x0000000000002018 0x00003c 0x00003c R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x002db8 0x0000000000003db8 0x0000000000003db8 0x000248 0x000248 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .plt.got .plt.sec .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .data .bss 
   06     .dynamic 
   07     .note.gnu.property 
   08     .note.gnu.build-id .note.ABI-tag 
   09     .note.gnu.property 
   10     .eh_frame_hdr 
   11     
   12     .init_array .fini_array .dynamic .got 
   None   .comment .symtab .strtab .shstrtab 

PHT也是一个数组结构,而且ELF中也有一个专门的数据结构表示PHT,

arduino 复制代码
typedef struct {
    Elf64_Word      p_type;         // segment的类型 LOAD 表示会被映射到内存中; INTERP表示链接器类型 用于载入动态连接器
    Elf64_Word      p_flags;        // segment的权限属性 R 读 W写 E执行
    Elf64_Off       p_offset;       //  偏移
    Elf64_Addr      p_vaddr;        //  segment在内存的虚拟地址空间的的地址
    Elf64_Addr      p_paddr;        //  物理装载地址
    Elf64_Xword     p_filesz;       //  segment在elf文件中占据的空间长度
    Elf64_Xword     p_memsz;        //  segment在虚拟地址空间中占据的长度
    Elf64_Xword     p_align;        //  对齐属性 对齐2的p_align次方
} Elf64_Phdr;

PHT的存在,使得操作系统加载程序时尽可能少的产生内存碎片,尽可能的提高内存的使用效率。

如果我们仔细观察,会发现PHT的starting at offset=64,和ELF Header的大小一致,也就是说PHT其实紧跟在ELF Header之后存放。

小结

通过ELF文件头,SHT,PHT这三个数据结构,操作系统已经可以从总体上获得了这个程序文件的使用说明书了。

  • 通过读取ELF-Header,可以获得程序的执行入口,SHT的地址,PHT的地址。
  • 通过PHT可以获得程序的最佳载入方式
  • 通过SHT,可以在程序执行过程中定位到不同功能的段(.symtab/.data/.init)进而获得详细的信息。
相关推荐
全栈开发圈几秒前
新书速览|Java网络爬虫精解与实践
java·开发语言·爬虫
WaaTong3 分钟前
《重学Java设计模式》之 单例模式
java·单例模式·设计模式
面试鸭5 分钟前
离谱!买个人信息买到网安公司头上???
java·开发语言·职场和发展
沈询-阿里1 小时前
java-智能识别车牌号_基于spring ai和开源国产大模型_qwen vl
java·开发语言
AaVictory.1 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
LuckyLay1 小时前
Spring学习笔记_27——@EnableLoadTimeWeaving
java·spring boot·spring
向阳12182 小时前
Dubbo负载均衡
java·运维·负载均衡·dubbo
Gu Gu Study2 小时前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
WaaTong2 小时前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式