首页 > 编程 > .NET > 正文

.Net平台下CLR程序载入原理分析

2024-07-10 13:02:14
字体:
来源:转载
供稿:网友
flier lu <[email protected]>
  
注意:本系列文章在水木清华bbs(smth.org)之.net版首发,
     转载请保留以上信息,发表请与作者联系
  
  与传统的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.net/framework
(%windir%表示windows系统目录所在位置)目录下,
我们可以看到以版本号为目录名的多个clr版本,
如%windir%/microsoft.net/framework/v1.0.3705等等,
也可以在注册表的
hkey_local_machine/software/microsoft/.netframework/policy/v1.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] << " <assembly name> <class name> <main
function name> <parameters>" << 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,
bstrclassname);
  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<argcount; i++)
    {
      variantinit(&pargs[i]);
      pargs[i].vt = vt_bstr;
      pargs[i].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_method,
      &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>
    /// summary description for class1.
    /// </summary>
    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 <mscoree.h>
  
#import <mscorlib.tlb> rename("reportevent", "reportevent_")
using namespace mscorlib;
  
#include <assert.h>
  
#include <string>
#include <memory>
#include <iostream>
using namespace std;
  
typedef hresult (__stdcall * getinfofunc)(lpwstr pbuffer, dword cchbuffer,
dword* dwlength);
  
#define check(v) /
  if(failed(v)) /
    cerr << "com function call failed - " << getlasterror() << " at " <<
__file__ << ", " << __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<icorruntimehost> 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<iunknown> 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] << " <assembly name> <class name> <main
function name> <parameters>" << 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, bstrclassname);
      ccomptr<idispatch> 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<argcount; i++)
        {
          variantinit(&pargs[i]);
          pargs[i].vt = vt_bstr;
          pargs[i].bstrval = _bstr_t(argv[4+i]);
        }
      }
  
      check(spdisp->getidsofnames(iid_null, &rgszname, 1,
locale_system_default, &dispid));
      check(spdisp->invoke(dispid, iid_null, locale_system_default,
dispatch_method,
        &dispparamsargs, null, null, null));
      delete[] dispparamsargs.rgvarg;
    }
  }
  
  check(sphost->stop());
  
    return 0;
}
  
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表