ELF的全称为Executable and Linkable Format即:可执行与可链接格式。我们知道,使用C语言编译的可执行程序中常量和全局变量通常存放在数据段中,程序在使用这些常量和变量时,需要通过使用它们的地址来访问他们的内容,例如:
int global_var = 0x11111111; int main(int argc, char **args) { global_var = 0x22222222; return 0; }
对上面程序反汇编:
00000000 <.text>: ... 4c: 55 push %ebp 4d: 89 e5 mov %esp,%ebp 4f: 51 push %ecx 50: 83 ec 14 sub $0x14,%esp 53: c7 05 00 20 00 00 22 movl $0x22222222,0x2000 00002000 <.data>: 2000: 11 11 adc %edx,(%ecx) 2002: 11 11 adc %edx,(%ecx)
可以看到,全局变量global_var的地址为0x2000,为其赋值也是操作的这个地址movl $0x22222222, 0x2000。于是问题出现了,这此地址都是相对于程序的代码段地址、数据段地址而言。当程序被读入内存准备运行时,可执行程序被内核程序载入到一个物理地址中,那么程序内部的这些相对地址也就失去了本来的意义。这些地址在物理内存中是错误的地址。解决的办法是将这些全局的或是常量所在的地址通过一些特殊的标记记录下来,当操作系统载入程序时,将这些地址转为一个实际可用的物理地址。这一过程被称为重定位。在Linux下,使用gcc编译的C程序都是ELF文件格式,它的格式定义如下:
ELF文件头:
字段 |
大小 |
值 |
描述 |
e_ident[0] |
1字节 |
0x7f |
固定的开始内容 |
e_ident[1] |
1字节 |
0x45 |
字符E |
e_ident[2] |
1字节 |
0x4c |
字符L |
e_ident[3] |
1字节 |
0x46 |
字符F |
e_ident[4] |
1字节 |
0x1 |
文件类别: 0:非法文件 1:32位目标文件 2:64位目录文件 |
e_ident[5] |
1字节 |
0x1 |
编码方式: 0:非法编码 1:小尾编码方式 2:大尾编码方式 |
e_ident[6] |
1字节 |
0x1 |
版本号 |
e_ident[7 ~ 15] |
9字节 |
0x0 |
保留 |
e_type |
2字节 |
0x1 |
文件类型: 0:未知类型 1:可重定位文件 2:可执行文件 3:动态链接库 4:CORE文件 0xff00:特定处理器文件扩展下边界 0xffff:特定处理器文件扩展上边界 |
e_machine |
2字节 |
0x3 |
体系结构类型:Intel 80386 |
e_version |
4字节 |
0x1 |
当前版本号 |
e_entry |
4字节 |
0x0 |
程序入口地址 |
e_phoff |
4字节 |
0x0 |
程序头偏移地址 |
e_shoff |
4字节 |
0xfc |
节区头偏移地址 |
e_flags |
4字节 |
0x0 |
标志 |
e_ehsize |
2字节 |
0x34 |
elf头大小 |
e_phentsize |
2字节 |
0x0 |
程序头大小 |
e_phnum |
2字节 |
0x0 |
程序段数量 |
e_shentsize |
2字节 |
0x28 |
节区头大小 |
e_shnum |
2字节 |
0x1b |
节区头数量 |
e_shstrndx |
2字节 |
0x8 |
节区索引 |
程序头:
字段 |
大小 |
值 |
描述 |
p_type |
4字节 |
0x000034 |
类型 |
p_offset |
4字节 |
0x08048034 |
程序偏移地址 |
p_vaddr |
4字节 |
0x08048034 |
段加载后在进程空间中占用的内存起始地址 |
p_paddr |
4字节 |
0x08048034 |
在支持paging的OS中该字段被忽略 |
p_filesz |
4字节 |
0x00120 |
该段在文件中占用的字节大小 |
p_memsz |
4字节 |
0x00120 |
该段在内存中占用的字节大小 |
p_flags |
4字节 |
0x0 |
标志 |
p_align |
4字节 |
0x4 |
对齐 |
节区头:
字段 |
大小 |
值 |
描述 |
sh_name |
4字节 |
0x0 |
节区名(实际是在shstrtab中的索引号) |
sh_type |
4字节 |
0x0 |
节区类型 |
sh_flags |
4字节 |
0x0 |
标志 |
sh_addr |
4字节 |
0x26 |
相对可执行地址 |
sh_offset |
4字节 |
0x48 |
偏移地址 |
sh_size |
4字节 |
0x28 |
节区大小 |
sh_link |
4字节 |
0x0 |
其它节区索引 |
sh_info |
4字节 |
0x0 |
扩展信息 |
sh_addralign |
4字节 |
0x0 |
节区对齐 |
sh_entsize |
4字节 |
0x0 |
入口表大小 |
符号表:
字段 |
大小 |
值 |
描述 |
st_name |
4字节 |
0x0 |
在字符串中的索引号 |
st_value |
4字节 |
0x00000008 |
值,用于重定位 |
st_size |
4字节 |
0x4 |
大小 |
st_info |
1字节 |
0x0 |
符号信息 |
st_other |
1字节 |
0x0 |
其它信息 |
st_shndx |
2字节 |
0x6 |
节区索引 |
重定位:
字段 |
大小 |
值 |
描述 |
r_offset |
4字节 |
0x0000001b |
需要重定位的偏移地址 |
r_info |
4字节 |
0x00000b01 |
(符号 << 8) | 重定位类型 0:R_386_NONE 计算方式:无 1:R_386_32 计算方式:S + A 2:R_386_PC32 计算方式:S + A - P |
在Linux中可以使用readelf命令来查看elf文件中的相关信息。首先编写一个C程序:
start_main.c void start_main() { main(1, 0); for(;;){} } system.c unsigned int g1 = 0x1111; unsigned int g2 = 0x2222; int *p1 = &g1; int *p2 = &g2; int main(int argc, char **args) { int g3 = 0x3333; int g4 = 0x4444; int *p3 = &g1; int *p4 = &g2; p1 = &g3; p2 = &g4; __asm__ volatile("int $0x80" :: "a"(p1)); __asm__ volatile("int $0x80" :: "a"(p2)); __asm__ volatile("int $0x80" :: "a"(p3)); __asm__ volatile("int $0x80" :: "a"(p4)); for (;;) { } return 0; }
使用gcc命令编译start_main.c和system.c这里我们使用gcc时要使用它的-c参数,让gcc为我们编译源代码,但并不链接重定位成可执行文件。然后使用ld命令将start_main.o和system.o链接在一起,让start_main()函数作为程序的入口函数,即:让start_main()函数处于.text节区的起始地址,也就是0x0:
gcc -m32 -c start_main.c -o start_main.o gcc -m32 -c system.c -o system.o ld -m elf_i386 -Ttext 0x0 -e start_main -r start_main.o system.o -o system.ecc
生成了system.ecc文件之后我们就可以通过使用readelf命令来查看它内部的详细信息,并可以找到其所有需要重定位的地址和内容。
elf文件头:
readelf system.ecc -h 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: Intel 80386 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 836 (bytes into file) Flags: 0x0 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: 13 Section header string table index: 10 There are 13 section headers, starting at offset 0x344:
节区信息:
readelf system.ecc -S Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 000063 00 AX 0 0 1 [ 2] .rel.text REL 00000000 0002ec 000038 08 I 11 1 4 [ 3] .eh_frame PROGBITS 00000000 000098 000068 00 A 0 0 4 [ 4] .rel.eh_frame REL 00000000 000324 000010 08 I 11 3 4 [ 5] .data PROGBITS 00000000 000100 000010 00 WA 0 0 4 [ 6] .rel.data REL 00000000 000334 000010 08 I 11 5 4 [ 7] .bss NOBITS 00000000 000110 000000 00 WA 0 0 1 [ 8] .comment PROGBITS 00000000 000110 00005a 01 MS 0 0 1 [ 9] .note.GNU-stack PROGBITS 00000000 00016a 000000 00 0 0 1 [10] .shstrtab STRTAB 00000000 00016a 00005b 00 0 0 1 [11] .symtab SYMTAB 00000000 0001c8 0000f0 10 12 9 4 [12] .strtab STRTAB 00000000 0002b8 000033 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
符号表:
readelf system.ecc -s Symbol table '.symtab' contains 15 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 5 4: 00000000 0 SECTION LOCAL DEFAULT 7 5: 00000000 0 SECTION LOCAL DEFAULT 8 6: 00000000 0 SECTION LOCAL DEFAULT 9 7: 00000000 0 FILE LOCAL DEFAULT ABS start_main.c 8: 00000000 0 FILE LOCAL DEFAULT ABS system.c 9: 0000000c 4 OBJECT GLOBAL DEFAULT 5 p2 10: 00000000 4 OBJECT GLOBAL DEFAULT 5 g1 11: 00000008 4 OBJECT GLOBAL DEFAULT 5 p1 12: 00000004 4 OBJECT GLOBAL DEFAULT 5 g2 13: 00000000 23 FUNC GLOBAL DEFAULT 1 start_main 14: 00000017 76 FUNC GLOBAL DEFAULT 1 main
重定位内容:
readelf system.ecc -r Relocation section '.rel.text' at offset 0x2ec contains 7 entries: Offset Info Type Sym.Value Sym. Name 0000000e 00000e02 R_386_PC32 00000017 main 0000002e 00000a01 R_386_32 00000000 g1 00000035 00000c01 R_386_32 00000004 g2 0000003d 00000b01 R_386_32 00000008 p1 00000045 00000901 R_386_32 0000000c p2 0000004a 00000b01 R_386_32 00000008 p1 00000051 00000901 R_386_32 0000000c p2 Relocation section '.rel.eh_frame' at offset 0x324 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000020 00000102 R_386_PC32 00000000 .text 00000054 00000102 R_386_PC32 00000000 .text Relocation section '.rel.data' at offset 0x334 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000008 00000a01 R_386_32 00000000 g1 0000000c 00000c01 R_386_32 00000004 g2
可以看到在elf文件头中的type字段中的值为1(Relocatable file),也就是说这只是一个可重定位文件,它并不是一个可执行的文件。如果直接生成在Linux下可执行的文件,这些重定位地址将会被替换成一系列的虚拟地址,比如:0x80482f0、0x08048168、0x0804a028等等。另外,gcc还会给这个可执行文件加入一些系统函数,用于处理程序执行前的预处理和调用main函数,比如:_init函数__gmon_start__函数、__libc_start_main函数以及_start函数等等。这些在我们的操作系统中是无用的,我们需要将一个可重定位文件载入到内存中,并读取它的重定位信息。通过重定位信息修改程序中的相关内容,最后执行程序。所以内核程序载入一个可重定位文件就可以让其运行。下面来看一下elf相关的数据结构,与前面所述的elf格式一致:
//ELF头前16字节固定长 #define EI_NIDENT 16 //ELF文件头 typedef struct elf32_hdr { //固定的开始内容 u8 e_ident[EI_NIDENT]; //文件类型 u16 e_type; //体系结构类型 u16 e_machine; //当前版本号 u32 e_version; //程序入口地址 u32 e_entry; //程序头偏移地址 u32 e_phoff; //节区头偏移地址 u32 e_shoff; //标志 u32 e_flags; //elf头大小 u16 e_ehsize; //程序头大小 u16 e_phentsize; //程序段数量 u16 e_phnum; //节区头大小 u16 e_shentsize; //节区头数量 u16 e_shnum; //节区索引 u16 e_shstrndx; } Elf32_Ehdr; //程序头 typedef struct elf32_phdr { //类型 u32 p_type; //程序偏移地址 u32 p_offset; //段加载后在进程空间中占用的内存起始地址 u32 p_vaddr; //在支持paging的OS中该字段被忽略 u32 p_paddr; //该段在文件中占用的字节大小 u32 p_filesz; //该段在内存中占用的字节大小 u32 p_memsz; //标志 u32 p_flags; //对齐 u32 p_align; } Elf32_Phdr; //节区头 typedef struct elf32_shdr { //节区名(实际是在shstrtab中的索引号) u32 sh_name; //节区类型 u32 sh_type; //标志 u32 sh_flags; //相对可执行地址 u32 sh_addr; //偏移地址 u32 sh_offset; //节区大小 u32 sh_size; //其它节区索引 u32 sh_link; //扩展信息 u32 sh_info; //节区对齐 u32 sh_addralign; //入口表大小 u32 sh_entsize; } Elf32_Shdr; //符号表 typedef struct elf32_sym { //在字符串中的索引号 u32 st_name; //值,用于重定位 u32 st_value; //大小 u32 st_size; //符号信息 u8 st_info; //其它信息 u8 st_other; //节区索引 u16 st_shndx; } Elf32_Sym; //重定位 typedef struct elf32_rel { //需要重定位的偏移地址 u32 r_offset; //其值为:(符号 << 8) | 重定位类型 u32 r_info; } Elf32_Rel;
Copyright © 2015-2023 问渠网 辽ICP备15013245号