如何为 Visual Studio 2012 调试器编写自定义本机可视化工具 DLL?

Posted

技术标签:

【中文标题】如何为 Visual Studio 2012 调试器编写自定义本机可视化工具 DLL?【英文标题】:How to write a custom native visualizer DLL for Visual Studio 2012 debugger? 【发布时间】:2012-07-17 17:27:20 【问题描述】:

在 C++ 中为 Visual Studio 2012 调试器编写自定义本机可视化 DLL 需要什么?我想显示一个只能按需从类/结构计算的值,因此需要本机可视化器 DLL。 Visual Studio 2012 使用一种新方法来实现称为 Natvis 的本机可视化工具。截至目前,关于 Natvis 的正确信息非常少,尤其是关于使用 Natvis 调用可视化程序 DLL 的信息。 DLL 将根据类/结构成员值计算显示字符串。

【问题讨论】:

code.msdn.microsoft.com/windowsdesktop/… 那是一篇很棒的 natvis 帖子,我在下面也提到过,但它没有一个关于编写自定义原生可视化 dll 的字眼。我会大方地说,微软现在涵盖整个主题还为时过早。 【参考方案1】:

这是构成插件 DLL 的 C++ 代码。我将文件命名为 NatvisAddIn.cpp,项目创建了 NatvisAddIn.dll。

#include "stdafx.h"
#include <iostream>
#include <windows.h>

#define ADDIN_API __declspec(dllexport)

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 );

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

class MyClass

public:
    int publicInt;
;

struct MyStruct  int i; ;

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

    MyClass c;
    DWORD nGot;
    pHelper->ReadDebuggeeMemory(pHelper,dwAddress,sizeof(MyClass),&c,&nGot);
    sprintf_s(pResult,max,"Dll MyClass: max=%d nGot=%d MyClass=%x publicInt=%d",max,nGot,dwAddress,c.publicInt);
    return S_OK;


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

    MyStruct s;
    DWORD nGot;
    pHelper->ReadDebuggeeMemory(pHelper,dwAddress,sizeof(MyStruct),&s,&nGot);
    sprintf_s(pResult,max,"Dll MyStruct: max=%d nGot=%d MyStruct=%x i=%d",max,nGot,dwAddress,s.i);
    return S_OK;

这是 Visual Studio 2012 调试器用来显示值的 .natvis 文件。将其放在 .natvis 文件中。我将其命名为 NatvisAddIn.natvis。该文件指示 VS 2012 调试器调用 NatvisAddIn.dll。 dll 包含两个可视化方法调用; MyClassFormatter 格式化 MyClass 和 MyStructFormatter 格式化 MyStruct。对于指定类型(MyClass、MyStruct)的每个实例,调试器将在 Auto、Watch 或工具提示显示中显示方法的格式化值。

<?xml version="1.0" encoding="utf-8"?>
    <AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
    <Type Name="MyClass">
        <DisplayString LegacyAddin="NatvisAddIn.dll" Export="MyClassFormatter"></DisplayString>
    </Type>
    <Type Name="MyStruct">
        <DisplayString LegacyAddin="NatvisAddIn.dll" Export="MyStructFormatter"></DisplayString>
    </Type>
</AutoVisualizer>

将已编译的 NatvisAddIn.dll 文件和 NatvisAddIn.natvis 文件放入以下三个位置之一:

%VSINSTALLDIR%\Common7\Packages\Debugger\Visualizers (requires admin access)

%USERPROFILE%\My Documents\Visual Studio 2012\Visualizers\

VS extension folders

您需要确保以下注册表项存在且值为 1:

[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\11.0_Config\Debugger]

"EnableNatvisDiagnostics"=dword:00000001

如果一切顺利,您将看到 natvis 消息出现在 Visual Studio 的调试器输出窗口中。这些消息将显示 Natvis 是否能够解析 .natvis 文件。解析每个 .natvis 文件的结果显示在输出窗口中。如果出现问题,请使用命令“dumpbin/exports”仔细检查 DLL 方法的名称是否与 .navis 文件的 Type= 完全匹配。还要确保当前的 .dll 和 .natvis 文件已复制到相应的目录。

Natvis: Parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\atlmfc.natvis.
Natvis: Done parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\atlmfc.natvis.
Natvis: Parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\concurrency.natvis.
Natvis: Done parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\concurrency.natvis.
Natvis: Parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\NatvisAddIn.natvis.
Natvis: Done parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\NatvisAddIn.natvis.
Natvis: Parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\stl.natvis.
Natvis: Done parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\stl.natvis.
Natvis: Parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\windows.natvis.
Natvis: Done parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\windows.natvis.
Natvis: Parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\winrt.natvis.
Natvis: Done parsing natvis xml file: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\Visualizers\winrt.natvis.

测试程序:

#include "stdafx.h"
#include <iostream>

class MyClass

public:
    int publicInt;
;

struct MyStruct  int i; ;

int _tmain(int argc, _TCHAR* argv[])

    struct MyStruct s = 1234;
    std::cout << s.i << std::endl;
    MyClass *c = new MyClass;
    c->publicInt = 1234;
    std::cout << c->publicInt << std::endl;
    return 0;

信息资源:

\Xml\Schemas\natvis.xsd

http://code.msdn.microsoft.com/windowsdesktop/Writing-type-visualizers-2eae77a2

http://blogs.msdn.com/b/mgoldin/archive/2012/06/06/visual-studio-2012-and-debugger-natvis-files-what-can-i-do-with-them.aspx

http://blogs.msdn.com/b/vcblog/archive/2012/07/12/10329460.aspx

【讨论】:

感谢您的提问。有几个downvotes 可能是因为我没有填写所有的存根。我是实时添加的。我了解到,如果可能的话,我应该在最初的帖子上完成帖子。该帖子现已完成,因此应该有很多赞成票。 首先感谢您发布它,但我认为这不适用于 64 位应用程序。 (例如,DWORD 用于地址和其他内容)。另外,如何制作一个以某种方式在另一端处理 32 位/64 位代码的插件? 好问题。对不起,我不知道。我想知道是否还有更复杂的情况,因为 Visual Studio 调试器是一个 32 位进程。有人吗? @malkia 可以使用GetRealAddressReadDebuggeeMemoryEx的组合来处理64位地址。 当我在另一个 DLL 中移动类和结构时,我无法让它工作【参考方案2】:

对于 64 位版本调试,应使用以下行:

auto realAddress = pHelper->GetRealAddress(pHelper);
pHelper->ReadDebuggeeMemoryEx(pHelper, realAddress, sizeof(MyClass), &c, &nGot );

对于前面的示例,64 位版本可能如下所示:

#include "stdafx.h"
#include <iostream>
#include <windows.h>

#define ADDIN_API __declspec(dllexport)

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 );

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

class MyClass

public:
    int publicInt;
;

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

    MyClass c;
    DWORD nGot;
    auto realAddress = pHelper->GetRealAddress(pHelper);
    pHelper->ReadDebuggeeMemoryEx(pHelper, realAddress, sizeof(MyClass), &c, &nGot );
    sprintf_s(pResult,max,"Dll MyClass: max=%d nGot=%d MyClass=%llx publicInt=%d",max, nGot, realAddress, c.publicInt);
    return S_OK;

【讨论】:

【参考方案3】:

我需要一项与为加载上述“NatvisAddIn.dll”而查找的搜索路径相关的说明。让我尝试通过扩展上面的例子来解释

为了可视化我的自定义 C++ 类对象(比如说 MyCustomeType),我需要在“Export”属性中提到的函数(例如 MyClassFormatter)的实现中调用一些额外的 API(计算 MyCustomeType 的显示字符串) “显示字符串”XML。

调用这些额外的 API 会在上述“NatvisAddIn.dll”上创建一个库/dll 依赖项。如果这个附加依赖项只是一个或两个库,我可以将这些库放在 NatvisAddIn.dll 所在的同一位置。但是,在我的情况下,依赖项包含一长串库。

有人可以建议我一些优雅的方法来解决依赖关系,而无需将完整的库链拉入 %USERPROFILE%\My Documents\Visual Studio 2012\Visualizers 文件夹吗?

为了演示我的用例,让我们考虑下面的依赖树: NatvisAddIn.dll a.dll a1.dll a2.dll a3.dll

我将 a.dll 复制到 NatvisAddIn.dll 所在的同一位置。它的依赖 dll(a1、a2 和 a3)存在于添加到 PATH 变量的位置。当我尝试在 Visual Studio 调试器中可视化 MyCustomeType 对象时,natvis 诊断在输出窗口中给出以下错误

Natvis:C:\Users\myUser\Documents\Visual Studio 2017\Visualizers\mydata.natvis(9,6):错误:无法从 C:\Users\myuser\Documents\Visual Studio 加载插件2017\Visualizers\NatvisAddIn.dll for type MyCustomeType: : 找不到指定的模块。

我对上述错误的理解是,Visual Studio 调试器无法解析 a.dll(a1、a2 和 a3)的依赖关系,因此无法加载 NatvisAddIn.dll

当我尝试在我的 testApplication 中使用 a.dll 并为 MyCustomeType 计算 DisplayString 时,依赖项得到解析,a.dll 被加载,我得到了预期的输出字符串,而无需复制 a1.dll、a2.dll 和 a3.dll .依赖的 dll 是从窗口 PATH 变量中解析/提取的。 但是,对于 Visual Studio 调试器,依赖的 DLL 不会从 PATH 变量中解析

通过depends工具识别,一些没有被调试器解析的dll是:

api-ms-win-core-errorhandling-l1-1-0.dll api-ms-win-crt-time-l1-1-0.dll api-ms-win-crt-heap-l1-1-0.dll

其中一些 dll 存在于 Visual Studio 安装中,其他的存在于 c:\windows\WinSxS

我已经在 Visual Studio 2012 和 Visual Studio 2017 上尝试了我的用例。我在这两个 Visual Studio 中都遇到了同样的问题。

【讨论】:

以上是关于如何为 Visual Studio 2012 调试器编写自定义本机可视化工具 DLL?的主要内容,如果未能解决你的问题,请参考以下文章

Visual Studio 2012 分析远程调试过程

visual studio 2012如何调试和运行程序?

如何为office安装visual studio工具?

如何为 Visual Studio Express 2013 桌面项目创建安装程序

Visual Studio 扩展 - 如何为代码着色?

Visual Studio 2012 与 Visual Studio 2010 (delta) [关闭]