目标文件是源代码编译后但未进行链接的中间文件,与可执行文件有相同的结构个格式。在Linux下为ELF(Executable Linkable Format)文件,Windows下主要为PE(Portable Executable)格式。
一、目标文件结构
目标文件中含有编译后的机器指令代码、数据还有一些链接时需要的符号表、字符串等。目标文件将这些信息按属性以段(Section or Segment)的形式存储。
主要部分:
| Segment | Content |
|---|---|
| .text | 代码 |
| .data | 已经初始化的全局变量和局部静态变量 |
| .bss | 未初始化的全局变量和局部静态变量或者初始化为0的(全局变量/局部静态变量)的大小总和 |
| 为什么程序指令和程序数据要分开两个段存放 |
- 数据段和代码段会被映射到两个不同的虚拟内存区域。
- 代码段对于进程只读,数据段可读可修改,分开两个段设置不同的权限,防止指令被恶意修改。
- CPU缓存有数据缓存和指令缓存,分开存储可以提高缓存命中率。
- 同一个程序的多个副本进程他们的程序指令是相同的,运行时内存中只需要一份只读的指令,其他只读的资源也是。
目标文件结构详解
int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main(void)
{
static int static_init_var = 85;
static int static_uninit_var;
int a = 1;
int b;
func1(static_init_var + static_uninit_var + a + b);
return a;
}
objdump -h source.o 查看目标文件结构
source.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000005f 0000000000000000 0000000000000000 00000040 2**0 代码段
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 000000a0 2**2 数据段
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a8 2**2 BSS段
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a8 2**0 只读数据段
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002b 0000000000000000 0000000000000000 000000ac 2**0 编译器版本信息
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000d7 2**0 堆栈提示段
CONTENTS, READONLY
6 .note.gnu.property 00000020 0000000000000000 0000000000000000 000000d8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .eh_frame 00000058 0000000000000000 0000000000000000 000000f8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
段属性 CONTENTS 该段在目标文件中存在,BSS段没有CONTENTS属性,BSS在目标文件中不存在内容。 BSS段只是未初始化的全局变量和局部静态变量或者初始化为0的(全局/局部静态)变量的大小总和。 程序运行时会根据BSS段记录的内存大小,为这些变量申请内存空间。
size source.o 查看ELF文件中各个段的大小
text data bss dec hex filename
219 8 4 231 e7 source.o
代码段 数据段和只读数据段
- .data段:已经初始化的全局变量和局部静态变量
- .rodata段:字符串常量
- const 修饰的变量会被存放在只读数据段
指定变量或代码存放到特定的段中:
__attribute__((section("FOO"))) void foo()
{
}
二、ELF文件结构描述
1. 文件头
readelf -h source.o # -h header file 查看文件头
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 # ABI版本
Type: REL (Relocatable file) # ELF文件类型(可重定位文件)
Machine: Advanced Micro Devices X86-64 # 硬件平台
Version: 0x1 # 硬件平台版本
Entry point address: 0x0 # 入口地址
Start of program headers: 0 (bytes into file) # 程序头入口
Start of section headers: 1184 (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: 14 # 段表描述符数量 ELF文件中段的数量
Section header string table index: 13 # 段表字符串表所在的段在段表中的下标
ELF文件头描述了整个文件的一些基本信息,最重要的是段表在ELF中的地址偏移量和ELF文件中段的数量。段表相当于一个数组,每一个元素都是一个段的段描述符,段描述符记录一个段的基本属性。 目标文件没有程序头 Program Header,关于Program header的更多信息在 Chapter-6。
补充: ELF文件类型有:
| 类型 | 说明 | 实例 |
|---|---|---|
| REL (Relocatable file) | 可重定位文件,包含代码和数据,可以被用来链接成可执行文件或共享目标文件 | .o 或 .obj 文件 |
| DYN (Shared object file) | 共享目标文件,包含代码和数据。 | .so 或 dll 文件 |
| EXEC (Executable file) | 可执行文件 | /bin/bash 文件 |
| CORE (Core file) |
2. 段表
段表是段描述符的数组,段描述符记录了ELF的各个段的信息(段的名字、段的长度、段在文件中的偏移、读写权限等)。
readelf -S source.o # -s Segment 查看段表
There are 14 section headers, starting at offset 0x4a0:
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 000000000000005f 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000380 0000000000000078 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000a0 0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a8 0000000000000004 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a8 0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000ac 000000000000002b 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000d7 0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 000000d8 0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 000000f8 0000000000000058 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 000003f8 0000000000000030 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000150 00000000000001b0 0000000000000018 12 12 8
[12] .strtab STRTAB 0000000000000000 00000300 0000000000000080 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000428 0000000000000074 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)
- 段描述符大小固定,所以Name字段(sh_name)只是字符串在 .shstrtab 段表字符串表中的偏移。
- 段偏移Offset(sh_offset)表示段在ELF文件中的偏移,BSS段和只读数据段的Offset想同,因为BSS不存在与ELF文件在,无意义。
- 类型 NOBITS 表示该段在文件中没有内容,比如BSS段。
- 段的标志位表示该段在进程虚拟地址空间中的属性。A (alloc) 表示该段在进程空间中需要分配空间。
- RELA 可重定位类型段。Link 字段表示符号表的下标,Info字段表示它是哪个段的重定位段(表)。
- Address 是虚拟空间地址,链接前虚拟空间地址还未分配,所以全都是 0.
3. 可重定位表
source.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000017 R_X86_64_PC32 .rodata-0x0000000000000004
0000000000000021 R_X86_64_PLT32 printf-0x0000000000000004
000000000000003d R_X86_64_PC32 .data
0000000000000043 R_X86_64_PC32 .bss-0x0000000000000004
0000000000000056 R_X86_64_PLT32 func1-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
0000000000000040 R_X86_64_PC32 .text+0x0000000000000028
结合目标文件汇编代码:
0000000000000028 <main>:
0000000000000028: f3 0f 1e fa endbr64
000000000000002c: 55 push %rbp
000000000000002d: 48 89 e5 mov %rsp,%rbp
0000000000000030: 48 83 ec 10 sub $0x10,%rsp
0000000000000034: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) # 初始化变量 a
000000000000003b: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 41 <main+0x19>
0000000000000041: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 47 <main+0x1f>
0000000000000047: 01 c2 add %eax,%edx
0000000000000049: 8b 45 f8 mov -0x8(%rbp),%eax
000000000000004c: 01 c2 add %eax,%edx
000000000000004e: 8b 45 fc mov -0x4(%rbp),%eax
0000000000000051: 01 d0 add %edx,%eax
0000000000000053: 89 c7 mov %eax,%edi
0000000000000055: e8 00 00 00 00 callq 5a <main+0x32>
000000000000005a: 8b 45 f8 mov -0x8(%rbp),%eax
000000000000005d: c9 leaveq
000000000000005e: c3 retq
可以看到Offset 0x0056 正是对 func1 函数的地址引用,链接前留空为 00 00 00 00,需要在链接的时候重定位。 重定位只修正全局符号和外部符号。
链接器在链接的时候,需要对目标文件中的绝对地址的引用进行修正(例如:printf, func1 外部变量的地址引用)。这些重定位信息都记录在对应的重定位段中(重定位表)。代码段 .text 对应的重定位段就是 .rela.text 。
4. 字符串表和段表字符串表
ELF中所有的字符串(变量名、函数名等)会集中存放在字符串表中。在段表中对这些字符串的引用就可以这些字符串在字符串表中的偏移量来代替。
Hex dump of section '.strtab':
0x00000000 00736f75 7263652e 63007374 61746963 .source.c.static
0x00000010 5f696e69 745f7661 722e3139 32320073 _init_var.1922.s
0x00000020 74617469 635f756e 696e6974 5f766172 tatic_uninit_var
0x00000030 2e313932 3300676c 6f62616c 5f696e69 .1923.global_ini
0x00000040 745f7661 7200676c 6f62616c 5f756e69 t_var.global_uni
0x00000050 6e69745f 76617200 61646472 0066756e nit_var.addr.fun
0x00000060 6331005f 474c4f42 414c5f4f 46465345 c1._GLOBAL_OFFSE
0x00000070 545f5441 424c455f 00707269 6e746600 T_TABLE_.printf.
0x00000080 6d61696e 0076616c 756500 main.value.
Hex dump of section '.shstrtab':
0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
0x00000010 002e7368 73747274 6162002e 72656c61 ..shstrtab..rela
0x00000020 2e746578 74002e64 61746100 2e627373 .text..data..bss
0x00000030 002e7265 6c612e64 6174612e 72656c2e ..rela.data.rel.
0x00000040 6c6f6361 6c002e72 6f646174 61002e63 local..rodata..c
0x00000050 6f6d6d65 6e74002e 6e6f7465 2e474e55 omment..note.GNU
0x00000060 2d737461 636b002e 6e6f7465 2e676e75 -stack..note.gnu
0x00000070 2e70726f 70657274 79002e72 656c612e .property..rela.
0x00000080 65685f66 72616d65 00 eh_frame.
5. 符号表
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 source.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_init_var.1920
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_uninit_var.1921
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 9
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 40 FUNC GLOBAL DEFAULT 1 func1
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
17: 0000000000000028 55 FUNC GLOBAL DEFAULT 1 main
符号表中包含的内容:
- 定义在本目标文件的全局符号,可以被其他目标文件引用。例如:global_init_var、main、func1等。
- 定义在外部文件,在本文件中引用的全局符号,也称为外部符号。例如:printf。
- 局部符号(static_init_var)。
- 段名。
说明:
-
Value 是符号的值,对于变量或者函数来说(如果是在目标文件中)就是他的地址(所在段的偏移地址)例如:global_init_var 是 .data 段的第一个数据所以它的 Value 是 0x000000,大小为4个字节,static_init_var 是 .data 段的第二个数据,它的Value就是0x000004。
Hex dump of section '.data': 0x00000000 54000000 55000000 T...U... -
如果是可执行文件,Value 就是符号最后的虚拟地址。
54: 0000000000001129 24 FUNC GLOBAL DEFAULT 14 func 55: 0000000000001170 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init 56: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 24 _end 57: 0000000000001040 47 FUNC GLOBAL DEFAULT 14 _start 58: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 24__bss_start 59: 0000000000001141 37 FUNC GLOBAL DEFAULT 14 main -
Type 指代了符号的类型,段(SECTION)或对象(OBJECT)或函数(FUNC)。
-
Ndx 指代符号所在的段的下标,初始化的全局变量(global_init_var)和局部静态变量(static_init_var)都在 .data 段(3)。printf 属于未定义的外部变量(UND)。未初始化的全局变量(global_uninit_var)被放在了 COMMON 块,这是因为在最终链接为一个可执行文件的时候(也是ELF文件格式),才会在 .bss 段分配空间。
6. 强符号和弱符号
extern int ext; // 外部变量 不是强符号 也不是 弱符号
int weak; // 为初始化的全局变量 弱符号
int strong = 1; // 初始化的全局变量 强符号
__attribute__((weak)) weak2 = 2; // 指定为弱符号
int main()
{
return 0;
}
- 不允许强符号被多次定义
- 如果一个符号在一个目标文件中是强符号,在其他目标文件中都是弱符号,那么他是强符号。
- 如果一个符号在所有目标文件中都是弱符号,那么选中之中占用空间大的。
- 强符号引用如果未定义链接会报错误符号未定义
- 弱符号引用如果未定义,不会报错,链接器会默认其为0(弱符号 global_uninit_var)或者一个特殊值。
- 变量 a b 是在栈上的,b 未初始化,值随机。