您的位置:首页 > 其它

让VS2010调试器帮你格式化显示自定义数据

2017-10-11 15:22 423 查看
参考: https://msdn.microsoft.com/en-us/library/8fwk67y3.aspx
// 头文件

typedef struct tagDEBUGHELPER

{
DWORD dwVersion;
HRESULT (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 );
HRESULT (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 );

// 源文件

static HRESULT FormatDateTime( SYSTEMTIME *pSystemTime, char *pResult, size_t max )

{

    GetDateFormat( GetThreadLocale(), DATE_SHORTDATE, pSystemTime, NULL, pResult, (int)max );

    size_t len = _tcslen( pResult );

    if ( (max - len) < 2 )

        return E_FAIL;      // if not enough room in buffer

    pResult[ len ] = ' ';

    len++;

    GetTimeFormat( GetThreadLocale(), TIME_NOSECONDS, pSystemTime, NULL, pResult + len, (int)(max - len) );

    return S_OK;

}

ADDIN_API HRESULT WINAPI AddIn_SystemTime( DWORD dwAddress, DEBUGHELPER *pHelper, int nBase, BOOL bUniStrings, char *pResult, size_t max, DWORD reserved )

{
//__asm int 3;

    SYSTEMTIME SysTime;

    DWORD nGot;

    // read system time from debuggee memory space

    if (pHelper->ReadDebuggeeMemoryEx(pHelper, pHelper->GetRealAddress(pHelper), sizeof(SysTime), &SysTime, &nGot) != S_OK)

        return E_FAIL;

    if (nGot != sizeof(SysTime))

        return E_FAIL;

    return FormatDateTime( &SysTime, pResult, max );

}

ADDIN_API HRESULT WINAPI AddIn_FileTime( DWORD dwAddress, DEBUGHELPER *pHelper, int nBase, BOOL bUniStrings, char *pResult, size_t max, DWORD reserved )

{
//__asm int 3;
//char str[MAX_PATH] = {0};
//sprintf_s(str, MAX_PATH, 
// "pHelper = %X, GetRealAddress = %X, ReadDebuggeeMemory = %X, ReadDebuggeeMemoryEx = %X, "
// "GetRealAddress(pHelper) = %X, dwAddress = %X, pResult = %X, max = %d", 
// pHelper, pHelper->GetRealAddress, pHelper->ReadDebuggeeMemory, pHelper->ReadDebuggeeMemoryEx, 
// pHelper->GetRealAddress(pHelper), dwAddress, pResult, max);
//MessageBox(NULL, str, "b", MB_OK);

    FILETIME FileTime;

    SYSTEMTIME SysTime;

    DWORD nGot;

    // read file time from debuggee memory space
if (pHelper->dwVersion < 0x20000)
{
// Visual C++ 6.0 version
//MessageBox(NULL, "ReadDebuggeeMemory", "c", MB_OK);
if (pHelper->ReadDebuggeeMemory(pHelper, dwAddress, sizeof(FileTime), &FileTime, &nGot) != S_OK)
{
return E_FAIL;
}
}
else
{
//MessageBox(NULL, "ReadDebuggeeMemoryEx", "c", MB_OK);
if (pHelper->ReadDebuggeeMemoryEx(pHelper,
pHelper->GetRealAddress(pHelper), sizeof(FileTime), &FileTime, &nGot) != S_OK)
{
return E_FAIL;
}
}

    if (nGot != sizeof(FileTime))

        return E_FAIL;

    // convert to SystemTime

    if (!FileTimeToSystemTime( &FileTime, &SysTime ))

        return E_FAIL;

    return FormatDateTime( &SysTime, pResult, max );

}

// 导出函数

#define ADDIN_API __declspec(dllexport)

extern "C" ADDIN_API HRESULT WINAPI AddIn_SystemTime( DWORD dwAddress, DEBUGHELPER *pHelper, int nBase, BOOL bUniStrings, char *pResult, size_t max, DWORD reserved );

extern "C" ADDIN_API HRESULT WINAPI AddIn_FileTime( DWORD dwAddress, DEBUGHELPER *pHelper, int nBase, BOOL bUniStrings, char *pResult, size_t max, DWORD reserved );

// 修改 <root>\Common7\Packages\Debugger\autoexp.dat

; see EEAddIn sample for how to use these

_SYSTEMTIME=$ADDIN(EEAddIn.dll,AddIn_SystemTime)

_FILETIME=$ADDIN(EEAddIn.dll,AddIn_FileTime)

将编译出来的dll放到devenv.exe所在的目录下, 即可看到效果

//===================================================================================================================

// 参考文章

这里讲解的是针对vs2010之前的版本的(即vs2005,vs2008。因为vs2010对于这方面有了一些改动),并以CEGUI
0.7.9版本(因为这个版本的CEGUI的String对象采用统一utf32编码,调试时很难查看字符串信息)中的CEGUI::String类型为例讲解,

首先介绍一点此版本的CEGUI::String类需要注意的地方。
有一个很重要的地方需要注意,0.7.9的版本中CEGUI::String对于const char*,以及对于const utf8*(即const unsigned char*)的构造函数有区别。
前者(const char*)会直接将传入的字符串,逐一地,原封不动的,放到utf32(即unsigned int)缓冲区中。也就是一个简单的容量扩充操作。这对于ASCII字符集中的字符时没有问题的,因为utf32编码的ASCII字符集,与原来的ASCII码的值在数值上是相等的。但是如果是非ASCII字符集的字符,采用这种方式得到的将是一个错误的utf32编码。
但是如果传入是const utf8*,那么构造函数将会将此传入的缓冲区,看待成utf8编码的字符缓冲区,并进行utf8转到到utf32的编码操作。

很多时候我们想通过CEGUI::String::c_str()函数,让CEGUI::String返回c风格字符串,但是我要告诉你,CEGUI::String::c_str()是个文不达意的函数,其真正功能是将保存的utf32字符串转换成utf8编码的字符串。这对于ascii字符集中的字符没有什么问题,但是对非ASCII字符集的字符,你调用CEGUI::String::c_str()将会返回乱码。
假如你有以下代码:
CEGUI::String strTest = "中国";
std::cout << strTest.c_str() << std::endl;


你将得不到“中国”这样的输出。
这是为什么呢?这正是前面第一点提到的,因为CEGUI::String::String( const char* )构造函数,对于非ASCII字符集字符串的构造根本就是错误的。这点在CEGUI::String::Assign(const utf8*)中的注释中CEGUI已经考虑到了。但是未做过多处理。

然后我们来看一下如果让vs调试器帮你格式化显示CEGUI::String类型。

用过CEGUI.0.7.9的开发人员都知道,CEGUI::String类中直接将字符串全部保存到utf32(即一个字符为4个字节)的缓冲区中!这将意味着vs调试器不能直接查看CEGUI::String里面的字符,因为这个缓冲区里面到处都有c风格字符串的结尾符(即字节的值为0)。所以你很难查看到一个CEGUI::String对象的字符含义。当然如果你的CEGUI::String里面只保存的是ascii字符,那么有个简陋的方法是可以看到字符串。那就是使用VS的Memory查看器,我们将字符串头地址传给Memory查看器,Memory查看器会自动将能显示的ascii字符显示出来。这样能勉强能满足你的愿望。

但是,如果你的CEGUI::String对象,保存的是中文,那么没有任何简单的方法能让你再次看到其字符含义。要想让其格式化显示中文,我们必须给vs调试器写一个小插件(听着插件,似乎很麻烦,但实际上很简单,主要就牵扯到几个函数)。以下是具体的步骤:
vs调试器给了我们一个接口,可以为每个类型提供一个格式化其显示信息的机会。这个接口就是:
HRESULT WINAPI CustomViewer(
DWORD dwAddress,       // low 32-bits of address
DEBUGHELPER *pHelper,  // callback pointer to access helper functions
int nBase,             // decimal or hex
BOOL bIgnore,          // not used
char *pResult,         // where the result needs to go
size_t max,            // how large the above buffer is
DWORD dwReserved       // always pass zero
)
只要函数类型符合就可以,函数名字随便。只要我们完成这个函数,然后调试器每次显示你的数据类型的对象的时候,就会调用这个接口,你所需要做的就是将想要显示的信息填充到pResult所指向的字符缓冲区中。这是我们的中心思想,但是为了完成这个任务,我们有不少困难需要克服。后面会一一列举。

其中DEBUGHELPER定义如下:
typedef struct tagDEBUGHELPER
{
DWORD dwVersion;

HRESULT (WINAPI *ReadDebuggeeMemory)(
struct tagDEBUGHELPER *pThis, //DEBUGHELPER pointer
DWORD dwAddr,//the address of object you want to show formatted prompt information
DWORD nWant, //the object size in byte.
VOID* pWhere, //the dest buffer for storing the object
DWORD *nGot );//number bytes are transferred.

// from here only when dwVersion >= 0x20000

DWORDLONG (WINAPI *GetRealAddress)( struct tagDEBUGHELPER *pThis );

//use for 64-bit system.
HRESULT (WINAPI *ReadDebuggeeMemoryEx)( struct tagDEBUGHELPER *pThis, DWORDLONG qwAddr,
DWORD nWant, VOID* pWhere, DWORD *nGot );

int (WINAPI *GetProcessorType)( struct tagDEBUGHELPER *pThis );

} DEBUGHELPER;


重要的函数我已经提供的注释。需要注意的是,这个类型并不存在于window.h中,我们需要手动添加其声明。我们将只用到ReadBuggeeMemory()函数。

现在我们开始真正去完成插件,首先要做的是创建一个dll工程,这个dll工程将会是我们的插件。
【打开vs】-》【创建工程】-》【选择win32程序】-》【创建时选择空的dll工程】
创建一个main.cpp。然后把下面代码粘贴上!
// CEGUIDbg.cpp : Defines the exported functions for the DLL application.
//#include "stdafx.h"
#include <Windows.h>
#include "tchar.h"
#include <string>
#include <sstream>
#include <vector>

#include "ceguistring.h"

#define ADDIN_API __declspec(dllexport)

typedef struct tagDEBUGHELPER { DWORD dwVersion; HRESULT (WINAPI *ReadDebuggeeMemory)( struct tagDEBUGHELPER *pThis, //DEBUGHELPER pointer DWORD dwAddr,//the address of object you want to show formatted prompt information DWORD nWant, //the object size in byte. VOID* pWhere, //the dest buffer for storing the object DWORD *nGot );//number bytes are transferred. // from here only when dwVersion >= 0x20000 DWORDLONG (WINAPI *GetRealAddress)( struct tagDEBUGHELPER *pThis ); //use for 64-bit system. HRESULT (WINAPI *ReadDebuggeeMemoryEx)( struct tagDEBUGHELPER *pThis, DWORDLONG qwAddr, DWORD nWant, VOID* pWhere, DWORD *nGot ); int (WINAPI *GetProcessorType)( struct tagDEBUGHELPER *pThis ); } DEBUGHELPER;

// 多字节编码转为UTF8编码
bool MultiByteToUtf8( char* pszDestUtf8, int iDestUtf8Size, const char* pszMultiByte, int iMultiByteSize = -1 )
{
if( NULL == pszDestUtf8 || NULL == pszMultiByte )
{
return false;
}

// convert an MBCS string to widechar
int iWideCharSize = MultiByteToWideChar( CP_ACP, 0, pszMultiByte, iMultiByteSize, NULL, 0 );
std::vector< WCHAR > vctWideChar( iWideCharSize );

int iNumWritten = MultiByteToWideChar( CP_ACP, 0, pszMultiByte, iMultiByteSize, &vctWideChar.front(), iWideCharSize );
if( iNumWritten != iWideCharSize )
{
return false;
}

// convert an widechar string to utf8
int iUtf8Size = WideCharToMultiByte(CP_UTF8, 0, &vctWideChar.front(), -1, NULL, 0, NULL, NULL);
if ( iUtf8Size <= 0)
{
return false;
}

if( iUtf8Size > iDestUtf8Size )
{
iUtf8Size = iDestUtf8Size;
}

iNumWritten = WideCharToMultiByte( CP_UTF8, 0, &vctWideChar.front(), -1, pszDestUtf8, iUtf8Size, NULL, NULL );
if ( iNumWritten != iUtf8Size )
{
return false;
}

return true;
}

ADDIN_API HRESULT WINAPI CEGUIDbg_String(DWORD dwAddress, DEBUGHELPER *pHelper,
int nBase, BOOL bUniStrings, char *pResult,
size_t max, DWORD reserved )
{
CEGUI::String strDebug;
DWORD nGot;

//get CEGUI::String data member.
if (pHelper->ReadDebuggeeMemory(pHelper,dwAddress,sizeof( strDebug),&strDebug,&nGot) != S_OK)
{
return E_FAIL;
}
if( nGot != sizeof( strDebug ) )
{
return E_FAIL;
}

const CEGUI::utf32* pszUtf32 = strDebug.ptr();
int iLength = strDebug.length();

std::vector< CEGUI::utf32 > vctBuffer;
//if the string data is stored in a memory allocated by new(), we have to copy the data to out memory block.
if( iLength > STR_QUICKBUFF_SIZE )
{
vctBuffer.resize( iLength );
if( S_OK != pHelper->ReadDebuggeeMemory( pHelper, ( DWORD )pszUtf32, iLength * sizeof( CEGUI::utf32 ), &vctBuffer.front(), &nGot ) )
{
return E_FAIL;
}
if( nGot != vctBuffer.size() * sizeof( CEGUI::utf32 ) )
{
return E_FAIL;
}

pszUtf32 = &vctBuffer.front();
}

//get ascii character.
//although the data pointer is utf32*, but the data isn't encoded by utf32 if you pass const char* to CEGUI::String constructor. In contrary, it only store each ascii character
//in a utf32-type element.
int iSize = iLength + 1;

if( iSize > max )
{
iSize = max;
iLength = iSize - 1;
}

std::vector< char > vctAscii( iSize );

for( int i = 0; i < iLength; ++i )
{
vctAscii[ i ] = ( char )( unsigned char )pszUtf32[ i ];
}
vctAscii[ iLength ] = 0;

//convert ascii character set to utf8 character set. //Because debugger accepts utf8 character set. //If you pass ascii string to pResult, chinese character can't be shown. if( false == MultiByteToUtf8( pResult, max, &vctAscii.front() ) ) { return E_FAIL; }

//set all data to 0, then CEGUI::String::~String won't delete anything should't be deleted. memset( &strDebug, 0, sizeof( strDebug ) );

return S_OK;
}


可以看到我们需要包含“CEGUIString.h"这样的头文件,我们的做法是直接拷贝CEGUIString.h,CEGUIString.cpp到工程来,因为我们需要CEGUI::String这个类的声明和实现(因为我们需要对这种类型进行一些解析操作)。

但是CEGUI::String.h包含了CEGUIBase.h。所以我们需要添加CEGUI头文件的搜索目录。做法是【项目属性】-》【C/C++】-》【General】-》【Addtional include direstories】,向其中添加CEGUI SDK中的CEGUI/Include文件路径。

同时为了能够静态编译CEGUIString.h,CEGUIString.cpp,我们在【C/C++】-》【Preprocessor】中添加CEGUI_STATIC宏,这表明使用静态库形式编译CEGUI。

这样我们就完成了插件的编写(具体插件里面怎么个原理一会再讲)。然后我们将编译出来的dll放到devenv.exe所在的目录下,我这是【D:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE】,你的很有可能是在c盘。这是vs调试器搜索插件的目录(我猜的)。

然后我们需要改一个文件,叫做【autoexp.dat】,它在【D:\Program Files\Microsoft Visual Studio 9.0\Common7\Packages\Debugger】,这个是调试器启动是读取的关于自动展开类型的文件。我们打开文件在其【[AutoExpand]】的字段后添加如下语句:
[AutoExpand]
CEGUI::String=$ADDIN(ceguidbg.dll,?CEGUIDbg_String@@YGJKPAUtagDEBUGHELPER@@HHPADIK@Z)


CEGUI::String是需要格式化显示信息的数据类型,其中$ADDIN()是:
; $ADDIN allows external DLLs to be added to display even more complex
; types via the EE Add-in API. The first argument is the DLL name, the
; second argument is the name of the export from the DLL to use. For
; further information on this API see the sample called EEAddIn.


ceguidbg.dll是插件名称,?CEGUIDbg_String@@YGJKPAUtagDEBUGHELPER@@HHPADIK@Z是dll中导出函数经过名称修饰的函数名称。可以通过dumpbin /exports ceguibdg.dll查看导出函数名。通过添加这一行,调试器才知道碰到CEGUI::String这种类型的对象,去调用ceguidbg.dll中的对应函数,然后将此函数返回的pResult显示出来。

好了,我们插件做完了,我们重新调试便能看到结果。只需要重新调试即能看到新的格式化后的提示信息,如需重启vs。



这是在CEGUI 0.7.9的版本中,实现的效果。

main.cpp中的原理讲解:
首先我们拷贝出了需要显示提示信息的对象的数据。
CEGUI::String strDebug;
DWORD nGot;

//get CEGUI::String data member.
if (pHelper->ReadDebuggeeMemory(pHelper,dwAddress,sizeof( strDebug),&strDebug,&nGot) != S_OK)
{
return E_FAIL;
}
if( nGot != sizeof( strDebug ) )
{
return E_FAIL;
}

然后我们判断,CEGUI::String对象是否动态分配了一块字符缓冲区,因为我们的dll(插件)是在调试器进程中的,所以我们不能访问其他程序动态申请的内存,因为每个进程都有自己的虚拟内存地址空间。你访问的成员变量所指向的内存在你的进程中根本就没有分配。所以我们需要自己创建一块内存,通过ReadDebuggeeMemory函数读取。即用ReadDebuggeeMemory读取时可以的,这是系统保证的。由于我不太喜欢处理动态内存申请这类的问题,我使用了vector来帮助了我(确实有点难看)。
const CEGUI::utf32* pszUtf32 = strDebug.ptr();
int iLength = strDebug.length();

std::vector< CEGUI::utf32 > vctBuffer;
//if the string data is stored in a memory allocated by new(), we have to copy the data to out memory block.
if( iLength > STR_QUICKBUFF_SIZE )
{
vctBuffer.resize( iLength );
if( S_OK != pHelper->ReadDebuggeeMemory( pHelper, ( DWORD )pszUtf32, iLength * sizeof( CEGUI::utf32 ), &vctBuffer.front(), &nGot ) )
{
return E_FAIL;
}
if( nGot != vctBuffer.size() * sizeof( CEGUI::utf32 ) )
{
return E_FAIL;
}

pszUtf32 = &vctBuffer.front();
}

再然后,我们将假utf32编码格式,转换成多字节编码。
//get ascii character.
//although the data pointer is utf32*, but the data isn't encoded by utf32 if you pass const char* to CEGUI::String constructor. In contrary, it only store each ascii character
//in a utf32-type element.
int iSize = iLength + 1;

if( iSize > max )
{
iSize = max;
iLength = iSize - 1;
}

std::vector< char > vctAscii( iSize );

for( int i = 0; i < iLength; ++i )
{
vctAscii[ i ] = ( char )( unsigned char )pszUtf32[ i ];
}
vctAscii[ iLength ] = 0;


可以看到,我直接将32位的utf32编码给了char变量。所以我基于这样的前提,程序中我们都使用CEGUI::String::String( const char* )构造函数构造,而不使用CEGUI::String::String( const utf8* ),因为要使用后者,我们还需要将我们字符串转换成utf8编码格式,才能让CEGUI::String正常工作。所以一般人都会使用前者,也是最常见的构造方法。

然后最重要的,也是我耗费一下午时间才找到的解决方案。网上的例子都是老外,老外都用英文,ASCII字符就够了,所以直接将多字节编码的字符串给pResult。结果我发现,多字节编码的汉子是无法显示的,调试器根本不识别,而且从网上找各种例子,搜集资料也没找到解决方法。偶然情况下,我想是不是调试器识别Unicode编码啊,于是将多字节编码转换成utf8编码,果真成功了!真是皇天不负有心人啊,耗了我好多精力啊!
//convert ascii character set to utf8 character set.
//Because debugger accepts utf8 character set.
//If you pass ascii string to pResult, chinese character can't be shown.
if( false == MultiByteToUtf8( pResult, max, &vctAscii.front() ) )
{
return E_FAIL;
}


具体如何转换成,直接看MultiByteToUtf8的函数实现,里面不懂的函数直接看msdn就可以了。

最后一个非常重要的地方:
//set all data to 0, then CEGUI::String::~String won't delete anything should't be deleted.
memset( &strDebug, 0, sizeof( strDebug ) );


既然你声明了一个该类型的对象,并填充了其中的数据成员,那么这个对象析构的时候必然会走析构函数,而析构函数一定会将申请的内存释放。但此时的对象内的指针都是非空且无效的,那么析构的时候一定会出问题。而且CEGUI::String类型不提供清空方法,我们只能来硬的了。幸亏对象有没有虚函数表以及多重继承的问题,否则很难搞。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐