我们知道 CLR 中 Assembly 是在名为 AppDomain 的逻辑空间中被载入运行的,而 AppDomain 是介于操作系统层面进程和线程概念之间,同时具有线程的轻便和进程的封闭性,使用者可以通过 AppDomain.CreateDomain 创建新的 AppDomain。这样一来就出现了一个鸡生单还是蛋生鸡的问题,这个 AppDomain.CreateDomain 方法肯定是要在一个载入了 AppDomain 类型的 AppDomain 里面被调用的,但这个 AppDomain 又是谁调用 AppDomain.CreateDomain 方法创建的呢?呵呵
我们可以使用 WinDbg + SOS 的 EEHeap 命令,通过列出 CLR 执行引擎的堆信息,获取当前运行的 AppDomain 情况。我们以下面这段代码为例
以下内容为程序代码:
//
// AppDomain.cs
//
using System;
public class EntryPoint
{
public static void Main(string[] args)
{
Console.Out.WriteLine("Hello AppDomain!"
;Console.In.ReadLine();
}
}
这个典型的 CLR 程序的输出如下:
以下为引用:
0:003 !EEHeap
succeeded
Loaded Son of Strike data table version 5 from "E:WINDOWSMicrosoft.NETFrameworkv1.1.4322mscorwks.dll"
Loader Heap:
--------------------------------------
System Domain: 793e6fc8
LowFrequencyHeap:00960000(2000:00001000)
Size: 0x00001000(4096) bytes.
HighFrequencyHeap:00962000(8000:00001000)
Size: 0x00001000(4096) bytes.
StubHeap:0096a000(2000:00001000)
Size: 0x00001000(4096) bytes.
Total size: 0x3000(12288)bytes
--------------------------------------
Shared Domain: 793e83f8
LowFrequencyHeap:00990000(2000) 06c40000(10000:00007000)
Size: 0x00009000(36864) bytes.
HighFrequencyHeap:00992000(8000:00001000)
Size: 0x00001000(4096) bytes.
StubHeap:0099a000(2000:00001000)
Size: 0x00001000(4096) bytes.
Total size: 0xb000(45056)bytes
--------------------------------------
Domain 0: 147330
LowFrequencyHeap:00970000(2000) 06c60000(10000:00004000)
Size: 0x00006000(24576) bytes.
HighFrequencyHeap:00972000(8000:00004000)
Size: 0x00004000(16384) bytes.
StubHeap:0097a000(2000:00001000)
Size: 0x00001000(4096) bytes.
Total size: 0xb000(45056)bytes
--------------------------------------
Jit code heap:
Normal Jit:06c80000(10000:00002000)
Size: 0x00002000(8192) bytes.
Total size 0x00002000(8192)bytes.
--------------------------------------
Total LoaderHeap size: 0x1b000(110592)bytes
=======================================
generation 0 starts at 0x04aa1040
generation 1 starts at 0x04aa1034
generation 2 starts at 0x04aa1028
segment
begin allocated
size
04aa0000 04aa1028
04aa4000 00002fd8(12248)
Large object heap starts at 0x05aa1028
segment
begin allocated
size
05aa0000 05aa1028
05aa6000 0x00004fd8(20440)
Total Size
0x7fb0(32688)
------------------------------
GC Heap Size
0x7fb0(32688)
我们可以看到,虽然这个程序非常简单,没有自己创建任何 AppDomain,但实际上 CLR 已经有了三个 AppDomain:"System Domain", "Shared Domain" 和 "Domain 0"。而进一步使用 DumpDomain 命令查看三个 AppDomain:
以下为引用:
0:003 !DumpDomain 793e6fc8
Domain: 793e6fc8
LowFrequencyHeap: 793e702c
HighFrequencyHeap: 793e7080
StubHeap: 793e70d4
Name:
Assembly: 00158e48 [mscorlib]
ClassLoader: 00158f20
Module Name
79b66000 e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll
0:003 !DumpDomain 793e83f8
Domain: 793e83f8
LowFrequencyHeap: 793e845c
HighFrequencyHeap: 793e84b0
StubHeap: 793e8504
Name:
0:003 !DumpDomain 147330
Domain: 00147330
LowFrequencyHeap: 00147394
HighFrequencyHeap: 001473e8
StubHeap: 0014743c
Name: appdomain.exe
Assembly: 0015c2c0 [appdomain]
ClassLoader: 00161008
Module Name
00161d50 d: empappdomain.exe
我们可以看到,System Domain 实际上是专门用于载入 mscorlib.dll 这个 BCL 基础库的;Shared Domain 暂时没有使用;而 Domain 0 则负责运行我们的目标 Assembly。我们可以猜测 System Domain 是 CLR 专门用来载入系统基础库的,而系统将进一步使用此 mscorlib 创建其他 AppDomain 以运行用户目标 Assembly。我们接下来看看 Rotor 的相关代码,是否能够予以印证。
在 CLR 启动时负责加载执行引擎的 EEStartup 函数(vmceemain.cpp:206)中,可以发现此函数首先在进行基础性初始化工作后,调用 SystemDomain::Attach 函数载入 SystemDomain,然后加载并初始化异常处理、JITer等等支持代码,最后会调用 SystemDomain::Init 函数完成初始化 SystemDomain 等等工作。
SystemDomain::Attach 函数(vmappdomain.cpp:912)主要完成四部分工作:初始化系统 stub 管理器和 SystemDomain 的静态成员变量;以全局静态数组 g_pSystemDomainMemory 的内存区,构造并初始化 SystemDomain 对象,并将指针保存到 m_pSystemDomain 静态变量中,用于以后判断 SystemDomain 是否被构造等功能使用;构造缺省的 AppDomain;构造 SharedDomain。函数的简要功能代码如下:
以下内容为程序代码:
SystemDomain*
SystemDomain::m_pSystemDomain = NULL;
static BYTE
g_pSystemDomainMemory[sizeof(SystemDomain)];
HRESULT SystemDomain::Attach()
{
// 判断 SystemDomain 是否已经构造
_ASSERTE(m_pSystemDomain == NULL);
if(m_pSystemDomain != NULL)
return COR_E_EXECUTIONENGINE;
// 初始化系统 stub 管理器和 SystemDomain 的静态成员变量
// ...
// 构造 SystemDomain 对象
m_pSystemDomain = new (&g_pSystemDomainMemory) SystemDomain();
if(m_pSystemDomain == NULL) return COR_E_OUTOFMEMORY;
// 初始化 SystemDomain 对象
HRESULT hr = m_pSystemDomain-BaseDomain::Init(); // Setup the memory heaps
if(FAILED(hr)) return hr;
m_pSystemDomain-GetInterfaceVTableMapMgr().SetShared();
// 构造缺省的 AppDomain
hr = m_pSystemDomain-CreateDefaultDomain();
if(FAILED(hr)) return hr;
// 构造 SharedDomain
hr = SharedDomain::Attach();
return hr;
}
值得注意的是,为了让 SystemDomain 的构造不会失败,SystemDomain 及其基类 BaseDomain 的构造函数都为空,而初始化代码放到 Init 方法中完成,CLR 中很多类型的代码都使用类似的模式将构造和初始化分离以保障构造成功。BaseDomain::Init 函数在 SystemDomain::Attach 中直接被调用以初始化 SystemDomain 的父类;SystemDomain::Init 函数则在上面提到的 EEStartup 函数末尾才被调用,待会再详细讨论。
BaseDomain::Init 函数(vmappdomain.cpp:310)除了要负责初始化 BaseDomain 对象的一大堆成员变量外,主要负担堆和缓存的初始化。CLR 中的堆,实际上是在每个 AppDomain 中存在的,这也是为什么我们刚刚可以使用 EEHeap 命令列举 AppDomain 的原因。在初始化 BaseDomain 之后,会将 SystemDomain 的接口 VTable 映射表设置为共享,这是因为 SystemDomain 负责载入的 mscorlib 中类型实际上是所以 AppDomain 中都需要使用到的。
接着 SystemDomain::Attach 会调用 SystemDomain::CreateDefaultDomain 函数(vmappdomain.cpp:2522)构造缺省的 AppDomain,也就是前面试验中的 "Domain 0",用作载入用户指定 Assembly 执行。此函数只是简单地调用 SystemDomain::NewDomain 函数以非 Managed 方式构造新的 AppDomain 实例;然后将此 AppDomain 设置为缺省的 AppDomain。
以下内容为程序代码:
HRESULT SystemDomain::CreateDefaultDomain()
{
HRESULT hr = S_OK;
// 防止多次初始化
if (m_pDefaultDomain != NULL)
return S_OK;
// 以非 Managed 方式构造新的 AppDomain 实例
AppDomain* pDomain = NULL;
if (FAILED(hr = NewDomain(&pDomain)))
return hr;
// 将此 AppDomain 设置为缺省的 AppDomain
pDomain-GetSecurityDescriptor()-SetDefaultAppDomainProperty();
m_pDefaultDomain = pDomain;
// ...
}
SystemDomain::NewDomain 函数(vmappdomain.cpp:2480)比较简单,构造 AppDomain 实例后,通知此