65.9K
CodeProject 正在变化。 阅读更多。
Home

通过调试器进行调试

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (8投票s)

2009年3月20日

CPOL

11分钟阅读

viewsIcon

25033

使用 WinDbg 在 .NET 世界中生存。

引言

对于我们仍然使用 COM 和 ATL 的人来说,与以 .NET 为中心的 API 进行互操作可能会变得徒劳无功。例如,考虑 Visual Studio 插件。 Visual Studio Extensibility (VSX) API 公开了 COM 接口,但所有文档和示例都只针对 .NET 开发者编写。当 COM 或 ATL 程序员访问 MSDN 查找像 OnConnectionQueryStatusExec 这样的函数的返回码时,可能会惊讶地发现没有 COM 文档,也没有非托管 C++ 示例。这可能是一场 .NET 阴谋。

Redmond 最新的趋势是让一个团队博客介绍他们的技术,然后在论坛 (social.microsoft.com) 上进行问答。显然,usenet 不再好使了。对于 Visual Studio 插件,我们访问 VSX Team Blog 并阅读以前在 MSDN 上提供的信息。我们熟悉选定的博客条目(注意,搜索 VSX 博客似乎总是返回 0 个结果),并偶然发现了 Craig Skibo 的建议,即访问 VSX Forum。Craig 礼貌地告诉我们,他没有时间在他的博客上回答问题,所以论坛是提问的地方。

现在,我们找到了一个地方——一个可以向专家提问的地方。我们的问题很简单:当 Visual Studio 调用一个我们不准备处理的命令时,我们应该返回什么?是返回 E_INVALIDARG(并冒着被卸载的风险),S_FALSE,还是 S_OK(并默默地吞下这个错误)。因此,我们在 VSX 论坛上提出了我们的问题:Addin: QueryStatus 和 Exec 方法 (C++/ATL)。不幸的是,我们没有收到任何答案。看来我们被 VB 和 C# 同行排斥了。这明显是一场 .NET 阴谋。

但并非所有希望都渺茫……

背景信息

Microsoft MVP Carlos J. Quintero 提供了关于插件的一些最全面、最易于理解的信息。Quintero 先生提供了 大量与 Visual Studio 插件相关的资源。请访问他的 Visual Studio .NET 可扩展性资源页面。

有两个优秀的调试参考资料我一直在使用。用于开发,是 Visual Studio 调试器和 John Robbin 的 Debugging Microsoft .NET 2.0 Applications。对于繁重的工作,我使用 WinDbg 和 Hewardt 和 Pravat 的 Advanced Windows Debugging

WinDbg 是 Microsoft 提供的免费下载,随 Debugging Tools for Windows 一起提供。有几个理由可以推荐 WinDbg 而不是其他调试器。这些理由与它们的质量无关。OllyDebug 是一个很棒的调试器。然而,OllyDbg 不是由 Microsoft 的操作系统团队编写的——而 WinDbg 是。其次,当 Microsoft 更改 PDB 格式时(这可能发生在 Visual Studio 或操作系统的下一个版本中),WinDbg 将在第一天就提供相应的功能。维护其他调试器的人可能没有这些信息,因此可能会出现一段时间的不兼容。最后,关于 WinDbg 的问题会在 microsoft.public.windbg 中得到解答。该组由负责调试器开发的人员维护。

需要一些准备工作。对于那些仍然认为汇编语言知识很有用的人来说,一本好的命令参考手册是必不可少的。我使用的汇编语言参考是 Microsoft MASM 的Assembly Language Development System 和 Borland 的Turbo Assembler Reference Manual。这两本书都有很多用铅笔做的笔记,所以我需要参考 Intel 的Architecture Software Developer's Manual, Volume 2: Instruction Set Reference Manual

创建 C++/ATL Visual Studio 插件

在本文中,我将使用一个名为 CryptoPPAddinCrypto++ 插件(这是一个真实的插件)。Crypto++ 是 Wei Dai 编写和维护的一个免费、开源的加密库。如果您手头没有插件代码,可以按照以下步骤创建插件。图 1 显示了 Visual Studio 插件项目向导的位置。

Visual Studio Add-In Wizard

图 1:Visual Studio 插件向导

在图 2 中,我们在第四步取消勾选所有选项。这会导致向导生成一个临时插件的代码。有关临时插件和永久插件的讨论,请参阅 Quintero 的 HOWTO: 从插件向 Visual Studio .NET 添加按钮、命令栏和工具栏。对于向导的其余部分,默认值是可接受的。

Temporary Visual Studio Add-In

图 2:临时 Visual Studio 插件

编译插件

一切顺利的话,我们会干净地编译插件,然后按 F5 运行它。编译和链接的输出是一个 DLL;用于调试它的可执行文件是 Visual Studio 本身。因此,如果提示输入可执行文件,请选择 devenv.exe。图 3 显示了 Crypto++ 的插件。

Crypto++ Add-in

图 3:Crypto++ 插件

一旦插件从调试器中运行,插件就会被注册,然后我们可以使用 WinDbg 进行下一步。请注意,我们是从 \Debug 目录运行插件的。无需将 DLL 移动到特殊位置。事实上,移动文件可能会在 WinDbg 尝试查找模块符号时导致问题。

符号

Local Symbol Cache

正如 Hewardt 和 Pravat 在Advanced Windows Debugging 中所说,“没有符号的应用程序调试非常困难。” 如果您还没有这样做,请设置您的_NT_SYMBOL_PATH 环境变量。我的路径使用了一个本地文件夹进行缓存(D:\Symbols),以及 Microsoft 的符号服务器来检索操作系统和 Visual Studio 的 PDB 文件。

SRV*D:\Symbols*http://msdl.microsoft.com/download/symbols

请注意,如果您刚刚添加了环境变量,获取本次练习的所有文件将需要相当长的时间。在初始下载后,调试器将使用文件的本地副本,并且过程将近乎透明。

使用 WinDbg 调试插件

回想一下,要回答的问题是:在我们无法响应调用时,应该在 QueryStatus 等函数中返回什么? 为了回答这个问题,请打开 WinDbg。从File菜单中,选择Open Executable并导航到devenv.exe。无需为 WinDbg 指定其他参数。

Opening Visual Studio under WinDbg

图 4:调试调试器

WinDbg 加载 devenv 并输出初始调试信息后,目标将停止。然后我们输入 g 继续执行。在下面的图 5 中,我们看到插件 (CryptoPPAddin) 已加载到地址 0x1000000。我们还看到来自 ATLTRACE 语句的调试信息输出,例如 OnConnection

Visual Studio and Add-In Load

图 5:Visual Studio 和插件加载

接下来,我们需要设置一个断点来开始跟踪 QueryStatus。首先,我们使用 .cls 命令清除调试输出。然后,我们通过按 CTRL-Break 进入 WinDbg。在下面的图 6 中,我们看到我们已停止在 ntdllDbgBreakPoint 函数中。调试器显示了相关的寄存器值以及进程 ID/线程 ID 对 (954:9dc)。提示符 0:002> 表示断点由第一个处理器(该机器有双 Pentium III)处理,并且进程中的第二个线程 (002) 已停止。这告诉我们,当进程停止时,第二个线程是进程中当前执行的线程。

WinDbg Break

图 6:WinDbg 断点

接下来,我们需要找到感兴趣的函数,以便设置断点。我们真正感兴趣的是调用者如何处理我们的返回值,但我们还不知道调用函数。所以,我们想在 QueryStatus 处断点。有几种方法可以找到 QueryStatus

在设置断点之前,我们需要了解一些有关符号的信息。首先,WinDbg 命令解释器接受通配符。其次,x 用于检查符号。第三,! 用于将名称限制到一个模块(类似于 C++ 的 '::' 运算符)。x *!* 将返回所有模块中的所有符号(这可能是一个坏主意)。首先,我们输入命令x *!*QueryStaus*。输出(“电话簿”)显示在图 7 中。

Symbols matching QueryStatus

图 7:匹配 QueryStatus 的符号

在图 8 中,我们使用 x /v CryptoPPAddin!*QueryStatus* 优化搜索。/v 开关为我们提供了检查符号命令的详细输出。下面,我们看到有两个符号匹配:一个公共符号和一个私有符号。

CryptoPPAddin symbols matching QueryStatus

图 8:匹配 QueryStatus 的 CryptoPPAddin 符号

现在,我们设置函数上的断点。断点的命令是 bp,所以我们发出 bp CryptoPPAddIn!CConnect::QueryStatus。对于不太熟悉的人来说,命令窗口允许我们复制和粘贴。从命令窗口的上部区域获取完整名称,然后按 CTRL-C 复制到剪贴板,然后按 CTRL-V 执行粘贴。在完成复制粘贴操作后,我们使用 bl 列出进程中的断点。

Breakpoint on QueryStatus

图 9:QueryStatus 断点

然后我们输入 g 让 Visual Studio 运行。要触发断点,请导航到Tools菜单。无需点击插件 - Visual Studio 将查询其状态以确定如何在其Tools菜单上绘制命令的按钮。在图 10 中,我们发出 .cls 命令来清除断点后的屏幕。如果我们的注意力不集中,我们可以使用 .lastevent 来解释为什么我们被停止在调试器中。回想一下,我们想在 QueryStatus 处停止是为了确定 devenv.exe 中的调用者。现在,我们发出 k - 堆栈跟踪。

Stack Trace to determine Caller

图 10:确定调用者的堆栈跟踪

在图 10 中,我们看到 AddinPerformQueryStatus 是调用者。由于我们关心函数如何处理我们的返回值(HRESULT),AddinPerformQueryStatus 是我们的下一个目标。请注意,我们感兴趣的地址是我们函数上的 RetAddr,而不是 AddinPerformQueryStatusRetAddr

为了方便起见,我们首先用 bc 清除旧断点。然后发出 bp 5034e125。5034e125 是 AddinPerformQueryStatus+0x144 - WinDbg 会为我们进行计算。请注意,我们不想在 AddinPerformQueryStatus 处断点,因为它只是函数的开始。我们想在调用返回我们的调用之后断点,而我们的函数是在 AddinPerformQueryStatus+0x144 之前被调用的。

AddinPerformQueryStatus Breakpoint

图 11:AddinPerformQueryStatus 断点

接下来,我们继续执行目标,直到遇到断点。由于屏幕已被清除,我发出 .lastevent 来查看程序停止的原因;然后输入 r 显示寄存器。观察到我已到达断点,我想反汇编代码来看看 Visual Studio 在做什么。如果我们有行信息和源文件,WinDbg 将像 Visual Studio 一样逐步引导我们完成源代码。但是,由于我们只有公共符号,所以我们只能粗略地处理。要反汇编序列,我们使用 unassemble 命令:u

图 12:AddinPerformQueryStatus 断点

反汇编后,我们看到 Visual Studio 在 e125(地址 5034e125)处开始清理来自插件 DLL 调用的代码。我们的 HRESULT 在堆栈上,并且还没有被使用。这段代码对我们来说几乎没有兴趣,除了值 80040104 被保存在指令 e12f。Error Lookup 没有 80040104 的条目,winerror.h 也没有定义它,并且搜索网络提供的很少。更多关于 80040104 的信息稍后。

接下来,我们使用 p 命令开始单步执行代码。在 e0e7 (5034e0e7) 处,我们看到一个结构化异常处理程序被拆除;然后执行了一个返回。这是一个好迹象,表明我们正在接近感兴趣的区域——Visual Studio 用异常处理程序包装了对我们插件的调用。图 13 显示了我们的状态。

图 13:单步执行代码

再单步执行一次 (p) 我们会进入 CVSCommandTarget::QueryStatusCmd。我们越来越近了,但还没有到达目的地——我们正在观察一个 VxDTE::Commands 对象析构函数,后面跟着一个 SysFreeString。我们再单步执行一点,直到到达 50077e11。然后我们反汇编 (u),如图 14 所示。

QueryStatus HRESULT Validation

图 14:QueryStatus HRESULT 验证

当到达 77e11 时,我们就有了答案。0x80040100 是 DRAGDROP_E_NOTREGISTERED,最有可能表明控件存在问题(我认为关键在于“E_NOTREGISTERED”)。80040104 介于 DRAGDROP_E_FIRSTDRAGDROP_E_LAST 之间,但我认为它不一定是拖放错误代码。这是为私有使用保留的 FACILITY_IFT (4) 代码。

在图 14 中,代码行 77e17、77e23 和 77e31 都使用相同的跳转目标:500197ef。其中两条线执行了与错误代码的比较,而最后一条(77e31)执行了无条件跳转。这导致了以下 C/C++ 翻译

switch( EDI )
{
    // 80040100
    case E_NOTREGISTERED:
        goto BadReturn;
    // FACILITY_ITF(4):
    case 80040104:
        goto BadReturn;
    // S_OK
    case S_OK:
        goto GoodReturn;
    default:
        goto BadReturn;
}

如果我们遵循 S_OK 代码路径,我们会看到最终落入 CVSShellMenu::IsCommandVisible。此时,COM 底层的东西似乎已经完成。在图 15 中,我们观察到了 QueryStatusvsCommandStatus 参数返回值的测试。

Checking of QueryStatus, vsCommandStatus Parameter

图 15:检查 QueryStatus 的 vsCommandStatus 参数

结论

尽管文档不全,但 C++ 或 ATL 的纯粹主义者可以使用较新的 Microsoft API。然而,我们必须意识到返回 S_OKS_FALSE(这仍然被视为成功,因为 S_FALSE = 1)和其他 COM 错误的影响。如有疑问,就为团队牺牲一把,返回 S_OK

© . All rights reserved.