目标文件有什么
hahahanba Lv1

目标文件里有什么

目标文件的格式

现在PC平台流行的可执行文件格式(Executable)主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows下的.obj和Linux下的.o),它跟可执行文件的内容与格式很相似,所以一般与可执行文件采用一种格式存储。

COFF的主要贡献是在目标文件里面引入了“段”的机制,不同的目标文件可以拥有不同数量及不同类型的“段”。另外,它还定义了调试数据格式。

不光是可执行文件(Windows的.exe和Linux下的ELF可执行文件)按照可执行文件格式存储。动态链接库(DLL,Dynamic Linking Library)(Windows的.dll和Linux的.so)及静态链接库(Static Linking Library)(Windows的.lib和Linux的.a)文件都按照可执行文件格式存储。它们在Windows下都按照PE-COFF格式存储,Linux下按照ELF格式存储。下表为系统中采用的ELF文件格式类型:

ELF文件类型 说明 实例
可重定位文件(Relocatable File) 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类 Linux的.o Windows的.obj
可执行文件(Executable File) 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,他们一般都没有扩展名 比如/bin/bash文件 Windows的.exe
共享目标文件 (Shared Object File) 这种文件包含了代码和数据,可以在以下两种情况使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接,产生可以将几个这种共享目标文件与可执行文件结合,作为进程影响的一部分来运行 Linux的.so,如/lib/glibc-2.5.so Windows的DLL
核心转储文件(Core Dump File) 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 Linux下的从core dump

在Linux下,我们可以使用file命令来查看相应的文件格式:

1
2
3
4
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ file /bin/bash
bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6386b644ab2d987986aeb40325a787a035a4f0d8, stripped

目标文件是什么样的

程序的源代码编译后的机器指令经常被放倒代码段(Code Section)里,代码段中常见的名字有.code,.text;全局变量和局部静态变量数据经常放在数据段(Data Section),数据段的一般名字都叫.data。一个简单的程序被编译成目标文件后的结构如下图所示:

image)

下面让我们来看看目标文件中具体会有什么。

挖掘SimpleSection.o文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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_var = 85;
static int static_var2;

int a = 1;
int b;

func1(static_var + static_var2 + a+ b);

return a;
}

我们可以先将上述程序生成.o文件,然后利用objdump命令查看其ELF文件段的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ gcc -c SimpleSection.c
$ objdump -h SimpleSection.o // 参数-h可以打印ELF文件各个段的基本信息

SimpleSection.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 00000004 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

从上面的结果来看,SimpleSection.o的段的数量比我们想象中的多,除了最基本的代码段、数据段和BSS段以外,还有4个段分别为只读数据段.rodata、注释信息段.commit、堆栈提示段.note.GNU-stack和堆栈回溯段.eh_frame。每个段的第一行容易理解的是段的长度(Size)和段所在的位置(File Offset),每个段中的第二行中的CONTENTSALLOC等标识段的各种属性,CONTENTS表示该段在文件中存在。我们可以看到BSS段没有CONTENTS,表示实际上在ELF文件中不存在内容。.note.GNU-stack段虽有CONTENTS,但长度为0。那么在ELF文件中实际存在的也就是.text.data.rodata.comment.eh_frame这5个段了。只考虑前四个段,那么它们在ELF中的结构如下图所示。

image

代码段

我们可以利用objdump来查看代码段的内容。Contents of section .text 就是.text的十六进制内容,总共0x57字节。

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
// -s可以将所有段的内容以十六进制的方式打印出来
// -d可以将所有包含指令的段反汇编
$ objdump -s -d SimpleSection.o
SimpleSection.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...
......
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>
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 <main+0x15>
39: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3f <main+0x1b>
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

数据段和只读数据段

1
2
3
4
5
6
......
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
......

.data段保存的是那些已经初始化了的全局变量和局部静态变量,分别为global_init_varabalstatic_var,这两个变量每个4字节,一共8个字节,即54000000对应于十进制84,55000000对应于十进制85。另外,我们在程序中用到了一个字符串常量“%d\n”,它是一种只读数据,所以放到了.rodata段。

.rodata段存放的只读数据一般是程序里面的只读变量(如const修饰的变量)和字符串常量。单独设立“.rodata”不光在语意上支持了c++的const关键字,而且操作系统在加载的时候可以将“.rodata”段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理,保证了程序的安全性。

BSS段

.bss段保存的是未初始化的全局变量和局部静态变量,如global_uninit_varabalstatic_var2。可以看到两个变量共占有8个字节,实际上该段的大小为4字节,这是因为不同的语言与不同的编译器实现有关,有的编译器会将全局的未初始化变量存放在.bss段,有的只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。(编译单元内部可见的静态变量的确是存放在.bss段)

1
2
3
4
Sections:
Idx Name Size VMA LMA File off Algn
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2
ALLOC

其他段

常用的段名 说明
.rodata1 跟.rodata一样
.comment 编译器版本信息,如GCC: (Ubuntu 7. 5.0-3ubuntu1~18.04) 7.5.0.
.debug 调试信息
.dynamic 动态链接信息
.hash 符号哈希表
.line 调试时的行号表,即源代码行号与编译后指令的对应表
.note 额外的编译器信息。如程序的公司名、发布的版本号等
.strtab String Table字符串表,用于存储ELF文件中用到的各种字符串
.symtab Symbol Table符号表
.shstrtab Section String Table段名表
.plt .got 动态链接的跳转表和全局入口表
.init .fini 程序初始化与终结代码段

参考资料

  • 程序员的自我修养-链接,装载与库
 评论