callvirt如何在引擎盖下工作?

时间:2010-11-27 15:39:17

标签: .net clr cil

我试图理解CLR如何实现引用类型和多态。我已经提到了Don Box的Essential .Net第1卷,这对于大部分内容都是很有帮助的。但是当我尝试使用一些IL代码来更好地理解时,我对以下问题感到困惑/困惑。

我会尽力解释这个问题。 请考虑以下代码

class Base
{
    public void m()
    {
        Console.WriteLine("Base.m");
    }
}
class Derived : Base
{
    public void m()
    {
        Console.WriteLine("Derived.m");
    }
}

现在考虑一个简单的控制台应用程序,IL的主要方法如下所示。 我手动调整了编译器创建的IL,以便用ILAsm.exe重新理解和组装

.class private auto ansi beforefieldinit Console1.Program
       extends [mscorlib]System.Object
{
    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       44 (0x2c)
      .maxstack  1
      .locals init ([0] class Console1.Base d)
      nop
      newobj     instance void Console1.Base::.ctor()
      stloc.0
      ldloc.0
      callvirt   instance void Console1.Derived::m()
      nop
      call       string [mscorlib]System.Console::ReadLine()
      pop
      ret
    } // end of method Program::Main
} // end of class Console1.Program

我希望这个代码 NOT 能够运行,因为对象引用指向Base的对象,并且基础对象的方法表无法为方法m提供条目( )在Derived类中定义。

但神奇的是这段代码执行了Derived.m()!!

所以,上面的代码中有两个我不理解的问题:

  1. 以下IL代码中指定的Type的含义是什么?我试图通过将其更改为不同类型(例如System.Exception !!)进行实验,并且不会报告任何错误。为什么?

    .locals init([0]类Console1.Base d)

  2. callvirt的确如何运作?如何将调用路由到Derived.m()?
  3. 提前致谢!!

    此致 阿贾伊

5 个答案:

答案 0 :(得分:5)

我的猜测是,抖动意识到Derived.m不是虚拟的,因此永远不会指向其他地方。因此,callvirt减少为空检查和调用,而不是通过v表进行调用。

尝试虚拟Derived.m。我打赌它会扔掉。

C#编译器发出callvirt指令即使在调用非虚方法时如果它不能证明this!=null,那么它也会得到空检查。在这种情况下,抖动足够智能,可以通过固定地址的普通呼叫替换虚拟呼叫(甚至可以内联)。

你应该检查你的代码是否可以验证。我认为不是。

答案 1 :(得分:2)

您的代码无法验证(通过peverify运行)。我已经写了blog post关于callvirt如何在幕后工作,可能有助于你理解它的作用,以及你的代码如何执行。

请记住,如果作为普通程序运行,CLR会尝试执行不可验证的代码;只有当它确实导致问题时才会发生麻烦。

在您的示例中,在Derived.m()的实例上调用Base是有效的,因为对象实例的实际运行时二进制表示是相同的; this对象基本相同,并且不访问对象的实例字段。

尝试将实例字段访问权限放入两种方法中,看看会发生什么......

答案 2 :(得分:1)

请注意,默认情况下,未验证从本地计算机执行的代码。这意味着可以编写和执行无效代码。我怀疑你的主要功能不会按原样传递。 PEVerify工具可以检查程序集以确保代码是类型安全的,或者您可以通过Security Policy Administration对来自本地计算机或特定位置的代码启用这些检查。

locals语句中类型的目的是声明局部变量的类型。这提供了类型验证程序所需的信息,以验证对局部变量的成员访问是否在正确类型的对象上运行。

Callvirt可以通过多种方式实现。最可能的方式是以相同的方式实现C ++ vtable:一个对象包含一个函数指针表。每个函数都位于表中的预定义偏移处。要调用该函数,将加载并调用预定义偏移量处的地址。请注意,在某些情况下,如果已知对象的类型,CLR可以执行其他优化。这是否已经完成,我不知道。

答案 3 :(得分:1)

我认为这是JIT编译器优化的副作用。如果m()方法是虚拟的,则必须生成机器代码以将方法表指针挖出对象,然后进行虚拟调用。但是这个方法不是虚拟的,JIT编译器已经知道Derived类的方法表指针。因此它绕过指针检索并直接提供它。按照您的观察方式使呼叫正常工作。您可以通过检查生成的机器代码来验证我的猜测。

是的,IL验证者在这里没有得分。通过让Derived.m()方法修改仅在Derived中声明的字段,可以使其更有趣。我已经看到太多的Reflection.Emit代码崩溃与AccessViolation相比,这是非常惊讶。然而,它可能是有意的,无需验证IL无论如何都会崩溃。不确定,利用这种验证漏洞尚不常见。值得庆幸的是

答案 4 :(得分:0)

有关这方面的工作原理的更多信息,请查看此StackExchange问​​题/答案: How does the callvirt .NET instruction work for interfaces?