CLR 调试接口的架构与应用 [3] 调试事件

原创 2004年07月07日 20:01:00

http://www.blogcn.com/user8/flier_lu/index.html?id=2042872


    在上一节中简单介绍了 CLR 调试器的框架结构,其中提到 CLR 调试环境同时支持 Native 和 Managed 两种模式的调试事件。这一节将从整体上对调试事件做一个概括性的介绍。

     首先看看 CLR 通过 ICorDebugManagedCallback 回调接口提供的 Managed 调试事件。这部分的调试事件可以大致分为被动调试事件和主动调试事件:前者由 CLR 在调试程序时自动引发被动调试事件,如创建一个新的线程;后者由调试器通过 CLR 的其他调试接口,控制 CLR 调试环境完成某种调试任务,并在适当的时候引发主动调试事件,如断点和表达式计算。

     就被动调试事件来说,基本上对应于 CLR 载入运行程序的若干个步骤

     首先是动态环境的建立,分为进程、AppDomain和线程三级,并分别有对应的建立和退出调试事件:
 

以下为引用:

 interface ICorDebugManagedCallback : IUnknown
 {
   //...
  HRESULT CreateProcess([in] ICorDebugProcess *pProcess);
  HRESULT ExitProcess([in] ICorDebugProcess *pProcess);

  HRESULT CreateAppDomain([in] ICorDebugProcess *pProcess,
                    [in] ICorDebugAppDomain *pAppDomain);
  HRESULT ExitAppDomain([in] ICorDebugProcess *pProcess,
                   [in] ICorDebugAppDomain *pAppDomain);

  HRESULT CreateThread([in] ICorDebugAppDomain *pAppDomain,
                  [in] ICorDebugThread *thread);
  HRESULT ExitThread([in] ICorDebugAppDomain *pAppDomain,
                 [in] ICorDebugThread *thread);

   HRESULT NameChange([in] ICorDebugAppDomain *pAppDomain,
         [in] ICorDebugThread *pThread);
   //...
 };
 



     在 CLR 的实现上,实际上是存在有物理上的 Native Thread 和逻辑上的 Managed Thread 两个概念的。进程和 Native Thread 对应着操作系统提供的相关概念,而 AppDomain 和 Managed Thread 则对应着 CLR 内部的相关抽象。上面的线程相关调试事件,实际上是 Native Thread 第一次以 Managed Thread 身份执行 Managed Code 的时候被引发的。更完整的控制需要借助后面要提及的 Native Thread 的调试事件。
     此外 AppDomain 和 Managed Thread 在创建并开始运行后,都会根据情况改名,并调用 NameChange 调试事件,让调试器有机会更新界面显示上的相关信息。

     其次是静态 Metadata 的载入和解析工作,也分为Assembly, Module和Class三级,并分别有对应的建立和退出调试事件:
 

以下为引用:

 interface ICorDebugManagedCallback : IUnknown
 {
   //...
  HRESULT LoadAssembly([in] ICorDebugAppDomain *pAppDomain,
                  [in] ICorDebugAssembly *pAssembly);
  HRESULT UnloadAssembly([in] ICorDebugAppDomain *pAppDomain,
                    [in] ICorDebugAssembly *pAssembly);

  HRESULT LoadModule([in] ICorDebugAppDomain *pAppDomain,
                 [in] ICorDebugModule *pModule);
  HRESULT UnloadModule([in] ICorDebugAppDomain *pAppDomain,
                  [in] ICorDebugModule *pModule);

  HRESULT LoadClass([in] ICorDebugAppDomain *pAppDomain,
                [in] ICorDebugClass *c);
  HRESULT UnloadClass([in] ICorDebugAppDomain *pAppDomain,
                 [in] ICorDebugClass *c);
   //...
 };
 



     在 CLR 中,Assembly 很大程度上是一个逻辑上的聚合体,真正落实到实现上的更多的是其 Module。一个 Assembly 在载入时,可以只是保护相关 Manifest 和 Metadata,真正的代码和数据完全可以存放在不同地点的多个 Module 中。因此,在 Managed 调试事件中,明确分离了 Assembly 和 Module 的生命周期。

     然后就是对 IL 代码中特殊指令和功能的支持用调试事件:
 

以下为引用:

 interface ICorDebugManagedCallback : IUnknown
 {
   //...
  HRESULT Break([in] ICorDebugAppDomain *pAppDomain,
             [in] ICorDebugThread *thread);

  HRESULT Exception([in] ICorDebugAppDomain *pAppDomain,
                [in] ICorDebugThread *pThread,
                [in] BOOL unhandled);

  HRESULT DebuggerError([in] ICorDebugProcess *pProcess,
                         [in] HRESULT errorHR,
                         [in] DWORD errorCode);

   HRESULT LogMessage([in] ICorDebugAppDomain *pAppDomain,
                      [in] ICorDebugThread *pThread,
                 [in] LONG lLevel,
                 [in] WCHAR *pLogSwitchName,
                 [in] WCHAR *pMessage);

  HRESULT LogSwitch([in] ICorDebugAppDomain *pAppDomain,
                     [in] ICorDebugThread *pThread,
                [in] LONG lLevel,
                [in] ULONG ulReason,
                [in] WCHAR *pLogSwitchName,
                [in] WCHAR *pParentName);

   HRESULT ControlCTrap([in] ICorDebugProcess *pProcess);

  HRESULT UpdateModuleSymbols([in] ICorDebugAppDomain *pAppDomain,
                               [in] ICorDebugModule *pModule,
                               [in] IStream *pSymbolStream);
   //...
 };
 



     Break 事件在执行 IL 指令 Break 时被引发,可被用于实现特殊的断点等功能;
     Exception 事件在代码抛出异常时,以及异常未被处理时被引发,类似于 Win32 Debug API 中的异常事件。后面介绍调试器中对异常的处理方法时再详细介绍;
     DebuggerError 事件则是在调试系统处理 Win32 调试事件发生错误时被引发;
     LogMessage 和 LogSwitch 事件分别用于处理内部类 System.Diagnostics.Log 的相关功能,类似于 Win32 API 下 OutputDebugString 函数的功能,等有机会再单独写篇文章介绍相关内容;
     ControlCTrap 事件响应用户使用 Ctrl+C 热键直接中断程序,等同于 Win32 API 下 SetConsoleCtrlHandler 函数的功能;
     UpdateModuleSymbols 事件在系统更新某个模块调试符号库的时候被引发,使调试器有机会同步状态。

     最后还省下几个主动调试事件,在调试器调用 CLR 调试接口相关功能被完成或异常时引发:
 

以下为引用:

 interface ICorDebugManagedCallback : IUnknown
 {
   //...
  HRESULT Breakpoint([in] ICorDebugAppDomain *pAppDomain,
                 [in] ICorDebugThread *pThread,
                 [in] ICorDebugBreakpoint *pBreakpoint);
   HRESULT BreakpointSetError([in] ICorDebugAppDomain *pAppDomain,
                              [in] ICorDebugThread *pThread,
                              [in] ICorDebugBreakpoint *pBreakpoint,
                              [in] DWORD dwError);

  HRESULT StepComplete([in] ICorDebugAppDomain *pAppDomain,
                  [in] ICorDebugThread *pThread,
                  [in] ICorDebugStepper *pStepper,
                  [in] CorDebugStepReason reason);

  HRESULT EvalComplete([in] ICorDebugAppDomain *pAppDomain,
                        [in] ICorDebugThread *pThread,
                        [in] ICorDebugEval *pEval);
  HRESULT EvalException([in] ICorDebugAppDomain *pAppDomain,
                         [in] ICorDebugThread *pThread,
                         [in] ICorDebugEval *pEval);

   HRESULT EditAndContinueRemap([in] ICorDebugAppDomain *pAppDomain,
                                [in] ICorDebugThread *pThread,
                                [in] ICorDebugFunction *pFunction,
                                [in] BOOL fAccurate);
   //...
 };
 



     Breakpoint 和 BreakpointSetError 在断点被触发或设置断点失败时被调用,下一节介绍断点的实现时再详细讨论;
     StepComplete 则在调试环境因为某种原因完成了一次代码步进(step)时被调用,以后介绍单步跟踪等功能实现时再详细讨论;
     EvalComplete 和 EvalException 在表达式求值完成或失败时被调用,以后介绍调试环境当前信息获取时再详细讨论;
     EditAndContinueRemap 则用于实现调试时代码编辑功能,暂不涉及。

     下面是一个比较直观的实例,显示一个简单的 CLR 调试环境在运行一个普通 CLR 程序除非相关调试事件的顺序
 

以下为引用:

 ManagedEventHandler.CreateProcess(3636)
 ManagedEventHandler.CreateAppDomain(DefaultDomain @ 3636)

 ManagedEventHandler.LoadAssembly(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ DefaultDomain)
 ManagedEventHandler.LoadModule(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ DefaultDomain)

 ManagedEventHandler.NameChange(AppDomain=cordbg)

 ManagedEventHandler.CreateThread(3944 @ cordbg)

 ManagedEventHandler.LoadAssembly(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg)
 ManagedEventHandler.LoadModule(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg)

 ManagedEventHandler.NameChange(AppDomain=cordbg.exe)

 ManagedEventHandler.LoadAssembly(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)
 ManagedEventHandler.LoadModule(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)

 ManagedEventHandler.CreateThread(2964 @ cordbg.exe)

 ManagedEventHandler.UnloadModule(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg.exe)
 ManagedEventHandler.UnloadAssembly(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg.exe)

 ManagedEventHandler.UnloadModule(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)
 ManagedEventHandler.UnloadAssembly(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)

 ManagedEventHandler.UnloadModule(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ cordbg.exe)
 ManagedEventHandler.UnloadAssembly(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ cordbg.exe)

 ManagedEventHandler.ExitAppDomain(cordbg.exe @ 3636)
 ManagedEventHandler.ExitThread(3944 @ cordbg.exe)
 ManagedEventHandler.ExitProcess(3636)
 



     可以看到 CLR 首先构造进程和 AppDomain;然后将系统执行所需的 mscorlib.dll 载入;接着将要执行的 Assembly 和缺省 Module 载入;并分析其外部应用(system.dll),载入之;建立一个新的 Managed Thread 执行之;最后卸载相关 Module 和 Assembly,并退出环境。

     在打印调试事件信息时值得注意的是很多调试接口都提供了类似的函数从 Unmanaged 环境中获取字符串或整数,如
 

以下为引用:

 interface ICorDebugAppDomain : ICorDebugController
 {
  HRESULT GetName([in] ULONG32 cchName,
                   [out] ULONG32 *pcchName,
                   [out, size_is(cchName),
                   length_is(*pcchName)] WCHAR szName[]);
 };

 interface ICorDebugAssembly : IUnknown
 {
  HRESULT GetName([in] ULONG32 cchName,
                   [out] ULONG32 *pcchName,
                   [out, size_is(cchName),
                   length_is(*pcchName)] WCHAR szName[]);
 };
 



     因此在实现上可以将之抽象为一个 delegate,以便共享基于尝试策略的数据获取算法,如
 

以下为引用:

 public class CorObject
 {
   protected delegate void GetStrFunc(uint cchName, out uint pcchName, IntPtr szName);

   protected string GetString(GetStrFunc func, uint bufSize)
   {
     uint size = bufSize;

     IntPtr szName = Marshal.AllocHGlobal((int)size);

     func(size, out size, szName);

     if(size > bufSize)
     {
       szName = Marshal.ReAllocHGlobal(szName, new IntPtr(size));

       func(size, out size, szName);
     }

     string name = Marshal.PtrToStringUni(szName, (int)size-1);

     Marshal.FreeHGlobal(szName);

     return name;
   }

   protected string GetString(GetStrFunc func)
   {
     return GetString(func, 256);
   }
 }
 



     这里使用 Marshal 对 Native 内存的直接操作,避免编写 unsafe 代码。使用的时候可以很简单地使用
 

以下为引用:

 public class CorAssembly : CorObject
 {
   private ICorDebugAssembly _asm;

  public CorAssembly(ICorDebugAssembly asm)
  {
     _asm = asm;
  }

   public string Name
   {
     get
     {
       return GetString(new GetStrFunc(_asm.GetName));
     }
   }
 }
 



     等到 CLR 2.0 支持泛型编程后,实现将更加方便。 :P

     这一小节,从整体上大致分析了 Managed 调试事件的分类和相关功能。具体的使用将在以后的文章中结合实际情况有针对性的介绍。至于 Win32 API 调试事件,介绍的资料就比较多了,这里就不在罗嗦,有兴趣进一步研究的朋友可以参考我以前的一个系列文章。

     Win32 调试接口设计与实现浅析 [2] 调试事件


     下一节将介绍 CLR 调试接口中断点如何实现和使用。

 to be continue...

《程序员》杂志试刊一发刊词

发刊词    60年的计算机发展史,就是一部程序员历史。    “软件推动计算机,计算机推动历史”,这样的说法并不过分。中国的PC时代,也完全是一个程序员时代,严援朝、吴晓军、求伯君、王选、鲍岳桥、王...
  • ycrao
  • ycrao
  • 2000-11-24 16:59:00
  • 3136

关于ContextSwitchDeadlock问题

调试时, 遇到这样一个提示"检测到 ContextSwitchDeadlockMessage: CLR 无法从 COM 上下文 0x17f050 转换为 COM 上下文 0x17ee28,这种状态已持...
  • tjvictor
  • tjvictor
  • 2009-05-26 20:53:00
  • 2462

CLR 调试接口的架构与应用 [2] 调试框架

  • zgqtxwd
  • zgqtxwd
  • 2008-04-24 08:45:00
  • 110

ContextSwitchDeadlock异常处理

C# ContextSwitchDeadlock 异常处理
  • jyxyscf
  • jyxyscf
  • 2017-08-17 09:20:35
  • 97

使用Fiddler调试你的移动应用后台api接口

我们在做移动app(android,ios,wp等)时,由于需要和服务器接口进行数据交互,为了每次调试后台接口,得需要单独根据开发库打一个版本的app用于测试,其实不用这么麻烦,直接使用正式服务器地址...
  • coolcaosj
  • coolcaosj
  • 2014-09-26 18:19:05
  • 6136

clr 工程设置 调试

今天简单的封装了下sqlite,但是在调试的过程中,怎么调不到工程里面,用depends打开生成的dll发现有说缺少库,不过程序运行正常,后来看了下工程设置,改成没有clr,就可以调试了,于是在网上查...
  • giskook
  • giskook
  • 2013-07-18 16:30:41
  • 598

CLR Debugger使用技巧

  1.1.  中断程序的执行在本章前面讲到,调试器之所以能够在程序中设置断点,是由于符号文件的关系,而符号文件是用来保存程序中指令和源代码位置的一一对应关系的。因此微软的调试器除了提供了一般的在源代...
  • Donjuan
  • Donjuan
  • 2008-12-05 18:22:00
  • 3930

解决死锁:ContextSwitchDeadlock 

详细错误信息如下: 检测到 ContextSwitchDeadlockMessage: CLR 无法从 COM 上下文 0x199018 转换为 COM 上下文 0x198ea8,这种状态已持续 60...
  • coofucoo
  • coofucoo
  • 2008-12-31 11:04:00
  • 3795

从int 3探索Windows应用程序调试原理

【系统篇】从int 3探索Windows应用程序调试原理 探索调试器下断点的原理   在Windows上做开发的程序猿们都知道,x86架构处理器有一条特殊的指令——int 3,也就是机器码0x...
  • guyue35
  • guyue35
  • 2015-05-14 09:45:55
  • 922

微信订阅号开发者模式,在线调试显示“请求失败” ()

en.情况是这样的,博主被领导要求去搞那个微信公众号首页的修改,但是没有成效啊 题主填好测试号的url和tokan之后   关注一下,然后进行调试,那么问题来了 ,现在发送消息的话那边是有回复的,而...
  • zhalcie2011
  • zhalcie2011
  • 2017-01-24 16:44:38
  • 356
收藏助手
不良信息举报
您举报文章:CLR 调试接口的架构与应用 [3] 调试事件
举报原因:
原因补充:

(最多只允许输入30个字)