wenmo8 发布的文章

人们会问“为什么本机调试器不能调试托管代码?”.
原因是CLR提供了许多在典型的本地C++应用程序中所获得的酷服务,例如:运行在虚拟机/JITEN、动态类布局、类型系统、垃圾收集、反射等等。每一个都对调试器提出了特殊的挑战。换句话说,一个完成了所有这些功能的本地应用程序根本无法与传统的本地调试器进行调试。

1) 本机调试可以在硬件级别抽象,但是托管调试需要在IL级别抽象。托管代码不能仅仅被压缩成C/C++本地调试范例。一个原因是这可能会限制CLR执行IL的选项。例如,尽管目前(从v2.0起)jit-IL,我们还是希望为诸如解释IL、推销很少使用的jitted代码、甚至重新jitting代码等事情敞开大门。如果ICorDebug对所有内容都使用本机代码偏移量,它将无法调试解释的IL。
2) 托管调试需要很多信息,直到运行时才可用。对于托管代码,编译器只生成IL,真正的调试信息直到运行时才得到解析。例如,JIT将在运行时将IL编译为本机代码,加载程序将在运行时动态确定大多数类的布局。类型系统可以在运行时创建新类型. 对于本机代码,这都是在编译时确定的。托管调试器需要某种方法在运行时获取所有这些信息。一些解决方案包括:

a、 让CLR在运行时在信息确定时创建辅助pdb。这可能是一个巨大的性能命中率,如果没有附加调试器,我们不愿意这样做。但是如果我们在没有附加调试器的情况下不这样做,那么如果以后调试器附加了调试器,它可能就不可用了。
b、 让托管调试器检查相关的CLR数据结构(直接从进程外或通过进程内运行的“helper”线程)。这里的一个重要警告是确保当CLR数据结构处于不一致状态时,调试器不会请求此类信息。CLR当前使用帮助线程。

3) 托管调试器需要与垃圾回收器(GC)协调。CLR有一个标记-清除压缩GC。这意味着GC将移动对象来整理堆碎片,并在整个过程中相应地更新所有引用(“GC根”)。这会从几个方面影响调试:

a、 调试对象在GC期间暂时处于不一致的状态。调试器必须与GC协调,以确保在此窗口期间不会检查调试对象。
b、 调试器可以让用户更改变量的值。此更新必须与GC的更新相协调。
c、 没有方便的对象标识。在本机代码中,对象的原始指针值唯一地标识该对象,因为对象不会四处移动。

Interop-Debugging,互操作调试,又称混合调试。

一般调试背景
当调试进程时,它会生成调试事件,调试器可以监听和响应这些事件。这些事件包括CreateProcess、LoadModule、异常、ExitThread、断点等。
调度调试事件时,调试对象将停止,直到调试器继续调试事件为止。当调试对象停止时,调试器可以在窗口期间检查调试对象。一旦debugger继续运行,它将自由运行,直到下一个调试事件。调试事件以每个线程为基础。
托管+本机调试都共享这个概念模型,尽管它们有不同的调试事件集,以及在停止和运行之间循环的不同方法。
调试器使用调试事件和检查API来实现所有调试操作。本文只涉及解释互操作调试如何路由托管+本机调试事件。如何在这些事件的基础上构建调试器不在本文的讨论范围之内。

本机调试:
本机调试由操作系统实现。操作系统提供用于监听和继续调试事件(WaitForDebugEvent和ContinueDebugEvent)的调试API。在调试事件中,操作系统冻结调试对象。本机调试API非常小。只有少数本机调试事件。
本机调试的关键属性:

-本机调试完全是进程外的(oop)。调试器不需要来自调试对象的额外合作(在操作系统支持之外)。
-我们将本机调试对象停止状态称为“冻结”。操作系统已停止冻结进程,并且在恢复之前不执行任何用户代码。
-本机调试事件可以在进程自由运行的任何时候出现。
-对win32调试API的所有调用都必须在同一线程上进行。我们称这个线程为W32ET,它变成了一个非常特殊的b/c,它决不能阻塞。

 

托管调试

托管调试完全是由CLR在用户模式下实现的,因此操作系统不知道何时进行纯托管调试。
CLR在为来自托管调试API的请求提供服务的每个托管进程中都有一个特殊线程(称为帮助线程)。CLR中专门用于托管调试的部分称为Left-Side(LS)。驻留在调试器进程中的ICorDebug的实现称为right-side(RS)。LS和RS通过各种用户模式进程间通信(IPC)机制进行通信,例如命名事件和共享内存块。
托管调试接口(ICorDebug)比其本机接口丰富得多。关键属性:

-托管调试是一个进程内模型。帮助线程必须存在并在调试对象的进程中运行,以便托管调试正常工作。
-我们将托管调试对象停止状态称为“已同步”。从操作系统的角度来看,同步进程是实时的,但是所有托管线程都被CLR停止。
-托管调试事件完全是在用户模式下创建和调度的。
-托管调试事件可以建立在本机调试事件之上。
-托管调试操作只能在托管停止状态下发生(这需要助手线程正在运行)

Managed vs. Native operations.

托管调试和本机调试是两个不同的世界。对于任何调试操作,查看它是在托管世界还是在本机世界中进行都非常重要。CLR调试服务只实现托管调试操作,不实现任何本机调试操作。同样,本机调试API不提供任何托管调试支持。
例如,最终用户只想到“步进”,但实际上有两个离散的操作,“托管步进”和“本机步进”,有两个完全不相交的实现。托管单步执行是通过CLR的ICorDebug API实现的;而本机单步执行是通过使用Win32调试事件的非CLR本机调试库实现的。
本机调试API级别很低,而托管调试API非常丰富。例如,本机执行控制(例如步进和断点)完全在异常之上实现,并且在本机调试API中没有显式的支持。托管调试API显式具有断点和步进器功能。
抽象级别的这种差异阻止了托管+本机调试操作之间的代码共享。

那么什么是互操作调试?
最终用户对互操作调试的看法是能够在单个调试会话中调试应用程序的托管+本机部分。这包括在托管+本机代码和运行混合调用堆栈之间单步执行的能力。托管+本机调试事件完全不相交,由调试器中的不同组件处理(我们称之为托管调试引擎和本机调试引擎)。
如果托管+本机调试器只是天真地同时附加到同一进程上,它们将相互干扰。互操作调试器确保这两个不相交的模型协同工作。
从接口的角度来看,互操作调试是将同一进程上的托管调试接口和本机调试接口公开给单个调试器。这意味着调试器的本机调试引擎和托管调试引擎可以同时运行,只需稍加修改。从理论上讲,这意味着可以轻松地扩展一个只进行托管调试和只进行本机调试的调试器来执行互操作调试。
理想情况下,本机+管理的调试引擎将彼此充分合作,以向最终用户呈现统一的模型。实际上,调试引擎之间的这种通信可能需要对调试器的设计进行重大的更改和规划。

 

对于纯托管和纯本机调试,只能将1个调试器附加到进程。
为什么?
本机调试器从托管调试器下面窃取调试事件。这会混淆托管调试器并导致其崩溃。本机调试器无法与托管调试器协调。

执行此操作时遇到的问题:
在windows中,操作系统强制一次只能将一个本机调试器附加到调试对象(已调试进程的DebugActiveProcess将失败)。仅托管调试服务具有类似的检查,以强制一次只能将一个托管调试器附加到调试对象。仅托管调试实际上与纯本机调试是分开的。(这与使用辅助线程有关)。如果一个进程只被管理调试,操作系统不会将其视为只进行本机调试。
如果允许同时将1个托管调试器和1个本机调试器附加到应用程序。这样就可以绕过检查。更糟糕的是,我们无法完全检测到这种情况。本机调试api对托管调试一无所知,因此不能强制执行。托管调试api的进程内部分可以调用IsDebuggerPresent来检查是否附加了本机调试器,但是本机调试器可以在这些调用之间附加。即使在检测到这种情况时,托管调试器也没有一种好的响应方式。

为什么有人想这么做?
实际上有一些场景可以激励将多个调试器附加到一个应用程序上。

1) 获得更多的功能-仅本机调试器只能调试应用程序的本机部分。同样,仅托管调试器只能调试应用程序的仅托管部分。那么,如果你想调试一个同时使用托管代码和本机代码的应用程序呢?一种解决方案是附加两个调试器并同时使用它们。(我认为这是您在其他类似场景中必须做的;例如调试VB6调用本机COM对象)。我们开发了互操作调试(也称为“混合模式”)来显式地启用这个场景。它允许单个调试器调试应用程序的托管部分和本机部分。VisualStudio支持混合模式调试。
2)

正在调试服务器。一些服务器应用程序(如ASP.Net+SQL)可以代表多个用户运行代码。理想情况下,每个用户只需同时附加一个调试器并调试服务器上运行的部分代码。我们希望在未来的CLR版本中通过每appdomain调试(而不是仅仅是每个进程的调试)来支持这些场景。

在调试.NET应用程序中的转储文件时,有时我们可能会遇到这样的情况:我们希望得到引用RCW对象的System.__ComObject包装器引用的COM对象。
你可能会认为抛弃这个系统。也许你能给出答案,但事实并非如此。

如下例子

Name: System.__ComObject

MethodTable:
79307098EEClass: 790dfa34

Size:
16(0x10) bytes

GC Generation:
2(C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)

Fields:

MT Field
Offset Type
VT Attr Value Name
79330740 400018a 4System.Object0 instance 00000000__identity79333178 400027e 8 ...ections.Hashtable 0 instance 00000000 m_ObjectToDataMap

什么是!dumpheap?

!dumpheap是来自SOS扩展的命令,用于转储托管堆的内容。您可以获得堆上当前活动的所有托管对象的所有地址和一些附加信息。
在WinDbg的最后两个版本中,SOS实际上被PSSCOR取代,它有一个很好的帮助系统。对于大多数命令,您只需键入“!help commandName”,例如,“!help dumpheap”,您将获得关于参数和如何使用的详细帮助。

!dumpheap 参数

  • -stat–只输出堆上所有类型对象的统计摘要、它们的计数和它们自己的大小(不带引用)
  • -nostrings–排除字符串的输出(不使用-stat时)。
  • -gen X–仅输出属于X代的对象,其中X可以具有以下值:对于1.1–0、1、2和3,对于大型对象(大于85Kb的对象,没有其引用)。对于1.0,除了使用-1而不是3。
  • -min X–忽略小于X的对象(其中X是字节数)。
  • -max X–忽略大于X的对象(其中X是字节数)。
  • -mt MethodTable–仅列出具有给定MethodTable的对象。
  • -type type–仅列出类型名为math类型的子字符串的对象。
  • -缓存–将对象保存在内部缓存中以供以后使用(有助于加快速度,而不是重新扫描堆)。
  • -lx–只打印每个堆中的X个项,而不是所有对象。
  • -short–只打印出对象地址。用于与.foreach命令组合使用。
  • -fix START END–使用给定的起始地址和结束地址,只扫描这些地址之间的堆。

注意:如果我没记错的话,-cache、-nostring和-short都是在最近两个版本的SOS(以前是PSSCOR)中添加的新命令,其余命令在大多数版本的SOS中都可以使用相当长的时间。

-short参数

你可以说,第二代的内容是如何打印的。在-short命令之前,你必须运行“!dumpheap-gen2“将输出复制到记事本,解析它,只留下对象地址,然后你就可以手动运行!do对每个地址执行操作,或将.foreach与/f命令一起使用。
现在,使用-short,您只需运行以下命令行:

.foreach ( obj { !dumpheap -gen 2 -short } ) { !do ${obj} }