目标文件

代码文件经过编译器编译、链接器链接后,将会生成目标文件。
与认知不同的是,即使是用于执行的可执行目标文件,都并非单纯由指令序列组成。所有的目标必须包含一系列供操作系统加载器使用的结构信息,这个结构信息由操作系统进行约定,因此不同操作系统的结构信息各异。

比较主流的目标文件格式如下表:

操作系统文件格式MAGIC
LinuxELFELF
WindowsPEMZ
MacMach-O0xfeedface/0xfeedfacf

ELF

Executable and linking format

ELF是Linux系统下一种常见的目标文件,有三种主要类型

  1. 可执行文件
    用于提供程序的进程映像,加载到内存执行
  2. 可重定位文件
    用于与其他目标文件链接,一起创建可执行文件或共享目标文件
  3. 共享目标文件
    用于与其他可重定位目标文件和共享文件链接成其他目标文件,或与可执行文件组合创建进程映像。

在看文件结构的详细定义之前,我们先动手编译一个简单的目标文件,亲自察看其文件内容并配合结构定义能更方便的理解。
为了更直观的理解,程序显然越简单越好。比如下面这个就很不错:

int main() {}

gcc编译得到.out文件后,我们使用readelf来实际查看其文件内容

gcc -march=i686 -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc a.cpp
readelf -a a.out

ELF Header在文件开头描述了整个文件的组织,结构如下

struct elfhdr {
  uint magic;  // must equal ELF_MAGIC
  uchar elf[12];
  ushort type;
  ushort machine;
  uint version;
  uint entry;  // 程序入口的虚拟地址
  uint phoff;  // program header 表的位置偏移
  uint shoff;
  uint flags;
  ushort ehsize;
  ushort phentsize;
  ushort phnum; //program header表中的入口数目
  ushort shentsize;
  ushort shnum;
  ushort shstrndx;
};

而在我们的目标文件中,ELF Header长这个样子

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x1070
  Start of program headers:          52 (bytes into file)
  Start of section headers:          16020 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         12
  Size of section headers:           40 (bytes)
  Number of section headers:         33
  Section header string table index: 32

可以发现,ELF Header实际上是该文件的大体信息,包括Magic,文件类型,入口地址等等。

如果是可执行文件,紧接着Header将会是Program Header数组,结构如下

struct proghdr {
  uint type;   // 段类型
  uint offset;  // 段相对文件头的偏移值
  uint va;     // 段的第一个字节将被放到内存中的虚拟地址
  uint pa;
  uint filesz;
  uint memsz;  // 段在内存映像中占用的字节数
  uint flags;
  uint align;
};

每个程序结构描述了一个段或者系统准备执行所必需的其它信息。
每个段包含一个或者多个 “节区”(section) ,也就是“段内容”(Segment Contents)

我们的目标文件Program Headers如下

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00000034 0x00000034 0x00180 0x00180 R   0x4
  INTERP         0x0001b4 0x000001b4 0x000001b4 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x00000000 0x00000000 0x003d4 0x003d4 R   0x1000
  LOAD           0x001000 0x00001000 0x00001000 0x00254 0x00254 R E 0x1000
  LOAD           0x002000 0x00002000 0x00002000 0x00164 0x00164 R   0x1000
  LOAD           0x002edc 0x00003edc 0x00003edc 0x0012c 0x00130 RW  0x1000
  DYNAMIC        0x002ee4 0x00003ee4 0x00003ee4 0x000f8 0x000f8 RW  0x4
  NOTE           0x0001c8 0x000001c8 0x000001c8 0x00060 0x00060 R   0x4
  GNU_PROPERTY   0x0001ec 0x000001ec 0x000001ec 0x0001c 0x0001c R   0x4
  GNU_EH_FRAME   0x002008 0x00002008 0x00002008 0x0004c 0x0004c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x002edc 0x00003edc 0x00003edc 0x00124 0x00124 R   0x1

PE

Portable application

首先要说的是,PE这个“可移植可执行”中的“可移植”,并不是现在大家所理解的跨平台性。在我看来,这个可移植应该理解为从低版本操作系统到高版本操作系统的可移植性(比如为16位DOS编译的目标文件,依旧能在32位Windows上执行)。
也正是因为这个“可移植”,Windows的目标文件比Linux复杂了不少。

MS-DOS年代的目标文件由马克·茨柏克沃斯基(Mark Zbikowski)主持开发,因此他使用了自己名字缩写作为MS-DOS目标文件的MAGIC。
到了Windows年代,考虑到DOS-MZ文件格式的兼容性,MZ这个MAGIC需予以保留。而作为新的操作系统,自然要支持包含新特性的PE目标文件格式。微软使用了一个取巧的方法,它在MZ文件头之后,又增加了一个可选的COFF文件头,当文件含有这个文件头时,意味该目标文件为PE格式,操作系统将会使用PE的方式去解析。

与ELF一样,PE也有三种主要的目标文件

  1. 可执行文件(.exe)
  2. 静态链接库(.lib)
  3. 动态链接库(.dll)

与ELF一样,我们在看PE文件结构的详细定义之前,我们先动手编译一个只含有空白main函数的目标文件。

windows主要使用cl指令来编译,而使用dumpbin来实际查看其文件内容

gcc -march=i686 -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc a.cpp
readelf -a a.out

MZ Header如下

struct mzhdr {
  char signature[2] = "MZ";
  short lastsize;
  short nblocks;
  short nreloc;
  short hdrsize;
  short minalloc;
  short maxalloc;
  void *ss;
  void *sp;
  short checksum;
  void *ip;
  void *cs;
  short relocpos;
  short noverlay;
  short reserved1[4];
  short oem_id;
  short oem_info;
  short reserved2[10];
  long  e_lfanew;
}

在我们的目标文件中,通过dumpbin能看到以下信息

FILE HEADER VALUES
             14C machine (x86)
               4 number of sections
        5F93F9FA time date stamp Sat Oct 24 17:55:06 2020
               0 file pointer to symbol table
               0 number of symbols
              E0 size of optional header
             102 characteristics
                   Executable
                   32 bit word machine
最后修改:2020 年 10 月 27 日 01 : 32 AM
如果觉得我的文章对你有用,请随意赞赏