清单文件格式
清单文件的文件格式尽可能多地从 C++ 和 IDL 借用。 因此,采用普通的 C++ SDK 头文件并将其修改为清单文件相当容易。 分析程序完全支持 C 和 C++ 样式注释,以帮助组织和记录文件。
如果尝试添加清单文件或对现有文件进行更改,最好的方法是进行试验。 在调试器中发出 !logexts.logi 或 !logexts.loge 命令时,Logger 将尝试分析清单文件。 如果遇到问题,将生成一条错误消息,可能指示错误。
清单文件由以下基本元素组成:模块标签、类别标签、函数声明、COM 接口定义和类型定义。 其他类型的元素也存在,但这些元素是最重要的。
模块标签
模块标签只是声明 DLL 导出随后声明的函数的内容。 例如,如果清单文件用于从 Comctl32.dll 记录一组函数,则会在声明任何函数原型之前包含以下模块标签:
module COMCTL32.DLL:
模块标签必须显示在清单文件中的任何函数声明之前。 清单文件可以包含任意数量的模块标签。
类别标签
与模块标签类似,类别标签标识所有后续函数和/或 COM 接口所属的“类别”。 例如,如果要创建Comctl32.dll清单文件,则可以将以下内容用作类别标签:
category CommonControls:
清单文件可以包含任意数量的类别标签。
函数声明
函数声明是实际提示 Logger 记录内容的内容。 它几乎与 C/C++ 头文件中的函数原型完全相同。 格式有一些值得注意的新增功能,以下示例可以最好地说明这一点:
HANDLE [gle] FindFirstFileA(
LPCSTR lpFileName,
[out] LPWIN32_FIND_DATAA lpFindFileData);
函数 FindFirstFileA 采用两个参数。 第一个是 lpFileName,它是一个完整路径, (通常使用通配符) 定义搜索文件的位置。 第二个是指向WIN32_FIND_DATAA结构的指针,该结构将用于包含搜索结果。 返回的 HANDLE 用于将来对 FindNextFileA 的调用。 如果 FindFirstFileA 返回INVALID_HANDLE_VALUE,则函数调用失败,可以通过调用 GetLastError 函数获取错误代码。
HANDLE 类型声明如下:
value DWORD HANDLE
{
#define NULL 0 [fail]
#define INVALID_HANDLE_VALUE -1 [fail]
};
如果此函数返回的值为 0 或 -1 (0xFFFFFFFF) ,则 Logger 将假定该函数失败,因为此类值在值声明中具有 [fail] 修饰符。 (请参阅本节后面的“值类型”部分。) 由于函数名称前面有 [gle] 修饰符,Logger 识别此函数使用 GetLastError 返回错误代码,因此它会捕获错误代码并将其记录到日志文件。
lpFindFileData 参数上的 [out] 修饰符通知记录器数据结构由函数填充,应在函数返回时记录数据结构。
COM 接口定义
COM 接口基本上是可由 COM 对象的客户端调用的函数向量。 清单格式大量借用 COM 中用于定义接口的接口定义语言 (IDL) 。
请考虑以下示例:
interface IDispatch : IUnknown
{
HRESULT GetTypeInfoCount( UINT pctinfo );
HRESULT GetTypeInfo(
UINT iTInfo,
LCID lcid,
LPVOID ppTInfo );
HRESULT GetIDsOfNames(
REFIID riid,
LPOLECHAR* rgszNames,
UINT cNames,
LCID lcid,
[out] DISPID* rgDispId );
HRESULT Invoke(
DISPID dispIdMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS* pDispParams,
VARIANT* pVarResult,
EXCEPINFO* pExcepInfo,
UINT* puArgErr );
};
这会声明一个名为 IDispatch 的接口,该接口派生自 IUnknown。 它包含四个成员函数,这些函数在接口的大括号内按特定顺序声明。 记录器将通过替换接口 vtable 中的函数指针来截获和记录这些成员函数, (运行时使用的函数指针的实际二进制向量,) 替换为自己的函数指针。 有关 Logger 如何在分发接口时捕获接口的详细信息,请参阅本节后面的 COM_INTERFACE_PTR 类型部分。
类型定义
定义数据类型是清单文件开发中最重要的 (和最繁琐) 部分。 清单语言允许你为传入或从函数返回的数值定义用户可读的标签。
例如,Winerror.h 定义一个名为“WinError”的类型,该类型是大多数 Microsoft Win32 函数返回的错误值及其相应的用户可读标签的列表。 这允许 Logger 和 LogViewer 将非信息错误代码替换为有意义的文本。
还可以标记位掩码内的单个位,以允许 Logger 和 LogViewer 将 DWORD 位掩码分解为其组件。
清单支持 13 种基本类型。 下表中列出了这些字段。
类型 | Length | 显示示例 |
---|---|---|
指针 |
4 个字节 |
0x001AF320 |
VOID |
0 字节 |
|
BYTE |
1 个字节 |
0x32 |
WORD |
2 个字节 |
0x0A23 |
DWORD |
4 个字节 |
-234323 |
BOOL |
1 个字节 |
TRUE |
LPSTR |
长度字节加上任意数量的字符 |
“快速棕色狐狸” |
LPWSTR |
长度字节加上任意数量的 Unicode 字符 |
“跳过懒狗” |
GUID |
16 个字节 |
{0CF774D0-F077-11D1-B1BC-00C04F86C324} |
COM_INTERFACE_PTR |
4 个字节 |
0x0203404A |
值 |
依赖于基类型 |
ERROR_TOO_MANY_OPEN_FILES |
掩码 |
依赖于基类型 |
WS_MAXIMIZED |WS_ALWAYSONTOP |
struct |
取决于封装类型的大小 |
+ lpRect nLeft 34 nRight 54 nTop 100 nBottom 300 |
清单文件中的类型定义的工作方式类似于 C/C++ typedef。 例如,以下语句将 PLONG 定义为指向 LONG 的指针:
typedef LONG *PLONG;
已在 Main.h 中声明了大多数基本 typedef。 只需添加特定于组件的 typedef。 结构定义的格式与 C/C++ 结构类型相同。
有四种特殊类型:值、掩码、GUID 和COM_INTERFACE_PTR。
值类型
值是按用户可读标签划分的基本类型。 大多数函数文档仅引用函数中使用的特定常量的 #define 值。 例如,大多数程序员不知道 GetLastError 返回的所有代码的实际值是什么,因此在 LogViewer 中看到一个神秘的数值毫无帮助。 清单值通过允许值声明来克服此问题,如以下示例所示:
value LONG ChangeNotifyFlags
{
#define SHCNF_IDLIST 0x0000 // LPITEMIDLIST
#define SHCNF_PATHA 0x0001 // path name
#define SHCNF_PRINTERA 0x0002 // printer friendly name
#define SHCNF_DWORD 0x0003 // DWORD
#define SHCNF_PATHW 0x0005 // path name
#define SHCNF_PRINTERW 0x0006 // printer friendly name
};
这将声明派生自 LONG 的名为“ChangeNotifyFlags”的新类型。 如果将其用作函数参数,将显示人类可读别名,而不是原始数字。
掩码类型
与值类型类似,掩码类型是一种基本类型 (通常为 DWORD) ,它针对每个具有意义的位划分为人类可读的标签。 请参见以下示例:
mask DWORD DirectDrawOptSurfaceDescCapsFlags
{
#define DDOSDCAPS_OPTCOMPRESSED 0x00000001
#define DDOSDCAPS_OPTREORDERED 0x00000002
#define DDOSDCAPS_MONOLITHICMIPMAP 0x00000004
};
这会声明一个派生自 DWORD 的新类型,如果用作函数参数,则会在 LogViewer 中为用户细分各个值。 因此,如果值为 0x00000005,LogViewer 将显示:
DDOSDCAPS_OPTCOMPRESSED | DDOSDCAPS_MONOLITHICMIPMAP
GUID 类型
GUID 是 COM 中广泛使用的 16 字节全局唯一标识符。 它们通过两种方式声明:
struct __declspec(uuid("00020400-0000-0000-C000-000000000046")) IDispatch;
或
class __declspec(uuid("11219420-1768-11D1-95BE-00609797EA4F")) ShellLinkObject;
第一种方法用于 (IID) 声明接口标识符。 LogViewer 显示时,“IID_”将追加到显示名称的开头。 第二种方法用于 (CLSID) 声明类标识符。 LogViewer 将“CLSID_”追加到显示名称的开头。
如果 GUID 类型是函数的参数,则 LogViewer 会将该值与所有声明的 IID 和 CLSID 进行比较。 如果找到匹配项,它将显示 IID 友好名称。 否则,它将以标准 GUID 表示法显示 32 个十六进制字符值。
COM_INTERFACE_PTR类型
COM_INTERFACE_PTR类型是 COM 接口指针的基类型。 声明 COM 接口时,实际上是在定义派生自 COM_INTERFACE_PTR 的新类型。 因此,指向此类类型的指针可以是函数的参数。 如果COM_INTERFACE_PTR基本类型声明为函数的 OUT 参数,并且有一个单独的参数具有 [iid] 标签,则记录器会将在 IID 中传递的 与所有已声明的 GUID 进行比较。 如果存在匹配项,并且声明了与 IID 同名的 COM 接口,记录器将挂钩该接口中的所有函数并记录它们。
以下是示例:
STDAPI CoCreateInstance(
REFCLSID rclsid, //Class identifier (CLSID) of the object
LPUNKNOWN pUnkOuter, //Pointer to controlling IUnknown
CLSCTX dwClsContext, //Context for running executable code
[iid] REFIID riid, //Reference to the identifier of the interface
[out] COM_INTERFACE_PTR * ppv
//Address of output variable that receives
//the interface pointer requested in riid
);
在此示例中, riid 具有 [iid] 修饰符。 这向 Logger 指示 ppv 中返回的指针是由 riid 标识的接口的 COM 接口指针。
还可以声明函数,如下所示:
DDRESULT DirectDrawCreateClipper( DWORD dwFlags, [out] LPDIRECTDRAWCLIPPER *lplpDDClipper, IUnknown *pUnkOuter );
在此示例中,LPDIRECTDRAWCLIPPER 定义为指向 IDirectDrawClipper 接口的指针。 由于 Logger 可以标识 lplpDDClipper 参数中返回的接口类型,因此不需要对任何其他参数使用 [iid] 修饰符。