前几天公司里一个项目要做 MUI 支持,于是要生成一堆 XXX.dll.mui 的文件。如果这些 MUI DLL 的工程手动去建立、维护的话,那就太!@#@!#!了。当时是另外一个同事去做这方面的工作的,后来他给了个工具,按照它定义的简单格式来书写多语言字符串,这个工具会从一个已经设定好的 DLL 项目出发,更改 RC 文件里的字符串,然后调用 VS 的 IDE 来生成 DLL。再然后调用 MUIRCT.exe 来生成 MUI 文件。
这可以节省很多时间。但是,由于是调用 VS IDE 来编译的,一个带有近百个 Project 的 Solution 编译起来并不快,需要一到两分钟。这让我有了另辟蹊径的念头。
何不自己来“编译”生成 DLL 呢?
不错,后来我就往这个方向琢磨了。之前曾写过一个修改 PE 文件版本号的小工具,所以现在对于 PE 的资源格式有点并不那么恐惧了。但是,往细处做下去,问题就来了。现在网上的关于 PE 格式的文章,对 NTHeader 解释得很详细,而资源段往往只讲到资源目录、资源项,具体各项的存储结构却没有详细说明了。
这里,关于 PE 头等就不多说了,请参考网上的文章,特别是 http://bbs.pediy.com/showthread.php?threadid=21932。本文将着眼于资源段。
首先来看一下几个数据结构(这些内容好多文章也有提及):
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
这是资源目录,共 16 字节,其中最后两个 WORD 加起来是紧跟在后面的子项的数目。
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 字节。其中第一个成员为数据成员,最高位 1 表示数据是字符串,剩下 31 位是字符串的偏移;否则就是数值。第二个成员最高位为 1 表示下一层仍然是目录,后 31 位指向另一个 IMAGE_RESOURCE_DIRECTORY 结构;否则整个成员指向一个 IMAGE_RESOURCE_DATA_ENTRY 结构(这个马上会讲到)。需要注意的是,这里的两个 Offset 都表示从资源段开头到目标位置的偏移。
最后来看 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 字节。其中第一个成员 OffsetToData 指向具体的数据,这个偏移是个 RVA,跟前面两个不一样。Size 表示具体数据的总字节数。后两个成员可以为 0,CodePage 不建议使用。
PE 文件中的资源就是通过这三个结构表示的,它们都在 WinNT.h 中定义。通常会有 3 层结构,第一层表示资源类型,第二层表示 ID,第三层标识语言。
以上所说的是我能查到的资料里能够提到的最大程度的内容了。但是具体的数据如何存储,却几乎没有文章提及。于是,花了一两天时间来慢慢的看、加上试验,我认为我对字符串资源的格式基本清楚了。(下面内容是我自己分析得出,其正确性我并不保证)。
我们先来看一个具体的例子。这是一个资源 DLL,用 Resource Hacker 查看如图:
其资源段数据如下:
我用桔色框起来的是资源目录,用粉色框起来的是资源目录项,用浅绿色框起来的是资源数据项。
先看第一行,这是第一层目录,最后两个 WORD 是 0x0000 和 0x0001,表示后面“命名”的目录项有 0 个,使用 ID 的目录项有 1 个。第二行开头的 8 字节就是这个目录项,DWORD 0x00000006 表示资源类型是 6,也就是字串表,后面的地址是 0x80000018,最高位为 1,表示指向的仍然是一个目录,其偏移是 0x00000018,也就是 0218h 处。
0218h 处这个资源目录是第二层了。最后仍然是 0 和 1,于是我们来看 0228h 处的目录项。第一个 DWORD 是 1,这个跟 ID 有关,稍候讨论。他的第二个 DWORD 是 0x80000030,仍然指向目录。
0230 处的目录是第三层目录。注意到最后是 0 和 2,下面将有连续两个目录项。第一个目录项值为 0x00000409(1033,英语(美国)),偏移地址 0x00000050,最高位 0,表示指向的是数据项,而不是目录了。第二个目录项值为 0x00000804(2052,中文(中国)),偏移地址 0x0000009C。
这三层结构和 Resource Hacker 中显示的是一一对应的。
我们先来看英语的那个数据项,OffsetToData 是 0x00001060(RVA),Size 是 0x0000003C。这个 DLL 文件的资源段的 VirtualAddress 是 1000h,1060h-1000h+200h = 260h,我们来看 260h 处(其实就是紧接着的地方)。我第一次看这段数据的时候也很奇怪,为什么前面空了 2 个字节,后面有多出好多字节。于是我改它的 ID,试了好些次,终于找到规律了。资源目录第二层的 ID(下文称 ResID)和最终的字符串 ID(下文称 StrID)有这么一个对应关系:ResID = StrID / 16 + 1。StrID 0 到 15 所对应的 ResID 都是 1, StrID 16 到 31 对应 ResID 2,……。反过来说,资源目录中的 ResID 不能完全表达 StrID 的信息。所以,在 260h 开始的 3Ch 个字节的数据块里,其实要存储 16 个字符串,其 StrID 分别是 0,1,2,……,15。这 16 个字符串是连续存储的,结构是:字符串长度(WORD)+字符串内容(不含结束符 0)。那些空位就由一个 WORD 0 来填充(也可理解为长度为 0 的字符串)。我在图中用红褐色的竖线划出了这 16 个字符串的界限。后面那个中文的也是如此,就不重复说了。
到现在为止,对于字串表的结构,应该说差不多清楚了。于是拿程序去生成似乎不是难事了,不过要注意的是,目录项必须紧跟在目录后面,目录项指向的位置可以随意。
事实上上面这个 DLL 是我用程序生成的。我现在做到了从内部数据结构到资源 DLL 这个过程的实现。如果这也可以被称为“编译”的话,现在是实现了后端。至于前端,我还没想好原始资源格式。要想让这个工具有点用处,原始资源格式必须要:1、足够简单(至少比 RC 文件简单),并且维护方便;2、足够存储多语言字符串。这方面我希望大家能给我一些建议。
当然,本文的主要内容还是讨论字串表的格式,这个已经讲完了,所以,over~ bow~
posted on 2009-09-23 22:57
溪流 阅读(2266)
评论(3) 编辑 收藏 引用 所属分类:
ASM & Crack