从编程员的角度理解 NTFS 2000:流和硬链接

简介

自 1994 年以来,有关 Microsoft(R) Windows NT(R) 的完全面向对象版的神话已流传一段时日了。Cairo — 传说中的 OS 版本的代码名 — 从未在 Redmond 的实验室以外得以实现。自有 Cairo 起,它的一些基本思想就不时地被公之于众。

Cairo 背后的基本思想是:文件和文件夹应成为对象和对象的集合。文件夹的内容不必局限于基础文件系统存储机制,您可将那些对象作为独立的、单独的项目,访问并复 制它们。文件和文件夹对象将用方法和属性的术语展示可编程的 API,这些术语既可以是标准的,也可以是由拥有者或作者定义的。

而我们今天所拥有的,是一个在某些内部结构中注册文件和文件夹的文件系统,当文件和文件夹在磁盘中移动时,它会被复制。文件和文件夹具有一套固定的 功能,而这些功能太少了,不能满足现代应用程序的需求。作为工作区的一部分,在过去的几年中,我们提供了几项技术用于向文件和文件夹添加附加信息。 Shell 和 namespace 扩展名、desktop.ini 文件、FileSystemObject 和“Shell 自动对象模型”就是几个例子。不过,所有这些功能仅仅是少量的和局部的解决方案。它们完全不能成为对 Windows 的文件系统进行有机的重新设计的基点。因为向前兼容性是一个严肃的问题,所以 Windows 仍然采用建立在文件分配表 (FAT) 上的旧式文件系统,它的诞生日期可追溯到 Microsoft MS-DOS(R) 2.0 版!即使最近做了更多的改进,如支持高容量的硬盘,FAT 对于存储文件和文件夹信息来说,仍然是一种不太合适的方法。

几年来的实践经验表明,我们遇到的最重要的限制是必须处理程序员正确管理并识别文件所需的附加信息。最近,有人请我检索 Word 97 文档的实际创 建日期。您可能认为这是一项简单的工作,因为创建日期是一个可以通过某些 API 功能轻易检索到的属性。这只是部分正确的。试着在不同的机器上、甚至相同的文件夹中复制相同的 Word 文件,然后比较两个副本的创建日期。奇怪的是,它们并不相同!当复制文件时,您创建了一个带有表明何时进行创建的时间标记的全新文件。当继续处理副本时, 您丢失了关于何时初创文件的潜在有价值的信息。

幸好,Word 文档在内部的 SummaryInformation 字段保留此信息。因此,在我的情况中,我得以解决了该问题并成功地通告了客户。如果是 Access 或文本文件,那么我的努力就白费了。

对于 Windows NT,Microsoft 引入了称为 NTFS 的新式文件系统。在它所有引人注目的功能中,B 树结构尤为显著,它加速了大文件夹上的文件检索、基于文件的安全、记录、增强的文件系统可恢复性以及比 FAT 或 FAT32 更好地利用磁盘空间。(顺便说明,Windows 2000 提供对 FAT32 卷的完全支持和访问。)

自从它们被 Windows 3.1 采用以后,NTFS 卷还具有另一个通常被忽视的功能:它们支持多个数据流流入单个文件。对于 Windows 2000,流支持再次被加强了,并增加了其他一些顺手的功能,以帮助您无缝地处理文件。让我们来见识一下 NTFS 2000 — 与 Windows 2000 同步的 NTFS 版 — 的主要功能吧。

NTFS 2000 概述

如果多个数据流不是 NTFS 2000 卷文件的独占功能,那么还有其他几种功能要求使用 Windows 2000。它们是:

  • 文件和目录加密
  • 每个用户、每卷的磁盘配额
  • 重新分析点和分级存储管理
  • 装入点
  • 硬链接
  • 更改记录

在 Windows 2000 安装过程中,要求您指定是否想将 Windows 2000 卷转换为 NTFS 2000。不过,只在机器作为域控制器时,才要求使用 NTFS 2000 文件系统。您可以在任何时候,通过使用命令行实用程序 convert.exe,将 FAT 分区转换为 NTFS:

 CONVERT volume /FS:NTFS [/V]

其中 volume 参数指定驱动器号,后面跟着一个冒号。它也可以是一个装入点或一个卷名。/FS:NTFS 选项指明该卷必须被转换为 NTFS。最后,如果您希望以详细模式运行实用程序,则使用 /V。当您运行 convert.exe 时,它进行初始化,然后请求您重新启动。重新启动之后,转换立即生效。

除了上述列出的所有功能外,Windows 2000 整个文件夹管理的显著方面,是它提供给 desktop.ini 文件的全面而稍微扩展的支持。在本文的其余部分中,我将主要侧重于流和硬链接。然而表 1 概述了涉及 NTFS 2000 其他关键功能的要点。

表 1. NTFS 2000 的主要功能

 

多文件流

在 NTFS 文件系统下,每个文件可拥有多个数据流。值得指出的是,流并不是 NTFS 2000 的特性,而是从 Windows NT 3.1 起就已存在了。当您读取位于非 NTFS 卷中的文件内容时(如:Windows 98 机器上的磁盘分区),您只能访问一个数据流。从而,您认为它是该文件的真正而“唯一”的内容。此类主流没有名称,而且是非 NTFS 文件系统能够处理的唯一的流。但当您在 NTFS 卷上创建文件时,情形就不同了。请参见图 1 来理解一下大的图形吧。

图 1. 多流文件的结构

多流文件是所有嵌入相同文件系统项目的单流文件的一类集合。它们看上去无疑像唯一的、基本的单元,然而包含一系列独立的子单元,您可以分别创建、删 除、修改它们。有一些常见的编程环境,其中的流是绰绰有余的。但是,如果您打算使用它们,请记住一旦您将多流文件复制到非 NTFS 存储设备(如 CD、软盘或非 NTFS 磁盘分区)上,则所有多余的流便丢失,且不可恢复。遗憾的是,这种兼容性问题使得流在实际应用中不那么受欢迎。对于设计并限定只在 NTFS 卷上运行的服务器端应用程序来说,流是一个出色的工具,可被沿用于建立杰出的、具有创造性的解决方案。

流的基本原理

当您在非 NTFS 卷上复制多流文件时,只复制了主流文件。这意味着您丢失了多余的数据,因为即使您将该文件复制回 NTFS 磁盘,它们也不会再次出现了。现在,假设您专门在 NTFS 机器上工作,让我们看看如何创建命名流。在代码示例 1 您可看到 Windows Script Host (WSH),以及 Microsoft Visual Basic(R) Scripting Edition (VBScript) 文件,它演示如何从 NTFS 文件中读写流。

要想在文件中识别命名流,您应遵守特殊的命名规则,并在文件名末尾加上一个冒号,然后是流的名称。例如,要想访问 test.txt 文件上的 VersionInfo 流,您应使用以下文件名:

 Test.txt:VersionInfo

与操纵文件的任何 Microsoft Win32(R) API 函数一起使用这个文件名。要想访问 VersionInfo 流的内容,将该名称传递给 CreateFile(),然后用 ReadFile()WriteFile() 照常完成读写。如果您想要检查某个特定的流是否存在于文件中,按如下所示编写文件流的名称,并使用 CreateFile() 检查它是否存在:

HANDLE hfile = CreateFile(szFileStreamName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);CloseHandle(hfile);if (hfile == NULL) MessageBox(hWnd, "Error", NULL, MB_OK);

要想对流进行处理,您不必是一个熟练的 C++ 程序员。您也可以在 Visual Basic 中、甚至在脚本代码中利用流,如代码示例 1 所示。使这种透明性称为可能的关键因素是,所有低级 Win32 API 函数,特别是 CreateFile(),均支持 NTFS 分区上基于流的文件名。如果您试图在非 NTFS 分区上,例如在 Windows 98 机器上,打开称为 Test.txt:VersionInfo 的文件,您将会得到“未找到文件”的出错消息。请注意,问题的实质是,只是含有该文件的卷的文件系统,而不是调用应用程序所驻留的 Windows 平台或磁盘分区类型。换言之,您也可以通过连接的 Windows 98 机器,成功地访问 NTFS 分区上共享文件夹中特定的命名流。此外,应考虑到,即使对长文件名来说,冒号也不是有效的字符。因此,当 CreateFile() 遇到文件名中的冒号时,会知道它具有特殊的含义。

代码示例 1 所示,您也可以与 VBScript 一起使用流,因为 FileSystemObject 对象模式非常有效地运用 CreateFile() 来打开、写入、创建和测试文件。在示例代码中,我创建的文本文件带有空数据、0 长度的主流以及所需的任意多个命名流。请试着运行演示程序并创建两个流。可以将它们命名为 VersionInfoVersionInfoEx。Windows shell 中没有任何迹象有助于您推断出在某一特定的文件中有多个流的存在。在图 2 中,您可看到 test.txt文件在“Windows 资源管理器”中的样子。

图 2. 一个文件的长度可为 0,但具有命名流。

Size 列只显示了未命名的主流的大小,甚至在属性对话框中也无法获取关于流的更多信息。只有在 NTFS 卷上,Windows 2000 属性对话框中,您才有唯一的机会能够读到所有文件的相关信息,包括文本文件。单击摘要选项卡,然后输入,比如,一个作者名,如图 3 所示。

顺便提一句,由于 Windows 2000 的 shell 用户界面的改进,此类名称可在特定的作者列中显示出来。有关详细信息,请参阅位于 http://msdn.microsoft.com/msdnmag/(英文)上 MSDN Magazine 中的首要问题。

图 3. NTFS 卷上 .txt 文件的相关附加信息

嗨,等一下。尽管摘要信息是您为 Word 或 Excel 文档设置的一般数据,但它更无疑是文档本身的一部分。能不能将它与文本文件相结合而又不改变纯文本的内容呢?当然能。Shell 通过流来完成它!应用那些改变后,立即尝试将该文件复制到另一个非 NTFS 分区中。将出现如图 4 所示的对话框。

图 4. Windows 2000 关于可能的流数据丢失的预警。

事实证明 test.txt 文件包含一个带文档摘要信息的流。当您试图将带有附加信息的文件复制到不支持该文件的卷中时,系统会有所察觉。在非 NTFS 分区中,只复制了未命名的主流,其余的则被废除。因此,如果目标文件不相符,则基于流的文件几乎不会被交换。

流备份和枚举

是否有办法 — 一种或两种 API 函数 — 来枚举某一特定文件拥有的所有流呢?是的,有。但它并非那么简单而直观。Win32 备份 API 函数(BackupReadBackupWrite 等等),可被用来枚举文件中的流。不过,它们用起来有点怪异,而且看上去更像一个工作区,而不是有效的最终的解决方案。

其思路是,当您想要备份一个文件或整个文件夹时,您需要打包并存储全部可能存在的信息。因此,当需要尝试枚举文件中的流时,BackupRead() 是您最好的朋友。我将重点介绍该函数的原型:

BOOL BackupRead( HANDLE hFile, LPBYTE lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, BOOL bAbort, BOOL bProcessSecurity, LPVOID *lpContext );

为了我们的目的,您此处可忽略诸如上下文和安全等方面。hFile 参数必须通过调用 CreateFile() 获得,而 lpBuffer 应指向 WIN32_STREAM_ID 数据结构:

typedef struct _WIN32_STREAM_ID { DWORD dwStreamId; DWORD dwStreamAttributes; LARGE_INTEGER Size; DWORD dwStreamNameSize; WCHAR cStreamName[ANYSIZE_ARRAY]; } WIN32_STREAM_ID, *LPWIN32_STREAM_ID;

这种结构的前 20 个字节表示每个流的标题。流的名称紧随 dwStreamNameSize 字段后面出现,名称后面跟着流的内容。因为传统的文件内容可被视为流 — 尽管是未命名的流,所以要想枚举所有的流,您只需进行循环,直到 BackupRead 返回 False。实际上,BackupRead 应该能读取所有与给定的文件或文件夹相关的信息:

WIN32_STREAM_ID sid;ZeroMemory(&sid, sizeof(WIN32_STREAM_ID));DWORD dwStreamHeaderSize = (LPBYTE)&sid.cStreamName - (LPBYTE)&sid+ sid.dwStreamNameSize;bContinue = BackupRead(hfile, (LPBYTE) &sid, dwStreamHeaderSize, &dwRead, FALSE, FALSE, &lpContext);

上面的这个小段是在流的标题中读到的关键代码。如果该操作是成功的,即可尝试读取该流的实际名称:

WCHAR wszStreamName[MAX_PATH]; BackupRead(hfile, (LPBYTE) wszStreamName, sid.dwStreamNameSize, &dwRead, FALSE, FALSE, &lpContext);

在访问下一个流之前,首先要调用 BackupSeek(),向前移动备份指示器:

BackupSeek(hfile, sid.Size.LowPart, sid.Size.HighPart, &dw1, &dw2, &lpContext);

在多数情况下,您可将流视为常规文件 — 如,要删除流,可以用 DeleteFile()。如果想要刷新流的内容,只需使用 ReadFile()WriteFile()。没有正式的和得到支持的方法来移动或重新命名流。在本文的最后部分,我将利用本代码建立一个 NTFS 2000 专用的 Windows shell 扩展,将新的属性页添加到所有带流信息的文件中。同时,让我们来快速浏览一下 NTFS 的另一个特性。

硬链接

您知道快捷方式吗? — 就是那些小 .lnk 文件,它们多数散布在桌面上,用于引用其他内容。毫无疑问,快捷方式是一个有用的特性,但也存在一些缺点。首先,如果使来自不同文件夹的多个快捷方式指向 同一个目标,您实际上拥有了同一个 — 幸好较小 — 文件的多个副本。更为重要的是,快捷方式的目标对象会随着时间而更改。它可能会被移动、删除或仅仅是重新命名。您的快捷方式情形如何呢?它们能否检测并跟 踪到那些更改,从而正确地(自动)更新呢?很遗憾,它们不能。其主要原因是,快捷方式是应用程序级的功能。从系统的角度来看,它们只不过是用户定义的文 件,当您想要打开它们时,只需做一些额外的工作即可。考虑到拥有快捷方式是一种特权,您可能会决定也将其分配给其他文件类。假如这样做有意义,您可以创建 属于自己的、扩展名不是 .lnk 的快捷方式类。完成该任务的是在类节点下叫做 IsShortcut 的注册表项。假设您想让 .xyz 文件作为快捷方式。通过在 HKEY_CLASSES_ROOT 下创建 .xyz 节点来注册该文件类,并使其指向另一个节点,通常是 xyzfile。然后将空的 REG_SZ 项目添加到:

HKEY_CLASSES_ROOTxyzfile

这样就做完了。

其他操作系统,尤其是 Posix 和 OS/2,具有在系统级上起作用的类似功能。特别是 OS/2,将它们称为 shadows。 硬链接是给定文件在系统级上的快捷方式。通过创建现有文件的硬链接,您既没有复制该文件,也没有复制对其基于文件的引用(即,快捷方式)。相反,您将信息 添加到 NTFS 级上它的目录项中。物理文件在原始位置上原封未动。简言之,您现在可使用两个或更多个名称来访问相同的内容了!

硬链接使您免于保留同一个文件的多个(除需要外)副本,使负责管理不同路径名的系统处理单一的物理内容。这就极大地简化了您的工作,并节省了宝贵的 磁盘空间。另外,硬链接作为系统级的快捷方式,始终指向正确的目标文件 — 无论您是否重新命名或移动了它。由于该链接存储在文件系统级,因此所有的更改都自动而透明地得到应用。值得注意的是,硬链接必须在相同的 NTFS 卷中被创建。比如,您不能让驱动器 C: 上的硬链接,指向驱动器 D: 上的文件。

为了听起来更熟悉,可以将硬链接想象为文件的别名。您可以使用任何一个别名访问该文件,只有当删除了所有别名以后,该文件才能被删除。(别名的作用正如引用计数一样。)因为硬链接是别名,所以使它们的内容同步是不成问题的。

CreateHardLink() 是用于创建硬链接的 API 函数。其原型如下所示:

BOOL CreateHardLink( LPCTSTR lpFileName, LPCTSTR lpExistingFileName, LPSECURITY_ATTRIBUTES lpSecurityAttributes ); 

在旧的 MIND 一文所含的代码中(请参阅 “Windows 2000 for Web Developers” 的 MIND(英文),1999 年 3 月),我提供了一个 COM 对象,使您能够通过脚本代码创建硬链接。代码示例 2 显示了使用它来创建给定文件的硬链接的 VBScript 程序。尽管很容易找出一个文件有多少个硬链接,然而没有枚举所有硬链接的工具。API 函数 GetFileInformationByHandle() 填充了 BY_HANDLE_FILE_INFORMATION 结构,其 nNumberOfLinks 字段向您发出关于枚举的通知。枚举所有链接文件的名称稍微困难一点。基本上,您必须扫描整个卷,并且为每一个文件跟踪分配给它的唯一 ID。当您遇到现有的 ID 时,就已经找到该文件的一个硬链接了。文件的唯一 ID 是由系统分配的,并被存储在 BY_HANDLE_FILE_INFORMATIONnFileIndexHigh nFileIndexLow 字段中。

享受 NTFS 功能

对于向文件添加附加信息,而又不改变或损坏原始格式,同时不占用磁盘空间来说,流的作用特别重要。当然,流会占用其本身的空间,然而 “Windows 资源管理器”似乎没有觉察到这一点。流对于“Windows 资源管理器”来说是不可见的,所以尽管看上去似乎有足够的可用磁盘空间,但实际上可用的磁盘空间已经降低到危险的程度了。您可以将附加(不可见的)信息添 加给任何文件,包括文本和可执行文件。

另一方面,硬链接是聚集共享信息的杰出资源。您只有一个真正的、可以从各个不同的路径访问的信息库。要知道,硬链接对于 Windows NT 技术来说,并不是一个全新的概念。自从 Windows NT 一出台,就有了硬链接。然而,直到有了 Windows 2000,Microsoft 才提供了创建硬链接的公用函数。每个文件至少有一个到其自身的链接,因而 GetFileInformationByHandle 总会返回大于零的链接数。您不能将硬链接设置到目录,而只能设置到文件。

流和硬链接有个共同的实际问题,就是它们从 shell 得到的支持极为有限。为了补救这一问题,我编写了一个 shell 扩展,以提供有关给定文件的流和硬链接的信息。图 5 图示了它的外观和感观。

图 5. 流选项卡显示了关于流和硬链接的信息。

Shell 扩展的源代码用 BackupRead() API 函数枚举流。只需通过调用 DeleteFile(),选定的流的内容即可被删除。编辑流按钮运行代码示例 1 中的脚本代码,通过它,您可以添加或更新流。同样地,创建硬链接按钮运行代码示例 2 中的代码,以创建附加的链接。只有在刷新后,用户界面才反映出所有的更改。最后应注意,要记住如果您删除了硬链接(即删除了文件),只要被删除的文件仍在“回收站”里,则链接的总数就不会被更新。

摘要

在本文中,我只是粗浅地介绍了 NTFS 2000,侧重于其主要功能,如流和硬链接。如果您想对 Windows 2000 文件系统的新功能有更为广泛的了解,我建议您参阅“A File System for the 21st Century: Previewing the Windows NT 5.0 File System(21 世纪的文件系统:预览 Windows NT 5.0 文件系统)”一文,它是由 Jeff Richter 和 Luis Cabrera 于 1998 年 11 月为 MSJ 撰写的,(http://www.microsoft.com/msj/1198/ntfs/ntfs.htm(英文))。该文并未涉猎一些引人注目的话题,尤其是稀疏流和重新分析点,不过,如果您对此文感兴趣的话,请告知我们,我们会进一步帮助您。

代码示例 1

代码示例 2

Read: 1063

VC++中用内存映射文件

软件的开发过程中,有时需要控制一些程序使他们不能同时运行,也就是多个程序间互斥运行(还包括禁止同一程序运行多个实例)。针对这一问题,我们在Visual C++6.0中利用内存映射文件实现了多个程序间的互斥运行。内存映射文件可以创建一个没有和磁盘文件相联系的内存对象,将文件的信息映射到一个进程的地址空间上,我们可以访问该文件中的数据,就如同它位于内存中一样。同时,在程序设计中可以给内存映射文件对象起一个名字,这个名字在整个系统中是唯一的,这个名字可以在多个进程之间共享,通过名字共享能实现进行信息交换,进而实现多个程序间的互斥运行。

在讲述具体的编程方法之前,让我们先介绍和内存映射文件操作有关的几个重要的函

  1. CreateFileMapping的函数为指定的文件创建一个文件映射对象,该函数的原形如下:

    HANDLE CreateFileMapping(HANDLE hFile,//用于映射的文件句柄 LPSECURITY?ATTRIBUTES FileMappingAttributes,//内存映射文件的安全描述符 DWORD Flprotect,//文件映射对象的最大长度的高32位 DWORD dwMaximumSizelow,//最大长度的低32位 LPCTSTR IPNAME//指定这个内存映射文件的名字)

    值得注意的是,参数如果是OXFFFFFFFF,将在操作系统虚拟内存页面替换文件中创建文件映射对象,而不是使用磁盘文件,同时必须给出这个映射对象的大小。

  2. NAO VUEWIFFILE函数将文件的视图映射到一个进程地址空间上,返回LPVOID类型的内存指针。通过它,就可以直接访问文件视图中的信息。

LPVOID MAP VIEWLFFILE(HANDLE HFILEMAPPINGOBUCT,//映射文件对象句柄 DWORD DWDESIREDACCESS,//访问模式 DWORD DWFILEOFFSETHIGH,//文件偏移地址的高32位 DWORD DWFILEOFFSETHIGH,//文件偏移地址的低32位 DWORD DWNUMBEROFBYTESTOMAP//映射视图的大小)

在Visual c++6.0中我们用默认方式生成基于对话框的应用程序,在程序的初始化阶段,在CwinApp生类的Initln_stance函数的开始处,添加以下代码:

(//创建内存映射文件对象,mu_texRunning是其名字,所有需要互斥运行 //的程序都使用这个名字(这些代码对于需要互斥运行的程序是通用的)HANDLE hMap=CreateFileMapping((HANDLE)0Xffffffffnull,PAGE_READWRLTE,0,128,"Mu_texRunning") if(hMap==NULL)//如果创建失败 (AfxMessageBox("创建用于互斥运行的内存映文件对象失败!”,MB?OK MB??ICLNSTOP);

return FALSE;//退出此程序)

//如果已经存在这个同名对象,说明已有需要互斥的其他程序运行了

else if(GetLastError( )==ER_ROR_ALREADY_EXISTS)

(LPVOID ipMen=MapViewOFFile(hMap,FILE_MAP_WRITE,0,0,0);

Cstring str=(char*)ipMem;//获得已在运行的程序的描述信息

UnmapViewofFile(lpMem);//解除映射图

CloseHandle(hMap);//关闭此对象

AfxMessageBox(str,MB_ok MB_ICONSTOP);显示有关的描述信息

Return FALSE;//退出此程序)

Else//经过上面的检查,说明这是第一个运行的互斥程序

(LPVOID ip_mem=MapViewofFile(hMap,FILE_MAP_WRITE,0,0,0);

//这里可写入该程序运行的描述信息,上面的错误提示就是这信信息

strcpy((char*)lpMem,"xxx程序正在运行!”);

UnmapViewofFile(lpMem);//解除映射图)

//下面可以继续执行函数INITIN?STANCE原有的代码了

AfxEnableControl_comtainer();

//当程序运行结束了,要记住调用CHANDIE(HMAP)关闭这个对象句柄,//这里可以在Initinstance函数最后returnFALSE之前调用

CloseHandle(hMap);//关闭内存映文件对象句柄 RETURN false;)以上的程序在Visual C++6.中已调试通过。其他非对话框类型的程序可以在各自的初始化和终止阶段添加类似的代码,只是如果内存映射文件对象的句柄hMap可能在不同函数中使用,那就要将其定义成CwinApp生类的成员变量或是全局变量了。

Read: 810

如何将一个文件分割成多个小文件

   你也许会遇到到这样一个问题?当你有一个较大的软件,而无法用一张软盘将其全部拷下时,你也许会想到该将它分解开,分盘拷回去后,再将它们合并起来。现在的这种分割工具很多,你想自己动手做一个适合自己的分割工具么?下面就让我用以前用VC做的一个<袖珍文件分割器>的例程来告诉你吧!程序运行后界面如下图:

基本构成思想:文件分割的基本思想比我之前发表的另一篇文章<如何将多个文件合并为一个可执行程序>的构成思想简单多了,它主要也分为分割文件和合并分割后的文件二大部分。分割文件,将原文件按指定分割大小进行等分,然后顺序读取其指定分割大小数据后到写到各自的新建文件中。合并文件,将各分割后的文件顺序读取后,写入到一个文件中既可。

1、分割文件时:打开文件,读取指定的分割大小一段数据,写入到一新建文件中,接着再读同样大小的一段数据,再写入到一新建文件中……,直到读出文件最后一部分数据,写入到最后一个新建文件中。对每一个分割后的新建文件名,采用原文件名前加数字信息的方法,按分割的顺序,按个加上一数字标识信息,以便合并时使用。
分割文件的部分代码实现如下:
//文件分割涵数
int CFileSpltDlg::SplitMe()
{
…… (省略:此部分代码实现省略掉)
//分割文件
do {
//动态建立一个新建文件名的前的数字
name = _ltoa(l, buff, 10);
name += _T("_");
CString newpath;

//判断选择目录未尾是否已有""符
if(m_targetpath.Right(1)==’\’)
newpath = m_targetpath;
else
newpath = m_targetpath + _T("\");
if (!destFile.Open(newpath + name + m_SourceFile.GetFileName(),
CFile::modeWrite |
CFile::shareExclusive |
CFile::typeBinary |
CFile::modeCreate, &ex)) {
TCHAR szError[1024];
ex.GetErrorMessage(szError, 1024);
::AfxMessageBox(szError);
m_SourceFile.Close();
return 1;
}
do {
dwRead = m_SourceFile.Read(buffer, nCount);
destFile.Write(buffer, dwRead);
}//当文件小于指定要分割的大小时
while (dwRead > 0 && destFile.GetLength() < newlen);
destFile.Close();

l++;
UpdateWindow();
}while (dwRead > 0);
m_SourceFile.Close();
return 0;

2、合并文件时:和上面分割所采用的方法相反,将各个分割后的小文件读出后,按其分割后文件名前数字大小的顺序,按个写入到新建的文件中,这一新建文件的名字,为去掉分割后文件前面数字部分后的文件名(既原文件名)。
合并文件的部分代码实现如下:
// 文件合并涵数
int CFileSpltDlg::MergeMe()
{
…… (省略:此部分代码实现省略掉)
//开始合并文件
do {
//自动定位分割文件名前的数字信息
pref = _ltoa(l, buff, 10);
pref += _T("_");
//打开新的分割文件
if (!m_SourceFile.Open(newpath + pref + m_filename,
CFile::modeRead |
CFile::shareExclusive |
CFile::typeBinary, &ex)) {
TCHAR szError[1024];
ex.GetErrorMessage(szError, 1024);
destFile.Close();
m_path = _T("");
m_filename = _T("");
newpath = _T("");
UpdateData(FALSE);
return 0;
}
else
//形成一个新的文件名
name = _T(newpath + pref + m_filename);
do {//写入到目标文件中
dwRead = m_SourceFile.Read(buffer, nCount);
destFile.Write(buffer, dwRead);
}while (dwRead > 0);

m_SourceFile.Close();

l++;
UpdateWindow();
}while (l < 500);//little bit dirty solution, but you can always improve it!…

return 0;
}

以上各部分代码的具体实现,请在下载例程后,参看源代码既可。

联系方式:
地址:陕西省西安市劳动路2号院六单元
邮编:710082
作者EMAIL:jingzhou_xu@163.net
如何将一个文件分割成多个小文件

Read: 762

用Visual C++操作INI文件

在我们写的程序当中,总有一些配置信息需要保存下来,以便完成程序的功能,最简单的办法就是将这些信息写入INI文件中,程序初始化时再读入.具体应用如下:

  一.将信息写入.INI文件中.

  1.所用的WINAPI函数原型为:

BOOL WritePrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpString,
LPCTSTR lpFileName
);

  其中各参数的意义:

   LPCTSTR lpAppName 是INI文件中的一个字段名.

   LPCTSTR lpKeyName 是lpAppName下的一个键名,通俗讲就是变量名.

   LPCTSTR lpString 是键值,也就是变量的值,不过必须为LPCTSTR型或CString型的.

   LPCTSTR lpFileName 是完整的INI文件名.

  2.具体使用方法:设现有一名学生,需把他的姓名和年龄写入 c:studstudent.ini 文件中.

CString strName,strTemp;
int nAge;
strName="张三";
nAge=12;
::WritePrivateProfileString("StudentInfo","Name",strName,"c:\stud\student.ini");

  此时c:studstudent.ini文件中的内容如下:

   [StudentInfo]
Name=张三

  3.要将学生的年龄保存下来,只需将整型的值变为字符型即可:

strTemp.Format("%d",nAge);
::WritePrivateProfileString("StudentInfo","Age",strTemp,"c:\stud\student.ini");
二.将信息从INI文件中读入程序中的变量.

  1.所用的WINAPI函数原型为:

DWORD GetPrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpDefault,
LPTSTR lpReturnedString,
DWORD nSize,
LPCTSTR lpFileName
);

  其中各参数的意义:

   前二个参数与 WritePrivateProfileString中的意义一样.

   lpDefault : 如果INI文件中没有前两个参数指定的字段名或键名,则将此值赋给变量.

   lpReturnedString : 接收INI文件中的值的CString对象,即目的缓存器.

   nSize : 目的缓存器的大小.

   lpFileName : 是完整的INI文件名.

  2.具体使用方法:现要将上一步中写入的学生的信息读入程序中.

CString strStudName;
int nStudAge;
GetPrivateProfileString("StudentInfo","Name","默认姓名",strStudName.GetBuffer(MAX_PATH),MAX_PATH,"c:\stud\student.ini");

  执行后 strStudName 的值为:"张三",若前两个参数有误,其值为:"默认姓名".

  3.读入整型值要用另一个WINAPI函数:

UINT GetPrivateProfileInt(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
INT nDefault,
LPCTSTR lpFileName
);

  这里的参数意义与上相同.使用方法如下:

nStudAge=GetPrivateProfileInt("StudentInfo","Age",10,"c:\stud\student.ini");
三.循环写入多个值,设现有一程序,要将最近使用的几个文件名保存下来,具体程序如下:

  1.写入:

CString strTemp,strTempA;
int i;
int nCount=6;
file://共有6个文件名需要保存
for(i=0;i {strTemp.Format("%d",i);
strTempA=文件名;
file://文件名可以从数组,列表框等处取得.
::WritePrivateProfileString("UseFileName","FileName"+strTemp,strTempA,
"c:\usefile\usefile.ini");
}
strTemp.Format("%d",nCount);
::WritePrivateProfileString("FileCount","Count",strTemp,"c:\usefile\usefile.ini");
file://将文件总数写入,以便读出.

  2.读出:

nCount=::GetPrivateProfileInt("FileCount","Count",0,"c:\usefile\usefile.ini");
for(i=0;i {strTemp.Format("%d",i);
strTemp="FileName"+strTemp;
::GetPrivateProfileString("CurrentIni",strTemp,"default.fil", strTempA.GetBuffer(MAX_PATH),MAX_PATH,"c:\usefile\usefile.ini");

file://使用strTempA中的内容.

}

  补充四点:

   1.INI文件的路径必须完整,文件名前面的各级目录必须存在,否则写入不成功,该函数返回 FALSE 值.

为在VC++中, \ 才表示一个 .

   3.也可将INI文件放在程序所在目录,此时 lpFileName 参数为: ".\student.ini".

 

Read: 747

PE 格式学习总结(四)– PE文件中的资源

    程序所用到的各种资源,比如 bmp,cursor,menu,对话框等都存在PE文件中。
    我们将详细介绍关于资源的各种结构,通过一个例子来说明资源及其相关结构是怎么放在PE文件中的。以及如何在遍历PE文件中的所有资源。我们只最终找到这些资源在文件中的位置和长度。而不具体分析某种资源的格式,比如有个BMP的资源,我们不分析BMP格式。

一 找到资源在文件中位置。

    资源都放在PE文件的某个节中,该节的节表项中的PointerToRawData,就是资源节在文件中的位置。

1.1 得到PE Header在文件中的位置。
    通过DOS Header结构的成员e_lfanew,可以确定PE Header的在文件中的位置。

1.2 得到文件中节的数目。
    确定PE Header的在文件中的位置之后,就可以确定PE Header中的成员FileHeader和成员OptionalHeader在文件中的位置。根据 FileHeader 中的 成员NumberOfSections 的值,就可以确定文件中节的数目,也就是节表数组中元素的个数。

1.3 得到节表在文件中的位置。
    PE Header在文件中的位置加上PE Header结构的大小就可以得到节表在文件中的开始位置。PE Header结构的大小可以由Signature的大小加上FileHeader的大小再加上FileHeader中的 SizeOfOptionalHeade来确定。其实到目前为止SizeOfOptionalHeade也就是结构Optional Header的大小也是固定的,所以整个PE Header结构的大小也是固定。不过为了安全起见,还是用Signature的大小加上FileHeader的大小再加上FileHeader中的SizeOfOptionalHeade来确定比较保险。

1.4 得到资源节在文件中的位置。
    第1.2步中我们确定了文件中节的数目,第1.3步中我们确定了节表在文件中的位置。
    现在有两种方法来确定资源在文件中的位置。
    第一种方法,根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
比较每一个节表项的Name字段,看是否等于".rsrc"。如果等于。就找到了资源节的节表项。
这个节表项中的 PointerToRawData 中的值,就是资源节在文件中的位置。
    第二种方法,取得PE Header中的Optional Header中的DataDirectory数组中的第三项,
也就是资源项。DataDirectory[]数组的每项都是IMAGE_DATA_DIRECTORY结构,该结构定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
取得DataDirectory数组中的第三项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。
然后根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),
到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。
我们遍历整个节表,看我们取得的资源节的RVA,在哪个节表项的RVA范围之内。
如果在范围之内,就找到了资源节的节表项。
这个节表项中的 PointerToRawData 中的值,就是资源节在文件中的位置。
如果这个PE文件没有资源的话,DataDirectory数组中的第三项内容为0。

这样我们就得到了资源在文件中开始的位置。

二 PE文件中的资源。

    我们已经得到了资源节在文件中的位置。
资源节最开始是一个IMAGE_RESOURCE_DIRECTORY结构。
在WINNT.H中定义如下。

typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
这个结构长度为16字节,共有6个字段。
各字段含义如下:
Characteristics: Resource flags,保留用于以后使用,目前都为0。
TimeDateStamp:资源编译器产生资源的时间。
MajorVersion:
MinorVersion:
NumberOfNamedEntries:用字符串来标示IMAGE_RESOURCE_DIRECTORY_ENTRY项的,紧跟着本结构的IMAGE_RESOURCE_DIRECTORY_ENTRY数组的成员个数。
Number of ID Entries:用整形数字来表示IMAGE_RESOURCE_DIRECTORY_ENTRY项的,紧跟着本结构的IMAGE_RESOURCE_DIRECTORY_ENTRY数组的成员个数。

    IMAGE_RESOURCE_DIRECTORY后面一定会紧跟着一个IMAGE_RESOURCE_DIRECTORY_ENTRY数组。
IMAGE_RESOURCE_DIRECTORY_ENTRY结构定义如下。

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

这个结构长度为8个字节。共有两个字段,每个字段4个字节。
根据不同情况,这两个字段的含义不一样。这个结构的定义如果看不懂的话,后面的例子一看就会明白了。
第 一个字段,当第一个字段的最高位是1的时候,表示,这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容是一个 IMAGE_RESOURCE_DIR_STRING,用里面的字符串来标明这个IMAGE_RESOURCE_DIRECTORY_ENTRY。当第一 个字段的最高位是0的时候,表示,这个DWORD的低WORD中的值作为id标明这个IMAGE_RESOURCE_DIRECTORY_ENTRY。
第二个字段,当第二个字段的最高位是1的时候,表示,还有下一层的结构。这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容会是一个下一层的IMAGE_RESOURCE_DIRECTORY结构,这个请看后面的例子中的说明。
当 第二个字段的最高位是0的时候,表示,已经没有下一层的结构了。这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容会是一个 IMAGE_RESOURCE_DATA_ENTRY结构,IMAGE_RESOURCE_DATA_ENTRY结构会说明资源的位置。

    标示一个IMAGE_RESOURCE_DIRECTORY_ENTRY一般都是使用id,就是一个整数。
但是也有少数的使用IMAGE_RESOURCE_DIR_STRING来标示一个IMAGE_RESOURCE_DIRECTORY_ENTRY。
IMAGE_RESOURCE_DIRECTORY_ENTRY结构定义如下。
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length;
WCHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
这个结构中将有一个Unicode的字符串,是字对齐的。所有这些用来标识的IMAGE_RESOURCE_DIR_STRING都放在一起,这个结构的长度是可变的,由第一个字段Length指明后面的Unicode字符串的长度。

    经过3层IMAGE_RESOURCE_DIRECTORY_ENTRY(一般是3层,也有可能更少些。第一层资源类型bmp,menu等等,第二层资源 名,第三层是资源的Language。)最终会找到一个IMAGE_RESOURCE_DATA_ENTRY结构,这个结构中存有相应(某资源类型,某资 源名,某资源Language)资源的位置和大小,就真正找到资源了。IMAGE_RESOURCE_DATA_ENTRY定义如下。
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData;
DWORD Size;
DWORD CodePage;
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
这个结构长16个字节,有4个字段。
OffsetToData:这是一个内存中的RVA,要转化成文件中的位置,需要用这个值减去资源节的开始RVA,
资源节的开始RVA可以由Optional Header中的DataDirectory数组中的第三项中的VirtualAddress的值得到。
或者节表中,资源节那项中的VirtualAddress的值得到。相减之后,就可以得到相对于资源节开始的偏移。
再加上资源节在文件中的开始位置,节表中资源节那项中的PointerToRawData的值,就是资源在文件中的位置。

Size:资源的大小,以字节为单位。
CodePage:一般来说是Unicode code page。
Reserved:保留,值为0。

上面是资源各种结构的说明,知道这些结构还远远不够,下面我们通过一个例子来看如何通过这些结构找到资源。

我们的例子是Win2k中的可执行文件telnet.exe。为了防止大家版本不同,本文附带了这个PE文件。

PE文件的资源的各种结构放在一个树型结构中,这个结构一般有3层,如图4.1,就是telnet.exe中的情况。

图4.1

图中长的长方形表示一个IMAGE_RESOURCE_DIRECTORY结构,长16个字节,简称directory。
图中短的长方形表示一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,长8个字节,简称directory_entry。
图中圆圈表示一个IMAGE_RESOURCE_DATA_ENTRY结构,长16个字节,简称data_entry。

为了以后的叙述方便还给树的每一个节点起了名字,第一层的叫11,第二层的叫21,22,23,24,第三层的叫31,32,33,34,35,36,37,38,39,310,311,312。

在资源节开始处,是一个directory结构,这个结构中指明了紧跟在它后面的一个directory_entry结 构数组中的元素的个数。这个directory结构之后,紧跟着的就是那个directory_entry结构数组。他们一起组成了11。就如图4.1中 所示。其他的每个节点,21,22..31,32..312,都是这样,每个 directory结构后面紧跟directory_entry 结构数组。11中的directory_entry结构数组中的每一个元素,都存有到下一层某个节点的偏移。也就是通过directory_entry结 构数组的每个元素可以找到21,22,23,24。其他的节点中情况也是一样。图中看不到的一点是,所有的节点之间都是紧紧的挨在一起存放的,11之后紧 跟着的是21,21之后紧跟着的是22,22之后紧跟着的是23。依此类推。directory_entry结构数组中的每一个元素除了有到下一层某节点 的偏移,(是下一层的节点,还是已经到了最终的data_entry,后面详细叙述)还有一个Name或者Id字段(是Name还是Id后面详细叙述), 根据不同的层,代表的含义也不一样。第一层的每个directory_entry的这个值,代表类型。比如11的第一个directory_entry的 Id值为3,3代表icon,从这个directory_entry往下的都是都是图标了(关于不同类型值的定义,后面详细叙述)。第二层每个 directory_entry的这个值代表Name,第三层代表Language。11,21,31的左边那个data_entry,的三个值分别为 3,1,409(都是16进制),就是说是一个图标类型,Name为1h,Language为409h的资源。

下面我们来通过telnet.exe中资源节的具体内容来看,用开始讲到的寻找资源节在文件中位置的方法,我们找到了资源节在文件中的位置为00013600h。

我们为了看起来清楚,每一行是一个结构,并且每个结构的不同成员用 / 分开,例如,
一个directory结构 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 04 00  
可 以看到结构成员,Characteristics为0,TimeDateStamp为0,MajorVersion为4,(如果你不明白为什么是0004 而不是0400的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍),MinorVersion为0,NumberOfNamedEntries为0, NumberOfIdEntries为4。

一个directory_entry结构 03 00 00 00 / 30 00 00 80
可以看到结构 成员,第一个字段的第一个字节00h的二进制为00000000,最高位为0,所以低两个字节中的值为Id,Id为3。第二个字段的第一个字节80h(如 果你不明白为什么第一个字节是80h而不是30h的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍)的二进制为10000000,最高位为1 所以说明还有下一层,还没有到叶子,所以第二字段代表到下一层某个节点的偏移 OffsetToData 值为30。

一个data_entry结构 E0 23 03 00 / 30 01 00 00 / E4 04 00 00 / 00 00 00 00
可 以看到结构成员,OffsetToData为323E0h(这是一个内存中的RVA,要转化成文件中的位置,需要用这个值减去资源节的开始RVA,资源节 的开始RVA可以由Optional Header中的DataDirectory数组中的第三项中的VirtualAddress的值得到。或者节表中,资源节那项中的 VirtualAddress的值得到。相减之后,就可以得到相对于资源节开始的偏移。再加上资源节在文件中的开始位置,节表中资源节那项中的 PointerToRawData的值,就是资源在文件中的位置。),Size为130h,CodePage为4E4h,Reserved为0。

下面就是telnet.exe中的内容,可以用16进制编辑器打开附带的telnet.exe对照着看。

00013600h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 04 00
(directory结构,16字节长。图4.1中11中的directory。0个NamedEntries,4个IdEntries。)
00013610h: 03 00 00 00 / 30 00 00 80
(directory_entry 结构,8字节长。图4.1中11中的directory_entry数组第一个元素。第一个字段高位为0,说明第一个字段表示id,由于是第一层,所以类 型id为3。第二个字段高位为1,说明还有下一层,第二字段中的低31位为到图4.1中21的偏移,30+00013600h=00013630h。)
00013618h: 06 00 00 00 / 50 00 00 80
00013620h: 0E 00 00 00 / A0 00 00 80
00013628h: 10 00 00 00 / B8 00 00 80
00013630h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory21)
00013640h: 01 00 00 00 / D0 00 00 80 (d0+00013600h=000136d0h。)
00013648h: 02 00 00 00 / F0 00 00 80
00013650h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 08 00 (directory22)
00013660h: 08 00 00 00 / 10 01 00 80
00013668h: 09 00 00 00 / 30 01 00 80
00013670h: 0C 00 00 00 / 50 01 00 80
00013678h: 0D 00 00 00 / 70 01 00 80
00013680h: 10 00 00 00 / 90 01 00 80
00013688h: 11 00 00 00 / B0 01 00 80
00013690h: 12 00 00 00 / D0 01 00 80
00013698h: 39 00 00 00 / F0 01 00 80
000136a0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 01 00 / 00 00
(directory结构,16字节长。图4.1中23。1个NamedEntries,0个IdEntries。)
000136b0h: D0 03 00 80 / 10 02 00 80
(directory结构中已经表明这是一个NamedEntries,第一个字段中的高位为1,说明第一个字段中的值为一个指向IMAGE_RESOURCE_DIR_STRING结构的偏移,3D0+00013600h=000139D0h。)
000136b8h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 01 00 (directory24)
000136c8h: 01 00 00 00 / 30 02 00 80
000136d0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory31)
000136e0h: 09 04 00 00 / 50 02 00 00
(directory_entry 结构,8字节长。第一个字段高位为0,说明第一个字段表示id,由于是第三层,所以Language id为409h。第二个字段高位为0,说明已经是叶子了,第二字段中的低31位为到一个data_entry结构的偏移,250+00013600h= 00013850h。)
000136e8h: 04 08 00 00 / 60 02 00 00
000136f0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory32)
00013700h: 09 04 00 00 / 70 02 00 00
00013708h: 04 08 00 00 / 80 02 00 00
00013710h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory33)
00013720h: 09 04 00 00 / 90 02 00 00
00013728h: 04 08 00 00 / A0 02 00 00
00013730h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory34)
00013740h: 09 04 00 00 / B0 02 00 00
00013748h: 04 08 00 00 / C0 02 00 00
00013750h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory35)
00013760h: 09 04 00 00 / D0 02 00 00
00013768h: 04 08 00 00 / E0 02 00 00
00013770h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory36)
00013780h: 09 04 00 00 / F0 02 00 00
00013788h: 04 08 00 00 / 00 03 00 00
00013790h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory37)
000137a0h: 09 04 00 00 / 10 03 00 00
000137a8h: 04 08 00 00 / 20 03 00 00
000137b0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory38)
000137c0h: 09 04 00 00 / 30 03 00 00
000137c8h: 04 08 00 00 / 40 03 00 00
000137d0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory39)
000137e0h: 09 04 00 00 / 50 03 00 00
000137e8h: 04 08 00 00 / 60 03 00 00
000137f0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory310)
00013800h: 09 04 00 00 / 70 03 00 00
00013808h: 04 08 00 00 / 80 03 00 00
00013810h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory311)
00013820h: 09 04 00 00 / 90 03 00 00
00013828h: 04 08 00 00 / A0 03 00 00
00013830h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory312)
00013840h: 09 04 00 00 / B0 03 00 00
00013848h: 04 08 00 00 / C0 03 00 00
00013850h: E0 23 03 00 / 30 01 00 00 / E4 04 00 00 / 00 00 00 00
(data_entry 结构,16字节长,存有一个资源的RVA和大小。资源节开始处的RVA为32000。先算出该资源相对于资源开始处的偏移323E0-32000= 3E0h。再用偏移加上资源节开始处的文件偏移13600得到该资源在文件中的位置,3E0+13600=139E0h。)
00013850h: 10 25 03 00 / 30 01 00 00 / E4 04 00 00 / 00 00 00 00
00013850h: 40 26 03 00 / E8 02 00 00 / E4 04 00 00 / 00 00 00 00
00013860h: 28 29 03 00 / E8 02 00 00 / E4 04 00 00 / 00 00 00 00
00013870h: 10 2C 03 00 / 70 00 00 00 / E4 04 00 00 / 00 00 00 00
00013880h: 80 2C 03 00 / 70 00 00 00 / E4 04 00 00 / 00 00 00 00
00013890h: F0 2C 03 00 / 56 03 00 00 / E4 04 00 00 / 00 00 00 00
000138a0h: 48 30 03 00 / C0 01 00 00 / E4 04 00 00 / 00 00 00 00
000138b0h: F0 2C 03 00 / 56 03 00 00 / E4 04 00 00 / 00 00 00 00
000138c0h: 48 30 03 00 / C0 01 00 00 / E4 04 00 00 / 00 00 00 00
000138d0h: 08 32 03 00 / A8 01 00 00 / E4 04 00 00 / 00 00 00 00
000138e0h: B0 33 03 00 / F4 00 00 00 / E4 04 00 00 / 00 00 00 00
000138f0h: A4 34 03 00 / B6 00 00 00 / E4 04 00 00 / 00 00 00 00
00013900h: 5C 35 03 00 / 94 00 00 00 / E4 04 00 00 / 00 00 00 00
00013910h: F0 35 03 00 / 40 04 00 00 / E4 04 00 00 / 00 00 00 00
00013920h: 30 3A 03 00 / DC 02 00 00 / E4 04 00 00 / 00 00 00 00
00013930h: 0C 3D 03 00 / 32 02 00 00 / E4 04 00 00 / 00 00 00 00
00013940h: 40 3F 03 00 / 90 01 00 00 / E4 04 00 00 / 00 00 00 00
00013950h: D0 40 03 00 / FC 04 00 00 / E4 04 00 00 / 00 00 00 00
00013960h: CC 45 03 00 / C0 03 00 00 / E4 04 00 00 / 00 00 00 00
00013970h: 8C 49 03 00 / B6 00 00 00 / E4 04 00 00 / 00 00 00 00
00013980h: 44 4A 03 00 / 84 00 00 00 / E4 04 00 00 / 00 00 00 00
00013990h: C8 4A 03 00 / 22 00 00 00 / E4 04 00 00 / 00 00 00 00
000139a0h: EC 4A 03 00 / 22 00 00 00 / E4 04 00 00 / 00 00 00 00
000139b0h: 10 4B 03 00 / 60 03 00 00 / E4 04 00 00 / 00 00 00 00
000139c0h: 70 4E 03 00 / 60 03 00 00 / E4 04 00 00 / 00 00 00 00
000139d0h: 06 00 / 54 00 45 00 4C 00 4E 00 45 00 54 00 00 00
(IMAGE_RESOURCE_DIR_STRING结构,长度可变。第一个字段2个字节长,值为6。表明其后的Unicode字符串长度为6。第二字段是一个Unicode字符串,不包括最后的结束符,长度为6,内容是"TELNET")
000139e0h: 28 00 00 00 20 00 00 00 40 00 00 00 01 00 01 00
000139f0h: …

需要补充说明的是,每个directory后面紧跟的是directory_entry数组, directory_entry数组的每个元素,有两个字段,每个字段的高位用来判断该字段代表的含义。尤其是第二字段 OffsetToData ,如果高位为1表明还有下一层,指向另一个directory。如果高位为0,表明指向一个data_entry。directory_entry第一个 字段通常都是作为id,里面低WORD中的值,用来标示这个directory_entry,很少的情况下,第一字段保存一个到unicode字符串的偏 移(本例中000136a0h),用字符串来标示这个directory_entry。如果一个directory后两个字段都不为0的话,即后面紧跟的 directory_entry数组既有NamedEntries,又有IDEntries,那么directory_entry数组首先是 NamedEntries之后紧跟着IDEntries。一般情况下都是一般来说都是有三层,第一层中的directory_entry数组的每个元素的 id,代表不同的类型,不同类型的值在 WINGDI.H 中定义如下
#define RT_CURSOR MAKEINTRESOURCE(1)
#define RT_BITMAP MAKEINTRESOURCE(2)
#define RT_ICON MAKEINTRESOURCE(3)
#define RT_MENU MAKEINTRESOURCE(4)
#define RT_DIALOG MAKEINTRESOURCE(5)
#define RT_STRING MAKEINTRESOURCE(6)
#define RT_FONTDIR MAKEINTRESOURCE(7)
#define RT_FONT MAKEINTRESOURCE(8)
#define RT_ACCELERATOR MAKEINTRESOURCE(9)
#define RT_RCDATA MAKEINTRESOURCE(10)
#define RT_MESSAGETABLE MAKEINTRESOURCE(11)

#define DIFFERENCE 11
#define RT_GROUP_CURSOR MAKEINTRESOURCE((DWORD)RT_CURSOR + DIFFERENCE)
#define RT_GROUP_ICON MAKEINTRESOURCE((DWORD)RT_ICON + DIFFERENCE)
#define RT_VERSION MAKEINTRESOURCE(16)
#define RT_DLGINCLUDE MAKEINTRESOURCE(17)
#if(WINVER >= 0x0400)
#define RT_PLUGPLAY MAKEINTRESOURCE(19)
#define RT_VXD MAKEINTRESOURCE(20)
#define RT_ANICURSOR MAKEINTRESOURCE(21)
#define RT_ANIICON MAKEINTRESOURCE(22)
#endif /* WINVER >= 0x0400 */
#define RT_HTML MAKEINTRESOURCE(23)
也有可能有不到三层的情况,比如只有类型和Name两层,没有Language层。

我们再来看几个data_entry
00013850h: 10 25 03 00 / 30 01 00 00 / E4 04 00 00 / 00 00 00 00
00013850h: 40 26 03 00 / E8 02 00 00 / E4 04 00 00 / 00 00 00 00
00013860h: 28 29 03 00 / E8 02 00 00 / E4 04 00 00 / 00 00 00 00
可 以算出 00013850h 处的 data_entry 中,资源的文件位置为 13B10h(32510-32000+13600) 长度为 130h,所以该资源结束处的位置在文件中的 13C40 h处。而一下个data_entry (00013850h处)中,资源在文件中的位置为 13C40h (32640-32000+13600) 长度为 2E8h。我们可以看到两个资源是首尾相接的,就是说一个资源和另一个资源是紧挨在一起的,中间没有空隙,其他的资源用相同的方法计算,也可以得到同样的 结论。

总结,找到资源节开始的位置,首先是一个directory,后面紧跟着directory_entry数 组,数组的每个元素代表的资源类型不同,通过每个元素,我们可以找到第二层另一个directory,后面紧跟着directory_entry数组。这 一层的数组的每个元素代表的资源Name不同。然后我们可以找到第三层的每个directory,后面紧跟着directory_entry数组。这一层 的数组的每个元素代表的资源Language不同。然后通过每个directory_entry我们可以找到每个data_entry。通过每个 data_entry,我们就可以找到每个真正的资源。
本部分内容较为复杂,需要多阅读几遍。

三 遍历PE文件中的资源

    遍历那个树型结构,找到每个资源的方法之一是,

    一个函数,用来处理directory和它后面紧跟着的directory_entry数组。比如叫 DumpResourceDirectory(),它的参数中的一个是一个directory的地址,函数根据这个地址,得到一个directory结 构,从中得到directory_entry数组元素的个数。然后for循环遍历每个元素,对于每个元素做判断看是否已经到了叶子,也就是 directory_entry的第二个字段的高位是否为0,是1表示没有到叶子,递归调用本函数,不过传入的参数,是根据这个 directory_entry中保存的另一个directory的地址。是0表示已经到了叶子,调用另一个处理叶子的函数,传入相关地址。

   处理叶子的函数,用来处理data_entry结构,负责根据data_entry结构找到真正的资源。比如叫DumpResourceEntry(),它的参数中的一个是一个data_entry的地址。然后跟据data_entry中的值作处理。

   这样,通过递归和判断,就能遍历PE文件中所有的资源。

   用这种方法遍历图4.1中的树,顺序会是11,21,31,32,22,33,34,35,36,37,38,39,310,23,311,24,312。

    这种遍历方法的源程序,可以参考 PEDUMP – Matt Pietrek 1995 。《Windows95系统程式设计大奥秘》附书源码中有。

本文所使用的PE文件 telnet

Read: 758