Windows PE 文件头解析
0x01 PE文件基本介绍
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(来自百度百科)。包括DOS头、PE头、节表、导入表和导出表。
本文主要介绍DOS头、NT头、标准PE头、可选PE头和节表。
在介绍前先了解几个概念:
虚拟地址(Virtual Address,VA):在windows系统中,PE文件被系统加载器映射到内存中。每个程序都有自己的虚拟空间,这个虚拟空间的内存地址称为虚拟地址。
相对虚拟地址(Relative Virtual Address,RVA):RVA是PE文件被装在到内存中,某个数据位置相对于装入地址的偏移量。假设一个程序从400000h处装入,代码开始与401000h,于是RVA = 401000h - 400000h,为1000h
虚拟地址(RV)= 基地址(ImageBase)+ 相对虚拟地址(RVA)
有关ImageBase的内容后面会介绍到。
下图为PE文件的基本结构:
0x02 DOS头
本文使用32位程序做演示。
DOS头(IMAGE_DOS_HEADER)的大小为40H(64字节)
DOS头的基本结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| struct _IMAGE_DOS_HEADER { 0x00 WORD e_magic; * 0x02 WORD e_cblp; 0x04 WORD e_cp; 0x06 WORD e_crlc; 0x08 WORD e_cparhdr; 0x0a WORD e_minalloc; 0x0c WORD e_maxalloc; 0x0e WORD e_ss; 0x10 WORD e_sp; 0x12 WORD e_csum; 0x14 WORD e_ip; 0x16 WORD e_cs; 0x18 WORD e_lfarlc; 0x1a WORD e_ovno; 0x1c WORD e_res[4]; 0x24 WORD e_oemid; 0x26 WORD e_oeminfo; 0x28 WORD e_res2[10]; 0x3c DWORD e_lfanew; * };
|
在DOS头中比较重要的两个参数:
e_magic
:在DOS头开始的位置,大小为2字节,存储的内容一般为5A 4D
,也就是’MZ’的十六进制,’MZ’是MS-DOS的创建者之一Mark Zbikowski名字的缩写,通常用来判断文件是否为可执行文件。
e_lfanew
:在DOS头结束的位置,大小为4字节,是PE头相对于文件的偏移,用于定位PE文件头的位置。
[](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/2022/05/075e24a7fc194f011f9f6e6fa4a2c5e9.png)
可以通过e_lfanew
中存储的数据找到PE文件头的位置,图中e_lfanew
存储的内容为00 00 00 F0
(小端存储),所以可以定位到PE文件头的位置在F0H。
在e_lfanew
到PE文件头的这段空间,是由编译器生成,用来存储一些程序运行的信息,可以随意更改,对程序没有影响。
0x03 PE文件头
PE文件头是PE相关结构NT映像头的简称,其中包含许多PE装载器能用到的重要字段,在NT头中除了存放了4个字节的PE文件头标识以外还有标准PE头和可选PE头。
NT头(IMAGE_NT_HEADERS)的基本结构如下:
1 2 3 4 5
| struct _IMAGE_NT_HEADERS { 0x00 DWORD Signature; 0x04 _IMAGE_FILE_HEADER FileHeader; 0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader; };
|
可以看到在NT头的开始处是一个32位的标志信息,PE
,DOS头中的e_lfanew
指向该位置
1、标准PE头
标准PE头(IMAGE_FILE_HEADER)的大小为12H(20字节)
标准PE头的基本结构如下:
1 2 3 4 5 6 7 8 9
| struct _IMAGE_FILE_HEADER { 0x00 WORD Machine; * 0x02 WORD NumberOfSections; * 0x04 DWORD TimeDateStamp; * 0x08 DWORD PointerToSymbolTable; 0x0c DWORD NumberOfSymbols; 0x10 WORD SizeOfOptionalHeader; * 0x12 WORD Characteristics; * };
|
在标准PE头中比较重要的几个参数:
Machine
:存储了程序运行的cpu型号,2字节大小,一般0x14C遇见的比较多,这是在网上找到的winnt.h中各个值对应的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #define IMAGE_FILE_MACHINE_UNKNOWN 0 #define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 #define IMAGE_FILE_MACHINE_I386 0x014c #define IMAGE_FILE_MACHINE_R3000 0x0162 #define IMAGE_FILE_MACHINE_R4000 0x0166 #define IMAGE_FILE_MACHINE_R10000 0x0168 #define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 #define IMAGE_FILE_MACHINE_ALPHA 0x0184 #define IMAGE_FILE_MACHINE_SH3 0x01a2 #define IMAGE_FILE_MACHINE_SH3DSP 0x01a3 #define IMAGE_FILE_MACHINE_SH3E 0x01a4 #define IMAGE_FILE_MACHINE_SH4 0x01a6 #define IMAGE_FILE_MACHINE_SH5 0x01a8 #define IMAGE_FILE_MACHINE_ARM 0x01c0 #define IMAGE_FILE_MACHINE_THUMB 0x01c2 #define IMAGE_FILE_MACHINE_ARMNT 0x01c4 #define IMAGE_FILE_MACHINE_AM33 0x01d3 #define IMAGE_FILE_MACHINE_POWERPC 0x01F0 #define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1 #define IMAGE_FILE_MACHINE_IA64 0x0200 #define IMAGE_FILE_MACHINE_MIPS16 0x0266 #define IMAGE_FILE_MACHINE_ALPHA64 0x0284 #define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 #define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 #define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64 #define IMAGE_FILE_MACHINE_TRICORE 0x0520 #define IMAGE_FILE_MACHINE_CEF 0x0CEF #define IMAGE_FILE_MACHINE_EBC 0x0EBC #define IMAGE_FILE_MACHINE_AMD64 0x8664 #define IMAGE_FILE_MACHINE_M32R 0x9041 #define IMAGE_FILE_MACHINE_ARM64 0xAA64 #define IMAGE_FILE_MACHINE_CEE 0xC0EE
|
NumberOfSections
:存储了文件中节的总数,2字节大小,不同的可执行文件节的总数是不一样的,可以通过NumberOfSections得知当前文件节的总数,对节的内容进行遍历,如果要增加或者删除节,也是修改此参数。
TimeDateStamp
:存储了文件创建的时间,4字节大小,是由编译器写入的。
SizeOfOptionalHeader
:存储了可选PE头的大小,2字节大小,可选PE头的大小一般是不固定的,通常情况下32位程序为0xE0,64位程序为0xF0,此值可以自定义。
Characteristics
:存放了文件属性,2字节大小,它的每一位的含义都不同,当前程序的值为0x0122
,所以换算成二进制就是0000 0001 0010 0010
,对应的属性如下图所示
[](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/2022/05/7483c26aa1146b5a476c764b392441ff1.png)
标准PE头在程序中的位置:
2、可选PE头
在标准PE头后面就是可选PE头(IMAGE_OPTIONAL_HEADER),32位程序和64位程序可选PE头大小分别是0xE0和0xF0,在内容上也略有不同。
32位可选PE头的基本结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| struct _IMAGE_OPTIONAL_HEADER32 { 0x00 WORD Magic; * 0x02 BYTE MajorLinkerVersion; 0x03 BYTE MinorLinkerVersion; 0x04 DWORD SizeOfCode; * 0x08 DWORD SizeOfInitializedData; * 0x0c DWORD SizeOfUninitializedData; * 0x10 DWORD AddressOfEntryPoint; * 0x14 DWORD BaseOfCode; * 0x18 DWORD BaseOfData; * 0x1c DWORD ImageBase; * 0x20 DWORD SectionAlignment; * 0x24 DWORD FileAlignment; * 0x28 WORD MajorOperatingSystemVersion; 0x2a WORD MinorOperatingSystemVersion; 0x2c WORD MajorImageVersion; 0x2e WORD MinorImageVersion; 0x30 WORD MajorSubsystemVersion; 0x32 WORD MinorSubsystemVersion; 0x34 DWORD Win32VersionValue; 0x38 DWORD SizeOfImage; * 0x3c DWORD SizeOfHeaders; * 0x40 DWORD CheckSum; * 0x44 WORD Subsystem; 0x46 WORD DllCharacteristics; 0x48 DWORD SizeOfStackReserve; * 0x4c DWORD SizeOfStackCommit; * 0x50 DWORD SizeOfHeapReserve; * 0x54 DWORD SizeOfHeapCommit; * 0x58 DWORD LoaderFlags; 0x5c DWORD NumberOfRvaAndSizes; * 0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16]; };
|
在32位程序中如下图所示:
IMAGE_OPTIONAL_HEADER64
和IMAGE_OPTIONAL_HEADER32
不同的是IMAGE_OPTIONAL_HEADER64
没有BaseOfData
参数,并且ImageBase、SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit
的大小为ULONGLONG,也就是64位8个字节。
64位可选PE头的基本结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| struct _IMAGE_OPTIONAL_HEADER { 0x00 WORD Magic; * 0x02 BYTE MajorLinkerVersion; 0x03 BYTE MinorLinkerVersion; 0x04 DWORD SizeOfCode; * 0x08 DWORD SizeOfInitializedData; * 0x0c DWORD SizeOfUninitializedData; * 0x10 DWORD AddressOfEntryPoint; * 0x14 DWORD BaseOfCode; * 0x1c ULONGLONG ImageBase; * 0x20 DWORD SectionAlignment; * 0x24 DWORD FileAlignment; * 0x28 WORD MajorOperatingSystemVersion; 0x2a WORD MinorOperatingSystemVersion; 0x2c WORD MajorImageVersion; 0x2e WORD MinorImageVersion; 0x30 WORD MajorSubsystemVersion; 0x32 WORD MinorSubsystemVersion; 0x34 DWORD Win32VersionValue; 0x38 DWORD SizeOfImage; * 0x3c DWORD SizeOfHeaders; * 0x40 DWORD CheckSum; * 0x44 WORD Subsystem; 0x46 WORD DllCharacteristics; 0x48 ULONGLONG SizeOfStackReserve; * 0x50 ULONGLONG SizeOfStackCommit; * 0x58 ULONGLONG SizeOfHeapReserve; * 0x60 ULONGLONG SizeOfHeapCommit; * 0x68 DWORD LoaderFlags; 0x6C DWORD NumberOfRvaAndSizes; * 0x70 _IMAGE_DATA_DIRECTORY DataDirectory[16]; };
|
在64位程序中如下图所示:
在32位程序中可选PE头比较重要的参数有以下几个:
Magic
:说明文件是ROM镜像(0107h),还是普通的可执行的镜像(010Bh),一般来说32位程序是010Bh,64位程序是020Bh。
SizeOfCode
:存储了所有代码节的和,他必须是FileAlignment文件对齐的整数倍,此程序文件对齐的大小是200H,所以SizeOfCode的大小为1000H,为200H的整数倍,由编译器填写。
SizeOfInitializedData
:存储已经初始化数据块的大小,即在编译的时候所构成的块的大小(不包括代码段),此值也为文件对齐的整数倍,由编译器填写。
SizeOfUninitializedData
:未初始化数据块的大小,装载程序要在虚拟地址空间中为这些数据约定空间,一般存在.bss节中,为文件对齐的整数倍,由编译器填写。
AddressOfEntryPoint
:可选PE头中最重要的一个参数,也就是我们通常说的OEP,是当前程序的入口位置,该地址是一个相对虚拟地址,指向了程序执行的第一条代码,如果程序被加壳,那么这个地址就会被修改,通常在使用OD进行动态调试的时候,OD首次停留的位置就是AddressOfEntryPoint。
BaseOfCode
:代码开始的基址,在内存中,代码段通常在PE文件头之后,数据段开始之前,此值通常由编译器填写。
BaseOfData
:数据开始的基址,数据段通常在内存的末尾,在64位程序中没有该参数,此值通常由编译器编写。
ImageBase
:内存的镜像地址,也称基地址,是文件在内存中的首选装入地址,如果文件需要在内存中执行的话,会首先使用ImageBase中存放的地址,如果地址被占用,文件会被装入到其他地址中,因为直接装入这个地址不需要进行重定位,所以速度会很快,如果当前地址被占用就需要重定位后装入其他的地址,相对来说速度就会慢一些。
SectionAlignment
:程序被装入内存后的对齐大小,通常为1000H。
FileAlignment
:程序在没有被装入内存前文件对齐的大小,通常为200H或者1000H,在为1000H的时候文件对齐和内存对齐相同,会加快程序的运行速度。为200H时程序装载到内存中需要进行拉伸操作,把对齐大小拉伸到1000H,这样做相对来说速度会慢一些,但是在磁盘中存储会节省空间。
SizeOfImage
:是程序在装入内存后的整个PE文件在内存中的映射尺寸,指的就是装入文件从ImageBase到最后一个块的大小,可以比实际的值大,但必须是SectionAlignment内存对齐的整数倍。
SizeOfHeaders
:是DOS头,PE文件头和节表的总大小,该值必须是正确的,否则程序无法运行。
CheckSum
:映像的校验和,可以用来检查文件是否被更改。
SizeOfStackReserve
:初始化时保留的堆栈大小。
SizeOfStackCommit
:初始化时实际提交的大小。
SizeOfHeapReserve
:初始化时保留的堆的大小。
SizeOfHeapCommit
:初始化时实际提交的大小。
NumberOfRvaAndSizes
:数据目录的项数。
DataDirectory
:数据目录表,由数个IMAGE_DATA_DIRECTORY结构组成,指向输出表、输入表、资源块等数据,如下图所示
0x04 节表
节表大小为24H(40字节)
节表不止一个,可能有多个,节表的数量存在标准PE头中的NumberOfSections属性,虽然节表有多个,但是每个节表中的结构是相同的。
节表的基本结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| typedef struct _IMAGE_SECTION_HEADER { 0x00 BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { 0x08 DWORD PhysicalAddress; 0x08 DWORD VirtualSize; } Misc; 0x0c DWORD VirtualAddress; 0x10 DWORD SizeOfRawData; 0x14 DWORD PointerToRawData; 0x18 DWORD PointerToRelocations; 0x1c DWORD PointerToLinenumbers; 0x20 WORD NumberOfRelocations; 0x22 WORD NumberOfLinenumbers; 0x24 DWORD Characteristics; };
|
Name
:存储节表的名字,一般情况下‘.’开始,””来结束,内容可以自定义,所以并不能完全靠Name
参数来判断节表中的内容。
VirtualSize
:实际使用的节的大小,也就是对齐前的节的大小。如果VirtualSize的值大SizeOfRawData,那么SizeOfRawData表示来自可执行文件初始化数据的大小,与VirtualSize相差的字节用0填充。
VirtualAddress
:存储节区在内存中的偏移地址,需要加上ImageBase才是真实的地址。
SizeOfRawData
:节在文件中的尺寸,也就是该节在磁盘中所占用的空间。
PointerToRawData
:节区在文件中的偏移。
Characteristics
:节的属性,通过属性表里的值相加得到。
[](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/2022/05/9f1a4ebbb644711176733cd47592353e.png)
本例中节的属性值为60 00 00 20H,也就是包含可执行代码+该块可执行+该块可读。
节表在程序中如下图所示:
0x05 导入表和导出表
导出表
DESC: 记录本模块导出函数的相关信息, 结构如图所示。
RVA: IMAGE_DATA_DIRECTORY[0].VirtualAddress。
导出函数有两种导出方式:
(1) 序号导出。直接在导出函数地址表通过序号得到函数的 RVA。
(2) 名称导出。先在导出函数名称表中找到该函数位置, 再在导出函数名称序号表对应位置找到该导出函数的序号, 最后在导出函数地址表通过序号得到函数的 RVA。
Name
DESC : 本模块名称字符串的 RVA。
NumberOfFunctions
DESC : 导出函数个数,实际等于导出函数地址表的元素数目。
NumberOfNames
DESC : 以函数名称导出的函数个数。
AddressOfFunctions
DESC : 导出函数地址表的 RVA,存储所有导出函数入口点的 RVA。每项地址大小 4 字节,总大小为NumberOfFunctions * 4。
不一定每一项都表示一个导出函数的地址, 可以为 0x00000000 的填充。
AddressOfNames
DESC : 导出函数名称表的 RVA,存储所有以名称导出的函数名称字符串的地址。每项地址大小 4 字节, 总大小为 NumberOfNames * 4。
AddressOfNameOrdinals
DESC : 以名称导出的函数的序号表的 RVA, 与名称表对应, 存储所有以名称导出的函数的序号。每项序号大小 2 字节, 总大小为 NumberOfNames * 2。
导入表
DESC : 用于记录所有外部导入函数的信息, 由 IMAGE_IMPORT_DESCRIPTOR 结构体数组构成, 最后一个元素以 0x00 填充表示结束。每个结构体描述一个模块被使用的导入函数的相关信息。导入表大小 IMAGE_DATA_DIRECTORY.Size 值包括最后的空结构体大小。
DUMMYUNIONNAME.Characteristics
DESC : 值为 0 表示 IMAGE_IMPORT_DESCRIPTOR 结构体数组终止。
DUMMYUNIONNAME.OriginalFirstThunk
DESC: 该模块导入名称表 (INT) 的 RVA。指向一个 IMAGE_THUNK_DATA32 (32位) 或 IMAGE_THUNK_DATA64 (64位) 结构体数组。
TimeDateStamp
DESC : 时间戳。如果没有绑定, 值为 0。否则为 -1, 且真实时间戳在目录项的绑定导入表 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 中。
Name
DESC : 模块名称字符串的 RVA。
FirstThunk
DESC: 该模块导入地址表 (IAT) 的 RVA。指向一个 IMAGE_THUNK_DATA32 (32位)或 IMAGE_THUNK_DATA64 (64位)结构体数组。如果绑定了,IAT具有真实的函数地址。
1.INT
DESC : 导入函数名称表, 描述导入函数的导出序号或名称等相关信息。由 IMAGE_THUNK_DATA32 (32位)或 IMAGE_THUNK_DATA64 (64位)结构体数组构成, 最后一个元素以 0x00 填充表示结束, 每个元素描述一个导入函数。
该结构体只有一个成员, 如果值的最高位为 1 表示函数是以序号形式导出的, 剩余 31 位(或 63 位)表示导入函数在其模块导出函数地址表中的序号。如果为 0 则表示函数是以名称形式导出的, 值为一个结构体 IMAGE_IMPORT_BY_NAME 的 RVA, 结构体如下:
2.IAT
DESC : 导入函数地址表, 记录每个导入函数的实际内存地址 (VA)。
由于模块加载基址的不确定, 在加载到内存时 IAT 一般由 PE 加载器依次获取每个函数的实际地址后重新填充。如果使用目录项中的绑定导入表, IAT 的值已经是每个导入函数的实际地址, 可以跳过 IAT 的填充使得装载更快。
由 IMAGE_THUNK_DATA32 (32位)或 IMAGE_THUNK_DATA64 (64位)结构体数组构成, 最后一个元素以 0x00 填充表示结束, 每个元素的值为该导入函数的在该模块中的 RVA。
0x06 简单的32位PE头解析器编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| #include "stdafx.h" #include "malloc.h" #include "windows.h"
LPVOID ReadPEFile(LPSTR lpszFile){ FILE* pFile = NULL; DWORD FileSize = 0; LPVOID pFileBuffer = NULL;
pFile = fopen(lpszFile,"rb"); if(!pFile){ printf("无法打开exe文件"); return NULL; }
fseek(pFile,0,2); FileSize = ftell(pFile); fseek(pFile,0,0);
pFileBuffer = malloc(FileSize); if(!pFileBuffer){ printf("初始化空间失败"); fclose(pFile); return NULL; }
size_t n = fread(pFileBuffer,FileSize,1,pFile); if(!n){ printf("读取数据失败"); free(pFileBuffer); fclose(pFile); return NULL; }
fclose(pFile); return pFileBuffer;
}
VOID PrintSectionHeader(){ LPVOID pFileBuffer = NULL; PIMAGE_DOS_HEADER pDosHeader = NULL; PIMAGE_NT_HEADERS pNTHeader = NULL; PIMAGE_FILE_HEADER pPEHeader = NULL; PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL; PIMAGE_SECTION_HEADER pSeationHeader = NULL; int SectionsCounts = 0;
pFileBuffer = ReadPEFile("xxxxx/notepad.exe"); if(!pFileBuffer){ printf("读取文件失败"); return ; }
if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE){ printf("不是有效的MZ标志\n"); free(pFileBuffer); return ; }
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
if(*((PWORD)((DWORD)pFileBuffer + pDosHeader -> e_lfanew)) != IMAGE_NT_SIGNATURE){ printf("不是有效的PE标志\n"); free(pFileBuffer); return ; }
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader -> e_lfanew); printf("****************NT头***************\n"); printf("NT:%x\n\n",pNTHeader -> Signature); pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4); printf("****************PE头***************\n"); printf("PE:%x\n",pPEHeader -> Machine); printf("节的数量:%x\n",pPEHeader -> NumberOfSections); printf("可选PE头的大小:%x\n",pPEHeader -> SizeOfOptionalHeader); SectionsCounts = pPEHeader -> NumberOfSections; printf("节表数:%x\n\n",SectionsCounts); pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER); printf("****************OPTION_PE头***************\n"); printf("OPTION_PE: %x\n\n",pOptionHeader -> Magic); pSeationHeader = (PIMAGE_SECTION_HEADER)(((DWORD)pOptionHeader) + pPEHeader -> SizeOfOptionalHeader); printf("****************节表***************\n");
for (int i = 0; i < SectionsCounts; i++,pSeationHeader++){ printf("节表%d名字:%s\n",(i + 1),pSeationHeader); printf("Misc:%x\n",pSeationHeader -> Misc); printf("VirtualAddress:%x\n",pSeationHeader -> VirtualAddress); printf("SizeOfRawData:%x\n",pSeationHeader -> SizeOfRawData); printf("PointerToRawData:%x\n",pSeationHeader -> PointerToRawData); printf("Characteristics:%x\n\n",pSeationHeader -> Characteristics); } free(pFileBuffer); }
int main(int argc, char* argv[]) { PrintSectionHeader(); getchar(); return 0; }
|