wenmo8 发布的文章

理想情况下,无论是否附加了调试器,应用程序都会执行相同的操作。这源于非常实际的动机:

  1. bug通常首先出现在调试器之外(一些测试失败),然后您只想在调试器下重新运行测试来重新生成问题。如果调试器更改了行为,这将妨碍您重新编程的能力。
  2. 另一方面,开发人员可以在IDE下开发代码,并通过在调试器下检查代码来确保代码正常工作。如果调试器引入了隐藏缺陷的行为更改,那么开发人员将在这里忽略它。
  3. 如果应用程序的行为只发生在调试器下,人们会将其行为归咎于调试器。例如,如果一个应用程序只在调试器下抛出异常,那么人们会认为调试器导致了异常,或者调试器已损坏并错误地报告了异常。(我们发现了很多这样的缺陷)。这种错误的指责会阻碍真正的问题得到解决。
  4. 行为差异会削弱人们对调试器的信心。
  5. 调试时,调试器是否附加到正在运行的进程,或者调试对象是否从调试器下启动都不重要。行为上的改变会在这两种情况之间造成一个楔子。这对于只能附加的调试器场景特别重要(例如,调试ASP.Net或SQL)

那么,为什么应用程序在托管调试器下的执行方式会有所不同呢?

  1. 用户检查:最大的罪魁祸首:程序可以显式调用System.Diagnostics.Debugger.IsAttached以询问是否附加了托管调试器,然后行为不同。(win32 API IsDebuggerPresent()同样检查是否附加了本机调试器。)。这是最容易引起痛苦的方法。例如,如果附加了托管调试器,WinForms将显式使用不同的“可调试”WndProc。这个可调试的wndproc在用户回调(不可调试的wndproc没有)周围有一个额外的try-catch,用于通知用户回调是否抛出异常。另一个最受欢迎的方法似乎是在附加了调试器以通知用户时抛出异常。
  2. CLR/JIT检查:ICorDebug允许调试器更改实时编译器的codegen选项(例如禁用优化)。这会导致从IL生成不同的本机代码,这肯定会改变行为。如果附加了调试器,v1.0和v1.1clrs中的jit实际上会自动生成可调试代码。这是个非常糟糕的主意。在v2.0中,JIT显式地不知道是否附加了调试器,尽管调试器可以显式地请求JIT生成可调试的代码。
  3. 操作系统检查:在极少数情况下,操作系统可能会处理不同的事情。例如,在Windows上,如果附加了本机(或互操作)调试器,则不会针对未处理的异常执行筛选器。(自己试试看)
  4. 利用非确定性:应用程序执行的某些部分是不确定的,例如定时问题和对机器范围内状态的任何引用(例如页面共享)。例如,在多线程应用程序中,调试器肯定会影响计时,这可能会暴露(或隐藏)现有的竞争条件。我个人觉得这比我想的要普遍得多。正因为这个原因,我不得不调试太多只会在调试器之外的优化版本上重新生成的bug。
  5. 调试器检查:以上所有原因都假定调试器行为良好。然而,作为API的产品,我们不能阻止API的使用者做改变debuggee行为的“愚蠢的”事情。例如:
    .在托管进程下启动:应用程序通常在自己的进程中启动。有人可以编写一个调试器,在宿主进程内部启动调试对象。这是调试和非调试的区别。
    .Func eval:属性求值(Func eval)的每个实例都是只在调试器下运行的代码。如果函数求值有任何副作用,则可以更改调试对象的行为。如果调试器在不提示用户的情况下执行函数求值,这一点尤其重要。

我觉得CLR调试器团队对这个问题太天真了。幸运的是,后面版本中采取了很多伟大的措施来解决这个问题:

  1. 已经认识到这一点很重要。VS对手帮助我们明确了这一点。
  2. 已经明确地消除了任何依赖于是否附加调试器的非调试器CLR行为。如前所述,JIT将不再自动生成可调试代码。我们现在还将始终跟踪jit的调试信息(jit生成的IL-->Native映射)。
  3. 已经尝试识别用户呼叫的常见模式调试器.IsAttached然后让ICorDebug提供其他解决方案,这些解决方案不需要调试人员有不同的行为。例如,Winforms具有可调试回调,因为它们希望在异常跨越回调和处理程序之间的边界时通知用户。如果ICorDebug只是(a)意识到这个边界是重要的,并且(b)提供了一个异常将在其上展开的通知,那么Winforms就永远不需要添加try-catch。

有时我们会遇到这样一个场景:一个进程正在使用大量内存,而此时我们能够获得的唯一数据就是用户转储。通常,来自umdh或xperf等工具的数据会更好,因为它们提供一段时间内的内存使用数据,并且可以包括调用堆栈信息。但是,umdh需要重新启动进程(这会丢失高内存使用率的状态),xperf需要安装Windows Performance Toolkit,这可能并不总是一个立即的选项。
当我们有这样一个转储时,我们可能无法明确指出是哪段代码产生了高内存使用率,但我们可以将故障排除的范围缩小到特定的dll。我们需要做的第一件事是确定什么类型的内存使用了大部分地址空间。调试器命令!address -summary允许我们执行以下操作:

0:000> !address -summary

 

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal

Free                                    489      7fe`6ff5a000 (   7.994 Tb)           99.92%

Heap                                   9094        1`75ed1000 (   5.843 Gb)  93.47%    0.07%

<unknown>                               275        0`12e41000 ( 302.254 Mb)  4.72%    0.00%

Image                                   937        0`05a6a000 (  90.414 Mb)  1.41%    0.00%

Stack                                   138        0`01700000 (  23.000 Mb)  0.36%    0.00%

Other                                    14        0`001bd000 (   1.738 Mb)  0.03%    0.00%

TEB                                      46        0`0005c000 ( 368.000 kb)   0.01%   0.00%

PEB                                       1        0`00001000 (   4.000 kb)  0.00%    0.00%

从这个例子中我们可以看到大部分内存被堆使用。一个进程通常有多个堆,每个堆都是通过调用HeapCreate创建的。我们可以检查这些堆的大小!heap -s:

0:000> !heap -s

LFH Key                   : 0x0000006c1104d280

Termination on corruption : ENABLED

          Heap     Flags  Reserv  Commit  Virt  Free  List   UCR Virt  Lock  Fast

                            (k)     (k)   (k)     (k) length      blocks cont. heap

-------------------------------------------------------------------------------------

0000000000100000 00000002  16384  12824  16384  1180   254     5   0      3   LFH

0000000000010000 00008000     64      4     64     1     1     1   0      0     

00000000003d0000 00001002   1088    708   1088   121    20     2   0      0   LFH

0000000003080000 00001002   1536    700   1536     4     4     2   0      0   LFH

00000000033a0000 00001002 5229696 1377584 5229696 414244  4039  3059   0     2c   LFH

    External fragmentation  30 % (4039 free blocks)

    Virtual address fragmentation  73 % (3059 uncommited ranges)

0000000003380000 00001002     64      8     64     3     1     1   0      0     

0000000003600000 00001002    512     56    512     3     1     1   0      0     

0000000003c20000 00001002    512      8    512     3     1     1   0      0     

0000000003220000 00001002    512      8    512     3     1     1   0      0     

0000000003e50000 00001002    512      8    512     3     1     1   0      0     

0000000003d00000 00001002    512    148    512     5     3     1   0      0   LFH

从上面的输出中我们可以看到大部分内存被堆00000000033a0000占用。在这一点上,我们需要尝试确定这个堆的用途。执行此操作的一种强力方法是使用“s”命令搜索内存。

0:000> s -q 0 l?7fffffffffffffff 00000000033a0000

....

000007fe`f21810a0  00000000`033a0000 00000000`00000001

's'命令的输出可能很详细。您将需要手动检查“s”找到命中的地址。大多数地址可能在堆内存中,我们正在寻找与模块匹配的地址。我截取了上面的输出以显示相关的命中,一个加载模块内部的地址。

什么是std::__non_rtti_object异常?

不是一个RTTI对象异常

继承关系:

__non_rtti_object : public bad_typeid

备注

当指针指向的是一个无效的对象,引发此异常。例如,它是一个错误的指针,或者代码不是用/GR编译的

 

有一天,我在调试一个问题,在一个进程上弹出一个Waston对话框。让我吃惊的是,在Waston触发的堆栈上,有一个带有Testcatch(…)块的非托管C++函数。据我理解,此块应该捕获Windows中抛出的任何用户模式异常,包括来自RaiseExceptionr调用(例如,C++异常)、AV、堆栈溢出等的异常。为什么异常会逃出这样的块而变得未处理?我发现异常是调试中断。在X86中,它由操作码0xCC或“int3”触发。当我调试到VCRT的EH代码时,我发现catch(…)故意让调试中断。它确实有意义:debug break是为了停止调试器,因此源代码不应该处理它。我只是从来没有意识到。

您看到的是一个非托管异常,它在CLR中引发。它是非常通用的,用于在深入本机代码时发出错误条件的信号。名称的“消息”部分是为Microsoft测试人员设计的。只需等待这个异常变成一个托管异常。如果是良性的话就被吞下去。
或者取消选中Project+Properties,Debugging,Enable unmanaged code Debugging复选框,这样你就看不到它了。或者使用Debug+Exceptions并取消选中Win32异常的抛出复选框,这样调试器就不会在这类异常上停止。