目标文件就是链接前的.o文件或者其他格式的二进制文件,通过链接可以生成可执行文件。
目标文件的格式 现在流行的可执行文件格式(Executable)主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),都是COFF(Common file format)的变种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux的.o),因为和可执行文件结构相似,也用相同的结构存储。
静态链接库和动态链接库同样用可执行文件的格式存储。静态链接库稍有不同,它把很多目标文件绑定在一起,再加上一些索引,可以简单理解为一个包含有很多目标文件的文件包。
ELF格式主要有四类:
ELF文件类型
说明
实例
可重定位文件(Relocatable File)
这类文件包含了代码和数据,可以被用来链接成为可执行文件和共享目标文件,静态链接库也归于这一类
Linux的.o
可执行文件(Executable File)
这类文件包含了可以直接执行的程序,它的代表是ELF可执行文件
/bin/bash
共享目标文件(Shared Object File)
这种文件包含了代码和数据,可以在两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,第二帧是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来执行
Linux的.so
核心转换文件(Core Dump File)
当进程意外终止的时候,系统可以将该进程的地址空间的内容及终止时的一些其他信息转存到核心转储文件
Linux的Core Dump
Linux下使用file命令来查看对应的文件格式。
目标文件是什么样的 目标文件不仅包含了编译后的代码和数据,还包括了链接时所需要的一些信息,例如符号表、调试信息、字符串等。根据这些信息的不同属性,以Section(翻译为节或段,书中用段)的形式存储。
程序编译后机器指令会放在代码段,代码段一般名字为”.code”或”.text”。已经初始化的全局变量和局部静态变量经常放在数据段”.data”,而未初始化的全局变量和局部静态变量一般放在一个叫做”.bss”的段里面,因为未初始化的全局变量和局部静态变量默认值都为0,本来它们也可以被放在.data段的,但是因为他们都是0,所以没有必要在.data段中分配空间存储,但是在程序运行过程中它们确实要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记作.bss段,所以.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,并没有内容,在文件中不占据空间。
example 书本提供了如下的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int printf (const char * format, ...) ;int global_init_var = 84 ;int global_uninit_var;static int global_static_var;extern int reference_to_out;void func1 (int i) { printf ("%d\n" , i); } int main (void ) { static int static_var = 85 ; static int static_var2; int a = 1 ; int b; func1(static_var + static_var2 + a + b); return a; }
用gcc -c simple_section.c
得到simple_section.o,然后用objdump查看:
1 objdump -h simple_section.o
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 simple_section.o: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000057 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000008 0000000000000000 0000000000000000 000000a0 2**2 ALLOC 3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .comment 0000002a 0000000000000000 0000000000000000 000000a4 2**0 CONTENTS, READONLY 5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000ce 2**0 CONTENTS, READONLY 6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d0 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
由于编译器版本和架构的问题,输出的结果和书本上的不完全一样,但是大部分是相同的。
除了代码段,数据段和BSS段之外,还有只读数据段(.rodata),注释信息段(.comment)和堆栈提示段(.note.GNU-stack)。eh_frame段在书中没有,这个应该是后来新增的段,暂时不研究。
注意几个段的属性,最容易理解的是段的长度(Size)和段所在的位置(File offset),每个段的第二行中的“CONTENTS, ALLOC, LOAD, DATA”等表示段的各种属性。CONTENTS表示该段在文件中存在。
执行size simple_section.o
,得到:
1 2 text data bss dec hex filename 179 8 8 195 c3 simple_section.o
在这里发现size命令和objdump显示的.text段大小不一样,查了一下找到了一篇文章:
https://blog.csdn.net/Little_ant_/article/details/119214033
为什么 size 和 objdump 查看目标文件的 .text 段的大小不一样呢? 因为size默认是运行在”Berkeley compatibility mode”下。在这种模式下,会将不可执行的拥有”ALLOC”属性的只读段归到.text段下,很典型的就是.rodata段。而在我们这个例子中,使用 size 命令得到的 text 段长度 = .text + .rodata + .eh_frame 。如果你使用”size -A obj.o”,那么size会运行在”System V compatibility mode”,此时,用objdump -h和size显示的.text段大小就差不多了
1 2 3 4 5 6 7 8 9 10 11 size -A simple_section.o simple_section.o : section size addr .text 87 0 .data 8 0 .bss 8 0 .rodata 4 0 .comment 42 0 .note.GNU-stack 0 0 .eh_frame 88 0 Total 237
代码段 用objdump -s -d simple_section.o
得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 simple_section.o: file format elf64-x86-64 Contents of section .text: 0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E... 0010 488d3d00 000000b8 00000000 e8000000 H.=............. 0020 0090c9c3 554889e5 4883ec10 c745f801 ....UH..H....E.. 0030 0000008b 15000000 008b0500 00000001 ................ 0040 c28b45f8 01c28b45 fc01d089 c7e80000 ..E....E........ 0050 00008b45 f8c9c3 ...E... Contents of section .data: 0000 54000000 55000000 T...U... Contents of section .rodata: 0000 25640a00 %d.. Contents of section .comment: 0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7. 0010 352e302d 33756275 6e747531 7e31382e 5.0-3ubuntu1~18. 0020 30342920 372e352e 3000 04) 7.5.0. Contents of section .eh_frame: 0000 14000000 00000000 017a5200 01781001 .........zR..x.. 0010 1b0c0708 90010000 1c000000 1c000000 ................ 0020 00000000 24000000 00410e10 8602430d ....$....A....C. 0030 065f0c07 08000000 1c000000 3c000000 ._..........<... 0040 00000000 33000000 00410e10 8602430d ....3....A....C. 0050 066e0c07 08000000 .n...... Disassembly of section .text: 0000000000000000 <func1>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10 ,%rsp 8: 89 7d fc mov %edi,-0x4(%rbp) b: 8b 45 fc mov -0x4(%rbp),%eax e: 89 c6 mov %eax,%esi 10: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi 17: b8 00 00 00 00 mov $0x0 ,%eax 1c: e8 00 00 00 00 callq 21 <func1+0x21> 21: 90 nop 22: c9 leaveq 23: c3 retq 0000000000000024 <main>: 24: 55 push %rbp 25: 48 89 e5 mov %rsp,%rbp 28: 48 83 ec 10 sub $0x10 ,%rsp 2c: c7 45 f8 01 00 00 00 movl $0x1 ,-0x8(%rbp) 33: 8b 15 00 00 00 00 mov 0x0(%rip),%edx 39: 8b 05 00 00 00 00 mov 0x0(%rip),%eax 3f: 01 c2 add %eax,%edx 41: 8b 45 f8 mov -0x8(%rbp),%eax 44: 01 c2 add %eax,%edx 46: 8b 45 fc mov -0x4(%rbp),%eax 49: 01 d0 add %edx,%eax 4b: 89 c7 mov %eax,%edi 4d: e8 00 00 00 00 callq 52 <main+0x2e> 52: 8b 45 f8 mov -0x8(%rbp),%eax 55: c9 leaveq 56: c3 retq
objdump的-s参数可以将所有段的内容以十六进制的形式打印出来,-d则可以将所有包含指令的段进行反汇编。
数据段和只读数据段 .data段保存的是那些已经初始化了的全局静态变量和局部静态变量
在上面打印的内容中,与.data相关的有:
1 2 3 4 Contents of section .data: 0000 54000000 55000000 T...U... Contents of section .rodata: 0000 25640a00 %d..
.data段里前四个字节0x54,0x00,0x00,0x00,这是小端模式,所以就是0x00000054,也就是84,这就是simple_section.c第一个全局静态变量,值为84。
BSS段 用objdump -s -x -d simple_section.o
得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 simple_section.o: file format elf64-x86-64 simple_section.o architecture: i386:x86-64, flags 0x00000011: HAS_RELOC, HAS_SYMS start address 0x0000000000000000 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000057 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000008 0000000000000000 0000000000000000 000000a0 2**2 ALLOC 3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .comment 0000002a 0000000000000000 0000000000000000 000000a4 2**0 CONTENTS, READONLY 5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000ce 2**0 CONTENTS, READONLY 6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d0 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 simple_section.c 0000000000000000 l d .text 0000000000000000 .text 0000000000000000 l d .data 0000000000000000 .data 0000000000000000 l d .bss 0000000000000000 .bss 0000000000000000 l O .bss 0000000000000004 global_static_var 0000000000000000 l d .rodata 0000000000000000 .rodata 0000000000000004 l O .data 0000000000000004 static_var.1804 0000000000000004 l O .bss 0000000000000004 static_var2.1805 0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack 0000000000000000 l d .eh_frame 0000000000000000 .eh_frame 0000000000000000 l d .comment 0000000000000000 .comment 0000000000000000 g O .data 0000000000000004 global_init_var 0000000000000004 O *COM* 0000000000000004 global_uninit_var 0000000000000000 g F .text 0000000000000024 func1 0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_ 0000000000000000 *UND* 0000000000000000 printf 0000000000000024 g F .text 0000000000000033 main Contents of section .text: 0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E... 0010 488d3d00 000000b8 00000000 e8000000 H.=............. 0020 0090c9c3 554889e5 4883ec10 c745f801 ....UH..H....E.. 0030 0000008b 15000000 008b0500 00000001 ................ 0040 c28b45f8 01c28b45 fc01d089 c7e80000 ..E....E........ 0050 00008b45 f8c9c3 ...E... Contents of section .data: 0000 54000000 55000000 T...U... Contents of section .rodata: 0000 25640a00 %d.. Contents of section .comment: 0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7. 0010 352e302d 33756275 6e747531 7e31382e 5.0-3ubuntu1~18. 0020 30342920 372e352e 3000 04) 7.5.0. Contents of section .eh_frame: 0000 14000000 00000000 017a5200 01781001 .........zR..x.. 0010 1b0c0708 90010000 1c000000 1c000000 ................ 0020 00000000 24000000 00410e10 8602430d ....$....A....C. 0030 065f0c07 08000000 1c000000 3c000000 ._..........<... 0040 00000000 33000000 00410e10 8602430d ....3....A....C. 0050 066e0c07 08000000 .n...... Disassembly of section .text: 0000000000000000 <func1>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: 89 7d fc mov %edi,-0x4(%rbp) b: 8b 45 fc mov -0x4(%rbp),%eax e: 89 c6 mov %eax,%esi 10: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 17 <func1+0x17> 13: R_X86_64_PC32 .rodata-0x4 17: b8 00 00 00 00 mov $0x0,%eax 1c: e8 00 00 00 00 callq 21 <func1+0x21> 1d: R_X86_64_PLT32 printf-0x4 21: 90 nop 22: c9 leaveq 23: c3 retq 0000000000000024 <main>: 24: 55 push %rbp 25: 48 89 e5 mov %rsp,%rbp 28: 48 83 ec 10 sub $0x10,%rsp 2c: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 33: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 39 <main+0x15> 35: R_X86_64_PC32 .data 39: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3f <main+0x1b> 3b: R_X86_64_PC32 .bss 3f: 01 c2 add %eax,%edx 41: 8b 45 f8 mov -0x8(%rbp),%eax 44: 01 c2 add %eax,%edx 46: 8b 45 fc mov -0x4(%rbp),%eax 49: 01 d0 add %edx,%eax 4b: 89 c7 mov %eax,%edi 4d: e8 00 00 00 00 callq 52 <main+0x2e> 4e: R_X86_64_PC32 func1-0x4 52: 8b 45 f8 mov -0x8(%rbp),%eax 55: c9 leaveq 56: c3 retq
与书上不完全相同,我的编译器输出的符号表里未初始化的全局变量和静态变量都在BSS段中,
1 2 2 .bss 00000008 0000000000000000 0000000000000000 000000a0 2**2 ALLOC
大小为8个字节(两个int)
其他段
还可以用objcopy将二进制文件作为目标文件的一个段,也可以自定义段,如下:
ELF文件结构
文件头 执行如下readelf -h simple_section.o
,输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 readelf -h simple_section.o 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: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 1144 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 13 Section header string table index: 12
在/usr/include/elf.h中可以找到与之对应的结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 typedef uint16_t Elf32_Half;typedef uint16_t Elf64_Half;typedef uint32_t Elf32_Word;typedef int32_t Elf32_Sword;typedef uint32_t Elf64_Word;typedef int32_t Elf64_Sword;typedef uint64_t Elf32_Xword;typedef int64_t Elf32_Sxword;typedef uint64_t Elf64_Xword;typedef int64_t Elf64_Sxword;typedef uint32_t Elf32_Addr;typedef uint64_t Elf64_Addr;typedef uint32_t Elf32_Off;typedef uint64_t Elf64_Off;... #define EI_NIDENT (16) typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; 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 { unsigned char e_ident[EI_NIDENT]; Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; Elf64_Off e_phoff; Elf64_Off e_shoff; 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;
可以看到,根据机器的字长,定义了Elf32_Ehdr和Elf64_Ehdr,两个结构体的变量相同,就是部分变量的长度不同。readelf的结果除了e_ident之外都可以与Elf32_Ehdr和Elf64_Ehdr中的成员变量对应
魔数
e_ident有16个元素,unsigned char类型,正好存储了16个字节的Magic(魔数)。
最开始的四个字节必须是0x7f 0x45 0x4c 0x46,接下来一个字节是表示ELF文件类的,0x01是32位,0x02是64位。第六个字节是大小端,第七个字节规定ELF文件的主版本号,一般是1,因为没有更新。后面的九个字节没有定义,一般填0,有些平台会扩展。
文件类型
就是三种ELF文件:可重定位文件,可执行文件和共享目标文件,还有提到过的CORE DUMP,不过这个似乎不太常见
1 2 3 4 5 6 #define ET_NONE 0 #define ET_REL 1 #define ET_EXEC 2 #define ET_DYN 3 #define ET_CORE 4 #define ET_NUM 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 #define ET_NONE 0 #define ET_REL 1 #define ET_EXEC 2 #define ET_DYN 3 #define ET_CORE 4 #define ET_NUM 5 #define ET_LOOS 0xfe00 #define ET_HIOS 0xfeff #define ET_LOPROC 0xff00 #define ET_HIPROC 0xffff #define EM_NONE 0 #define EM_M32 1 #define EM_SPARC 2 #define EM_386 3 #define EM_68K 4 #define EM_88K 5 #define EM_IAMCU 6 #define EM_860 7 #define EM_MIPS 8 #define EM_S370 9 #define EM_MIPS_RS3_LE 10 #define EM_PARISC 15 #define EM_VPP500 17 #define EM_SPARC32PLUS 18 #define EM_960 19 #define EM_PPC 20 #define EM_PPC64 21 #define EM_S390 22 #define EM_SPU 23 #define EM_V800 36 #define EM_FR20 37 #define EM_RH32 38 #define EM_RCE 39 #define EM_ARM 40 #define EM_FAKE_ALPHA 41 #define EM_SH 42 #define EM_SPARCV9 43 #define EM_TRICORE 44 #define EM_ARC 45 #define EM_H8_300 46 #define EM_H8_300H 47 #define EM_H8S 48 #define EM_H8_500 49 #define EM_IA_64 50 #define EM_MIPS_X 51 #define EM_COLDFIRE 52 #define EM_68HC12 53 #define EM_MMA 54 #define EM_PCP 55 #define EM_NCPU 56 #define EM_NDR1 57 #define EM_STARCORE 58 #define EM_ME16 59 #define EM_ST100 60 #define EM_TINYJ 61 #define EM_X86_64 62 #define EM_PDSP 63 #define EM_PDP10 64 #define EM_PDP11 65 #define EM_FX66 66 #define EM_ST9PLUS 67 #define EM_ST7 68 #define EM_68HC16 69 #define EM_68HC11 70 #define EM_68HC08 71 #define EM_68HC05 72 #define EM_SVX 73 #define EM_ST19 74 #define EM_VAX 75 #define EM_CRIS 76 #define EM_JAVELIN 77 #define EM_FIREPATH 78 #define EM_ZSP 79 #define EM_MMIX 80 #define EM_HUANY 81 #define EM_PRISM 82 #define EM_AVR 83 #define EM_FR30 84 #define EM_D10V 85 #define EM_D30V 86 #define EM_V850 87 #define EM_M32R 88 #define EM_MN10300 89 #define EM_MN10200 90 #define EM_PJ 91 #define EM_OPENRISC 92 #define EM_ARC_COMPACT 93 #define EM_XTENSA 94 #define EM_VIDEOCORE 95 #define EM_TMM_GPP 96 #define EM_NS32K 97 #define EM_TPC 98 #define EM_SNP1K 99 #define EM_ST200 100 #define EM_IP2K 101 #define EM_MAX 102 #define EM_CR 103 #define EM_F2MC16 104 #define EM_MSP430 105 #define EM_BLACKFIN 106 #define EM_SE_C33 107 #define EM_SEP 108 #define EM_ARCA 109 #define EM_UNICORE 110 #define EM_EXCESS 111 #define EM_DXP 112 #define EM_ALTERA_NIOS2 113 #define EM_CRX 114 #define EM_XGATE 115 #define EM_C166 116 #define EM_M16C 117 #define EM_DSPIC30F 118 #define EM_CE 119 #define EM_M32C 120 #define EM_TSK3000 131 #define EM_RS08 132 #define EM_SHARC 133 #define EM_ECOG2 134 #define EM_SCORE7 135 #define EM_DSP24 136 #define EM_VIDEOCORE3 137 #define EM_LATTICEMICO32 138 #define EM_SE_C17 139 #define EM_TI_C6000 140 #define EM_TI_C2000 141 #define EM_TI_C5500 142 #define EM_TI_ARP32 143 #define EM_TI_PRU 144 #define EM_MMDSP_PLUS 160 #define EM_CYPRESS_M8C 161 #define EM_R32C 162 #define EM_TRIMEDIA 163 #define EM_QDSP6 164 #define EM_8051 165 #define EM_STXP7X 166 #define EM_NDS32 167 #define EM_ECOG1X 168 #define EM_MAXQ30 169 #define EM_XIMO16 170 #define EM_MANIK 171 #define EM_CRAYNV2 172 #define EM_RX 173 #define EM_METAG 174 #define EM_MCST_ELBRUS 175 #define EM_ECOG16 176 #define EM_CR16 177 #define EM_ETPU 178 #define EM_SLE9X 179 #define EM_L10M 180 #define EM_K10M 181 #define EM_AARCH64 183 #define EM_AVR32 185 #define EM_STM8 186 #define EM_TILE64 187 #define EM_TILEPRO 188 #define EM_MICROBLAZE 189 #define EM_CUDA 190 #define EM_TILEGX 191 #define EM_CLOUDSHIELD 192 #define EM_COREA_1ST 193 #define EM_COREA_2ND 194 #define EM_ARC_COMPACT2 195 #define EM_OPEN8 196 #define EM_RL78 197 #define EM_VIDEOCORE5 198 #define EM_78KOR 199 #define EM_56800EX 200 #define EM_BA1 201 #define EM_BA2 202 #define EM_XCORE 203 #define EM_MCHP_PIC 204 #define EM_KM32 210 #define EM_KMX32 211 #define EM_EMX16 212 #define EM_EMX8 213 #define EM_KVARC 214 #define EM_CDP 215 #define EM_COGE 216 #define EM_COOL 217 #define EM_NORC 218 #define EM_CSR_KALIMBA 219 #define EM_Z80 220 #define EM_VISIUM 221 #define EM_FT32 222 #define EM_MOXIE 223 #define EM_AMDGPU 224 #define EM_RISCV 243 #define EM_BPF 247 #define EM_NUM 248
(这似乎就比较多了,列一下好了)
段表 段表用于存储段的基本结构,比如每个段的段名,段的长度,在文件中的偏移,文件读写权限及其他属性。readelf读取段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 $ readelf -S simple_section.o There are 13 section headers, starting at offset 0x478: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000057 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 00000368 0000000000000078 0000000000000018 I 10 1 8 [ 3] .data PROGBITS 0000000000000000 00000098 0000000000000008 0000000000000000 WA 0 0 4 [ 4] .bss NOBITS 0000000000000000 000000a0 0000000000000008 0000000000000000 WA 0 0 4 [ 5] .rodata PROGBITS 0000000000000000 000000a0 0000000000000004 0000000000000000 A 0 0 1 [ 6] .comment PROGBITS 0000000000000000 000000a4 000000000000002a 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 000000ce 0000000000000000 0000000000000000 0 0 1 [ 8] .eh_frame PROGBITS 0000000000000000 000000d0 0000000000000058 0000000000000000 A 0 0 8 [ 9] .rela.eh_frame RELA 0000000000000000 000003e0 0000000000000030 0000000000000018 I 10 8 8 [10] .symtab SYMTAB 0000000000000000 00000128 00000000000001b0 0000000000000018 11 12 8 [11] .strtab STRTAB 0000000000000000 000002d8 000000000000008f 0000000000000000 0 0 1 [12] .shstrtab STRTAB 0000000000000000 00000410 0000000000000061 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)
前文用objdump -h看到的是一些关键段,不包括像符号表这样的辅助段。
段表的结构也比较简单,是以一个Elf32_Shdr或Elf64_Shdr结构体为元素的数组,结构体也被称为段描述符(Section Descriptor)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 typedef struct { 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_Word sh_name; Elf64_Word sh_type; Elf64_Xword sh_flags; Elf64_Addr sh_addr; Elf64_Off sh_offset; Elf64_Xword sh_size; Elf64_Word sh_link; Elf64_Word sh_info; Elf64_Xword sh_addralign; Elf64_Xword sh_entsize; } Elf64_Shdr;
根据Section Table可以把段给重构出来,如果出现了某些地方的段不连续,可能是对齐的问题。
段的类型
段的标志位
类似于rwx,用对应的位是否为0或1来表示这个段是否具有这个特性
段的链接信息 如果段的类型与链接相关(动态或静态)比如重定位表、符号表等,如下:
对于其他类型的段,这两个成员没有意义
重定位表 链接器在处理目标文件的时候,需要对目标文件中的某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。对于每个需要重定位的代码段或者数据段,都会有一个对应的重定位表。
字符串表 ELF文件中用到了很多字符串,比如段名和变量名,因为字符串的长度是不定的,所以用固定的结构表示很困难,因此把字符串集中存起来放到字符串表,然后用字符串在表中的偏移来引用字符串,如下:
有两个字符串表,一个是字符串表,一个是段表字符串表。前者用于保存普通的字符串,后者用于保存段表用到的字符串,最常见的就是段名。
ELF头文件中,最后一个元素是e_shstrndx:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; 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;
Section header string table index,可以通过这个访问到段表字符串表,然后再结合获得的段表信息,就可以解析整个ELF文件了。
链接的接口——符号 在链接的过程中,统一把函数和变量称为符号,函数名和变量名称为符号名。
每一个目标文件都有一个符号表,记录了目标文件中所用到的符号。每个定义的符号有一个对应的值叫做符号值,对于函数和变量来说,符号值就是地址。
符号表的符号进行分类,如下:
定义在本目标文件的全局符号,可以被其他目标文件引用。
在本目标文件中引用的全局符号,却没有定义在本目标文件。比如simple_section.o中定义了printf()函数,没有实现,这一般叫做外部符号。
段名,这种符号一般是编译器产生,是该段的起始地址。
局部符号,这类符号只有在编译单元内部可见,比如static_var和static_var2.这些符号对链接没有用,一般被忽略。
行号信息,用于调试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int printf (const char * format, ...) ;int global_init_var = 84 ;int global_uninit_var;static int global_static_var;extern int reference_to_out;void func1 (int i) { printf ("%d\n" , i); } int main (void ) { static int static_var = 85 ; static int static_var2; int a = 1 ; int b; func1(static_var + static_var2 + a + b); return a; }
一般关注全局符号,因为这对链接比较重要。
ELF符号表结构 1 2 3 4 5 6 7 8 9 typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Section st_shndx; } Elf32_Sym;
st_name:符号名,这个成员包含了该符号名在字符串表的下标
st_value:符号对应的地址值,可能是绝对值,也可能是一个地址,对不同符号的意义不同
st_size:符号大小,比如对于一个double的符号,占8个字节,如果该值为0,则表示该符号大小为0或未知
st_info: 符号类型喝绑定信息
st_other:This member currently specifies a symbol’s visibility. A list of the values and meanings appears below. The following code shows how to manipulate the values for both 32 and 64-bit objects. Other bits contain 0 and have no defined meaning. 这个标志位应该是最近几年才被规定的,书上显示预留,现在表示符号的可见性。
st_shndx 符号所在的段
st_info 成员低4位表示符号类型,高28位表示符号绑定信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #define STB_LOCAL 0 #define STB_GLOBAL 1 #define STB_WEAK 2 #define STB_NUM 3 #define STB_LOOS 10 #define STB_GNU_UNIQUE 10 #define STB_HIOS 12 #define STB_LOPROC 13 #define STB_HIPROC 15 #define STT_NOTYPE 0 #define STT_OBJECT 1 #define STT_FUNC 2 #define STT_SECTION 3 #define STT_FILE 4 #define STT_COMMON 5 #define STT_TLS 6 #define STT_NUM 7 #define STT_LOOS 10 #define STT_GNU_IFUNC 10 #define STT_HIOS 12 #define STT_LOPROC 13 #define STT_HIPROC 15
st_shndx 如果符号定义在本目标文件中,这个成员变量表示符号所在的下标,但是如果不是定义在本目标文件中,或者对于有些特殊符号,st_shndx的值比较特殊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define SHN_UNDEF 0 #define SHN_LORESERVE 0xff00 #define SHN_LOPROC 0xff00 #define SHN_BEFORE 0xff00 #define SHN_AFTER 0xff01 #define SHN_HIPROC 0xff1f #define SHN_LOOS 0xff20 #define SHN_HIOS 0xff3f #define SHN_ABS 0xfff1 #define SHN_COMMON 0xfff2 #define SHN_XINDEX 0xffff #define SHN_HIRESERVE 0xffff
符号值
在目标文件中,如果是符号的定义且该符号不是COMMON块,,则st_value表示该符号在段中的偏移
如果是该符号是COMMON块则st_value是该符号的对齐属性
在可执行文件中,st_value表示符号的虚拟地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 readelf -s simple_section.o Symbol table '.symtab' contains 18 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS simple_section.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 global_static_var 6: 0000000000000000 0 SECTION LOCAL DEFAULT 5 7: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1804 8: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 static_var2.1805 9: 0000000000000000 0 SECTION LOCAL DEFAULT 7 10: 0000000000000000 0 SECTION LOCAL DEFAULT 8 11: 0000000000000000 0 SECTION LOCAL DEFAULT 6 12: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var 13: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var 14: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 func1 15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf 17: 0000000000000024 51 FUNC GLOBAL DEFAULT 1 main
对照段表来看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 There are 13 section headers, starting at offset 0x478: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000057 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 00000368 0000000000000078 0000000000000018 I 10 1 8 [ 3] .data PROGBITS 0000000000000000 00000098 0000000000000008 0000000000000000 WA 0 0 4 [ 4] .bss NOBITS 0000000000000000 000000a0 0000000000000008 0000000000000000 WA 0 0 4 [ 5] .rodata PROGBITS 0000000000000000 000000a0 0000000000000004 0000000000000000 A 0 0 1 [ 6] .comment PROGBITS 0000000000000000 000000a4 000000000000002a 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 000000ce 0000000000000000 0000000000000000 0 0 1 [ 8] .eh_frame PROGBITS 0000000000000000 000000d0 0000000000000058 0000000000000000 A 0 0 8 [ 9] .rela.eh_frame RELA 0000000000000000 000003e0 0000000000000030 0000000000000018 I 10 8 8 [10] .symtab SYMTAB 0000000000000000 00000128 00000000000001b0 0000000000000018 11 12 8 [11] .strtab STRTAB 0000000000000000 000002d8 000000000000008f 0000000000000000 0 0 1 [12] .shstrtab STRTAB 0000000000000000 00000410 0000000000000061 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)
可以通过Ndx和段表来找到符号所在的段。
具体细节在书中的P85,比较简单。
特殊符号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int printf (const char * format, ...) ;int global_init_var = 84 ;int global_uninit_var;static int global_static_var;extern int reference_to_out;void func1 (int i) { printf ("%d\n" , i); } int main (void ) { static int static_var = 85 ; static int static_var2; int a = 1 ; int b; func1(static_var + static_var2 + a + b); return a; }
这里面有一些符号是程序的开始地址和结束地址等。
符号修饰和函数签名 在这里就是因为早期C和Fortran如果一起用的话,难免用到同名符号,所以就要对符号名进行处理,而到了C++之后,编译的过程会根据更复杂的规则生成符号。
因此如果遇到C和C++混编的时候需要用extern “C”操作来避免找不到对应的符号。
弱符号和强符号 如果多个目标文件中出现重复的符号,链接的时候会出现符号重复定义的错误,这种符号称为强符号。而如果存在一个强符号和多个弱符号,则不会出错。特别注意的是,C和C++编译器默认初始化的全局变量为强符号,未初始化的为弱符号
在这里定义了两个简单的代码文件1.c和2.c:
1 2 3 4 5 6 7 #include <stdio.h> int x = 1 ;int main () { printf ("%d\n" ,x); }
执行:
没有问题,因为2.c中的x是弱符号。
而将2.c改为
再用gcc编译的时候遇到了报错:
1 2 3 /tmp/ccO97LRd.o:(.data+0x0): multiple definition of `x' /tmp/ccVlsyAp.o:(.data+0x0): first defined here collect2: error: ld returned 1 exit status
而我们也可以用如下的方式来定义一个弱符号:
1 2 3 4 5 6 7 #include <stdio.h> __attribute__((weak)) int x = 1 ; int main () { printf ("%d\n" ,x); }
再执行:
规则主要如下:
不允许多个强符号被多次定义
如果一个符号在某个目标文件中是强符号,其他文件中都是弱符号,那么选择强符号
只有多个弱符号则与链接顺序相关
强引用和弱引用 对目标文件外的文件的符号进行引用的时候需要被正确决议,如果没有找到符号的定义,就会报符号未定义错误,这种引用被称为强引用。
对应的是弱引用,在处理弱引用的时候,有就决议,没有就忽视。差别在于对未定义的弱引用,链接器不认为是个错误。
1 __attribute__((weakref))
调试信息 gcc加上-g参数,就可以增加调试信息了,会多了很多debug相关段。