ELF之格式解析
ELF文件类型
ELF主要包括三种类型文件
- 可重定位文件(relocatable):编译器和汇编器产生的.o文件,尚未会被Linker所处理,符号尚未被全局确认地址,不存在程序段(segments)
- 可执行文件(executable):通过Linker对所有的.o文件进行链接处理输出的文件,包括所有的符号,包括程序段(segments)和节区(sections)
- 共享对象文件(shared object):动态库文件.so,包括程序段(segments)和节区(sections),会对本地符号进行链接处理
可以通过 file 命令来查询类型,如下:
$ file ./cJSON.o
./cJSON.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), with debug_info, not stripped
$ file ./test.elf
./test.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
ELF文件布局
ELF文件从概念上来说包括了5个部分:
● ELF header:描述体系结构和操作系统等基本信息,指出Entry point address、section header table和program header table在文件的位置
● section header table:节区表,这个保存了所有的section的信息,这是从编译和链接的角度来看ELF文件的,主要是Linker链接时使用
● program header table:程序头表,这个是从运行的角度来看ELF文件的,主要给出了各个segment的信息,实际是多个sections合并后的结果
● sections:就是各个节区
● segments:就是在运行时的各个段
sections 和 segments 所在的位置是一样的,左边是链接视图,右边是加载视图,sections是程序员可见的,是给链接器使用的概念,而segments是程序员不可见的,是给加载器使用的概念,一般是一个segment包含多个sections。
ELF Header
使用如下命令查询文件的 ELF header
> readelf -h app.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: ARM
Version: 0x1
Entry point address: 0x1
Start of program headers: 0 (bytes into file)
Start of section headers: 2944 (bytes into file)
Flags: 0x5000000, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 11
Section header string table index: 10
代码中的结构体如下所示,给出了程序段和节区信息等:
/* ELF32 Header */
#define EI_NIDENT 16
typedef struct elf32_hdr {
/*ELF的一些标识信息,固定值*/
unsigned char e_ident[EI_NIDENT]; /* ELF identification */
/*目标文件类型:1-可重定位文件,2-可执行文件,3-共享目标文件等*/
Elf32_Half e_type; /* object file type */
/*文件的目标体系结构类型:ARM,X86-64*/
Elf32_Half e_machine; /* machine */
/*目标文件版本:1-当前版本*/
Elf32_Word e_version; /* object file version */
/*程序入口的虚拟地址,重定位文件中一定为0*/
Elf32_Addr e_entry; /* virtual entry point */
/*程序头表(segment header table)的偏移量,没有可为0*/
Elf32_Off e_phoff; /* program header table offset */
/*节区头表(section header table)的偏移量,没有可为0*/
Elf32_Off e_shoff; /* section header table offset */
/*与文件相关的,特定于处理器的标志*/
Elf32_Word e_flags; /* processor-specific flags */
/*ELF头部的大小,单位字节*/
Elf32_Half e_ehsize; /* ELF header size */
/*程序头表每个表项的大小,单位字节*/
Elf32_Half e_phentsize; /* program header entry size */
/*程序头表表项的个数*/
Elf32_Half e_phnum; /* number of program header entries */
/*节区头表每个表项的大小,单位字节*/
Elf32_Half e_shentsize; /* section header entry size */
/*节区头表表项的数目*/
Elf32_Half e_shnum; /* number of section header entries */
/*.shstrtab 段在段表中的索引位置*/
Elf32_Half e_shstrndx; /* index of ".shstrtab" in segment table */
} Elf32_Ehdr;
ELF 头中包含了程序运行的关键信息,如下:
- e_ident
在数组 e_ident[0~3] 的前4个字节中存储了 ELF 的magic number,分别为 0x7F、E、L、F,主要是用来识别是否为ELF 格式的文件,只有检测到这个魔数,才会进行下一步解析,否则直接退出报错。
名称 | 索引 | 作用 |
---|---|---|
EI_MAG0 | 0 | 文件标识 |
EI_MAG1 | 1 | 文件标识 |
EI_MAG2 | 2 | 文件标识 |
EI_MAG3 | 3 | 文件标识 |
EI_CLASS | 4 | 文件类 [ 1: ELFCLASS32; 2: ELFCLASS64 ] |
EI_DATA | 5 | 数据编码 [ 1: ELFDATA2LSB; 2: ELFDATA2MSB] |
EI_VERSION | 6 | 文件版本 |
EI_PAD | 7 | 补齐字节开始处 |
-
e_type
在ELF头中可以通过 e_type 成员获取文件的类型:- 可重定位类型:ET_REL = 1,也就是我们常见到 .o 文件,只是经过编译后产生的文件
- 可执行类型:ET_EXEC = 2,也就是我们常见的编译后的可以运行的文件,经过编译和完整的链接后产生的文件
- 共享库类型:ET_DYN = 3,也就是我们常见的 .so 文件,经过了编译和 -share 链接产生的文件
-
e_entry
- 可执行程序:指明程序运行时的需要跳转的第一条指令的位置
- 对于共享库文件:在没有指定入口地址的情况下,默认是 .text 的第一个函数
- 对于可重定位文件:默认为0,也可以使用 ld 在链接时使用 -e 来指明入口地址(多个.o 文件也可以使用 LD 工具 配合 -r -S 选项将多个 .o 文件打包成一个 .o 文件)
ELF Section Header Table
节区表是给连接器用来进行本地成员的重定位,节区表是一个数组,每个成员都是 ELF32_Shdr 类型,在 Elf32_Ehdr 中使用 e_shoff 给出节区表在重定位文件中相对于文件开始的偏移。
使用如下命令查询文件的 ELF section header table
> readelf -S app.o
There are 11 section headers, starting at offset 0xb80:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000038 000250 00 AX 0 0 8
[ 2] .rel.text REL 00000000 000a14 000118 08 I 8 1 4
[ 3] .rodata PROGBITS 00000250 000288 0001f8 01 AMS 0 0 1
[ 4] .data PROGBITS 00000448 000480 000000 00 WA 0 0 1
[ 5] .bss NOBITS 00000448 000480 000491 00 WA 0 0 1
[ 6] .comment PROGBITS 00000000 000480 0000fe 01 MS 0 0 1
[ 7] .ARM.attributes ARM_ATTRIBUTES 00000000 00057e 00003d 00 0 0 1
[ 8] .symtab SYMTAB 00000000 0005bc 0002f0 10 9 25 4
[ 9] .strtab STRTAB 00000000 0008ac 000165 00 0 0 1
[10] .shstrtab STRTAB 00000000 000b2c 000051 00 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),
y (purecode), p (processor specific)
代码中的结构体如下所示:
/* ELF32 Section Header */
typedef struct elf32_shdr {
/*节区名称,实际是一个索引,指向.shstrtab "节区名字表"某个位置*/
Elf32_Word sh_name; /* name - index into section header string table section */
/*节区类型:PROGBITS-程序信息,NOBITS-不占用文件空间(bss),REL-重定位表项*/
Elf32_Word sh_type; /* type */
/*权限掩码,表示节区是否载入内存、是否可读、可写、可执行等信息*/
Elf32_Word sh_flags; /* flags */
/*节区映射到内存的虚拟地址*/
Elf32_Addr sh_addr; /* address */
/*节区的第一个字节相对于文件头的偏移*/
Elf32_Off sh_offset; /* file offset */
/*节区的长度,单位字节,NOBITS类型时有大小但是在文件中不占用空间*/
Elf32_Word sh_size; /* section size */
/*本节所使用的符号表在节头表中的索引*/
Elf32_Word sh_link; /* section header table index link */
/*节区附加信息,sh_type=SHT_REL/SHT_RELA:对于该重定位表所作用的节在节头表中的下标*/
Elf32_Word sh_info; /* extra information */
/*指明节区内数据的对齐方式*/
Elf32_Word sh_addralign; /* address alignment */
/*某些节区中包含固定大小的项目,如符号表,指明其成员固定大小,为0则表示不包括固定大小*/
Elf32_Word sh_entsize; /* section entry size */
} Elf32_Shdr;
- sh_name
节名称 sh_name 并不是一个字符串,而是指向 .shstrtab "节区名字表"下的一个索引位置,这个索引可以从 .shstrtab 中获取当前节的字符串名称,常见节区名称和解释如下:
区名 描述说明
.bss 本区中保存初始化为0的全局和静态变量。保存未初始化的全局和静态变量,运行时载入内存并初始化为0,但是,
在目标文件中.bss区不占用空间,其长度为0,所以它的区类型为NOBITS。
.data 本区用于存放程序中被初始化过的全局变量。在目标文件中,占用实际的存储空间的,在运行时载入内存。
.rodata 本区保存了只读数据,可以读取但不能修改,可以加载到内存中使用,也可以在XIP模式下存储在 norFlash 上。。
.rodata.str1.x:本区保存只读字符串数据,目的是为了更紧凑或更符合CPU运行的方式存储字符串,使用-fno-merge-constants 可以取消优化,
而统一存储在.rodata中。
.text 本区包含程序指令代码。可以加载到内存中执行,也可以在XIP模式下在 norFlash 上运行。
.dynamic 本区包含动态链接信息。包括依赖于哪些共享目标文件、动态连接符号表的位置、动态连接重定位表的位置等,运行时载入内存,
具有SHF_ALLOC和SHF_WRITE属性。
.strtab 本区用于存放字符串,主要是那些符号表项的名字。其中含有符号名称,存储的是变量名,函数名等。
.symtab 本区用于存放符号表。其中含有动态链接过程中使用的符号和连接阶段已完成重定位的符号,无需载入内存。
.dynstr 本区含有用于动态链接的字符串节。包括了.dynsym中动态符号的名字字符串,具有SHF_ALLOC属性,运行是载入内存。
.dynsym 本区含有动态链接符号表。具有SHF_ALLOC属性,只保存动态链接过程中用到的符号是 .symtab 的子集,__attribute__ ((visibility ("xxx")))可以有效减少动态符号表大小。
.got 本区包含全局偏移量表(global offset table),位于数据段的开始,用于保存全局变量以及库函数(外部函数)的引用。
.hash 本区包含一张符号哈希表,用于快速寻找。
.interp 本区含有ELF程序解析器的路径名。这是一个ASCII字符串,仅在动态链接的可执行文件类型中存在。
.plt 本区包含函数链接表。动态链接时使用的过程链接表(precedure linkage table)
.rel.text 本区用于存储代码段的重定位项,指明需要重定位的地方在.text段内,以offset指定具体要进行指令修改的位置。
在连接时候由连接器完成重定位工作,一般由编译器编译产生,存在于obj文件内。
.rel.data 数据段重定位信息
.rel.rodata 常量区重定位信息
.rel.dyn 重定位的地方在.got段内。主要是针对外部数据变量符号。例如全局数据。重定位在程序运行时定位,
定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置下的
value。另外,.rel.dyn 含义是指和dyn有关,一般是指在程序运行时候,动态加载。
区别于rel.plt,rel.plt是指和plt相关,具体是指在某个函数被调用时候加载。
.rel.plt 重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次
被调用时候重定位。可看汇编,理解其首次访问是如何重定位的,实际很简单,就是初次重定位函数地址,
然后把最终函数地址放到.got.plt内,以后读取该.got.plt就直接得到最终函数地址(参考过程说明)。
所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。
一般由连接器产生,存在于可执行文件或者动态库文件。
借助这两个辅助段可以动态修改对应.got和.got.plt段,从而实现运行时重定位。
.init 本区包含进程初始化时要执行的程序指令,当程序开始运行时,系统会在进入主函数之前执行这一区中的代码。
.fini 本区包含进程终止代码,当程序结束运行时,系统会在最后执行这一区中的代码.
.debug 本区中含有调试信息。所有以".debug"为前缀的区名字都是保留的。
.line 本区也是一个用于调试的区,它包含那些调试符号的行号,为程序指令码与源文件的行号建立起联系。
.shstrtab 本区是"节区名字表",含有所有其它区的名字,如 `.data`,`.bss`,`.text`...
- sh_type
节类型 sh_type 表明了当前节的作用。
名称 | 值 | 类型 |
---|---|---|
SHT_NULL | 0 | 无效节 |
SHT_PROGBITS | 1 | 可编程节,例如.text .data .rodata |
SHT_SYMTAB | 2 | 符号表,例如.symtab |
SHT_STRTAB | 3 | 字符串表,例如.strtab .shstrtab .dynstr |
SHT_RELA | 4 | 重定位表,例如.rel.text rel.dyn .rel.plt |
SHT_HASH | 5 | 符号表的哈希表,例如.hash |
SHT_DYNAMIC | 6 | 动态链接信息节,例如.dynamic |
SHT_NOTE | 7 | 提示性的节 |
SHT_NOBITS | 8 | 提示该节不占用文件空间,例如.bss |
SHT_REL | 9 | 表示该节包含重定位向,例如.rel.text .rel.dyn |
SHT_SHLIB | 10 | 保留 |
SHT_DYNSYM | 11 | 动态链接符号表,例如.dynsym |
- sh_flags
节类型 sh_flags 表明了当前节是需要否载入内存、是具有否可读、可写、可执行权限等信息。
名称 | 值 | 类型 |
---|---|---|
SHF_WRITE | 0x1 | 表示节在载入后是可写的 |
SHF_ALLOC | 0x2 | 表示节在运行时需要载入并占用内存空间 |
SHF_EXECINSTR | 0x4 | 表示节是可执行的指令 |
SHF_MERGE | 0x10 | 表示节是可合并的节 |
SHF_STRINGS | 0x20 | 表示节包含以空字符结尾的字符串 |
SHF_STRINGS | 0x20 | 表示节包含以空字符结尾的字符串 |
- sh_info
节的附加信息 sh_link 和 sh_info 是与节的类型有关的
类型 | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 本节用到的字符串所在节的索引 | 0 |
SHT_HASH | 本节用到的符号表所在节的索引 | 0 |
SHT_REL | 本节用到的符号表所在节的索引 | 表示重定位节未来会修改的节的索引 |
SHT_SYMTAB/SHT_DYNSYM | 本节用到的字符串所在节的索引 | 符号表中本地(LOCAL)符号最大索引值加1 |
ELF Relocation Table
重定位表的作用是找到 待重定位的指令的位置 和 待重定位指令使用到的符号地址,在编译链接时或动态链接时进行重定位,主要是通过r_offset 和 重定位基地址 可以计算出 需要重定位的指令所在地址,再通过 r_info 可以从符号节中找到对应的符号表项,最终找到符号所在的地址,再根据重定位项指明的 重定位类型 进行指令的修改。
使用如下命令查询文件的 ELF relocation table
>readelf -r ./app.o
Relocation section '.rel.text' at offset 0xa14 contains 35 entries:
Offset Info Type Sym.Value Sym. Name
000000bc 00000402 R_ARM_ABS32 00000000 .bss
000000c0 00002802 R_ARM_ABS32 00000101 device_info_get
000000c4 00002902 R_ARM_ABS32 00000000 msleep
...
00000228 00002702 R_ARM_ABS32 00000000 memset
0000022c 00000202 R_ARM_ABS32 00000000 .rodata
00000230 00001902 R_ARM_ABS32 00000000 printf
00000234 00000202 R_ARM_ABS32 00000000 .rodata
00000238 00001c02 R_ARM_ABS32 00000000 memcpy
0000023c 00000202 R_ARM_ABS32 00000000 .rodata
00000240 00000202 R_ARM_ABS32 00000000 .rodata
00000244 00002a02 R_ARM_ABS32 00000000 snprintf
00000248 00002c02 R_ARM_ABS32 00000000 strlen
0000024c 00000202 R_ARM_ABS32 00000000 .rodata
代码中的结构体如下所示:
/* r_info - get symbol table index affected by relocation */
#define ELF32_R_SYM(i) ((i) >> 8)
/* r_info - get relocation type */
#define ELF32_R_TYPE(i) ((i)& 0xff)
/* ELF32 Relocation entry without implicit addend */
typedef struct elf32_rel {
/*重定位偏移,重定位文件:受重定位作用的存储单元(即需要指令修正的位置)在节中的偏移量,可执行/共享目标文件:被重定位的指令的虚拟地址*/
Elf32_Addr r_offset; /* offset of relocation */
/*重定位信息,指出重定位项使用的符号在符号表中的索引,也给出了重定位的类型*/
Elf32_Word r_info; /* symbol table index and type */
/*加数是隐含在被修改的位置里*/
} Elf32_Rel;
/* ELF32 Relocation entry with explicit addend */
typedef struct elf32_rela {
Elf32_Addr r_offset; /* offset of relocation */
Elf32_Word r_info; /* symbol table index and type */
Elf32_Sword r_addend; /* an addend */
} Elf32_Rela;
ARM重定位类型
值 | 名称 | 类型 | 重定位方式 | 计算 |
---|---|---|---|---|
0 | R_ARM_NONE | NONE | ALL | NONE |
2 | R_ARM_ABS32 | 静态 | 数据重定位 | (S + A) | T |
10 | R_ARM_THM_CALL | 静态 | Thumb32指令重定位 | ((S + A) | T) – P |
21 | R_ARM_GLOB_DAT | 动态 | 数据重定位 | (S + A) | T |
22 | R_ARM_JUMP_SLOT | 动态 | 数据重定位 | (S + A) | T |
23 | R_ARM_RELATIVE | 动态 | 数据重定位 | B(S) + A |
27 | R_ARM_PLT32 | 静态 | ARM指令重定位 | ((S + A) | T) – P |
28 | R_ARM_CALL | 静态 | ARM指令重定位 | ((S + A) | T) – P |
29 | R_ARM_JUMP24 | 静态 | ARM指令重定位 | ((S + A) | T) – P |
30 | R_ARM_THM_JUMP24 | 静态 | Thumb32指令重定位 | ((S + A) | T) – P |
38 | R_ARM_TARGET1 | 静态 | 数据重定位 | (S + A) | T |
40 | R_ARM_V4BX | 静态 | 杂项重定位 | xxx |
42 | R_ARM_PREL31 | 静态 | 数据重定位 | ((S + A) | T) – P |
43 | R_ARM_MOVW_ABS_NC | 静态 | ARM指令重定位 | ((S + A) | T) – P |
44 | R_ARM_MOVT_ABS | 静态 | ARM指令重定位 | S + A |
47 | R_ARM_THM_MOVW_ABS_NC | 静态 | Thumb32指令重定位 | S + A |
48 | R_ARM_THM_MOVT_ABS | 静态 | Thumb32指令重定位 | (S + A) | T |
重定位的类型分为静态和动态重定位方式:
-
静态重定位用于在静态链接中使用
-
动态重定位用于在动态链接时使用
重定位的方式决定了是对数据(符号地址)进行寻找还是涉及对指令进行重定位,主要有以下方式:
- 数据重定位:这种类型的重定位不涉及操作指令,只是对待重定位地址下存储的地址偏移数据进行修改,例如:
// weak_symbol_test 中只是进行了 my_printf("%s\r\n", "hello");的打印
00000000 <weak_symbol_test>:
0: 4801 ldr r0, [pc, #4] ; (8 <weak_symbol_test+0x8>)
2: 4b02 ldr r3, [pc, #8] ; (c <weak_symbol_test+0xc>)
4: 4718 bx r3
6: bf00 nop
8: 00000018 .word 0x00000018
8: R_ARM_ABS32 .rodata
c: 00000000 .word 0x00000000
c: R_ARM_ABS32 my_printf
ldr r0, [pc, #4] 是在 Thumb 模式下基于 PC 位置的相对寻址,意思的是将 PC 中的值加 4 作为新地址,取该内存地址上的数据,保存到r0寄存器,其中 [pc, #4] 是获取 PC+4 地址下的内容,PC寄存器中的值 = 当前地址 + 4(Thumb模式下+4,ARM模式下+8),所以是获取地址标签 8 下的内容,此处内容还需要通过 R_ARM_ABS32 方式的计算才能得到符号的真正位置,其实就是获取只读字符串**"hello"**的首地址。
ldr r3, [pc, #8] 则是获取符号 my_printf 的地址到 r3 寄存器中,最后使用 bx 来调用 my_printf 函数。
- ARM指令重定位:这种重定位类型主要是对 ARM 模式下的指令所包含操作数进行重定位,例如BL/BLX,ADD, SUB等指令下的立即数,只有修正了立即数才能正确的跳转到符号所在的位置。
- Thumb指令重定位:这种重定位类型主要是对 Thumb 模式下的指令所包含操作数进行重定位,例如BL/BLX,ADD, SUB等指令下的立即数,只有修正了立即数才能正确的跳转到符号所在的位置。
ELF Symbol Table
使用如下命令查询文件的 ELF symbol table,app.o不存在引用其他共享库的符号所以不存在 .dynsym 动态符号表
>readelf -s app.o
Symbol table '.symtab' contains 47 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 SECTION LOCAL DEFAULT 1
2: 00000000 0 SECTION LOCAL DEFAULT 3
3: 00000000 0 SECTION LOCAL DEFAULT 4
4: 00000000 0 SECTION LOCAL DEFAULT 5
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000000 0 SECTION LOCAL DEFAULT 7
...
39: 00000000 0 NOTYPE GLOBAL DEFAULT UND memset
40: 00000101 96 FUNC GLOBAL DEFAULT 1 device_info_get
41: 00000000 0 NOTYPE GLOBAL DEFAULT UND msleep
42: 00000000 0 NOTYPE GLOBAL DEFAULT UND snprintf
43: 00000000 0 NOTYPE GLOBAL DEFAULT UND device_state_get
44: 00000000 0 NOTYPE GLOBAL DEFAULT UND strlen
45: 00000000 0 NOTYPE GLOBAL DEFAULT 4 __data_start
46: 00000004 4 OBJECT GLOBAL DEFAULT COM gIrqVal
代码中的结构体如下所示:
/* st_info - get symbol binding type for use in program headers */
#define ELF_ST_BIND(info) ((info) >> 4)
/* st_info - get symbol type for use in section headers */
#define ELF_ST_TYPE(info) ((info) & 0xf)
/* ELF32 Symbol Table Entry */
typedef struct elf32_sym {
/*符号名称,实际是一个索引,指向.strtab 字符串表某个位置*/
Elf32_Word st_name; /* name - index into string table */
/*符号值,可执行/共享库文件:为符号所在的虚拟地址;重定位文件:是节内地址偏移,所在节由st_shndx给出,如果st_shndx=SHN_COMMON,则表示对齐字节*/
Elf32_Addr st_value; /* symbol value */
/*符号的大小*/
Elf32_Word st_size; /* symbol size */
/*符号信息:包括符号绑定,符号类型,符号信息*/
Elf_Byte st_info; /* type and binding */
/*未使用,默认为0*/
Elf_Byte st_other; /* 0 - no defined meaning */
/*符号所在节的节头表索引,指向节头表的某个位置(或特殊索引), 可执行/共享库文件中此值无效*/
Elf32_Half st_shndx; /* section header index */
} Elf32_Sym;
符号表主要存在于 .dynsym 节和 .symtab节中,用于存放用到的全局变量名称和函数名称,由于局部变量不需要被其他模块访问所以无需放入符号表,在执行时只需要将使用到的局部变量处理后放入寄存器即可。
.dynsym节是 .symtab节的一个子集,主要存放运行时动态链接过程中用到的全局变量和函数,在运行时是需要加载到内存中供动态链接器进行重定位使用,而 .symtab 节存放不仅存放了全局符号,还保留了一些本地符号(static 限定的函数或变量),用于编译过程中连接器进行符号重定位,编译完成后,可执行文件中可以不包括 .symtab 节。
对应静态链接来说,.symtab符号表中存放了所有链接时输入文件的符号信息,而不是存放符号内容,符号表的存在是为了让我们能够更方便的找到符号,符号表在链接过程重定位完成后就可以从静态可执行文件中删除。
而对于动态链接来说,完成在编译链接时完成静态链接类似的步骤后,只会保留动态重定位表.dynsym,用于运行时给动态连接器使用。
-
st_info
绑定类型:
名称 | 值 | 说明 |
---|---|---|
STB_LOCAL | 1 | 在当前文件中定义的本地符号(static修饰的函数和变量),其他模块不可见 |
STB_GLOBAL | 2 | 是全局的变量或者是函数,在链接后其他模块可以引用 |
STB_WEAK | 3 | 是显示申明的弱符号,可以被同名强符号覆盖 |
符号类型:
名称 | 值 | 说明 |
---|---|---|
STT_NOTYPE | 0 | 未指定的符号类型 |
STT_OBJECT | 1 | 数据类型符号,例如变量 数组等 |
STT_FUNC | 2 | 函数类型符号 |
STT_SECTION | 3 | 与节相关的符号类型 |
STT_FILE | 4 | 文件名相关的符号类型 |
STT_COMMON | 5 | 未初始化化的全局数据类型符号 |
-
st_shndx
特殊节头表索引:
名称 | 值 | 说明 |
---|---|---|
STN_UNDEF | 0 | 表明符号在当前文件中没有定义,需要再链接过程中寻找具体符号地址 |
STN_ABS | 0xfff1 | 表明符号是一个绝对值,在重定位过程中,此值不需要改变 |
STN_COMMON | 0xfff2 | 表明符号是一个未分配地址的全局未初始化变量,在链接过程中链接器分配地址 |
ELF String Table
字符串表可以通过如下命令查看,包括符号字符串表(.strtab) 和动态符号字符串表(.dynstr),其中的内容是符号的字符串名称,包含多个以 '/0' 结尾的字符串,通过符号表中的 st_name 索引进行符号字符串获取。
> readelf -x .strtab ./app.o
> readelf -x .dynstr ./app.so
ELF Dynamic section
使用如下命令查询文件的 ELF dynamic section
> readelf -d ./out/dlapp.so
Dynamic section at offset 0x518 contains 13 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdlapp11.so]
0x7ffffffd (AUXILIARY) Auxiliary library: [PIC]
0x00000004 (HASH) 0x94
0x00000005 (STRTAB) 0x284
0x00000006 (SYMTAB) 0x134
0x0000000a (STRSZ) 214 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000011 (REL) 0x35c
0x00000012 (RELSZ) 112 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x00000016 (TEXTREL) 0x0
0x6ffffffa (RELCOUNT) 6
0x00000000 (NULL) 0x0
>readelf -S ./out/dlapp.so
There are 15 section headers, starting at offset 0xa8c:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .hash HASH 00000094 000094 0000a0 04 A 2 0 4
[ 2] .dynsym DYNSYM 00000134 000134 000150 10 A 3 3 4
[ 3] .dynstr STRTAB 00000284 000284 0000d6 00 A 0 0 1
[ 4] .rel.dyn REL 0000035c 00035c 000070 08 A 2 0 4
[ 5] .text PROGBITS 000003cc 0003cc 000098 00 AX 0 0 4
[ 6] .rodata PROGBITS 00000464 000464 0000b1 01 AMS 0 0 1
[ 7] .dynamic DYNAMIC 0000051c 000518 000088 08 WA 3 0 4
[ 8] .got PROGBITS 000005a4 0005a0 00000c 04 WA 0 0 4
[ 9] .bss NOBITS 000005b0 0005ac 000408 00 WA 0 0 4
[10] .comment PROGBITS 00000000 0005ac 00007e 01 MS 0 0 1
[11] .ARM.attributes ARM_ATTRIBUTES 00000000 00062a 00003d 00 0 0 1
[12] .symtab SYMTAB 00000000 000668 0002a0 10 13 24 4
[13] .strtab STRTAB 00000000 000908 00010e 00 0 0 1
[14] .shstrtab STRTAB 00000000 000a16 000074 00 0 0 1
代码中的结构体如下所示:
typedef struct {
/* Tag 用于控制对d_un 的解析*/
Elf32_Sword d_tag; /* entry tag value */
union {
Elf32_Word d_val; /* value type */
Elf32_Addr d_ptr; /* addr type */
} d_un;
} Elf32_Dyn;
动态链接信息节存在于共享类型的重定位文件和动态链接的可执行文件中,需要载入内存中,由动态连接器在运行时使用,包括依赖于哪些共享目标文件、动态连接符号表的位置、动态连接重定位表的位置等,可以用于动态重定位前进行关键信息的获取。
- Tag名称
Tag | 值 | d_un | 说明 |
---|---|---|---|
DT_NULL | 0 | NULL | 表示动态信息表结束 |
DT_NEEDED | 1 | d_val | 指明所需库的名称在字符串表中的索引 |
DT_PLTRELSZ | 2 | d_val | .rel.plt 函数链接重定位节总大小 |
DT_PLTGOT | 3 | d_ptr | 表示动态链接器中GOT表的地址或PLT的地址 |
DT_HASH | 4 | d_ptr | .hash 符号哈希表节虚拟地址 |
DT_STRTAB | 5 | d_ptr | .dynstr 动态符号字符串表节的虚拟地址 |
DT_SYMTAB | 6 | d_ptr | .dynsym 动态符号表节的虚拟地址 |
DT_RELA | 7 | d_ptr | 同 DT_REL |
DT_RELASZ | 8 | d_val | 同 DT_RELSA |
DT_RELAENT | 9 | d_val | 同 DT_RELENT |
DT_STRSZ | 10 | d_val | .dynstr 动态符号字符串表节的总大小 |
DT_SYMENT | 11 | d_val | .dynsym 动态符号表节中每个项的大小 |
DT_INIT | 12 | d_ptr | .init 构造节函数的虚拟地址 |
DT_FINI | 13 | d_ptr | .finit 析构节函数的虚拟地址 |
DT_SONAME | 14 | d_val | 共享目标在字符串表中的偏移 |
DT_RPATH | 15 | d_val | 用于库文件文件的路径径名在字符串表中的偏移 |
DT_REL | 17 | d_ptr | .rel.dyn 动态重定位表的虚拟地址 |
DT_RELSZ | 18 | d_val | .rel.dyn 动态重定位表的大小 |
DT_RELENT | 19 | d_val | .rel.dyn 动态重定位表中每个项的大小 |
DT_PLTREL | 20 | d_val | .rel.plt 函数链接表应用的重定位项的类型,REL/RELA |
DT_TEXTREL | 22 | NULL | 如果存在表示在只读段在重定位中可以通过连接器进行修改 |
DT_JMPREL | 23 | d_ptr | rel.plt 函数链接重定位表节的虚拟地址 |
ELF Program Header Table
一个目标文件的程序段包含一个或者多个节区。程序的头部只有对于可执行文件和共享目标文件有意义
使用如下命令查询文件的 ELF program header table
> readelf -l ./out/app.bin
Elf file type is EXEC (Executable file)
Entry point 0x405710
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000008e3b8 0x000000000008e3b8 R E 0x200000
LOAD 0x000000000008e870 0x000000000068e870 0x000000000068e870
0x0000000000002130 0x0000000000017738 RW 0x200000
DYNAMIC 0x000000000008fdb8 0x000000000068fdb8 0x000000000068fdb8
0x0000000000000220 0x0000000000000220 RW 0x8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 0x4
GNU_EH_FRAME 0x000000000007e7d0 0x000000000047e7d0 0x000000000047e7d0
0x0000000000003204 0x0000000000003204 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x000000000008e870 0x000000000068e870 0x000000000068e870
0x0000000000001790 0x0000000000001790 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .data.rel.ro .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .data.rel.ro .dynamic .got
代码中的结构体如下所示
/* ELF32 Program Header */
typedef struct elf32_phdr {
/*段类型:PT_LOAD=1 可加载的段*/
Elf32_Word p_type; /* segment type */
/*从文件头到该段的偏移*/
Elf32_Off p_offset; /* segment offset */
/*该段被放到进程中内存中的虚拟地址*/
Elf32_Addr p_vaddr; /* virtual address of segment */
/*在linux中这个成员没有任何意义,值与p_vaddr相同*/
Elf32_Addr p_paddr; /* physical address - ignored? */
/*该段在文件映像中所占的字节数,一般p_memsz>=p_filesz,.bss段在内存中占用空间*/
Elf32_Word p_filesz; /* number of bytes in file for seg. */
/*该段在内存映像中占用的字节数*/
Elf32_Word p_memsz; /* number of bytes in mem. for seg. */
/*段标志*/
Elf32_Word p_flags; /* flags */
/*p_vaddr是否对齐*/
Elf32_Word p_align; /* memory alignment */
} Elf32_Phdr;
段是从程序运行的视角来看待 ELF文件,每个段包括了多个节区,不同节区有不同的类型,程序段仅存在于共享类型的重定位文件和可执行文件中。
- p_type
名称 | 值 | 说明 |
---|---|---|
PT_NULL | 0 | 未定义的程序类型 |
PT_LOAD | 1 | 表示是一个可装载的段,所有此类型的段按p_vddr升序排列,需要载入内存 |
PT_DYNAMIC | 2 | 表示是动态连接的信息,包含.dynamic |
PT_INTERP | 3 | 表示ELF解析器路径,只存在于动态可执行文件中 |
- p_flags
名称 | 值 | 说明 |
---|---|---|
PF_X | 1 | 执行权限 |
PF_W | 2 | 写权限 |
PF_R | 4 | 读权限,例如.dynsym .dynstr仅有只读权限 |
PF_R+PF_X | 5 | 读权限,例如.text .plt具有读和执行权限 |
PF_R+PF_W | 6 | 读权限,例如.got .data .bss具有读写权限 |
PF_R+PF_W+PF_X | 7 | 读权限 |
表中的虚拟地址说明
在如上的各种表中涉及到一些虚拟地址,并不是映射到内后的真实虚拟地,如下所示:
ELF头中的 Elf32_Ehdr->e_entry(程序入口虚拟地址)
节区表中 Elf32_Shdr->sh_addr (节映射到内存的虚拟地址)
符号表中的 Elf32_Sym->st_value (对于可执行/共享文件指向符号所在虚拟地址)
重定位表中 Elf32_Rel->r_offset (对于可执行/共享文件指向需要重定位的指令地址)
程序头表中 Elf32_Phdr->p_vaddr (对于可执行/共享文件指向该段在内存中的虚拟地址)
这些虚拟地址不是在进程中的真实地址,只是用来在重定位时进行偏移量的计算,例如
- 通过节区计算程序真实入口地址:重定位基地址 + (e_entry - .text 节的sh_addr)
- 通过程序段计算程序真实入口地址:重定位基地址 + (e_entry - LOAD段的低p_vaddr)
- 共享/可执行文件中计算需要重定位的指令地址:重定位基地址 + r_offset
- 共享/可执行文件中计算符号地址:重定位基地址 + (st_value - LOAD段的低p_vaddr)