目标文件里有什么

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

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)的形式存储。段就是表示一个定长的区域。下图就是一个目标文件的大概格式,后面会慢慢详细讲解目标文件中的内容。
在这里插入图片描述

  • ELF Header:包含描述整个文件的基本属性,比如文件版本、目标机器型号等。主要描述生成文件的一些基本信息,段的位置和大小等等
  • .text :存放已经编译好的机器码指令
  • .data :已经初始化的全局和局部静态变量。局部变量属于函数私有变量,是不会保存在 .data 段中,只会作为 .text 段中机器指令的操作数
  • .rodata:存放只读数据,一般是程序里的只读变量(如const)和字符串常量(有些编译器放在数据段)。好处有:语义上支持C++ const关键字;安全,操作系统加载的时候将属性映射成只读,防止修改;支持只读存储器(ROM)访问
  • .bss :未初始化的全局和静态变量,或者已经初始化值为0的全局和静态变量。目标文件中不占有实际的磁盘空间。 有些编译器不存放,只是在符号表预留一个符号,链接的时候再在 .bss 段分配空间
  • .comment:存放的是编译器版本信息,比如字符串“GCC:(GNU)4.2.0”
  • .shstrtab(section head string table):段表字符串表,保存段表中用到的字符串,比如段名。字符串的长度往往是不定的,固定表示它比较困难。一种常见的做法是集中存放到一个表里,然后用偏移来表示。
  • Section Table:段表,除文件头(ELF Header)以外最重要的结构,描述了各个段的信息,比如段名、段长度、在文件中的偏移、读写权限和其他属性。编译器、链接器和装载器都是依靠段表定位和访问各个段的属性的。段表的位置由文件头的 “e_shoff” 成员决定。段名对于编译器、链接器有意义,但对于操作系统无实际意义,操作系统的处理由段类型和段的标志位决定
  • .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							;/* 将 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 <main+0x15>
  37:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 3d <main+0x1b>
  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 个字节。

.bss 是不占用可执行文件空间的,其内容由操作系统初始化(清零);
.data 需要占用,其内容由程序初始化。
.bss 段,不为数据分配空间,只是记录数据所需空间的大小;
.bss 段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块,紧跟在 data 段后面。在程序运行时,才会给 .bss 段里面的变量分配内存空间

  其实我们可以通过符号表(后面章节会讲到)看到(紧接着下面也有),只有 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。但是应用程序自定义的段名不能用 “.” 作为前缀,否则容易和系统保留段名冲突。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值