今天我们来探究一下Linux 系统的基石:ELF文件。 为什么这么说呢,因为 unix 的哲学,一切皆文件。 在操作系统眼里,一切东西无非就是两种属性,读和写。那为什么不把他们抽象成一种数据类型:文件。拥有的方法无非就是Open,Close,read,write。所有的东西都可以看成是File,包括我们接入的鼠标,键盘,u 盘,键盘接入操作系统,操作系统给他分配一个路径,如/dev/input,键盘输入hello world 即写入一个 /dev/input/event0 的文件,然后进程去这个路径读取这个 event0 文件就好了,进行一系列处理之后删除这个文件。
对于ELF文件而言,也是类似的道理,进程启动的时候就是将文件的内容搬运到内存上,然后再搬运到cpu中,读取并进行一些处理,然后移除,关闭
虽然都是文件,但是文件都有其特有的编码规范,比如class文件就是安卓Class文件规范来写的,我们读的时候按照对应的规范完善就好了。同理ELF文件也有其规范。我们来解读一下ELF文件规范:
ELF 文件类型
ELF全称Executable and Linkable Format,可执行连接格式,ELF格式的文件用于存储Linux程序。ELF文件(目标文件)格式主要三种:
- 可重定位文件:用户和其他目标文件一起创建可执行文件或者共享目标文件,例如lib*.a文件。
- 可执行文件:用于生成进程映像,载入内存执行,例如编译好的可执行文件a.out。
- 共享目标文件:用于和其他共享目标文件或者可重定位文件一起生成elf目标文件或者和执行文件一起创建进程映像,例如lib*.so文件。
一般的 ELF 文件包括三个索引表:ELF header,Program header table,Section header table。
- ELF header:在文件的开始,保存了路线图,描述了该文件的组织情况。
- Program header table:告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
- Section header table:包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。
ELF 文件头
我们看看Linux系统源码中的ELF header 的结构体定义:
// 32 位系统
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number和其它信息 */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
// 64 位系统
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file 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; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
下面解释下 header 的含义:
- e_ident[EI_NIDENT] 占16个字节。前四个字节被称作ELF的Magic Number。后面的字节描述了ELF文件内容如何解码等信息。
- e_type 2字节,描述了ELF文件的类型。以下取值有意义:
ET_NONE, 0, No file type
ET_REL, 1, Relocatable file(可重定位文件,通常是文件名以.o结尾,目标文件)
ET_EXEC, 2, Executable file (可执行文件)
ET_DYN, 3, Shared object file (动态库文件,你用gcc编译出的二进制往往也属于这种类型,惊讶吗?)
ET_CORE, 4, Core file (core文件,是core dump生成的吧?)
ET_NUM, 5,表示已经定义了5种文件类型
ET_LOPROC, 0xff00, Processor-specific
ET_HIPROC, 0xffff, Processor-specific
- e _machine 2字节。描述了文件面向的架构
- e_version 2字节,描述了ELF文件的版本号
- e_entry,(32位4字节,64位8字节),执行入口点,如果文件没有入口点,这个域保持0。
- e_phoff, (32位4字节,64位8字节),program header table的offset,如果文件没有PH,这个值是0。
- e_shoff, (32位4字节,64位8字节), section header table 的offset,如果文件没有SH,这个值是0。
- e_flags, 4字节,特定于处理器的标志,32位和64位Intel架构都没有定义标志,因此eflags的值是0。
- e_ehsize, 2字节,ELF header的大小,32位ELF是52字节,64位是64字节。
- e_phentsize,2字节。program header table中每个入口的大小。
- e_phnum, 2字节。如果文件没有program header table, e_phnum的值为0。e_phentsize乘以e_phnum就得到了整个program header table的大小。
- e_shentsize, 2字节,section header table中entry的大小,即每个section header占多少字节。
- e_shnum, 2字节,section header table中header的数目。如果文件没有section header table, e_shnum的值为0。e_shentsize乘以e_shnum,就得到了整个section header table的大小。
- e_shstrndx, 2字节。section header string table index. 包含了section header table中section name string table。如果没有section name string table, e_shstrndx的值是SHN_UNDEF.
我们在结合readelf来解读一下ELF header 例如我将一个go程序编译成main(ELF文件),然后执行readelf -h mian,得到如下结果:
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: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x45d310
Start of program headers: 64 (bytes into file)
Start of section headers: 456 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 25
Section header string table index: 3
以上信息展示得很明确了,我们来看看具体得二进制信息是怎样的,根据以上信息,我们知道该ELF header 有64个字节,所以,我们来只查看该elf文件的前64该字节的内容:
0000000 457f 464c 0102 0001 0000 0000 0000 0000
0000010 0002 003e 0001 0000 d310 0045 0000 0000
0000020 0040 0000 0000 0000 01c8 0000 0000 0000
0000030 0000 0000 0040 0038 0007 0040 0019 0003
0000040
这64个字节的数据通过前面的ELF header的定义能够得到readelf解析的内容,就是一个对应的解析,难度不大,这里我就抽几个变量进行解析:
- e_ident[EI_NIDENT] 16 个字节 内容为457f 464c 0102 0001 0000 0000 0000 0000
- e_entry 程序的入口点为:0x45d310
ELF Section
在ELF header中有e_shoff字段,我们根据该字段的信息能够找到section header table,这个文件是干什么的呢?ELF header 存储的整个ELF文件的头信息,那该字段自然就是存储的Section header的信息。每个 section 都会对应一个 section header,我们通过section header就能找到对应的section
Section Header 数据结构定义如下:
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;
根据对应的Section header 我们来解读对应的Section信息 readelf -S main1
There are 25 section headers, starting at offset 0x1c8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000401000 00001000
000000000009050c 0000000000000000 AX 0 0 16
[ 2] .rodata PROGBITS 0000000000492000 00092000
000000000004b5ca 0000000000000000 A 0 0 32
[ 3] .shstrtab STRTAB 0000000000000000 000dd5e0
00000000000001bc 0000000000000000 0 0 1
[ 4] .typelink PROGBITS 00000000004dd7a0 000dd7a0
0000000000000a9c 0000000000000000 A 0 0 32
[ 5] .itablink PROGBITS 00000000004de240 000de240
0000000000000050 0000000000000000 A 0 0 8
[ 6] .gosymtab PROGBITS 00000000004de290 000de290
0000000000000000 0000000000000000 A 0 0 1
[ 7] .gopclntab PROGBITS 00000000004de2a0 000de2a0
000000000006fa5c 0000000000000000 A 0 0 32
[ 8] .go.buildinfo PROGBITS 000000000054e000 0014e000
0000000000000020 0000000000000000 WA 0 0 16
[ 9] .noptrdata PROGBITS 000000000054e020 0014e020
000000000000e080 0000000000000000 WA 0 0 32
[10] .data PROGBITS 000000000055c0a0 0015c0a0
0000000000007110 0000000000000000 WA 0 0 32
[11] .bss NOBITS 00000000005631c0 001631c0
0000000000029950 0000000000000000 WA 0 0 32
[12] .noptrbss NOBITS 000000000058cb20 0018cb20
0000000000002828 0000000000000000 WA 0 0 32
[13] .zdebug_abbrev PROGBITS 0000000000590000 00164000
0000000000000119 0000000000000000 0 0 8
[14] .zdebug_line PROGBITS 0000000000590119 00164119
000000000001a8d7 0000000000000000 0 0 8
[15] .zdebug_frame PROGBITS 00000000005aa9f0 0017e9f0
0000000000006194 0000000000000000 0 0 8
[16] .zdebug_pubnames PROGBITS 00000000005b0b84 00184b84
0000000000001547 0000000000000000 0 0 8
[17] .zdebug_pubtypes PROGBITS 00000000005b20cb 001860cb
00000000000033ac 0000000000000000 0 0 8
[18] .debug_gdb_script PROGBITS 00000000005b5477 00189477
000000000000002a 0000000000000000 0 0 1
[19] .zdebug_info PROGBITS 00000000005b54a1 001894a1
000000000003068d 0000000000000000 0 0 8
[20] .zdebug_loc PROGBITS 00000000005e5b2e 001b9b2e
00000000000167d4 0000000000000000 0 0 8
[21] .zdebug_ranges PROGBITS 00000000005fc302 001d0302
0000000000008970 0000000000000000 0 0 8
[22] .note.go.buildid NOTE 0000000000400f9c 00000f9c
0000000000000064 0000000000000000 A 0 0 4
[23] .symtab SYMTAB 0000000000000000 001d9000
000000000000f630 0000000000000018 24 126 8
[24] .strtab STRTAB 0000000000000000 001e8630
000000000000f818 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),
l (large), p (processor specific)
以上信息将每个节的内容清晰地展示出来了,我们如果要知道每个section的具体内容,可以通过 readelf -x [index] main 就可以看到具体内容了。通过 objdump –d hello 能够看到反汇编代码。代码量过大,但是汇编代码其实很简单。截取一小段我们看看我最后的mian函数的汇编代码:
0000000000491410 <main.main>:
491410: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
491417: ff ff
491419: 48 3b 61 10 cmp 0x10(%rcx),%rsp
49141d: 0f 86 df 00 00 00 jbe 491502 <main.main+0xf2>
491423: 48 83 ec 68 sub $0x68,%rsp
491427: 48 89 6c 24 60 mov %rbp,0x60(%rsp)
49142c: 48 8d 6c 24 60 lea 0x60(%rsp),%rbp
491431: 48 c7 04 24 0c 00 00 movq $0xc,(%rsp)
491438: 00
491439: e8 62 7c f7 ff callq 4090a0 <runtime.convT64>
49143e: 48 8b 44 24 08 mov 0x8(%rsp),%rax
491443: 0f 57 c0 xorps %xmm0,%xmm0
491446: 0f 11 44 24 50 movups %xmm0,0x50(%rsp)
49144b: 48 8d 0d 6e df 00 00 lea 0xdf6e(%rip),%rcx # 49f3c0 <type.*+0xd3c0>
491452: 48 89 4c 24 50 mov %rcx,0x50(%rsp)
491457: 48 89 44 24 58 mov %rax,0x58(%rsp)
49145c: 48 8b 05 ad 1d 0d 00 mov 0xd1dad(%rip),%rax # 563210 <os.Stdout>
491463: 48 8d 15 36 a2 04 00 lea 0x4a236(%rip),%rdx # 4db6a0 <go.itab.*os.File,io.Writer>
49146a: 48 89 14 24 mov %rdx,(%rsp)
49146e: 48 89 44 24 08 mov %rax,0x8(%rsp)
491473: 48 8d 44 24 50 lea 0x50(%rsp),%rax
491478: 48 89 44 24 10 mov %rax,0x10(%rsp)
49147d: 48 c7 44 24 18 01 00 movq $0x1,0x18(%rsp)
491484: 00 00
491486: 48 c7 44 24 20 01 00 movq $0x1,0x20(%rsp)
49148d: 00 00
49148f: e8 6c 99 ff ff callq 48ae00 <fmt.Fprintln>
491494: 90 nop
491495: 48 c7 04 24 0d 00 00 movq $0xd,(%rsp)
49149c: 00
49149d: e8 fe 7b f7 ff callq 4090a0 <runtime.convT64>
4914a2: 48 8b 44 24 08 mov 0x8(%rsp),%rax
4914a7: 0f 57 c0 xorps %xmm0,%xmm0
4914aa: 0f 11 44 24 40 movups %xmm0,0x40(%rsp)
4914af: 48 8d 0d 0a df 00 00 lea 0xdf0a(%rip),%rcx # 49f3c0 <type.*+0xd3c0>
4914b6: 48 89 4c 24 40 mov %rcx,0x40(%rsp)
4914bb: 48 89 44 24 48 mov %rax,0x48(%rsp)
4914c0: 48 8b 05 49 1d 0d 00 mov 0xd1d49(%rip),%rax # 563210 <os.Stdout>
4914c7: 48 8d 0d d2 a1 04 00 lea 0x4a1d2(%rip),%rcx # 4db6a0 <go.itab.*os.File,io.Writer>
4914ce: 48 89 0c 24 mov %rcx,(%rsp)
4914d2: 48 89 44 24 08 mov %rax,0x8(%rsp)
4914d7: 48 8d 44 24 40 lea 0x40(%rsp),%rax
4914dc: 48 89 44 24 10 mov %rax,0x10(%rsp)
4914e1: 48 c7 44 24 18 01 00 movq $0x1,0x18(%rsp)
4914e8: 00 00
4914ea: 48 c7 44 24 20 01 00 movq $0x1,0x20(%rsp)
4914f1: 00 00
4914f3: e8 08 99 ff ff callq 48ae00 <fmt.Fprintln>
4914f8: 48 8b 6c 24 60 mov 0x60(%rsp),%rbp
4914fd: 48 83 c4 68 add $0x68,%rsp
491501: c3 retq
491502: e8 d9 85 fc ff callq 459ae0 <runtime.morestack_noctxt>
491507: e9 04 ff ff ff jmpq 491410 <main.main>
以上汇编代码无非就是执行了一些将寄存器的数据mov,cmp,add等操作,有一个很神奇的语法就是callq,通过该语法进行函数调用.汇编语法就暂时不做讲解了。我们已经知道了Section的代码规范了,当然具体的内容还需要和符号表对应上。内容过多,我们只用知道这些规范,需要的时候去查就可以了。这里就不进行详细解析了。
程序表
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;
readelf -l main 结果如下:
Elf file type is EXEC (Executable file)
Entry point 0x45d310
There are 7 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x0000000000000188 0x0000000000000188 R 0x1000
NOTE 0x0000000000000f9c 0x0000000000400f9c 0x0000000000400f9c
0x0000000000000064 0x0000000000000064 R 0x4
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000009150c 0x000000000009150c R E 0x1000
LOAD 0x0000000000092000 0x0000000000492000 0x0000000000492000
0x00000000000bbcfc 0x00000000000bbcfc R 0x1000
LOAD 0x000000000014e000 0x000000000054e000 0x000000000054e000
0x00000000000151c0 0x0000000000041348 RW 0x1000
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x8
LOOS+0x5041580 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x8
Section to Segment mapping:
Segment Sections...
00
01 .note.go.buildid
02 .text .note.go.buildid
03 .rodata .typelink .itablink .gosymtab .gopclntab
04 .go.buildinfo .noptrdata .data .bss .noptrbss
05
06
补充
- text section是可执行指令的集合,.data和.text都是属于PROGBITS类型的section,这是将来要运行的程序与代码。查询段表可知.text section的位偏移为0x0000320,size为0x0000192。
- strtab section是属于STRTAB类型的section,可以在文件中看到,它存着字符串,储存着符号的名字。位偏移为0x000106f,size为0x0000106
- symtab section存放所有section中定义的符号名字,比如“data_items”,“start_loop”。 .symtab section是属于SYMTAB类型的section,它描述了.strtab中的符号在“内存”中对应的“内存地址”。 位偏移为0x0001628,size为0x0000430。
- rodata section,ro代表read only。位偏移为0x000050c,size为0x00000b0。