文章作为书籍《程序员的自我修养》的随写笔记,若有不妥之处,望不吝赐教!
目标文件编译源代码后生成的文件叫做目标文件。那么目标文件里面到底存放的是什么呢?
目标文件从结构上来说,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。
现在PC平台流行的可执行文件格式(Executable)主要是Windows下的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format),他们都是COFF(Common file format)的变种。
目标文件就是源代码经过编译后,但未进行链接的那些中间文件(Windows下的 .obj 和 Linux下的 .o),它和可执行文件的内容和结构很相似,所以一般跟可执行文件格式一起采用一种存储格式。从广义上看,可执行文件和目标文件的格式几乎是一样的,所以我们可以广义地将目标文件和可执行文件看成是一种文件类型,在Windows下,我们可以统称它们为PE-COFF文件格式。在Linux下,我们可以将它们统称为ELF文件。
其它不太常见地可执行文件格式还有Inter/Microsoft 的OFM、UNIX 的a.out格式和MS-DOS、.COM格式等。
不光是可执行文件,动态链接库、静态链接库文件都按照可执行文件格式(Windows PE-COFF,Linux下的ELF)存储。静态链接库稍有不同,它是把很多目标文件捆绑在一起形成一个文件,再加上一些索引,可以简单的把它理解为一个包含有很多目标文件的文件包。ELF文件标准里面把系统中采用的ELF格式的文件归为下图4类:
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件(Relocatable File) | 包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据 | Linux的.o和Windows的.obj |
可执行文件(Executable File) | 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名 | 比如/bin/bash文件和Windows的.exe |
共享目标文件(Shared Object File) | 包含可在两种上下文中链接的代码和数据。首先链接器可以将它和其它可重定位文件和共享目标文件链接, 产生新的目标文件。其次动态链接器(Dynamic Linker)可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行 | Linux的.so,Windows的DLL |
核心转储文件(Core Dump File) | 当进程意外终止,系统可以将该进程的地址空见的内容以及终止时的一些其它信息转储到核心转储文件(手动去设置) | Linux 下的 core dump |
Linux下可通过 file 命令查看相应的文件格式。
liang@liang-virtual-machine:/bin$ file bash
bash: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6f072e70e3e49380ff4d43cdde8178c24cf73daa, stripped
liang@liang-virtual-machine:~/cfp$ file client
client.o: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=979e7e8de73c5f61c49bf19cb9dcb20d1744c782, not stripped
目标文件与可执行文件格式跟操作系统和编译器密切相关,所以不同的系统平台下会有不同的格式,但这些格式又大同小异,目标文件格式与可执行文件格式的历史几乎是操作系统的发展史。
目标文件中存有编译后的机器码,数据以及链接所需要一些信息,比如符号表,重定向表等。目标文件中把这些信息按照不同的属性以段/节(section)的形式存储。段就是表示一个定长的区域。下图就是一个目标文件的大概格式,后面会慢慢详细讲解目标文件中的内容。
后面会用这段代码开启接下来的目标文件内容详细分析。
/*
* SimpleSection.c
*/
int printf(const char* format,...);
int golobal_init_var = 84;
int golbal_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;
}
笔者所用的环境:
liang@liang-virtual-machine:~$ cat /proc/version
Linux version 4.15.0-142-generic (buildd@lgw01-amd64-039) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)) #146~16.04.1-Ubuntu SMP Tue Apr 13 09:27:15 UTC 2021
我们使用GCC来编译这个文件(参数 -c 表示只编译不链接)
gcc -c SimpleSection.c
该命令会生成一个SimpleSection.o的目标文件。我们可以看到,该文件类型为可重定位文件。
liang@liang-virtual-machine:~/cfp$ file SimpleSection.o
SimpleSection.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
我们使用 objdump 工具,来查看目标文件的结构和内容。
先简要介绍 objdump 命令的使用方法及一些参数。
-d
从objfile中反汇编那些特定指令机器码的section。
-h
显示目标文件各个section的头部摘要信息。
-H
简短的帮助信息。
-s
显示指定section的完整内容。默认所有的非空section都会被显示。
-S
尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。
-x
显示所可用的头信息,包括符号表、重定位入口。-x 等价于-a -f -h -r -t 同时指定。
-M
这个参数比较重要,x86架构汇编指令一般有两种格式:Intel汇编和AT&T汇编,DOS、Windows使用Intel汇编,而Unix、Linux、MacOS使用AT&T汇编,通过 -M 我们可以手动选择反汇编的格式,每种格式的反汇编是不一样的。
liang@liang-virtual-machine:~/cfp$ objdump -h SimpleSection.o
SimpleSection.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000055 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 00000036 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000da 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000e0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
Size属性:段长度,File off属性:段偏移
每个段的第二行中的“CONTENTS”、“ALLOC”等表示段的各种属性。“CONTENTS”表示该段在文件中存在。我们可以看到BSS段没有“CONTENTS”,表示它实际上在ELF文件中不存在内容。“.note.GNU-stack”段虽然有“CONTENTS”,但它的长度为0,这个段很古怪,我们暂且忽略它,认位它在ELF文件中也不存在。
在《ELF文件格式》ELF文件格式一文中简要介绍了.eh_frame section。这个.eh_frame段中存储着跟函数入栈相关的关键数据。当函数执行入栈指令后,在该段会保存跟入栈指令一一对应的编码数据,根据这些编码数据,就能计算出当前函数栈大小和cpu的哪些寄存器入栈了,在栈中什么位置。
objdump的 -s 参数可以将所有段的内容以16进制的方式打印出来,-d 参数可以将所有包含指令的段反汇编。
liang@liang-virtual-machine:~/cfp$ 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 bf000000 00b80000 0000e800 00000090 ................
0020 c9c35548 89e54883 ec10c745 f8010000 ..UH..H....E....
0030 008b1500 0000008b 05000000 0001c28b ................
0040 45f801c2 8b45fc01 d089c7e8 00000000 E....E..........
0050 8b45f8c9 c3 .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 7520352e .GCC: (Ubuntu 5.
0010 342e302d 36756275 6e747531 7e31362e 4.0-6ubuntu1~16.
0020 30342e31 32292035 2e342e30 20323031 04.12) 5.4.0 201
0030 36303630 3900 60609.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 22000000 00410e10 8602430d ...."....A....C.
0030 065d0c07 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: bf 00 00 00 00 mov $0x0,%edi
15: b8 00 00 00 00 mov $0x0,%eax
1a: e8 00 00 00 00 callq 1f <func1+0x1f>
1f: 90 nop
20: c9 leaveq
21: c3 retq
0000000000000022 <main>:
22: 55 push %rbp
23: 48 89 e5 mov %rsp,%rbp
26: 48 83 ec 10 sub $0x10,%rsp
2a: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
31: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 37
37: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3d
3d: 01 c2 add %eax,%edx
3f: 8b 45 f8 mov -0x8(%rbp),%eax
42: 01 c2 add %eax,%edx
44: 8b 45 fc mov -0x4(%rbp),%eax
47: 01 d0 add %edx,%eax
49: 89 c7 mov %eax,%edi
4b: e8 00 00 00 00 callq 50 <main+0x2e>
50: 8b 45 f8 mov -0x8(%rbp),%eax
53: c9 leaveq
54: c3 retq
最左边一列是偏移量,中间4列是十六进制内容,最右边一列是 .text 段的ASCLL码形式。
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
这里我们可以看到,0x00000055 十进制为 85。0x00000054 十进制为84。对照上面的C代码用例,刚好是全局变量 golobal_init_var的值和局部静态变量 static_var 的值。而 0x25640a00,刚好是字符串常量 “%d\n” 的ASCLL字节,最后以“\0”结尾。
另外值得一提的是,有时候(很少)编译器会把字符串常量放到 “.data”段,一般放在 “.rodata” 段。
.bss 段存放的是未初始化的全局变量和局部静态变量,如上述代码中的 global_uninit_var 和 static_var2 就是被存放在 .bss 段。其实更准确的说法是 .bss 段为它们预留了空间。但是实际上,我们看到,.bss段的大小只有4个字节,并不是8个字节。
其实我们可以通过符号表(后面章节会讲到)看到(紧接着下面也有),只有 static_var2 被存放在了 .bss 段,而 global_uninit_var 却没有被存放在任何段,只是一个未定义的 “COMMON” 符号。这其实跟不同语言与不同的编译器实现有关系。有些编译器,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在. bss 段分配空间。我们将在 “弱符号与强符号” 和 “COMMON块” 这两个章节深入分析这个问题。原则上讲,我们可以简单地把它当作全局未初始化变量存放在 .bss 段。(这里剧透一下,实际上,未初始化全局变量最终是被放在 BSS 段的)
liang@liang-virtual-machine:~/cfp$ objdump -x -s -d SimpleSection.o
SimpleSection.o: file format elf64-x86-64
SimpleSection.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 00000055 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 00000036 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000da 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000e0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 SimpleSection.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000004 l O .data 0000000000000004 static_var.1840
0000000000000000 l O .bss 0000000000000004 static_var2.1841
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 golobal_init_var
0000000000000004 O *COM* 0000000000000004 golbal_uninit_var
0000000000000000 g F .text 0000000000000022 func1
0000000000000000 *UND* 0000000000000000 printf
0000000000000022 g F .text 0000000000000033 main
除了.text、.data、.bss这三个最常用的段之外,ELF文件也有可能包含其他的段,用来保存与程序相关的其他信息。
.init: 该节包含进程初始化时要执行的程序指令;当程序开始运行时,系统会在进程进入主函数之前先执行这一个节中的指令代码;
.fini: 该节中包含进程终止时要执行的指令代码;当程序退出时,系统会执行这个节中的指令代码;
.dynamic: 该节中包含动态链接信息,并且可能有SHF_ALLOC和SHF_WRITE等属性;
.dynstr : 该节中包含用于动态链接的字符串,一般是那些与符号表相关的动态符号的名字;
.dynsym : 该节中包含动态链接符号表;
.got : 该节中包含全局偏移表(Global Offset Table),存放外部变量的地址,亦是类似相对_GLOBAL_OFFSET_TABLE_的偏移,这是链接器为外部符号填充的实际偏移表;GOT表中的地址需要动态链接器在装载模块,进行地址重定位时进行填充。在访问外部符号,可以先通过相对地址找到GOT表中相关的项,再从中取出最终地址
.plt : 该节中包含函数链接表(Procedure Linkage Table),主要由如下作用:(1)调用链接器来解析某个外部函数的地址, 并填充到.got.plt中, 然后跳转到该函数; 或者直接在.got.plt中查找并跳转到对应外部函数(如果已经填充过).
.got.plt 它包含目标地址(在它们被查找之后)或.plt中触发查找的地址。第一项保存的是“.dynamic”段的地址,第二项保存的是本模块的ID,第三项保存的是_dl_runtime_resolve()的地址。
.data.rel.ro 保存的是程序的只读数据,与.rodata类似,唯一不同的是它在重定位时会被改写,然后将会被置为只读。
.hash : 该节中包含一张哈希表,用于动态段中查找动态符号;
.interp : 该节中包含ELF文件解析器的路径名;如果该节被包含在某个可装载的段中,那么该节的属性中应设置SHF_ALLOC标志位;
.strtab : 该节用于存放字符串,主要是那些符号表项的名字;如果一个目标文件中有一个可装载的段,并且其中含有符号表,则该节的属性中应该有SHF_ALLOC属性;
.symtab : 该节用于存放符号表。在程序中被定义和引用的函数名和全局变量名都属于符号;如果一个目标文件中有一个可装载的段,并且其中含有符号表,则该节的属性中应该有SHF_ALLOC属性;
.shstrtab: 该节是节名字表,含有所有其它节的名字;
.comment: 该节中包含版本控制信息;
.line : 该节中包含调试信息,包括哪些调试符号的行号,为程序指令码与源文件的行号建立联系;
.note : 该节中包含注释;
rel.dyn节的每个表项对应了除了外部过程调用的符号以外的所有重定位对象;
.rel.plt节的每个表项对应了所有外部过程调用符号的重定位信息。
这些段的名字都是由 "."作为前缀,表示这些表的名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名。比如我们可以在ELF文件中插入一个 “music”的段,里面存放了一首MP3音乐,当ELF文件运行起来后可以读取这个段播放这首MP3。但是应用程序自定义的段名不能用 “.” 作为前缀,否则容易和系统保留段名冲突。