大家好,这里是物联网心球。
今天我们来聊聊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)信息,指导操作系统加载程序。
程序头表字段解析:
2.3 节区
ELF文件中包含多种节(Section),这些节在文件的编译、链接及执行过程中发挥关键作用。以下是一些常见的ELF文件节:
.text:包含程序的代码段,是程序执行的主要部分。
.data:包含已初始化的全局和静态变量,程序运行时需要的数据。
.rodata:包含只读数据,如常量字符串、浮点数等。
.bss:包含未初始化的全局和静态变量,运行时被分配内存并初始化为零。
.symtab:符号表,包含程序中使用的符号信息,如函数名、变量名等。
.strtab:字符串表,包含符号表中使用的字符串。
.shstrtab:节名字符串表,包含所有节的名字。
.dynamic:动态链接信息,包含动态链接器所需的各种信息。
通过查看ELF文件符号表,可以协助我们排查程序bug:
#readelf -s ELF文件
节头表(Section Header Table),用于描述文件中各个节(Section)的属性和信息。 每个节都有一个对应的节表头,它包含了节的名称、类型、大小、偏移量等关键数据,为链接器和加载器提供必要的信息。
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)
- Flags:节属性:只读属性(R),只写属性(W),可执行属性(E)。
- Link:链接,表示与该节相关联的符号表或者字符串表。
前面已经介绍介绍了ELF文件,相信很多小伙伴都很好奇,ELF文件是如何转变成Linux进程的。
- 步骤2:子进程加载ELF文件生成新的进程地址空间。
对于用户程序来说,实现以上两个步骤需要调用fork和execve两个系统调用。 Linux子进程的创建必须由父进程完成,因为这样的一个约束,Linux进程之间形成了类似于家族关系的进程关系,而所有进程的共同祖先就是1号进程(init)。 子进程创建时需要继承父进程的关键信息才能正常工作,从父进程继承而来的信息只能保证子进程按照父进程的方式去工作。当子进程有特定的任务需要执行,此时从父进程继承的信息就没有意义了。子进程如果需要执行特定的任务,需要加载ELF文件,替换掉子进程从父进程继承的旧信息,生成新的进程信息,这样子进程就能独立工作了。 如下图所示,用户程序调用fork系统调用后,内核主要完成两部分工作: Linux内核克隆子进程是一个很复杂的过程,这里我们保留一些关键流程,我们来看一下子进程从父进程继承了哪些信息:- files:已打开文件表,子进程和父进程拥有相同的已打开文件表,父子进程可以操作相同的文件。
- sighand:信号处理函数表,子进程和父进程处理信号的方式相同。
- mm:进程地址空间,子进程和父进程的代码,数据相同。需要注意:数据相同表示虚拟地址和内存存储内容相同,而实际的物理地址则不相同。
fork创建完子进程,如果子进程不需要执行特定的任务,此时子进程已经可以工作。如果子进程需要执行特定的任务,那么我们需要将任务编译成ELF文件,再通过ELF文件加载至子进程。
如下图所示,execve系统调用主要工作就是替换进程地址空间(mm),而替换进程地址空间需要用到编译好的ELF文件。由于进程地址空间替换时,原来从父进程继承的信息已经没有用,所以替换的过程需要清理旧信息。 如下图所示,我们来看一下ELF文件转变成Linux进程的详细流程,用户程序调用execve系统调用后,首先会根据文件路径在磁盘中找到ELF文件,找到文件后打开文件(open),读取文件内容(read)。
此时ELF文件已经从磁盘读入内存, 接着execve系统调用按照ELF文件格式解析ELF文件,解析出.bss,.data,.text将三者以文件映射方式映射至进程地址空间。文件映射可以减少拷贝,提高访问效率。
了解ELF文件以及ELF文件如何转换为Linux进程,可以让我们对Linux程序有更深入的理解,我们在编程和调试程序的时,思路会更加清晰。