专栏名称: Linux内核之旅
Linux内核之旅
目录
相关文章推荐
Linux内核之旅  ·  进程如何使用内存 学习笔记 ·  5 天前  
Linux内核之旅  ·  公平CFS调度类:SCHED_NORMAL、 ... ·  2 周前  
Linux内核之旅  ·  公平调度类的延伸:EEVDF ·  2 周前  
Linux内核之旅  ·  调用栈过深导致的火焰图错误该如何解决 ·  1 周前  
Linux内核之旅  ·  eBPF Talk: tailcall 问题知多少 ·  1 周前  
51好读  ›  专栏  ›  Linux内核之旅

从ELF文件到Linux进程

Linux内核之旅  · 公众号  · linux  · 2025-01-11 16:11

正文

    大家好,这里是物联网心球。

    今天我们来聊聊ELF文件,了解一下Linux如何创建进程以及ELF文件如何转变成Linux进程?


1.什么是ELF文件?

        ELF(Executable and Linkable Format)文件是一种目标文件格式,用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件。它主要用于Linux平台,用于存储和传输可执行文件和库。

        ‌文件类型‌:

    • 可执行文件‌:包含可执行的机器代码,可直接运行。

    • 可重定位文件‌(.o文件):机器代码和数据地址相对,需重定位才能运行,通常用于编译过程。

    • 共享对象文件(.so文件)‌:动态链接库,包含可共享代码和数据,可在运行时被多个进程共享。

    • 核心转储文件‌(core文件):程序崩溃或异常终止时生成,包含内存状态和寄存器信息,用于调试。


2.ELF文件格式

        如下图所示,ELF文件主要由:ELF头,程序头表,节区,节头表组成。

    • ‌ELF头‌:包含文件的基本信息,如类型、架构、入口地址等。

    • ‌程序头表‌:描述可执行文件中的段(Segment)信息,如类型、偏移地址、大小等。

    • ‌节区:存储实际的代码、数据等信息。

    • ‌节头表‌:描述目标文件中的节(Section)信息,如名字、类型、偏移地址、大小等。

        注意:.o文件没有程序头表,ELF文件并不一定有程序头表。

    段(Segment)和节(Section)有什么区别?

    节是ELF文件的基本单位,包含了程序的代码,数据等信息。Linu系统为了高效加载ELF文件,将多个节划分为一个组(段),段是节的集合,Linux系统通过段加载代码(节)和数据(节)。


2.1 ELF头

    ELF头是整个ELF文件的起始部分,位置固定,包含了识别和解释文件内容的关键信息。

    查看ELF文件头信息:

    #read -h ELF文件 

root@raspberrypi:/home/mfn# readelf -h a.out
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 (Position-Independent Executable file)
  Machine: AArch64
  Version: 0x1
  Entry point address: 0x600
  Start of program headers: 64 (bytes into file)
  Start of section headers: 68504 (bytes into file)
  Flags: 0x0
  Size of this header: 64 (bytes)
  Size of program headers: 56 (bytes)
  Number of program headers: 9
  Size of section headers: 64 (bytes)
  Number of section headers: 29
  Section header string table index: 28

ELF文件头字段解析:

  • Magic:魔数,ELF文件识别信息。

  • Class:文件类型,32位或64位。

  • Data:编码方式,大端或小端。

  • Version:ELF版本。

  • OS/ABI:操作系统信息。

  • ABI:ABI版本。

  • ‌Type:文件类型‌:可重定位文件(REL)、可执行文件(DYN)、共享对象文件(DYN),核心转储文件(CORE)。

  • ‌Machine:‌机器类型‌,表示ELF文件的平台属性,如x86、AArch64等。

  • ‌Entry point address:‌ 程序入口地址。

  • Start of program headers: 程序头表偏移量。

  • Start of section headers: 节头表偏移量。

  • Flags‌:标志。

  • Size of this header:ELF头大小。

  • Size of program headers:程序头表条目大小。

  • Number of program header:程序头表有多少条目。

  • Size of section headers:节头表条目大小。

  • Number of section headers:节头表有多少条目。


2.2 程序头表

    程序头表(Program Header Table用于描述文件中的段(Segment)信息,指导操作系统加载程序。

        程序头表由多个程序头组成,每个头对应一个段,包含该段的详细信息:段的类型、在文件中的偏移地址、映射到内存的虚拟地址、大小及权限等。

    查看程序头表信息:

    #readelf -l ELF文件 

程序头表字段解析:

  • TYPE:段类型,常见类型如下:

    • PT_PHDR‌:程序头表本身在文件中的位置和大小。

    • PT_LOAD:表示一个可加载的段,这种段包含了程序的实际代码和数据,需要被加载到内存中以便执行。

    • PT_DYNAMIC‌:指向动态链接信息,包含了动态链接器所需的各种表和字符串,如动态库路径、依赖库列表、符号表等。

    • PT_INTERP‌:指定了程序解释器的路径,这通常是动态链接器的路径。

    • PT_NOTE‌:附加信息。

  • Offset:文件偏移,段在文件中的偏移量。

  • VirtAddr:虚拟地址,段加载至内存后的虚拟地址。

  • PhysAddr:物理地址:段的物理地址。

  • FileSiz:文件大小,段在文件中大小。

  • MemSiz:内存大小,段在内存中大小。

  • Flags:段标识,段属性:只读属性(R),只写属性(W),可执行属性(E)。

  • Align:对齐方式。


2.3 节区

    ELF文件中包含多种节(Section),这些节在文件的编译、链接及执行过程中发挥关键作用。以下是一些常见的ELF文件节:

  • ‌.text‌:包含程序的代码段,是程序执行的主要部分。

  • ‌.data‌:包含已初始化的全局和静态变量,程序运行时需要的数据。

  • ‌.rodata‌:包含只读数据,如常量字符串、浮点数等。

  • ‌.bss‌:包含未初始化的全局和静态变量,运行时被分配内存并初始化为零。

  • ‌.symtab‌:符号表,包含程序中使用的符号信息,如函数名、变量名等。

  • ‌.strtab‌:字符串表,包含符号表中使用的字符串。

  • ‌.shstrtab‌:节名字符串表,包含所有节的名字。

  • ‌.dynamic‌:动态链接信息,包含动态链接器所需的各种信息。

    通过查看ELF文件符号表,可以协助我们排查程序bug:

    #readelf -s ELF文件


2.4 节头表

        节头表(Section Header Table),用于描述文件中各个节(Section)的属性和信息。
        每个节都有一个对应的节表头,它包含了节的名称、类型、大小、偏移量等关键数据,为链接器和加载器提供必要的信息。
       查看节头表信息:
        #readelf -s ELF文件
root@raspberrypi:/home/mfn# readelf -S a.out
There are 29 section headers, starting at offset 0x10b98:

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 0000000000000238 00000238
       000000000000001b 0000000000000000 A 0 0 1
  [ 2] .note.gnu.bu[...] NOTE 0000000000000254 00000254
       0000000000000024 0000000000000000 A 0 0 4
  [ 3] .note.ABI-tag NOTE 0000000000000278 00000278
       0000000000000020 0000000000000000 A 0 0 4
  [ 4] .gnu.hash GNU_HASH 0000000000000298 00000298
       000000000000001c 0000000000000000 A 5 0 8
  [ 5] .dynsym DYNSYM 00000000000002b8 000002b8
       00000000000000d8 0000000000000018 A 6 3 8
  [ 6] .dynstr STRTAB 0000000000000390 00000390
       000000000000008d 0000000000000000 A 0 0 1
  [ 7] .gnu.version VERSYM 000000000000041e 0000041e
       0000000000000012 0000000000000002 A 5 0 2
  [ 8] .gnu.version_r VERNEED 0000000000000430 00000430
       0000000000000030 0000000000000000 A 6 1 8
  [ 9] .rela.dyn RELA 0000000000000460 00000460
       00000000000000c0 0000000000000018 A 5 0 8
  [10] .rela.plt RELA 0000000000000520 00000520
       0000000000000060 0000000000000018 AI 5 22 8
  [11] .init PROGBITS 0000000000000580 00000580
       0000000000000018 0000000000000000 AX 0 0 4
  [12] .plt PROGBITS 00000000000005a0 000005a0
       0000000000000060 0000000000000000 AX 0 0 16
  [13] .text PROGBITS 0000000000000600 00000600
       000000000000012c 0000000000000000 AX 0 0 64
  [14] .fini PROGBITS 000000000000072c 0000072c
       0000000000000014 0000000000000000 AX 0 0 4
  [15] .rodata PROGBITS 0000000000000740 00000740
       0000000000000004 0000000000000004 AM 0 0 4
  [16] .eh_frame_hdr PROGBITS 0000000000000744 00000744
       000000000000003c 0000000000000000 A 0 0 4
  [17] .eh_frame PROGBITS 0000000000000780 00000780
       00000000000000a4 0000000000000000 A 0 0 8
  [18] .init_array INIT_ARRAY 000000000001fdc8 0000fdc8
       0000000000000008 0000000000000008 WA 0 0 8
  [19] .fini_array FINI_ARRAY 000000000001fdd0 0000fdd0
       0000000000000008 0000000000000008 WA 0 0 8
  [20] .dynamic DYNAMIC 000000000001fdd8 0000fdd8
       00000000000001e0 0000000000000010 WA 6 0 8
  [21] .got PROGBITS 000000000001ffb8 0000ffb8
       0000000000000030 0000000000000008 WA 0 0 8
  [22] .got.plt PROGBITS 000000000001ffe8 0000ffe8
       0000000000000038 0000000000000008 WA 0 0 8
  [23] .data PROGBITS 0000000000020020 00010020
       0000000000000010 0000000000000000 WA 0 0 8
  [24] .bss NOBITS 0000000000020030 00010030
       0000000000000008 0000000000000000 WA 0 0 1
  [25] .comment PROGBITS 0000000000000000 00010030
       000000000000001f 0000000000000001 MS 0 0 1
  [26] .symtab SYMTAB 0000000000000000 00010050
       0000000000000828 0000000000000018 27 65 8
  [27] .strtab STRTAB 0000000000000000 00010878
       000000000000021d 0000000000000000 0 0 1
  [28] .shstrtab STRTAB 0000000000000000 00010a95
       0000000000000103 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), p (processor specific)
节头表字段解析:
  • Name:节名。
  • Type:节类型
  • Address:虚拟地址。
  • Offset:文件偏移量。
  • Size:节大小,节在文件中的大小。
  • EntSize:节条目大小。
  • Flags:节属性:只读属性(R),只写属性(W),可执行属性(E)。
  • Link:链接,表示与该节相关联的符号表或者字符串表。
  • Info:节信息。
  • Align:对齐方式。

3.从ELF文件到Linux进程
    前面已经介绍介绍了ELF文件,相信很多小伙伴都很好奇,ELF文件是如何转变成Linux进程的。
    Linux创建一个新的进程需要经过两个步骤:
  • 步骤1:父进程通过克隆方式创建子进程。
  • 步骤2:子进程加载ELF文件生成新的进程地址空间。    
    对于用户程序来说,实现以上两个步骤需要调用fork和execve两个系统调用。
    Linux子进程的创建必须由父进程完成,因为这样的一个约束,Linux进程之间形成了类似于家族关系的进程关系,而所有进程的共同祖先就是1号进程(init)。
    子进程创建时需要继承父进程的关键信息才能正常工作,从父进程继承而来的信息只能保证子进程按照父进程的方式去工作。当子进程有特定的任务需要执行,此时从父进程继承的信息就没有意义了。子进程如果需要执行特定的任务,需要加载ELF文件,替换掉子进程从父进程继承的旧信息,生成新的进程信息,这样子进程就能独立工作了。

3.1 fork创建子进程
    如下图所示,用户程序调用fork系统调用后,内核主要完成两部分工作:
  • 创建子进程并克隆关键信息。
  • 将子进程插入CPU就绪队列,等待CPU调度。
    具体流程已在图中详细展示,这里不再赘述。
    Linux内核克隆子进程是一个很复杂的过程,这里我们保留一些关键流程,我们来看一下子进程从父进程继承了哪些信息:
  • files:已打开文件表,子进程和父进程拥有相同的已打开文件表,父子进程可以操作相同的文件。
  • fs:文件系统信息。
  • sighand:信号处理函数表,子进程和父进程处理信号的方式相同。
  • signal:信号信息,同上。
  • mm:进程地址空间,子进程和父进程的代码,数据相同。需要注意:数据相同表示虚拟地址和内存存储内容相同,而实际的物理地址则不相同。
  • nxproxy:命名空间。
    fork创建完子进程,如果子进程不需要执行特定的任务,此时子进程已经可以工作。如果子进程需要执行特定的任务,那么我们需要将任务编译成ELF文件,再通过ELF文件加载至子进程。

3.2 execve加载ELF文件
    如下图所示,execve系统调用主要工作就是替换进程地址空间(mm),而替换进程地址空间需要用到编译好的ELF文件。由于进程地址空间替换时,原来从父进程继承的信息已经没有用,所以替换的过程需要清理旧信息。
    进程地址空间替换内核流程如下图:
    具体流程已经在图中详细展示,这里也不再赘述。
    如下图所示,我们来看一下ELF文件转变成Linux进程的详细流程,用户程序调用execve系统调用后,首先会根据文件路径在磁盘中找到ELF文件,找到文件后打开文件(open),读取文件内容(read)。
    此时ELF文件已经从磁盘读入内存, 接着execve系统调用按照ELF文件格式解析ELF文件,解析出.bss,.data,.text将三者以文件映射方式映射至进程地址空间。文件映射可以减少拷贝,提高访问效率。

总结:
    了解ELF文件以及ELF文件如何转换为Linux进程,可以让我们对Linux程序有更深入的理解,我们在编程和调试程序的时,思路会更加清晰。