Thursday, April 07, 2005

 

Some notes on CLR Load and Unload

1.
作者:Flier Lu (早期作品)
出处:bbs.smth.edu.cn

与传统的Win32可执行程序中的本机代码(Native Code)不同,微软推出的.Net架构中,可执行程序的代码是以类似Java Byte Code的IL (Intermediate Language)伪代码形式存在的。在.Net可执行程序载入后,IL代码由CLR (Common Language Runtime)从可执行文件中取出,交由JIT (Just-In-Time)编译器,根据相应的元数据(Metadata),实时编译成本机代码后执行。

因此,一个CLR可执行程序的启动过程可以分为三个步骤。
首先,Windows的可执行程序载入器(OS Loader)载入PE (Portable Executable)结构的可执行文件映像(PE Image),将执行权传递给CLR的支持库中的Unmanaged Code。
其次,启动或使用现有的CLR引擎,建立新的应用域(Application Domain),将配件(Assembly)载入到此应用域中。
最后,将执行权从Unmanaged Code传递给Managed Code,执行配件的代码。

下面我将详细说明以上步骤。
自从Win95发布以来,可执行程序的PE结构就没有发生大的改动。此次.Net平台发布,也只是利用了PE结构中现有的预留空间,以保持PE结构的稳定,最大程度保持向后兼容。(详情请参看笔者《MS.Net平台下CLR 扩展PE结构分析》一文)
CLR程序在编译后,将可执行程序入口直接以一个间接跳转指令指向mscoree.lib中的_CorExeMain函数(DLL将入口指向_CorDllMain函数)。因此CLR可执行程序在被OS Loader载入后,将由_CorExeMain函数处理CLR引擎启动事宜。此函数将启动或使用一个现有的CLR Host来加载IL代码。

常见的CLR Host有ASP.Net、IE、Shell、数据库引擎等等,他们的作用是启动一个CLR实例,管理在此CLR实例中运行的CLR程序。
我们接着来看一看一个CLR Host是如何实际运作的。
CLR作为一个引擎,在同一台计算机上是可以存在多个版本的,不同版本之间可以通过配置良好共存。在
%windir%Microsoft.NETFramework(%windir%表示Windows系统目录所在位置)目录下,
我们可以看到以版本号为目录名的多个CLR版本,如%windir%Microsoft.NETFrameworkv1.0.3705等等,
也可以在注册表的HKEY_LOCAL_MACHINESOFTWAREMicrosoft.NETFrameworkpolicyv1.0
键下查看详细的版本兼容性.Name是Build号,Value是兼容的Build号.而每一个CLR版本又分为Server和Workstation两类运行库,我们等会讲创建CLR时会详细谈到.
CLR Host在启动CLR之前,必须通过一个startup shim的库进行操作,实际上就是mscoree.dll,他提供了版本无关的操作函数,以及启动CLR所需的支持,如CorBindToRuntimeEx函数.
CLR Host通过shim的支持库,将CLR引擎载入到进程中.具体函数如下
STDAPI CorBindToRuntimeEx(LPCWSTR pwszVersion,
LPCWSTR pwszBuildFlavor, DWORD startupFlags,
REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv);
参数pwszVersion指定要载入的CLR版本号,注意必须在前面带一个小写的"v",如"v1.0.3705",可以通过查阅前面提到的注册表键,获取当前系统安装的不同CLR版本情况,或指定固定的CLR版本.也可以传递NULL给这个参数,系统将自动选择最新版本的CLR载入.
参数pwszBuildFlavor则指定载入的CLR类型,"srv"和"wks".前者适用于多处理器的计算机,能够利用多CPU提高并行性能.对单CPU系统而言,无论指定哪种类型都会载入"wks",传递NULL也是如此.
参数startupFlags是一个组合参数.由多个标志位组成.
STARTUP_CONCURRENT_GC标志指定是否使用并发的GC(Garbage Collection)机制,使用并发GC能够提高系统的用户界面相应效率,适合窗口界面使用较多的程序.但并发GC会因为无谓的线程上下文(Thread Context)切换损失效率.
以下三个参数用于指定配件载入优化策略.我们等会详细讨论.
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN = 0x1 << 1,
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN = 0x2 << 1,
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = 0x3 << 1,
接着的三个参数用于获取ICorRuntimeHost接口.
实际调用实例如下.
CComPtr< ICorRuntimeHost> spHost;
CHECK(CorBindToRuntimeEx(NULL, L"wks",
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (void **)&spHost));
这行代码载入最高版本CLR的wks类型运行库,为单应用域进行优化并使用并发GC机制.
前面提到了配件载入优化策略,要理解这个概念,我们必须先了解应用域的概念.传统Win程序中,资源的分配管理单位是进程,操作系统以进程边界将应用程序实例隔离开,单个进程的崩溃不会对其他进程产生直接影响,进程也不能直接使用其他进程的资源.进程很好,但使用进程的代价太大,为此Win32引入了线程的概念.同一进程中的线程能够共享资源,线程管理和切换的代价也远远小于进程.但因为在同一进程中,线程的崩溃会直接
影响到其他线程的运行,也无法约束线程间数据的直接访问等等.
为此,CLR中引入了Application Domain应用域的概念.应用域是介于进程和线程之间的一种逻辑上的概念.他既有线程轻巧,管理切换快捷的优点,也有进程在稳定性方面的优点,单个应用域的崩溃不会直接影响到同一进程中的其他应用域,应用域也无法直接访问同一进程中的其他应用域的资源,这方面和进程完全相同.
而CLR的管理就是完全面向应用域一级.CLR不能卸载(Unload)某个类型或配件,必须以应用域为单位启动/停止代码,获取/释放资源.
CLR在执行一个配件时,会新建一个应用域,将此配件放入新的应用域.如果多个应用域同时使用到一个配件,就要涉及到前面提到的配件载入优化策略了.最简单的方法是使用STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN标志,每个应用域拥有一份独立的配件的镜像,这样速度最快,管理最方便,但占用内存较多.相对的是所有应用域共享一份配件的镜像,(使用STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN标志),这样节约内存,但在此配件中存在静态变量等数据时,因为要保证每个应用域有独立的数据,所以会一定程度上影响效率.折中的方案是使用(STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST)标志.此时,只有那些有Strong Name的配件才会被多个应用域共享.
这里又涉及到一个概念Strong Name.他是一个配件的身份证明,他由配件的名字/版本/culture以及数字签名等组成.在配件发布时用以区别不同版本.也在安全/版本控制等方面起到重要作用,以后有机会会专门讲解.暂且跳过.
获取了ICorRuntimeHost接口的指针后,我们可以以此指针取得当前/缺省应用域,并可枚举CLR引擎实例中所有的应用域.
CComPtr< IUnknown> spUnk;
CComPtr<_AppDomain> spAppDomain;
CHECK(spHost->GetDefaultDomain(&spUnk));
spAppDomain = spUnk; spUnk = NULL;
wcout << L"Default AppDomain is " << (wchar_t *)spAppDomain->GetFriendlyName
() << endl;
CHECK(spHost->CurrentDomain(&spUnk));
spAppDomain = spUnk; spUnk = NULL;
wcout << L"Current AppDomain is " << (wchar_t *)spAppDomain->GetFriendlyName
() << endl;
HDOMAINENUM hEnum;
CHECK(spHost->EnumDomains(&hEnum));
spUnk = NULL;
while(spHost->NextDomain(hEnum, &spUnk) != S_FALSE)
{
spAppDomain = spUnk; spUnk = NULL;
wcout << (wchar_t *)spAppDomain->GetFriendlyName() << endl;
}
CHECK(spHost->CloseEnum(hEnum));
当前应用域是指当前线程运行时所在应用域.注意线程属于进程,但不属于某个应用域,一个线程可以跨应用域操作.可以通过线程类的Thread.GetDomain获取线程当前所在应用域.
缺省应用域是CLR引擎载入后自动建立的应用域,其生命期贯串CLR引擎的使用期,一般在此应用域中执行CLR Host的Managed Code端管理代码,而不执行用户代码.
接下来,是载入用户代码所在配件的时候了.方法有两种,一是接着使用完全的Native Code或者说Unmanaged Code通过BCL的COM包装接口操作;二是将操作移交给Managed Code部分的CLR Host代码执行.后者实现简单,速度较快.笔者以后将单独以一篇文章介绍CLR Host的Managed Code部分代码的设计编写.
这里将简要介绍第一种实现.
以Unmanaged Code完整实现CLR Host虽然麻烦,但功能更加强大.但因为操作中要不断在Unmanaged/Managed Code之间切换,效率受到一定影响.(切换的调用是通过IDispatch接口实现,本身效率就很低,加上CCW(COM Callable Wrapper)的包装,低于直接使用Managed Code的效率.
以Unmanaged Code调用配件,必须知道配件的部分信息,如配件的名字,要调用的类的名字,要调用的函数等等.可以指定参数的方式来使用,也可以通过PE映像中CLR头的IL入口EntryPointToken以及Metadata的信息来获取(详情请参看笔者《MS.Net平台下CLR 扩展PE结构分析》一文Metadata篇)这里为了示例简单,采用参数传递方式.
if(argc < 4)
{
cerr << "Usage: " << argv[0] << "
ion Name> " << endl;
}
else
{
_bstr_t bstrAssemblyName(argv[1]),
bstrClassName(argv[2]),
bstrMainFuncName(argv[3]);
...
}
例子中以命令行方式传递配件/类/函数名信息.
spUnk = NULL;
CHECK(spHost->GetDefaultDomain(&spUnk));
spAppDomain = spUnk; spUnk = NULL;
首先获取缺省应用域,在此应用域中创建指定配件中指定类.这里为例子简洁直接在缺省应用域中载入配件,实际开发中应避免这种方式,而采用建立新应用域的方式来载入配件.关于新建应用域以及建立时的配置,设计问题较多,以后再专门写文章详述,这里略去.
_ObjectHandlePtr spObj = spAppDomain->CreateInstance(bstrAssemblyName, bstrC
lassName);
CComPtr< IDispatch> spDisp = spObj->Unwrap().pdispVal;
建立配件中类实例后,取得一个_ObjectHandlePtr类型值,通过Unwrap()调用获取IDispatch接口,然后就可以通过此接口,以传统的COM方式控制CLR中的类.
int ArgCount = argc-4;
DISPID dispid;
LPOLESTR rgszName = bstrMainFuncName;
VARIANTARG *pArgs = new VARIANTARG[ArgCount];
for(int i=0; i {
VariantInit(&pArgs);
pArgs.vt = VT_BSTR;
pArgs.bstrVal = _bstr_t(argv[4+i]);
}
DISPPARAMS dispparamsNoArgs = {pArgs, NULL, ArgCount, 0};
CHECK(spDisp->GetIDsOfNames(IID_NULL, &rgszName, 1, LOCALE_SYSTEM_DEFAULT,
&dispid));
CHECK(spDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_MET
HOD,
&dispparamsNoArgs, NULL, NULL, NULL));
delete[] pArgs;
以上例子代码,将命令行传入参数放入参数数组,以IDispatch->Invoke调用指定名字的方法.其后台操作均由CCW进行传递.如果要直接运行一个Assembly,可以使用IAppDomain.ExecuteAssembly更加便捷.如
CHECK(spAppDomain->ExecuteAssembly(bstrAssemblyName, NULL));
至此,一个简单但完整的CLR Host程序就完成了,他可以以完全的Unmanaged Code启动CLR引擎,载入指定Assembly,以指定参数运行指定的类的方法.
下面是完整的示例程序,VC7编译通过,VC6修改一下应该也没有问题.
hello.cs
using System;
namespace Hello
{
///
/// Summary description for Class1.
///

public class Hello
{
public void SayHello(string Name)
{
Console.WriteLine("Hello "+Name);
}
}
}
ClrHost.cpp
// CLRHost.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#import rename("ReportEvent", "ReportEvent_")
using namespace mscorlib;
#include
#include
#include
#include
using namespace std;
typedef HRESULT (__stdcall * GetInfoFunc)(LPWSTR pbuffer, DWORD cchBuffer, DWO
RD* dwlength);
#define CHECK(v)
if(FAILED(v))
cerr << "COM function call failed - " << GetLastError() << " at " << __FIL
E__ << ", " << __LINE__ << endl;
wstring GetInfo(GetInfoFunc Func)
{
wchar_t szBuf[MAX_PATH];
DWORD dwLength;
if(SUCCEEDED((Func)(szBuf, MAX_PATH, &dwLength)))
return wstring(szBuf, dwLength);
else
return NULL;
}
int _tmain(int argc, _TCHAR* argv[])
{
CComPtr spHost;
CHECK(CorBindToRuntimeEx(NULL, L"wks",
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (void **)&spHost));
wcout << L"Load CLR " << GetInfo(GetCORVersion)
<< L" from " << GetInfo(GetCORSystemDirectory)
<< endl;
CHECK(spHost->Start());
CComPtr spUnk;
CComPtr<_AppDomain> spAppDomain;
#ifdef _DEBUG
CHECK(spHost->GetDefaultDomain(&spUnk));
spAppDomain = spUnk; spUnk = NULL;
wcout << L"Default AppDomain is " << (wchar_t *)spAppDomain->GetFriendlyName
() << endl;
CHECK(spHost->CurrentDomain(&spUnk));
spAppDomain = spUnk; spUnk = NULL;
wcout << L"Current AppDomain is " << (wchar_t *)spAppDomain->GetFriendlyName
() << endl;
HDOMAINENUM hEnum;
CHECK(spHost->EnumDomains(&hEnum));
spUnk = NULL;
while(spHost->NextDomain(hEnum, &spUnk) != S_FALSE)
{
spAppDomain = spUnk; spUnk = NULL;
wcout << (wchar_t *)spAppDomain->GetFriendlyName() << endl;
}
CHECK(spHost->CloseEnum(hEnum));
#endif // _DEBUG
if((argc < 2) || (argc == 3))
{
cerr << "Usage: " << argv[0] << "
ion Name> " << endl;
}
else
{
spUnk = NULL;
CHECK(spHost->GetDefaultDomain(&spUnk));
spAppDomain = spUnk; spUnk = NULL;
_bstr_t bstrAssemblyName(argv[1]);
if(argc == 2)
{
CHECK(spAppDomain->ExecuteAssembly(bstrAssemblyName, NULL));
}
else
{
_bstr_t bstrClassName(argv[2]),
bstrMainFuncName(argv[3]);
_ObjectHandlePtr spObj = spAppDomain->CreateInstance(bstrAssemblyName, b
strClassName);
CComPtr spDisp = spObj->Unwrap().pdispVal;
DISPID dispid;
LPOLESTR rgszName = bstrMainFuncName;
DISPPARAMS dispparamsArgs = {NULL, NULL, 0, 0};
int ArgCount = argc-4;
if(ArgCount > 0)
{
dispparamsArgs.cArgs = ArgCount;
dispparamsArgs.rgvarg = new VARIANTARG[ArgCount];
VARIANTARG *pArgs = dispparamsArgs.rgvarg;
for(int i=0; i {
VariantInit(&pArgs);
pArgs.vt = VT_BSTR;
pArgs.bstrVal = _bstr_t(argv[4+i]);
}
}
CHECK(spDisp->GetIDsOfNames(IID_NULL, &rgszName, 1, LOCALE_SYSTEM_DEFAUL
T, &dispid));
CHECK(spDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_M
ETHOD,
&dispparamsArgs, NULL, NULL, NULL));
delete[] dispparamsArgs.rgvarg;
}
}
CHECK(spHost->Stop());
return 0;
}

2.
CLR Loader and Java Class Loader Compared

By Junfeng Zhang
From http://blog.joycode.com/junfeng/archive/2004/04/10/18901.aspx

Flier Lu问到CLR Loader和Java Class Loader有什么不一样。要回答这个问题不容易,因为我对Java一窍不通。但既然问题提出来了,打肿脸来充胖子也得回答啊。



于是今天下午我在google上逛了一圈,找了些关于Java Class Loader的文章看了看。我的结论是,和Java Class Loader类似的东东,在CLR里是不存在的。



简单的说,CLR里不存在一个类似于Java.Lang.ClassLoader的东西。所以你无法实现你自己的CLR Loader。CLR里只有一个Loader,那就是CLR Loader。当然,CLR提供了自己独特的方式让你做动态装载。你所拥有的自由度是远比Java要大得多。



下面是具体分析。



我对Java Class Loader的理解主要是从这两篇文章里来的。

“Inside Class Loader” by Andeas Schaefer (http://www.onjava.com/lpt/a/4337) 和 “The basics of Java Class Loaders” by Chuck McManis (http://www.javaworld.com/javaworld/jw-10-1996/jw-10-indepth_p.html)。我看了看JVM Spec。但是我觉得不如上面两篇文章清楚。



先说Java里的Type吧。Java里的Type就是Class 。Class就是Namespace + class name. Class通过Class Loader来装载的。System Class Loader缺省的话只在CLASSPATH中寻找Class。如果你要在CLASSPATH之外转载Class的话,你就需要自己的Class Loader。如果两个Class有相同的名字,并且在同一个Class Loader里,那么它们就被JVM认为是相同的,可以互相赋值。如果有一个不一样的话,它们就被认为是不一样的。互相赋值会发生ClassCaseException。换句话说,Class name + Class Loader是一个Type的独特的ID。



Java的这个模型有很多问题。首先它把Class name做为Type的ID。两个Class如果有同样的名字,但是实际内容不一样的话,如果它们被同一个Class Loader装载,JVM会认为它们是同一个 Type。这个太搞笑了。结果就是Class Loader必须要有一些特别的办法来管理Class name。一般的办法是加一个前缀,比如说Class的URL。如果Namespace管理不好的话,很容易就是安全漏洞。这是为什么JVM Spec里提到Class Loader必须要让Parent Class Loader先搜索Class,再自己寻找。而且Class loader必须要保存Class resolution的结果,这样下次Class resolution的时候,Class loader会返回同样的结果。Java Class Loader有太多的限制,同时又有太多的责任。



另一个问题是共享问题。如果同一个Class被两个不同的Class Loader装载的话,JVM认为这两个Class不是同一个Type,不能互相赋值。结果就是Class无法共享。被不同Class Loader装载的Class无法直接对话。直接对话会产生linkage错误。它们只能间接对话,比如通过Interface,或者共同的Base Class。



还有一个问题是Versioning。因为Java的Type里只有名字,所以当你看到一个Type的时候,你无法知道它是第一版,还是第二版。所以如果你需要第二版,但是Class loader给你装载了第一版的时候,祈祷吧。也许上帝能救你。



现在回过头来看看CLR的模型。CLR的Type包括两部分:Class name + Assembly Name。Class name是和 Java的Type类似的东西,就是namespace。Assembly是Java没有的东西。Assembly Name的属性包括Name, Version, Culture, PublicKey(Token)。 如果两个Assembly有一个属性不一样,那么它们就认为是不一样的。两个Type如果有同样的Class name,但是Assembly Name不一样的话,它们也认为是不一样的。



CLR的Type引用总是包括Class name 和 Assembly Name的。所以CLR在寻找一个Type的时候,主要是寻找Assembly。找到了Assembly之后,CLR看看这个Assembly里有没有这个Class。有的话就万事大吉,没有的话就是TypeLoadException。



CLR区别Type主要靠Assembly。Assembly有PublicKey(Token)。别人无法冒充你。所以象Java里的namespace的问题就不存在了。Assembly有自己的Version。你要第二版的时候CLR不会装载第一版给你。所以象Java Class Loader那样的限制,责任和问题都不存在了。从这个角度上讲,这正是为什么CLR里没有象Java Class Loader那样的东西。



但并不是说你就不能动态装载了,而是说你动态装载的时候就不用考虑那些垃圾了。



关于共享的问题,因为没有了你自己的Class loader,所以任何Type都可以直接对话。同样的Type也不会因为Class Loader不一样而被认为不一样。



CLR Loader有自己的规则怎么寻找Assembly。它先看GAC,再看你的程序目录。都没有的话它还有一个AssemblyResolveEvent去问你,看你能不能提供。如果你要动态装载的话,你有Assembly.Load, Assembly.LoadFrom, Assembly.LoadFile, Assembly.Load(byte[])。你可以提供privateBin,你也可以提供codebase hint。你还能有policy。总之,你想从哪找,就可以去哪找。细节问题我就不多说了。你可以去看MSDN,Suzanne的Blog,Alan的Blog,和我的英文Blog.



结论,CLR Loader远比Java Class Loader要Secure, Powerful and Flexible。

3.
关于CLR Loader和Java Class Loader的讨论

By Flier Lu
From http://www.blogcn.com/user8/flier_lu/blog/1587099.html

MS的Fusion组成员Junfeng Zhang在其中文版本BLog上发表了一篇文章,CLR Loader and Java Class Loader Compared,简要地比较了CLR Loader和Java Class Loader。得出的结论是Java Class Loader在CLR中并不存在等价的东西,而且他在分析过程中详细地评述了Java Class Loader 的弱点,非常精彩,呵呵,感谢 Zhang 的努力 :)

不过就我个人观点来看,CLR和JVM只是通过两个不同的思路实现了相同的概念,各有千秋。

Java Class Loader与CLR Loader相比,实际上最大的缺陷就是对类型和版本的控制粒度不足,如此一来就造成Zhang所说的类型载入上的共享、安全和版本控制问题。这个问题一方面因为JVM设计目标就和CLR有所不同;另一方面可能因为Sun没有经历过MS的Dll Hell,处理这种问题经验不足和重视程度不够,呵呵
从设计目标和指导思想来说,JVM的结构可以归结于两个S:Simple and Small。毕竟JVM最初是为嵌入家电系统设计的,各种资源都非常有限,而对不同来源不同版本类型的side by side运行,可以说基本上没有什么需求。分析过Java Class和CLR Metadata格式的朋友就会发现,两者结构上的复杂度可以说相差几个数量级,呵呵。而这种Simple and Small的设计思路,可以说是Java早期成功的最大助力,但也随着Java的飞速发展成为其硬伤之一。

与此同时,JVM实现结构的简单也促使Java在设计上肯下功夫,通过设计上的复杂性和灵活性来弥补实现上的简单带来的缺陷。例如 Zhang 所说JVM Class Loader的很多问题,除了不同Class Loader之间类型共享外,其实都可以通过定制Class Loader来实现,而且我记得已经有类似的解决方案,只是并没有归纳进标准中去。而类型共享和安全方面的一些问题,就我的理解,JVM中Class Loader实际上有点象一个为类型和安全定制的轻量级AppDomain,是一个独立的隔离域。因此并不能完全将两个Class Loader的类型之间的问题,等同于一个AppDomain的不同类型之间的问题来比较看待。
而对于CLR来说,功能点一级的实现可以说比较完美了,长期以来对Dll Hell和类型共享的强烈需求,促使MS从COM到CLR一直在完善版本和类型控制系统。但是结构上感觉还是有些松散,缺乏一个类似Class Loader的组件,将分散在多个基础类上的功能聚合到一起。CLR把太多的细节隐藏在底层实现中,甚至过于依赖对实现的控制,秉承了MS作为操作系统厂商一贯的风格,呵呵。

这也体现了Java和MS两大阵营的风格不同:前者注重于开放性的设计;后者注重于实用性的实现。

如果Java能将CLR的版本和签名吸收进去,CLR多提供一些面向应用而非面向实现的工具类,这个世界将会美好得多 :P

4.
MS vs Sun,口水战ing...

By Flier Lu
From http://www.blogcn.com/User8/flier_lu/blog/6166820.html

MS和Sun又开始了新的一轮口水战,呵呵。

Sun的开发产品部门 CTO,Java 的创始人之一 James Gosling 在本周一次讲话中,批评 MS 在其 .NET 中提供 C/C++ 语言支持,可能会导致严重的安全漏洞。这一特性 “has left open a security hole large enough to drive many, many large trucks through”,而原因在于 “C++ allowed you to do arbitrary casting, arbitrary adding of images and pointers, and converting them back and forth between pointers in a very, very unstructured way”。

Huge security hole in .NET: Java creator
http://www.zdnet.com.au/news/security/print.htm?TYPE=story&AT=39179932-2000061744t-10000005c

如果从相对于较为安全的 Java/C# 语言本身来说,CLR 提供对 unmanaged code 的支持,的确还是有可能造成一些安全隐患的,Gosling 的说法也没有什么原则性的错误。但不幸的是他在挑起争端(无论是有意还是无意)之前,忘了动脑子想想 Java 自己也是有 JNI 的,呵呵。

于是 Don Box 跳了出来,抓住 JNI 这一把柄,坚决有力而又避重就轻的痛击 Sun 的软肋。

Huge Security Hole in Solaris and JVM
http://pluralsight.com/blogs/dbox/archive/2005/02/06/5596.aspx

对于 Java/C# 这类“安全语言”来说,实际上是存在静态代码安全的“Verifiable”与“Non-verifiable”,以及动态程序执行的“Managed”与“Unmanaged”两种模式。代码可验证性一般是指虚拟机在载入并执行代码之前,通过对静态代码块进行分析,确保类型使用和转换安全,指针和堆栈操作不会越界等等;代码执行模式则是指代码被载入虚拟机运行时,动态验证其内存数据和外部资源访问是否正确等等。在此两类正交模型下是程序的三种状态组合:

1.Verifiable + Managed,Java/C#/VB.NET 的缺省模式,重新编写新的可验证托管代码
2.Non-verifiable + Managed,C# 在使用 unsafe 关键字后的模式,适用于与现有代码进行交互
3.Non-verifiable + Unmanaged,JAVA 的本地接口 JNI,以及 CLR 中 C/C++ 的主要执行模式,直接集成现有代码

可以看到 JVM 实际上也是提供了 1, 3 两种模式的支持的,而 CLR 通过 C# 提供了模式 2 这种更加灵活的方式。而通过模式 2 可以获得一些其他模式没有的好处:

1.可以使用与编写 DLL 相同的语言来与之进行交互。如 Managed C++ 中可以直接使用现有 C++ 的头文件进行编译,大大降低移植代码的工作量,同时在调试时也可以很大程度上减少无效工作。

2.运行时可在两种执行模式之间快速切换。对 CLR 和 JVM 来说,要在运行时从 Managed 状态切换到 Unmanaged 状态的代价是很昂贵的,因为这种切换需要做很多前期准备工作。如 CLR 需要构造合适的帧放到堆栈中,以便从 Unmanaged 状态恢复回来后回到合适位置,并需要提供安全和栈保护等工作。而 unsafe 模式,虽然在静态代码里面只是一个 Metadata 标记,但运行时因为无需做这种薄记工作,使得跨模式调用速度大大加快。

而 CLR 在这方面另一个优势,是可以通过安全策略和代码访问安全来限制对 unmanaged 代码的访问,如缺省配置是不能从远程加载不可验证代码的。Java 这方面虽然也有一些控制机制,但在灵活性和控制粒度方面就远远不如 CLR 完整了。

不过即便如此,对 Unmanaged 代码的支持,也是一定程度上为 .NET 留下了安全隐患。因为 CLR 这方面实在做的太强大太方便了,而 Windows 平台上对此技术的使用频度,也是远远超出 Java 里的 JNI。虽然 Don Box 说 Indigo 的 1123 个 C# 文件中只有 19 个用到 unsafe 关键字,但其带来的影响肯定会远远超过 Java 中使用 JNI 的影响。CLR 强大 Interop 功能是一把双刃剑,用的好能保障 MS 顺利过渡到 Managed 世界,用的不好会使 MS 后期付出很多不必要的代价,起码目前的 .NET 缺省安全模型是无法让人满意的。

有兴趣的朋友可以看看 theserverside 上的评论。

Gosling Claims Huge Security Hole in .NET
http://www.theserverside.com/news/thread.tss?thread_id=31643

5.
CLR 中代码访问安全检测实现原理

作者:Flier Lu
出处:http://www.blogcn.com/user8/flier_lu/blog/3692778.html
主页:http://flier_lu.blogone.net/
日期:2004-10-10

在传统的操作系统级安全模型中,安全管理的粒度都是 Principal-based 层面的。用户从认证登陆成功开始,就获得此帐号的所有权限,而其运行的程序,也自动被授予帐号及其所在组的所有权限。例如我在《DACL, NULL or not NULL》一文中介绍的,NT 用户从登陆到系统建立 Session 开始,就缺省使用相同权限,新建进程自动获得父进程的权限,除非程序本身手动进行限制。而 *nix 系统下面的思路也是类似,只不过从 Token 编程了各种 uid/gid 等等。
这种 Principal-based 的安全模型,在以主机为中心的孤立环境中是非常合适的,而且足够简单和高效。但随着网络的普遍使用,这种安全模型开始受到挑战,最直接的就是如何处理从网络上运行程序的策略问题。按照现有模型,所有程序都会自动获得最大权限集,但这显然是不现实的,安全管理粒度过于粗放。
因此在 Java 和 IE 等涉及网络的应用程序中,提出了新的基于位置的安全模型。一个程序运行时获得的权限,并不由其父进程或者说宿主来决定(这些程序往往用于较高权限),而是由其程序所在位置觉得具有多少权限。Java 中将之简化为对代码源的安全策略限定,如在策略文件中指定所有来自 www.nsfocus.com 的程序都有写 c:\temp 目录的权限,而来自其他网址的程序只能读取 c:\temp 目录内容。而 IE 中则更进一步,将这些来源分类为本机(my computer)、内网(intranet)、外网(internet)、可信站点(trusted)和不可信站点(untrusted)。
如果说 Principal-based 的安全模型中,关键因素是:我是谁(当前帐号)、我要访问什么(目标资源)、我要怎么访问(操作类型);则在 基于位置的安全模型中变成了:我来自哪里(代码来源)、我要访问什么(目标资源)、我要怎么访问(操作类型)。关键因素虽然只有一个我是谁到我来自哪里的转变,但其控制粒度能够大大提升。
但是这样的基于位置的安全模型还是存在其问题,因为组件之间的可信程度是不同的。例如一个本机控件因为来源于本机,受到系统的信任,被赋予很高的权限。同时一个恶意代码来源于不受信任的位置,无法执行某项操作。如果权限限定完整的话,本来不会出现问题,但因为某些权限依赖关系的管理混乱,造成恶意代码可以通过受信任代码执行本不应允许他执行的功能。这也是众多 IE 相关漏洞的问题根本所在,其受到 IE 现有安全模型的单级信任机制的限制,注定无法彻底解决。

要彻底解决此类问题,归根结底必须建立一个可信链验证机制,也就是说执行某个操作的时候,必须检查此操作所有的上级操作组件是否拥有安全权限,而不仅仅只检测最后一级的组件。这一思路正是 CLR 中代码访问安全检测的设计思路,其关键因素增加了一个:调用我的人都有哪些,他们是否有相应权限的检测。
例如我在建立一个文件的时候,可以强制性检测调用链上的所有组件是否都拥有操作此文件的权限:
以下内容为程序代码:

public void CreateFile()
{
// Create a new FileIO permission object
FileIOPermission perm = new FileIOPermission(FileIOPermissionAccess.Write, @"C:\SomeFile.txt");

try {
// Demand the FileIOPermission
perm.Demand( );
} catch (SecurityException se) {
// Callers do not have necessary permission
}

// Method implementation...
}



FileIOPermission 类描述了我需要检测的权限,对 C:\SomeFile.txt 文件可写;FileIOPermission.Demand() 则执行这一权限的检测工作,遍历此方法调用链上的所有组件,检测他们是否有次权限。这样一来就可以从理论上避免恶意代码通过调用可信组件执行越权操作的问题。具体的权限定义和使用方法,这里就不详细介绍了。下面就这种检测如何实现做一个结构上的简要分析。

System.Security.CodeAccessPermission
System.Security.Permissions.EnvironmentPermission
System.Security.Permissions.FileDialogPermission
System.Security.Permissions.FileIOPermission
System.Security.Permissions.ReflectionPermission
System.Security.Permissions.RegistryPermission
System.Security.Permissions.SecurityPermission
System.Security.Permissions.UIPermission
System.Security.Permissions.IsolatedStoragePermission
System.Security.Permissions.IsolatedStorageFilePermission

System.Security.Permissions.StrongNameIdentityPermission
System.Security.Permissions.PublisherIdentityPermission
System.Security.Permissions.SiteIdentityPermission
System.Security.Permissions.UrlIdentityPermission
System.Security.Permissions.ZoneIdentityPermission

为进行代码访问权限检测,CLR 缺省定义了以上这些权限类型。首先他们都是从 CodeAccessPermission 类型继承出来,其次就其意义可进一步分为代码访问权限和代码身份权限。代码访问权限定义代码将如何去访问资源,如读写文件、弹出对话框等等;代码身份权限则定义访问此资源的组件必须符合什么样的身份,如只能是本地文件、或者只能是 nsfocus.com 发布的组件等等。
虽然权限种类众多,但各种子类只负责定义自身权限的特性以及如何对自身权限验证,而所有的调用链遍历和验证工作,都是由 CodeAccessPermission.Demand() 方法完成的:
以下内容为程序代码:

public void CodeAccessPermission.Demand()
{
CodeAccessSecurityEngine engine = SecurityManager.GetCodeAccessSecurityEngine();

if ((engine != null) && !this.IsSubsetOf(null))
{
StackCrawlMark mark = StackCrawlMark.LookForMyCallersCaller;
engine.Check(this, ref mark);
}
}



可以看到 CodeAccessPermission.Demand 方法,实际上是将验证操作转发给安全管理器 SecurityManager 的代码访问安全引擎 CodeAccessSecurityEngine 类型的 Check 方法完成的。
以下内容为程序代码:

internal class CodeAccessSecurityEngine
{
internal virtual void Check(CodeAccessPermission cap, ref StackCrawlMark stackMark)
{
if (!PreCheck(cap, null, 1, ref stackMark, PermissionType.DefaultFlag))
{
Check(PermissionToken.GetToken(cap), cap, ref stackMark, -1, ((cap is IUnrestrictedPermission) ? 1 : 0));
}
}

internal virtual void Check(CodeAccessPermission cap, ref StackCrawlMark stackMark, PermissionType permType)
{
int num1 = 0;
if (CodeAccessSecurityEngine.GetResult(permType, out num1))
{
return;
}
if (this.PreCheck(cap, null, 1, ref stackMark, permType))
{
CodeAccessSecurityEngine.SetResult(permType, num1);
return;
}
this.Check(PermissionToken.GetToken(cap), cap, ref stackMark, -1, ((cap is IUnrestrictedPermission) ? 1 : 0));
}

[MethodImpl(MethodImplOptions.InternalCall)]
private void Check(PermissionToken permToken, CodeAccessPermission demand, ref StackCrawlMark stackMark, int checkFrames, int unrestrictedOverride);
}



CodeAccessSecurityEngine 内部类的 Check 方法,将最终调用通过 Unmanaged 代码实现的内部方法进行安全检测。rotor 中的 COMCodeAccessSecurityEngine 类型 (ComCodeAccessSecurityEngine.cpp) 实现了这个检测逻辑。
COMCodeAccessSecurityEngine::Check 函数 (ComCodeAccessSecurityEngine.cpp:683) 通过调用 COMCodeAccessSecurityEngine::CheckInternal 函数 (ComCodeAccessSecurityEngine.cpp:697) 填充一个堆栈遍历请求结构 CasCheckWalkData 的内容,最终将请求转发给 StandardCodeAccessCheck 函数 (ComCodeAccessSecurityEngine.cpp:563) 完成检测。此结构的指针将作为堆栈遍历回调函数的参数传递给回调函数进行实际权限验证,而 StandardCodeAccessCheck 只是负责调用全局堆栈遍历支持 StackWalkFunctions 函数(StackWalk.cpp:512),以 CodeAccessCheckStackWalkCB 函数 (ComCodeAccessSecurityEngine.cpp:449) 为回调函数,以 CheckInternal 函数填充的 CasCheckWalkData 结构为参数,通过现成的堆栈遍历支持 Thread::StackWalkFrames 完成堆栈遍历。

通过堆栈遍历实现代码访问安全检测调用流程如下:

以下为引用:

CodeAccessSecurityEngine::Check 内部调用定义,由下面的函数实现
COMCodeAccessSecurityEngine::Check 转发检测请求 (ComCodeAccessSecurityEngine.cpp:683)
COMCodeAccessSecurityEngine::CheckInternal 填充 CasCheckWalkData 结构 (ComCodeAccessSecurityEngine.cpp:697)
StandardCodeAccessCheck 执行堆栈遍历
Thread::StackWalkFrames 遍历当前线程堆栈
CodeAccessCheckStackWalkCB 检测当前组件权限 (ComCodeAccessSecurityEngine.cpp:449)





因此现在 CAS 检测的问题被分为两个部分:如何遍历调用堆栈;如何检测某个组件是否拥有权限。

过于如何遍历调用对象,因为涉及到比较复杂的堆栈帧类型处理,这里就不详细解释,等有空专门写篇文章介绍。目前需要了解的是 CLR 将堆栈切分成各种不同类型的帧,每个帧代表一种状态的迁移。例如调用一个函数、从一个 Assembly 调用另一个 Assembly、从一个 AppDomain 调用另一个 AppDomain、乃至从 Managed 代码调用 Unmanaged 代码等等。而这些帧又通过链表被串到一起。堆栈遍历的工作实际上就是从当前调用帧开始,反向遍历所有的堆栈帧,处理能够处理的,跳过不能处理的,最终到栈顶结束遍历。如果遍历过程中,堆栈帧的回调处理函数发现问题,则可以通过设置标志中断堆栈遍历操作,返回异常给上级程序进行处理。如 CodeAccessCheckStackWalkCB 发现某个调用链上的方法所在 Assembly 权限不够,则中断遍历操作抛出异常。

对检测某个组件是否拥有权限的 CodeAccessCheckStackWalkCB 函数 (ComCodeAccessSecurityEngine.cpp:449) 来说,其主要工作如下:
以下内容为程序代码:

enum StackWalkAction {
SWA_CONTINUE = 0, // continue walking
SWA_ABORT = 1, // stop walking, early out in "failure case"
SWA_FAILED = 2 // couldn't walk stack
};

// 堆栈帧的封装类
class CrawlFrame {
// ...
};

static StackWalkAction CodeAccessCheckStackWalkCB(CrawlFrame* pCf, VOID* pData)
{
// CheckInternal 填充的检测数据
CheckWalkHeader *pCBdata = (CheckWalkHeader*)pData;

// 获取当前帧的相关信息
MethodDesc * pFunc = pCf->GetFunction();
Assembly *pAssem = pFunc->GetModule()->GetAssembly();
AppDomain *pAppDomain = pCf->GetAppDomain();

// 遍历操作回调函数的返回动作,SWA_CONTINUE 继续;SWA_ABORT 中断;SWA_FAILED 失败。
StackWalkAction action ;

// 跳过特殊情况的帧
if (Security::SecWalkCommonProlog (&(pCBdata->prologData), pFunc, &action, pCf))
return action ;

if (pAssem != pCBdata->pPrevAssembly)
{
// 当 Assembly 变化时进行 CAS 检测
// ...

pCBdata->pPrevAssembly = pAssem;
}

if (pAppDomain != pCBdata->pPrevAppDomain)
{
// 当 AppDomain 变化时进行 CAS 检测
// ...

pCBdata->pPrevAppDomain = pAppDomain;
}

OBJECTREF *pFrameObjectSlot = pCf->GetAddrOfSecurityObject();
if (pFrameObjectSlot != NULL && *pFrameObjectSlot != NULL)
{
// 当帧保护安全对象时进行 CAS 检测
// ...
}

return SWA_CONTINUE;
}



从当前帧中可以获取各种需要检测的信息,如方法、Assembly和AppDomain。只有当 Assembly 或 AppDomain 发生变化时,对新的对象进行 CAS 检测,同时如果帧具有显式的安全对象,也要进行 CAS 检测。不过对于符合 Security::SecWalkCommonProlog 函数 (Security.cpp:406) 定义的特殊帧,将完全跳过 CAS 检测。

首先来看看 Security::SecWalkCommonProlog 函数 (Security.cpp:406) 定义的特殊帧。首先会跳过遍历操作的调用者自己的帧;然后当进行堆栈遍历指定最大帧数为1时,跳过所有 Reflection/Remoting 调用的内部帧;最后针对 LookForMyCallersCaller 这种特殊调用,以及指定最大帧数进行处理。
以下内容为程序代码:

BOOL Security::SecWalkCommonProlog (SecWalkPrologData * pData, MethodDesc * pMeth, StackWalkAction * pAction, CrawlFrame * pCf)
{
*pAction = SWA_CONTINUE;

// 跳过 CAS 检测调用帧
if ((pData->pStackMark != NULL) && !IsInCalleesFrames(pCf->GetRegisterSet(), pData->pStackMark))
return TRUE;


// 跳过内部函数帧,如 Reflection / Remoting 调用的内部帧等等
if (pData->dwFlags & CORSEC_SKIP_INTERNAL_FRAMES)
{
// 跳过 Remoting.Messaging.StackBuilderSink.PrivateProcessMessage 方法
if (pMeth == s_stdData.pMethPrivateProcessMessage)
{
pData->bSkippingRemoting = TRUE;
return TRUE
}

// 跳过 Remoting 透明代理帧
if (!pCf->IsFrameless() && pCf->GetFrame()->GetFrameType() == Frame::TYPE_TP_METHOD_FRAME)
{
pData->bSkippingRemoting = FALSE;
return TRUE;
}

// 跳过 Remoting 帧
if (pData->bSkippingRemoting)
return TRUE;

// 跳过 Refection 相关类型的方法帧
MethodTable *pMT = pMeth->GetMethodTable();

if (pMT == s_stdData.pTypeRuntimeMethodInfo ||
pMT == s_stdData.pTypeMethodBase ||
pMT == s_stdData.pTypeRuntimeConstructorInfo ||
pMT == s_stdData.pTypeConstructorInfo ||
pMT == s_stdData.pTypeRuntimeType ||
pMT == s_stdData.pTypeType ||
pMT == s_stdData.pTypeRuntimeEventInfo ||
pMT == s_stdData.pTypeEventInfo ||
pMT == s_stdData.pTypeRuntimePropertyInfo ||
pMT == s_stdData.pTypePropertyInfo ||
pMT == s_stdData.pTypeActivator ||
pMT == s_stdData.pTypeAppDomain ||
pMT == s_stdData.pTypeAssembly)
{
return TRUE;
}
}

// 如果只希望检测调用者的调用者两级,则跳过其他的所有帧
if ((pData->pStackMark != NULL) && (*pData->pStackMark == LookForMyCallersCaller) && !pData->bFoundCaller)
{
pData->bFoundCaller = TRUE;
return TRUE;
}

// 最多只检测 cCheck 帧
if (pData->cCheck >= 0)
{
if (pData->cCheck == 0)
{
pData->dwFlags |= CORSEC_STACKWALK_HALTED;
*pAction = SWA_ABORT;
return TRUE;
}
else
{
--(pData->cCheck);
}
}

return FALSE;
}



在跳过了这些无需处理的堆栈帧后,CodeAccessCheckStackWalkCB 函数将对 Assembly/AppDomain 变化,和显式指定安全对象的情况,进行 CAS 检测。
前面介绍堆栈帧时曾经提到过,对所有跨越 Assembly/AppDomain 的调用,都会有特殊的帧被放入堆栈中做标记。因此在调用链跨越两个 Assembly/AppDomain 时,堆栈遍历回调函数将有一个合适的时机,检测新的 Assembly/AppDomain 是否拥有足够权限。这两类检测思路类似,都是先通过 Assembly::GetSecurityDescriptor 函数 (Assembly.cpp:747) 或 AppDomain::GetSecurityDescriptor 函数 (AppDomain.cpp:1146) 获取一个安全描述符 SecurityDescriptor;然后对需要进行 CAS 检测的情况,调用 SecurityDescriptor::GetGrantedPermissionSet 函数 (Security.cpp:2218) 获得相关权限集;最后调用回调函数传入参数中的 pfnCheckGrants 函数指针,进行权限验证。
值得注意的是,当安全描述符被标记为完全可信任 (SecurityDescriptor::IsFullyTrusted())、堆栈遍历参数指定非受限模式 (IUnrestrictedPermission)、以及 Assembly 是系统 BCL 类库(mscorlib.dll) 或 AppDomain 是缺省 AppDomain (用于加载 BCL 类库,具体说明参见《用WinDbg探索CLR世界 [6] AppDomain 的创建过程 》一文),则忽略 CAS 检测。
对完全可信任概念的含义,可以参考《可怕的 Fully Trusted Code》一文。

如果需要进行 CAS 检测,则 CheckGrants 函数 (ComCodeAccessSecurityEngine.cpp:128) 将完成权限的验证工作。而其实际工作,则将通过 Managed 方法 CodeAccessSecurityEngine::CheckHelper 方法 (CodeAccessSecurityEngine.cs:230) 完成。而 CheckHelper 方法将通过权限类型本身的 IsSubsetOf/Intersect 等方法的实现,来判断 Assembly/AppDomain 现有权限集,是否包括请求的权限。而 Assembly/AppDomain 现有权限集,则是在 Assembly 被载入以及 AppDomain 被创建时,由 CLR Loader 创建的。以后有机会再专门写篇文章分析这个权限集的构建逻辑。
以下内容为程序代码:

private static void CheckHelper(PermissionSet grantedSet, PermissionSet deniedSet, CodeAccessPermission demand, PermissionToken permToken)
{
if (permToken == null)
permToken = PermissionToken.GetToken(demand);

try
{
// 获取权限集不能为空
if (grantedSet == null)
{
throw new SecurityException(...);
}
// 不处理权限集不受限或请求权限为非受限权限的情况
else if (!grantedSet.IsUnrestricted() || !(demand is IUnrestrictedPermission))
{
CodeAccessPermission grantedPerm = (CodeAccessPermission)grantedSet.GetPermission(permToken);

if (grantedPerm == null)
{
if (!demand.IsSubsetOf( null ))
throw new SecurityException(String.Format(...);
else
return;
}
}

// 验证权限没有被显式禁止
if (deniedSet != null)
{
CodeAccessPermission deniedPerm = (CodeAccessPermission)deniedSet.GetPermission(permToken);

if (deniedPerm != null)
{
if (deniedPerm.Intersect(demand) != null)
{
throw new SecurityException(...);
}
}
}
}
catch (Exception e)
{
// 所有的非 SecurityException 异常将都被转换为 SecurityException 异常
// 因为这些异常的发生都是因为获取指定权限操作失败的原因
if (e is SecurityException)
throw e;
else
throw new SecurityException(...);
}
}




最后 CodeAccessCheckStackWalkCB 还需要处理显式指定了安全对象帧的情况。对帧安全对象进行检测的 CheckFrameData 方法 (ComCodeAccessSecurityEngine.cpp:231) 与 CheckGrants 类似,也是最终通过 CheckHelper 方法实现的,这里就不罗嗦了。堆栈帧的安全对象,等到介绍堆栈结构的时候再详细解释。

至此,CLR 中代码访问安全检测的大致实现思路以及比较清晰了,等把堆栈帧结构和 CLR Loader 安全权限集构建的文章弄完,再整理篇完整的,呵呵。

6.
Assembly.Unload

By Flier Lu
From http://www.blogcn.com/user8/flier_lu/blog/2164751.html

CLR 产品单元经理(Unit Manager) Jason Zander 在前几天一篇文章 Why isn't there an Assembly.Unload method? 中解释了为什么 CLR 中目前没有实现类似 Win32 API 中 UnloadLibrary 函数功能的 Assembly.Unload 方法。
他认为之所以要实现 Assembly.Unload 函数,主要是为了回收空间和更新版本两类需求。前者在使用完 Assembly 后回收其占用资源,后者则卸载当前版本载入更新的版本。例如 ASP.NET 中对页面用到的 Assembly 程序的动态更新就是一个很好的使用示例。但如果提供了 Assembly.Unload 函数会引发一些问题:

1.为了包装 CLR 中代码所引用的代码地址都是有效的,必须跟踪诸如 GC 对象和 COM CCW 之类的特殊应用。否则会出现 Unload 一个 Assembly 后,还有 CLR 对象或 COM 组件使用到这个 Assembly 的代码或数据地址,进而导致访问异常。而为了避免这种错误进行的跟踪,目前是在 AppDomain 一级进行的,如果要加入 Assembly.Unload 支持,则跟踪的粒度必须降到 Assembly 一级,这虽然在技术上不是不能实现,但代价太大了。

2.如果支持 Assembly.Unload 则必须跟踪每个 Assembly 的代码使用到的句柄和对现有托管代码的引用。例如现在 JITer 在编译方法时,生成代码都在一个统一的区域,如果要支持卸载 Assembly 则必须对每个 Assembly 都进行独立编译。此外还有一些类似的资源使用问题,如果要分离跟踪技术上虽然可行,但代价较大,特别是在诸如 WinCE 这类资源有限的系统上问题比较明显。

3.CLR 中支持跨 AppDomain 的 Assembly 载入优化,也就是 domain neutral 的优化,使得多个 AppDomain 可以共享一份代码,加快载入速度。而目前 v1.0 和 v1.1 无法处理卸载 domain neutral 类型代码。这也导致实现 Assembly.Unload 完整语义的困难性。

基于上述问题, Jason Zander 推荐使用其他的设计方法来回避对此功能的使用。如 Junfeng Zhang 在其 BLog 上介绍的 AppDomain and Shadow Copy,就是 ASP.NET 解决类似问题的方法。

在构造 AppDomain 时,通过 AppDomain.CreateDomain 方法的 AppDomainSetup 参数中 AppDomainSetup.ShadowCopyFiles 设置为 "true" 启用 ShadowCopy 策略;然后设置 AppDomainSetup.ShadowCopyDirectories 为复制目标目录;设置 AppDomainSetup.CachePath + AppDomainSetup.ApplicationName 指定缓存路径和文件名。
通过这种方法可以模拟 Assembly.Unload 的语义。实现上是将需要管理的 Assembly 载入到一个动态建立的 AppDomain 中,然后通过跨 AppDomain 的透明代理调用其功能,使用 AppDomain.Unload 实现 Assembly.Unload 语义的模拟。chornbe 给出了一个简单的包装类,具体代码见文章末尾。

这样做虽然在语义上能够基本上模拟,但存在很多问题和代价:

1.性能:在 CLR 中,AppDomain 是类似操作系统进程的逻辑概念,跨 AppDomain 通讯就跟以前跨进程通讯一样受到诸多限制。虽然通过透明代理对象能够实现类似跨进程 COM 对象调用的功能,自动完成参数的 Marshaling 操作,但必须付出相当的代价。Dejan Jelovic给出的例子(Cross-AppDomain Calls are Extremely Slow)中,P4 1.7G 下只使用内建类型的调用大概需要 1ms。这对于某些需要被频繁调用的函数来说代价实在太大了。如他提到实现一个绘图的插件,在 OnPaint 里面画 200 个点需要 200ms 的调用代价。虽然可以通过批量调用进行优化,但跨 AppDomain 调用效率的惩罚是肯定无法逃脱的。好在据说 Whidbey 中,对跨 AppDomain 调用中的内建类型,可以做不 Marshal 的优化,以至于达到比现有实现调用速度快 7 倍以上,...,我不知道该夸奖 Whidbey 实现的好呢,还是痛骂现有版本之烂,呵呵

2.易用性:需要单独卸载的 Assembly 中类型可能不支持 Marshal,此时就需要自行处理类型的管理。

3.版本:在多个 AppDomain 中如何包装版本载入的正确性。

此外还有安全方面问题。对普通的 Assembly.Load 来说,载入的 Assembly 是运行在载入者的 evidence 下,而这绝对是一个安全隐患,可能遭受类似 unix 下面通过溢出以 root 权限读写文件的程序来改写系统文件的类似攻击。而单独在一个 AppDomain 中载入 Assembly 就能够单独设置 CAS 权限,降低执行权限。因为 CLR 架构下的四级权限控制机制,最细的粒度只能到 AppDomain。好在据说 Whidbey 会加入对使用不同 evidence 载入 Assembly 的支持。

通过这些讨论可以看到,Assembly.Unload 对于基于插件模型的程序来说,其语义的存在是很重要的。但在目前和近几个版本来说,通过 AppDomain 来模拟其语义是比较合适的选择,虽然要付出性能和易用性的问题,但能够更大程度上控制功能和安全性等方面因素。长远来说,Assembly.Unload 的实现是完全可行的,Java 中对类的卸载就是最好的例子,前面那些理由实际上都是工作量和复杂度方面的问题,并不存在无法解决的技术问题。



以下为引用:

// ObjectLoader.cs
using System;
using System.Reflection;
using System.Collections;

namespace Loader{

/* contains assembly loader objects, stored in a hash
* and keyed on the .dll file they represent. Each assembly loader
* object can be referenced by the original name/path and is used to
* load objects, returned as type Object. It is up to the calling class
* to cast the object to the necessary type for consumption.
* External interfaces are highly recommended!!
* */
public class ObjectLoader : IDisposable
{
// essentially creates a parallel-hash pair setup
// one appDomain per loader
protected Hashtable domains = new Hashtable();
// one loader per assembly DLL
protected Hashtable loaders = new Hashtable();

public ObjectLoader() {/*...*/}

public object GetObject( string dllName, string typeName, object[] constructorParms )
{
Loader.AssemblyLoader al = null;
object o = null;

try{
al = (Loader.AssemblyLoader)loaders[ dllName ];
} catch (Exception){}

if( al == null )
{
AppDomainSetup setup = new AppDomainSetup();
setup.ShadowCopyFiles = "true";

AppDomain domain = AppDomain.CreateDomain( dllName, null, setup );

domains.Add( dllName, domain );

object[] parms = { dllName };
// object[] parms = null;
BindingFlags bindings = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public;

try{
al = (Loader.AssemblyLoader)domain.CreateInstanceFromAndUnwrap(
"Loader.dll", "Loader.AssemblyLoader", true, bindings, null, parms, null, null, null);
} catch (Exception){
throw new AssemblyLoadFailureException();
}

if( al != null )
{
if( !loaders.ContainsKey( dllName ) )
{
loaders.Add( dllName, al );
}
else
{
throw new AssemblyAlreadyLoadedException();
}
}
else
{
throw new AssemblyNotLoadedException();
}
}

if( al != null )
{
o = al.GetObject( typeName, constructorParms );

if( o != null && o is AssemblyNotLoadedException )
{
throw new AssemblyNotLoadedException();
}

if( o == null || o is ObjectLoadFailureException )
{
string msg = "Object could not be loaded. Check that type name " + typeName +
" and constructor parameters are correct. Ensure that type name " + typeName +
" exists in the assembly " + dllName + ".";

throw new ObjectLoadFailureException( msg );
}
}
return o;
}

public void Unload( string dllName )
{
if( domains.ContainsKey( dllName ) )
{
AppDomain domain = (AppDomain)domains[ dllName ];
AppDomain.Unload( domain );
domains.Remove( dllName );
}
}

~ObjectLoader()
{
dispose( false );
}

public void Dispose()
{
dispose( true );
}

private void dispose( bool disposing )
{
if( disposing )
{
loaders.Clear();

foreach( object o in domains.Keys )
{
string dllName = o.ToString();
Unload( dllName );
}
domains.Clear();
}
}
}
}






以下为引用:

// Loader.cs
using System;
using System.Reflection;

namespace Loader {
// container for assembly and exposes a GetObject function
// to create a late-bound object for casting by the consumer
// this class is meant to be contained in a separate appDomain
// controlled by ObjectLoader class to allow for proper encapsulation
// which enables proper shadow-copying functionality.
internal class AssemblyLoader : MarshalByRefObject, IDisposable {

#region class-level declarations
private Assembly a = null;
#endregion

#region constructors and destructors
public AssemblyLoader( string fullPath )
{
if( a == null )
{
a = Assembly.LoadFrom( fullPath );
}
}

~AssemblyLoader()
{
dispose( false );
}

public void Dispose()
{
dispose( true );
}

private void dispose( bool disposing )
{
if( disposing )
{
a = null;
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
System.GC.Collect( 0 );
}
}
#endregion

#region public functionality
public object GetObject( string typename, object[] ctorParms )
{
BindingFlags flags = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public;

object o = null
;
if( a != null )
{
try
{
o = a.CreateInstance( typename, true, flags, null, ctorParms, null, null );
}
catch (Exception)
{
o = new ObjectLoadFailureException();
}
}
else
{
o = new AssemblyNotLoadedException();
}
return o;
}

public object GetObject( string typename )
{
return GetObject( typename, null );
}
#endregion

}
}

7.
MultiModule Assemblies

作者:Junfeng Zhang
出处:http://blogs.msdn.com/junfeng/archive/2004/07/15/183813.aspx

(按:很舒服的一个新特性)

If you are programming with .Net framework, you will deal with Assemblies all the time. After all, assemblies are the building blocks of .Net framework.



Assemblies can have single file, or can have multiple files. We typically refer an assembly with multiple files as MultiModule Assembly, and each file as a module. An assembly will all ways have one manifest module, and zero or more non-manifest modules. Any non-manifest module will show up as “.file” in the assembly’s manifest. Here is an example:



// Metadata version: v2.0.40607

.module extern client.netmodule

.assembly extern mscorlib

{

.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..

.ver 2:0:3600:0

}

.assembly extern Microsoft.VisualBasic

{

.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:

.ver 8:0:1200:0

}

.assembly myAssembly

{

.hash algorithm 0x00008004

.ver 0:0:0:0

}

.file client.netmodule

.hash = (93 2D 15 13 F0 DE 8F D0 B2 1E 45 09 9E 21 4C 5F // .-........E..!L_

E0 6D 8F 58 ) // .m.X

.file stringer.netmodule

.hash = (B1 9B 7C AE AE 81 2E 6F BA 92 2D B1 66 66 6B BC // ..|....o..-.ffk.

1E 2C 2E 87 ) // .,..

.class extern public myStringer.Stringer

{

.file stringer.netmodule

.class 0x02000002

}

.module myAssembly.exe

// MVID: {7E4BC53F-B02D-48D7-B422-C87DC359570F}

.imagebase 0x00400000

.file alignment 0x00000200

.stackreserve 0x00100000

.subsystem 0x0003 // WINDOWS_CUI

.corflags 0x00000001 // ILONLY

// Image base: 0x03B00000





There are several reasons for multimodule assemblies.

1. Combine modules written in different languages into one single assembly.



Currently there is no tool can create the assembly from source code written in different languages. Those sources have to be compiled into modules by the corresponding compilers, and then a separate tool will merge them into a single assembly. MSDN shows how to use Assembly Linker al.exe to create a multimodule assembly.



2. Incremental download.



The theory is, when you publish you assembly in internet, maybe you can separate the commonly used code from seldom used code. And CLR will only download the commonly used code most of the time, and download the seldom used code if necessary. If broadband adoption rate keeps increasing, this becomes less an issue.



3. Unfortunately, publisher policy



Publisher policy has to be carried with a publisher policy assembly to be validated, since we did not have the technology to validate XML files (there is XMLDSIG, but it is not available when publisher policy is designed). And the publisher policy config file is carried as the first module of the publisher policy assembly.



Multimodule assemblies have many problems:



1. Modules can’t reference each other without special techniques. Imagine there is a two modules assembly A, with module B and C. In order to use the types in module B, module B has to exist. So you have to build module B, then module C, then link to the final assembly. Module B can’t use any types in module C, unless special techniques are used, like the one described here. This is not a problem for single language single file assemblies.



2. When you link the modules into the final assembly, the hash of each module is recorded in the manifest file, as shown in the example above. Now if you change one source file, and build the corresponding module, you have to re-link the assembly again. Otherwise the assembly will have invalid hash value for that module. This either will result in a runtime load failure, or rejected by fusion if you want to install it to GAC. This one has been the source of many confusion and frustration, and it is not easy to track down exactly what was wrong.



3. Since multimodule assemblies have multiple files, when those assemblies are deployed, it is easy to leave some files behind, results in partial deployed assemblies.



For those reasons, multimodule assemblies are not recommended, unless necessary. The multiple languages assembly scenario is one of them.



Of course, the newest and greatest Whidbey will save the world. And this time, it is our old friend link.exe.



In Whidbey, link.exe is able to link several (.Net) modules (and obj files) into a single file assembly. There is really no new syntax. You simply put the modules on the command line.



Let’s use the MSDN sample as an example.



C:\ >more stringer.vb

' Assembly building example in the .NET Framework SDK.

Imports System

Namespace myStringer

Public Class Stringer

Public Sub StringerMethod()

Console.WriteLine("This is a line from StringerMethod.")

End Sub

End Class

End Namespace



C:\ >more client.cs

using System;

using myStringer; //The namespace created in Stringer.netmodule.

class MainClientApp

{

// Static method Main is the entry point method.

public static void Main()

{

Stringer myStringInstance = new Stringer();

Console.WriteLine("Client code executes");

//myStringComp.Stringer();

myStringInstance.StringerMethod();

}

}



C:\ >vbc /t:module Stringer.vb

Microsoft (R) Visual Basic .NET Compiler version 8.0.40607.16

for Microsoft (R) .NET Framework version 2.0.40607.16

Copyright (C) Microsoft Corporation 1987-2003. All rights reserved.





C:\ >csc /addmodule:Stringer.netmodule /t:module Client.cs

Microsoft (R) Visual C# .NET Compiler version 8.00.40607.16

for Microsoft (R) Windows (R) .NET Framework version 2.0.40607

Copyright (C) Microsoft Corporation 2001-2003. All rights reserved.



C:\ >link /entry:MainClientApp.Main /out:main.exe client.netmodule stringer.netmodule

Microsoft (R) Incremental Linker Version 8.00.40607.16

Copyright (C) Microsoft Corporation. All rights reserved.



client.netmodule : non-native module found; restarting link with /LTCG; add /LTC

G to the link command line to improve linker performance

Microsoft (R) Incremental Linker Version 8.00.40607.16

Copyright (C) Microsoft Corporation. All rights reserved.



Generating code

Finished generating code



Here is main.exe’s manifest. It does not contain any “.file”.



// Metadata version: v2.0.40607

.assembly extern mscorlib

{

.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..

.ver 2:0:3600:0

}

.assembly extern Microsoft.VisualBasic

{

.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:

.ver 8:0:1200:0

}

.assembly main

{

.hash algorithm 0x00008004

.ver 0:0:0:0

}

.module main.exe

// MVID: {EA1BE75E-F051-48B5-9B7E-B3D6C5048242}

.imagebase 0x00400000

.file alignment 0x00000200

.stackreserve 0x00100000

.subsystem 0x0003 // WINDOWS_CUI

.corflags 0x00000001 // ILONLY

// Image base: 0x03B00000

8.
CLR 2.0 中的宿主监控和管理接口 [1] 概述

by Flier Lu
from http://www.blogcn.com/User8/flier_lu/index.html?id=3451359&run=.095D565

简单介绍了 CLR 1.0 中如果以宿主 (Host) 程序身份,通过代码手工加载 CLR 运行时环境。在 Whidbey 发布的测试版 CLR 2.0.40607 中,通过一组宿主管理器接口,大大增强了宿主对 CLR 运行时环境的监控和管理能力。
在【文 1】中,我们知道加载 CLR 运行时环境可以通过 mscoree.dll 提供的 CorBindToRuntimeEx 函数,加载并获取绑定到 CLR 运行时环境的接口。只不过 CLR 2.0 中提供了功能更强大的 ICLRRuntimeHost 接口,以取代 CLR 1.x 中并非那么灵活的 ICorRuntimeHost 接口。
与 CLR 1.x 中不同,CLR 2.0 中的 ICLRRuntimeHost 接口不再只是类似 ICorRuntimeHost 这样的 CLR 加载和启动管理,更是通过主动宿主管理接口 (ICLRHostXXXManager) 和被动宿主监控接口 (IHostXXXManager),能够全方位监控和管理 CLR 运行时环境。
以下内容为程序代码:

//*****************************************************************************
// New interface for hosting mscoree
//*****************************************************************************
[
uuid(90F1A06C-7712-4762-86B5-7A5EBA6BDB01),
version(1.0),
helpstring("Common Language Runtime Hosting Interface"[img]/images/wink.gif[/img],
pointer_default(unique),
local
]
interface ICLRRuntimeHost : IUnknown
{
// Starts the runtime. This is equivalent to CoInitializeCor().
HRESULT Start();

// Terminates the runtime, This is equivalent CoUninitializeCor();
HRESULT Stop();

// Returns an object for configuring runtime, e.g. threading, lock
// prior it starts. If the runtime has been initialized this
// routine returns an error. See IHostControl.
HRESULT SetHostControl([in] IHostControl* pHostControl);

HRESULT GetCLRControl([out] ICLRControl** pCLRControl);

HRESULT UnloadAppDomain([in] DWORD dwAppDomainId);

HRESULT ExecuteInAppDomain([in] DWORD dwAppDomainId,
[in] FExecuteInAppDomainCallback pCallback,
[in] void* cookie);

HRESULT GetCurrentAppDomainId([out] DWORD *pdwAppDomainId);

HRESULT ExecuteApplication([in] LPCWSTR pwzAppFullName,
[in] DWORD dwManifestPaths,
[in] LPCWSTR *ppwzManifestPaths, // optional
[in] DWORD dwActivationData,
[in] LPCWSTR *ppwzActivationData, // optional
[out] int *pReturnValue);

HRESULT ExecuteInDefaultAppDomain([in] LPCWSTR pwzAssemblyPath,
[in] LPCWSTR pwzTypeName,
[in] LPCWSTR pwzMethodName,
[in] LPCWSTR pwzArgument,
[out] DWORD *pReturnValue);
};

ICLRRuntimeHost 接口分为三个部分:

首先是启动和停止 CLR 运行时环境的 Start() 和 Stop() 方法;
其次是 CLR 运行时环境的监控和管理接口的 SetHostControl 和 GetCLRControl 方法;
最后是对 AppDomain 进行控制的 XXXAppDomain() 方法;

除了监控和管理接口的相关方法,其他的与 ICorRuntimeHost 接口大同小异,有兴趣的朋友可以参考【文 1】中的介绍。
而新增的监控和管理接口则分为监控和管理两个部分。前者先 CLR 环境提供若干个回调接口,实现 CLR 运行时诸如内存分配等操作的监控;后者则从 CLR 运行时环境获取若干管理接口,精确控制 CLR 的运行时行为。两者相辅相成,互相配合能非常精确的控制 CLR 的行为,较之以前的 Profiler 和 Debugger 接口一点也不逊色,而且使用更加简便。通过这些强大的接口,.NET 可以说是正式拉开了与现有系统大规模集成的序幕,如正在开发中的 SQL Server 2005 (Yukon) 就完全内建并基于 CLR 运行时环境,连存储过程都可以通过 CLR 直接支持。

就宿主监控和管理接口来说,CLR 将之大概分为以下部分:

功能模块 CLR 管理 Host 监控

内存 IEEMemoryManager IHostMemoryManager
任务 ICLRTaskManager IHostTaskManager
线程池 ICorThreadpool IHostThreadpoolManager
IO ICLRIoCompletionManager IHostIoCompletionManager
同步 ICLRSyncManager IHostSyncManager
Assembly IHostAssemblyManager
跨边界调用 IHostCrossAssemblyCallManager
GC ICLRGCManager IHostGCManager
安全 IHostSecurityManager IHostSecurityManager
策略 ICLRPolicyManager IHostPolicyManager
调试 ICLRDebugManager
CLR 事件 ICLROnEventManager
宿主保护 ICLRHostProtectionManager
绑定与身份 ICLRAssemblyIdentityManager
绑定策略 ICLRHostBindingPolicyManager
类型名称 ITypeNameBuilder

其中部分有些部分只有监控接口,有些部分只有管理接口。

对管理接口,使用上是直接通过通过 ICLRRuntimeHost::GetCLRControl 获得 ICLRControl 接口实例:
以下内容为程序代码:

[
uuid(9065597E-D1A1-4fb2-B6BA-7E1FCE230F61),
version(1.0),
helpstring("Common Language Runtime Control Interface"[img]/images/wink.gif[/img],
pointer_default(unique),
local
]
interface ICLRControl : IUnknown
{
HRESULT GetCLRManager(
[in] REFIID riid,
[out] void **ppObject);

HRESULT SetAppDomainManagerType(
[in] LPCWSTR pwzAppDomainManagerAssembly,
[in] LPCWSTR pwzAppDomainManagerType);
}

而 ICLRControl::GetCLRManager 方法,则可以根据各个接口的 IID 获取相应接口实例。如下列代码演示了如何获取一个 GC 管理接口的实例(为代码简便,对错误和异常检测代码都被忽略):
以下内容为程序代码:

HRESULT hr;

//...

CComPtr spClrHost;

hr = CorBindToRuntimeEx(NULL, NULL, 0, CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (PVOID*)&spClrHost);

check(hr, "CorBindToRuntimeEx"[img]/images/wink.gif[/img];

CComPtr spClrCtrl;

hr = spClrHost->GetCLRControl(&spClrCtrl);

CComPtr spGCMgr;

hr = spClrCtrl->GetCLRManager(IID_ICLRGCManager, (LPVOID *)&spGCMgr);

//...

绝大多数的 CLR 管理接口 (名为 ICLRxxxManager) 都可以通过类似的方式获得,不过有的管理接口还需要单独处理,如 ICorThreadpool 等。

对监控接口,使用上则是通过 ICLRRuntimeHost::SetHostControl 将实现了 IHostControl 接口的 COM 对象实例,交给 CLR 运行时环境,由它在合适的使用再通过 IHostControl::GetHostManager 方法获取监控接口实例,如设置代码:
以下内容为程序代码:

class CHostControl : ...
{
template
static I *CreateComObject()
{
CComObject *pObj;

CComObject::CreateInstance(&pObj);

return static_cast< I *>(pObj);
}
}

HRESULT hr;

//...

CComPtr spClrHost;

hr = CorBindToRuntimeEx(NULL, NULL, 0, CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (PVOID*)&spClrHost);

CComPtr spHostCtrl(CHostControl::CreateComObject());

hr = spClrHost->SetHostControl(spHostCtrl);

//...

这儿的 CHostControl 是使用 ATL 完成的一个 IHostControl 接口实现 COM 对象。
以下内容为程序代码:

#pragma once

#include

#include

class CHostControl : public CComObjectRoot, public IHostControl
{
public:
CHostControl(void)
{
}
virtual ~CHostControl(void)
{
}

BEGIN_COM_MAP(CHostControl)
COM_INTERFACE_ENTRY(IHostControl)
END_COM_MAP()

// IHostControl
HRESULT __stdcall GetHostManager(REFIID riid, void **ppObject);
HRESULT __stdcall SetAppDomainManager(DWORD dwAppDomainID, IUnknown *pUnkAppDomainManager);
HRESULT __stdcall GetDomainNeutralAssemblies(ICLRAssemblyReferenceList **ppReferenceList);
};

其主要功能在于 GetHostManager 方法,根据 CLR 传入的 riid,判断并构造相应的宿主监控管理器接口,其他的函数不实现亦可。
以下内容为程序代码:

HRESULT __stdcall CHostControl::GetHostManager(REFIID riid, void **ppObject)
{
if(riid == IID_IHostMemoryManager)
{
std::cout << "IHostControl::GetHostManager(IHostMemoryManager)" << std::endl;

*ppObject = CreateComObject();

return S_OK;
}
else if(riid == IID_IHostTaskManager)
//...
else
{
std::cout << "IHostControl::GetHostManager()" << std::endl;
}

return E_NOTIMPL;
}

HRESULT __stdcall CHostControl::SetAppDomainManager(DWORD dwAppDomainID, IUnknown *pUnkAppDomainManager)
{
std::cout << "IHostControl::SetAppDomainManager()" << std::endl;

return E_NOTIMPL;
}

HRESULT __stdcall CHostControl::GetDomainNeutralAssemblies(ICLRAssemblyReferenceList **ppReferenceList)
{
std::cout << "IHostControl::GetDomainNeutralAssemblies()" << std::endl;

return E_NOTIMPL;
}

这一小节介绍了 CLR 2.0 中的宿主管理接口的相关结构。



<< Home

This page is powered by Blogger. Isn't yours?