0 前言
上一篇,我们知道了ELF文件是什么,以及怎么用,本篇重点介绍ELF的文档规范
1 整体结构
ELF适用于3种文件结构
- 重定向文件
也就是经常见到的.o
文件,用于保存代码和数据,链接器用使用这个文件来创建可执行文件
或者共享文件
- 可执行文件
也就是linux下面的.out
文件,用于执行 - 共享文件
常见的静态库.lib
或者动态库.so
,就是这个文件格式,就是由多个.o
压缩成的,可以用于保存代码或者数据,节省编译时间
上面三种文件,再综合一下,可以分为两种
- 链接文件(中间态)
- 可执行文件(最终态)
2 准备工作
在介绍各部分含义之前,我们需要先做一下准备工作,在树莓派上生成几个需要的文件
这里,我采用的是树莓派Raspbian系统自带的gcc编译器,CPU为ARM-A系列内核
.o
文件1
root@raspberrypi:/home/pi/tmp# gcc -c -O0 -g main.c root@raspberrypi:/home/pi/tmp# gcc -c -O0 -g fun.c
.so
文件1
root@raspberrypi:/home/pi/tmp# gcc -shared -fPIC -O0 -g -o libfun.so fun.c
.out
文件1
root@raspberrypi:/home/pi/tmp# gcc main.o fun.o
现在,你的目录下,应该有如下几个文件:1
root@raspberrypi:/home/pi/tmp# ls
a.out fun.c fun.o libfun.so main.c main.o
ok, 我们开始分析ELF结构
3 ELF header
它主要为系统提供基本信息,具体包括:
- 这是什么文件
- 大小多少
- 程序入口在哪里
- header表在哪里
- header表里面有多少项
- 等等
规范里面,提供了一个结构体定义,如下所示
我们来看看.o
,.so
,.out
三者的header分别是怎么样的,如下所示
fun.o
ELF header1
root@raspberrypi:/home/pi/tmp# readelf -h fun.o ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) # 0 注意文件类型 Machine: ARM Version: 0x1 Entry point address: 0x0 # 1 注意入口地址 Start of program headers: 0 (bytes into file) Start of section headers: 1220 (bytes into file) Flags: 0x5000000, Version5 EABI Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 20 Section header string table index: 17
libfun.so
ELF header1
root@raspberrypi:/home/pi/tmp# readelf -h libfun.so ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) # 0 注意文件类型 Machine: ARM Version: 0x1 Entry point address: 0x414 # 1 注意入口地址 Start of program headers: 52 (bytes into file) Start of section headers: 4680 (bytes into file) Flags: 0x5000402, has entry point, Version5 EABI, hard-float ABI Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 5 Size of section headers: 40 (bytes) Number of section headers: 32 Section header string table index: 29 root@raspberrypi:/home/pi/tmp#
a.out
ELF header1
root@raspberrypi:/home/pi/tmp# readelf -h a.out ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) # 0 注意文件类型 Machine: ARM Version: 0x1 Entry point address: 0x102f8 # 1 注意入口地址 Start of program headers: 52 (bytes into file) Start of section headers: 5808 (bytes into file) Flags: 0x5000402, has entry point, Version5 EABI, hard-float ABI Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 8 Size of section headers: 40 (bytes) Number of section headers: 36 Section header string table index: 33
我们可以看到,不同的文件,readelf工具读出来的文件类型
和入口地址
是不一样的,具体细节,请对照着ELF规范来逐个字节分析
哦,忘了告诉你,怎么通过看原始HEX信息了,请使用xxd
工具,如下所示1
root@raspberrypi:/home/pi/tmp# xxd a.out
0000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............
0000010: 0200 2800 0100 0000 f802 0100 3400 0000 ..(.........4...
0000020: b016 0000 0204 0005 3400 2000 0800 2800 ........4. ...(.
0000030: 2400 2100 0100 0070 1805 0000 1805 0100 $.!....p........
0000040: 1805 0100 0800 0000 0800 0000 0400 0000 ................
0000050: 0400 0000 0600 0000 3400 0000 3400 0100 ........4...4...
0000060: 3400 0100 0001 0000 0001 0000 0500 0000 4...............
0000070: 0400 0000 0300 0000 3401 0000 3401 0100 ........4...4...
0000080: 3401 0100 1900 0000 1900 0000 0400 0000 4...............
0000090: 0100 0000 0100 0000 0000 0000 0000 0100 ................
00000a0: 0000 0100 2405 0000 2405 0000 0500 0000 ....$...$.......
00000b0: 0000 0100 0100 0000 2405 0000 2405 0200 ........$...$...
...
对于一个可执行文件来说,系统最关心的是程序入口地址,是否需要动态库,数据区大小,代码区大小,size等信息;而对于一个重定向文件来说,最关心的是里面有哪些函数,哪些符号,对应的地址是多少,符号类型等信息;elf分别用下面的program header
和section header
来描述这些信息
4 Program header table
我们知道,嵌入式系统中,包含RAM和FLASH,无论我们的程序里面包含了多少个段,最终还是要映射到FLASH或者RAM中,链接脚本决定了如何映射各种段(.bss, .data .init 等),但对于加载器来说,根本不需要知道这些段的名字,加载器只需要知道段的地址和大小,以及将要映射的区域,就足够了。这也是ELF为什么要区分为两个视图的原因,链接器需要知道详细的段信息,就给它详细信息;加载器需要简单信息,就单独提供一个program header
给它,各取所需,分工明确。
我们先看看三个文件里面的program header
信息
a.out
program header1
root@raspberrypi:/home/pi/tmp# readelf -l a.out Elf file type is EXEC (Executable file) Entry point 0x102f8 There are 8 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align EXIDX 0x000518 0x00010518 0x00010518 0x00008 0x00008 R 0x4 PHDR 0x000034 0x00010034 0x00010034 0x00100 0x00100 R E 0x4 INTERP 0x000134 0x00010134 0x00010134 0x00019 0x00019 R 0x1 [Requesting program interpreter: /lib/ld-linux-armhf.so.3] LOAD 0x000000 0x00010000 0x00010000 0x00524 0x00524 R E 0x10000 LOAD 0x000524 0x00020524 0x00020524 0x0011c 0x00124 RW 0x10000 DYNAMIC 0x000530 0x00020530 0x00020530 0x000e8 0x000e8 RW 0x4 NOTE 0x000150 0x00010150 0x00010150 0x00044 0x00044 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 Section to Segment mapping: Segment Sections... 00 .ARM.exidx 01 02 .interp 03 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .ARM.exidx .eh_frame 04 .init_array .fini_array .jcr .dynamic .got .data .bss 05 .dynamic 06 .note.ABI-tag .note.gnu.build-id 07
fun.o
program header1
root@raspberrypi:/home/pi/tmp# readelf -l fun.o There are no program headers in this file.
libfun.so
program header1
root@raspberrypi:/home/pi/tmp# readelf -l libfun.so Elf file type is DYN (Shared object file) Entry point 0x414 There are 5 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x00000000 0x00000000 0x005c4 0x005c4 R E 0x10000 LOAD 0x0005c4 0x000105c4 0x000105c4 0x00118 0x00120 RW 0x10000 DYNAMIC 0x0005d0 0x000105d0 0x000105d0 0x000e0 0x000e0 RW 0x4 NOTE 0x0000d4 0x000000d4 0x000000d4 0x00024 0x00024 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 Section to Segment mapping: Segment Sections... 00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .eh_frame 01 .init_array .fini_array .jcr .dynamic .got .data .bss 02 .dynamic 03 .note.gnu.build-id 04
可以看到a.out
和libfun.so
都有program header信息,fun.o
没有program header信息,这是因为前两者中各个元素地址信息已经确定下来了,可以加载到系统中执行,但fun.o
仅仅作为中间产物,地址信息没有确定,无法加载到系统运行,所以对加载器来说,该文件完全没有有用信息,所以就不存在对应的program header。
需要注意的: Section to Segment mapping
下面的几个条目和Program Headers
几个条目分别对应,后面的信息,表示哪几个段分配到哪几个节中,以libfun.so
为例,如果整合到一起,则如下所示
1 | Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align Segment Sections LOAD 0x000000 0x00000000 0x00000000 0x005c4 0x005c4 R E 0x10000 00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .eh_frame LOAD 0x0005c4 0x000105c4 0x000105c4 0x00118 0x00120 RW 0x10000 01 .init_array .fini_array .jcr .dynamic .got .data .bss DYNAMIC 0x0005d0 0x000105d0 0x000105d0 0x000e0 0x000e0 RW 0x4 02 .dynamic NOTE 0x0000d4 0x000000d4 0x000000d4 0x00024 0x00024 R 0x4 03 .note.gnu.build-id GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 04 |
有了感性认识,我们来看看规范里面对program header的定义
从上文可以知道,ELF header里面包含了program header的偏移地址和size信息1
Start of program headers: 52 (bytes into file)
Size of program headers: 32 (bytes)
Number of program headers: 8
表示program header在文件的52字节偏移处,然后总共8个元素,每个元素32字节大小
元素的定义如下所示
结构体所描述的信息,可以通过readelf来读出来,上面已经展示过了,是一一对应的
其中,最重要的是LOAD类型的节,这部分是用来加载到内存中,我们可以将这部分复制出来,从而导出bin文件
5 Section header table
则是从链接器的视角来管理内容,我们先看看a.out
,fun.o
,libfun.so
这三个的section header信息,从而有一个感性的认识
a.out
section header1
root@raspberrypi:/home/pi/tmp# readelf -S a.out There are 36 section headers, starting at offset 0x16b0: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 00010134 000134 000019 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 00010150 000150 000020 00 A 0 0 4 [ 3] .note.gnu.build-i NOTE 00010170 000170 000024 00 A 0 0 4 [ 4] .gnu.hash GNU_HASH 00010194 000194 00002c 04 A 5 0 4 [ 5] .dynsym DYNSYM 000101c0 0001c0 000050 10 A 6 1 4 [ 6] .dynstr STRTAB 00010210 000210 000043 00 A 0 0 1 [ 7] .gnu.version VERSYM 00010254 000254 00000a 02 A 5 0 2 [ 8] .gnu.version_r VERNEED 00010260 000260 000020 00 A 6 1 4 [ 9] .rel.dyn REL 00010280 000280 000008 08 A 5 0 4 [10] .rel.plt REL 00010288 000288 000020 08 AI 5 12 4 [11] .init PROGBITS 000102a8 0002a8 00000c 00 AX 0 0 4 [12] .plt PROGBITS 000102b4 0002b4 000044 04 AX 0 0 4 [13] .text PROGBITS 000102f8 0002f8 000208 00 AX 0 0 4 [14] .fini PROGBITS 00010500 000500 000008 00 AX 0 0 4 [15] .rodata PROGBITS 00010508 000508 000010 00 A 0 0 4 [16] .ARM.exidx ARM_EXIDX 00010518 000518 000008 00 AL 13 0 4 [17] .eh_frame PROGBITS 00010520 000520 000004 00 A 0 0 4 [18] .init_array INIT_ARRAY 00020524 000524 000004 00 WA 0 0 4 [19] .fini_array FINI_ARRAY 00020528 000528 000004 00 WA 0 0 4 [20] .jcr PROGBITS 0002052c 00052c 000004 00 WA 0 0 4 [21] .dynamic DYNAMIC 00020530 000530 0000e8 08 WA 6 0 4 [22] .got PROGBITS 00020618 000618 000020 04 WA 0 0 4 [23] .data PROGBITS 00020638 000638 000008 00 WA 0 0 4 [24] .bss NOBITS 00020640 000640 000008 00 WA 0 0 4 [25] .comment PROGBITS 00000000 000640 00003d 01 MS 0 0 1 [26] .ARM.attributes ARM_ATTRIBUTES 00000000 00067d 000031 00 0 0 1 [27] .debug_aranges PROGBITS 00000000 0006ae 000040 00 0 0 1 [28] .debug_info PROGBITS 00000000 0006ee 000106 00 0 0 1 [29] .debug_abbrev PROGBITS 00000000 0007f4 0000b0 00 0 0 1 [30] .debug_line PROGBITS 00000000 0008a4 00006d 00 0 0 1 [31] .debug_frame PROGBITS 00000000 000914 00005c 00 0 0 4 [32] .debug_str PROGBITS 00000000 000970 0000cb 01 MS 0 0 1 [33] .shstrtab STRTAB 00000000 000a3b 000157 00 0 0 1 [34] .symtab SYMTAB 00000000 000b94 0007a0 10 35 95 4 [35] .strtab STRTAB 00000000 001334 00037a 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
fun.o
section header1
root@raspberrypi:/home/pi/tmp# readelf -S fun.o There are 20 section headers, starting at offset 0x4c4: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 000030 00 AX 0 0 4 [ 2] .data PROGBITS 00000000 000064 000000 00 WA 0 0 1 [ 3] .bss NOBITS 00000000 000064 000000 00 WA 0 0 1 [ 4] .debug_info PROGBITS 00000000 000064 00006e 00 0 0 1 [ 5] .rel.debug_info REL 00000000 000454 000048 08 I 18 4 4 [ 6] .debug_abbrev PROGBITS 00000000 0000d2 00005b 00 0 0 1 [ 7] .debug_aranges PROGBITS 00000000 00012d 000020 00 0 0 1 [ 8] .rel.debug_arange REL 00000000 00049c 000010 08 I 18 7 4 [ 9] .debug_line PROGBITS 00000000 00014d 000035 00 0 0 1 [10] .rel.debug_line REL 00000000 0004ac 000008 08 I 18 9 4 [11] .debug_str PROGBITS 00000000 000182 000066 01 MS 0 0 1 [12] .comment PROGBITS 00000000 0001e8 000020 01 MS 0 0 1 [13] .note.GNU-stack PROGBITS 00000000 000208 000000 00 0 0 1 [14] .debug_frame PROGBITS 00000000 000208 000030 00 0 0 4 [15] .rel.debug_frame REL 00000000 0004b4 000010 08 I 18 14 4 [16] .ARM.attributes ARM_ATTRIBUTES 00000000 000238 000031 00 0 0 1 [17] .shstrtab STRTAB 00000000 000269 0000b2 00 0 0 1 [18] .symtab SYMTAB 00000000 00031c 000120 10 19 16 4 [19] .strtab STRTAB 00000000 00043c 000015 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
像其它的header table一样,section
libfun.so
section header1
root@raspberrypi:/home/pi/tmp# readelf -S libfun.so There are 32 section headers, starting at offset 0x1248: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .note.gnu.build-i NOTE 000000d4 0000d4 000024 00 A 0 0 4 [ 2] .gnu.hash GNU_HASH 000000f8 0000f8 000050 04 A 3 0 4 [ 3] .dynsym DYNSYM 00000148 000148 000130 10 A 4 3 4 [ 4] .dynstr STRTAB 00000278 000278 0000cb 00 A 0 0 1 [ 5] .gnu.version VERSYM 00000344 000344 000026 02 A 3 0 2 [ 6] .gnu.version_r VERNEED 0000036c 00036c 000020 00 A 4 1 4 [ 7] .rel.dyn REL 0000038c 00038c 000040 08 A 3 0 4 [ 8] .rel.plt REL 000003cc 0003cc 000010 08 AI 3 10 4 [ 9] .init PROGBITS 000003dc 0003dc 00000c 00 AX 0 0 4 [10] .plt PROGBITS 000003e8 0003e8 00002c 04 AX 0 0 4 [11] .text PROGBITS 00000414 000414 0001a4 00 AX 0 0 4 [12] .fini PROGBITS 000005b8 0005b8 000008 00 AX 0 0 4 [13] .eh_frame PROGBITS 000005c0 0005c0 000004 00 A 0 0 4 [14] .init_array INIT_ARRAY 000105c4 0005c4 000004 00 WA 0 0 4 [15] .fini_array FINI_ARRAY 000105c8 0005c8 000004 00 WA 0 0 4 [16] .jcr PROGBITS 000105cc 0005cc 000004 00 WA 0 0 4 [17] .dynamic DYNAMIC 000105d0 0005d0 0000e0 08 WA 4 0 4 [18] .got PROGBITS 000106b0 0006b0 000028 04 WA 0 0 4 [19] .data PROGBITS 000106d8 0006d8 000004 00 WA 0 0 4 [20] .bss NOBITS 000106dc 0006dc 000008 00 WA 0 0 4 [21] .comment PROGBITS 00000000 0006dc 00001f 01 MS 0 0 1 [22] .ARM.attributes ARM_ATTRIBUTES 00000000 0006fb 00002f 00 0 0 1 [23] .debug_aranges PROGBITS 00000000 00072a 000020 00 0 0 1 [24] .debug_info PROGBITS 00000000 00074a 00006e 00 0 0 1 [25] .debug_abbrev PROGBITS 00000000 0007b8 00005b 00 0 0 1 [26] .debug_line PROGBITS 00000000 000813 000035 00 0 0 1 [27] .debug_frame PROGBITS 00000000 000848 000030 00 0 0 4 [28] .debug_str PROGBITS 00000000 000878 00006c 01 MS 0 0 1 [29] .shstrtab STRTAB 00000000 0008e4 00012e 00 0 0 1 [30] .symtab SYMTAB 00000000 000a14 0005b0 10 31 75 4 [31] .strtab STRTAB 00000000 000fc4 000281 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
像其它的header表一样,section header也是用基本数组元素来表示的,数组的结构体如下所示
其中,需要注意,为了便于统一管理和数组元素大小一致,section的sh_name成员,用字符串表里面的索引来表示
这也是ELF header里面单独列出字符串表索引的重要原因
6 总结
我们总结一下,ELF有两种视图,分别为链接视图和加载视图
分析ELF的过程如下所示
- 链接视图
- 找到ELF Header
- 在ELF Header中找到Section Header入口地址和元素大小,数量
- 找到字符串表的索引和入口地址
- 分析Section Header,找到各个表项的入口地址和基本信息
- 加载视图
- 找到ELF Header
- 在ELF Header中找到Program Header入口地址和元素大小,数量
- 找到字符串表的索引和入口地址
- 对于动态链接来说,先调用系统加载器,加载所需要的so文件;对于静态来说,构建基本运行环境
- 加载LOAD节到系统内存
- 加载调试信息给debuger
- 开始运行或者调试