zoukankan      html  css  js  c++  java
  • .Net平台下CLR程序载入原理分析

      与传统的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之前,必须通过一个tartup 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引擎实例中所有的应用域.   当前应用域是指当前线程运行时所在应用域.注意线程属于进程,但不属于某个应用域, 一个线程可以跨应用域操作.可以通过线程类的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 Funct
     ion 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, 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<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_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>
             /// 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, 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<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 Funct
     ion 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, b
     strClassName);
           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_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;
     }

  • 相关阅读:
    【转】CSR蓝牙驱动程序引起的Win7奇怪问题
    c# .net WebRequest 始终报域名无法解析
    sql server 安装时提示要重启
    https 不检验证书
    windows 日志,IIS应用程序池回收日志
    excel sum
    .net core 连接sql server 时提示Connection Timeout Expired
    python2.0_day20_bbs系统开发
    SVN常用命令与分支操作
    SVN使用教程之-分支/标记 合并 subeclipse
  • 原文地址:https://www.cnblogs.com/answercard/p/1373372.html
Copyright © 2011-2022 走看看