






















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 线程之前,初始化调试服务:
因为 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 函数及其相关伪代码如下:
首先调用 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 等对象创建的内存分派,都接管到调试堆中。代码如下:
创建的调试用内核对象基本上是为 CLR 和调试接口各准备了一套,其中面向调试接口的一套内核对象,命名方式遵循 "CorDB + IPC + 对象名 + _ + 进程 ID" 的模式,并且将其安全属性通过 IPCManager 设置为只有同一帐号才能进行控制。如果在 Rotor 下则前缀 CorDB 改为 RotorDB,如:
通过分析上述代码可以知道,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...
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。