`
RednaxelaFX
  • 浏览: 3048959 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

CLR上的接口调用也是在运行时检查的

阅读更多
作者:RednaxelaFX
主页:http://rednaxelafx.iteye.com
日期:2009-06-02

系列笔记:
JVM在校验阶段不检查接口的实现状况
为什么JVM与CLR都不对接口方法调用做静态校验?

刚才的一帖,JVM在校验阶段不检查接口的实现状况,我提到JVM在处理invokeinterface时,如果遇到被调用对象没有实现指定的接口时,在运行时抛出异常的行为。那么.NET的CLR在这方面的行为又如何呢?

CLR对MSIL的校验是与JIT同时进行的。JIT编译器一边检查代码的正确性,一边生成native code;一个方法被调用时,如果还没有JIT过的话,要在JIT之后才进入运行状态。于是CLR执行托管方法可以分为一个[加载-链接-校验-JIT]的阶段与一个[执行]的阶段。与JVM一样,如果CLR遇到被调用对象没有实现指定的接口的状况,也是在运行时抛出异常的。

要观察这个行为也同样需要对MSIL做些操作。先用这个源码来编译得到exe:
TestInterfaceCall.cs
using System;

static class TestInterfaceCall {
    static void Main(string[] args) {
        IFoo f = new FooImpl();
        f.Method();

        Bar b = new Bar();
        ((IFoo)b).Method(); // << watch this
    }
}

public interface IFoo {
    void Method();
}

public class FooImpl : IFoo {
    public virtual void Method() {
        Console.WriteLine("FooImpl.Method()");
    }
}

public class Bar {
    public virtual void AnotherMethod() {
        Console.WriteLine("Bar.AnotherMethod()");
    }
}


然后利用ILDASM将其反编译为MSIL:
TestInterfaceCall.il
//  Microsoft (R) .NET Framework IL Disassembler.  Version 3.5.30729.1
//  Copyright (c) Microsoft Corporation.  All rights reserved.



// Metadata version: v2.0.50727
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 2:0:0:0
}
.assembly TestInterfaceCall
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                             63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module TestInterfaceCall.exe
// MVID: {075D6351-0AF0-459F-A822-40E817B58699}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x03010000


// =============== CLASS MEMBERS DECLARATION ===================

.class private abstract auto ansi sealed beforefieldinit TestInterfaceCall
       extends [mscorlib]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       33 (0x21)
    .maxstack  1
    .locals init (class IFoo V_0,
             class Bar V_1)
    IL_0000:  nop
    IL_0001:  newobj     instance void FooImpl::.ctor()
    IL_0006:  stloc.0
    IL_0007:  ldloc.0
    IL_0008:  callvirt   instance void IFoo::Method()
    IL_000d:  nop
    IL_000e:  newobj     instance void Bar::.ctor()
    IL_0013:  stloc.1
    IL_0014:  ldloc.1
    IL_0015:  castclass  IFoo
    IL_001a:  callvirt   instance void IFoo::Method()
    IL_001f:  nop
    IL_0020:  ret
  } // end of method TestInterfaceCall::Main

} // end of class TestInterfaceCall

.class interface public abstract auto ansi IFoo
{
  .method public hidebysig newslot abstract virtual 
          instance void  Method() cil managed
  {
  } // end of method IFoo::Method

} // end of class IFoo

.class public auto ansi beforefieldinit FooImpl
       extends [mscorlib]System.Object
       implements IFoo
{
  .method public hidebysig newslot virtual 
          instance void  Method() cil managed
  {
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "FooImpl.Method()"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method FooImpl::Method

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method FooImpl::.ctor

} // end of class FooImpl

.class public auto ansi beforefieldinit Bar
       extends [mscorlib]System.Object
{
  .method public hidebysig newslot virtual 
          instance void  AnotherMethod() cil managed
  {
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Bar.AnotherMethod()"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Bar::AnotherMethod

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method Bar::.ctor

} // end of class Bar


// =============================================================

// *********** DISASSEMBLY COMPLETE ***********************
// WARNING: Created Win32 resource file D:\experiment\test\TestInterfaceCall.res

与前一帖一样,这里我们要避免castclass指令干扰测试的结果。把Main()里的IL_0015那行注释掉,然后再用ILASM重新编译为TestInterfaceCall.exe。

运行结果如下:
D:\experiment\test>TestInterfaceCall.exe
FooImpl.Method()

Unhandled Exception: System.EntryPointNotFoundException: Entry point was not found.
   at IFoo.Method()
   at TestInterfaceCall.Main(String[] args)

可以看到,这一点上CLR与JVM的行为一致。

===================================================================

CLR 2.0之前是用全局的接口查找表来做接口方法调用的分发(dispatch)的。到2.0之后,CLR改用基于stub的方式来做接口方法调用分发。对接口方法的调用点,JIT一开始会生成一个间接调用指向一个lookup stub,后者又指向一个resolver stub,用于查找实际的方法实现;对同一个被调用对象类型成功调用2次之后会生成特化于那个类型的dispatch stub(是一个monomorphic inline cache),然后记录该stub调用失败的次数,达到100次之后就退化回到非特化的resolver stub。
如果被调用对象没有实现指定的接口,则在调用点初次被调用时,进到resolver stub之后会发现找不到接口方法的具体实现,然后就抛出异常。

===================================================================

值得注意的是,这两帖里提到的“接口方法调用”都是指正常的、直接的调用,而不是通过反射去做的调用。如果把这帖里的代码例子中第9行的((IFoo)b).Method();改为反射调用:
typeof(IFoo).GetMethod("Method").Invoke(b, new object[0]);

则编译和校验都不会出现问题,而运行时抛出的异常会是这样的:
Unhandled Exception: System.Reflection.TargetException: Object does not match target type.
   at System.Reflection.RuntimeMethodInfo.CheckConsistency(Object target)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at TestInterfaceCall.Main(String[] args)

与直接在IL里去调用接口方法不同。这是因为反射本身就是通过托管代码实现的,也就是说在进到虚拟机底层之前,在托管代码里就已经做了很多检查,这些检查发现所要调用的方法与实际提供的被调用对象不匹配,于是在托管代码的层次上就抛出了异常。
分享到:
评论
2 楼 RednaxelaFX 2009-06-03  
幸存者 写道
不知接口方法和虚方法分发有什么区别?似乎在CIL中都是callvirt指令。

对,MSIL里都是callvirt,但JIT的时候得到了不同的处理:

对虚方法的分发是编译成这样:
mov  ecx, esi              ; 假设现在ESI是一个指向对象实例的指针,复制到ECX里
mov  eax, dword ptr [ecx]  ; 对象实例的第一项是指向方法表的指针,复制到EAX里
call dword ptr [eax + 7Ch] ; EAX现在指向方法表,0x7C是我随便写的一个偏移量。这个在加载类的时候就可以确定


对接口方法的分发是编译成:
mov  ecx, esi             ; 跟上面一样,ESI是指向对象的指针,复制到ECX
call dword ptr [0099EEA0h] ; JIT的时候指向一个固定地址的stub(这里数字是随便编的,别在意)

这个call是对一个固定地址做间接调用。一开始是调用到一个通用的resolver stub,去找具体的方法的地址。如果在同一个调用点出现两次相同类型的被调用对象,则间接调用会指向为该类型特化的一个dispatch stub上,样子类似这样:
cmp dword ptr [ecx], 009A3377h ; 后面的常量是特定类型的方法表地址
jne 0091A012                   ; 比较不相等的话,跳转到一个特定的resolver stub上
jmp 00EBD070                   ; 相等的话则直接跳转到目标方法的地址

这段代码被称为monomorphic inline method cache,怎么翻译好呢……单态内联方法缓存?
比较失败时会跳转到一个resolver stub;它会维护一个“不命中计数器”。如果在某个调用点累计失败了100次,就会再次更新之前的间接调用为直接指向通用的resolver stub。

这样,如果在一个接口方法的调用点上总是同一个类型的实例被调用,则分发效率跟虚方法调用差不多快。如果被调用对象的类型经常变,速度就会慢下来了……
1 楼 幸存者 2009-06-03  
不知接口方法和虚方法分发有什么区别?似乎在CIL中都是callvirt指令。

相关推荐

    MFC和CLR的结合用法

    MFC是一套由微软提供的C++类库,用于简化Windows API的使用,而CLR是.NET Framework的核心组成部分,它提供了跨语言的运行时环境,支持多种编程语言。 MFC是微软为Windows应用程序设计的面向对象的类库,它封装了...

    CLR项目开发文档.rar

    CLR(Common Language Runtime)是.NET框架的核心组成部分,它为各种编程语言提供了运行环境,使得开发者可以用不同的语言编写代码,然后在CLR上统一运行。在这个"CLR项目开发文档.rar"中,我们可以期待找到关于如何...

    CLR详解

    - 平台独立:可以在任何支持CLR的平台上运行。 - 可延迟编译:在程序运行时才进行最终编译。 - 支持类型安全性:CLR会在编译过程中进行类型检查。 - **IL的生成过程**: - .NET编译器将源代码编译成IL。 - IL...

    CLRInsideOut2008_01.7z

    1. **CLR(Common Language Runtime)**:CLR是.NET Framework的核心组成部分,它提供了跨语言运行时环境,使得多种编程语言如C++、C#等能在同一平台上运行。CLR执行诸如类型安全检查、内存管理、异常处理等任务。 ...

    框架设计 CLR Via C# 中文版

    《框架设计 CLR Via C#》是一本深入探讨.NET框架核心组件——公共语言运行时(Common Language Runtime, CLR)的专业书籍。这本书通过C#语言详细阐述了CLR的工作原理、设计思想以及如何利用它来构建高效、稳定的软件...

    CLR.rar_bios.h graphic.h_clr_clr C++

    综上所述,这个压缩包提供的是一个与.NET Framework的CLR接口相关的C++库,包括了对BIOS和图形功能的头文件引用,以及可能用于在C++中调用.NET服务的C语言函数实现。开发者可以利用这些资源来开发混合模式的应用程序...

    clr+via+c#

    《CLR via C#》是微软资深开发人员Jeffrey Richter所著的一本经典书籍,主要讲解.NET Framework的公共语言运行时(Common Language Runtime, CLR)以及如何通过C#语言进行深入理解和利用。这本书的第三版提供了最新...

    CLR via C# 第四版示例源码

    6. **反射**:反射允许程序在运行时动态地获取类型信息并操作类型。源码将展示如何使用反射创建对象、调用方法和访问属性。 7. **元数据和IL(中间语言)**:.NET程序编译后会产生元数据,这些数据描述了类型和成员...

    C# CLR原理与线程池详解

    - **示例**:使用反射,可以在运行时创建对象、调用方法或获取属性值,这对于实现灵活的编程模型非常重要。 #### 八、NameSpace(命名空间) - **定义**:命名空间是.NET框架中用于组织类和其他类型的容器。 - **...

    CLR.via.C#.(中文第3版)(自制详细书签)

    《CLR via C#(第3版) 》针对.NET Framework 4.0和多核编程进行了全面更新和修订,是帮助读者深入探索和掌握公共语言运行时、C#和.NET开发的重要参考,同时也是帮助开发人员构建任何一种应用程序(如Microsoft ...

    讨论C#的一些东西,CLR等

    这使得C#程序可以在多种操作系统上运行,只要安装了.NET框架。 在C#中,类和对象是面向对象编程的基础。类定义了对象的属性(数据成员)和行为(方法)。C#支持封装、继承和多态性这三种面向对象的基本原则。封装...

    c#学习笔记.txt

    当接口具有一个或多个显式基接口时,在该接口声明中,接口标识符后跟一个冒号以及由逗号分隔的基接口标识符列表。接口的基接口是显式基接口及其基接口。换言之,基接口集是显式基接口、它们的显式基接口(依此类推)...

    CLR VIA C PDF下载

    通过CLR,不同的编程语言可以在相同的运行时环境中无缝交互,实现跨语言互操作性。 2. "Hello, world"程序 学习任何编程语言的第一步通常是从编写简单的"Hello, world"程序开始。在C#中,这通常涉及定义一个包含...

    SQL Server 2005中的CLR应用研究.pdf

    2. **强类型和类型安全**:CLR提供了强类型检查和类型安全机制,能够防止运行时类型转换错误和内存溢出问题。 3. **异常处理**:CLR支持结构化异常处理,使得错误处理更加规范和高效,避免了T-SQL中的TRY...CATCH块...

    C#编写Dll供C++调用

    - 在调用COM组件时,错误处理是不可或缺的,需要检查接口创建和方法调用的返回状态,确保能够处理异常情况。 - C#创建的托管代码和C++原生代码之间存在差异,需要注意内存管理和异常处理的异同,确保程序的稳定性...

    MFC调用WebService(托管)-vc.net2005

    而托管代码则是.NET Framework中的一个概念,指的是运行在.NET CLR(Common Language Runtime)之上的代码。 首先,让我们理解Web服务的基本概念。Web服务是一种基于网络的、松散耦合的软件组件,它通过标准协议如...

    微软常用运行库合集v2023.05.15.zip

    当程序启动时,它会调用运行库中的函数来完成特定任务,如内存管理、图形渲染、网络通信等。这样,开发者可以专注于编写应用程序的核心逻辑,而不是重复实现基础功能。 **Windows运行库的重要性:** 1. **兼容性** ...

    C#调用windowsAPI.pdf

    这意味着,尽管C#运行在公共语言运行时(CLR)上,但通过`DllImport`我们可以与非托管的DLL进行交互。例如,调用User32.dll中的MessageBox函数可以这样写: ```csharp [DllImport("user32.dll", EntryPoint = ...

    微软常用运行库合集v2022.10.04.zip

    当你的计算机上缺少某些运行库时,可能会导致一些程序无法正常启动或运行。微软的运行库合集通常包括以下重要组件: 1. Microsoft Visual C++ Redistributable: 这是一系列用于执行使用Visual C++编译器开发的应用...

    重温C# clr 笔记总结

    JIT(Just-In-Time)编译是CLR的另一个重要特性,它会在方法首次运行时将中间语言(IL)编译成机器码。这导致了第一次调用时的性能损失,但之后的调用会快速且高效,因为已经转换成了本地代码。 方法签名是方法的...

Global site tag (gtag.js) - Google Analytics