文章目录
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偏移处的名字为:.interp -
000042a0 23 00 00 00
: 23偏移处的名字为:.note.gnu.property -
000042e0 36 00 00 00
: 23偏移处的名字为:.note.gnu.build-id -
00004320 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
= 1 -
LD_BIND_NOW
= on -
LD_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