Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

CLR 调试支持机制原理浅析 [1] 整体结构 [草稿]

Posted on 2005-08-28 22:15  Flier Lu  阅读(5123)  评论(10编辑  收藏  举报

原文:http://www.blogcn.com/User8/flier_lu/blog/24143356.html

    Yun Jin 的 blog 上最近有两篇有趣的文章,介绍了 CLR 中线程概念的内部实现以及缺省提供的特殊线程。
    
    Thread, System.Threading.Thread, and !Threads (I)
    Special threads in CLR
    
    其中提到 EE 在启动时会初始化一个专用的调试线程。

2. Debugger helper thread. As its name suggests, this thread helps interop debugger to get information of the managed process and to execute certain debugging operations. The thread is created when EE initializes debugger during start up. In Rotor, the thread proc for this thread is DebuggerRCThread::ThreadProcStatic (debug\ee\Rcthread.cpp). Also see Mike Stall's blog about impact of this helper thread。    

    与传统的 Native Win32 程序不同,CLR 对调试的支持是通过 In-Proc 模式提供的。Mike Stall 在其 blog 上介绍了这种模式的优劣:
    
     Implications of using a helper thread for debugging
    
    首先,我们来看看运行时的调试支持情况。
    
    用 windbg 启动一个 CLR 程序后,可以用 ~ 命令和 sos 的 ~threads 命令,看看 Native 线程和 CLR 线程的对应情况如下:

0:003> ~
   0  Id: 1040.1fbc Suspend: 1 Teb: 7ffdd000 Unfrozen
   1  Id: 1040.1fc4 Suspend: 1 Teb: 7ffdc000 Unfrozen
   2  Id: 1040.1e64 Suspend: 1 Teb: 7ffdb000 Unfrozen
.  3  Id: 1040.1e14 Suspend: 1 Teb: 7ffda000 Unfrozen

0:003> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
                             PreEmptive   GC Alloc               Lock     
       ID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
  0  1fbc 0015e890      6020 Enabled  00bfe82c:00bffff4 00158ef8     0 STA
  2  1e64 00151530      b220 Enabled  00000000:00000000 00158ef8     0 MTA (Finalizer)
    
    可以看到正如 Yun Jin 所说,除了与主线程 (ID:1fbc) 和 Finalizer 线程 (ID:1e64) 对应的 Native 线程外,还有两个 (ID: 1fc4, 1e14) Native 线程。
用 ~* 命令查看线程的详细情况如下:

0:003> ~*
   0  Id: 1040.1fbc Suspend: 1 Teb: 7ffdd000 Unfrozen
      Start: *** WARNING: Unable to verify checksum for image00400000
*** ERROR: Module load completed but symbols could not be loaded for image00400000
image00400000+0x61de (004061de) 
      Priority: 0  Priority class: 32
   1  Id: 1040.1fc4 Suspend: 1 Teb: 7ffdc000 Unfrozen
      Start: KERNEL32!BaseThreadStartThunk (7c82b5bb) 
      Priority: 0  Priority class: 32
   2  Id: 1040.1e64 Suspend: 1 Teb: 7ffdb000 Unfrozen
      Start: KERNEL32!BaseThreadStartThunk (7c82b5bb) 
      Priority: 2  Priority class: 32
.  3  Id: 1040.1e14 Suspend: 1 Teb: 7ffda000 Unfrozen
      Start: ntdll!DbgUiRemoteBreakin (7c975e73) 
      Priority: 0  Priority class: 32


      其中线程 (ID: 1e14) 是 WinDbg 调试用,可以暂且不记。另外一个线程 (ID: 1fc4) 就是我们的目标:调试支持线程。用 sysinternals 的 Process Explorer 可以查看其运行时堆栈如下:

...
KERNEL32.dll!WaitForMultipleObjects+0x18
mscorwks.dll!DebuggerRCThread::MainLoop+0x90
mscorwks.dll!DebuggerRCThread::ThreadProc+0x68
mscorwks.dll!DebuggerRCThread::ThreadProcStatic+0xb
KERNEL32.dll!BaseThreadStart+0x34

      在大致了解了其运行时状况后,我们来看看实际的实现方法。

      在 《用WinDbg探索CLR世界 [2] 线程》 一文中曾介绍过 CLR 初始化 EE (Execute Engine) 的步骤。通过 CoInitializeEE -> TryEEStartup -> EEStartup 的调用,最终由 EEStartup 函数 (vm\ceemain.cpp:206) 完成实际的初始化工作。
EEStartup 函数会在完成最基本的初始化工作,如启动 IPC 引擎后;在建立任何 EE 线程之前,初始化调试服务:
#ifdef DEBUGGING_SUPPORTED
    
// This must be done before initializing the debugger services so that
    
// if the client chooses to attach the debugger that it gets in there
    
// in time for the initialization of the debugger services to
    
// recognize that someone is already trying to attach and get everything
    
// to work accordingly.
    IfFailGo(NotifyService());

    
//
    
// Initialize the debugging services. This must be done before any
    
// EE thread objects are created, and before any classes or
    
// modules are loaded.
    
//
    hr = InitializeDebugger();
    _ASSERTE(SUCCEEDED(hr));
    IfFailGo(hr);
#endif // DEBUGGING_SUPPORTED

      因为 CLR 的调试支持依赖于基于 IPC 的 Notify 机制,因此在初始化调试支持之前,需要先完成对 IPC 和 Notify 服务的支持。
Notify 服务本质上就是一块全局的共享内存块,其 Section 在 Rotor 中名称为 ROTORSvcEventQueue,在 .NET 1.1 中名称为 CORSvcEventQueue。通过这块共享内存,CLR 会维护一个 ServiceEventBlock 类 (clr/src/inc/corsvcpriv.h:70),提供跨进程一级的消息队列机制。这个内存块会由一个独立的服务进程维护,负责对 CLR 启动和服务停止等事件进行分发。如果此全局服务存在,NotifyService 函数会在队列中现有事件已经分发完毕后,构造一个类型为 runtimeStarted 的 ServiceEvent 提交给它,以通告 CLR 正在启动。通过这个机制可以完成一些非常有趣的功能,但因为不涉及此次讨论的主体,暂且放下回头有空专门写文章讨论。

      负责初始化调试器支持的 InitializeDebugger 函数 (vm/ceemain.cpp:1868) 主要负责构造调试器对象和初始化调试器接口。InitializeDebugger 函数及其相关伪代码如下:
//
// InitializeDebugger initialized the Runtime-side COM+ Debugging Services
//
static HRESULT InitializeDebugger(void)
{
  
// 构造面向外部调试器的 EE 调试接口
  EEDbgInterfaceImpl::Init();

// 构造调试器 Debugger 类实例
if (SUCCEEDED(CorDBGetInterface(&g_pDebugInterface)))
{
  g_pDebugInterface
->SetEEInterface(g_pEEDbgInterfaceImpl);
  
  
// 启动调试器
  if (SUCCEEDED(g_pDebugInterface->Startup()))
  
{
      
// 如果使用调试器线程控制接口,则更新特殊线程列表
      if (CorHost::GetDebuggerThreadControl())
      
{
        CorHost::RefreshDebuggerSpecialThreadList();
      }

  }

}


  
if (IDebuggerThreadControl *pDTC = CorHost::GetDebuggerThreadControl())
    g_pDebugInterface
->SetIDbgThreadControl(pDTC);
}


FORCEINLINE 
void EEDbgInterfaceImpl::Init(void)
{
    g_pEEDbgInterfaceImpl 
= new EEDbgInterfaceImpl();
}


HRESULT __cdecl CorDBGetInterface(DebugInterface
** rcInterface)
{
  HRESULT hr 
= S_OK;
 
if (rcInterface != NULL)
{
  
if (g_pDebugger == NULL)
  
{
    g_pDebugger 
= new Debugger();
    
if (g_pDebugger == NULL) 
hr 
= E_OUTOFMEMORY;
  }

  
*rcInterface = g_pDebugger;
}

return hr;
}


      首先调用 EEDbgInterfaceImpl::Init 函数 (vm/eedbginterfaceimpl.h:53) 构造面向外部调试器的 EE 调试接口,并存储在全局变量 g_pEEDbgInterfaceImpl 中。
然后调用 CorDBGetInterface 函数 (debug/ee/debugger.cpp:130) 构造调试器 Debugger 类 (debug/ee/debugger.h:631) 的实例,并存储于全局变量 g_pDebugger 中。而 CLR 内部则通过通用保存全局变量 g_pDebugInterface 中的 DebugInterface 抽象类 (vm/dbginterface.h:34) 的指针使用此实例。
最后在 Debugger 构造成功后,调用其 Startup 方法启动调试器。

      如果使用者通过 CLR 的 Host API 接口 ICorRuntimeHost,获取配置接口 ICorConfiguration,并设置调试线程控制接口 IDebuggerThreadControl,则 InitializeDebugger 函数还需要针对此接口做特殊处理。
主要工作包括调用 CorHost::RefreshDebuggerSpecialThreadList 函数 (vm/corhost.cpp:654) 更新特殊线程列表,避免调试器针对这些线程进行处理,不过貌似没有直接使用到此机制。

      通过上面的分析我们可以知道,CLR 的调试支持机制实际上是分为两个层面的。

      EEDbgInterfaceImpl 通过 facade 模式,将 EE 内部对调试所需功能的支持集成到一个 EEDebugInterface 接口 (vm/eedbginterface.h:55)。
      Debugger 则通过实现 DebugInterface 抽象类的功能,将 ICorDebugInfo 接口 (inc/corinfo.h:1168) 暴露给最终的调试功能使用者。

      EEDbgInterfaceImpl 的实现基本上是对 EE 内部对象和功能的调用,自身没有数据,因此这儿暂时不讨论。
Debugger 则负责向外部调试机制使用者提供接口,其 Debugger::Startup 方法 (debug/ee/debugger.cpp:513) 完成对调试环境的启动工作。
      Debugger::Startup 方法的主要工作包括:

      1.初始化多线程安全的调试堆
      2.建立、初始化并启动调试控制线程 DebuggerRCThread
      3.创建一组调试用 Event/Mutex 内核对象
      4.创建并初始化用于枚举 appdomain 的 IPC 共享内存控制块

      此外对于在调试器中启动 CLR 的情况,将做特殊处理。

      伪代码大致如下:

      对 Debugger 对象的调试堆来说,功能上就是为基础的 gmallocHeap 堆,增加了多线程安全的线程同步支持,避免因为并行使用调试接口造成内存分派上的问题。而 debugger.h 中通过重新定义全局 new/delete 操作符的方式,将 DebuggerRCThread 等对象创建的内存分派,都接管到调试堆中。代码如下:
class InteropSafe {};
#define interopsafe (*(InteropSafe*)NULL)
        
static inline void * __cdecl operator new(size_t n, const InteropSafe&[img]/images/wink.gif[/img]
{
    _ASSERTE(g_pDebugger 
!= NULL);
    _ASSERTE(g_pDebugger
->m_heap != NULL);
    
    
return g_pDebugger->m_heap->Alloc(n);
}


//  其他对应的 new[], delete, delete[] 操作符重载
      创建的调试用内核对象基本上是为 CLR 和调试接口各准备了一套,其中面向调试接口的一套内核对象,命名方式遵循 "CorDB + IPC + 对象名 + _ + 进程 ID" 的模式,并且将其安全属性通过 IPCManager 设置为只有同一帐号才能进行控制。如果在 Rotor 下则前缀 CorDB 改为 RotorDB,如:
#define CorDBIPCSetupSyncEventName L"Global\\RotorDBIPCSetupSyncEvent_%d"
#define CorDBIPCLSEventAvailName   L"Global\\RotorDBIPCLSEventAvailName_%d"
#define CorDBIPCLSEventReadName    L"Global\\RotorDBIPCLSEventReadName_%d"
#define CorDBDebuggerAttachedEvent L"Global\\RotorDBDebuggerAttachedEvent_%d"
      通过分析上述代码可以知道,CLR 的调试支持机制,实际上在运行时是由进程内的 DebuggerRCThread 线程,与进程外的调试器线程,通过一组 CorDBIPCxxx_xxx 事件进行通讯来完成的。就好比在调试支持初始化并启动后,如果 CLR 在调试器中启动,则触发 m_debuggerAttachedEvent 来通知调试器可以进行调试。这种机制与 Native WIN32 API 的调试支持机制基本相同,都是基于事件的触发调试模型。只不过 Native 的调试器是由内核一级的调试子系统来完成事件的分发和处理工作,而 CLR 因为不希望涉及内核态操作,自行通过调试控制线程完成类似的机制。因此我们如果希望进一步深入了解调试支持,就可以简化为对调试事件的支持与分析。

      而调试支持线程 DebuggerRCThread 的构造与初始化,基本上就是对其内部结构的处理。其中值得注意的是调试控制线程中,为 InProc/OutOfProc 两部分调试支持机制,各维护了一个 DebuggerIPCControlBlock 控制块结构,并维护了各部分的调试相关基础信息。对 InProc 这部分的信息,Init 方法会自行初始化;而 OutOfProc 部分的信息,则通过 IPCManager,在调试器 attach 的时候从外部进程获取。

      这一小节我们了解了 CLR 调试支持机制的整体结构,下一节我们继续分析调试支持线程是如何处理各种相关事件的。

to be continue...