目标文件里有什么

文章作为书籍《程序员的自我修养》的随写笔记,若有不妥之处,望不吝赐教!

1、目标文件

  目标文件编译源代码后生成的文件叫做目标文件。那么目标文件里面到底存放的是什么呢?
  目标文件从结构上来说,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

2、目标文件的格式

  现在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

  目标文件与可执行文件格式跟操作系统和编译器密切相关,所以不同的系统平台下会有不同的格式,但这些格式又大同小异,目标文件格式与可执行文件格式的历史几乎是操作系统的发展史。

3、目标文件是什么样的

  目标文件中存有编译后的机器码,数据以及链接所需要一些信息,比如符号表,重定向表等。目标文件中把这些信息按照不同的属性以段/节(section)的形式存储。段就是表示一个定长的区域。下图就是一个目标文件的大概格式,后面会慢慢详细讲解目标文件中的内容。
目标文件里有什么_第1张图片

  • ELF 头:包含描述整个文件的基本属性,比如文件版本、目标机器型号等。主要描述生成文件的一些基本信息,段的位置和大小等等。
  • .text :存放已经编译好的机器码指令。
  • .data :已经初始化的全局和局部静态变量。局部变量是保存在栈中。
  • .rodata:存放只读数据,一般是程序里的只读变量(如const)和字符串常量(有些编译器放在数据段)。好处有:语义上支持C++ const关键字;安全,操作系统加载的时候将属性映射成只读,防止修改;支持只读存储器(ROM)访问。
  • .bss :未初始化的全局和静态变量,或者已经初始化值为0的全局和静态变量。目标文件中不占有实际的磁盘空间。 有些编译器不存放,只是在符号表预留一个符号,链接的时候再在 .bss 段分配空间。
  • .comment:存放的是编译器版本信息,比如字符串“GCC:(GNU)4.2.0”
  • .shstrtab(section head string table):段表字符串表,保存段表中用到的字符串,比如段名。字符串的长度往往是不定的,固定表示它比较困难。一种常见的做法是集中存放到一个表里,然后用偏移来表示。
  • Section Table:段表,除文件头以外最重要的结构,描述了各个段的信息,比如段名、段长度、在文件中的偏移、读写权限和其他属性。编译器、链接器和装载器都是依靠段表定位和访问各个段的属性的。段表的位置由文件头的“e_shoff”成员决定,图中位于偏移0x118。段名对于编译器、链接器有意义,但对于操作系统无实际意义,操作系统的处理由段类型和段的标志位决定。
  • .symtab: 符号表,存放程序中定义和引用的函数和全局变量信息。在链接中,我们将函数和变量统称为符号,函数名和变量名就是符号名。此外,符号还包括文件名、段名和行号(可选)。每个目标文件都有一个符号表,记录了所有的符号。每个符号都有一个对应的符号值,对于函数和变量来说,符号值就是地址。
  • .rel.text .rel.data:重定位表, 目标文件中某些部分需要重定位,即代码段和数据段中的绝对地址的引用位置是存在这块。 还存在其他的section,比如.debug、.line等等不过不是本次学习的重点,先可以省略。

Tips:程序的指令和数据为什么要分开存放?
  • 数据区对于进程来说是可读可写的,而指令区是只读的,分开存放,方便设置存储区域的权限。
  • 计算机中通常会有强大的缓存(Cache),一般被设计为指令Cache和数据Cache,程序的指令和数据分开存放对CPU缓存命中率提高有好处。
  • 最重要的原因,内存共享。当系统中运行着多个该程序的副本时,它们指令都是一样的,所以内存中只需要保存一份该程序的指令部分。对于指令这种只读区域来说是这样,对于其它的只读数据也一样。比如很多程序里面带有的图标、图片、文本等资源也是属于可以共享的。

3.1 详细探究目标文件的细节

  后面会用这段代码开启接下来的目标文件内容详细分析。

/*
 * 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文件中也不存在。

  • comment:注释信息段
  • .note.GNU-stack:堆栈提示段

在《ELF文件格式》ELF文件格式一文中简要介绍了.eh_frame section。这个.eh_frame段中存储着跟函数入栈相关的关键数据。当函数执行入栈指令后,在该段会保存跟入栈指令一一对应的编码数据,根据这些编码数据,就能计算出当前函数栈大小和cpu的哪些寄存器入栈了,在栈中什么位置。

3.2 代码段

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码形式。

3.3 数据段和只读数据段

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” 段。

3.4 BSS段

  .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

3.5 其它段

  除了.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。但是应用程序自定义的段名不能用 “.” 作为前缀,否则容易和系统保留段名冲突。

你可能感兴趣的:(程序员的自我修养——链接,装载与库,c语言,内核,编译,链接,装载)