CLR寄宿(上) MSCOREE.DLL
托管代码调用非托管代码,.NET提供了P/Invoke(平台调用)方式,它作为.NET的基础出现在各类书籍和网络资源上,这里不再讨论。那么非托管代码如何去调用托管代码呢?我们知道,一个托管应用程序首先被操作系统启动,然后由操作系统调用CLR来托管该程序。那么.NET框架到底以什么方式让操作系统来认识它并且可以启动它呢?微软实际将其作为COM服务器实现在一个DLL中,并提供了标准的COM接口。既然是COM服务,也就意味着普通的非托管程序也可以调用CLR来运行托管代码,把这种调用方式叫做寄宿,把调用CLR的非托管程序叫做宿主。宿主程序不仅可以调用CLR,还可以通过它来进行内存管理、垃圾回收管理、策略管理、事件管理以及线程控制等高级管理。
1.1 核心组件MSCOREE.DLL
MSCOREE.DLL负责选择.NET版本、调用和初始化CLR等工作。非托管程序想要启动CLR也必须引用MSCOREE.DLL,利用它的导出函数加载托管代码和进行定制CLR等操作。既然现在的焦点集中在MSCOREE.DLL,下面一同来看它的庐山真面目。
查看MSCOREE.DLL的头文件
对于MSCOREE.DLL文件,我们没有办法查看它的源代码,但是FrameWork SDK提供了mscoree.h文件,可以到相关版本的SDK安装目录中查看该文件。由于该文件很大,这里就不贴出源代码进行分析了。接下来会对相关的接口和函数做介绍。
MSCOREE.DLL的导出函数
在命令行启动DumpBin.exe,执行以下代码查看MSCOREE.DLL的导出函数列表:
C:\Windows\System32>dumpbin /exports MSCOREE.DLL
结果如图1-3所示。
图1-3 输出MSCOREE.DLL的导出函数列表
从运行的结果可以知道,MSCOREE.DLL中的导出函数有100多个,所有这些函数都可以被公开调用,而且在MSDN上可以找到每个函数的解释。这里只介绍其中几个常用的函数。
CorBindToRuntimeEx函数
该函数由宿主调用来加载CLR,该函数的定义如代码清单1-4所示。
代码清单1-4 CorBindToRuntimeEx函数定义
HRESULT CorBindToRuntimeEx (
[in] LPWSTR pwszVersion,
[in] LPWSTR pwszBuildFlavor,
[in] DWORD startupFlags,
[in] REFCLSID rclsid,
[in] REFIID riid,
[out] LPVOID* ppv
);
代码中的CorBindToRuntimeEx函数需要6个参数,每个参数的定义如下:
pwszVersion。一个字符串,描述要加载的 CLR 的版本。
pwszBuildFlavor。一个字符串,指定是加载 CLR 的服务器版本还是工作站版本。有效值为:svr 和 wks。服务器版本已经过优化,可以利用多个处理器进行垃圾回收,而工作站版本则针对单处理器计算机上运行的客户端应用程序进行了优化。
startupFlags。STARTUP_FLAGS 枚举值的组合。这些标志控制并发垃圾回收、非特定于域的代码以及 pwszVersion 参数的行为。如果未设置标志,则默认值为一个域。
rclsid。实现 ICorRuntimeHost 接口的 coclass 的 CLSID。支持的值为 CLSID_CorRuntimeHost或 CLSID_CLRRuntimeHost。
riid。请求自 rclsid 接口的 IID。支持的值为 IID_ICorRuntimeHost 或 IID_ICLRRuntimeHost。
ppv。返回的指向 riid 的接口指针。
提示使用CorBindToCurrentRuntime函数并使用存储在 XML文件中的版本信息可以将公共语言运行库 (CLR) 加载到进程中,还可以使用CorBindToRuntimeByCfg 函数从 XML文件中读取的版本信息将公共语言运行库 (CLR) 加载到进程中。
CorExitProcess函数
该函数用来关闭当前的非托管进程,定义如代码清单1-5所示。
代码清单1-56 CorExitProcess 函数定义
void STDMETHODCALLTYPE CorExitProcess (
[out] int exitCode
);
其中,参数exitCode表示一个指定进程退出代码的整数。
ClrCreateManagedInstance 函数
该函数用来创建指定托管类型的实例,定义如代码清单1-6所示。
代码清单1-6 ClrCreateManagedInstance 函数定义
STDAPI ClrCreateManagedInstance (
[in] LPCWSTR pTypeName,
[in] REFIID riid,
[out] void **ppObject
);
代码中函数ClrCreateManagedInstance的各参数定义如下:
pTypeName。一个指向所请求的实例类型名称的指针。
riid。所请求实例类型的 IID。
**ppObject。一个指向指针的指针,它指向的指针指向调用方请求的托管类型实例。
CallFunctionShim函数
该函数用来调用指定库中具有指定名称和参数的函数。定义如代码清单1-7所示。
代码清单1-7 CallFunctionShim 函数定义
HRESULT CallFunctionShim (
[in] LPCWSTR szDllName,
[in] LPCSTR szFunctionName,
[in] LPVOID lpvArgument1,
[in] LPVOID lpvArgument2,
[in] LPCWSTR szVersion,
[in] LPVOID pvReserved
);
代码中函数CallFunctionShim的各参数定义如下:
szDllName。包含函数的库的名称。
szFunctionName。函数的名称。
lpvArgument1。要传递到函数的第一个参数。
lpvArgument2。要传递到函数的第二个参数。
szVersion。包含函数的库的版本。
pvReserved。留作未来使用。在此参数中传递零。
更多的导出函数读者可自行操作获得它们的名称,然后查阅MSDN来进一步了解。
MSCOREE.DLL宿主接口
宿主接口使宿主能够对运行库的更多方面进行控制,从而能够在 CLR 和宿主的执行模型之间进行更紧密的集成。在.NET Framework 1 版中,宿主模型使非托管宿主能够将 CLR 加载到进程中、配置某些设置以及接收事件通知。但在通常情况下,宿主和 CLR 可以在该进程中独立运行。通过 .NET Framework 2.0 版及更高版本中新的抽象层,宿主可以提供当前由 Win32 程序集中的类型提供的多种资源,并扩展了宿主可以配置的功能集。
MSCOREE.DLL同样提供了多个宿主接口,这里只介绍较为重要的几个,更多的内容请读者参考MSDN。
ICorRuntimeHost 接口
该接口是调用CLR要用到的第一个接口,它负责初始化工作。该接口由上面提到的CorBindToRuntimeEx函数返回,其定义如代码清单1-8所示。
代码清单1-8 ICorRuntimeHost接口定义
interface ICLRRuntimeHost : IUnknown {
HRESULT ExecuteApplication (
[in] LPCWSTR pwzAppFullName,
[in] DWORD dwManifestPaths,
[in] LPCWSTR *ppwzManifestPaths,
[in] DWORD dwActivationData,
[in] LPCWSTR *ppwzActivationData,
[out] int *pReturnValue
);
HRESULT ExecuteInAppDomain (
[in] DWORD appDomainId,
[in] FExecuteInDomainCallback pCallback,
[in] void* cookie
);
HRESULT ExecuteInDefaultAppDomain (
[in] LPCWSTR pwzAssemblyPath,
[in] LPCWSTR pwzTypeName,
[in] LPCWSTR pwzMethodName,
[in] LPCWSTR pwzArgument,
[out] DWORD *pReturnValue
);
HRESULT GetCLRControl (
[out] ICLRControl **pCLRControl
);
HRESULT GetCurrentAppDomainId (
[out] DWORD *pdwAppDomainId
);
HRESULT SetHostControl (
[in] IHostControl *pHostControl
);
HRESULT Start();
HRESULT Stop();
HRESULT UnloadAppDomain (
[in] DWORD dwAppDomainId
[in] BOOL fWaitUntilDone
);
};
对代码清单1-8中的各方法说明如下:
q ICLRRuntimeHost::ExecuteApplication方法
一个字符串,描述要加载基于清单的 ClickOnce 部署方案中用于指定要在新域中激活的应用程序。
q ICLRRuntimeHost::ExecuteInAppDomain 方法
指定要在其中执行指定托管代码的 AppDomain。
q ICLRRuntimeHost::ExecuteInDefaultAppDomain 方法
调用指定程序集中属于指定类型的指定方法。
q ICLRRuntimeHost::GetCLRControl 方法
获取一个ICLRControl类型的接口指针,宿主可以使用该类型自定义公共语言运行库 (CLR) 的各个方面。
q ICLRRuntimeHost::GetCurrentAppDomainId 方法
获取当前正在执行的AppDomain的数字标识符。
q ICLRRuntimeHost::SetHostControl 方法
设置主机控制接口。在调用 Start 之前必须调用 SetHostControl。
q ICLRRuntimeHost::Start 方法
将 CLR 初始化到进程中。
q ICLRRuntimeHost::Stop 方法
使运行库停止代码的执行。
q ICLRRuntimeHost::UnloadAppDomain 方法
卸载与指定的数字标识符对应的 AppDomain。
ICLRGCManager接口
该接口提供允许宿主与公共语言运行库的垃圾回收系统进行交互的方法。该接口的定义如代码清单1-9所示。
代码清单1-9 ICLRGCManager接口定义
interface ICLRGCManager : IUnknown {
HRESULT Collect (
[in] LONG Generation
);
HRESULT GetStats (
[in, out] COR_GC_STATS *pStats
);
HRESULT SetGCStartupLimits (
[in] DWORD SegmentSize,
[in] DWORD MaxGen0Size
);
};
对代码清单1-10中ICLRGCManager接口的各方法说明如下:
q ICLRGCManager::Collect方法
为指定的生成强制执行垃圾回收。
q ICLRGCManager::GetStats方法
获取有关垃圾回收系统的一组当前统计信息。
q ICLRGCManager::SetGCStartupLimits方法
设置垃圾回收段的大小和垃圾回收系统零代的最大大小。
IHostControl接口
该接口提供一些方法,以配置程序集的加载和确定宿主支持的宿主接口。该接口的定义如代码清单1-10所示。
代码清单1-10 IHostControl接口定义
interface IHostControl : IUnknown {
HRESULT GetHostManager (
[in] REFIID riid,
[out, iid_is(riid)] IUnknown** ppObject
);
HRESULT SetAppDomainManager (
[in] DWORD dwAppDomainID,
[in] IUnknown* pUnkAppDomainManager
);
};
对代码清单1-10中IHostControl接口中的方法说明如下:
q IHostControl::GetHostManager 方法
获取一个接口指针,该指针指向宿主对具有指定 IID 接口的实现。
q IHostControl::SetAppDomainManager 方法
通知宿主已创建了一个应用程序域。
其他接口
其他接口这里不详细介绍,读者可根据需要按照下面的简介自行查阅相关的文档。
IActionOnCLREvent提供为已注册的事件执行回调的方法。
IApartmentCallback提供用于在单元内进行回调的方法。
IAppDomainBinding提供用于设置运行时配置的方法。
ICatalogServices提供用于编录服务的方法。(此接口支持.NET Framework 基础结构,但不应在代码中直接使用。)
ICLRAssemblyIdentityManager提供支持宿主和 CLR 之间就程序集问题进行通信的方法。
ICLRAssemblyReferenceList管理由CLR(而非宿主)加载的程序集的列表。
ICLRControl提供一些方法,以便宿主可以获取对CLR的访问权限并对CLR的各个方面进行配置。
ICLRDebugManager提供使宿主能够将一组任务与某个标识符及友好名称关联起来的方法。
ICLRErrorReportingManager提供使宿主能够为错误报告配置自定义堆转储的方法。
ICLRHostBindingPolicyManager提供允许宿主计算并传达程序集策略信息中的更改的方法。
ICLRHostProtectionManager使宿主能够阻止特定的托管类、方法、属性和字段在部分受信任的代码中运行。
ICLRIoCompletionManager实现使宿主能够向 CLR 通知指定 I/O 请求的状态的回调方法。
ICLRMemoryNotificationCallback使宿主能够使用与 Win32 CreateMemoryResourceNotification 函数方法类似的方法报告内存压力情况。
ICLROnEventManager提供使宿主能够为 CLR 事件注册和注销回调的方法。
ICLRPolicyManager提供使宿主能够指定在出现故障和超时的情况下采取的策略操作的方法。
ICLRProbingAssemblyEnum提供方法,这些方法使宿主能够使用 CLR 内部的程序集标识信息来获取该程序集的探测标识,而无需创建或了解该标识。
ICLRReferenceAssemblyEnum提供方法,这些方法使宿主能够对文件或流通过 CLR 内部的程序集标识数据引用的一组程序集进行操作,而无需创建或了解这些标识。
ICLRSyncManager提供方法,以便让宿主在其同步实现中获取有关请求任务的信息并进行死锁检测。
ICLRTask提供方法,这些方法使宿主能够向 CLR 发出请求,或者向 CLR 提供与关联的任务有关的通知。
ICLRTaskManager提供方法,这些方法使宿主能够显式请求 CLR 创建一个新任务,获取当前正在执行的任务,以及设置该任务的地理语言和区域性。
ICLRValidator提供用于验证可移植可执行 (PE) 映像和报告验证错误的方法。
ICorConfiguration提供用于配置 CLR 的方法。
ICorThreadpool提供用于访问线程池的方法。
IDebuggerInfo提供用于获取调试服务状态信息的方法。
IDebuggerThreadControl提供方法,用于向宿主发出有关通过调试服务阻止和取消阻止线程的通知。
IGCHostControl提供使垃圾回收器能够请求宿主更改虚拟内存限制的方法。
IGCThreadControl提供用于参与线程调度的方法,以防止因阻塞而执行垃圾回收。
IHostAssemblyManager提供方法,这些方法使宿主能够指定应由 CLR 或宿主加载的多组程序集。
IHostAssemblyStore提供方法,这些方法使宿主能够独立于 CLR 加载程序集和模块。
IHostAutoEvent提供由宿主实现的自动重置事件的表示形式。
IHostCrst用做线程临界区的宿主表示形式。
IHostIoCompletionManager提供方法,这些方法使 CLR 能够与宿主提供的 I/O 完成端口进行交互。
IHostMAlloc提供一些方法,以便 CLR 可以请求从堆到宿主的细化分配。
IHostManualEvent提供宿主的手动重置事件的表示形式的实现。
IHostMemoryManager提供方法,以便 CLR 可以通过宿主而不是使用标准 Win32 虚拟内存函数来请求虚拟内存。
IHostPolicyManager提供一些方法,以便通知宿主 CLR 在中止、超时或失败时所执行的操作。
IHostSecurityContext使 CLR 能够维护由宿主实现的安全性上下文信息。
IHostSecurityManager提供允许访问和控制当前正在执行的线程的安全性上下文的方法。
IHostSemaphore提供由宿主实现的信号量的表示形式。
IHostSyncManager提供方法,以便 CLR 可以通过调用宿主而不是使用 Win32 同步函数来创建同步基元。
IHostTask提供使 CLR 能够与宿主通信以管理任务的方法。
IHostTaskManager提供方法,这些方法使 CLR 能够通过宿主而不是使用标准操作系统线程或线程函数来处理任务。
IHostThreadPoolManager提供一些方法,以便 CLR 可以配置线程池并对线程池中的工作项进行排队。
IManagedObject提供用于控制托管对象的方法。
IObjectHandle提供用于通过间接寻址打开按值封送对象的方法。
ITypeName提供用于获取类型名称信息的方法。(此接口支持 .NET Framework 基础结构,但不应在代码中直接使用。)
ITypeNameBuilder提供用于生成类型名称的方法。(此接口支持 .NET Framework 基础结构,但不应在代码中直接使用。)
ITypeNameFactory提供用于解构类型名称的方法。(此接口支持 .NET Framework 基础结构,但不应在代码中直接使用。)
IValidator提供用于验证可移植可执行 (PE) 映像和报告验证错误的方法。
1.2 MSCOREE.DLL接口层次模型
所有的CLR Hosting API提供的主要功能包括:CLR的启动和关闭、App Domain相关、自定义错误处理、编程模型的执行、对调试器的支持、Assembly的Load相关、CLR的内部事件、CLR Engine相关、内存管理和垃圾回收、Threading、同步和I/O的支持等。图1-4展示了MSCOREE.DLL中接口的层次模型。
图1-4 MSCOREE.DLL中的接口层次模型
和图1-4对应的是MSCOREE.DLL各个接口在应用过程中的功能模型,如图1-5所示。
图1-5 MSCOREE.DLL各个接口在应用过程中的功能模型
现在看代码清单1-8中ICLRRuntimeHost接口的定义里面的
HRESULT STDMETHODCALLTYPE SetHostControl方法和HRESULT STDMETHODCALLTYPE GetCLRControl方法。这两个方法根据功能实现者(CLR或Host)的不同对控制权进行了选择。将控制权交给CLR的起始是SetCLRControl方法。
注意 如果某一个特定的功能是由CLR来实现的,调用这个功能的相应的接口就用ICLR来开头,如果这个功能是Host实现的,就调用IHost开头的接口定义的函数。
1.3 加载CLR与执行代码实例
下面展示一下如何采用一个非托管的宿主来加载CLR并且执行里面的代码。请看代码清单1-11。
代码清单1-11 加载CLR与执行代码实例
//首先,在非托管宿主里面加载CLR并且启动
ICLRRuntimeHost *pCLRHost = NULL;
HRESULT hr = CorBindToRuntimeEx(
L"v2.0.40103", //需要加载的CLR版本,Null表示最新的
L"wks", //GC的风格,Null表示默认的工作站模式
STARTUP_CONCURRENT_GC,
CLSID_CLRRuntimeHost, //CLR的CLSID
IID_ICLRRuntimeHost, //ICLRRuntimeHost的IID
(PVOID*) &pCLRHost); //返回的COM接口
// 初始化并且启动CLR
pCLRHost->Start();
//然后执行一段托管代码
hr = pCLRHost ->ExecuteInDefaultAppDomain(L"test.exe",
L" test.Program",
L"Start",
NULL,
&retVal);
代码清单1-11只是一个最简单的示例,在实际应用中可以实施更多的配置,由于篇幅所限,本书就不更多地去讲解相关内容了,可以明确的是,一切操作都不会脱离上述接口。
-----------------------注:本文部分内容改编自《.NET安全揭秘》