Visual C++ 为我们提供了强大易用的调试环境,特别是对于简单数据类型和常见的类类型可以在调试的时候直接看到变量的值。然而这种方便是有局限性的,如果我们定义了一个复杂的数据类型,则在调试器中就很有可能看不到我们需要的信息。举个简单的例子,对于FILETIME这种类型来说:
typedef struct STRUCT tagFILETIME {
DWORD dwLowDateTime;
DWORD dwHighDateTime;
} FILETIME;
显然在调试器中我们无法直接看到常规格式的日期。再来一个更极端的例子:
如果我们在代码里面用 DWORD id 来表示一个文件的索引,每个id对应一个文件名,而且我们希望在调试的时候直接看到id对应的文件名该怎么办?显然,Visual C++ 调试器对此无能为力。但是!我没有说“完全”无能为力,微软在 Visual C++ 调试器中支持用户自定义表达式分析扩展!
这究竟是怎么实现的呢?原来,通过正确注册自定义数据类型分析求值函数之后,Visual C++ 调试器会在表达式求值的时候调用用户自定义的求值函数。
下面我就举一个简单的例子来说明如何编写 Visual C++ 的表达式分析插件。
1 新建一个标准dll工程
起名字叫做MyAddin
2 在MyAddin.def文件中写
EXPORTS
AddIn_MyID2STRING
3 在MyAddin.h文件中写
extern "C" ADDIN_API
HRESULT WINAPI
AddIn_ID2STRING(DWORD, DEBUGHELPER*, int, BOOL, char*, size_t, DWORD);
4 新建一个vsaddin.h文件,写入
#ifndef VSADDIN_H
#define VSADDIN_H
#include <windows.h>
typedef struct tagDEBUGHELPER
{
DWORD dwVersion;
BOOL (WINAPI *ReadDebuggeeMemory)(struct tagDEBUGHELPER *pThis,
DWORD dwAddr,
DWORD nWant,
VOID* pWhere,
DWORD *nGot);
// from here only when dwVersion >= 0x20000
DWORDLONG (WINAPI *GetRealAddress)(struct tagDEBUGHELPER *pThis);
BOOL (WINAPI *ReadDebuggeeMemoryEx)(struct tagDEBUGHELPER *pThis,
DWORDLONG qwAddr,
DWORD nWant,
VOID* pWhere,
DWORD *nGot);
int (WINAPI *GetProcessorType)(struct tagDEBUGHELPER *pThis);
} DEBUGHELPER;
typedef HRESULT (WINAPI *CUSTOMVIEWER)(DWORD dwAddress,
DEBUGHELPER *pHelper,
int nBase,
BOOL bUniStrings,
char *pResult,
size_t max,
DWORD reserved);
#endif /* VSADDIN_H */
5 在MyAddin.cpp中写
#include "vsaddin.h"
#include "MyAddin.h"
class ID2STRING
{
public:
DWORD ID;
};
char* ID_List[10] = {"Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"};
static bool
BridgeObject(DEBUGHELPER* pHelper, void* realAddress,
ID2STRING* stringObject)
{
if (false == GetMemory(pHelper, realAddress, sizeof(ID2STRING), stringObject))
{
OutputDebugString("GetMemory Error A");
return false;
}
return true;
}
HRESULT WINAPI
AddIn_MyID2STRING(DWORD dwAddress, DEBUGHELPER* pHelper,
int /*nBase*/, BOOL /*bUniStrings*/,
char* pResult, size_t max,
DWORD /*reserved*/)
{
*pResult = '';
__try
{
// We can't use a stack allocated ID2STRING here due to the use of SEH
char stringBuffer[sizeof(ID2STRING)];
ID2STRING* Id2String = (ID2STRING*)stringBuffer;
void* realAddress = GetRealAddress(pHelper, dwAddress);
if (false == BridgeObject(pHelper, realAddress, Id2String))
{
OutputDebugString("BridgeObject Error");
return E_FAIL;
}
snprintf(pResult, max, "%s", ID_List[Id2String->ID]);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
OutputDebugString("EXCEPTION_EXECUTE_HANDLER");
return E_FAIL;
}
return S_OK;
}
OK,代码部分就是这样了,编译即可
6 把编译好的 MyAddin.dll 放到Visual Studio 安装路径的 Common7IDE 和 Common7PackagesDebugger 下(这个MSDN中写的有错,我的经验是两个目录里都放一份,或者看下面的说明)
7 用文本编辑器打开 Common7PackagesDebugger 目录下的 autoexp.dat,找到
; see EEAddIn sample for how to use these
;_SYSTEMTIME=$ADDIN(EEAddIn.dll,AddIn_SystemTime)
;_FILETIME=$ADDIN(EEAddIn.dll,AddIn_FileTime)
在下面添加一行:
ID2STRING=$ADDIN(MyAddin.dll,AddIn_MyID2STRING)
当然,这里可以写绝对路径,那么就不用按照步骤6里面说得那样放置文件了。而且,在修改这个文件的介绍上,MSDN写错了,写的时候要小心。
好,完工!
我们新建一个工程,在里面使用
class ID2STRING
{
public:
DWORD ID;
};
调试的时候看到的ID就是
char* ID_List[10] = {"Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"};
的内容了
当然,我在代码里面没有做下标越界的处理,这仅仅是一个例子而已
注意事项:我在代码中使用了SEH,这是因为如果没有正确处理异常,插件将导致 Visual Studio IDE 崩溃。