65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET Manifest Resources

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (8投票s)

2005年10月28日

CPOL

5分钟阅读

viewsIcon

83131

downloadIcon

512

一篇关于 .NET Manifest Resources 内部格式的文章。

output

引言

本文讨论的是 .NET Manifest Resources 的内部格式(或者更确切地说,是其中包含的 ".resources" 文件)。我不知道这里的代码是否对你有用(很可能没用),但我喜欢撰写关于未公开主题的文章。事实上,这篇文章没什么了不起的,我写它只是因为我在网上找不到任何关于这个主题的文档,甚至在 .NET 元数据规范(Partition II MetaData.doc)中也没有。

前段时间,我(因为需要)编写了一个名为 CFF Explorer 的 PE 编辑器,并支持 .NET 元数据,因为当时没有这样的工具。我能找到的唯一工具是 Asmex(你可以在 CSDN 上找到它),但这个工具的问题在于你无法修改元数据字段,而且它仍然依赖于 .NET Framework。我并不是想批评 Asmex,它肯定很有用,只是因为我需要一些不同的东西。总之,我为 PE 编辑器编写了一个资源查看器,并想展示元数据资源。所以,为了做到这一点,并且避免使用外部 .NET 程序集,我不得不分析 Manifest Resource 格式。

让我们来看看 .NET 程序集中的 Manifest Resources

CFF Explorer

正如你所见,可以有各种类型的文件。例如,读取一个位图非常简单:每个 Manifest Resource 都以一个 DWORD 开头,它告诉我们实际嵌入资源的大小……仅此而已……之后,就是我们的位图。好的,但那些 ".resources" 文件呢?这篇文章就全是关于它们的。在这个截图中,.NET 程序集中的每个对话框都有一个 ".resources" 文件,这意味着对话框的所有资源都包含在对话框自己的 ".resources" 文件中。

使用 .NET Framework 来处理这些已编译的资源文件非常容易。你可以通过一个名为 Resgen.exe 的实用工具(可在 MSDN 上下载)将它们转换为 XML 文件(".resx" 文件),或者直接使用 System.Resources 命名空间下的成员(它们也能处理 ".resx" 文件)来做任何你想做的事情。例如,你可以创建一个(代码来自 MSDN)

using System;
using System.Resources;

public class WriteResources {
   public static void Main(string[] args) {
      
      // Creates a resource writer.
      IResourceWriter writer = new ResourceWriter("myResources.resources");
    
      // Adds resources to the resource writer.
      writer.AddResource("String 1", "First String");

      writer.AddResource("String 2", "Second String");

      writer.AddResource("String 3", "Third String");

      // Writes the resources to the file or stream, and closes it.
      writer.Close();
   }
}

通过使用 ResourceWriter,创建资源文件是一项非常容易的任务。那么如何读取它们呢?

using System;
using System.Resources;
using System.Collections;
 
public class ReadResources {

   public static void Main(string[] args) {

      // Opens a resource reader and gets an enumerator from it.
      IResourceReader reader = new ResourceReader("myResources.resources");
      IDictionaryEnumerator en = reader.GetEnumerator();
      
      // Goes through the enumerator, printing out the key and value pairs.
      while (en.MoveNext()) {
         Console.WriteLine();
         Console.WriteLine("Name: {0}", en.Key);
         Console.WriteLine("Value: {0}", en.Value);
      }
      reader.Close();
   }
}

确实非常容易。通过 IResourceReader 接口,我们可以请求一个枚举器,它会给我们每一个资源的名称和值。也可以直接从文件中加载特定资源等等。所以,正如你所见,Framework 提供了我们处理资源文件所需的一切。无论如何,对于那些仍然对了解内部格式感兴趣的人,请继续阅读。

资源文件格式

一个非常简短的描述。第一个 DWORD 是一个签名,必须是 0xBEEFCACE,否则资源文件将被视为无效。第二个 DWORD 包含此资源文件的读取器数量,别担心,这不是我们需要讨论的……这是 Framework 的东西。第三个 DWORD 是读取器类型的大小。这个数字仅对我们跳过后面的字符串(或字符串)有好处,字符串类似:"System.Resources.ResourceReader, mscorlibsSystem.Resources.RuntimeResourceSet, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"。它告诉 Framework 使用哪个读取器来处理这个资源文件。

好了,现在我们进入有趣的部分。下一个 DWORD 告诉我们资源文件的版本(现有版本是 1 和 2)。在版本之后,另一个 DWORD 给出了文件中实际资源的数量。再紧随其后的是另一个 DWORD,它给出资源类型的数量。

为了收集我们所需的附加信息,我们需要跳过资源类型。对于每种类型,都有一个 7 位编码的整数,它给出后面字符串的大小。要解码这类整数,你需要读取每个字节,直到找到一个最高位未设置的字节,并进行一些额外的操作来获得最终值……目前,我们先坚持格式。在跳过类型之后,我们需要将我们的位置对齐到 8 字节基准。然后我们有 DWORD * NumberOfResources,每个 DWORD 包含一个资源的哈希值。然后我们有相同数量的 DWORD,这次是资源名称的偏移量。紧接着一个重要的 DWORD:数据部分偏移量。我们需要这个偏移量来检索资源偏移量。在这个 DWORD 之后,就是资源名称。好吧,实际上不仅仅是名称(我只是这样称呼它),每个名称(7 位编码整数 + Unicode 字符串)后面都有一个 DWORD,这是一个偏移量,你可以将其加到数据部分偏移量来检索资源偏移量。给定资源偏移量后,我们找到的第一项是一个 7 位编码的整数,它是当前资源的类型索引。

源代码

我把所有代码都放在一个简单的类里了

class CResourcesFile
{

public:

    CResourcesFile();
    ~CResourcesFile();

    BYTE *pBaseAddress;
    UINT Size;

    DWORD Version;
    DWORD NumberOfResources;
    DWORD NumberOfTypes;

    BYTE *pTypes;
    BYTE *pNamesOffsets;
    BYTE *pDataSection;
    BYTE *pNames;

    BOOL ProcessResourcesFile(BYTE *pAddress, UINT uSize);
    BOOL ReadName(UINT nResource, WCHAR *Str, UINT Len);
    BOOL GetResourceInfo(UINT nResource, WCHAR *Str, UINT Len,
        DWORD *Offset, INT *TypeIndex);

private:

    BOOL DecodeInt(BYTE *pAddress, INT *Value, UINT *uSize);
};

这个类的使用非常简单,你只需将类及其所有成员复制/粘贴到你的项目中,并以某种方式像这样使用它

void main()
{
    TCHAR FileName[MAX_PATH];

    _tprintf(_T("Resources File to open:\n"));
    _tscanf(_T("%s"), FileName);

    //
    // Open and read file
    //

    // ....

    CResourcesFile ResFile;

    if (ResFile.ProcessResourcesFile(BaseAddress, FileSize) == FALSE)
    {
        VirtualFree(BaseAddress, 0, MEM_RELEASE);
        return;
    }

    _tprintf(_T("\n\nFile: %s\n"), FileName);
    _tprintf(_T("Version: %d\n"), ResFile.Version);
    _tprintf(_T("Number of resources: %d\n"), ResFile.NumberOfResources);
    _tprintf(_T("Number of types: %d\n"), ResFile.NumberOfTypes);

    _tprintf(_T("\nList resources:\n\n"));

    WCHAR ResName[1024];

    for (UINT x = 0; x < ResFile.NumberOfResources; x++)
    {
        DWORD Offset;
        INT TypeIndex = 0;

        if (ResFile.GetResourceInfo(x, ResName, 1024, &Offset, &TypeIndex))
        {
            _tprintf(_T("Name: %S - Offset: %08X - TypeIndex: %d\n"), 
                ResName, Offset, TypeIndex);
        }
    }

    VirtualFree(BaseAddress, 0, MEM_RELEASE);

    getch();
}

首先需要做的是使用 ProcessResourcesFile 函数处理资源文件

BOOL CResourcesFile::ProcessResourcesFile(BYTE *pAddress, UINT uSize)
{
    BYTE *ptr = pAddress;

    //
    // Collect basic information: pointer and size of the file
    //

    pBaseAddress = ptr;
    Size = uSize;

    //
    // Read the magic number, its value has to be: 0xBEEFCACE
    //

    DWORD MagicNumber;

    MagicNumber = *(DWORD *) ptr;
    ptr += sizeof (DWORD);

    if (MagicNumber != RESOURCES_MAGIC_NUMBER)
        return FALSE;

    DWORD NumberOfReaderTypes;

    NumberOfReaderTypes = *(DWORD *) ptr;
    ptr += sizeof (DWORD);

    DWORD SizeOfReaderTypes;

    SizeOfReaderTypes = *(DWORD *) ptr;
    ptr += sizeof (DWORD);

    //
    // Skip ReaderTypes
    //

    ptr += SizeOfReaderTypes;

    //

    Version = *(DWORD *) ptr;
    ptr += sizeof (DWORD);

    //
    // Read number of resources
    //

    NumberOfResources = *(DWORD *) ptr;
    ptr += sizeof (DWORD);

    //
    // Read number of types
    //

    NumberOfTypes = *(DWORD *) ptr;
    ptr += sizeof (DWORD);

    //
    // Skip Types: (CHAR *Type;) * NumOfTypes
    // (Save position)
    //

    pTypes = ptr;

    for (UINT x = 0; x < NumberOfTypes; x++)
    {
        INT StringSize = 0;
        UINT ValueSize = 0;

        if (!DecodeInt(ptr, &StringSize, &ValueSize))
            return FALSE;
        ptr += ValueSize;

        ptr += StringSize;
    }

    //
    // Alignes position
    //

    DWORD Position = (DWORD) (((ULONG_PTR) ptr) - ((ULONG_PTR) pBaseAddress));

    DWORD Aligned = Position & 7;

    if (Aligned != 0)
    {
        ptr += (8 - Aligned);
    }

    //
    // Skip name hashes
    //

    ptr += (sizeof (DWORD) * NumberOfResources);

    //
    // Skip name positions (first save location)
    //

    pNamesOffsets = ptr;

    ptr += (sizeof (DWORD) * NumberOfResources);

    //
    // Read Data Section Offset
    //

    DWORD DataSectionOffset;

    DataSectionOffset = *(DWORD *) ptr;
    ptr += sizeof (DWORD);

    pDataSection = (BYTE *) (DataSectionOffset + ((ULONG_PTR) pBaseAddress));

    //
    // Save names position
    //

    pNames = ptr;

    return TRUE;
}

ReadName 只是 GetResourceInfo 的一个简短版本,所以我们只看 GetResourceInfo,跳过其余部分

//
// Collect Resource Info
//

BOOL CResourcesFile::GetResourceInfo(UINT nResource, WCHAR *Str, UINT Len, 
                                     DWORD *Offset, INT *TypeIndex)
{
    //
    // Read name
    //

    DWORD NameOffset = *(DWORD *) ((nResource * sizeof (DWORD)) + 
        ((ULONG_PTR) pNamesOffsets));

    if (NameOffset > (DWORD) (((ULONG_PTR) pNames) - ((ULONG_PTR) pDataSection)))
        return FALSE;

    ZeroMemory(Str, Len * sizeof (WCHAR));

    BYTE *ptr = (BYTE *) (NameOffset + ((ULONG_PTR) pNames));

    INT NameSize = 0;
    UINT ValueSize = 0;

    if (!DecodeInt(ptr, &NameSize, &ValueSize))
        return FALSE;
    ptr += ValueSize;

    memcpy(Str, ptr, NameSize);

    ptr += NameSize;

    //
    // After reading the name
    //

    DWORD DataOffset = *(DWORD *) ptr;

    BYTE *pData = (BYTE *) (DataOffset + ((ULONG_PTR) pDataSection));

    //
    // Collect info
    //

    if (Offset) *Offset = (DWORD) (((ULONG_PTR) pData) - 
        ((ULONG_PTR) pBaseAddress));

    if (TypeIndex)
    {
        *TypeIndex = 0;
        ValueSize = 0;

        if (!DecodeInt(pData, TypeIndex, &ValueSize))
            return FALSE;
    }
    
    return TRUE;
}

就这样,希望它有所帮助。

后记

当然,这还不是全部,如果你想处理资源,你需要根据它们的类型来处理。这意味着你需要从 TypeIndex 中获取它们的类型。检索类型字符串非常简单;与其跳过所有类型,不如只跳过位于 TypeIndex 之前的那些类型,然后读取后面的字符串。在我的 CFF Explorer 中,我支持某些类型的资源(并且只显示那些),例如位图、图标和 PNG。

CFF Explorer

玩得开心!

© . All rights reserved.