前面有关显式访问内存的讨论,展示了CLR类型系统和指令集是如何丰富。托管和非托管指针的使用是程序员高效访问内存的途径,同时还不必牺牲CLR提供的服务。现在,我们将明确地讨论CLR如何支持那些服务,以及什么时候(与为什么)要绕过那些服务。
CLR是基于一个简单的前提:这就是CLR是无所不如(omniscient)、无所不能(omnipotent)。CLR具备通晓运行程序所有方面的能力。这就是元数据至关重要的原因所在,国为元数据是CLR理解内存中对象和值之间关系的关键。此外,CLR还需要具备管理和控制运行程序各个方面的能力,这是托管执行的关键。
CLR支持两种执行模式:托管执行和非托管执行。在托管执行(managed execution)模式下,CLR可以询问每一个线程的每一个堆栈帧(stack frame)这种能力包括能够调查局部变量和参数,查找用于每一个堆栈帧的方法代码和元数据,侦查到哪一个在堆栈中的对象引用是“存活的”,以及哪一个不再需要,以及在堆压缩之后调整存活对象引用。简而言之,托管执行模式使你的程序对CLR完全透明。
相反,非托管执行(unmanaged execution)模式CLR表现出无知以及无能为力的一面。当运行在非托管执行模式下,CLR不能从调用堆栈中收集到任何有意义的信息:除了简单地挂起运行线程之外,它对执行代码无所适从。就CLR而言,非托管执行代码就是模糊的黑箱,CLR对其敬而远之。
你可以基于方法调用改变执行模式,也就是用CLR的元数据对每一个方法进行标记(managed或者unmanaged):VB.NET和C#编译器只能发射managed方法。c++编译器在使用/CLR开关时,默认情况下发射managed方法。然而,C++编译器也支持发射非托管的代码。当C++方法体包含内联的IA-32汇编语句或者setjmp/longjmp调用(这两种都使托管执行无法实行)时,编译器将自动发射非托管的方法。可以通过在源程序中使用#pragma managed和#pragma unmanaged指令,显式地控制方法模式。
CLR对非托管方法是无知的,因此,非托管方法不能使用CLR对象,这是因为CLR垃圾回收器不能检测到它们的存在,也无法在堆压缩时调整它们的位置。这意味着下面的c++代码无法被编译:
void f(){
//这些语句需要托管模式
System::Object *obj=new System::Object();
System::Console::WriteLine(obj- >ToString());
//这条语句需要非托管模式
__asm
{
push eax
xor eax,eax
pop eax
}
}
为了使这段代码能够工作,需要将代码的两个区域划分为两个方法,它们的模式各自反映出代码的不同需求。如下所示:
#pragma managed
void f(){
f1();// call managed code
f2(); //call unmanaged code
}
#pragma managed
void f1(){
System::Object *obj=new System::Object();
System::Console::WriteLine(obj- >ToString())
}
#pragma unmanaged
void f2()
{
__asm
{
push eax
xor eax,eax
pop eax
}
}
注意,#pragma的使用是可选的,因为c++编译器将基于方法体是否使用__asm或者Setjmp/longjmp来设置模式。
在托管方法和非托管方法的方法体之间没有本质的差别。方法的序言(Prolog)和尾声(cpilog)看起来都是一样的,实际上即将执行的本机代码也是同样的。托管方法和非托管方法之间的差别是:CLR能够推断用于托管方法的堆栈帧的所有信息,相反,CLR无法推断非托管方法有关其堆栈帧的信息。这里需要注意,在方法调用过程中,推断有关托管堆栈帧的能力不需要另外的指令。准确地说,从某个托管方法到另一个托管方法的调用,与从传统C函数上的调用是难以区分的。然而,因为CLR控制托管方法的序言和尾声,所以,CLR能够可靠地遍历调堆栈的托管区域,经常(但不是总是)使用大多数调试器用到的1A-32的ebp寄存器。由于CLR只有在诸如安全要求、垃圾回收和异常处理等相对比较少出现的事件中才需要堆栈检查,所以,托管代码的一般情形的代码路径与传统c编译器生成的代码路径难以区分。
如上所述,相同种类、相同模式的调用与c风格的函数调用差别另不大,相反,交叉模式(cross-mode),即不同种类的调用则不那么简单。当托管方法调用非托管方法,或者非托管方法调用托管方法时,交叉模式调用就发生了,不管哪种情形,用于调用所发射的代码与普通相同模式所发射的代码大相径庭。
交叉模式调用需要执行额外的工作,标识执行语义的变化。其中,调用方需要将一个标记(sentinel)压入堆栈,在堆栈帧中标明新调用链的起始点。CLR将堆栈帧划分为链(chain)。每个链表示一系列的相同模式的方法调用。当JIT编译器编译交叉模式调用时,它就发射另外的代码,用于将额外的转换帧(transition frame)压入堆栈中,作为-个标记。如图10.2所示,
每一个转换帧包含一个向后指针,指向前面链开始的转换帧。这些转换帧允许CLR高效地跳过它不关心的区域,也就是非托管链中的帧,在转换帖压入堆栈之后,调用方组建目标方法所期望的常规堆栈帧。注意,这个技术将导致交叉模式方法调用两个堆栈帧,且每个模式一个帧。
过渡转换程序为交叉目标方法准备堆栈之后,转换程序必须调整当前线程的执行状态,以反射执行模式的改变。在线程的本地存储区,将指向新形成的转换帧的指针缓存起来,这也是准备工作的一部分,此外,过渡转换程序必须在线程的本地存储区切换一个标志位,以标明线程的当前执行模式。当该位被设置时,线程就以托管的执行模式运行;当该位被清除时,线程就以非托管的执行模式运行。在过渡转换程序准备好线程状态之后,转换程序就跳到目标方法体。当目标方法返回时,它返回到另外的转换代码,将线程状志复位,并从堆栈中弹山转换帧。对于带有简单方法签名的调用,生成转换的整个开销是32条IA-32指令。因为在形成交叉模式调用时需要在转换帧之后建立第二个堆栈帧,所以,交叉模式调用的开销依赖于传递给方法的参数的数量和类型。参数的数量越人,进行转换的开销也就越大。