调试信息段,.debug
调试信息位于.debug段之中,同时PE文件格式也支持单独的调试文件(通常由.DBG扩展名标识)作为一种将调试信息集中的方法。调试段包含了调试信息,但是调试目录却位于早先提到的.rdata段之中。这其中每个目录都涉及了.debug段之中的调试信息。调试目录的结构IMAGE_DEBUG_DIRECTORY被定义为:
WINNT.H
typedef struct _IMAGE_DEBUG_DIRECTORY {
ULONG Characteristics;
ULONG TimeDateStamp;
USHORT MajorVersion;
USHORT MinorVersion;
ULONG Type;
ULONG SizeOfData;
ULONG AddressOfRawData;
ULONG PointerToRawData;
} IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;
这个段被分为单独的部分,每个部分为不同种类的调试信息数据。对于每个部分来说都是一个像上边一样的调试目录。不同的调试信息种类如下:
WINNT.H
#define IMAGE_DEBUG_TYPE_UNKNOWN 0
#define IMAGE_DEBUG_TYPE_COFF 1
#define IMAGE_DEBUG_TYPE_CODEVIEW 2
#define IMAGE_DEBUG_TYPE_FPO 3
#define IMAGE_DEBUG_TYPE_MISC 4
每个目录之中的Type域表示该目录的调试信息种类。如你所见,在上边的表中,PE文件格式支持很多不同的调试信息种类,以及一些其它的信息域。对于那些来说,IMAGE_DEBUG_TYPE_MISC信息是唯一的。这一信息被添加到描述可执行映像的混杂信息之中,这些混杂信息不能被添加到PE文件格式任何结构化的数据段之中。这就是映像文件中最合适的位置,映像名称则肯定会出现在这里。如果映像导出了信息,那么导出数据段也会包含这一映像名称。
每种调试信息都拥有自己的头部结构,该结构定义了它自己的数据。这些结构都列于WINNT.H之中。关于IMAGE_DEBUG_DIRECTORY一件有趣的事就是它包括了两个标识调试信息的域。第一个是AddressOfRawData,为相对文件装载的数据虚拟地址;另一个是PointerToRawData,为数据所在PE文件之中的实际偏移量。这就使得定位指定的调试信息相当容易了。
作为最后的例子,请你考虑以下的函数代码,它从IMAGE_DEBUG_MISC结构中提取了映像名称。
PEFILE.C
int WINAPI RetrieveModuleName(LPVOID lpFile, HANDLE hHeap,
char **pszModule)
{
PIMAGE_DEBUG_DIRECTORY pdd;
PIMAGE_DEBUG_MISC pdm = NULL;
int nCnt;
if (!(pdd = (PIMAGE_DEBUG_DIRECTORY)ImageDirectoryOffset
(lpFile, IMAGE_DIRECTORY_ENTRY_DEBUG)))
return 0;
while (pdd->SizeOfData)
{
if (pdd->Type == IMAGE_DEBUG_TYPE_MISC)
{
pdm = (PIMAGE_DEBUG_MISC)((DWORD)pdd->PointerToRawData +
(DWORD)lpFile);
nCnt = lstrlen(pdm->Data) * (pdm->Unicode ? 2 : 1);
*pszModule = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY,
nCnt+1);
CopyMemory(*pszModule, pdm->Data, nCnt);
break;
}
pdd ++;
}
if (pdm != NULL)
return nCnt;
else
return 0;
}
你看到了,调试目录结构使得定位一个特定种类的调试信息变得相对容易了些。只要定位了IMAGE_DEBUG_MISC结构,提取映像名称就如同调用CopyMemory函数一样简单。
如上所述,调试信息可以被剥离到单独的.DBG文件中。Windows NT SDK包含了一个名为REBASE.EXE的程序可以实现这一目的。例如,以下的语句可以将一个名为TEST.EXE的调试信息剥离:
rebase -b 40000 -x c:/samples/testdir test.exe
调试信息被置于一个新的文件中,这个文件名为TEST.DBG,位于c:/samples/testdir之中。这个文件起始于一个单独的IMAGE_SEPARATE_DEBUG_HEADER结构,接着是存在于原可执行映像之中的段头部的一份拷贝。在段头部之后,是.debug段的数据。也就是说,在段头部之后,就是一系列的IMAGE_DEBUG_DIRECTORY结构及其相关的数据了。调试信息本身保留了如上所描述的常规映像文件调试信息。
PE文件格式总结
Windows NT的PE文件格式向熟悉Windows和MS-DOS环境的开发者引入了一种全新的结构。然而熟悉UNIX环境的开发者会发现PE文件格式与COFF规范很相像(如果它不是以COFF为基础的话)。
整个格式的组成:一个MS-DOS的MZ头部,之后是一个实模式的残余程序、PE文件标志、PE文件头部、PE可选头部、所有的段头部,最后是所有的段实体。
可选头部的末尾是一个数据目录入口的数组,这些相对虚拟地址指向段实体之中的数据目录。每个数据目录都表示了一个特定的段实体数据是如何组织的。
PE文件格式有11个预定义段,这是对Windows NT应用程序所通用的,但是每个应用程序可以为它自己的代码以及数据定义它自己独特的段。
.debug预定义段也可以分离为一个单独的调试文件。如果这样的话,就会有一个特定的调试头部来用于解析这个调试文件,PE文件中也会有一个标志来表示调试数据被分离了出去。
PEFILE.DLL函数描述
PEFILE.DLL主要由一些函数组成,这些函数或者被用来获得一个给定的PE文件中的偏移量,或者被用来把文件中的一些数据复制到一个特定的结构中去。每个函数都有一个需求——第一个参数是一个指针,这个指针指向PE文件的起始处。也就是说,这个文件必须首先被映射到你进程的地址空间中,然后映射文件的位置就可以作为每个函数第一个参数的lpFile的值来传入了。
我意在使函数的名称使你能够一见而知其意,并且每个函数都随一个详细描述其目的的注释而列出。如果在读完函数列表之后,你仍然不明白某个函数的功能,那么请参考EXEVIEW.EXE示例来查明这个函数是如何使用的。以下的函数原型列表可以在PEFILE.H中找到:
PEFILE.H
/* 获得指向MS-DOS MZ头部的指针 */
BOOL WINAPI GetDosHeader(LPVOID, PIMAGE_DOS_HEADER);
/* 决定.EXE文件的类型 */
DWORD WINAPI ImageFileType(LPVOID);
/* 获得指向PE文件头部的指针 */
BOOL WINAPI GetPEFileHeader(LPVOID, PIMAGE_FILE_HEADER);
/* 获得指向PE可选头部的指针 */
BOOL WINAPI GetPEOptionalHeader(LPVOID, PIMAGE_OPTIONAL_HEADER);
/* 返回模块入口点的地址 */
LPVOID WINAPI GetModuleEntryPoint(LPVOID);
/* 返回文件中段的总数 */
int WINAPI NumOfSections(LPVOID);
/* 返回当可执行文件被装载入进程地址空间时的首选基地址 */
LPVOID WINAPI GetImageBase(LPVOID);
/* 决定文件中一个特定的映像数据目录的位置 */
LPVOID WINAPI ImageDirectoryOffset(LPVOID, DWORD);
/* 获得文件中所有段的名称 */
int WINAPI GetSectionNames(LPVOID, HANDLE, char **);
/* 复制一个特定段的头部信息 */
BOOL WINAPI GetSectionHdrByName(LPVOID, PIMAGE_SECTION_HEADER,
char *);
/* 获得由空字符分隔的导入模块名称列表 */
int WINAPI GetImportModuleNames(LPVOID, HANDLE, char **);
/* 获得一个模块由空字符分隔的导入函数列表 */
int WINAPI GetImportFunctionNamesByModule(LPVOID, HANDLE, char *,
char **);
/* 获得由空字符分隔的导出函数列表 */
int WINAPI GetExportFunctionNames(LPVOID, HANDLE, char **);
/* 获得导出函数总数 */
int WINAPI GetNumberOfExportedFunctions(LPVOID);
/* 获得导出函数的虚拟地址入口点列表 */
LPVOID WINAPI GetExportFunctionEntryPoints(LPVOID);
/* 获得导出函数顺序值列表 */
LPVOID WINAPI GetExportFunctionOrdinals(LPVOID);
/* 决定资源对象的种类 */
int WINAPI GetNumberOfResources (LPVOID);
/* 返回文件中所使用的所有资源对象的种类 */
int WINAPI GetListOfResourceTypes(LPVOID, HANDLE, char **);
/* 决定调试信息是否已从文件中分离 */
BOOL WINAPI IsDebugInfoStripped(LPVOID);
/* 获得映像文件名称 */
int WINAPI RetrieveModuleName(LPVOID, HANDLE, char **);
/* 决定文件是否是一个有效的调试文件 */
BOOL WINAPI IsDebugFile(LPVOID);
/* 从调试文件中返回调试头部 */
BOOL WINAPI GetSeparateDebugHeader(LPVOID,
PIMAGE_SEPARATE_DEBUG_HEADER);
除了以上所列的函数之外,本文中早先提到的宏也定义在了PEFILE.H中,完整的列表如下:
/* PE文件标志的偏移量 */
#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + /
((PIMAGE_DOS_HEADER)a)->e_lfanew))
/* MS操作系统头部标识了双字的NT PE文件标志;PE文件头部就紧跟在这个双字之后 */
#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + /
((PIMAGE_DOS_HEADER)a)->e_lfanew + /
SIZE_OF_NT_SIGNATURE))
/* PE可选头部紧跟在PE文件头部之后 */
#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + /
((PIMAGE_DOS_HEADER)a)->e_lfanew + /
SIZE_OF_NT_SIGNATURE + /
sizeof(IMAGE_FILE_HEADER)))
/* 段头部紧跟在PE可选头部之后 */
#define SECHDROFFSET(a) ((LPVOID)((BYTE *)a + /
((PIMAGE_DOS_HEADER)a)->e_lfanew + /
SIZE_OF_NT_SIGNATURE + /
sizeof(IMAGE_FILE_HEADER) + /
sizeof(IMAGE_OPTIONAL_HEADER)))
要使用PEFILE.DLL,你只用包含PEFILE.H文件并在应用程序中链接到这个DLL即可。所有的这些函数都是互斥性的函数,但是有些函数的功能可以相互支持以获得文件信息。例如,GetSectionNames可以用于获得所有段的名称,这样一来,为了获得一个拥有独特段名称(在编译期由应用程序开发者定义的)的段头部,你就需要首先获得所有名称的列表,然后再对那个准确的段名称调用函数GetSectionHeaderByName了。现在,你可以享受我为你带来的这一切了!(全文完)
MSDN原文:The Portable Executable File Format from Top to Bottom
原作者:Randy Kath
李马 2003/12/24 译于山西太原