程序员的自我修养之目标文件里有什么

210 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

这一系列是《程序员的自我修养》的阅读笔记:

程序员的自我修养之程序的编译和链接

目标文件的格式

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

ELF文件标准里面把系统中采用ELF格式的文件归为以下所列举的4类。

image.png

可以在 Linux 下使用 file 命令来查看相应的文件格式。我们查看上一篇中我们编译得到的 hello.o ,以及链接后得到的 a.out 的文件格式。

$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=dac95660539d9802b772d4658acead0886d28144, not stripped

目标文件是什么样的

一般目标文件将这些信息按不同的属性,以“段”(Section)的形式存储。

让我们来看一个简单的程序被编译成目标文件后的结构,如下图所示。

image.png

编译后的结构主要的段有以下几个:

  • File Header:文件头,它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表(Section Table),段表是一个描述文件中各个段的数组。
  • .text section:代码段,保存程序的指令。
  • .data section:数据段,已初始化的全局变量和局部静态变量都保存在这里。
  • .bss section:未初始化的全局变量和局部静态变量一般放在这里。.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中不占据空间。

目标文件格式实例

我们在原来的 hello.c 程序添加一些变量,编译成 hello.o 后观察它的文件格式。

#include <stdio.h> 

int main() { 
    static int static_var = 1;
    static int static_var2;
    
    int a = 1;
    int b;
    printf("Hello World\n"); 
    return 0; 
}

可以使用 binutils 的工具 objdump 来查看 object 内部的结构。参数“-h”就是把ELF文件的各个段的基本信息打印出来。

$ gcc -c hello.c
$ objdump -h hello.o

image.png

从上面的结果来看,hello.o的段的数量比我们想象中的要多,除了最基本的代码段、数据段和BSS段以外,还有只读数据段(.rodata)、注释信息段(.comment)、堆栈提示段(.note.GNU-stack)和供系统运行时调试使用的.eh_frame段。

段的属性,其中最容易理解的是段的长度(Size)和段所在的位置(File Offset),每个段的第2行中的“CONTENTS”、“ALLOC”等表示段的各种属性,“CONTENTS”表示该段在文件中存在。我们可以看到BSS段没有“CONTENTS”,表示它实际上在ELF文件中不存在内容。

代码段

objdump的“-s”参数可以将所有段的内容以十六进制的方式打印出来,“-d”参数可以将所有包含指令的段反汇编。

分析一下关于代码段的内容.

image.png

“Contents of section .text”就是.text的数据以十六进制方式打印出来的内容,总共0x20字节,跟前面我们了解到的“.text”段长度相符合。最左面一列是偏移量,中间4列是十六进制内容,最右面一列是.text段的ASCII码形式。对照下面的反汇编结果,可以很明显地看到,.text段里所包含的正是hello.c里函数main()的指令。

image.png

数据段和只读数据段

.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。前面的hello.c代码里面有一个这样的变量static_var。这个变量4个字节,所以“.data”这个段的大小为4个字节。

hello.c里面我们在调用“printf”的时候,用到了一个字符串常量“Hello World\n”,它是一种只读数据,所以它被放到了“.rodata”段。

“.rodata”段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。单独设立“.rodata”段有很多好处,不光是在语义上支持了C++的const关键字,而且操作系统在加载的时候可以将“.rodata”段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理,保证了程序的安全性。另外在某些嵌入式平台下,有些存储区域是采用只读存储器的,如ROM,这样将“.rodata”段放在该存储区域中就可以保证程序访问存储器的正确性。

BSS段

.bss段存放的是未初始化的全局变量和局部静态变量,如上述代码中static_var2就是被存放在.bss段,预留了4个字节的空间。