将callvirt更改为在IL中呼叫

本文关键字:IL 呼叫 callvirt | 更新日期: 2023-09-27 18:07:33

在IL的一些实验中,我试图将程序集中的callvirt调用更改为call方法。基本上,我调用的成员函数有一个继承链。

基本上调用类似于:

((MyDerivedClass)myBaseObject).SomeCall();

或IL

castclass MyDerivedClass   // ** 1
call SomeCall()            // ** 2

基类将SomeCall定义为抽象方法,派生类实现它。派生类是密封的。

我知道callvirt基本上相当于检查对象是否为空,如果它不是使用虚值表调用方法,如果是,则抛出异常。在我的情况下,我知道它从来没有null,我知道这是我想调用的实现。我理解为什么在这种情况下你通常需要一个callvirt

也就是说,因为我知道对象永远不会为空,并且总是派生类型的实例,所以我认为这不是问题:

  • 当你考虑数据和类型是分开的,我实际上认为(**1)可以被删除(对象的数据将是相同的)和
  • That(**2)可以是一个简单的call,因为我们确切地知道要调用哪个成员。不需要查找虚函数表。

在我看来,编译器在某些情况下也可以推断出这是一个相当合理的事情。对于那些感兴趣的人,是的,callvirt有速度惩罚,尽管它非常小。

然而

。PEVerify告诉我这是错误的。作为一个好孩子,我总是注意到PEVerify告诉我的一切。我在这里漏掉了什么?为什么更改此调用会导致不正确的程序集?


显然创建一个最小的测试用例不是那么简单…到目前为止,我的运气还不太好。

至于问题本身,我可以简单地在一个更大的程序中重现它:

[IL]: Error: [C:'tmp'emit'test.dll : NubiloSoft.Test::Value][offset 0x00000007] The 'this' parameter to the call must be the calling method's 'this' parameter.

值的IL代码:

L_0000: ldarg.0 
L_0001: ldfld class NubiloSoft.Test SomeField
L_0006: ldarg.1 
L_0007: call instance bool NubiloSoft.Test::Contains(int32)

字段类型为NubiloSoft.Test

对于Contains,它在基类中是抽象的,在派生类中被覆盖。正如你所料。当我删除'抽象基方法' + '重写'时,PEVerify再次喜欢它。

为了重现这个问题,我这样做了,到目前为止还没有在一个最小的测试用例中重现它:

public abstract class FooBase
{
    public abstract void MyMethod();
}
// sealed doesn't seem to do anything...
public class FooDerived : FooBase 
{
    public override void MyMethod()
    {
        Console.WriteLine("Hello world!");
    }
}
public class FooGenerator
{
    static void Main(string[] args)
    {
        Type t = CreateClass();
        object o = Activator.CreateInstance(t, new[] { new FooDerived() });
        var meth = t.GetMethod("Caller");
        meth.Invoke(o, new object[0]);
        Console.ReadLine();
    }
    public static Type CreateClass()
    {
        // Create assembly
        var assemblyName = new AssemblyName("testemit");
        var assemblyBuilder =
            AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName,
            AssemblyBuilderAccess.RunAndSave, @"c:'tmp");
        // Create module
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("testemit", "test_emit.dll", false);
        // Create type : IFoo
        var typeBuilder = moduleBuilder.DefineType("TestClass", TypeAttributes.Public, typeof(object));
        // Apparently we need a field to trigger the issue???
        var field = typeBuilder.DefineField("MyObject", typeof(FooDerived), FieldAttributes.Public);
        ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
            MethodAttributes.Public | MethodAttributes.HideBySig |
            MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
            CallingConventions.HasThis, new Type[] { typeof(FooDerived) });
        // Generate the constructor IL. 
        ILGenerator gen = constructorBuilder.GetILGenerator();
        // The constructor calls the constructor of Object
        gen.Emit(OpCodes.Ldarg_0);
        gen.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
        // Store the field
        gen.Emit(OpCodes.Ldarg_0);
        gen.Emit(OpCodes.Ldarg_1);
        gen.Emit(OpCodes.Stfld, field);
        // Return
        gen.Emit(OpCodes.Ret);
        // Add the 'Second' method
        var mb = typeBuilder.DefineMethod("Caller",
            MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual |
            MethodAttributes.Final | MethodAttributes.Public, CallingConventions.HasThis,
            typeof(void), Type.EmptyTypes);
        // Implement
        gen = mb.GetILGenerator();
        gen.Emit(OpCodes.Ldarg_0);
        gen.Emit(OpCodes.Ldfld, field);
        gen.Emit(OpCodes.Call, typeof(FooDerived).GetMethod("MyMethod"));
        gen.Emit(OpCodes.Ret);
        Type result = typeBuilder.CreateType();
        assemblyBuilder.Save("testemit.dll");
        return result;
    }
}

当你运行它并调用peverify时,它会告诉你代码没有错误…: - s

我不知道这是怎么回事…

将callvirt更改为在IL中呼叫

怀疑这篇博文是相关的。特别是:

一些人认为这通过继承侵犯了隐私。许多代码都是在这样的假设下编写的,即重写虚方法足以保证调用内部的自定义逻辑。直观地说,这是有意义的,c#会让你产生这种安全感,因为它总是以callvirts的形式发出对虚拟方法的调用。

然后:

在Whidbey的后期,一些人认为这有点奇怪,至少我们不希望部分受信任的代码来做这件事。这甚至是可能的,这常常让人们感到惊讶。我们通过引入新的验证规则解决了期望和现实之间的不匹配。

该规则限制了调用方对虚方法进行非虚调用的方式,特别是只允许在调用方的this指针上调用目标方法。这有效地允许对象向上(或向下)调用它自己的类型层次结构,尽管这很奇怪。

换句话说,假设这个更改就是您所说的(听起来像),该规则的存在是为了防止IL违反如何调用虚拟方法的正常期望。

你可能想尝试使SomeCall方法sealedMyDerivedClass…在这一点上,它不再是虚拟的,因为在MyDerivedClass类型的引用上调用SomeCall总是调用相同的方法…对于peverify来说,这是否足够非虚拟是另一回事:)