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数据结构)