我试图理解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()!!
所以,上面的代码中有两个我不理解的问题:
以下IL代码中指定的Type的含义是什么?我试图通过将其更改为不同类型(例如System.Exception !!)进行实验,并且不会报告任何错误。为什么?
.locals init([0]类Console1.Base d)
提前致谢!!
此致 阿贾伊
答案 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?