Linux:深入解析ELF文件结构

上篇文章:Linux C 语言编译链接全解析:静态库与动态库从原理到实战

目录

1.ELF文件

1.1ELF结构

[1.2查看ELF Header](#1.2查看ELF Header)

[1.3查看ELF Program Header Table](#1.3查看ELF Program Header Table)

[1.4 ELF Section Header Table](#1.4 ELF Section Header Table)

1.5查看具体的sections信息

2.ELF从形成到加载轮廓

2.1ELF形成可执行

2.2ELF可执行文件的加载

思考


1.ELF文件

通过编译和链接,在当前文件下有两个源文件,两个扩展名为.o的文件,还有一个可执行程序:

其中,.o文件被称为目标文件 ,目标文件是一个二进制文件,它的文件格式是ELF,是对二进制代码的一种封装。

使用file命令用于辨识文件类型:

为了更好的理解编译链接的细节,我们需要了解一下ELF文件,以下四种文件都属于ELF文件:

  • 可重定位文件(Relocatable File):即xxx.o文件。包含适用于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
  • 可执行文件(Executable File):即可执行程序。
  • 共享目标文件(Shared Object File):即xxx.so文件。
  • 内核转储(core dumps):存放当前进程的执行上下文,用于dump信号触发。

一个ELF文件由以下四部分组成:

  • ELF头(ELF header):描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
  • 程序头表(Program header table):列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分隔开。
  • 节头表(Section header table):包含对节(sections)的描述。
  • 节(Section):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。

最常见的节:

  • 代码节(.text):用于保存机器指令,是程序的主要执行部分。

  • 数据节(.data):保存已初始化的全局变量和局部静态变量。

    $ size code
    text data bss dec hex
    filename
    3312 636 4 3952 f70
    code

上图中的ELF Header和Section Header Table属于ELF的管理字段,类似:Super block,GDT,各种位图......

Section 1,Section 2......Section n为ELF的核心数据内容,类似:data block!

1.1ELF结构

1.2查看ELF Header

-h 或 --file-header:显示ELF文件的文件头信息。文件头包含了ELF文件的基本信息,比如文件类型、机器类型、版本、入口点地址、程序头表和节头表的位置和大小等。

内核中关于ELF Header相关的数据结构,操作系统自己必须能够识别特定格式的可执行程序:/linux/include/elf.h

复制代码
typedef struct elf32_hdr
{
    unsigned char e_ident[EI_NIDENT];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry; /* Entry point */
    Elf32_Off e_phoff;
    Elf32_Off e_shoff;
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
} Elf32_Ehdr;

typedef struct elf64_hdr
{
    unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
    Elf64_Half e_type;
    Elf64_Half e_machine;
    Elf64_Word e_version;
    Elf64_Addr e_entry; /* Entry point virtual address */
    Elf64_Off e_phoff;  /* Program header table file offset */
    Elf64_Off e_shoff;  /* Section header table file offset */
    Elf64_Word e_flags;
    Elf64_Half e_ehsize;
    Elf64_Half e_phentsize;
    Elf64_Half e_phnum;
    Elf64_Half e_shentsize;
    Elf64_Half e_shnum;
    Elf64_Half e_shstrndx;
} Elf64_Ehdr;

1.3查看ELF Program Header Table

查看section合并的segment

-l 或 --program-headers:显示ELF文件的程序头部(也称为段头)信息。可执行文件在内存中的布局和加载过程非常重要。

输入命令:

复制代码
readelf -l a.out

显示段头信息:

复制代码
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000628 0x0000000000000628  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x0000000000000199 0x0000000000000199  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x0000000000000124 0x0000000000000124  R      0x1000
  LOAD           0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
                 0x0000000000000258 0x0000000000000260  RW     0x1000
  DYNAMIC        0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
                 0x00000000000001f0 0x00000000000001f0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  NOTE           0x0000000000000368 0x0000000000000368 0x0000000000000368
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  GNU_EH_FRAME   0x000000000000201c 0x000000000000201c 0x000000000000201c
                 0x000000000000003c 0x000000000000003c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
                 0x0000000000000248 0x0000000000000248  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 

关于上述,PhysAddr:现代OS,可以不考虑了;LOAD:不是所有的section都要被加载到内存,有的是与加载链接有关的字段,但是有LOAD的就是将来要加载的部分。

内核中关于ELF Program Header相关的数据结构:

复制代码
typedef struct elf32_phdr
{
    Elf32_Word p_type;
    Elf32_Off p_offset;
    Elf32_Addr p_vaddr;
    Elf32_Addr p_paddr;
    Elf32_Word p_filesz;
    Elf32_Word p_memsz;
    Elf32_Word p_flags;
    Elf32_Word p_align;
} Elf32_Phdr;

typedef struct elf64_phdr
{
    Elf64_Word p_type;
    Elf64_Word p_flags;
    Elf64_Off p_offset;   /* Segment file offset */
    Elf64_Addr p_vaddr;   /* Segment virtual address */
    Elf64_Addr p_paddr;   /* Segment physical address */
    Elf64_Xword p_filesz; /* Segment size in file */
    Elf64_Xword p_memsz;  /* Segment size in memory */
    Elf64_Xword p_align;  /* Segment alignment, file & memory */
} Elf64_Phdr;

1.4 ELF Section Header Table

查看可执行程序的section

-S 或 --section-headers:显示ELF文件的节头信息。节头描述了ELF文件的各个节的起始地址、大小、标志等信息

命令:

复制代码
readelf -S a.out

显示结果:

复制代码
There are 31 section headers, starting at offset 0x36d0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.pr[...] NOTE             0000000000000338  00000338
       0000000000000030  0000000000000000   A       0     0     8
  [ 3] .note.gnu.bu[...] NOTE             0000000000000368  00000368
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .note.ABI-tag     NOTE             000000000000038c  0000038c
       0000000000000020  0000000000000000   A       0     0     4
  [ 5] .gnu.hash         GNU_HASH         00000000000003b0  000003b0
       0000000000000024  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           00000000000003d8  000003d8
       00000000000000a8  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           0000000000000480  00000480
       000000000000008d  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           000000000000050e  0000050e
       000000000000000e  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          0000000000000520  00000520
       0000000000000030  0000000000000000   A       7     1     8
  [10] .rela.dyn         RELA             0000000000000550  00000550
       00000000000000c0  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             0000000000000610  00000610
       0000000000000018  0000000000000018  AI       6    24     8
  [12] .init             PROGBITS         0000000000001000  00001000
       000000000000001b  0000000000000000  AX       0     0     4
  [13] .plt              PROGBITS         0000000000001020  00001020
       0000000000000020  0000000000000010  AX       0     0     16
  [14] .plt.got          PROGBITS         0000000000001040  00001040
       0000000000000010  0000000000000010  AX       0     0     16
  [15] .plt.sec          PROGBITS         0000000000001050  00001050
       0000000000000010  0000000000000010  AX       0     0     16
  [16] .text             PROGBITS         0000000000001060  00001060
       000000000000012b  0000000000000000  AX       0     0     16
  [17] .fini             PROGBITS         000000000000118c  0000118c
       000000000000000d  0000000000000000  AX       0     0     4
  [18] .rodata           PROGBITS         0000000000002000  00002000
       000000000000001c  0000000000000000   A       0     0     4
  [19] .eh_frame_hdr     PROGBITS         000000000000201c  0000201c
       000000000000003c  0000000000000000   A       0     0     4
  [20] .eh_frame         PROGBITS         0000000000002058  00002058
       00000000000000cc  0000000000000000   A       0     0     8
  [21] .init_array       INIT_ARRAY       0000000000003db8  00002db8
       0000000000000008  0000000000000008  WA       0     0     8
  [22] .fini_array       FINI_ARRAY       0000000000003dc0  00002dc0
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .dynamic          DYNAMIC          0000000000003dc8  00002dc8
       00000000000001f0  0000000000000010  WA       7     0     8
  [24] .got              PROGBITS         0000000000003fb8  00002fb8
       0000000000000048  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000004000  00003000
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000004010  00003010
       0000000000000008  0000000000000000  WA       0     0     1
  [27] .comment          PROGBITS         0000000000000000  00003010
       000000000000002d  0000000000000001  MS       0     0     1
  [28] .symtab           SYMTAB           0000000000000000  00003040
       0000000000000390  0000000000000018          29    19     8
  [29] .strtab           STRTAB           0000000000000000  000033d0
       00000000000001e6  0000000000000000           0     0     1
  [30] .shstrtab         STRTAB           0000000000000000  000035b6
       000000000000011a  0000000000000000           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),
  D (mbind), l (large), p (processor specific)

.got 通常与全局偏移表(Global Offset Table,简称 GOT)。

内核中关于ELF Section Header相关的数据结构

复制代码
typedef struct
{
    Elf32_Word sh_name;
    Elf32_Word sh_type;
    Elf32_Word sh_flags;
    Elf32_Addr sh_addr;
    Elf32_Off sh_offset;
    Elf32_Word sh_size;
    Elf32_Word sh_link;
    Elf32_Word sh_info;
    Elf32_Word sh_addralign;
    Elf32_Word sh_entsize;
} Elf32_Shdr;

typedef struct elf64_shdr
{
    Elf64_Word sh_name;       /* Section name, index in string tbl */
    Elf64_Word sh_type;       /* Type of section */
    Elf64_Xword sh_flags;     /* Miscellaneous section attributes */
    Elf64_Addr sh_addr;       /* Section virtual addr at execution */
    Elf64_Off sh_offset;      /* Section file offset */
    Elf64_Xword sh_size;      /* Size of section in bytes */
    Elf64_Word sh_link;       /* Index of another section */
    Elf64_Word sh_info;       /* Additional section information */
    Elf64_Xword sh_addralign; /* Section alignment */
    Elf64_Xword sh_entsize;   /* Entry size if section holds table */
} Elf64_Shdr;

1.5查看具体的sections信息

命令:

复制代码
objdump -S a.out

大致信息:

查看编译后的.o目标文件

2.ELF从形成到加载轮廓

2.1ELF形成可执行

第一步:将多份C/C++源代码,翻译成为目标.o文件 + 动静态库(ELF)

第二步:将多份.o文件section进行合并(库的本质是.o,都是ELF)

注意:实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并。

2.2ELF可执行文件的加载

一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment

合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。

这样,即使是不同的Section,在加载内存中,可能会以segment的形式,加载到一起

显然,这个合并工作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的程序头表(Program header table)中。

复制代码
# 查看可执⾏程序的section 
$ readelf -S a.out 

# 查看section合并的segment 
$ readelf -l a.out 

思考

为什么要将section合并为segment?

  • Section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
  • 操作系统在加载程序时,会将具有相同属性的section合并为一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。

对于程序头表(Program header table)和节头表(Section header table)又有什么用呢?

ELF文件提供两个不同的视图/视角来让我们理解这两个部分:

  1. 链接视图(Linking view) - 对应节头表(Section header table)
    • 文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息
    • 为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整为可执行的段(segment),可读写的段、只读段等。合并后,空间利用率就高了,否则,很小很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一起给你,比如4K),所以,链接器趁着链接就把小块们都合并了。
  2. 执行视图(execution view) - 对应程序头表(Program header table)
    • 告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有program header table

总结:一个在链接时作用,一个在运行加载时作用。

从链接视图看:

  • 命令readelf -S hello.o 可以帮助查看ELF文件的节头表。
  • .text节:是保存了程序代码指令的代码节。
  • .data节:保存了初始化的全局变量和局部静态变量等数据。
  • .rodata节:保存了只读的数据,如一行c语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能是在text段(不是data段)中找到.rodata节。
  • .BSS节:为未初始化的全局变量和局部变量预留位置
  • .symtab节:Symbol Table符号表,这就是源码里面那些函数名、变量名和代码的对应关系。
  • .got.plt节(全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。

使用readelf命令查看.so文件,可以看到该节。

从执行视图看:

  • 告诉操作系统哪些模块可以被加载进内存。
  • 加载进内存之后,哪些分段是可读可写,哪些分段是只读,哪些分段是可执行。

我们可以在ELF头中找到文件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。例如我们查看hello.o这个可重定位文件的主要信息:

复制代码
// 查看⽬标⽂件 
$ readelf -h hello.o 
ELF Header:
 Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # ELF⽂件标识符(魔数)
 Class: ELF64 # ⽂件类:64位架构
 Data: 2's complement, little endian # 数据编码:⼩端序⼆进制补码
 Version: 1 (current) # ELF版本:当前版本
(1)
 OS/ABI: UNIX - System V # 操作系统ABI:System V UNIX
 ABI Version: 0 # ABI扩展版本:未扩展(0)
 Type: REL (Relocatable file) # ⽂件类型:可重定位⽂件(⽬标⽂件)
 Machine: Advanced Micro Devices X86-64 # ⽬标平台:x86-64架构
 Version: 0x1 # 对象⽂件版本:1
 Entry point address: 0x0 # ⼊⼝点地址:⽆(⽬标⽂件为0)
 Start of program headers: 0 (bytes into file) # 程序头表起始偏移:⽆(⽬标⽂件为0)
 Start of section headers: 728 (bytes into file) # 节头表起始偏移:728字节处
 Flags: 0x0 # 处理器特定标志:
⽆标志(0)
 Size of this header: 64 (bytes) # ELF头⼤⼩:64字
节
 Size of program headers: 0 (bytes) # 程序头表条⽬⼤
⼩:⽆(⽬标⽂件为0)
 Number of program headers: 0 # 程序头表条⽬数:
⽆(⽬标⽂件为0)
 Size of section headers: 64 (bytes) # 节头表条⽬⼤⼩:
64字节
 Number of section headers: 13 # 节头表条⽬数:13
个节
 Section header string table index: 12 # 节名称字符串表索
引:第12节(.shstrtab)
 
// 查看可执⾏程序 
$ gcc *.o
$ readelf -h a.out 
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
 Class: ELF64 # 64位架构
 Data: 2's complement, little endian # ⼩端序⼆进制补码
 Version: 1 (current) # ELF版本:1(当前标准)
 OS/ABI: UNIX - System V # 操作系统ABI:System V UNIX
 ABI Version: 0 # ⽆扩展ABI
 Type: DYN (Shared object file) # ⽂件类型:动态共享库(.so)
 Machine: Advanced Micro Devices X86-64 # ⽬标平台:x86-64
 Version: 0x1 # 对象⽂件版本:1
 Entry point: 0x1060 # 程序⼊⼝点虚拟地址(动态链接后解析)
 Start of program headers: 64 (bytes into file) # 程序头表起始偏移:64字节
 Start of section headers: 14768 (bytes into file) # 节头表起始偏移:14768字节
 Flags: 0x0 # ⽆处理器特定标志
 Size of this header: 64 (bytes) # ELF头⼤⼩:64字节
 Size of program headers: 56 (bytes) # 程序头表每个条⽬⼤⼩:56字节
 Number of program headers: 13 # 程序头表条⽬数:13(如LOAD、DYNAMIC等段)
 Size of section headers: 64 (bytes) # 节头表每个条⽬⼤⼩:64字节
 Number of section headers: 31 # 节头表条⽬数:31个节(如.text、.data等)
 Section header string table index: 30 # 节名称字符串表索引:第30节(.shstrtab)

本章完。

相关推荐
互联网推荐官1 小时前
上海软件定制开发与数字化建设:D-coding 全平台应用架构及实施指南
大数据·运维
ShineWinsu1 小时前
对于Linux:进程间通信IPC(共享内存)的解析
linux·服务器·面试·笔试·进程·共享内存·ipc
代码中介商1 小时前
Linux 进程间通信:共享内存与消息队列完全指南
linux·运维·服务器
计算机安禾1 小时前
【Linux从入门到精通】第27篇:文本处理三剑客(上)——grep 正则表达式实战
linux·运维·正则表达式
码到成功>_<1 小时前
Linux中grep命令使用说明
linux
minji...2 小时前
Linux 网络套接字编程(六)TCP的通信是全双工的,自定义协议的定制,序列化和反序列化
linux·运维·服务器·网络·c++
小王C语言2 小时前
【linux进程信号】————产生信号:signal自定义信号处理动作(自定义捕捉)、前后台进程、产生信号的方式(函数、软条件、硬件异常)....等等
运维·服务器·前端
Gauss松鼠会2 小时前
效率起飞!GaussDB 管理平台(TPOPS)升级指南
服务器·数据库·性能优化·gaussdb·经验总结
晚风予卿云月2 小时前
【linux】僵尸进程与孤儿进程
linux·运维·服务器