第三章 目标文件
3.1 目标文件格式
编译器编译源代码后生成的文件叫做目标文件.从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或地址还没有调整.它本身就是按照可执行文件格式存储的,只是和真正的可执行文件在结构上稍有不同.(在Windows中文件格式为PE-COFF,Linux中文件格式为ELF)
不光是可执行文件,动态链接库(DLL,Dynamic Linking Library)(Windows的**.dll和Linux的.so**)以及静态链接库(Static Linking Library)(Windows的**.lib和Linux的.a)文件也是按照可执行文件格式存储.只是静态链接库稍有不同,它是将很多目标文件捆绑在一起形成一个文件**,再加上一些索引,(可以将其理解为包含有很多目标文件的文件包).
ELF文件类型 | 说明 | 示例 |
---|---|---|
可重定位文件 (Relocatable File) | 这类文件包含了代码和数据,可以被用来链接为可执行文件与共享目标文件,静态链接库也可以归为这一类 | Linux的.o Windows的.obj |
可执行文件 (Executable File) | 这种文件包含了可直接执行的程序,它的代表就是ELF可执行文件,一般没有扩展名 | 如/bin/bash文件 Windows的.exe |
共享目标文件 (Shared Object File) | 这种文件包含了代码和数据,可以在以下两种情况下使用: (1)连接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件. (2)动态连接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来执行 | Linux的.so,如/lib/glibc-2.5.so Window的DLL |
核心转储文件 (Core Dump File) | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 | Linux的core duMP |
3.2 目标文件是什么样的
目标文件中的内容至少有编译后的机器指令代码,数据,以及其他链接时需要的信息,如符号表,调试信息,字符串等.
程序源代码编译后的机器指令经常被放在代码段(Code Section)中.代码段常见的名字有<*.code>或<*.text">;全局变量和局部静态变量经常放在数据段(Data Section,<*.data>),如果是未初始化的全局变量和局部静态变量一般放在一个叫".bss
"的段中.我们知道未初始化的全局变量和局部静态变量默认值都为0,本来它们也可以被放在.data段中,但因为它们是0,所以在.data段中为其分配空间存放0是没有必要的.而.bss
段也只是为未初始化的全局变量以及局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间.
![](https://i-blog.csdnimg.cn/blog_migrate/5672ad92654be3d40df256ea9fe18f95.jpeg)
总体来说,程序源代码被编译之后主要分为两种段:程序指令和程序数据.代码段属于程序指令,而数据段属于程序数据.
为什么将程序的指令和数据分开存放?
- 当程序被装载后,数据和指令会被分别映射到两个虚存区域.数据区域对进程来说是可读写的,但指令区域对于进程来说是只读的,所以这两个虚存区域的权限被设置成可读写和只读,可以防止程序指令被有意或无意改写
- 程序的指令和数据被分开存放可以提高CPU的缓存命中率.现代CPU的缓存一般都将数据缓存和指令缓存分离.
- 最重要的一点:如果系统中运行了多个同一程序的副本,由于它们的指令都是一样的,所以内存中只需要保存一份改程序的指令部分,其他资源,如图片,文本等是可以共享的.这种分开存放的方法可以节省大量空间
3.3 例程分析:SimpleSection.o
C代码清单如下:
/*
* SimpleSection.c
* Linux:
* gcc -c SimpleSection.c
*/
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;
}
使用gcc编译这个文件:
$ gcc -c SimpleSection.c
使用以下命令查看目标文件的结构与内容:
$ objdump -h SimpleSection.o
-h
就是将ELF文件的各个段的基本信息打印出来.也可以使用objdump -x
显示更多的消息,但输出的消息太复杂.从上图可以看出,SimpleSection.o的段除了最基本的代码段,数据段以及BSS段以外,还有只读数据段(.rodata),注释信息段(.comment)和堆栈提示段(.note.GNU-stack).
上图中,Size
为段的长度,File Offset
为段所在的位置,每个段的第二行CONTENTS
表示该段在文件中存在,可以看到BSS段就没有"CONTENTS". .note.GNU-stack
虽然有CONTENTS,但它的长度为0,暂且忽略,也可以认为它不存在.那么ELF文件中实际存在的就是.text
,.data
,.rodata
和.comment
这四个段.
-
.text代码段
objdump -s -d SimpleSection.o
-s
可以将所有段的内容以十六进制的方式打印出来,-d
可以将所有包含指令的段反汇编
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...
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>
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
其中,Contents of section .text:
就是代码段.text的数据以十六进制方式打印出来的内容.为方便查看,单独复制下来:
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...
...
...
最左边一列是偏移量,中间一列是十六进制内容,最右边一列是.text段的ASCII码形式
对照下面的反汇编结果(为方便观看,也将其单独复制出来):
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
可以很明显的看出来.text里包含的就是SimpleSection.c里两个函数func1和main()的指令.
-
数据段和只读数据段
.data段保存的是已经初始化了的全局静态变量和局部静态变量.SimpleSection.c中有两个这样的变量:global_init_var
static_var.
两个变量每个4字节,一共8字节,正好.
-
BSS段
.bss段存放的是未初始化的全局变量和局部静态变量.如上述代码中
global_uninit_var
,static_var2
就是被存放在.bss段.其实更准确的说法式.bss段为它们预留了空间.观察上图我们可以看到,.bss段的size只有4个字节,global_uninit_var
,static_var2
两个变量加起来应该8字节.这其实是因为只有static_var2被存放bss段,而global_uninit_varmei没有放在这个段(实际上没有放在任何段),它只是一个未定义的"COMMON符号",造成这种结果的主要原因在于编译器.有些编译器会将未初始化的全局变量存放在bss,有些不存放,只预留一个未定义的全局变量符号,等到最终链接称可执行文件的时候再在.bss段为其分配空间(但其实我们也可以理解未它存放在bss段).一个小测试,查看以下代码:
static int x1=0; static int x2=1;
x1和x2会存放在哪里?答案是x1在bss,x2在data.因为x1为0,可以认为是未初始化的,因为未初始化的都是0,所以被优化掉了,放在bss,节省磁盘空间(bss不占磁盘空间.)
-
其他段…略
-
ELF文件描述
ELF目标文件的总体结构如下:
![](https://i-blog.csdnimg.cn/blog_migrate/6861c0953c88ff7fa3d0274ca8bf6d30.jpeg)
ELF Header为文件头,包含整个文件的基本属性,如ELF文件版本,目标机器型号,程序入口地址等.
接下来是ELF文件的各个段.其中ELF文件中与段有关的重要结构就是Section header table
,即段表,它描述了ELF文件包含的所有段的信息,比如每个段的段名,段的长度,在文件中的偏移,读写权限以及段的其他属性.
段表是ELF文件中除了文件头以外最重要的结构.可以说,ELF文件的段结构就是由段表决定的,编译器,链接器和装载器都是依靠段表来定位和访问各个段的属性的.