CIL之——call和callvirt的区别

CIL代码中调用方法的常用方法有call和callvirt两种,那它们到底有什么区别呢?下面让我们通过例子来看一看吧!

CIL代码准备工作

首先定义一个名为Person的基类,其中有一个介绍自己的虚方法,然后从Person派生出Guo和Yoyo,而且对其中介绍自己的方法做了如代码中的处理,一个在CIL的层面上未作处理(其实是省略了.override),另一个为虚方法增加了newslot属性。

.namespace ConsoleTest
{   
    .class public Person
    {
        .method public void .ctor()
        {
            .maxstack 1
            ldarg.0
            call instance void [mscorlib]System.Object::.ctor()
            ret
        }       
        .method public virtual void Introduce()
        {
            .maxstack 1
            ldstr "this is Person"
            call void [mscorlib]System.Console::WriteLine(string)
            ret
        }
    }
    .class public Guo extends ConsoleTest.Person
    {
        .method public void .ctor()
        {
            .maxstack 1
            ldarg.0
            call instance void [mscorlib]System.Object::.ctor()
            ret
        }
        .method public virtual void Introduce()
        {
            .maxstack 1
            ldstr "this is Guo"
            call void [mscorlib]System.Console::WriteLine(string)
            ret
        }
    }
    .class public Yoyo extends ConsoleTest.Person
    {
        .method public void .ctor()
        {
            .maxstack 1
            ldarg.0
            call instance void [mscorlib]System.Object::.ctor()
            ret
        }
        //此处使用newslot属性或者说标签,标志着该方法脱离了基类虚函数的那一套链,等同于C#中的new
        .method public newslot virtual void Introduce()
        {
            .maxstack 1
            ldstr "this is Yoyo"
            call void [mscorlib]System.Console::WriteLine(string)
            ret
        }
    }
}

编译时类型和运行时类型

在进行下文之前先要介绍一下编译时类型和运行时类型这两个概念,因为这和我们的方法调用息息相关。
举个C#的例子来说明这个问题:

public abstract class Band{ }
public class Beyond:Band{ }
class Program
{
    public static void Main(string[] args)
    {
        Band obj = new Beyond();
    }
}

对于编译器来说,变量的类型就是它被声明时的类型。在此变量obj的类型被定义为Band,也就是说obj的编译时类型是Band。
但我们之后实例化了一个Beyond类型的实例,并且将这个实例的引用赋值给了变量obj,因此在这段程序运行的时候,编译阶段被定义为Band类型的变量obj指向的是一块存储了Beyond类型的实例的内存。换言之,此时obj的运行时类型的Beyond。
那么编译时类型和运行时类型跟我们上面的CIL代码有什么关系呢?下面让我们通过对比来看一下!

call vs callvirt

下面我们将使用CIL代码来实现这个对比。
首先我们要声明3个局部变量来分别存储这三个类的实例,这3个变量是作为运行时类型存在的。
其次分别使用call和callvirt来调用各个类的方法,此时调用的类的类型充当的是编译时类型。

.method public static void Main(string[] args)
{
    .entrypoint
    .maxstack 3
    .locals init(
        class ConsoleTest.Person objPerson,
        class ConsoleTest.Guo objGuo,
        class ConsoleTest.Yoyo objYoyo
    )
    newobj instance void ConsoleTest.Person::.ctor()
    stloc objPerson
    newobj instance void ConsoleTest.Guo::.ctor()
    stloc objGuo
    newobj instance void ConsoleTest.Yoyo::.ctor()
    stloc objYoyo
    //Person
    //编译时类型为Console.Person,运行时类型为Console.Person,使用call
    ldloc objPerson
    call instance void ConsoleTest.Person::Introduce()
    //编译时类型为Console.Person,运行时类型为Console.Person,使用callvirt
    ldloc objPerson
    callvirt instance void ConsoleTest.Person::Introduce()

    //Guo
    //编译时类型为Console.Guo,运行时类型为Console.Guo,使用call
    ldloc objGuo
    call instance void ConsoleTest.Guo::Introduce()
    //编译时类型为Console.Guo,运行时类型为Console.Guo,使用callvirt
    ldloc objGuo
    callvirt instance void ConsoleTest.Guo::Introduce()
    //编译时类型为Console.Person,运行时类型为Console.Guo,使用call
    ldloc objGuo
    call instance void ConsoleTest.Person::Introduce()
    //编译时类型为Console.Person,运行时类型为Console.Guo,使用callvirt
    ldloc objGuo
    callvirt instance void ConsoleTest.Person::Introduce()

    //Yoyo
    //编译时类型为Console.Yoyo,运行时类型为Console.Yoyo,使用call
    ldloc objYoyo
    call instance void ConsoleTest.Yoyo::Introduce()
    //编译时类型为Console.Yoyo,运行时类型为Console.Yoyo,使用callvirt
    ldloc objYoyo
    callvirt instance void ConsoleTest.Yoyo::Introduce()
    //编译时类型为Console.Person,运行时类型为Console.Yoyo,使用call
    ldloc objYoyo
    call instance void ConsoleTest.Person::Introduce()
    //编译时类型为Console.Person,运行时类型为Console.Yoyo,使用callvirt
    ldloc objYoyo
    callvirt instance void ConsoleTest.Person::Introduce()

    call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
    pop
    ret
}

好了,我们PK的擂台已经搭好了。如果有兴趣的话,各位此时可以对照各个方法猜一下输出结果。
不过在正式揭晓结局之前,我先了总结一下这个过程:Person类作为基类,有一个虚函数Introduce。然后Guo类派生自Person,同时Guo类也有一个同名的虚函数Introduce,此时可以认为它重载了基类的同名方法。我为了对比的更有趣,又定义了一个派生自Person的Yoyo类,同样它也有一个同名的虚函数Introduce,唯一的不同就是此方法使用了newslot属性。
下面我们来揭晓结果:
这里写图片描述
我们将代码和结果一一对应,可以发现凡是使用call调用方法的:

  • call instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Person。输出:this is Person,调用了Person中定义的Introduce方法
  • call instance void ConsoleTest.Guo::Introduce() 编译时类型Guo,运行时类型Guo。输出:this is Guo,调用了Guo中定义的Introduce方法
  • call instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Guo。输出:this is Person,调用了Person中定义的Introduce方法
  • call instance void ConsoleTest.Yoyo::Introduce() 编译时类型Yoyo,运行时类型Yoyo。输出:this is Yoyo,调用了Yoyo中定义的Introduce方法
  • call instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Yoyo。输出:this is Person,调用了Person中定义的Introduce方法

从实际运行结果可以看出,call对变量的运行时类型不感兴趣,只对编译时类型的方法感兴趣,所以此处call只会调用变量编译时类型中定义的方法。为了方便大家观察结果,我已经把对应的关键字加粗。

而使用了callvirt来调用方法的:

  • callvirt instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Person。输出:this is Person,调用了Person中定义的Introduce方法
  • callvirt instance void ConsoleTest.Guo::Introduce() 编译时类型Guo,运行时类型Guo。输出:this is Guo,调用了Guo中定义的Introduce方法
  • callvirt instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Guo。输出:this is Guo,调用了Guo中定义的Introduce方法
  • callvirt instance void ConsoleTest.Yoyo::Introduce() 编译时类型Yoyo,运行时类型Yoyo。输出:this is Yoyo,调用了Yoyo中定义的Introduce方法
  • callvirt instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Yoyo。输出:this is Person,调用了Person中定义的Introduce方法

从实际运行结果可以看出,callvirt对变量的编译时类型不感兴趣,只对运行时类型的方法感兴趣,所以此处callvirt只会调用变量运行时类型中定义的方法。为了方便大家观察结果,我已经把对应的关键字加粗。

不过有一个例外,当编译时类型为Person,运行时类型为Guo,使用callvirt去调用Person::Introduce()时,执行的是子类Guo中的Introduce方法;
当编译时类型为Person,运行时类型为Yoyo,使用callvirt去调用Person::Introduce()时,执行的却是基类Person中定义的Introduce方法;
这是因为Yoyo类中的Introduce方法使用了newslot属性,所以此处才看到了截然不同的结果。

这个涉及到了虚函数的设计,简单来说可以想象成同一系列的虚函数(使用override关键字修饰)存放在一个槽(slot)中,在运行时会将没有使用newslot属性的虚函数放入这个槽中,在需要调用虚函数时就去这个槽中寻找符合条件的虚函数执行,那这个槽是谁定义的呢或者说如何去定位正确的槽呢?不错,就是通过基类。

如果有兴趣,各位可以把虚函数部分的C#代码编译成CIL代码,这样就能看到调用派生类重载的虚函数,在CIL中其实都是使用callvirt instance xxx baseclass:func 来实现的。所以使用了newslot属性的方法并没有放入到基类定义的那个槽中,而是自己定义了一个新的槽,所以

ldloc objYoyo 
callvirt instance void ConsoleTest.Person::Introduce()

只能调用基类中的Introduce方法。

解惑:CIL代码中的虚函数

先写一段C#代码:

namespace CILTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Person objPerson1 = new Guo();
            objPerson1.Introduce();
            Person objPerson2 = new Yoyo();
            objPerson2.Introduce();
            Guo objGuo = new Guo();
            objGuo.Introduce();
            Yoyo objYoyo = new Yoyo();
            objYoyo.Introduce();
            Console.ReadKey();
        }
    }
    public class Person
    {
        public Person()
        {
        }
        public virtual void Introduce()
        {
            Console.WriteLine("this is Person");
        }
    }
    public class Guo:Person
    {
        public Guo()
        {            
        }
        public override void Introduce()
        {
            Console.WriteLine("this is Guo");
        }
    }
    public class Yoyo : Person
    {
        public Yoyo()
        {
        }
        public new void Introduce()
        {
            Console.WriteLine("this is Yoyo");
        }
    }
}

Main方法对应的CIL代码截图如下:
这里写图片描述

通过分析CIL代码发现,调用派生类重载的虚函数,确实是使用callvirt instance xxx baseclass:func 来实现的。如果派生类重载的虚函数加了new关键字,那就会变为使用callvirt instance xxx class:func来实现。

发放源码

对比call和callvirt所用到的完整的il代码如下:

.assembly extern mscorlib
{
    .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
    .ver 4:0:0:0
}
.assembly GuoAssembly
{
    .ver 0:0:0:0
}
.module GuoModule
.namespace ConsoleTest
{   
    .class public Program extends [mscorlib]System.Object
    {
        .method public void .ctor()
        {
            .maxstack 8
            ldarg.0
            call instance void [mscorlib]System.Object::.ctor()
            ret
        }
        .method public static void Main(string[] args)
        {
            .entrypoint
            .maxstack 3
            .locals init(
                class ConsoleTest.Person objPerson,
                class ConsoleTest.Guo objGuo,
                class ConsoleTest.Yoyo objYoyo
            )
            newobj instance void ConsoleTest.Person::.ctor()
            stloc objPerson
            newobj instance void ConsoleTest.Guo::.ctor()
            stloc objGuo
            newobj instance void ConsoleTest.Yoyo::.ctor()
            stloc objYoyo

            ldloc objPerson
            call instance void ConsoleTest.Person::Introduce()
            ldloc objPerson
            callvirt instance void ConsoleTest.Person::Introduce()

            ldloc objGuo
            call instance void ConsoleTest.Guo::Introduce()
            ldloc objGuo
            callvirt instance void ConsoleTest.Guo::Introduce()
            ldloc objGuo
            call instance void ConsoleTest.Person::Introduce()
            ldloc objGuo
            callvirt instance void ConsoleTest.Person::Introduce()

            ldloc objYoyo
            call instance void ConsoleTest.Yoyo::Introduce()
            ldloc objYoyo
            callvirt instance void ConsoleTest.Yoyo::Introduce()
            ldloc objYoyo
            call instance void ConsoleTest.Person::Introduce()
            ldloc objYoyo
            callvirt instance void ConsoleTest.Person::Introduce()

            call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
            pop
            ret
        }
    }
    .class public Person
    {
        .method public void .ctor()
        {
            .maxstack 1
            ldarg.0
            call instance void [mscorlib]System.Object::.ctor()
            ret
        }

        .method public virtual void Introduce()
        {
            .maxstack 1
            ldstr "this is Person"
            call void [mscorlib]System.Console::WriteLine(string)
            ret
        }
    }
    .class public Guo extends ConsoleTest.Person
    {
        .method public void .ctor()
        {
            .maxstack 1
            ldarg.0
            call instance void [mscorlib]System.Object::.ctor()
            ret
        }
        .method public virtual void Introduce()
        {
            .maxstack 1
            ldstr "this is Guo"
            call void [mscorlib]System.Console::WriteLine(string)
            ret
        }
    }
    .class public Yoyo extends ConsoleTest.Person
    {
        .method public void .ctor()
        {
            .maxstack 1
            ldarg.0
            call instance void [mscorlib]System.Object::.ctor()
            ret
        }
        .method public newslot virtual void Introduce()
        {
            .maxstack 1
            ldstr "this is Yoyo"
            call void [mscorlib]System.Console::WriteLine(string)
            ret
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

changuncle

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值