`
RednaxelaFX
  • 浏览: 3015073 次
  • 性别: 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指令。

相关推荐

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

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

    c#学习笔记.txt

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

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

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

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

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

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

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

    C#微软培训资料

    2.2 公用语言运行时环境与公用语言规范.13 2.3 开 发 工 具 .17 2.4 小 结 .19 第三章 编写第一个应用程序 .20 3.1 Welcome 程序 .20 3.2 代 码 分 析 .20 3.3 运 行 程 序 .23 .4 添 加 注 释 .25 ...

    .NETFramework2.0中的分析器堆栈遍历:基础知识和高级知识

    本页内容 简介 谨慎地进行堆栈遍历 同步和异步调用 总结 做出最佳的表现 够了够了简介 本文面向的是对构建用于检查托管应用程序的分析器感兴趣的读者。我将描述如何编写分析器,以在.NETFramework的公共语言...

    易语言程序免安装版下载

    修改BUG:超级列表框在属性“整行选择”为真时,鼠标单击第一列右面也会导致第一列中的选择框被选中或取消选中。 21. 修改BUG:Sqlite3数据库支持库中“Sqlite数据库.取错误文本()”返回的文本是UTF-8编码(应是GB...

    c语言编写单片机技巧

    1. C语言和汇编语言在开发单片机时各有哪些优缺点? 答:汇编语言是一种用文字助记符来表示机器指令的符号语言,是最接近机器码的一种语言。其主要优点是占用资源少、程序执行效率高。但是不同的CPU,其汇编语言...

    Visual C++ 2010入门经典(第5版)--源代码及课后练习答案

    在个人实践中,Ivor Horton也是一名系统顾问。他从事程序设计教学工作已经超过了25年。  苏正泉,1995年毕业于解放军信息工程学院计算机及应用专业,高级工程师。在IT项目管理、软件开发、系统管理和网络管理方面都...

    基于AT89S52 单片的频率计

    一般说来,数字系统中运行的电信号,其大小往往并不改变,但在实践分布上 却有着严格的要求,这是数字电路的一个特点。 2 系统的总体设计: 2.1 原理设计 本频率计的设计以AT89S52 单片机为核心,利用它内部的定时/...

    Visual C++2010开发权威指南(共三部分).part1.rar

    1.6 Visual C++ 2010 clr简介 13 1.7 Visual C++ 2010 64位编程 14 1.8 支持新的C++语言标准 14 1.8.1 支持新的C++语言标准(C++ 0x) 14 1.8.2 Lambda表达式 15 1.8.3 静态断言static_assert 17 1.8.4 auto关键字 18 ...

    Visual C++ 2005入门经典--源代码及课后练习答案

    他曾在IBM工作多年,能使用多种语言进行编程(在多种机器上使用汇编语言和高级语言),设计和实现了实时闭环工业控制系统。Horton拥有丰富的教学经验(教学内容包括C、C++、Fortran、PL/1、APL等),同时还是机械、加工...

    Visual C++ 2005入门经典.part08.rar (整理并添加所有书签)

    10.6.1 检查自由存储器的函数 10.6.2 控制自由存储器的调试操作 10.6.3 自由存储器的调试输出 10.7 调试C++/CLI程序 10.8 小结 第11章 Windows编程的概念 11.1 Windows编程基础 11.1.1窗口的元素 11.1.2 Windows程序...

    Visual C++ 2005入门经典.part04.rar (整理并添加所有书签)

    10.6.1 检查自由存储器的函数 10.6.2 控制自由存储器的调试操作 10.6.3 自由存储器的调试输出 10.7 调试C++/CLI程序 10.8 小结 第11章 Windows编程的概念 11.1 Windows编程基础 11.1.1窗口的元素 11.1.2 Windows程序...

    Visual C++ 2005入门经典.part07.rar (整理并添加所有书签)

    10.6.1 检查自由存储器的函数 10.6.2 控制自由存储器的调试操作 10.6.3 自由存储器的调试输出 10.7 调试C++/CLI程序 10.8 小结 第11章 Windows编程的概念 11.1 Windows编程基础 11.1.1窗口的元素 11.1.2 Windows程序...

    Visual C++ 2005入门经典.part09.rar (整理并添加所有书签)

    10.6.1 检查自由存储器的函数 10.6.2 控制自由存储器的调试操作 10.6.3 自由存储器的调试输出 10.7 调试C++/CLI程序 10.8 小结 第11章 Windows编程的概念 11.1 Windows编程基础 11.1.1窗口的元素 11.1.2 Windows程序...

    Visual C++ 2005入门经典.part06.rar (整理并添加所有书签)

    10.6.1 检查自由存储器的函数 10.6.2 控制自由存储器的调试操作 10.6.3 自由存储器的调试输出 10.7 调试C++/CLI程序 10.8 小结 第11章 Windows编程的概念 11.1 Windows编程基础 11.1.1窗口的元素 11.1.2 Windows程序...

    Visual C++ 2005入门经典.part05.rar (整理并添加所有书签)

    10.6.1 检查自由存储器的函数 10.6.2 控制自由存储器的调试操作 10.6.3 自由存储器的调试输出 10.7 调试C++/CLI程序 10.8 小结 第11章 Windows编程的概念 11.1 Windows编程基础 11.1.1窗口的元素 11.1.2 Windows程序...

Global site tag (gtag.js) - Google Analytics