-
2022-01-17 15:54:30
文章目录
ELF文件解析
ELF(Executable and Linkable Format)是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的文件格式。
本文我们来解析一些ELF文件的具体格式信息。
1. 简介
1.1 分类
ELF文件有四种类型:
-
重定位文件(
ET_REL
),也就是常称的目标文件,包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。 -
可执行文件(
ET_EXEC
),包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。 -
共享目标文件(
ET_DYN
),即共享对象文件、动态库文件, 包含可在两种上下文中链接的代码和数据。- 首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理, 生成另外一个目标文件。
- 其次动态链接器可能将它与某 个可执行文件以及其它共享目标一起组合,创建进程映像。
-
核心转储文件(
ET_CORE
),包括程序运行的内存数据和代码。
除此之外还会有类型不确定的ELF文件(
ET_NONE
),即未知文件。1.2 作用
ELF文件参与了二进制文件的两个过程:
- 链接。
- 执行。
所以可以从不同的角度来看待ELF格式的文件:
-
如果用于编译和链接(可重定位文件),则编译器和链接器将把ELF文件看作是节头表描述的节的集合(Section),程序头表可选。
-
如果用于加载执行(可执行文件),则加载器则将把ELF文件看作是程序头表描述的段的集合(Segment),一个段可能包含多个节,节头表可选。
-
如果是共享文件,则两者都含有。
所以ELF文件的大致结构可以视为如下:
链接视图 执行视图 ELF文件头部 ELF文件头部 程序头部表(可选) 程序头部表 Section 1 Segment 1 Section ... Section N Segment 2 Section ... Section ... Segment ... 节区头部表 节区头部表(可选) 对于程序头部表和节区头部表来说:
- 程序头部表(Program Header Table) : 如果存在的话,告诉系统如何创建进程映像。
- 节区头部表(Section Header Table) :如果存在的话,包含了描述文件节区的信息,比如大小、偏移等。
如下我们可以看到Section和Segment的对应关系(Section to Segment mapping):
$ readelf -l hello Elf file type is DYN (Shared object 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 0x00000000000005f8 0x00000000000005f8 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x00000000000001f5 0x00000000000001f5 R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000160 0x0000000000000160 R 0x1000 LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000258 0x0000000000000260 RW 0x1000 DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8 0x00000000000001f0 0x00000000000001f0 RW 0x8 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 0x0000000000000044 0x0000000000000044 R 0x4 GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x0000000000002014 0x0000000000000044 0x0000000000000044 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
2. 格式分析
2.1 源码
我们来准备一个最简单的源码:
#include <stdio.h> int main(int argc, char* argv[]) { printf("hello world!\n"); return 0; }
分别将其编译成为三种不同的文件:
gcc -g -c hello.c
: 中间文件,生成hello.o。gcc -g -o hello hello.
: 编译成可执行文件。gcc -g -fPIC -o libhello.so -shared hello.c
: 编译成共享库。
2.2 ELF头部
ELF文件头部定义如下:
#define EI_NIDENT 16 #define SHN_UNDEF 0 typedef struct { unsigned char e_ident[EI_NIDENT]; uint16_t e_type; //文件标识和类型信息(包括魔术) uint16_t e_machine; //适用的处理器体系结构 uint32_t e_version; //目标文件的版本,EV_CURRENT ElfN_Addr e_entry; //程序入口地址 ElfN_Off e_phoff; //程序头表(program header table)开始处在文件中的偏移量 ElfN_Off e_shoff; //节头表(section header table)开始处在文件中的偏移量 uint32_t e_flags; //处理器特定的标志位 uint16_t e_ehsize; // ELF 文件头的大小,以字节为单位 uint16_t e_phentsize; //程序头表中每一个表项的大小,以字节为单位 uint16_t e_phnum; //程序头表中总共有多少个表项 uint16_t e_shentsize; //节头表中每一个表项的大小,以字节为单位 uint16_t e_shnum; //节头表中总共有多少个表项 uint16_t e_shstrndx; //节头表中与节名字表相对应的表项的索引。如果文件没有节名字表,此值应设置为 SHN_UNDEF。 } ElfN_Ehdr;
对于这个结构体的每个字段,我们可用在man(5)文档中找到详细信息,这里不再详细介绍,使用
readelf -h
可用查看ELF的文件头,如下:$ readelf -h hello 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 (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x1060 Start of program headers: 64 (bytes into file) Start of section headers: 16928 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 13 Size of section headers: 64 (bytes) Number of section headers: 36 Section header string table index: 35
我们使用hexdump查看原始文件内容信息如下:
$ hexdump -C -n 512 hello 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 03 00 3e 00 01 00 00 00 60 10 00 00 00 00 00 00 |..>.....`.......| 00000020 40 00 00 00 00 00 00 00 20 42 00 00 00 00 00 00 |@....... B......| 00000030 00 00 00 00 40 00 38 00 0d 00 40 00 24 00 23 00 |....@.8...@.$.#.| 00000040 06 00 00 00 04 00 00 00 40 00 00 00 00 00 00 00 |........@.......| 00000050 40 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 |@.......@.......| 00000060 d8 02 00 00 00 00 00 00 d8 02 00 00 00 00 00 00 |................| 00000070 08 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 |................| 00000080 18 03 00 00 00 00 00 00 18 03 00 00 00 00 00 00 |................| 00000090 18 03 00 00 00 00 00 00 1c 00 00 00 00 00 00 00 |................| 000000a0 1c 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 |................|
2.3 Section
ELF文件中的节是从编译器链接角度来看文件的组成的。从链接器的角度上来看,包括指令、数据、符号以及重定位表等等。
在ELF 文件头中,有很多字段是描述Section头的:
e_shoff
成员给出节头表在ELF文件中的位置,即相对于文件开始处的偏移量。e_shnum
成员指明节头表中包含多少个表项。e_shentsize
成员指明了每一个表项的大小。
某些表项的索引值被保留,有特殊的含义。 ELF 文件的节头表中不会出现索引值为以下各值的表项:
//SHN 大致是section indexes的简写 /* special section indexes */ #define SHN_UNDEF 0 //一个未定义的、不存在的节的索引 #define SHN_LORESERVE 0xff00 //被保留索引号区间的下限 #define SHN_LOPROC 0xff00 //处理器定制节所保留的索引号区间的下限 #define SHN_HIPROC 0xff1f //处理器定制节所保留的索引号区间的上限 #define SHN_LIVEPATCH 0xff20 #define SHN_ABS 0xfff1 //此节中所定义的符号有绝对的值,这个值不会因重定位而改变 #define SHN_COMMON 0xfff2 //此节中所定义的符号是公共的 #define SHN_HIRESERVE 0xffff //被保留索引号区间的上限
在程序的编译链接过程中,编译器将一个一个.o文件链接成一个可以执行的ELF文件的过程中,同时也生成了一个表。这个表记录了各个Section所处的区域。在程序中,程序的section header有多个项,但是大小是一样,结构定义如下:
typedef struct { uint32_t sh_name; //名称,字符串表的索引(偏移值 .shstrtab) uint32_t sh_type; //节类型, SHT_XXX uint64_t sh_flags; //节的读写执行属性 Elf64_Addr sh_addr; //映射地址,如果本节的内容需要映射到进程空间中去,此成员指定映射的起始地址;如果不需要映射,此值为 0。 Elf64_Off sh_offset; //偏移 uint64_t sh_size; //大小 uint32_t sh_link; //此成员是一个索引值,指向节头表中本节所对应的位置 uint32_t sh_info; //此成员含有此节的附加信息 uint64_t sh_addralign; //对齐字节 uint64_t sh_entsize; //有一些节的内容是一张表,其中每一个表项的大小是固定的,比如符号表。对于这种表来说,本成员指定其每一个表项的大小。 } Elf64_Shdr;
在这个结构体里面,
sh_type
和sh_flags
代表节的类型和属性,有如下定义/* sh_type */ #define SHT_NULL 0 //一个无效的节头,它也没有对应的节 #define SHT_PROGBITS 1 //此值表明本节所含有的信息是由程序定义的 #define SHT_SYMTAB 2 //完整符号表 #define SHT_STRTAB 3 //字符串表 #define SHT_RELA 4 //重定位节, 含有带明确加数(addend)的重定位项(一般来说jmp指令需要跳过指令长度) #define SHT_HASH 5 //哈希表,所有参与动态连接的目标文件都必须要包含一个符号哈希表。 #define SHT_DYNAMIC 6 //动态连接信息 #define SHT_NOTE 7 //表明本节包含的信息用于以某种方式来标记本文件 #define SHT_NOBITS 8 //此值表明这一节的内容是空的,节并不占用实际的空间 #define SHT_REL 9 //无附加的重定位项 #define SHT_SHLIB 10 //保留值 #define SHT_DYNSYM 11 //动态链接符号表 #define SHT_NUM 12 #define SHT_LOPROC 0x70000000 //为特殊处理器保留的节类型索引值的下边界 #define SHT_HIPROC 0x7fffffff //为特殊处理器保留的节类型索引值的上边界 #define SHT_LOUSER 0x80000000 //为应用程序保留节类型索引值的下边界 #define SHT_HIUSER 0xffffffff //为应用程序保留节类型索引值的下边界 /* sh_flags */ #define SHF_WRITE 0x1 //本节所包含的内容在进程运行过程中是可写的 #define SHF_ALLOC 0x2 //表示本节内容在进程运行过程中要占用内存单元(并不是所有节都会占用实际的内存,有一些起控制作用的节,在目标文件映射到进程空间时,并不需要占用内存) #define SHF_EXECINSTR 0x4 //表示此节内容是指令代码
此外对于Section header有两个比较特殊的成员:
- sh_link : 此成员是一个索引值,指向节头表中本节所对应的位置。根据节的类型不同,本成员的意义也有所不同。
- sh_info : 此成员含有此节的附加信息,根据节的类型不同,本成员的意义也有所不同。
对于某些节类型来说,
sh_link
和sh_info
含有特殊的信息,见下表。sh_type sh_link sh_info SHT_DYNAMIC 用于本节中项目的字符串表在节头表中相应的索引值 0 SHT_HASH 用于本节中哈希表的符号表在节头表中相应的索引值 0 SHT_REL /SHT_RELA 相应符号表在节头表中的索引值 本重定位节所应用到目标节在节头表中的索引值 SHT_SYMTAB / SHT_DYNSYM 相关字符串表的节头索引 符号表中最后一个本地符号的索引值加 1 其它 SHN_UNDEF 0 Section 通过偏移和大小来确定具体信息,同样使用
readelf -t
我们可用查看所有section 头部数组的结构,如下:$ readelf -t hello There are 36 section headers, starting at offset 0x4220: Section Headers: [Nr] Name Type Address Offset Link Size EntSize Info Align Flags [ 0] NULL 0000000000000000 0000000000000000 0 0000000000000000 0000000000000000 0 0 [0000000000000000]: [ 1] .interp PROGBITS 0000000000000318 0000000000000318 0 000000000000001c 0000000000000000 0 1 [0000000000000002]: ALLOC [ 2] .note.gnu.property NOTE 0000000000000338 0000000000000338 0 0000000000000020 0000000000000000 0 8 [0000000000000002]: ALLOC
同样我们知道在Section header在文件中的偏移(ELF文件头部有结构),可用查看数据如下:
$ hexdump -C -n 512 -s 0x4220 hello 00004220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00004260 1b 00 00 00 01 00 00 00 02 00 00 00 00 00 00 00 |................| 00004270 18 03 00 00 00 00 00 00 18 03 00 00 00 00 00 00 |................| 00004280 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00004290 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000042a0 23 00 00 00 07 00 00 00 02 00 00 00 00 00 00 00 |#...............| 000042b0 38 03 00 00 00 00 00 00 38 03 00 00 00 00 00 00 |8.......8.......| 000042c0 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ...............| 000042d0 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000042e0 36 00 00 00 07 00 00 00 02 00 00 00 00 00 00 00 |6...............| 000042f0 58 03 00 00 00 00 00 00 58 03 00 00 00 00 00 00 |X.......X.......| 00004300 24 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |$...............| 00004310 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00004320 49 00 00 00 07 00 00 00 02 00 00 00 00 00 00 00 |I...............| 00004330 7c 03 00 00 00 00 00 00 7c 03 00 00 00 00 00 00 ||.......|.......| 00004340 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ...............| 00004350 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00004360 57 00 00 00 f6 ff ff 6f 02 00 00 00 00 00 00 00 |W......o........| 00004370 a0 03 00 00 00 00 00 00 a0 03 00 00 00 00 00 00 |................| 00004380 24 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 |$...............| 00004390 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000043a0 61 00 00 00 0b 00 00 00 02 00 00 00 00 00 00 00 |a...............| 000043b0 c8 03 00 00 00 00 00 00 c8 03 00 00 00 00 00 00 |................| 000043c0 a8 00 00 00 00 00 00 00 07 00 00 00 01 00 00 00 |................| 000043d0 08 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 |................| 000043e0 69 00 00 00 03 00 00 00 02 00 00 00 00 00 00 00 |i...............| 000043f0 70 04 00 00 00 00 00 00 70 04 00 00 00 00 00 00 |p.......p.......| 00004400 82 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
因为
Elf64_Shdr
的大小为0x40,所以我们可用看一下几个Section:00004260 1b 00 00 00
.000042a0 23 00 00 00
.000042e0 36 00 00 00
.00004320 49 00 00 00
.
这些值都是对应的
uint32_t sh_name
成员在文件中的值。因为对于section的名称,有专门的section 来记录,索引为uint16_t e_shstrndx;
,因此我们可用找到这个Section的具体内容如下:$ hexdump -C -n 512 -s 0x40c3 hello 000040c3 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 |..symtab..strtab| 000040d3 00 2e 73 68 73 74 72 74 61 62 00 2e 69 6e 74 65 |..shstrtab..inte| 000040e3 72 70 00 2e 6e 6f 74 65 2e 67 6e 75 2e 70 72 6f |rp..note.gnu.pro| 000040f3 70 65 72 74 79 00 2e 6e 6f 74 65 2e 67 6e 75 2e |perty..note.gnu.| 00004103 62 75 69 6c 64 2d 69 64 00 2e 6e 6f 74 65 2e 41 |build-id..note.A| 00004113 42 49 2d 74 61 67 00 2e 67 6e 75 2e 68 61 73 68 |BI-tag..gnu.hash| 00004123 00 2e 64 79 6e 73 79 6d 00 2e 64 79 6e 73 74 72 |..dynsym..dynstr| 00004133 00 2e 67 6e 75 2e 76 65 72 73 69 6f 6e 00 2e 67 |..gnu.version..g| 00004143 6e 75 2e 76 65 72 73 69 6f 6e 5f 72 00 2e 72 65 |nu.version_r..re| 00004153 6c 61 2e 64 79 6e 00 2e 72 65 6c 61 2e 70 6c 74 |la.dyn..rela.plt| 00004163 00 2e 69 6e 69 74 00 2e 70 6c 74 2e 67 6f 74 00 |..init..plt.got.| 00004173 2e 70 6c 74 2e 73 65 63 00 2e 74 65 78 74 00 2e |.plt.sec..text..| 00004183 66 69 6e 69 00 2e 72 6f 64 61 74 61 00 2e 65 68 |fini..rodata..eh| 00004193 5f 66 72 61 6d 65 5f 68 64 72 00 2e 65 68 5f 66 |_frame_hdr..eh_f| 000041a3 72 61 6d 65 00 2e 69 6e 69 74 5f 61 72 72 61 79 |rame..init_array| 000041b3 00 2e 66 69 6e 69 5f 61 72 72 61 79 00 2e 64 79 |..fini_array..dy| 000041c3 6e 61 6d 69 63 00 2e 64 61 74 61 00 2e 62 73 73 |namic..data..bss| 000041d3 00 2e 63 6f 6d 6d 65 6e 74 00 2e 64 65 62 75 67 |..comment..debug| 000041e3 5f 61 72 61 6e 67 65 73 00 2e 64 65 62 75 67 5f |_aranges..debug_| 000041f3 69 6e 66 6f 00 2e 64 65 62 75 67 5f 61 62 62 72 |info..debug_abbr| 00004203 65 76 00 2e 64 65 62 75 67 5f 6c 69 6e 65 00 2e |ev..debug_line..| 00004213 64 65 62 75 67 5f 73 74 72 00 00 00 00 00 00 00 |debug_str.......| 00004223 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00004260 1b 00 00 00
: 1b偏移处的名字为:.interp000042a0 23 00 00 00
: 23偏移处的名字为:.note.gnu.property000042e0 36 00 00 00
: 23偏移处的名字为:.note.gnu.build-id00004320 49 00 00 00
: 49偏移处的名字为:.note.ABI-tag
和Section 头部完全匹配,如下:
$ readelf -t hello There are 36 section headers, starting at offset 0x4220: Section Headers: [Nr] Name Type Address Offset Link Size EntSize Info Align Flags [ 0] NULL 0000000000000000 0000000000000000 0 0000000000000000 0000000000000000 0 0 [0000000000000000]: [ 1] .interp PROGBITS 0000000000000318 0000000000000318 0 000000000000001c 0000000000000000 0 1 [0000000000000002]: ALLOC [ 2] .note.gnu.property NOTE 0000000000000338 0000000000000338 0 0000000000000020 0000000000000000 0 8 [0000000000000002]: ALLOC [ 3] .note.gnu.build-id NOTE 0000000000000358 0000000000000358 0 0000000000000024 0000000000000000 0 4 [0000000000000002]: ALLOC [ 4] .note.ABI-tag NOTE 000000000000037c 000000000000037c 0 0000000000000020 0000000000000000 0 4 [0000000000000002]: ALLOC
因此这里我们也知道
uint32_t sh_name
是字符串表中的偏移值。2.4 字符串表
字符串表是一个包含了若干以NULL结尾的字符序列,即字符串。在目标文件中这些字符串通常是符号的名字或者节的名字。在目标文件的其它部分中,当需要引用某个字符串时,只需要提供该字符串在字符串表中的序号即可。
字符串表中的第一个字符串(序号为 NULL)永远是空串,即NULL,它可以用于表示一个空的名字或者没有名字。所以,字符串表的第一个字节是NULL。由于每一个字符串都是以NULL结尾,所以字符串表的最后一个字节也必然为NULL。
字符串表也可以是空的,不含有任何字符串,这时,节头中的
sh_size
成员必须是 0。一个目标文件中可能有多个字符串表,例如:
.shstrtab
: Section的名字表。.strtab
: 普通字符串表。.dynstr
: 动态链接的字符串表。
我们看一个
.shstrtab
字符串表的实例:$ hexdump -C -n 512 -s 0x40c3 hello 000040c3 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 |..symtab..strtab| 000040d3 00 2e 73 68 73 74 72 74 61 62 00 2e 69 6e 74 65 |..shstrtab..inte| 000040e3 72 70 00 2e 6e 6f 74 65 2e 67 6e 75 2e 70 72 6f |rp..note.gnu.pro| 000040f3 70 65 72 74 79 00 2e 6e 6f 74 65 2e 67 6e 75 2e |perty..note.gnu.| 00004103 62 75 69 6c 64 2d 69 64 00 2e 6e 6f 74 65 2e 41 |build-id..note.A| 00004113 42 49 2d 74 61 67 00 2e 67 6e 75 2e 68 61 73 68 |BI-tag..gnu.hash| 00004123 00 2e 64 79 6e 73 79 6d 00 2e 64 79 6e 73 74 72 |..dynsym..dynstr|
这个字符串表大致可用表示如下:
序号(sh_name) 字符串 0x0 NULL 0x1 .symtab 0x9 .strtab 0x11 .shstrtab 0x1b .interp 这里也可用看到序号(sh_name)其实就是字符串表中的偏移值。
我们看一下一个字符串表的DUMP信息,如下:
$ readelf -t hello There are 36 section headers, starting at offset 0x4220: Section Headers: [Nr] Name Type Address Offset Link Size EntSize Info Align Flags [ 0] NULL 0000000000000000 0000000000000000 0 0000000000000000 0000000000000000 0 0 [0000000000000000]: [ ...] [34] .strtab STRTAB 0000000000000000 0000000000003ec0 0 0000000000000203 0000000000000000 0 1 [0000000000000000]: [35] .shstrtab STRTAB 0000000000000000 00000000000040c3 0 000000000000015a 0000000000000000 0 1 [0000000000000000]:
这里有两个字符串表:
.strtab
: 偏移为0x3ec0..shstrtab
: 偏移为0x40c3.
dump的文件内容如下:
$ hexdump -C -n 512 -s 0x3ec0 hello 00003ec0 00 63 72 74 73 74 75 66 66 2e 63 00 64 65 72 65 |.crtstuff.c.dere| 00003ed0 67 69 73 74 65 72 5f 74 6d 5f 63 6c 6f 6e 65 73 |gister_tm_clones| 00003ee0 00 5f 5f 64 6f 5f 67 6c 6f 62 61 6c 5f 64 74 6f |.__do_global_dto| 00003ef0 72 73 5f 61 75 78 00 63 6f 6d 70 6c 65 74 65 64 |rs_aux.completed| 00003f00 2e 37 39 37 30 00 5f 5f 64 6f 5f 67 6c 6f 62 61 |.7970.__do_globa| 00003f10 6c 5f 64 74 6f 72 73 5f 61 75 78 5f 66 69 6e 69 |l_dtors_aux_fini| 00003f20 5f 61 72 72 61 79 5f 65 6e 74 72 79 00 66 72 61 |_array_entry.fra| 00003f30 6d 65 5f 64 75 6d 6d 79 00 5f 5f 66 72 61 6d 65 |me_dummy.__frame| 00003f40 5f 64 75 6d 6d 79 5f 69 6e 69 74 5f 61 72 72 61 |_dummy_init_arra| 00003f50 79 5f 65 6e 74 72 79 00 68 65 6c 6c 6f 2e 63 00 |y_entry.hello.c.| 00003f60 5f 5f 46 52 41 4d 45 5f 45 4e 44 5f 5f 00 5f 5f |__FRAME_END__.__| $ hexdump -C -n 512 -s 0x40c3 hello 000040c3 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 |..symtab..strtab| 000040d3 00 2e 73 68 73 74 72 74 61 62 00 2e 69 6e 74 65 |..shstrtab..inte| 000040e3 72 70 00 2e 6e 6f 74 65 2e 67 6e 75 2e 70 72 6f |rp..note.gnu.pro| 000040f3 70 65 72 74 79 00 2e 6e 6f 74 65 2e 67 6e 75 2e |perty..note.gnu.| 00004103 62 75 69 6c 64 2d 69 64 00 2e 6e 6f 74 65 2e 41 |build-id..note.A| 00004113 42 49 2d 74 61 67 00 2e 67 6e 75 2e 68 61 73 68 |BI-tag..gnu.hash| 00004123 00 2e 64 79 6e 73 79 6d 00 2e 64 79 6e 73 74 72 |..dynsym..dynstr| 00004133 00 2e 67 6e 75 2e 76 65 72 73 69 6f 6e 00 2e 67 |..gnu.version..g| 00004143 6e 75 2e 76 65 72 73 69 6f 6e 5f 72 00 2e 72 65 |nu.version_r..re| 00004153 6c 61 2e 64 79 6e 00 2e 72 65 6c 61 2e 70 6c 74 |la.dyn..rela.plt| 00004163 00 2e 69 6e 69 74 00 2e 70 6c 74 2e 67 6f 74 00 |..init..plt.got.|
2.5 符号表
目标文件中的符号表(symbol table)所包含的信息用于定位和重定位程序中的符号定义和引用。目标文件的其它部分通过一个符号在这个表中的索引值来使用该符号。索引值从 0 开始计数,但值为 0 的表项(即第一项)并没有实际的意义,它表示未定义的符号。这里用常量 STN_UNDEF 来表示未定义的符号。
一般来说,符号表包括两个部分:
- .dynsym : 使用的动态库的符号。
- .symtab : 可执行文件的所有符号,当然也包括.dynsym部分的符号。
符号表项的定义格式如下:
typedef struct { uint32_t st_name; //字符串表的索引(偏移),字符串表有节头部指定下标 Elf32_Addr st_value; //符号的值或者地址(重定位文件中是偏移) uint32_t st_size; //各种符号的大小各不相同,比如一个对象的大小就是它实际占用的字节数。如果一个符号的大小为 0 或者大小未知,则这个值为 0。 unsigned char st_info; //符号的类型和属性 unsigned char st_other; //本数据成员目前暂未使用,在目标文件中一律赋值为 0 uint16_t st_shndx; //相关联的节(符号在那个节中的索引) } Elf32_Sym; typedef struct { uint32_t st_name; unsigned char st_info; unsigned char st_other; uint16_t st_shndx; Elf64_Addr st_value; uint64_t st_size; } Elf64_Sym;
对于
st_value
的值,没有固定的类型,它可能代表一个数值,也可以是一个地址,具体是什么要看上下文。对于不同的目标文件类型,符号表项的 st_value 的含义略有不同:- 在重定位文件中,如果一个符号对应的节的索引值是SHN_COMMON, st_value值是这个节内容的字节对齐数。
- 在重定位文件中,如果一个符号是已定义的,那么它的st_value值是该符号的起始地址在其所在节中的偏移量,而其所在的节的索引由st_shndx给出。
- 在可执行文件和共享库文件中, st_value不再是一个节内的偏移量,而是一个虚拟地址,直接指向符号所在的内存位置。这种情况下, st_shndx就不再需要了。
对于符号成员
st_info
比较重要,由一系列的比特位构成,标识了“符号绑定(symbol binding)”、“符号类型(symbol type)”和“符号信息(symbol infomation)”三种属性。下面几个宏分别用于读取这三种属性值。#define ELF_ST_BIND(x) ((x) >> 4) #define ELF_ST_TYPE(x) (((unsigned int) x) & 0xf) #define ELF32_ST_BIND(x) ELF_ST_BIND(x) #define ELF32_ST_TYPE(x) ELF_ST_TYPE(x) #define ELF64_ST_BIND(x) ELF_ST_BIND(x) #define ELF64_ST_TYPE(x) ELF_ST_TYPE(x)
符号绑定(Symbol Binding),符号绑定属性由
ELF32_ST_BIND
指定,如下:名字 值 STB_LOCAL 0 STB_GLOBAL 1 STB_WEAK 2 STB_LOPROC 13 STB_HIPROC 15 - STB_LOCAL : 表明本符号是一个本地符号。它只出现在本文件中,在本文件外该符号无效。所以在不同的文件中可以定义相同的符号名,它们之间不会互相影响(类似Static 函数)。
- STB_GLOBAL : 表明本符号是一个全局符号。当有多个文件被连接在一起时,在所有文件中该符号都是可见的。正常情况下,在一个文件中定义的全局符号,一定是在其它文件中需要被引用,否则无须定义为全局。
- STB_WEAK : 类似于全局符号,但是相对于 STB_GLOBAL,它们的优先级更低。 全局符号(global symbol)和弱符号(weak symbol)在以下两方面有区别:
- 当连接编辑器把若干个可重定位目标文件连接起来时,同名的STB_GLOBAL 符号不允许出现多次。而如果在一个目标文件中已经定义了一个全局的符号(global symbol),当一个同名的弱符号(weak symbol)出现时,并不会发生错误。连接编辑器会以全局符号为准,忽略弱符号。与全局符号相似,如果已经存在的是一个公用符号,即 st_shndx 域为SHN_COMMON 值的符号,当一个同名的弱符号(weak symbol)出现时,也不会发生错误。连接编辑器会以公用符号为准,忽略弱符号。
- 在查找符号定义时,连接编辑器可能会搜索存档的库文件。如果是查找全局符号,连接编辑器会提取包含该未定义的全局符号的存档成员,存档成员可能是一个全局的符号,也可能是弱符号;而如果是查找弱符号,连接编辑器不会去提取存档成员。未解析的弱符号值为 0。
符号类型(Symbol Types),符号类型属性由
ELF32_ST_TYPE
指定,如下:名字 值 STT_NOTYPE 0 STT_OBJECT 1 STT_FUNC 2 STT_SECTION 3 STT_FILE 4 STT_LOPROC 13 STT_HIPROC 15 - STT_NOTYPE : 本符号类型未指定。
- STT_OBJECT : 本符号是一个数据对象,比如变量、数组等。
- STT_FUNC : 本符号是一个函数,或者其它的可执行代码。函数符号在共享目标文件中有特殊的意义。当另外一个目标文件引用一个共享目标文件中的函数符号时,连接编辑器为被引用符号自动创建一个连接表项。非 STT_FUNC类型的共享目标符号不会通过这种连接表项被自动引用。
- STT_SECTION : 本符号与一个节相关联,用于重定位,通常具有 STB_LOCAL 属性。
- STT_FILE : 本符号是一个文件符号,它具有 STB_LOCAL 属性,它的节索引值是SHN_ABS。在符号表中如果存在本类符号的话,它会出现在所有STB_LOCAL 类符号的前部。
此外还有个
st_shndx
也是比较重要,任何一个符号表项的定义都与某一个“节”相联系,因为符号是为节而定义,在节中被引用。st_shndx
数据成员即指明了相关联的节。本数据成员是一个索引值,它指向相关联的节在节头表中的索引。在重定位过程中,节的位置会改变,本数据成员的值也随之改变,继续指向节的新位置。当本数据成员指向下面三种特殊的节索引值时,本符号具有如下特别的意义:- SHN_ABS : 符号的值是绝对的,具有常量性,在重定位过程中,此值不需要改变。
- SHN_COMMON : 本符号所关联的是一个还没有分配的公共节,本符号的值规定了其内容的字节对齐规则,与 sh_addralign 相似。也就是说,连接器会为本符号分配存储空间,而且其起始地址是向 st_value 对齐的。本符号的值指明了要分配的字节数。
- SHN_UNDEF : 当一个符号指向第 1 节(SHN_UNDEF)时,表明本符号在当前目标文件中未定义,在连接过程中,连接器会找到此符号被定义的文件,并把这些文件连接在一起。本文件中对该符号的引用会被连接到实际的定义上去。
我们可用使用readelf 和 objdump来查看符号信息,如下:
$ readelf -s hello Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2) 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2) Symbol table '.symtab' contains 70 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000318 0 SECTION LOCAL DEFAULT 1 2: 0000000000000338 0 SECTION LOCAL DEFAULT 2 3: 0000000000000358 0 SECTION LOCAL DEFAULT 3 4: 000000000000037c 0 SECTION LOCAL DEFAULT 4 5: 00000000000003a0 0 SECTION LOCAL DEFAULT 5 6: 00000000000003c8 0 SECTION LOCAL DEFAULT 6 7: 0000000000000470 0 SECTION LOCAL DEFAULT 7 8: 00000000000004f2 0 SECTION LOCAL DEFAULT 8 9: 0000000000000500 0 SECTION LOCAL DEFAULT 9 10: 0000000000000520 0 SECTION LOCAL DEFAULT 10 11: 00000000000005e0 0 SECTION LOCAL DEFAULT 11 12: 0000000000001000 0 SECTION LOCAL DEFAULT 12 13: 0000000000001020 0 SECTION LOCAL DEFAULT 13 14: 0000000000001040 0 SECTION LOCAL DEFAULT 14 15: 0000000000001050 0 SECTION LOCAL DEFAULT 15 16: 0000000000001060 0 SECTION LOCAL DEFAULT 16 17: 00000000000011e8 0 SECTION LOCAL DEFAULT 17 18: 0000000000002000 0 SECTION LOCAL DEFAULT 18 19: 0000000000002014 0 SECTION LOCAL DEFAULT 19 20: 0000000000002058 0 SECTION LOCAL DEFAULT 20 21: 0000000000003db8 0 SECTION LOCAL DEFAULT 21 22: 0000000000003dc0 0 SECTION LOCAL DEFAULT 22 23: 0000000000003dc8 0 SECTION LOCAL DEFAULT 23 24: 0000000000003fb8 0 SECTION LOCAL DEFAULT 24 25: 0000000000004000 0 SECTION LOCAL DEFAULT 25 26: 0000000000004010 0 SECTION LOCAL DEFAULT 26 27: 0000000000000000 0 SECTION LOCAL DEFAULT 27 28: 0000000000000000 0 SECTION LOCAL DEFAULT 28 29: 0000000000000000 0 SECTION LOCAL DEFAULT 29 30: 0000000000000000 0 SECTION LOCAL DEFAULT 30 31: 0000000000000000 0 SECTION LOCAL DEFAULT 31 32: 0000000000000000 0 SECTION LOCAL DEFAULT 32 33: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 34: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones 35: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones 36: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux
我们使用hexdump查看文件中符号的原始数据,如下:
$ readelf -S hello There are 36 section headers, starting at offset 0x4220: 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.propert NOTE 0000000000000338 00000338 0000000000000020 0000000000000000 A 0 0 8 [ 3] .note.gnu.build-i NOTE 0000000000000358 00000358 0000000000000024 0000000000000000 A 0 0 4 [ 4] .note.ABI-tag NOTE 000000000000037c 0000037c 0000000000000020 0000000000000000 A 0 0 4 [ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0 0000000000000024 0000000000000000 A 6 0 8 [ 6] .dynsym DYNSYM 00000000000003c8 000003c8 00000000000000a8 0000000000000018 A 7 1 8 [ 7] .dynstr STRTAB 0000000000000470 00000470 0000000000000082 0000000000000000 A 0 0 1 $ hexdump -C -n 512 -s 0x3c8 hello 000003c8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000003d8 00 00 00 00 00 00 00 00 3d 00 00 00 20 00 00 00 |........=... ...| 000003e8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000003f8 0b 00 00 00 12 00 00 00 00 00 00 00 00 00 00 00 |................| 00000408 00 00 00 00 00 00 00 00 1f 00 00 00 12 00 00 00 |................| 00000418 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000428 59 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 |Y... ...........| 00000438 00 00 00 00 00 00 00 00 68 00 00 00 20 00 00 00 |........h... ...| 00000448 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000458 10 00 00 00 22 00 00 00 00 00 00 00 00 00 00 00 |...."...........| $ objdump -s --section=.dynsym hello hello: file format elf64-x86-64 Contents of section .dynsym: 03c8 00000000 00000000 00000000 00000000 ................ 03d8 00000000 00000000 3d000000 20000000 ........=... ... 03e8 00000000 00000000 00000000 00000000 ................ 03f8 0b000000 12000000 00000000 00000000 ................ 0408 00000000 00000000 1f000000 12000000 ................ 0418 00000000 00000000 00000000 00000000 ................ 0428 59000000 20000000 00000000 00000000 Y... ........... 0438 00000000 00000000 68000000 20000000 ........h... ... 0448 00000000 00000000 00000000 00000000 ................ 0458 10000000 22000000 00000000 00000000 ...."........... 0468 00000000 00000000 ........
2.6 重定位
重定位(relocation)是把符号引用与符号定义连接在一起的过程。比如,当程序调用一个函数时,将从当前运行的指令跳转到一个新的指令地址去执行。在编写程序的时候,我们只需指明所要调用的函数名(即符号引用),在重定位的过程中,函数名会与实际的函数所在地址(即符号定义)联系起来,使程序知道应该跳转到哪里去。
重定位文件必须知道如何修改其所包含的“节”的内容,在构建可执行文件或共享目标文件的时候,把节中的符号引用换成这些符号在进程空间中的虚拟地址。包含这些转换信息的数据也就是“重定位项(relocation entries)”。
重定位结构信息定义如下:
typedef struct { Elf32_Addr r_offset; //重定位所作用的位置 uint32_t r_info; } Elf32_Rel; typedef struct { Elf64_Addr r_offset; uint64_t r_info; } Elf64_Rel; Relocation structures that need an addend : typedef struct { Elf32_Addr r_offset; uint32_t r_info; int32_t r_addend; //额外的加数 } Elf32_Rela; typedef struct { Elf64_Addr r_offset; uint64_t r_info; int64_t r_addend; } Elf64_Rela;
-
r_offset
: 给出重定位所作用的位置。对于重定位文件来说,此值是受重定位作用的存储单元在节中的字节偏移量(相对节的偏移);对于可执行文件或共享目标文件来说,此值是受重定位作用的存储单元的虚拟地址。 -
r_info
: 既给出了重定位所作用的符号表索引,也给出了重定位的类型。比如,如果是一个函数的重定位,本数据成员将要持有被调用函数所对应的符号表索引。如果索引值为 STN_UNDEF,即未定义索引,那么重定位过程中将使用 0 作为符号值。以下是应用于 r_info 的宏定义:
#define ELF32_R_SYM(x) ((x) >> 8) #define ELF32_R_TYPE(x) ((x) & 0xff) #define ELF64_R_SYM(i) ((i) >> 32) #define ELF64_R_TYPE(i) ((i) & 0xffffffff)
r_addend
: 指定了一个加数,这个加数用于计算需要重定位的域的值。
一个“重定位节(relocation section)”需要引用另外两个节:一个是符号表节,一个是被修改节。在重定位节中,节头的
sh_info
和sh_link
成员分别指明了引用关系。不同的目标文件中,重定位项的r_offset
成员的含义略有不同。-
在重定位文件中,
r_offset
成员含有一个节偏移量。也就是说,重定位节本身描述的是如何修改文件中的另一个节的内容,重定位偏移量(r_offset
)指向了另一个节中的一个存储单元地址。 -
在可执行文件或共享目标文件中,
r_offset
含有的是符号定义在进程空间中的虚拟地址。可执行文件和共享目标文件是用于运行程序而不是构建程序的,所以对它们来说更有用的信息是运行期的内存虚拟地址,而不是某个符号定义在文件中的位置。
综上所述,链接器可用通过这个节里面的内容找到:
- 需要重定位的文件地址。
- 需要重定位的链接符号。
而对于动态装载器,可以通过这个段里面的内容找到:
- 需要重定位的虚拟地址。
- 需要重定位的动态符号。
因此就可用在其他目标文件中找到相应的符号进行重定位,举个链接文件的例子:
//file1.c void fun(); void _start() { fun(); } //file2.c void fun() { }
我们对这两个文件进行编译:
$ gcc -g -c -O0 file1.c $ gcc -g -c -O0 file2.c $ gcc -g -nostdlib file1.o file2.o -o file.o
对于file1.o 我们可用看到信息如下:
$ readelf -r file1.o Relocation section '.rela.text' at offset 0x3b8 contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 00000000000a 000f00000004 R_X86_64_PLT32 0000000000000000 fun - 4 $ objdump -S file1.o file1.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <_start>: void fun(); void _start() { 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp fun(); 4: b8 00 00 00 00 mov $0x0,%eax 9: e8 00 00 00 00 callq e <_start+0xe> } e: 90 nop f: 5d pop %rbp 10: c3 retq
偏移地址为
00000000000a
,以及符号名称fun
,这样链接器就可用对其进行重定位了,结果如下:$ objdump -S file.o file.o: file format elf64-x86-64 Disassembly of section .text: 00000000000002b1 <_start>: void fun(); void _start() { 2b1: 55 push %rbp 2b2: 48 89 e5 mov %rsp,%rbp fun(); 2b5: b8 00 00 00 00 mov $0x0,%eax 2ba: e8 03 00 00 00 callq 2c2 <fun> } 2bf: 90 nop 2c0: 5d pop %rbp 2c1: c3 retq 00000000000002c2 <fun>: void fun() { 2c2: 55 push %rbp 2c3: 48 89 e5 mov %rsp,%rbp } 2c6: 90 nop 2c7: 5d pop %rbp 2c8: c3 retq
对于动态加载器来说,重定位节是修改动态导入变量或者函数的地址的,如下:
readelf -r hello Relocation section '.rela.dyn' at offset 0x520 contains 8 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000003db8 000000000008 R_X86_64_RELATIVE 1140 000000003dc0 000000000008 R_X86_64_RELATIVE 1100 000000004008 000000000008 R_X86_64_RELATIVE 4008 000000003fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0 000000003fe0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0 000000003fe8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000003ff0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0 000000003ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0 Relocation section '.rela.plt' at offset 0x5e0 contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 000000003fd0 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0 $ objdump -s --section=.got hello hello: file format elf64-x86-64 Contents of section .got: 3fb8 c83d0000 00000000 00000000 00000000 .=.............. 3fc8 00000000 00000000 30100000 00000000 ........0....... 3fd8 00000000 00000000 00000000 00000000 ................ 3fe8 00000000 00000000 00000000 00000000 ................ 3ff8 00000000 00000000 ........
对于puts函数,重定位的地址为
000000003fd0
,这是一个虚拟地址,同样我们通过名称puts@GLIBC_2.2.5 + 0
可以找到符号表对应的虚拟地址,填入地址000000003fd0
就完成了整个重定位的过程。3. 动态装载与动态链接
可执行文件和共享目标文件(动态连接库)是程序的静态存储形式。要执行一个程序,系统要先把相应的可执行文件和动态连接库装载到进程空间中,这样形成一个可运行的进程的内存空间布局,也可以称它为“进程镜像”。一个已装载完成的进程空间会包含多个不同的“段(segment)”,比如代码段(text segment),数据段(data segment),堆栈段(stack
segment)等等。准备一个程序的内存镜像,可以大体上分为装载和连接两个步骤。
- 把目标文件装载入内存。
- 解析目标文件中的符号引用
3.1 程序头
一个可执行文件或共享目标文件的程序头表(program header table)是一个数组,数组中的每一个元素称为“程序头(program header)”,每一个程序头描述了一个“段(segment)”或者一块用于准备执行程序的信息。一个目标文件中的“段(segment)”包含一个或者多个“节(section)”。程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略。在目标文件的文件头(elf header)中,
e_phentsize
和e_phnum
成员指定了程序头的大小。program header table数据结构定义如下:
/* These constants are for the segment types stored in the image headers */ #define PT_NULL 0 #define PT_LOAD 1 #define PT_DYNAMIC 2 #define PT_INTERP 3 #define PT_NOTE 4 #define PT_SHLIB 5 #define PT_PHDR 6 #define PT_TLS 7 /* Thread local storage segment */ #define PT_LOOS 0x60000000 /* OS-specific */ #define PT_HIOS 0x6fffffff /* OS-specific */ #define PT_LOPROC 0x70000000 #define PT_HIPROC 0x7fffffff #define PT_GNU_EH_FRAME 0x6474e550 #define PT_GNU_STACK (PT_LOOS + 0x474e551) typedef struct { uint32_t p_type; //描述段的类型,或者如何解析本程序头的信息 uint32_t p_flags; Elf64_Off p_offset; //文件中的偏移 Elf64_Addr p_vaddr; //在进程空间中虚拟地址 Elf64_Addr p_paddr; //在进程空间中物理地址,不再可用 uint64_t p_filesz; //文件大小 uint64_t p_memsz; //内存大小 uint64_t p_align; //内存对齐 } Elf64_Phdr;
对于
p_type
,取值如下:PT_NULL
: 此类型表明本程序头是未使用的,本程序头内的其它成员值均无意义。具有此种类型的程序头应该被忽略。PT_LOAD
: 此类型表明本程序头指向一个可装载的段。段的内容会被从文件中拷贝到内存中。如前所述,段在文件中的大小是p_filesz
,在内存中的大小是p_memsz
。如果p_memsz
大于p_filesz
,在内存中多出的存储空间应填 0 补充。在程序头表中,所有PT_LOAD
类型的程序头按照p_vaddr
的值做升序排列。PT_DYNAMIC
: 此类型表明本段指明了动态连接的信息。PT_INTERP
: 本段指向了一个以NULL结尾的字符串,这个字符串是一个 ELF 解析器的路径。这种段类型只对可执行程序有意义,当它出现在共享目标文件中时,是一个无意义的多余项。在一个 ELF 文件中它最多只能出现一次,而且必须出现在其它可装载段的表项之前。PT_NOTE
: 本段指向了一个以NULL结尾的字符串,这个字符串包含一些附加的信息。PT_SHLIB
: 该段类型是保留的,而且未定义语法。 UNIX System V 系统上的应用程序不会包含这种表项。PT_PHDR
: 此类型的程序头如果存在的话,它表明的是其自身所在的程序头表在文件或内存中的位置和大小。这样的段在文件中可以不存在,只有当所在程序头表所覆盖的段只是整个程序的一部分时,才会出现一次这种表项,而且这种表项一定出现在其它可装载段的表项之前。PT_LOPROC
~PT_HIPROC
: 类型值在这个区间的程序头是为特定处理器保留的。
3.1.1 基地址
程序头中出现的虚拟地址不能代表其相应的数据在进程内存空间中的虚拟地址。可执行文件中需要含有绝对的地址,比如变量地址,函数地址等,为了让程序正确地执行,“段”中出现的虚拟地址必须在创建可执行程序时被重新计算。另一方面,出于 ELF 通用性的要求,目标文件的段中又不能出现绝对地址,其代码是不应依赖于具体存储位置的,即同一个段在被加载到两个不同的进程中时,它的地址可能不同,但它的行为不能表现出不一样。
在被加载到进程空间里时,尽管“段”会被分配到一个不确定的地址,但是不同的段之间会有确定的“相对位置(relative position)”。也就是说,在目标文件中存储的两个段,它们的位置之间有多少偏移,当它们被加载到内存中时,这两个段的位置之间仍然保持这么大的偏移(距离)。一个段在内存中的虚拟地址与其在目标文件中的地址一般是不相等的,它们之间会有一个偏移量,这个偏移量被称为“基地址(base address)”,基地址的作用之一就是在动态连接过程中为程序重定位内存镜像。
一个可执行文件或共享目标文件的基地址是在运行期间由以下三个值计算出来的:内存加载地址,最大页面大小,程序可装载段的最低地址。为计算基地址,首先找出类型为 PT_LOAD(即可加载)而且 p_vaddr(段地址)最低的那个段,把这个段在内存中的地址与最大页面大小相除,得到一个段地址的余数;再把p_vaddr 与最大页面大小相除,得到一个 p_vaddr 的余数。基地址就是段地址的余数与p_vaddr的余数之差。
3.1.2 段权限
虽然 ELF 文件格式中没有规定,但是一个可执行程序至少会有一个可加载的段。当为可加载段创建内存镜像时,系统会按照 p_flags 的指示给段赋予一定的权限。
名字 值 含义 PF_X 0x1 可执行 PF_W 0x2 只写 PF_R 0x4 只读 PF_MASKPROC 0xf0000000 未指定 具体定义值如下:
/* These constants define the permissions on sections in the program header, p_flags. */ #define PF_R 0x4 #define PF_W 0x2 #define PF_X 0x1
3.1.3 实例
我们看一个ELF文件的程序头部信息,如下:
$ readelf -l hello Elf file type is DYN (Shared object 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 0x00000000000005f8 0x00000000000005f8 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x00000000000001f5 0x00000000000001f5 R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000160 0x0000000000000160 R 0x1000 LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000258 0x0000000000000260 RW 0x1000 DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8 0x00000000000001f0 0x00000000000001f0 RW 0x8 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 0x0000000000000044 0x0000000000000044 R 0x4 GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x0000000000002014 0x0000000000000044 0x0000000000000044 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
在最后面,我们可用看到每一个段对应的节的关系,一个Segment包含多个Section。
3.2 注释段
类型为 PT_NOTE 的段往往会包含类型为 SHT_NOTE 的节, SHT_NOTE 节可以为目标文件提供一些特别的信息,用于给其它的程序检查目标文件的一致性和兼容性。这些信息我们称为“注释信息”,这样的节称为“注释节(note section)”,所在的段即为“注释段(note segment)”。注释信息可以包含任意数量的“注释项”,每一个注释项是一个数组,数组的每一个成员大小为 4 字节,格式依目标处理器而定。下图解释了注释信息是如何组织的,但这仅是一种参考,不是规范的一部分。
内容(4字节) namesz descsz type name… desc… 对于其中的每一个字段,含义分别如下:
-
namesz 和 name : Namesz 和 name 成对使用。 Namesz 是一个 4 字节整数,而 name 是一个以NULL结尾的字符串。 Namesz 是 name 字符串的长度。字符串 name 的内容是本项的所有者的名字。没有正式的机制来避免名字冲突,一般按照惯例,系统提供商应把他们自己的名字写进 name 项里,比如”XYZ Computer Company”。如果没有名字
的话, namesz 是 0。由于数组项的大小是向 4 字节对齐的,所以如果字符串长度不是整 4 字节的话,需要填 0 补位。如果有补位的话, namesz 只计字符串长度,不计所补的空位。 -
descsz 和 desc : Descsz 和 desc 也成对使用,它们的格式与 namesz/name 完全相同。不过,desc 的内容没有任何规定、限制,甚至建议,它包含哪些信息完全是自由的。
-
type : 这个字段给出描述项(desc)的解释,或者说是描述项的类型。每一个提供商都会定义自己的类型,所以同一类型值对于不同的提供商其解释也是不同的。当一个程序读取注释信息的时候,它必须同时辨认出 name 和 type 才能理解 desc 的内容。
下面我们看一下notes段的内容如下:
$ readelf -n main Displaying notes found in: .note.ABI-tag Owner Data size Description HNU 0x00000010 NT_VERSION (version) description data: 00 00 00 00 03 00 00 00 02 00 00 00 00 00 00 00 Displaying notes found in: .note.gnu.build-id Owner Data size Description GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring) Build ID: 6628b6f822b0c5175adbb067b53d66b4b4a806ba
我们可用读取一下原始内容如下:
$ hexdump -C -s 0x254 -n 32 main 00000254 04 00 00 00 10 00 00 00 01 00 00 00 48 4e 55 00 |............HNU.| 00000264 00 00 00 00 03 00 00 00 02 00 00 00 00 00 00 00 |................| 00000274
- namesz的值为0x4.
- descsz的值为0x10.
- typs的值为0x1.
- name的值为HNU.
- desc的值为
00 00 00 00 03 00 00 00 02 00 00 00 00 00 00 00
.
上面的这些数据和使用命令
readelf -n
读取出来的一致。3.3 动态链接
动态链接也就是解析符号引用的过程,这个过程在进程初始化和进程运行期间都可能发生。
3.3.1 程序解析器
一个参与动态链接的可执行文件会包含一个类型为
PT_INTERP
的程序头项。当执行一个程序的时候,系统函数exec(cmd)
会被调用,在这个函数中,内核会去读取这个PT_INTERP
段,解析出其包含的一个路径字符串,这个串指明了一个ELF程序解析器,系统会转去初始化该解析器的进程镜像。也就是,在这时系统会暂停原来的工作,不是用待执行文件的段内容去初始化进程空间,而是把进程空间暂时“借”给解析器程序使用。然后,解析器程序将从系统手中接过控制权继续执行。例如,我们可用在ELF看到解析器如下:
f$ file main main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6628b6f822b0c5175adbb067b53d66b4b4a806ba, with debug_info, not stripped $ hexdump -C -s 0x238 -n 32 main 00000238 2f 6c 69 62 36 34 2f 6c 64 2d 6c 69 6e 75 78 2d |/lib64/ld-linux-| 00000248 78 38 36 2d 36 34 2e 73 6f 2e 32 00 04 00 00 00 |x86-64.so.2.....| 00000258
解析器以两种方式来接手系统的控制:
-
第一种,解析器取得可执行文件的描述符,内容指针定位于文件开始处,解析器可以读取并映射可执行程序的段到内存中。
-
第二种,对于有些可执行文件格式,系统直接将文件内容载入内存,并不把其文件描述符给解析器。
解析器可以是一个共享目标文件,也可以是一个可执行文件。
-
一般情况下,解析器会是一个共享目标文件,并且其段内容是位置不相关的,所以在不同的进程中,它的地址会不一样,系统会使用
mmap(...)
系统调用在动态段区域来为解析器创建段的镜像。所以,一般情况下不用担心解析器的段内容会与待执行文件的内容发生地址冲突。 -
如果解析器是独立的可执行文件,那么系统就要按照解析器程序的程序头来加载它,在加载的时候就有可能与待执行文件的段相冲突,这种情况下由解析器来负责解决冲突。
3.3.2 动态链接器
当创建一个可执行文件时,如果依赖其它的动态链接库,那么链接编辑器会在可执行文件的程序头中加入一个
PT_INTERP
项,告诉系统这里需要使用动态链接器。可执行文件与动态链接器一起创建了进程的镜像,这个过程包含以下活动:
- 添加可执行文件的段到进程空间;
- 添加共享目标文件的段到进程空间;
- 为可执行文件和共享目标文件进行重定位;
- 如果动态链接器使用了可执行文件的文件描述符,应关闭它;
- 把控制权交给程序。
链接编辑器也会为动态链接库组织一些数据,以方便它的链接过程。在“程序头”部分提到过,为了方便在运行的时候访问,这些数据放在可装载的段中。当然具体的数据格式是依处理器而不同的。
- 类型为
SHT_DYNAMIC
的.dynamic 节中包含有很多种动态链接信息。在这个节的最开始处有一个结构,其中包含有其它动态链接信息的地址。 - 类型为
SHT_HASH
的.hash节中含有符号哈希表。 - 类型为
SHT_PROGBITS
的.got和.plt节各包含一张表。
共享目标所占据的内存地址可能与文件程序头表中所记录的不同。在程序开始执行以前,动态链接器会为内存镜像做重定位,更新绝对地址。当然,库文件在被装载时,如果其内存地址与其文件中描述的完全相同的话,那些引用它们的绝对地址就是对的,不需要更新。但事实上,这种情况很少发生。
如果进程的环境变量中含有
LD_BIND_NOW
,而且其值不为空,那么动态连接器就要在程序开始运行之前把所有重定位都处理完。比如,在该环境变量为以下值时,动态连接器都需要这样做:LD_BIND_NOW
= 1LD_BIND_NOW
= onLD_BIND_NOW
= off
否则,如果
LD_BIND_NOW
没有出现或者其值为空,动态链接器就可以把处理重定位的工作推后,即只有当一个符号被引用的时候才去重定位它。因为在程序运行过程中,有一些函数并不会被调用到,推后重定位是一种提高效率的方法,可以避免为这些函数做不必要的重定位。3.3.3 动态段
如果一个目标文件参与动态链接的话,它的程序头表中一定会包含一个类型为
PT_DYNAMIC
的表项,其所对应的段称为动态段(dynamic segment),段的名字为.dynamic(也是.dynamic节)。动态段的作用是提供动态链接器所需要的信息,比如依赖于哪些共享目标文件、动态链接符号表的位置、动态链接重定位表的位置等等。这个动态段中包含有动态节,动态节由符号DYNAMIC所标记,它包含一个由如下结构体组成的数组。typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; extern Elf32_Dyn _DYNAMIC[]; typedef struct { Elf64_Sxword d_tag; union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn; extern Elf64_Dyn _DYNAMIC[];
对于每一个这种类型的目标项,
d_tag
控制着对d_un
的解析:d_tag
标记控制着对d_un
的解析。d_val
类型为Elf32_Word
/Elf64_Xword
的目标项代表的是整型数。d_ptr
类型为Elf32_Addr
/Elf64_Addr
的目标项代表的是进程空间里的地址。目标项在文件中的地址与其在进程空间内的地址可能会不同。当系统解析到这个动态节中的地址时,动态链接器就可以根据文件地址和内存基地址来计算出实际的内存地址。
d_tag
字段表示当前表项的具体类型,部分可选的枚举值如下:#define DT_NULL 0 //用于标记_DYNAMIC 数组的结束 #define DT_NEEDED 1 //依赖库, DT_STRTAB标记 #define DT_PLTRELSZ 2 //重定位项的总大小 #define DT_PLTGOT 3 //GOT表地址 #define DT_HASH 4 //哈希表地址 #define DT_STRTAB 5 //字符串表的地址 #define DT_SYMTAB 6 //符号表的地址 #define DT_RELA 7 //重定位表的地址 #define DT_RELASZ 8 //重定位表大小 #define DT_RELAENT 9 //重定位表项大小 #define DT_STRSZ 10 //字符串表大小 #define DT_SYMENT 11 //符号表项大小 #define DT_INIT 12 //初始化函数 #define DT_FINI 13 //析构函数 #define DT_SONAME 14 //别名索引 #define DT_RPATH 15 //RPATH #define DT_SYMBOLIC 16 #define DT_REL 17 #define DT_RELSZ 18 #define DT_RELENT 19 #define DT_PLTREL 20 #define DT_DEBUG 21 #define DT_TEXTREL 22 #define DT_JMPREL 23 #define DT_ENCODING 32
各个字段解释如下:
DT_NULL
: 用于标记_DYNAMIC 数组的结束。DT_NEEDED
: 此元素指明了一个所需的库的名字。不过此元素本身并不是一个字符串,它是一个指向由DT_STRTAB
所标记的字符串表中的索引,在表中,此索引处是一个以NULL结尾的字符串,这个字符串就是库的名字。在动态数组中可以包含若干个此类型的项,这些项出现的相对顺序是不能随意调换的。DT_PLTRELSZ
: 此元素含有与函数链接表相关的所有重定位项的总大小,以字节为单位。如果数组中有DT_JMPREL
项的话,DT_PLTRELSZ
也必须要有。DT_PLTGOT
: 此元素包含与函数链接表或全局偏移量表相应的地址。在Intel架构中,这一项的d_ptr
成员给出全局偏移量表中第一项的地址。对于全局偏移量表(GOT)中前三项都是保留的,其中两项用于持有函数链接表信息。DT_HASH
: 此元素含有符号哈希表的地址。这里所指的哈希表与DT_SYMTAB
所指的哈希表是同一个。DT_STRTAB
: 此元素包含字符串表的地址,此表中包含符号名、库名等等。DT_SYMTAB
:此元素包含符号表的地址。DT_RELA
: 此元素包含一个重定位表的地址,在重定位表中存储的是显式的加数。在一个目标文件中可以存在多个重定位节,当为可执行文件或共享目标文件创建重定位表的时候,链接器会把这些重定位节连接在一起,最后形成一张大的重定位表。当链接器为一个可执行文件创建进程空间,或者把一个共享目标添加到进程空间中去的时候,它会去读重定位表并执行相应的操作。如果在动态结构中包含有DT_RELA
元素的话,就必须同时还包含DT_RELASZ
和DT_RELAENT
元素。如果一个文件需要重定位的话,DT_RELA
或DT_REL
至少要出现一个。DT_RELASZ
: 此元素持有DT_RELA
相应的重定位表的大小,以字节为单位。DT_RELAENT
: 此元素持有DT_RELA
相应的重定位表项的大小,以字节为单位。DT_STRSZ
: 此元素持有字符串表的大小,以字节为单位。DT_SYMENT
: 此元素持有符号表项的大小,以字节为单位。DT_INIT
: 此元素持有初始化函数的地址。DT_FINI
: 此元素持有终止函数的地址。DT_SONAME
: 此元素持有一个字符串表中的偏移量,该位置存储了一个以NULL结尾的字符串,是一个共享目标的名字。相应的字符串表由DT_STRTAB
指定。DT_RPATH
: 此元素持有一个字符串表中的偏移量,该位置存储了一个以NULL结尾的字符串,是一个用于搜索库文件的路径名。相应的字符串表由DT_STRTAB
指定。DT_SYMBOLIC
: 在共享目标文件中,此元素的出现与否决定了动态链接器解析符号时所用的算法。如果此元素不出现的话,动态连接器先搜索可执行文件再搜索库文件;如果此元素出现的话,顺序刚好相反,动态链接器会先从本共享目标文件开始,后搜索可执行文件。DT_REL
: 此元素与DT_RELA
相似,只是它所指向的重定位表中,“加数”是隐含的而不是显式的。DT_RELSZ
: 此元素持有DT_REL
相应的重定位表的大小,以字节为单位。DT_RELENT
: 此元素持有DT_REL
相应的重定位表项的大小,以字节为单位。DT_PLTREL
: 本成员指明了函数连接表所引用的重定位项的类型。d_val
成员含有DT_REL
或DT_RELA
。函数连接表中的所有重定位类型都是相同的。DT_TEXTREL
: 如果此元素出现的话,在重定位过程中如果需要修改的是只读段的话,链接器可以做相应的修改;而如果此元素不出现的话,在重定位过程中,即使需要,也不能修改只读段。DT_JMPREL
: 此类型元素如果存在的话,其d_ptr
成员含有与函数链接表单独关联的重定位项地址。把多个重定位项分开可以让动态链接器在初始化的时候忽略它们,当然前提条件是“后期绑定”是激活的。如果此元素存在的话,DT_PLTRELSZ
和DT_PLTREL
也应该出现。DT_BIND_NOW
: 如果此元素存在的话,动态链接器必须在程序开始执行以前,完成所有包含此项的目标的重定位工作。如果此元素存在,即使程序应用了“后期绑定”,它对于此项所指定的目标也不适用,动态链接器仍需事先做好重定位。
我们来看一下可执行文件动态段的解析过程,首先我们看到的程序头部信息如下:
$ readelf -l hello Elf file type is DYN (Shared object 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 0x00000000000005f8 0x00000000000005f8 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x00000000000001f5 0x00000000000001f5 R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000160 0x0000000000000160 R 0x1000 LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000258 0x0000000000000260 RW 0x1000 DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8 0x00000000000001f0 0x00000000000001f0 RW 0x8 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 0x0000000000000044 0x0000000000000044 R 0x4 GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x0000000000002014 0x0000000000000044 0x0000000000000044 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
接着我们可以看到动态段的内容如下:
$ objdump -s --section=.dynamic hello hello: file format elf64-x86-64 Contents of section .dynamic: 3dc8 01000000 00000000 01000000 00000000 ................ 3dd8 0c000000 00000000 00100000 00000000 ................ 3de8 0d000000 00000000 e8110000 00000000 ................ 3df8 19000000 00000000 b83d0000 00000000 .........=...... 3e08 1b000000 00000000 08000000 00000000 ................ 3e18 1a000000 00000000 c03d0000 00000000 .........=...... 3e28 1c000000 00000000 08000000 00000000 ................ 3e38 f5feff6f 00000000 a0030000 00000000 ...o............ 3e48 05000000 00000000 70040000 00000000 ........p....... 3e58 06000000 00000000 c8030000 00000000 ................ 3e68 0a000000 00000000 82000000 00000000 ................ 3e78 0b000000 00000000 18000000 00000000 ................ 3e88 15000000 00000000 00000000 00000000 ................ 3e98 03000000 00000000 b83f0000 00000000 .........?...... 3ea8 02000000 00000000 18000000 00000000 ................ 3eb8 14000000 00000000 07000000 00000000 ................ 3ec8 17000000 00000000 e0050000 00000000 ................ 3ed8 07000000 00000000 20050000 00000000 ........ ....... 3ee8 08000000 00000000 c0000000 00000000 ................ 3ef8 09000000 00000000 18000000 00000000 ................ 3f08 1e000000 00000000 08000000 00000000 ................ 3f18 fbffff6f 00000000 01000008 00000000 ...o............ 3f28 feffff6f 00000000 00050000 00000000 ...o............ 3f38 ffffff6f 00000000 01000000 00000000 ...o............ 3f48 f0ffff6f 00000000 f2040000 00000000 ...o............ 3f58 f9ffff6f 00000000 03000000 00000000 ...o............ 3f68 00000000 00000000 00000000 00000000 ................ 3f78 00000000 00000000 00000000 00000000 ................ 3f88 00000000 00000000 00000000 00000000 ................ 3f98 00000000 00000000 00000000 00000000 ................ 3fa8 00000000 00000000 00000000 00000000 ................
虚拟的偏移地址为3dc8,跟我们目标匹配,这里我们看一下DT_NEEDED的数据为:3dc8 01000000 00000000 01000000 00000000;可以知道为DT_STRTAB的索引1的位置,DT_STRTAB的数据如下:3e48 05000000 00000000 70040000 00000000
查看表信息如下:
$ objdump -s --section=.dynstr hello hello: file format elf64-x86-64 Contents of section .dynstr: 0470 006c6962 632e736f 2e360070 75747300 .libc.so.6.puts. 0480 5f5f6378 615f6669 6e616c69 7a65005f __cxa_finalize._ 0490 5f6c6962 635f7374 6172745f 6d61696e _libc_start_main 04a0 00474c49 42435f32 2e322e35 005f4954 .GLIBC_2.2.5._IT 04b0 4d5f6465 72656769 73746572 544d436c M_deregisterTMCl 04c0 6f6e6554 61626c65 005f5f67 6d6f6e5f oneTable.__gmon_ 04d0 73746172 745f5f00 5f49544d 5f726567 start__._ITM_reg 04e0 69737465 72544d43 6c6f6e65 5461626c isterTMCloneTabl 04f0 6500 e.
注意,上面这些地址信息都是虚拟地址,并不是文件的偏移地址。
3.3.4 共享目标的依赖关系
当动态链接器为一个目标文件创建内存段的时候,动态结构中的
DT_NEEDED
项会指明所依赖的库,动态链接器会链接被引用的符号和它们所依赖的库,这个过程会反复地执行,直到一个完整的进程镜像被构建好。当解析一个符号引用的时候,动态链接器以一种“广度优先”的算法来查找符号表。就是说,动态链接器首先查找可执行程序自己的符号表,然后是DT_NEEDED
项所指明的库的符号表,再接下来是下一层依赖库的符号表,依次下去。共享目标文件必须是可读的,其它权限没有要求。即使一个共享目标在依赖关系中被引用多次,动态链接器也只会链接它一次。在依赖关系列表中的名字,即可以是
DT_SONAME
字符串,也可以是用于创建目标文件的共享目标文件的路径名。如果一个共享目标名字中含有斜线(/)字符,比如
/usr/lib/lib2
或者directory/file
,动态链接就直接把字符串作为路径名。如果名字中没有斜线,比如lib1,需要根据以下三种规则来查找库文件:-
第一,动态数组标记
DT_RPATH
可能给出了一个含有一系列目录名的字符串,各目录名以冒号:相隔。比如,如果字符串是/home/dir/lib:/home/dir2/lib:
,表明动态链接器的查找路径依次是/home/dir/lib
、/home/dir2/lib
和当前目录。 -
第二,进程的环境变量中会有一个
LD_LIBRARY_PATH
变量,它也含有一个目录名列表,各目录名以冒号:相隔,各目录名列表以分号;相隔(LD_LIBRARY_PATH
路径的优先级要低于DT_RPATH
所指明的路径)。 -
第三,如果如上两组路径都无法找到所要的库,动态链接库就搜索
/usr/lib
。
3.3.5 全局偏移量表
全局偏移量表(global offset table)在私有数据中包含绝对地址。出于方便共享和重用的考虑,目标文件中的很多内容是“位置无关”的,其映射到进程内存中的什么位置是不一定的,所以只适合使用相对地址,全局偏移量表是一个例外。
总的来说,位置独立的代码不能含有绝对的虚拟地址。全局偏移量表选择了在私有数据中含有绝对地址,这种办法在没有牺牲位置独立性和可共享性的前提下保存了绝对地址。引用全局偏移量表的程序可以同时使用位置独立的地址和绝对地址,把位置无关的引用重定向到绝对地址上去。
如果一个程序要求直接访问符号的绝对地址,那么这个符号在全局偏移量表中就必须有一个对应的项。可执行文件和共享目标文件有各自的全局偏移量表,所以一个符号的地址可能会出现在多个表中。动态链接器会在程序开始执行之前,处理好所有全局偏移量表的重定位工作,所以在程序执行的时候,可以保证所有这些符号都有正确的绝对地址。
全局偏移量表的第 0 项是保留的,它用于持有动态结构的地址,由符号
DYNAMIC
引用。这样,其它程序,比如动态链接器就可以直接找到其动态结构,而不用借助重定位项。这对于动态链接器来说尤为重要,因为它必须在不依赖于其它程序重定位其内存镜像的情况下初始化自己。在 Intel 架构中,全局偏移量表中的第 1 项和第 2 项也是保留的,它们持有函数连接表的信息。系统可能为同一个共享目标在不同的程序中选择不同的段地址;甚至也可能每次为同一个程序选择不同的地址。但是,在单次执行中,一旦一个进程的镜像建立起来之后,直到程序退出,内存段的地址都不会再改变了。
3.3.6 函数地址
在可执行文件和共享目标文件中,当引用到同一个函数时,函数地址可能并不相同。在共享目标文件中,函数的地址被动态链接器正常地解析为它所在的虚拟地址。但在可执行文件中则不同,但可执行文件引用一个共享库中的函数时,它不是直接指向函数的虚拟地址,而是被动态链接器定向到函数链接表中的一个表项。
但是,这样的话,来自可执行文件的函数地址和来自共享目标文件的同一函数地址就会不同,为了避免在比较两个函数地址时出现这样的逻辑错误,链接编辑器和动态链接器做了一些特别操作。当可执行文件引用一个在共享目标文件中定义的函数时,链接编辑器就把这个函数的函数链接表项的地址放到其相应的符号表项中去。动态链接器会特别对待这种符号表项。在可执行文件中,如果动态链接器查找一个符号时遇到了这种符号表项,就会按照以下规则行事:
-
如果符号表项的
st_shndx
成员不是SHN_UNDEF
,动态链接器就找到了一个符号的定义,把表项的st_value
成员作为符号的地址。 -
如果符号表项的
st_shndx
成员是SHN_UNDEF
,并且符号类型是STT_FUNC
,st_value
成员又非0的话,动态链接器就认定这是一个特殊的项,把st_value
成员作为符号的地址。 -
否则,动态链接器认为这个符号是在可执行文件中未定义的。
有些重定位与函数链接表项有关,这些表项用于给函数调用做定向,而不是引用函数地址。这种重定位不能像上面所描述的那样,用特别的方式去处理函数地址,因为动态链接器不可以把函数链接表项重定向到它们自己。
3.3.7 函数链接表
全局偏移量表用于把位置独立的地址重定向到绝对地址,与此功能类似,函数链接表(procedure linkage table)的作用是把位置独立的函数调用重定向到绝对地址。链接编辑器不能解析函数在不同目标文件之间的跳转,那么,它就把对其它目标文件中函数的调用重定向到一个函数链接表项中去。动态链接器决定目标的绝对地址,并且会相应地修改全局偏移量表的内存镜像。这样,动态链接器就可以在不牺牲位置无关性和代码的可共享性条件下,实现到绝对地址的重定位。可执行文件和共享目标文件有各自的函数链接表。
关于PLT和GOT的关系和动态解析的过程见其他分析文章。
3.3.8 解析符号
在以下的这些步骤中,动态链接器与程序合作来解析函数链接表和全局偏移量表中所有的符号引用。
-
在一开始创建程序内存镜像的时候,动态链接器把全局偏移量表中的第2和第3个表项设为特定值。
-
如果函数连接表是位置独立的,全局偏移量表的地址必须存储在%ebx 中。进程空间中的每一个共享目标文件都有自己的函数连接表,每一个表都是用于本文件内的函数调用。那么,主调函数就要负责在调用函数连接表项之前设置全局偏移量表。
环境变量
LD_BIND_NOW
可以改变动态连接器的行为,如果它的值为非NULL,动态连接器在传递控制权给程序之前会估计函数连接表项。否则,如果其值为NULL,这种估计仍然会进行,但并不是在初始化的时候,这个过程会被推后,直到在执行过程中,该函数连接表项被用到才开始。“延迟绑定/懒绑定” (lazy binding)一般来说都会提高应用程序的性能,因为这样可以避免用不到的符号在动态连接过程中被解析。但是,在两种情况下,延迟绑定的效果并不理想。
-
第一种情况,如果对一个共享目标函数的第一次引用比其后的引用要花更多时间的话,在第一次引用时,程序就要暂停下来,由动态连接器去解析符号,如果应用程序对这种不可预知的暂停比较敏感的话,后期绑定就不适用。
-
第二种情况,如果动态连接器解析一个符号失败,程序将会被终止。如果没有打开后期绑定的话,这一切都发生在程序实际得到控制权之前,进程将在初始化过程中被终止。而如果打开了后期绑定的话,错误会发生在程序运行过程中,如果应用程序对这种不可预知的错误敏感的话,后期绑定也不适用。
3.4 哈希表
一个 Elf32_Word 目标组成的哈希表支持符号表的访问。下面表示一个哈希表的具体结构
内容(4字节) nbucket nchain bucket[0]
…
bucket[nbucket - 1]chain[0]
…
chain[nchain - 1]Bucket 数组中含有 nbucket 个项, chain 数组中含有 nchain 个项,序号都从 0 开始。 Bucket 和 chain 中包含的都是符号表中的索引。符号表中的项数必须等于nchain,所以符号表中的索引号也可以用来索引 chain 表。一个哈希数输入一个符号名,输出一个值用于计算 bucket 索引。如果给出一个符号名,经哈希函数计算得到值 x,那么 x%nbucket 是 bucket 表内的索引, bucket[x%nbucket]给出一个符号表的索引值 y, y 同时也是 chain 表内的索引值。如果符号表内索引值为 y 的元素并不是所要的,那么 chain[y]给出符号表中下一个哈希值相同的项的索引。如果所有哈希值相同的项都不是所要的,最后的一个 chain[y]将包含值STN_UNDEF,说明这个符号表中并不含有此符号。
tatic unsigned elfhash(const char *_name) { const unsigned char *name = (const unsigned char *) _name; unsigned h = 0, g; while(*name) { h = (h << 4) + *name++; g = h & 0xf0000000; h ^= g; h ^= g >> 24; } return h; } static Elf32_Sym *soinfo_elf_lookup(struct soinfo *si, unsigned hash, const char *name) { Elf32_Sym *symtab = si->symtab; const char *strtab = si->strtab; unsigned n; for( n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n] ) { Elf32_Sym *s = symtab + n; if( strcmp(strtab + s->st_name, name) == 0 ) { return s; } } return NULL; }
3.5 初始化和终止函数
当动态链接器构建好进程镜像,并完成重定位后,每一个共享目标都有机会执行一些初始化代码。所有共享目标的初始化都发生在程序开始执行前。
一个目标的初始化代码被执行以前,必须保证它所依赖的所有目标已经被初始化过。这里所说的“依赖”,即出现在动态结构的
DT_NEEDED
项里。如果两个目标,它们互相依赖,或者彼此间的依赖关系构成环状的话,哪个应该被先初始化,这里未作定义。例如:-
如果一个目标A依赖于另外一个目标B,而目标B又依赖于目标C的话。当需要对 做初始化时,应先递规地初始化B和C,即先初始化 C,然后是,最后是A。
-
如果一个目标A依赖于另处两个目标B和C,而B和C之间没有依赖关系的话,B和C谁先被初始化都可以。
与初始化过程相似,每一个共享目标还可以有终止函数,将在进程准备终止的时候被调用。动态链接器调用终止函数的顺序正好与初始化过程相反,如果一个目标没有定义初始化函数的话,动态链接器应假设它有一个空的初始化函数并且被调用,并按照相反的顺序来调用其终止函数。
动态链接器必须保证,无论是初始化函数还是终止函数都不能被重复调用。共享目标把初始化和终止函数分别定义在动态结构的
DT_INIT
和DT_FINI
项中,初始化和终止函数的代码存放在.init
和.fini
节中。我们看一下这两个函数的寻找过程:
$ objdump -s --section=.dynamic hello hello: file format elf64-x86-64 Contents of section .dynamic: 3dc8 01000000 00000000 01000000 00000000 ................ 3dd8 0c000000 00000000 00100000 00000000 ................ 3de8 0d000000 00000000 e8110000 00000000 ................ 3df8 19000000 00000000 b83d0000 00000000 .........=...... 3e08 1b000000 00000000 08000000 00000000 ................ 3e18 1a000000 00000000 c03d0000 00000000 .........=...... 3e28 1c000000 00000000 08000000 00000000 ................ 3e38 f5feff6f 00000000 a0030000 00000000 ...o............ 3e48 05000000 00000000 70040000 00000000 ........p....... 3e58 06000000 00000000 c8030000 00000000 ................ 3e68 0a000000 00000000 82000000 00000000 ................ 3e78 0b000000 00000000 18000000 00000000 ................ 3e88 15000000 00000000 00000000 00000000 ................ 3e98 03000000 00000000 b83f0000 00000000 .........?...... 3ea8 02000000 00000000 18000000 00000000 ................ 3eb8 14000000 00000000 07000000 00000000 ................ 3ec8 17000000 00000000 e0050000 00000000 ................ 3ed8 07000000 00000000 20050000 00000000 ........ ....... 3ee8 08000000 00000000 c0000000 00000000 ................ 3ef8 09000000 00000000 18000000 00000000 ................ 3f08 1e000000 00000000 08000000 00000000 ................ 3f18 fbffff6f 00000000 01000008 00000000 ...o............ 3f28 feffff6f 00000000 00050000 00000000 ...o............ 3f38 ffffff6f 00000000 01000000 00000000 ...o............ 3f48 f0ffff6f 00000000 f2040000 00000000 ...o............ 3f58 f9ffff6f 00000000 03000000 00000000 ...o............ 3f68 00000000 00000000 00000000 00000000 ................ 3f78 00000000 00000000 00000000 00000000 ................ 3f88 00000000 00000000 00000000 00000000 ................ 3f98 00000000 00000000 00000000 00000000 ................ 3fa8 00000000 00000000 00000000 00000000 ................
3dd8 0c000000 00000000 00100000 00000000
和3de8 0d000000 00000000 e8110000 00000000
分别指示了.init
和.fini
的初始化和终止函数,这两个节的反汇编内容如下:$ objdump -d --section=.init hello hello: file format elf64-x86-64 Disassembly of section .init: 0000000000001000 <_init>: 1000: f3 0f 1e fa endbr64 1004: 48 83 ec 08 sub $0x8,%rsp 1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__> 100f: 48 85 c0 test %rax,%rax 1012: 74 02 je 1016 <_init+0x16> 1014: ff d0 callq *%rax 1016: 48 83 c4 08 add $0x8,%rsp 101a: c3 retq $ objdump -d --section=.fini hello hello: file format elf64-x86-64 Disassembly of section .fini: 00000000000011e8 <_fini>: 11e8: f3 0f 1e fa endbr64 11ec: 48 83 ec 08 sub $0x8,%rsp 11f0: 48 83 c4 08 add $0x8,%rsp 11f4: c3 retq
更多相关内容 -
-
elf_reader:Go库,用于读取和解析ELF文件
2021-05-26 00:12:04该库用于使用Go编程语言读取ELF文件。 Go的标准库已经包含与ELF相关的功能,但是这些功能不包含一些有用的功能,这些功能可以直接显示或访问ELF文件的某些方面。 该库支持大端和小端32位和64位ELF文件。 您可以通过... -
windows中查看elf文件
2022-05-10 13:31:51查看elf文件的工具包,包含有readelf.exe,nm.exe,objdump.exe以及如果不能运行,确实libiconv-2.dll这个组件也包含在里面 除了上述命令行工具,还有一个可视化的FileViewPro -
elf_inject:操作系统大作业:ELF文件注入
2021-03-11 17:45:49操作流程测试用例介绍:本elf注入的功能是在elf文件执行前,生成helloworld文件并写入内容:helloworld,之后再执行原elf文件功能gcc main.c -o main # 生成注入函数# 测试文件功能:打印 This is the program, ... -
如何将elf文件转换为hex文件
2020-03-17 16:39:41如何将elf文件转换为hex文件 elf(Executable and Linkable Format)文件一般是由gcc编译器生成的,在Linux开发环境使用较多,但Windows一般情况下需要使用hex文件来进行烧录,那么如何将elf格式转换为hex格式呢?... -
关于sdk烧写elf文件问题
2020-08-04 22:28:22新建的是helloworld工程模板,生成elf文件后右键helloworld工程,选择Run As----Run Configuration,出现的错误是:An internal error occurred during: Launching hello_world_0 Debug . -
ELF文件格式分析.pdf
2021-06-24 17:38:11ELF文件格式分析 -
elf文件转换hex文件小程序
2020-07-07 00:02:16将elf文件转换为hex文件的小程序,使用前安装arm-none-eabi编译工具链,复制到elf文件所在目录双击执行,若当前文件没有.elf文件,则会提示 No such file,若转换成功则会在目录内生成HexFile.hex文件 -
XELFViewer:适用于Windows,Linux和MacOS的ELF文件查看器
2021-03-28 08:15:04适用于Windows,Linux和MacOS的ELF文件查看器/编辑器。 如何在Linux上构建 安装Qt 5.15.2: : 克隆项目:git clone --recursive 编辑build_lin64.bat(检查QT_PATH变量) 运行build_lin64.bat 如何在OSX上构建 ... -
pyelfwriter:在python中创建ELF文件的非常简单的库
2021-07-09 09:19:09pyelfwriter 这是一个非常简单的库,用于... 所有其他(pyelftools、pydevtools、pylibelf)python ELF 库更适合读取现有的 ELF 文件,但它们都不允许从头开始创建 ELF 文件。 有关用法,请参阅文件末尾的测试示例。 -
C#的elf文件解析库
2019-01-10 23:31:19nupkg格式的C#库文件,专门用来解析elf类文件的,使用的时候先加载安装这个库,怎么安装自行百度C# nupkg, 然后using包含ELFSharp.ELF.XXX的命名空间,然后就可以操作对应的函数了, 实例: var elf = ELFReader.... -
Elf文件解析工具v1.5
2021-03-10 00:04:05支持keil、ccs、gcc等编译器产生的elf文件。支持全部类型elf、coredump文件的program、section、symbol等信息解析。 2021更新内容: 1. 增加rel、rela重定位文件解析。 2. 增加note 程序段解析。 3. 丰富symbol搜索... -
《ELF文件格式分析.pdf》与elf解析代码
2018-03-22 18:06:12《ELF文件格式分析.pdf》文档,非常不错的elf格式参考文档,参考elf解析过程,能很快掌握elf文件格式 -
Elf-Parser:python中的可执行链接文件解析器。 返回访问ELF部分所需的地址。 用于识别ELF文件的动态链接库
2021-05-17 09:47:34小精灵包解析可执行链接文件(Unix ELF二进制文件)。 有关一般参考,请参见 。 脚本用法elf.py <输入ELF文件名>特征32位和64位ELF解析ELF类,方便抽象ElfBytes类用于字节级操作简要检查动态链接依赖关系查找 -
Linux系统下的ELF文件分析
2020-08-14 11:27:38ELF(Executable and Linkable Format)即可执行连接文件格式,是Linux,SVR4和Solaris2.0默认的目标文件格式,目前标准接口委员...分析elf文件有助于理解一些重要的系统概念,例如程序的编译和链接,程序的加载和运行等 -
西北工业大学操作系统实验 解析ELF文件
2022-04-02 22:14:53西北工业大学操作系统实验 解析ELF文件 -
ELF文件格式分析.pdf-cdeKey_T6AII5KBAOOQAHXK5YFNNSEQU36YU76U.pdf
2020-04-26 20:44:41本文 是作者在探索系统软件构件的复用技术的过程中生成的技术笔记,重点分析了 UNIX 类操作系统中普遍采用的目标文件格式 ELF(Executable and Linkable Format),目的是研究操作系统中二进制级软件构件的静态、动态... -
Elf文件格式
2017-11-01 11:59:14Elf文件格式中文版.Android逆向之旅---SO(ELF)文件格式详解 -
ELF文件信息隐藏.zip
2019-12-30 22:17:54详细分析了Linux平台下标准的可执行文件格式ELF(Executable and Linking Format),并针对ELF文件提出了一系列可以用来信息隐藏的方案,利用MATLAB等工具,通过实验进行验证,并对这些方案进行优缺点分析。... -
ELF文件格式简介
2022-05-03 23:47:05简单了解下ELF文件的格式。 1 简介 可执行与可链接格式 (Executable and Linkable Format,ELF),常被称为 ELF格式,是一种用于可执行文件、目标代码、共享库和核心转储(core dump)的标准文件格式,...简单了解下ELF文件的格式。
1 简介
可执行与可链接格式 (Executable and Linkable Format,ELF),常被称为 ELF格式,是一种用于可执行文件、目标代码、共享库和核心转储(core dump)的标准文件格式,一般用于类Unix系统,比如Linux,Macox等。ELF 格式灵活性高、可扩展,并且跨平台。比如它支持不同的字节序和地址范围,所以它不会不兼容某一特别的 CPU 或指令架构。这也使得 ELF 格式能够被运行于众多不同平台的各种操作系统所广泛采纳。
ELF文件一般由三种类型的文件:- 可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。比如编译的中间产物
.o
文件; - 可执行文件:一个可执行文件;
- 共享目标文件:共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。比如linux下的
.so
文件。
2 ELF文件格式
在编译过程中ELF文件格式在链接和程序的运行阶段的格式不同。链接阶段每个
.o
文件都是一个独立的ELF文件,为了效率和便利性他们的段需要进行合并才能生成对应的可执行文件。
ELF文件包含一个Header描述文件的基本信息;程序头表告诉徐彤如何构建进程的内存镜像,因此只有可执行文件由程序头表;Sections描述了链接过程中的需要的符号表、数据、指令等信息,而在可执行文件中是Segments,是经过合并的Secitons;节/段头表指明了对应section/segment在文件中的偏移,链接阶段的ELF文件必须包含该表头;而每个节/段头描述了对应的section/segment的大小,入口等基本信息。
下图是32bit系统下面使用的字段的数大小,64bit系统类似,之后不在赘述。
2.1 ELF Header
ELF文件头描述了ELF文件的基本类型,地址偏移等信息,分为32bit和64bit两个版本,定义于linux源码的
/usr/include/elf.h
文件中。#define EI_NIDENT 16 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;
从上面的结构中能够看出32bit和64bit的区别仅仅是字长的区别,字段上没有实际上的差别。每个字段的含义如下:
e_ident
:ELF文件的描述,是一个16字节的标识,表明当前文件的数据格式,位数等:- [0,3]字节为魔数,即
e_ident[EI_MAG0-EI_MAG3]
,取值为固定的0x7f E L F
,标记当前文件为一个ELF文件; - [4,4]字节为
EI_CLASS
即e_ident[EI_CLASS]
,表明当前文件的类别:- 0:表示非法的类别;
- 1:表示32bit;
- 2:表示64bit;
- [5,5]字节为
EI_DATA
即e_ident[EI_DATA]
,表明当期那文件的数据排列方式:- 0表示非法;
- 1表示小端;
- 2表示大端;
- [6,6]字节为
EI_VERSION
即e_ident[EI_VERSION]
,表明当前文件的版本,目前该取值必须为EV_CURRENT
即1; - [7,7]字节为
EI_PAD
即e_ident[EI_PAD]
表明e_ident
中未使用的字节的起点(值是相对于e_ident[EI_PAD+1]
的偏移),未使用的字节会被初始化为0,解析ELF文件时需要忽略对应的字段;
- [0,3]字节为魔数,即
EI_MAG0,EI_MAG1,EI_MAG2,EI_MAG3,EI_CLASS,EI_DATA,EI_VERSION,EI_OSABI,EI_PAD是linux源码中定义的宏,取值分别为0-7,分别对应各个字段的下标;下面的宏定义将采用类似EI_MAG0(0)的方式,表示EI_MAG0的值为0。
e_type
:文件的标识字段标识文件的类型;ET_NONE(0)
:未知的文件格式;ET_REL(1)
:可重定位文件,比如目标文件;ET_EXEC(2)
:可执行文件;ET_DYN(3)
:共享目标文件;ET_CORE(4)
:Core转储文件,比如程序crash之后的转储文件;ET_LOPROC(0xff00)
:特定处理器的文件标识;ET_HIPROC(0xffff)
:特定处理器的文件标识;[ET_LOPROC,ET_HIPROC]
之间的值用来表示特定处理器的文件格式;
e_machine
:目标文件的体系结构(下面列举了少数处理器架构,具体ELF文件支持的架构在对应的文件中查看即可);ET_NONE(0)
:未知的处理器架构;EM_M32(1)
:AT&T WE 32100;EM_SPARC(2)
:SPARC;EM_386(3)
:Intel 80386;EM_68K(4)
:Motorola 68000;EM_88K(5)
:Motorola 88000;EM_860(6)
:Intel 80860;EM_MIPS(7)
:MIPS RS3000大端;EM_MIPS_RS4_BE(10)
:MIPS RS4000大端;- 其他,预留;
e_version
:当前文件的版本;EV_NONE(0)
:非法的版本;EV_CURRENT(`)
:当前版本;
e_entry
:程序的虚拟入口地址,如果文件没有对应的入口可以为0;e_phoff
:文件中程序头表的偏移(bytes),如果文件没有该项,则应该为0;e_shoff
:文件中段表/节表的偏移(bytes),如果文件没有该项,则应该为0;e_flags
:处理器相关的标志位,宏格式为EF_machine_flag
比如EF_MIPS_PIC
;e_ehsize
:ELF文件头的大小(bytes);e_phentsize
:程序头表中单项的大小,表中每一项的大小相同;e_phnum
:程序头表中的项数,也就是说程序头表的实际大小为ephentsize x e_phnum
,如果文件中没有程序头表该项为0;e_shentsize
:节表中单项的大小,表中每一项的大小相同;e_shnum
:节表中项的数量;e_shstrndx
:节表中节名的索引,如果文件没有该表则该项为SHN_UNDEF(0)
。
2.2 程序头表(Program Header Table)
可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段 或者系统准备程序执行所必需的其它信息。程序头表描述了ELF文件中Segment在文件中的布局,描述了OS该如何装载可执行文件到内存。程序头表的表项的描述如下,类似于ELF Header也有32和64位两个版本。
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;
p_type
:当前Segment的类型;PT_NULL(0)
:当前项未使用,项中的成员是未定义的,需要忽略当前项;PT_LOAD(1)
:当前Segment是一个可装载的Segment,即可以被装载映射到内存中,其大小由p_filesz
和p_memsz
描述。如果p_memsz>p_filesz
则剩余的字节被置零,但是p_filesz>p_memsz
是非法的。动态库一般包含两个该类型的段:代码段和数据段;PT_DYNAMIC(2)
:动态段,动态库特有的段,包含了动态链接必须的一些信息,比如需要链接的共享库列表、GOT等等;PT_INTERP(3)
:当前段用于存储一段以NULL为结尾的字符串,该字符串表明了程序解释器的位置。且当前段仅仅对于可执行文件有实际意义,一个可执行文件中不能出现两个当前段,如果一个文件中包含当前段。比如/lib64/ld-linux-x86-64.so.2
;PT_NOTE(4)
:用于保存与特定供应商或者系统相关的附加信息以便于兼容性、一致性检查,但是实际上只保存了操作系统的规范信息;PT_SHLIB(5)
:保留段;PT_PHDR(6)
:保存程序头表本身的位置和大小,当前段不能在文件中出现一次以上,且仅仅当程序表头为内存映像的一部分时起作用,它必须在所有加载项目之前;[PT_LPROC(0x70000000),PT_HIPROC(0x7fffffff)]
:该范围内的值用作预留;
p_offset
:当前段相对于文件起始位置的偏移量;p_vaddr
:段的第一个字节将被映射到到内存中的虚拟地址;p_paddr
:此成员仅用于与物理地址相关的系统中。因为 System V 忽略所有应用程序的物理地址信息,此字段对与可执行文件和共享目标文件而言具体内容是指定的;p_filesz
:段在文件映像中所占的字节数,可能为 0;p_memsz
:段在内存映像中占用的字节数,可能为 0;p_flags
:段相关的标志;p_align
:段在文件中和内存中如何对齐。可加载的进程段的p_vaddr
和-p_offset
取值必须合适,相对于对页面大小的取模而言;- 0和1表示不需要对齐;
- 其他值必须为2的幂次方,且必须 p _ a d d r ∣ p _ a l i g n = = p _ o f f s e t ∣ p a l i g n p\_addr|p\_align==p\_offset| p_align p_addr∣p_align==p_offset∣palign。
2.3 节头表(Section Header Table)
节头表描述了ELF文件中的节的基本信息。可执行文件不一定由节头表但是一定有节,节头表可利用特殊的方式去除。
段和节的区别是:
- 段包含了程序装载可执行的基本信息,段告诉OS如何装载当前段到虚拟内存以及当前段的权限等和执行相关的信息,一个段可以包含0个或多个节;
- 节包含了程序的代码和数据等内容,链接器会将多个节合并为一个段。
typedef struct elf32_shdr { 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;
sh_name
:值是节名称在字符串表中的索引;sh_type
:描述节的类型和语义;SHT_NULL(0)
:当前节是非活跃的,没有一个对应的具体的节内存;SHT_PROGBITS(1)
:包含了程序的指令信息、数据等程序运行相关的信息;SHT_SYMTAB(2)
:保存了符号信息,用于重定位;- 此种类型节的
sh_link
存储相关字符串表的节索引,sh_info
存储最后一个局部符号的符号表索引+1;
- 此种类型节的
SHT_DYNSYM(11)
:保存共享库导入动态符号信息;- 此种类型节的
sh_link
存储相关字符串表的节索引,sh_info
存储最后一个局部符号的符号表索引+1;
- 此种类型节的
SHT_STRTAB(3)
:一个字符串表,保存了每个节的节名称;SHT_RELA(4)
:存储可重定位表项,可能会有附加内容,目标文件可能有多个可重定位表项;- 此种类型节的
sh_link
存储相关符号表的节索引,sh_info
存储重定位所使用节的索引;
- 此种类型节的
SHT_HASH(5)
:存储符号哈希表,所有参与动态链接的目标只能包含一个哈希表,一个目标文件只能包含一个哈希表;- 此种类型节的
sh_link
存储哈希表所使用的符号表的节索引,sh_info
为0;
- 此种类型节的
SHT_DYAMIC(6)
:存储包含动态链接的信息,一个目标文件只能包含一个;- 此种类型的节的
sh_link
存储当前节中使用到的字符串表格的节的索引,sh_info
为0;
- 此种类型的节的
SHT_NOTE(7)
:存储以某种形式标记文件的信息;SHT_NOBITS(8)
:这种类型的节不占据文件空间,但是成员sh_offset
依然会包含对应的偏移;SHT_REL(9)
:包含可重定位表项,无附加内容,目标文件可能有多个可重定位表项;- 此种类型节的
sh_link
存储相关符号表的节索引,sh_info
存储重定位所使用节的索引;
- 此种类型节的
SHT_SHLIB(10)
:保留区,包含此节的程序与ABI不兼容;[SHT_LOPROC(0x70000000),SHT_HIPROC(0x7fffffff)]
:留给处理器专用语义;[SHT_LOUSER(0x80000000),SHT_HIUSER(0xffffffff)]
:预留;
sh_flags
:1bit位的标志位;SHF_WRITE(0x1)
:当前节包含进程执行过程中可写的数据;SHF_ALLOC(0x2)
:当前节在运行阶段占据内存;SHF_EXECINSTR(0x4)
:当前节包含可执行的机器指令;SHF_MASKPROC(0xf0000000)
:所有包含当前掩码都表示预留给特定处理器的;
sh_addr
:如果当前节需要被装载到内存,则当前项存储当前节映射到内存的首地址,否则应该为0;sh_offset
:当前节的首地址相对于文件的偏移;sh_size
:节的大小。但是对于类型为SHT_NOBITS
的节,当前值可能不为0但是在文件中不占据任何空间;sh_link
:存储节投标中的索引,表示当前节依赖于对应的节。对于特定的节有特定的含义,其他为SHN_UNDEF
;sh_info
:节的附加信息。对于特定的节有特定的含义,其他为0
;sh_addralign
:地址约束对齐,值应该为0或者2的幂次方,0和1表示未进行对齐;sh_entsize
:某些节是一个数组,对于这类节当前字段给出数组中每个项的字节数,比如符号表。如果节并不包含对应的数组,值应该为0。
2.3 一些特殊的节
ELF文件中有一些预定义的节来保存程序、数据和一些控制信息,这些节被用来链接或者装载程序。每个操作系统都支持一组链接模式,主要分为两类(也就是常说的动态库和静态库):
- Static:静态绑定的一组目标文件、系统库和库档案(比如静态库),解析包含的符号引用并创建一个完全自包含的可执行文件;
- Dynamic:一组目标文件、库、系统共享资源和其他共享库链接在一起创建可执行文件。当加载此可执行文件时必须使系统中其他共享资源和动态库可用,程序才能正常运行。
库文件无论是动态库还是静态库在其文件中都包含对应的节,一些特殊的节其功能如下:
.bss
,类型SHT_NOBITS
,属性SHF_ALLOC|SHF_WRITE
:存储未经初始化的数据。根据定义程序开始执行时,系统会将这些数据初始化为0,且此节不占用文件空间;.comment
,类型SHT_PROGBITS
,属性none
:存储版本控制信息;.data
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_WRITE
:存放初始化的数据;.data1
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_WRITE
:存放初始化的数据;.debug
,类型SHT_PROGBITS
,属性none
:存放用于符号调试的信息;.dynamic
,类型SHT_DYNAMIC
,属性SHF_ALLOC
,是否有属性SHF_WRITE
屈居于处理器:包含动态链接的信息,.hash
,类型SHT_HASH
,属性SHF_ALLOC
:.line
,类型SHT_PROGBITS
,属性none
:存储调试的行号信息,描述源代码和机器码之间的对应关系;.note
,类型SHT_NOTE
,属性none
:.rodata
,类型SHT_PROGBITS
,属性SHF_ALLOC
:存储只读数据;.rodata1
,类型SHT_PROGBITS
,属性SHF_ALLOC
:存储只读数据;.shstrtab
,类型SHT_STRTAB
,属性none
:存储节的名称;.strtab
,类型SHT_STRTAB
:存储常见的与符号表关联的字符串。如果文件有一个包含符号字符串表的可加载段,则该段的属性将包括 SHF_ALLOC 位; 否则,该位将关闭;.symtab
,类型SHT_SYMTAB
,属性``````:存储一个符号表。如果文件具有包含符号表的可加载段,则该节的属性将包括 SHF_ALLOC 位;否则,该位将关闭;.text
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_EXECINSTR
:存储程序的代码指令;.dynstr
,类型SHT_STRTAB
,属性SHF_ALLOC
:存储动态链接所需的字符串,最常见的是表示与符号表条目关联的名称的字符串;.dynsym
,类型SHT_DYNSYM
,属性SHF_ALLOC
:存储动态链接符号表;.fini
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_EXECINSTR
:存储有助于进程终止代码的可执行指令。 当程序正常退出时,系统执行本节代码;.init
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_EXECINSTR
:存储有助于进程初始化代码的可执行指令。 当程序开始运行时,系统会在调用主程序入口点(C 程序称为 main)之前执行本节中的代码;.interp
,类型SHT_PROGBITS
:保存程序解释器的路径名。 如果文件有一个包含该节的可加载段,则该节的属性将包括 SHF_ALLOC 位; 否则,该位将关闭;.relname
,类型SHT_REL
:包含重定位信息。如果文件具有包含重定位的可加载段,则这些部分的属性将包括 SHF_ALLOC 位;否则,该位将关闭。通常,名称由 重定位适用的部分。因此.text
的重定位部分通常具有名称.rel.text
或.rela.text
;.relaname
,类型SHT_RELA
:同relname
。- 其他:对于C++程序有些版本会有
.ctors
(有时也会是.init_array
,见Can’t find .dtors and .ctors in binary)和dtors
两个节存储构造和析构相关的代码。
带有点 (.) 前缀的部分名称是为系统保留的,但如果它们的现有含义令人满意,应用程序可以使用这些部分。 应用程序可以使用不带前缀的名称以避免与系统部分冲突。 目标文件格式允许定义不在上面列表中的部分。 一个目标文件可能有多个同名的部分。
2.4 字符串表
字符串表是一个存储字符串的表格,而每个字符串是以NULL也就是
\0
为结尾的。字符串表格中索引为0处的字符串被定义为空字符串。符号表中保存的字符串是节名和目标文件中使用到的符号。而需要使用对应字符串时,只需要在需要使用的地方指明对应字符在字符串表中的索引即可,使用的字符串就是索引处到第一个\0
之间的字符串。
2.5 符号表
目标文件的符号表包含定位和重定位程序的符号定义和引用所需的信息。符号表索引是该数组的下标。索引0既指定表中的第一个条目,又用作未定义的符号索引。
typedef struct elf32_sym{ Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym;
typedef struct elf64_sym { Elf64_Word st_name; /* Symbol name, index in string tbl */ unsigned char st_info; /* Type and binding attributes */ unsigned char st_other; /* No defined meaning, 0 */ Elf64_Half st_shndx; /* Associated section index */ Elf64_Addr st_value; /* Value of the symbol */ Elf64_Xword st_size; /* Associated symbol size */ } Elf64_Sym;
st_name
:存储一个指向字符串表的索引来表示对应符号的名称;st_value
:存储对应符号的取值,具体值依赖于上下文,可能是一个指针地址,立即数等。另外,不同对象文件类型的符号表条目对 st_value 成员的解释略有不同:- 在重定位文件中在可重定位文件中,
st_value
保存节索引为SHN_COMMON
的符号的对齐约束; - 在可重定位文件中,
st_value
保存已定义符号的节偏移量。 也就是说,st_value
是从st_shndx
标识的部分的开头的偏移量; - 在可执行文件和共享对象文件中,
st_value
保存一个虚拟地址。 为了使这些文件的符号对动态链接器更有用,节偏移(文件解释)让位于与节号无关的虚拟地址(内存解释)。
- 在重定位文件中在可重定位文件中,
st_size
:符号的大小,具体指为sizeof(instance)
,如果未知则为0;st_info
:指定符号的类型和绑定属性。可以用下面的代码分别解析出bind,type,info
三个属性:
#define ELF32_ST_BIND(i) ((i)>>4) #define ELF32_ST_TYPE(i) ((i)&0xf) #define ELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf))
BIND
STB_LOCAL(0)
:局部符号在包含其定义的目标文件之外是不可见的。 同名的本地符号可以存在于多个文件中,互不干扰;STB_GLOBAL(1)
:全局符号对所有正在组合的目标文件都是可见的。 一个文件对全局符号的定义将满足另一个文件对同一全局符号的未定义引用;STB_WEAK(2)
:弱符号类似于全局符号,但它们的定义具有较低的优先级;[STB_LOPROC(13),STB_HIPROC(15)]
:预留位,用于特殊处理器的特定含义;
TYPE
:STT_NOTYPE(0)
:符号的类型未指定;STT_OBJECT(1)
:符号与数据对象相关联,例如变量、数组等;STT_FUNC(2)
:符号与函数或其他可执行代码相关联;STT_SECTION(3)
:该符号与一个节相关联。 这种类型的符号表条目主要用于重定位,通常具有STB_LOCAL
BIND属性;STT_FILE(4)
:一个有STB_LOCAL
的BIND属性的文件符号的节索引为SHN_ABS
。并且如果存在其他STB_LOCAL
属性的符号,则当前符号应该在其之前;[STT_LOPROC(13),STT_HIPROC(15)]
:预留位,用于特殊处理器的特定含义;
INFO
:SHN_ABS
:符号有一个绝对值,不会因为重定位而改变;SHN_COMMON
:该符号标记尚未分配的公共块。 符号的值给出了对齐约束,类似于节的 sh_addralign 成员。 也就是说,链接编辑器将为符号分配存储空间,该地址是 st_value 的倍数。 符号的大小表明需要多少字节;SHN_UNDEF
:此节表索引表示该符号未定义。 当链接编辑器将此对象文件与另一个定义指定符号的文件组合时,此文件对符号的引用将链接到实际定义;
st_other
:该成员当前持有 0 并且没有定义的含义;st_shndx
:每个符号都有属于的节,当前成员存储的就是对应节的索引。
3 ELF文件示例
下面是使用下面的代码编译生成动态库
libadd.so
作为示例://add.h int add(int a, int b); static int mult(int a, int b);
//add.c //编译命令gcc add.c -shared -o libadd.so extern int extern_value; static int static_value = 1; static int static_value1; int add(int a, int b){ return 0; } static int mult(int a, int b){ return 1; }
3.1 ELF Header
使用命令
readelf -h <ELF文件名>
查看ELF文件的Header。//readelf -h libadd.so 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 (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x4a0 Start of program headers: 64 (bytes into file) Start of section headers: 6000 (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: 24 Section header string table index: 23
从上面的Magic Number中能够看出:当前文件类型为64bit的共享库,小端存储,版本为1,机器架构为x86-64,程序头表项有7项,节头表项有24项。
3.2 Program Header Table
使用命令
readelf -l <ELF文件名>
查看程序头表;//readelf -l libadd.so Elf file type is DYN (Shared object file) Entry point 0x4a0 There are 7 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000674 0x0000000000000674 R E 0x200000 LOAD 0x0000000000000e80 0x0000000000200e80 0x0000000000200e80 0x00000000000001a4 0x00000000000001b0 RW 0x200000 DYNAMIC 0x0000000000000e90 0x0000000000200e90 0x0000000000200e90 0x0000000000000150 0x0000000000000150 RW 0x8 NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8 0x0000000000000024 0x0000000000000024 R 0x4 GNU_EH_FRAME 0x00000000000005a8 0x00000000000005a8 0x00000000000005a8 0x000000000000002c 0x000000000000002c R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000000e80 0x0000000000200e80 0x0000000000200e80 0x0000000000000180 0x0000000000000180 R 0x1 Section to Segment mapping: Segment Sections... 00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .rela.dyn .init .plt .plt.got .text .fini .eh_frame_hdr .eh_frame 01 .init_array .fini_array .dynamic .got .got.plt .data .bss 02 .dynamic 03 .note.gnu.build-id 04 .eh_frame_hdr 05 06 .init_array .fini_array .dynamic .got
从上面看出上半部分的内容基本和程序头表项的每个字段基本对应。从下面的Segment Sections可以看出一个Segment是多个Section的集合。
3.3 Section Header Table
使用命令
readelf -S <ELF文件名>
查看节头表的内容。➜ tmp readelf -S libadd.so There are 24 section headers, starting at offset 0x1770: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.gnu.build-i NOTE 00000000000001c8 000001c8 0000000000000024 0000000000000000 A 0 0 4 [ 2] .gnu.hash GNU_HASH 00000000000001f0 000001f0 000000000000003c 0000000000000000 A 3 0 8 [ 3] .dynsym DYNSYM 0000000000000230 00000230 0000000000000108 0000000000000018 A 4 1 8 [ 4] .dynstr STRTAB 0000000000000338 00000338 000000000000007d 0000000000000000 A 0 0 1 [ 5] .rela.dyn RELA 00000000000003b8 000003b8 00000000000000a8 0000000000000018 A 3 0 8 [ 6] .init PROGBITS 0000000000000460 00000460 0000000000000017 0000000000000000 AX 0 0 4 [ 7] .plt PROGBITS 0000000000000480 00000480 0000000000000010 0000000000000010 AX 0 0 16 [ 8] .plt.got PROGBITS 0000000000000490 00000490 0000000000000008 0000000000000008 AX 0 0 8 [ 9] .text PROGBITS 00000000000004a0 000004a0 00000000000000fc 0000000000000000 AX 0 0 16 [10] .fini PROGBITS 000000000000059c 0000059c 0000000000000009 0000000000000000 AX 0 0 4 [11] .eh_frame_hdr PROGBITS 00000000000005a8 000005a8 000000000000002c 0000000000000000 A 0 0 4 [12] .eh_frame PROGBITS 00000000000005d8 000005d8 000000000000009c 0000000000000000 A 0 0 8 [13] .init_array INIT_ARRAY 0000000000200e80 00000e80 0000000000000008 0000000000000008 WA 0 0 8 [14] .fini_array FINI_ARRAY 0000000000200e88 00000e88 0000000000000008 0000000000000008 WA 0 0 8 [15] .dynamic DYNAMIC 0000000000200e90 00000e90 0000000000000150 0000000000000010 WA 4 0 8 [16] .got PROGBITS 0000000000200fe0 00000fe0 0000000000000020 0000000000000008 WA 0 0 8 [17] .got.plt PROGBITS 0000000000201000 00001000 0000000000000018 0000000000000008 WA 0 0 8 [18] .data PROGBITS 0000000000201018 00001018 000000000000000c 0000000000000000 WA 0 0 8 [19] .bss NOBITS 0000000000201024 00001024 000000000000000c 0000000000000000 WA 0 0 4 [20] .comment PROGBITS 0000000000000000 00001024 0000000000000029 0000000000000001 MS 0 0 1 [21] .symtab SYMTAB 0000000000000000 00001050 00000000000004c8 0000000000000018 22 41 8 [22] .strtab STRTAB 0000000000000000 00001518 0000000000000193 0000000000000000 0 0 1 [23] .shstrtab STRTAB 0000000000000000 000016ab 00000000000000c3 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)
从上面看出内容基本和程序头表项的每个字段基本对应。除了上面提到的特殊的节也有一些额外的节,比如
.got.plt
。3.4 符号表
readelf -s <ELF文件名>
查看符号表。//readelf -s libadd.so Symbol table '.dynsym' contains 11 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __cxa_finalize 2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 5: 0000000000201024 0 NOTYPE GLOBAL DEFAULT 18 _edata 6: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 19 _end 7: 0000000000000460 0 FUNC GLOBAL DEFAULT 6 _init 8: 000000000000057a 17 FUNC GLOBAL DEFAULT 9 add 9: 0000000000201024 0 NOTYPE GLOBAL DEFAULT 19 __bss_start 10: 000000000000059c 0 FUNC GLOBAL DEFAULT 10 _fini Symbol table '.symtab' contains 51 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000000001c8 0 SECTION LOCAL DEFAULT 1 2: 00000000000001f0 0 SECTION LOCAL DEFAULT 2 3: 0000000000000230 0 SECTION LOCAL DEFAULT 3 4: 0000000000000338 0 SECTION LOCAL DEFAULT 4 5: 00000000000003b8 0 SECTION LOCAL DEFAULT 5 6: 0000000000000460 0 SECTION LOCAL DEFAULT 6 7: 0000000000000480 0 SECTION LOCAL DEFAULT 7 8: 0000000000000490 0 SECTION LOCAL DEFAULT 8 9: 00000000000004a0 0 SECTION LOCAL DEFAULT 9 10: 000000000000059c 0 SECTION LOCAL DEFAULT 10 11: 00000000000005a8 0 SECTION LOCAL DEFAULT 11 12: 00000000000005d8 0 SECTION LOCAL DEFAULT 12 13: 0000000000200e80 0 SECTION LOCAL DEFAULT 13 14: 0000000000200e88 0 SECTION LOCAL DEFAULT 14 15: 0000000000200e90 0 SECTION LOCAL DEFAULT 15 16: 0000000000200fe0 0 SECTION LOCAL DEFAULT 16 17: 0000000000201000 0 SECTION LOCAL DEFAULT 17 18: 0000000000201018 0 SECTION LOCAL DEFAULT 18 19: 0000000000201024 0 SECTION LOCAL DEFAULT 19 20: 0000000000000000 0 SECTION LOCAL DEFAULT 20 21: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 22: 00000000000004a0 0 FUNC LOCAL DEFAULT 9 deregister_tm_clones 23: 00000000000004e0 0 FUNC LOCAL DEFAULT 9 register_tm_clones 24: 0000000000000530 0 FUNC LOCAL DEFAULT 9 __do_global_dtors_aux 25: 0000000000201024 1 OBJECT LOCAL DEFAULT 19 completed.7698 26: 0000000000200e88 0 OBJECT LOCAL DEFAULT 14 __do_global_dtors_aux_fin 27: 0000000000000570 0 FUNC LOCAL DEFAULT 9 frame_dummy 28: 0000000000200e80 0 OBJECT LOCAL DEFAULT 13 __frame_dummy_init_array_ 29: 0000000000000000 0 FILE LOCAL DEFAULT ABS add.c 30: 0000000000201020 4 OBJECT LOCAL DEFAULT 18 static_value 31: 0000000000201028 4 OBJECT LOCAL DEFAULT 19 static_value1 32: 000000000000058b 17 FUNC LOCAL DEFAULT 9 mult 33: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 34: 0000000000000670 0 OBJECT LOCAL DEFAULT 12 __FRAME_END__ 35: 0000000000000000 0 FILE LOCAL DEFAULT ABS 36: 0000000000200e90 0 OBJECT LOCAL DEFAULT 15 _DYNAMIC 37: 0000000000201028 0 OBJECT LOCAL DEFAULT 18 __TMC_END__ 38: 0000000000201018 0 OBJECT LOCAL DEFAULT 18 __dso_handle 39: 00000000000005a8 0 NOTYPE LOCAL DEFAULT 11 __GNU_EH_FRAME_HDR 40: 0000000000201000 0 OBJECT LOCAL DEFAULT 17 _GLOBAL_OFFSET_TABLE_ 41: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __cxa_finalize 42: 0000000000000460 0 FUNC GLOBAL DEFAULT 6 _init 43: 000000000000057a 17 FUNC GLOBAL DEFAULT 9 add 44: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 45: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 46: 0000000000201024 0 NOTYPE GLOBAL DEFAULT 19 __bss_start 47: 000000000000059c 0 FUNC GLOBAL DEFAULT 10 _fini 48: 0000000000201024 0 NOTYPE GLOBAL DEFAULT 18 _edata 49: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 19 _end 50: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
符号表可以看出有两个分别为
dynsym
和symtab
,symtab
中包含所有在程序中出现的符号以及一些库函数的符号,而dynsym
中的符号是symtab
中符号的子集,仅仅出现了外部可以看到的符号(静态函数mult
的符号在dynsym
就看不到)。这是因为dynsym
中的符号只有在动态链接时也就是运行时才能被解析。4 参考文献
- 可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。比如编译的中间产物
-
ELF文件格式分析-北京大学操作系统实验室.pdf
2019-08-19 20:33:42ELF文件格式分析-北京大学操作系统实验室,ELF文件格式分析-北京大学操作系统实验室 -
ELF format / ELF文件格式
2021-03-16 13:48:382003/06/11 15:14 28,626 ELF文件格式(中文)(一) .txt 2003/09/25 18:17 24,614 ELF文件格式(中文)(三) .txt 2003/06/11 10:41 26,811 ELF文件格式(中文)(二) .txt 2004/10/22 20:42 36,137 从程序员角度看ELF.txt ... -
ELF文件解析器
2022-02-11 14:07:03前言 最近选了Linux内核原理的选修课,虽然因为课时比较短涉及到的内容只能涵盖Linux知识的一小部分,但是老师的水平确实很高,讲的知识也很深入,这次布置的小作业是编写Linux平台下的...1.ELF文件介绍 在 Linux 系全部代码:https://github.com/Kakaluoto/ELFReader
前言
最近选了Linux内核原理的选修课,虽然因为课时比较短涉及到的内容只能涵盖Linux知识的一小部分,但是老师的水平确实很高,讲的知识也很深入,这次布置的小作业是编写Linux平台下的C语言程序实现如下功能:
模仿实现Linux下readelf工具的部分功能,能够对ELF可执行文件进行简单分析。(至少支持readelf工具的-h、-S、-s三个命令选项功能)
关于原理部分自认为没有能力和大佬们讲得一样透彻清楚,所以参考文章链接贴在了文章的末尾,看了一定就会明白的。
1.ELF文件介绍
在 Linux 系统中,一个 ELF 文件主要用来表示 3 种类型的文件:
- 可执行文件:被操作系统中的加载器从硬盘上读取,载入到内存中去执行;
- 目标文件:被链接器读取,用来产生一个可执行文件或者共享库文件;
- 共享库文件:在动态链接的时候,由 ld-linux.so 来读取;
2.readelf命令
-a :--all 显示全部信息,等价于 -h -l -S -s -r -d -V -A -I -h :--file-header 显示elf文件开始的文件头信息. -l :--program-headers ;--segments 显示程序头(段头)信息(如果有的话)。 -S :--section-headers ;--sections 显示节头信息(如果有的话)。 -g :--section-groups 显示节组信息(如果有的话)。 -t :--section-details 显示节的详细信息(-S的)。 -s :--syms ;--symbols 显示符号表段中的项(如果有的话)。 -e :--headers 显示全部头信息,等价于: -h -l -S -n :--notes 显示note段(内核注释)的信息。 -r :--relocs 显示可重定位段的信息。 -u :--unwind 显示unwind段信息。当前只支持IA64 ELF的unwind段信息。 -d :--dynamic 显示动态段的信息。 -V :--version-info 显示版本段的信息。 -A :--arch-specific 显示CPU构架信息。 -D :--use-dynamic 使用动态段中的符号表显示符号,而不是使用符号段。 -x <number or name> :--hex-dump=<number or name> 以16进制方式显示指定段内内容。number指定段表中段的索引,或字符串指定文件中的段名。 -w[liaprmfFsoR]或者 -debugdump[=line,=info,=abbrev,=pubnames,=aranges, =macro,=frames,=frames-interp,=str,=loc,=Ranges] 显示调试段中指定的内容。 -I :--histogram 显示符号的时候,显示bucket list长度的柱状图。 -v :--version 显示readelf的版本信息。 -H :--help 显示readelf所支持的命令行选项。 -W :--wide 宽行输出。
3.代码实现
Kakaluoto/ELFReader (github.com)
这次我一共分成了5个文件header.h ; main.cpp ; readelf_symbol.cpp ; readelf_Section.cpp ; readelf_header.cpp
头文件 header.h
#include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <elf.h> #ifndef ELFREADER_HEADER_H #define ELFREADER_HEADER_H using namespace std; void readelf_h(const char* filename); void readelf_S(const char* filename); void readelf_s(const char* filename); #endif //ELFREADER_HEADER_H
主函数 main.cpp
#include "header.h" int main(int argc, char* argv[]) { if (argc < 2) { exit(0); } //argv[0]是当前可执行文件路径 //arv[1]是参数,参数以空格作为分隔符 //argv[2]是被解析的可执行文件名 if (strcmp(argv[1], "-h") == 0) readelf_h(argv[2]); else if (strcmp(argv[1], "-S") == 0) readelf_S(argv[2]); else if (strcmp(argv[1], "-s") == 0) readelf_s(argv[2]); else printf("invalid argument!\n"); return 0; }
模仿readelf -h 的实现readelf_header.cpp
#include "header.h" void readelf_h(const char* filename) { FILE* fp;//定义文件指针 Elf64_Ehdr elf_header;//定义elf头用来存储 fp = fopen(filename, "r"); if (fp == NULL) { exit(0); } fread(&elf_header, sizeof(Elf64_Ehdr), 1, fp);//读header if (elf_header.e_ident[0] != 0x7f || elf_header.e_ident[1] != 'E') { exit(0); }//判断是否是elf文件 printf("ELF Header:\n"); printf(" Magic:\t"); for (unsigned char i : elf_header.e_ident) { printf("%02x ", i); } printf("\n"); printf(" Class:\t\t\t\t"); switch (elf_header.e_ident[EI_CLASS]) { case 1: printf("ELF%d\n", 32); break; case 2: printf("ELF%d\n", 64); break; default: printf("Error!\n"); } printf(" Data:\t\t\t\t\t"); switch (elf_header.e_ident[EI_DATA]) { case 1: printf("2's complement, little endian\n"); break; case 2: printf("2's complement, big endian\n"); break; default: printf("Error!\n"); } printf(" Version:\t\t\t\t%d (current)\n", elf_header.e_ident[EI_VERSION]); printf(" OS/ABI:\t\t\t\t"); switch (elf_header.e_ident[EI_OSABI]) { case ELFOSABI_NONE: printf("UNIX System V ABI\n"); break; case ELFOSABI_HPUX: printf("HP-UX\n"); break; case ELFOSABI_NETBSD: printf("NetBSD.\n"); break; case ELFOSABI_GNU: printf("Object uses GNU ELF extensions.\n"); break; case ELFOSABI_SOLARIS: printf("Sun Solaris.\n"); break; case ELFOSABI_AIX: printf("IBM AIX.\n"); break; case ELFOSABI_IRIX: printf("SGI Irix.\n"); break; case ELFOSABI_FREEBSD: printf("FreeBSD.\n"); break; case ELFOSABI_TRU64: printf("Compaq TRU64 UNIX.\n"); break; case ELFOSABI_MODESTO: printf("Novell Modesto.\n"); break; case ELFOSABI_OPENBSD: printf("OpenBSD.\n"); break; case ELFOSABI_ARM_AEABI: printf("ARM EABI\n"); break; case ELFOSABI_ARM: printf("ARM\n"); break; case ELFOSABI_STANDALONE: printf("Standalone (embedded) application\n"); break; default: printf("Error!\n"); } printf(" ABI Version:\t\t\t\t%d\n", elf_header.e_ident[EI_ABIVERSION]); printf(" Type:\t\t\t\t\t"); switch (elf_header.e_type) { case ET_REL: printf("REL (Relocatable file)\n"); break; case ET_EXEC: printf("EXEC (Executable file)\n"); break; case ET_DYN: printf("DYN (Shared object file)\n"); break; default: printf("Error!\n"); } printf(" Machine:\t\t\t\t"); switch (elf_header.e_machine) { case EM_NONE: printf("No Machine!\n"); break; case EM_386: printf("Intel 80386\n"); break; case EM_860: printf("Intel 80860\n"); break; case EM_ARM: printf("ARM\n"); break; case EM_X86_64: printf("AMD x86-64 architecture\n"); break; case EM_AVR: printf("Atmel AVR 8-bit microcontroller\n"); break; case EM_MSP430: printf("Texas Instruments msp430\n"); break; case EM_ALTERA_NIOS2: printf("Altera Nios II\n"); break; case EM_MICROBLAZE: printf("Xilinx MicroBlaze\n"); break; case EM_8051: printf("Intel 8051 and variants\n"); break; case EM_STM8: printf("STMicroelectronics STM8\n"); break; case EM_CUDA: printf("NVIDIA CUDA\n"); break; case EM_AMDGPU: printf("AMD GPU\n"); break; case EM_RISCV: printf("RISC-V\n"); break; case EM_BPF: printf("Linux BPF -- in-kernel virtual machine\n"); break; default: printf("Unknown Machine!\n"); } printf(" Version:\t\t\t\t%x\n", elf_header.e_ident[EI_VERSION]); printf(" Entry point address:\t\t\t0x%016lx\n", elf_header.e_entry); printf(" Start of program headers:\t\t%ld (bytes into file)\n", elf_header.e_phoff); printf(" Start of section headers:\t\t%ld (bytes into file)\n", elf_header.e_shoff); printf(" Flags:\t\t\t\t0x%x\n", elf_header.e_flags); printf(" Size of this header:\t\t\t%d (bytes)\n", elf_header.e_ehsize); printf(" Size of program headers:\t\t%d (bytes)\n", elf_header.e_phentsize); printf(" Number of program headers:\t\t%d\n", elf_header.e_phnum); printf(" Size of section headers:\t\t%d (bytes)\n", elf_header.e_shentsize); printf(" Number of section headers:\t\t%d\n", elf_header.e_shnum); printf(" Section header string table index:\t%d\n", elf_header.e_shstrndx); }
模仿readelf -S 的实现readelf_Section.cpp
#include "header.h" void readelf_S(const char* filename) { FILE* fp; Elf64_Ehdr elf_header; fp = fopen(filename, "r"); if (fp == NULL) { exit(0); } fread(&elf_header, sizeof(Elf64_Ehdr), 1, fp); if (elf_header.e_ident[0] != 0x7f || elf_header.e_ident[1] != 'E') { exit(0); } //定义数组用来存储段表里每一个section_header,段的数目:elf_header.e_shnum Elf64_Shdr* sec_headers = new Elf64_Shdr[elf_header.e_shnum]; //将指针移动到段表起始地址,段起始地址elf_header.e_shoff即相对于整个elf文件的偏移量,SEEK_SET从文件起始开始偏移 fseek(fp, elf_header.e_shoff, SEEK_SET); //读section_header,每一个header大小为sizeof(Elf64_Shdr),一共读elf_header.e_shnum个段表头 fread(sec_headers, sizeof(Elf64_Shdr), elf_header.e_shnum, fp); printf("There are %d section headers, starting at offset 0x%lx\n\n", elf_header.e_shnum, elf_header.e_shoff); printf("Section Headers:\n"); int str_tab_ind = elf_header.e_shstrndx;//获取字符串表在段表中的索引elf_header.e_shstrndx,用来读取段名 fseek(fp, sec_headers[str_tab_ind].sh_offset, SEEK_SET);//将指针移动到字符串表 char* string_table = new char[sec_headers[str_tab_ind].sh_size];//构造字符数组用来存储字符串表里的字符 fread(string_table, 1, sec_headers[str_tab_ind].sh_size, fp);//将字符串表里面的字符全部读出来 //获取section段的类型,输入类型对应的数值,返回字符串型的类型名 auto get_sh_type = [](int sh_type, string& sec_header_name) { switch (sh_type) { case SHT_NULL: sec_header_name = "NULL"; break; case SHT_PROGBITS: sec_header_name = "PROGBITS"; break; case SHT_SYMTAB: sec_header_name = "SYMTAB"; break; case SHT_STRTAB: sec_header_name = "STRTAB"; break; case SHT_RELA: sec_header_name = "RELA"; break; case SHT_HASH: sec_header_name = "HASH"; break; case SHT_DYNAMIC: sec_header_name = "DYNAMIC"; break; case SHT_NOTE: sec_header_name = "NOTE"; break; case SHT_NOBITS: sec_header_name = "NOBITS"; break; case SHT_REL: sec_header_name = "REL"; break; case SHT_SHLIB: sec_header_name = "SHLIB"; break; case SHT_DYNSYM: sec_header_name = "DYNSYM"; break; case SHT_INIT_ARRAY: sec_header_name = "INIT_ARRAY"; break; case SHT_FINI_ARRAY: sec_header_name = "FINI_ARRAY"; break; case SHT_PREINIT_ARRAY: sec_header_name = "PREINIT_ARRAY"; break; case SHT_GROUP: sec_header_name = "GROUP"; break; case SHT_SYMTAB_SHNDX: sec_header_name = "SYMTAB_SHNDX"; break; case SHT_NUM: sec_header_name = "NUM"; break; case SHT_GNU_HASH: sec_header_name = "GNU_HASH"; break; case SHT_GNU_versym: sec_header_name = "VERSYM"; break; case SHT_GNU_verneed: sec_header_name = "VERNEED"; break; default: sec_header_name = "UnknownType"; } }; //获取section段的标志flag,输入类型对应的数值,返回字符串型的flag //因为可能同时满足多个flag,所以根据对应位是否为1来判断是否满足对应的flag满足就将flag字符串拼凑 auto get_sh_flags = [](unsigned int sh_flags, string& sec_header_name) { if ((sh_flags & SHF_WRITE) >> 0) sec_header_name += "W"; if ((sh_flags & SHF_ALLOC) >> 1) sec_header_name += "A"; if ((sh_flags & SHF_EXECINSTR) >> 2) sec_header_name += "X"; if ((sh_flags & SHF_MERGE) >> 4) sec_header_name += "M"; if ((sh_flags & SHF_STRINGS) >> 5) sec_header_name += "S"; if ((sh_flags & SHF_INFO_LINK) >> 6) sec_header_name += "I"; if ((sh_flags & SHF_LINK_ORDER) >> 7) sec_header_name += "L"; if ((sh_flags & SHF_OS_NONCONFORMING) >> 8) sec_header_name += "O"; if ((sh_flags & SHF_GROUP) >> 9) sec_header_name += "G"; if ((sh_flags & SHF_TLS) >> 10) sec_header_name += "T"; if ((sh_flags & SHF_COMPRESSED) >> 11) sec_header_name += "C"; //特殊flag因为对应的位和上面的flag对应的位不重叠,所以可以单独处理 switch (sh_flags) { case SHF_MASKOS: sec_header_name = "o"; break; case SHF_MASKPROC: sec_header_name = "p"; break; case SHF_EXCLUDE: sec_header_name = "E"; break; } }; printf(" [Nr]\tName\t\t\tType\t\tAddr\t\tOffset\t\tSize\t\t" "EntSize\t\tFlags\tLink\tInfo\tAlign\n"); //遍历section_headers段表里的每个section,输出相应的信息 for (int i = 0; i < elf_header.e_shnum; i++) { printf(" [%2d]\t", i); printf("%-24s", &string_table[sec_headers[i].sh_name]); string sh_type; get_sh_type(sec_headers[i].sh_type, sh_type); printf("%-16s", sh_type.data()); printf("0x%08lx\t", sec_headers[i].sh_addr); printf("0x%08lx\t", sec_headers[i].sh_offset); printf("0x%08lx\t", sec_headers[i].sh_size); printf("0x%08lx\t", sec_headers[i].sh_entsize); string sh_flags; get_sh_flags(sec_headers[i].sh_flags, sh_flags); printf("%-8s", sh_flags.data()); printf("%-8d", sec_headers[i].sh_link); printf("%-8d", sec_headers[i].sh_info); printf("%-8ld", sec_headers[i].sh_addralign); printf("\n"); } printf("Key to Flags:\n" "\tW (write), A (alloc), X (execute), M (merge), S (strings), I (info),\n" "\tL (link order), O (extra OS processing required), G (group), T (TLS),\n" "\tC (compressed), x (unknown), o (OS specific), E (exclude),\n" "\tl (large), p (processor specific)\n"); //释放堆内存 delete[] string_table; delete[] sec_headers; fclose(fp); }
模仿readelf -s 的实现readelf_symbol.cpp
#include "header.h" void readelf_s(const char* filename) { FILE* fp; Elf64_Ehdr elf_header; fp = fopen(filename, "r"); if (fp == NULL) { exit(0); } fread(&elf_header, sizeof(Elf64_Ehdr), 1, fp); if (elf_header.e_ident[0] != 0x7f || elf_header.e_ident[1] != 'E') { exit(0); } Elf64_Shdr* sec_headers = new Elf64_Shdr[elf_header.e_shnum];//存放每个section_header的数组 fseek(fp, elf_header.e_shoff, SEEK_SET);//移动指针到段表对应的偏移地址 fread(sec_headers, sizeof(Elf64_Shdr), elf_header.e_shnum, fp);//将段表数据读到开辟的数组sec_headers里 int str_tab_ind = elf_header.e_shstrndx;//获取字符串表.shstrtab在段表中的索引 fseek(fp, sec_headers[str_tab_ind].sh_offset, SEEK_SET);//移动指针到字符串表.shstrtab对应的偏移地址 char* string_table = new char[sec_headers[str_tab_ind].sh_size];//开辟堆内存用来存放字符串表.shstrtab fread(string_table, 1, sec_headers[str_tab_ind].sh_size, fp);//将字符串表.shstrtab对应地址处的数据读到字符串数组里 int dynsym_ind = -1;//默认.dynsym符号表索引为-1 int symtab_ind = -1;//默认.symtab符号表索引为-1 int dynstr_ind = -1;//默认.dynstr字符串表索引为-1 int strtab_ind = -1;//默认.strtab字符串索引为-1 //遍历段表section_headers获取符号表.dynsym;.symtab;.dynstr;.strtab四张表在段表中的索引 for (int i = 0; i < elf_header.e_shnum; i++) { if (sec_headers[i].sh_type == SHT_DYNSYM)//是.dynsym符号表 dynsym_ind = i; else if (sec_headers[i].sh_type == SHT_SYMTAB)//是.symtab符号表 symtab_ind = i; if (strcmp(&string_table[sec_headers[i].sh_name], ".strtab") == 0)//是.strtab字符串表 strtab_ind = i; else if (strcmp(&string_table[sec_headers[i].sh_name], ".dynstr") == 0)//是.dynstr字符串表 dynstr_ind = i; } //获取符号表entry对应的st_info段,用来计算符号类型和绑定信息 auto get_st_info = [](unsigned int st_info, string& symbol_type, string& symbol_binding) { unsigned char st_type = st_info & 0x0000000f;//低4位表示符号类型 unsigned char st_binding = (st_info & (~0x0000000f)) >> 4;//高28位表示符号绑定信息 switch (st_binding) { case STB_LOCAL: symbol_binding = "LOCAL"; break; case STB_GLOBAL: symbol_binding = "GLOBAL"; break; case STB_WEAK: symbol_binding = "WEAK"; break; case STB_NUM: symbol_binding = "NUM"; break; case STB_LOOS: symbol_binding = "LOOS"; break; case STB_HIOS: symbol_binding = "HIOS"; break; case STB_LOPROC: symbol_binding = "LOPROC"; break; case STB_HIPROC: symbol_binding = "HIPROC"; break; default: symbol_binding = "UnknownType"; } switch (st_type) { case STT_NOTYPE: symbol_type = "NOTYPE"; break; case STT_OBJECT: symbol_type = "OBJECT"; break; case STT_FUNC: symbol_type = "FUNC"; break; case STT_SECTION: symbol_type = "SECTION"; break; case STT_FILE: symbol_type = "FILE"; break; case STT_COMMON: symbol_type = "COMMON"; break; case STT_TLS: symbol_type = "TLS"; break; case STT_NUM: symbol_type = "NUM"; break; case STT_LOOS: symbol_type = "LOOS"; break; case STT_HIOS: symbol_type = "HIOS"; break; case STT_LOPROC: symbol_type = "LOPROC"; break; case STT_HIPROC: symbol_type = "HIPROC"; break; default: symbol_type = "UnknownBinding"; } }; //获取符号所在的段在段表的索引,并对特殊符号进行特殊处理 auto get_st_shndx = [](unsigned int st_shndx, string& Ndx) { switch (st_shndx) { case SHN_UNDEF: Ndx = "UNDEF";break; case SHN_COMMON: Ndx = "COMMON";break; case SHN_ABS: Ndx = "ABS";break; default: Ndx = to_string(st_shndx); } }; //输出符号表信息,输入符号表在段表中的索引sym_ind,符号表entry数目entry_num,符号表对应的字符串表string_table auto show_symbol_table = [&](int sym_ind, unsigned long entry_num, char* string_table) { fseek(fp, sec_headers[sym_ind].sh_offset, SEEK_SET);//将指针移动到符号表对应的偏移地址 Elf64_Sym* sym_entries = new Elf64_Sym[entry_num];//开辟堆内存用来存储符号表中所有entry fread(sym_entries, sizeof(Elf64_Sym), entry_num, fp);//读符号表 printf(" Num:\t\tValue\t\tSize\tType\tBind\tVis\t" "Ndx\t\tName\n"); //遍历符号表里的每个entry,并且输出entry的信息 for (int i = 0; i < entry_num; i++) { printf(" %3d:\t", i); printf("0x%016lx:\t", sym_entries[i].st_value); printf("%4ld\t", sym_entries[i].st_size); string symbol_type; string symbol_binding; get_st_info(sym_entries[i].st_info, symbol_type, symbol_binding); printf("%s\t", symbol_type.data()); printf("%s\t", symbol_binding.data()); printf("DEFAULT\t"); string Ndx; get_st_shndx(sym_entries[i].st_shndx, Ndx); printf("%4s\t", Ndx.data()); //根据entry的st_name属性在符号表对应的字符串表表里找到entry的name printf("%s", &string_table[sym_entries[i].st_name]); printf("\n"); } //释放堆内存 delete[] sym_entries; }; //如果.dynsym段存在,且.dynstr存在 if ((dynsym_ind != -1) && (dynstr_ind != -1)) { //符号表大小sec_headers[dynsym_ind].sh_size每个entry大小sec_headers[dynsym_ind].sh_entsize // 计算entry数目entry_num unsigned long entry_num = sec_headers[dynsym_ind].sh_size / sec_headers[dynsym_ind].sh_entsize; printf("Symbol table '.dynsym' contains %ld entries\n", entry_num); fseek(fp, sec_headers[dynstr_ind].sh_offset, SEEK_SET);//将指针移动到.dynstr字符串表对应的偏移地址 //开辟堆内存用来存储字符串表 char* dynstr_string_table = new char[sec_headers[dynstr_ind].sh_size]; //将数据读到字符串表里 fread(dynstr_string_table, 1, sec_headers[dynstr_ind].sh_size, fp); show_symbol_table(dynsym_ind, entry_num, dynstr_string_table); //释放字符串表 delete[] dynstr_string_table; } else { printf("No Dynamic linker symbol table!\n"); } printf("\n"); //如果.symtab段存在,且.strtab存在 if ((symtab_ind != -1) && (strtab_ind != -1)) { unsigned long entry_num = sec_headers[symtab_ind].sh_size / sec_headers[symtab_ind].sh_entsize; printf("Symbol table '.symtab' contains %ld entries\n", entry_num); fseek(fp, sec_headers[strtab_ind].sh_offset, SEEK_SET); char* strtab_string_table = new char[sec_headers[strtab_ind].sh_size]; fread(strtab_string_table, 1, sec_headers[strtab_ind].sh_size, fp); show_symbol_table(symtab_ind, entry_num, strtab_string_table); delete[] strtab_string_table; } else { printf("No symbol table!\n"); } //释放 delete[] string_table; delete[] sec_headers; fclose(fp); }
4.使用CMake-Make编译并执行
进入ELFReader所在目录执行如下命令即可
./ELFReader -h filename
./ELFReader -S filename
./ELFReader -s filename
其中filename是被解析的elf文件所在路径。
示例:
在build文件夹下执行cmake … 再make最后工程目录结构如下,ELFReader是elf文件解析器,libMylib.so共享库和helloworld可执行文件是待解析测试文件
. ├── build │ ├── bin │ │ ├── ELFReader │ │ ├── helloworld │ │ └── libMylib.so ...... ├── CMakeLists.txt ├── readme.md └── src ├── CMakeLists.txt ├── header.h ├── main.cpp ├── readefl_h.cpp ├── readelf_s.cpp └── readelf_S.cpp
根目录CMakeLists.txt
cmake_minimum_required(VERSION 3.10) project(ELFReader) set(CMAKE_CXX_STANDARD 11) ADD_SUBDIRECTORY(./src)
src目录下CMakeLists.txt
cmake_minimum_required(VERSION 3.10) SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) AUX_SOURCE_DIRECTORY(./ DIR_SRCS) ADD_EXECUTABLE(ELFReader ${DIR_SRCS})
进入bin目录并执行
cd build/bin/ ./ELFReader -h helloworld
输出:
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 ABI Version: 0 Type: DYN (Shared object file) Machine: AMD x86-64 architecture Version: 1 Entry point address: 0x00000000000010c0 Start of program headers: 64 (bytes into file) Start of section headers: 36440 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 13 Size of section headers: 64 (bytes) Number of section headers: 36 Section header string table index: 35
执行-S命令
./ELFReader -S helloworld
输出:
There are 36 section headers, starting at offset 0x8e58 Section Headers: [Nr] Name Type Addr Offset Size EntSize Flags Link Info Align [ 0] NULL 0x00000000 0x00000000 0x00000000 0x00000000 0 0 0 [ 1] .interp PROGBITS 0x00000318 0x00000318 0x0000001c 0x00000000 A 0 0 1 [ 2] .note.gnu.property NOTE 0x00000338 0x00000338 0x00000020 0x00000000 A 0 0 8 [ 3] .note.gnu.build-id NOTE 0x00000358 0x00000358 0x00000024 0x00000000 A 0 0 4 [ 4] .note.ABI-tag NOTE 0x0000037c 0x0000037c 0x00000020 0x00000000 A 0 0 4 [ 5] .gnu.hash GNU_HASH 0x000003a0 0x000003a0 0x00000028 0x00000000 A 6 0 8 [ 6] .dynsym DYNSYM 0x000003c8 0x000003c8 0x00000138 0x00000018 A 7 1 8 [ 7] .dynstr STRTAB 0x00000500 0x00000500 0x00000163 0x00000000 A 0 0 1 [ 8] .gnu.version VERSYM 0x00000664 0x00000664 0x0000001a 0x00000002 A 6 0 2 [ 9] .gnu.version_r VERNEED 0x00000680 0x00000680 0x00000040 0x00000000 A 7 2 8 [10] .rela.dyn RELA 0x000006c0 0x000006c0 0x00000120 0x00000018 A 6 0 8 [11] .rela.plt RELA 0x000007e0 0x000007e0 0x00000060 0x00000018 AI 6 24 8 [12] .init PROGBITS 0x00001000 0x00001000 0x0000001b 0x00000000 AX 0 0 4 [13] .plt PROGBITS 0x00001020 0x00001020 0x00000050 0x00000010 AX 0 0 16 [14] .plt.got PROGBITS 0x00001070 0x00001070 0x00000010 0x00000010 AX 0 0 16 [15] .plt.sec PROGBITS 0x00001080 0x00001080 0x00000040 0x00000010 AX 0 0 16 [16] .text PROGBITS 0x000010c0 0x000010c0 0x00000205 0x00000000 AX 0 0 16 [17] .fini PROGBITS 0x000012c8 0x000012c8 0x0000000d 0x00000000 AX 0 0 4 [18] .rodata PROGBITS 0x00002000 0x00002000 0x00000013 0x00000000 A 0 0 4 [19] .eh_frame_hdr PROGBITS 0x00002014 0x00002014 0x00000054 0x00000000 A 0 0 4 [20] .eh_frame PROGBITS 0x00002068 0x00002068 0x00000148 0x00000000 A 0 0 8 [21] .init_array INIT_ARRAY 0x00003d78 0x00002d78 0x00000010 0x00000008 WA 0 0 8 [22] .fini_array FINI_ARRAY 0x00003d88 0x00002d88 0x00000008 0x00000008 WA 0 0 8 [23] .dynamic DYNAMIC 0x00003d90 0x00002d90 0x00000200 0x00000010 WA 7 0 8 [24] .got PROGBITS 0x00003f90 0x00002f90 0x00000070 0x00000008 WA 0 0 8 [25] .data PROGBITS 0x00004000 0x00003000 0x00000010 0x00000000 WA 0 0 8 [26] .bss NOBITS 0x00004040 0x00003010 0x00000118 0x00000000 WA 0 0 64 [27] .comment PROGBITS 0x00000000 0x00003010 0x0000002a 0x00000001 MS 0 0 1 [28] .debug_aranges PROGBITS 0x00000000 0x0000303a 0x00000030 0x00000000 0 0 1 [29] .debug_info PROGBITS 0x00000000 0x0000306a 0x00002c34 0x00000000 0 0 1 [30] .debug_abbrev PROGBITS 0x00000000 0x00005c9e 0x00000649 0x00000000 0 0 1 [31] .debug_line PROGBITS 0x00000000 0x000062e7 0x00000406 0x00000000 0 0 1 [32] .debug_str PROGBITS 0x00000000 0x000066ed 0x00001b08 0x00000001 MS 0 0 1 [33] .symtab SYMTAB 0x00000000 0x000081f8 0x00000780 0x00000018 34 55 8 [34] .strtab STRTAB 0x00000000 0x00008978 0x00000381 0x00000000 0 0 1 [35] .shstrtab STRTAB 0x00000000 0x00008cf9 0x0000015a 0x00000000 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)
执行-s命令
./ELFReader -s helloworld
输出:
Symbol table '.dynsym' contains 13 entries Num: Value Size Type Bind Vis Ndx Name 0: 0x0000000000000000: 0 NOTYPE LOCAL DEFAULT UNDEF 1: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ 2: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF __cxa_atexit 3: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc 4: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZNSolsEPFRSoS_E 5: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZNSt8ios_base4InitC1Ev 6: 0x0000000000000000: 0 NOTYPE WEAK DEFAULT UNDEF _ITM_deregisterTMCloneTable 7: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF __libc_start_main 8: 0x0000000000000000: 0 NOTYPE WEAK DEFAULT UNDEF __gmon_start__ 9: 0x0000000000000000: 0 NOTYPE WEAK DEFAULT UNDEF _ITM_registerTMCloneTable 10: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZNSt8ios_base4InitD1Ev 11: 0x0000000000000000: 0 FUNC WEAK DEFAULT UNDEF __cxa_finalize 12: 0x0000000000004040: 272 OBJECT GLOBAL DEFAULT 26 _ZSt4cout Symbol table '.symtab' contains 80 entries Num: Value Size Type Bind Vis Ndx Name 0: 0x0000000000000000: 0 NOTYPE LOCAL DEFAULT UNDEF 1: 0x0000000000000318: 0 SECTION LOCAL DEFAULT 1 2: 0x0000000000000338: 0 SECTION LOCAL DEFAULT 2 3: 0x0000000000000358: 0 SECTION LOCAL DEFAULT 3 4: 0x000000000000037c: 0 SECTION LOCAL DEFAULT 4 5: 0x00000000000003a0: 0 SECTION LOCAL DEFAULT 5 6: 0x00000000000003c8: 0 SECTION LOCAL DEFAULT 6 7: 0x0000000000000500: 0 SECTION LOCAL DEFAULT 7 8: 0x0000000000000664: 0 SECTION LOCAL DEFAULT 8 9: 0x0000000000000680: 0 SECTION LOCAL DEFAULT 9 10: 0x00000000000006c0: 0 SECTION LOCAL DEFAULT 10 11: 0x00000000000007e0: 0 SECTION LOCAL DEFAULT 11 12: 0x0000000000001000: 0 SECTION LOCAL DEFAULT 12 13: 0x0000000000001020: 0 SECTION LOCAL DEFAULT 13 14: 0x0000000000001070: 0 SECTION LOCAL DEFAULT 14 15: 0x0000000000001080: 0 SECTION LOCAL DEFAULT 15 16: 0x00000000000010c0: 0 SECTION LOCAL DEFAULT 16 17: 0x00000000000012c8: 0 SECTION LOCAL DEFAULT 17 18: 0x0000000000002000: 0 SECTION LOCAL DEFAULT 18 19: 0x0000000000002014: 0 SECTION LOCAL DEFAULT 19 20: 0x0000000000002068: 0 SECTION LOCAL DEFAULT 20 21: 0x0000000000003d78: 0 SECTION LOCAL DEFAULT 21 22: 0x0000000000003d88: 0 SECTION LOCAL DEFAULT 22 23: 0x0000000000003d90: 0 SECTION LOCAL DEFAULT 23 24: 0x0000000000003f90: 0 SECTION LOCAL DEFAULT 24 25: 0x0000000000004000: 0 SECTION LOCAL DEFAULT 25 26: 0x0000000000004040: 0 SECTION LOCAL DEFAULT 26 27: 0x0000000000000000: 0 SECTION LOCAL DEFAULT 27 28: 0x0000000000000000: 0 SECTION LOCAL DEFAULT 28 29: 0x0000000000000000: 0 SECTION LOCAL DEFAULT 29 30: 0x0000000000000000: 0 SECTION LOCAL DEFAULT 30 31: 0x0000000000000000: 0 SECTION LOCAL DEFAULT 31 32: 0x0000000000000000: 0 SECTION LOCAL DEFAULT 32 33: 0x0000000000000000: 0 FILE LOCAL DEFAULT ABS crtstuff.c 34: 0x00000000000010f0: 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones 35: 0x0000000000001120: 0 FUNC LOCAL DEFAULT 16 register_tm_clones 36: 0x0000000000001160: 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux 37: 0x0000000000004150: 1 OBJECT LOCAL DEFAULT 26 completed.8060 38: 0x0000000000003d88: 0 OBJECT LOCAL DEFAULT 22 __do_global_dtors_aux_fini_array_entry 39: 0x00000000000011a0: 0 FUNC LOCAL DEFAULT 16 frame_dummy 40: 0x0000000000003d78: 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_init_array_entry 41: 0x0000000000000000: 0 FILE LOCAL DEFAULT ABS main.cpp 42: 0x0000000000002004: 1 OBJECT LOCAL DEFAULT 18 _ZStL19piecewise_construct 43: 0x0000000000004151: 1 OBJECT LOCAL DEFAULT 26 _ZStL8__ioinit 44: 0x00000000000011e0: 77 FUNC LOCAL DEFAULT 16 _Z41__static_initialization_and_destruction_0ii 45: 0x000000000000122d: 25 FUNC LOCAL DEFAULT 16 _GLOBAL__sub_I_main 46: 0x0000000000000000: 0 FILE LOCAL DEFAULT ABS crtstuff.c 47: 0x00000000000021ac: 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__ 48: 0x0000000000000000: 0 FILE LOCAL DEFAULT ABS 49: 0x0000000000002014: 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR 50: 0x0000000000001000: 0 FUNC LOCAL DEFAULT 12 _init 51: 0x0000000000003d90: 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC 52: 0x0000000000003d88: 0 NOTYPE LOCAL DEFAULT 21 __init_array_end 53: 0x0000000000003d78: 0 NOTYPE LOCAL DEFAULT 21 __init_array_start 54: 0x0000000000003f90: 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_ 55: 0x0000000000004010: 0 NOTYPE GLOBAL DEFAULT 25 _edata 56: 0x0000000000004000: 0 NOTYPE WEAK DEFAULT 25 data_start 57: 0x0000000000002000: 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used 58: 0x0000000000000000: 0 FUNC WEAK DEFAULT UNDEF __cxa_finalize@@GLIBC_2.2.5 59: 0x00000000000011a9: 55 FUNC GLOBAL DEFAULT 16 main 60: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4 61: 0x0000000000004008: 0 OBJECT GLOBAL DEFAULT 25 __dso_handle 62: 0x00000000000012c8: 0 FUNC GLOBAL DEFAULT 17 _fini 63: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF __cxa_atexit@@GLIBC_2.2.5 64: 0x00000000000010c0: 47 FUNC GLOBAL DEFAULT 16 _start 65: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@@GLIBCXX_3.4 66: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZNSolsEPFRSoS_E@@GLIBCXX_3.4 67: 0x0000000000004010: 0 OBJECT GLOBAL DEFAULT 25 __TMC_END__ 68: 0x0000000000004040: 272 OBJECT GLOBAL DEFAULT 26 _ZSt4cout@@GLIBCXX_3.4 69: 0x0000000000004000: 0 NOTYPE GLOBAL DEFAULT 25 __data_start 70: 0x0000000000004158: 0 NOTYPE GLOBAL DEFAULT 26 _end 71: 0x0000000000004010: 0 NOTYPE GLOBAL DEFAULT 26 __bss_start 72: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZNSt8ios_base4InitC1Ev@@GLIBCXX_3.4 73: 0x0000000000001250: 101 FUNC GLOBAL DEFAULT 16 __libc_csu_init 74: 0x0000000000000000: 0 NOTYPE WEAK DEFAULT UNDEF _ITM_deregisterTMCloneTable 75: 0x00000000000012c0: 5 FUNC GLOBAL DEFAULT 16 __libc_csu_fini 76: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF __libc_start_main@@GLIBC_2.2.5 77: 0x0000000000000000: 0 NOTYPE WEAK DEFAULT UNDEF __gmon_start__ 78: 0x0000000000000000: 0 NOTYPE WEAK DEFAULT UNDEF _ITM_registerTMCloneTable 79: 0x0000000000000000: 0 FUNC GLOBAL DEFAULT UNDEF _ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4
参考文章
ELF文件结构描述 - yooooooo - 博客园 (cnblogs.com)
Linux系统中编译、链接的基石-ELF文件:扒开它的层层外衣,从字节码的粒度来探索 (qq.com)
C/C++ 实现ELF结构解析工具 - lyshark - 博客园 (cnblogs.com)
C语言实现ELF文件解析_Yuhan的博客-CSDN博客_elf文件读取
《程序员的自我修养——链接、装载与库》俞甲子,石凡,潘爱民
-
【Android 逆向】ELF 文件格式 ( ELF 文件头 | ELF 文件头标志 | ELF 文件位数 | ELF 文件大小端格式 )
2021-10-27 00:12:29一、ELF 文件简介、 二、ELF 文件头、 三、ELF 文件头标志、 四、ELF 文件位数、 五、ELF 文件大小端格式
一、ELF 文件简介
在上一篇博客 【Android 逆向】ELF 文件格式 ( 安装 010 Editor 二进制查看工具的 ELF.bt 插件模板 | 安装 ELF.bt 模板 | 打开 ELF 文件 ) 中 , 准备 ELF 文件解析环境 , 在 010 Editor 中安装了 ELF.bt 模板 ;
二、ELF 文件头
ELF 文件头区域如下 :
前 16 字节是 ELF 的标志 ,
三、ELF 文件头标志
0 ~ 3 字节 : 是 0x7F 和 ELF 的 ASCII 码 ; 这是 ELF 文件的特征 ;
四、ELF 文件位数
4 字节 : 表示该 ELF 文件的位数 32 位还是 64 位 ; 值为 01 , 表示该 ELF 文件是 32 位 文件 ;
五、ELF 文件大小端格式
5 字节 : 表示 有效位 格式 , 取值 LSB / MSB ; 此处值为 1 , LSB 格式 ; 这个值由编译器决定 ; 有些 CPU 加载 LSB 值快 , 有些 CPU 加载 MSB 值快 ; 大部分 程序 使用 LSB 格式 ;
LSB 表示最低有效位 ( 小端格式 ) , MSB 表示最高有效位 ( 大端格式 ) ;
注意与 大端格式 / 小端格式 区分 , 概念不同 ; LSB 最低有效位 , 某种程度上等效于 小端格式 ;
-
Linux下的ELF文件格式简介.pdf
2021-09-07 00:54:55Linux下的ELF文件格式简介.pdf -
【Android 逆向】ELF 文件格式 ( ELF 文件简介 | ELF 文件结构 )
2021-10-26 22:46:46一、ELF 文件简介、 二、ELF 文件结构 -
ELF 文件格式
2019-02-22 10:27:22Elf文件最详细的介绍。也是最好的一个介绍elf文件格式的资料。 看过都说好。 -
Linux系统下的ELF文件分析.pdf
2021-09-06 23:50:45Linux系统下的ELF文件分析.pdf