让我们一起学Metadata API(1)

    Metadata API是.NET提供的一系列用于读取和生成Metadata(元数据)的接口方法,它与ICorDebug和ICorProfiler接口相辅相成,构成了.NET提供给用户的核心功能。这篇文章是本系列第一篇,主要介绍一下Metadata API的背景和基本使用常识,并手把手的带你写出一个最基本的程序用来显示.NET文件的元数据信息,后续的文章将不断地完善这个程序,同时学习该接口提供的各种功能。(但我不能保证有时间写出一个完善的工具,而且类似工具已经有不少,因此可以留给各位自己实践。)

附件:metadatateach1.rar

    首先给出参考文献。
1、.NET Framework SDK/ToolDevelopGuide/Doc/Unmanaged Metadata API(这是V1.1的文档所有地址,V2.0已经被移至MSDN。)
2、本文所参考的代码主要是SDK V1.1中Samples里的Metainfo例子。
3、由于我用的是win32下的VC+WTL编写界面,所以如果你对WTL一无所知,可以参考CodeProject上的关于WTL的Tutorial。

一、  什么是Metadata API?
要说清这个问题,恐怕首先得了解什么是元数据。从知其然的角度上讲,元数据就是数据的数据,或者说,它的存在就是为了描述已有的数据(比如代码,属性等)。从知其所以然的角度讲,正是因为有了元数据,.NET才有可能做到跨平台。针对不同的平台(Windows,Unix,etc),微软只需要改变.NET面向平台的部分,而由于程序中包含了自我描述的元数据,因此它便可以在不同的但都装有.NET Framework的机器上运行。
    上面那段可能有点抽象,说白了,从逆向的角度看,元数据就是我们要得到的PE文件信息,其中包含了文件头,代码,资源,各种表,各种字符串,各种数据。就像win32下,如果我们要汉化一个程序,或者想分析一个程序的注册流程,必须要了解该程序的资源和反汇编代码一样,在.NET下想分析一个程序,也要了解它的元数据。

二、  Metadata API的实现方式
Win32下,我们可以编写软件读取文件头,然后定位到.text节,然后用反汇编引擎得到asm代码,既而分析之。在.NET下要达到这个目的,有三种方式。最传统的方法就是自已编写程序读取文件头,自已编写引擎进行MSIL的反汇编。这不是不可取,只是你必须对.NET PE结构有不一般的了解,而且,你必须有相当的时间和精力。但是.NET为我们提供了两种现成的方法,一是在托管代码(VB.NET,VC.NET,C#)中调用System.Reflection空间的方法,二是在win32程序中调用一系列IMetadata接口。今天我们要学的是第二种,也就是在win32下通过调用一系列COM接口来读取Metadata。

三、  COM基础
虽然这节叫COM基础,我却不准备多讲。一是本身学艺不精,二是没有必要将COM的理论全盘托出。我们只看几个最基本的概念。
1、  COM要求所有的方法都会返回一个HRESULT类型的错误号,而HRESULT其实就是一个类型定义:typedef LONG HRESULT。我们在代码中经常使用下面的代码来判断运行结果:

HRESULT hr;
if (FAILED(hr))
{
Goto exit;

2、COM中所有的接口都要求从IUnknown接口继承,我们的IMetadata*也不例外。当然,我们不用直接继承IUnknown,但是它提供了一个方法是我们程序中经常用的,也是最基本的:
HRESULT QueryInterface([in] REFIID riid,[out] void **ppv);
顾名思义,就是取得某个特点的接口。也只有取得了这些接口后,我们才能调用其中的方法。

(COM的东西就想讲多,想多了解的新手可以参考VC知识库在线杂志第26期的文章《COM技术初探》。)

四、  第一个例子
不多说了,直接看例子是最容易明白的,下面我们进入第一个例子,功能就是简单地显示一个PE文件的版本。
    首先在VS中新建VC WTL工程。如果你没有WTL也没关系,新建一个win32 app,然后自已添加如下的控件。主要是一个EDIT控件,用来显示Metadata信息。

 

    另外,对于WTL用户,由于要使用CString,必须在stdafx.h中加上
#define _WTL_USE_CSTRING
#include <atlmisc.h>
    还有三个.NET核心头文件
#include <cor.h>
#include <winerror.h>
#include <corhlpr.h>

下面开始添加代码。首先在对话框初始化中添加

cEdit.Attach(GetDlgItem(IDC_EDIT_MAIN));

    这只是便于后面调用EDIT控件方便,对于普通win32 API编程来说,没有必要了。如果用MFC,请自行添加,因为我不会MFC。

    然后在Browse控件中添加代码,主要实现的功能是:打开文件,初始化接口,调用显示信息的函数,最后注销COM。代码如下:
LRESULT CMainDlg::OnOpenFile(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/BOOL/*bHandled*/)
{
    
    HRESULT hr = 0;
    CFileDialog dlgOpen(TRUE,NULL,NULL,OFN_FILEMUSTEXIST,L".NET PE File\0*.*\0",m_hWnd);


    CoInitialize(0);    

    hr = CoCreateInstance(CLSID_CorMetaDataDispenser, NULL, CLSCTX_INPROC_SERVER, 
        IID_IMetaDataDispenserEx, (void **) &g_pDisp);

    if(FAILED(hr))
    {
        ShowErrorMsg("CoCreateInstance Fail!",hr);
        goto exit;
    }

    dlgOpen.m_ofn.lpstrTitle=L"Please choose .NET PE files";
    if (dlgOpen.DoModal()==IDOK)
    {
        csFile=dlgOpen.m_szFileName;
        ShowFile();
    }

exit:
    CoUninitialize();
    return 0;
}

    注意首先要调用CoInitialize(0)来告诉系统我要使用COM了,然后创建IMetaDataDispenserEx的实例(这个说法可能不恰当),取得它的指针。这样就可以利用该指针调用它提供的各种方法。参考中告诉该接口的功能如下“The dispenser API is used to map existing metadata so that it can be inspected (and added to), or to create a fresh in-memory area to define new metadata. ”。很明显,我们需要调用它来将PE文件的元数据信息映射入内存,并进行读取(或生成)操作。它提供了以下方法:
2 IMetadataDispenserEx .........................................................................21
2.1 DefineScope ..................................................................................21
2.2 OpenScope....................................................................................21
2.3 OpenScopeOnMemory.....................................................................22
2.4 SetOption......................................................................................22
2.5 GetOption .....................................................................................24
    我们暂时只需要用OpenScope,功能就是Open an existing file, and map its metadata into memory,打开现有文件并将Metadata映射入内存。
    接上述代码,在打开文件成功后,我们便开始显示信息。这里,还有几个接口要获得:

//<summary>
//最基本的几个接口
//</summary>
IMetaDataImport *g_pImport = NULL;
IMetaDataAssemblyImport *g_pAssemblyImport=NULL;
IMetaDataDispenserEx *g_pDisp = NULL;
IMetaDataTables *g_pTables=NULL;

    也就是说在我们获得了IMetaDataDispenserEx后,还有三个接口要用,下面的代码就是分别获取这三个接口,如果错误,会在EDIT控件中显示错误信息。

void    CMainDlg::ShowFile()
{
    HRESULT     hr = S_OK;
    CString        csSpec=L"file:"+csFile;

    hr = g_pDisp->OpenScope((LPCWSTR)csSpec, 0, IID_IMetaDataImport, (IUnknown**)&g_pImport);
    
    if (FAILED(hr))
    {
        ShowErrorMsg("OpenScope failed", hr);
        goto exit;
    }

     hr = g_pImport->QueryInterface(IID_IMetaDataAssemblyImport, (void**) &g_pAssemblyImport);
     if (FAILED(hr))
     {
         ShowErrorMsg("QueryInterface failed for IID_IMetaDataAssemblyImport.", hr);
         goto exit;
     }


     if (g_pImport)
         hr = g_pImport->QueryInterface(IID_IMetaDataTables, (void**)&g_pTables);
     else if (g_pAssemblyImport)
         hr = g_pAssemblyImport->QueryInterface(IID_IMetaDataTables, (void**)&g_pTables);
     
     if (FAILED(hr))
     {
         ShowErrorMsg("QueryInterface failed for IID_IMetaDataTables.", hr);
         goto exit;
     }

     
    ShowAssemblyInfo();

exit:
    return;

}

    看一下顺序,首先是OpenScope进行映射,然后通过Dispenser接口获取IMetaDataImport,再是通过IMetaDataImport得到IMetaDataAssemblyImport,最后是通过IMetaDataImport或者IMetaDataAssemblyImport得到IMetadataTables。几个接口都正确得到后,开始显示PE文件版本信息的源代码:

//<summary>
//显示Assembly基本信息
//</summary>
void    CMainDlg::ShowAssemblyInfo()
{
    HRESULT hr=S_OK;
    CString csResult;
    mdAssembly    mda;
    ASSEMBLYMETADATA MetaData;

    hr = g_pAssemblyImport->GetAssemblyFromScope(&mda);
    if (hr == CLDB_E_RECORD_NOTFOUND)
        return;
    else if (FAILED(hr))
    {
        ShowErrorMsg("GetAssemblyFromScope() failed.", hr);
        return;
    }
    
    ZeroMemory(&MetaData, sizeof(ASSEMBLYMETADATA));

    hr = g_pAssemblyImport->GetAssemblyProps(mda, 
        NULL, NULL,    // Public Key.
        NULL,          // Hash Algorithm.
        NULL, 0, NULL, // Name.
        &MetaData,
        NULL);         // Flags.


    if (FAILED(hr)) 
    {
        ShowErrorMsg("GetAssemblyProps() failed.", hr);
        return;
    }
    
    csResult.Format((LPCWSTR)L"\\\\\\\\\\Assembly Information\\\\\\\\\\\r\nMajor Version: %d\r\nMinor Version: %d\r\nBuild Number: %d\r\nRevision Number: %d\r\n",
                    MetaData.usMajorVersion,MetaData.usMinorVersion,MetaData.usBuildNumber,MetaData.usRevisionNumber);

    ShowMetaMsg(csResult);
    return;
}

    过程很清楚,代码也很少,首先是AssemblyImport->GetAssemblyFromScope的调用,得到mdAssembly mda,这是Assembly的token值。(在.NET中,token是非常基本也是非常重要的概念。)然后利用该token,并调用g_pAssemblyImport->GetAssemblyProps,得到一个基本结构ASSEMBLYMETADATA。ASSEMBLYMETADATA的结构如下:

typedef struct
{
    USHORT      usMajorVersion;         // Major Version.   
    USHORT      usMinorVersion;         // Minor Version.
    USHORT      usBuildNumber;          // Build Number.
    USHORT      usRevisionNumber;       // Revision Number.
    LPWSTR      szLocale;               // Locale.
    ULONG       cbLocale;               // [IN/OUT] Size of the buffer in wide chars/Actual size.
    DWORD       *rProcessor;            // Processor ID array.
    ULONG       ulProcessor;            // [IN/OUT] Size of the Processor ID array/Actual # of entries filled in.
    OSINFO      *rOS;                   // OSINFO array.
    ULONG       ulOS;                   // [IN/OUT]Size of the OSINFO array/Actual # of entries filled in.
} ASSEMBLYMETADATA;

    它的定义在cor.h中,其实就包含了两个version和两个Number,这就是我们今天要取得的信息。最后就是输出版本信息了。程序运行后的结果如图:
 

    今天就介绍这个简单的例子,下次我们会利用这个非常有趣的接口取得一些更深入的信息。你对这篇文章有什么看法,或本文有什么纰漏,请不吝赐教!