【问题标题】:Changing callvirt to call in IL将 callvirt 更改为在 IL 中调用
【发布时间】:2015-08-13 06:17:31
【问题描述】:

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

基本上调用是这样的:

((MyDerivedClass)myBaseObject).SomeCall();

或在 IL

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

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

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

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

  • 当您考虑将数据和类型分开时,我实际上认为 (**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

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

【问题讨论】:

  • peverify 是否提供特定信息?您能否提供一个简短但完整的 IL 程序来演示该问题?
  • 我在一个更大的程序中遇到了它。让我看看我是否可以编写一个最小的测试用例。
  • @JonSkeet 我尽了最大的努力,但到目前为止没有任何运气在最小的测试用例场景中重现该问题。更奇怪的是,最小测试用例的代码和IMO几乎一模一样。
  • 我不确定您为什么在这里使用 C# - 为什么不只显示无法验证的 IL?我只是编译一个小 C# 程序,然后用 ildasm 将其反编译为 IL,修改 IL,用 ilasm 重新组装,然后对其进行 peverify...
  • @JonSkeet 我添加了 Value 的 IL 代码。这就是其中的一切。至于 C# 代码,我只是把它放在一边……现在真的不重要了,不是吗?

标签: c# .net il peverify


【解决方案1】:

怀疑 this blog post 是相关的。特别是:

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

然后:

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

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

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

您可能想尝试在MyDerivedClass 中创建SomeCall 方法sealed...此时它不再是虚拟的,因为在MyDerivedClass 类型的引用上调用SomeCall总是调用相同的方法...尽管对于 peverify 来说这是否足够非虚拟是另一回事:)

【讨论】:

  • 有趣的是(或者可能不是对于比我更了解的人) - var h = new concrete(); h.SomeCall() 生成一个 callvirt 指令,即使 concrete 是一个密封类,并且不从任何东西继承(除了对象,当然)。我本来希望这会使用call。更有趣的是new concrete().SomeCall() 确实使用了call 指令,尽管它们应该是等效的
  • @Rob:我怀疑这更多是为了空保护——即使面对“明显非空”的目标——而不是其他任何事情。当您考虑编译器的复杂性时,我认为我不想通过建议它应该根据它是否“知道”引用为空来发出不同的 IL 来增加复杂性。此外,如果 Concrete 在不同的程序集中,它可能会在代码运行时没有密封... JIT 可以更好地省略此处的无效性检查和 vtable 查找,我怀疑。
  • @Rob 新的 Roslyn 编译器也是这样吗?无论哪种方式,RyuJIT 都使用 SSA,因此在这种情况下,它在 SSA 级别上应该是等效的。在较旧的 .NET 4.0 编译器中,iirc 并非如此。正如您在我的问题中所读到的,对callvirt 的调用是“默认”添加的,用于隐式空检查。以后也可以通过消除死代码来删除它。
猜你喜欢
  • 2020-03-25
  • 1970-01-01
  • 2011-05-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-09
  • 1970-01-01
相关资源
最近更新 更多