3

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

基本上调用是这样的:

((MyDerivedClass)myBaseObject).SomeCall();

或在 IL

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

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

我知道这callvirt基本上相当于检查对象是否为空,如果它没有使用 vtable 调用方法,如果是,则抛出异常。就我而言,我知道它永远不会null,我知道那是我想要调用的实现。callvirt我理解为什么在这种情况下您通常需要 a 。

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

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

在我看来,编译器在某些情况下也可以推断出相当合理的事情。对于那些感兴趣的人,是的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

我不确定这里发生了什么......在我看来它很相似。

4

1 回答 1

2

怀疑 这篇博文是相关的。尤其是:

有些人认为这是通过继承侵犯隐私的行为。许多代码是在假设重写虚拟方法足以保证调用内部的自定义逻辑的情况下编写的。直观地说,这是有道理的,而 C# 让您沉浸在这种安全感中,因为它总是以 callvirts 的形式发出对虚拟方法的调用。

接着:

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

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

换句话说,假设这种变化你在谈论的(听起来像),规则是为了防止 IL 违反对如何调用虚拟方法的正常期望。

您可能想尝试在...中创建该SomeCall方法,此时它不再是虚拟的,因为对类型引用的调用将始终调用相同的方法...对于 peverify 而言,这是否足够非虚拟是不过是另一回事:)sealedMyDerivedClassSomeCallMyDerivedClass

于 2015-08-13T06:24:03.830 回答