LLVM LLD PE 格式分析 (COFF)

Windows中的可执行文件为PE (Portable Executable)格式。从其名称可以看出 这种格式是架构无关的。
实际上PE格式就是对COFF格式的扩展。
微软关于PE格式的说明列在了 https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
下面我们结合前面关于COFF的介绍 LLVM LLD COFF格式分析,仍然以小用例的方式来剖析PE格式。
为了更好地演示结构(如section)的特点,我们提前插入一些静态链接的概念进来。关于静/动态链接,后续会另写文章介绍 TODO

用例
$ cat form.c

int g_init_var = 0x1234;
int g_uninit_var;

extern int Shared;

extern int foo(int I, char *str);

int main(){
  static int s_init_var = 0x1235;
  static int s_uninit_var;

  int loc_init_var = 1;
  int loc_uninit_var;

  int ret = foo(g_init_var + g_uninit_var + s_init_var + s_uninit_var + loc_init_var + loc_uninit_var + Shared, "xiang");

  return ret;
}

$ cat foo.c

int Shared = 16;
int foo(int I, char *str){
  if (str)
    return I;
  return 0;
}

为了简化起见这两个小用例都没调用标准库函数。
我们先生成目标文件(COFF):form.o foo.o

$clang form.c foo.c -c

链接之前我们先看看各个目标文件的段信息
xiangzh1@xiangzh1-mobl1 MINGW64 /c/Work/tests
$ objdump -h form.o

form.o:     file format pe-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000053  0000000000000000  0000000000000000  0000012c  2**4
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  000001c5  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  00000000  2**2
                  ALLOC
  3 .xdata        00000008  0000000000000000  0000000000000000  000001cd  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .rdata        00000006  0000000000000000  0000000000000000  000001d5  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA, LINK_ONCE_DISCARD (COMDAT ??_C@_05NGABEPOA@xiang?$AA@ 10)
  5 .pdata        0000000c  0000000000000000  0000000000000000  000001db  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
  6 .llvm_addrsig 00000006  0000000000000000  0000000000000000  00000205  2**0
                  CONTENTS, READONLY, EXCLUDE, NOREAD

xiangzh1@xiangzh1-mobl1 MINGW64 /c/Work/tests
$ objdump -h foo.o

foo.o:     file format pe-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000037  0000000000000000  0000000000000000  00000104  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000013b  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000000  2**2
                  ALLOC
  3 .xdata        00000008  0000000000000000  0000000000000000  0000013f  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .pdata        0000000c  0000000000000000  0000000000000000  00000147  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
  5 .llvm_addrsig 00000000  0000000000000000  0000000000000000  00000171  2**0
                  CONTENTS, READONLY, EXCLUDE, NOREAD

然后在链接成PE文件时,我们也通过指定入口函数为main来规避其它文件的介入。
$ lld-link -entry:main form.o foo.o -out:a.exe
$ objdump -h a.exe

a.exe:     file format pei-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000097  0000000140001000  0000000140001000  00000400  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rdata        00000018  0000000140002000  0000000140002000  00000600  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00000014  0000000140003000  0000000140003000  00000800  2**4
                  CONTENTS, ALLOC, LOAD, DATA
  3 .pdata        00000018  0000000140004000  0000000140004000  00000a00  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

链接后
我们发现PE文件段的数量减少了很多,之前的bss, xdata, llvm_addrsig 段都不见了。
pdata段视乎看起来是前面两个目标文件的pdata段累加 (0x18 = 0x0c + 0x0c)。
从size上看其它段好像并不是简单的累加,我们后面在接受静态链接的时候再仔细分析。
这里我们先关注 PE 格式本身。

查看段内容
用llvm-objdump查看段内容 ( -s Alias for --full-contents Display the content of each section)
$ llvm-objdump -s a.exe

a.exe:  file format coff-x86-64
Contents of section .text:
 140001000 4883ec38 c7442434 00000000 c7442430  H..8.D$4.....D$0
 140001010 01000000 8b0de61f 0000030d f01f0000  ................
 140001020 030dde1f 0000030d e01f0000 034c2430  .............L$0
 140001030 034c242c 030dce1f 0000488d 15bf0f00  .L$,......H.....
 140001040 00e81a00 00008944 24288b44 24284883  .......D$(.D$(H.
 140001050 c438c3cc cccccccc cccccccc cccccccc  .8..............
// 上半部分几乎来自form.o,可以看出有些地方有变化,应该是重定位或位置更新。
// form.o: file format coff-x86-64
// Contents of section .text:
//  0000 4883ec38 c7442434 00000000 c7442430  H..8.D$4.....D$0
//  0010 01000000 8b0d0000 0000030d 00000000  ................
//  0020 030d0000 0000030d 00000000 034c2430  .............L$0
//  0030 034c242c 030d0000 0000488d 15000000  .L$,......H.....
//  0040 00e80000 00008944 24288b44 24284883  .......D$(.D$(H.
//  0050 c438c3                               .8.

 140001060 4883ec18 48895424 08894c24 0448837c  H...H.T$..L$.H.|
 140001070 2408000f 840d0000 008b4424 04894424  $.........D$..D$
 140001080 14e90800 0000c744 24140000 00008b44  .......D$......D
 140001090 24144883 c418c3                      $.H....
下半部分来自foo.o,一点变化没有。
// foo.o:  file format coff-x86-64
// Contents of section .text:
//  0000 4883ec18 48895424 08894c24 0448837c  H...H.T$..L$.H.|
//  0010 2408000f 840d0000 008b4424 04894424  $.........D$..D$
//  0020 14e90800 0000c744 24140000 00008b44  .......D$......D
//  0030 24144883 c418c3

// 这里我们来看下text段 size 的关系
// 我们发现 0x97 (a.exe) - [0x53(form.o) + 0x37(foo.o)] = 13
// 而从地址 140001053 开始正好多了13个字节的 “0xcc” 。
// 为什么会多出13个cc呢?
// 因为function foo有个16字节对齐要求。为了满足对齐,这里填充了13个cc。
 
Contents of section .rdata:
 140002000 7869616e 67000000 01040100 04620000  xiang........b..
 140002010 01040100 04220000                    ....."..

// form.o: file format coff-x86-64
// Contents of section .xdata:
//  0000 01040100 04620000                    .....b..
// Contents of section .rdata:
//  0000 7869616e 6700                        xiang.

// foo.o:  file format coff-x86-64
// Contents of section .xdata:
//  0000 01040100 04220000                    ....."..

// 通过仔细对比我们发现 链接后 的rdata段 正好是 目标文件 rdata段和xdata段的合并:
// rdata(a.exe) = rdata(form.o) + xdata(form.o) + xdata(foo.o)
 
Contents of section .data:
 140003000 34120000 35120000 10000000 00000000  4...5...........
 140003010 00000000                             ....

// form.o: file format coff-x86-64
// Contents of section .data:
//  0000 34120000 35120000                    4...5...
// Contents of section .bss:
// <skipping contents of bss section at [0000, 0008)>

// foo.o:  file format coff-x86-64
// Contents of section .data:
// 0000 10000000

// 可以看出 链接后 目标文件data段中的内容 都在 执行文件的data段中,
// 但 执行文件的data段 比 两个目标文件data段加起来还要大8字节 (0x14 > 0x08 + 0x04)
// 而这多出来的8个字节正好是目标文件中的bss段:
// data(a.exe) = data(form.o) + bss(form.o) + data(foo.o)
 
Contents of section .pdata:
 140004000 00100000 53100000 08200000 60100000  ....S.... ..`...
 140004010 97100000 10200000                    ..... ..

// form.o:  file format coff-x86-64
// Contents of section .pdata:
//  0000 00000000 53000000 00000000           ....S.......
// foo.o:  file format coff-x86-64
// Contents of section .pdata:
//  0000 00000000 37000000 00000000           ....7.......

// 对于pdata段我们目前还不是很了解它,只知道它与异常有关,内容上也观察不出来什么。
// 但从size上看,链接后的pdata段的大小是两个目标文件的和:
// 0x18 size pdata(a.exe) = 0x0c size pdata(form.o) + 0x0c size pdata(foo.o)
// TODO: 这个我们以后再分析

列出所有信息
从各个段的分布图中我们发现有些地方并不连续,接下来我们用
llvm-readobj -a 来查看该PE文件中一共有哪些东西:
$ llvm-readobj -a a.exe

File: a.exe
Format: COFF-x86-64
Arch: x86_64
AddressSize: 64bit
ImageFileHeader {                                // size = 20
  Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)
  SectionCount: 4
  TimeDateStamp: 2022-08-30 06:22:44 (0x630DACB4)
  PointerToSymbolTable: 0x0
  SymbolCount: 0
  StringTableSize: 0
  OptionalHeaderSize: 240  // 在COFF中恒为0
  Characteristics [ (0x22)
    IMAGE_FILE_EXECUTABLE_IMAGE (0x2)
    IMAGE_FILE_LARGE_ADDRESS_AWARE (0x20)
  ]
}
// 可以看出这里的ImageFileHeader结构和普通COFF文件格式中的一模一样。
// 最主要的区别是这里的OptionalHeaderSize不是0了

// 下面的ImageOptionalHeader是PE文件的主要header
// size = sizeof(pe32plus_header) + sizeof(data_directory) * numberOfDataDirectory (lld/COFF/Writer.cpp)
// size =  112 + 8 * 16 = 240
ImageOptionalHeader { 
  Magic: 0x20B             // PE32+  ; 0x10B: PE32 ; 0x107: a ROM image
  MajorLinkerVersion: 14
  MinorLinkerVersion: 0
  SizeOfCode: 512              // (所有)代码段的大小 (Q:为什么不等于我们例子中的text段的大小0x97?见下)
      // 初始化的数据段大小,我们观察到 3个数据段 rdata,data,pdata都带有IMAGE_SCN_CNT_INITIALIZED_DATA标记
      // 因此这里的SizeOfInitializedData就是指这3个段在文件中占的大小. 
      // 显然和SizeOfCode的情况一样,这里的 (1536 = 512 * 3)明显大于我们的真实所需的数据大小。
  SizeOfInitializedData: 1536
  SizeOfUninitializedData: 0
    // 入口地址:当执行文件被加载到内存后 入口地址 相对于整个执行文件(在内存中的镜像)的偏移。对于DLL共享文件来说,它是可选的。
    // The address of the entry point relative to the image base when the executable file is loaded into memory.
    // For program images, this is the starting address.
    // For device drivers, this is the address of the initialization function. 
    // An entry point is optional for DLLs. When no entry point is present, this field must be zero.    
  AddressOfEntryPoint: 0x1000
    // 代码段起始位置 相对于 相对于整个执行文件(在内存中的镜像)的偏移。
    // (这里由于main就是代码段的开头,而且我们指定main为入口地址,所以它和AddressOfEntryPoint值一样。)
  BaseOfCode: 0x1000
    // 执行文件(镜像文件)加载到内存中的 “首选” 起始地址(preferred address). (64K的整数倍)
  ImageBase: 0x140000000    
  SectionAlignment: 4096   // 段加载到内存中的对齐,它必须大于或等于文件的对齐。默认为页的大小。
  FileAlignment: 512       // 段(section)在文件中的对齐要求。(A:现在我们明白为什么SizeOfCode = 512 了)
  MajorOperatingSystemVersion: 6
  MinorOperatingSystemVersion: 0
  MajorImageVersion: 0
  MinorImageVersion: 0
  MajorSubsystemVersion: 6
  MinorSubsystemVersion: 0
    // 镜像(在内存中的)大小:由于每个段都要求4096(页)对齐,所以4个段 + 文件头 一共占用了5个页 (20480=4096*5)
  SizeOfImage: 20480
    // 文件头(在文件中的)大小: MS-DOS stub, PE header 和 section header 合在一起 (对齐到FileAlignment)
  SizeOfHeaders: 1024
  Subsystem: IMAGE_SUBSYSTEM_WINDOWS_CUI (0x3)
  Characteristics [ (0x8160)
    IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE (0x40)
    IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA (0x20)
    IMAGE_DLL_CHARACTERISTICS_NX_COMPAT (0x100)
    IMAGE_DLL_CHARACTERISTICS_TERMINAL_SERVER_AWARE (0x8000)
  ]
  SizeOfStackReserve: 1048576  // 1MB
  SizeOfStackCommit: 4096      // The size of the stack to commit.
  SizeOfHeapReserve: 1048576
  SizeOfHeapCommit: 4096
    // 在windows装载时,往往要快速找到一些装载所需的数据结构,比如导入/导出表,重定位表等。这些常用数据的位置和大小
    // 被保存在下面的DataDirectory[{VirtualAddress, Size},]中。
  NumberOfRvaAndSize: 16   // 表示DataDirectory包含项目数(每项包含 数据的位置和长度)
  DataDirectory {
    ExportTableRVA: 0x0  
    ExportTableSize: 0x0
    ImportTableRVA: 0x0   // 导入表的位置
    ImportTableSize: 0x0  // 导入表的大小
    ResourceTableRVA: 0x0
    ResourceTableSize: 0x0
    ExceptionTableRVA: 0x4000
    ExceptionTableSize: 0x18
    CertificateTableRVA: 0x0
    CertificateTableSize: 0x0
    BaseRelocationTableRVA: 0x0  // 重定位表的位置
    BaseRelocationTableSize: 0x0 // 重定位表的大小
    DebugRVA: 0x0
    DebugSize: 0x0
    ArchitectureRVA: 0x0
    ArchitectureSize: 0x0
    GlobalPtrRVA: 0x0
    GlobalPtrSize: 0x0
    TLSTableRVA: 0x0
    TLSTableSize: 0x0
    LoadConfigTableRVA: 0x0
    LoadConfigTableSize: 0x0
    BoundImportRVA: 0x0
    BoundImportSize: 0x0
    IATRVA: 0x0
    IATSize: 0x0
    DelayImportDescriptorRVA: 0x0
    DelayImportDescriptorSize: 0x0
    CLRRuntimeHeaderRVA: 0x0
    CLRRuntimeHeaderSize: 0x0
    ReservedRVA: 0x0
    ReservedSize: 0x0
  }
}
// 参考链接: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-image-only

// 下面是 DOS MZ可执行文件格式的文件头,主要用来兼容DOS系统的。
DOSHeader {
  Magic: MZ
  UsedBytesInTheLastPage: 120
  FileSizeInPages: 1
  NumberOfRelocationItems: 0
  HeaderSizeInParagraphs: 4
  MinimumExtraParagraphs: 0
  MaximumExtraParagraphs: 0
  InitialRelativeSS: 0
  InitialSP: 0
  Checksum: 0
  InitialIP: 0
  InitialRelativeCS: 0
  AddressOfRelocationTable: 64
  OverlayNumber: 0
  OEMid: 0
  OEMinfo: 0
  AddressOfNewExeHeader: 120
}

// 接下来是 段描述符表,也叫段头表,段表, 和coff格式中的一样
Sections [
 // struct coff_section {                    // Size = 40
 //   char Name[COFF::NameSize (8)];         // 段名
 //   support::ulittle32_t VirtualSize;      // 该段被加载到内存后的大小
 //   support::ulittle32_t VirtualAddress;   // 该段被加载到内存后的虚拟地址
 //   support::ulittle32_t SizeOfRawData;    // 该段的原始大小 (在文件中的大小)
 //   support::ulittle32_t PointerToRawData; // 该原始段的位置 (该段在文件中的位置)
 //   support::ulittle32_t PointerToRelocations; // 该段的重定位表在文件中的位置
 //   support::ulittle32_t PointerToLinenumbers; // 该段的行号表在文件中的位置 (debug)
 //   support::ulittle16_t NumberOfRelocations;  // 该段(重定位表中)的重定位项数量
 //   support::ulittle16_t NumberOfLinenumbers;  // 该段(行号表中)的行号数量
 //   support::ulittle32_t Characteristics;      // 标志位,如可读,可执行等
  Section {
    Number: 1
    Name: .text (2E 74 65 78 74 00 00 00)
    VirtualSize: 0x97
    VirtualAddress: 0x1000
    RawDataSize: 512
    PointerToRawData: 0x400
    PointerToRelocations: 0x0
    PointerToLineNumbers: 0x0
    RelocationCount: 0
    LineNumberCount: 0
    Characteristics [ (0x60000020)
      IMAGE_SCN_CNT_CODE (0x20)
      IMAGE_SCN_MEM_EXECUTE (0x20000000)
      IMAGE_SCN_MEM_READ (0x40000000)
    ]
  }
  Section {
    Number: 2
    Name: .rdata (2E 72 64 61 74 61 00 00)
    VirtualSize: 0x18
    VirtualAddress: 0x2000
    RawDataSize: 512
    PointerToRawData: 0x600
    PointerToRelocations: 0x0
    PointerToLineNumbers: 0x0
    RelocationCount: 0
    LineNumberCount: 0
    Characteristics [ (0x40000040)
      IMAGE_SCN_CNT_INITIALIZED_DATA (0x40)
      IMAGE_SCN_MEM_READ (0x40000000)
    ]
  }
  Section {
    Number: 3
    Name: .data (2E 64 61 74 61 00 00 00)
    VirtualSize: 0x14
    VirtualAddress: 0x3000
    RawDataSize: 512
    PointerToRawData: 0x800
    PointerToRelocations: 0x0
    PointerToLineNumbers: 0x0
    RelocationCount: 0
    LineNumberCount: 0
    Characteristics [ (0xC0000040)
      IMAGE_SCN_CNT_INITIALIZED_DATA (0x40)
      IMAGE_SCN_MEM_READ (0x40000000)
      IMAGE_SCN_MEM_WRITE (0x80000000)
    ]
  }
  Section {
    Number: 4
    Name: .pdata (2E 70 64 61 74 61 00 00)
    VirtualSize: 0x18
    VirtualAddress: 0x4000
    RawDataSize: 512
    PointerToRawData: 0xA00
    PointerToRelocations: 0x0
    PointerToLineNumbers: 0x0
    RelocationCount: 0
    LineNumberCount: 0
    Characteristics [ (0x40000040)
      IMAGE_SCN_CNT_INITIALIZED_DATA (0x40)
      IMAGE_SCN_MEM_READ (0x40000000)
    ]
  }
]

// 我们可以看到下面的重定位表为空,因为重定位工作已经完成了
Relocations [
]

UnwindInformation [
  RuntimeFunction {
    StartAddress: (0x140001000)
    EndAddress: (0x140001053)
    UnwindInfoAddress: (0x140002008)
    UnwindInfo {
      Version: 1
      Flags [ (0x0)
      ]
      PrologSize: 4
      FrameRegister: -
      FrameOffset: -
      UnwindCodeCount: 1
      UnwindCodes [
        0x04: ALLOC_SMALL size=56
      ]
    }
  }
  RuntimeFunction {
    StartAddress: (0x140001060)
    EndAddress: (0x140001097)
    UnwindInfoAddress: (0x140002010)
    UnwindInfo {
      Version: 1
      Flags [ (0x0)
      ]
      PrologSize: 4
      FrameRegister: -
      FrameOffset: -
      UnwindCodeCount: 1
      UnwindCodes [
        0x04: ALLOC_SMALL size=24
      ]
    }
  }
]
Symbols [
]

PE总体分布图
最后我们再重新绘制各段的分布图,填补之前的空缺:
在这里插入图片描述
各个段对应的数据结构如下
(这里我们并不关注DOS相关的格式,COFF部分的数据结构可以参看 COFF数据结构,因此只列出ImageOptionalHeader和OutputSection数据结构)
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值