为什么 PEVerify 无法识别有效代码?

Why does PEVerify not recognize valid code?

我创建了一个简单的程序,它动态生成 GenericEmitExample1.dll 程序集。这样的程序集定义了以下类型:

public class Sample
{
    public static string test()
    {
        int num = default(int);
        return num.ToString();
    }
}

这是该程序的源代码:

using System;
using System.Reflection;
using System.Reflection.Emit;

public class Example
{
    public static void Main()
    {
        AppDomain myDomain = AppDomain.CurrentDomain;
        AssemblyName myAsmName = new AssemblyName("GenericEmitExample1");
        AssemblyBuilder myAssembly = myDomain.DefineDynamicAssembly(myAsmName, AssemblyBuilderAccess.RunAndSave);
        ModuleBuilder myModule =
            myAssembly.DefineDynamicModule(myAsmName.Name,
               myAsmName.Name + ".dll");
        TypeBuilder myType = myModule.DefineType("Sample", TypeAttributes.Public);
        var test_method = myType.DefineMethod("test", MethodAttributes.Public | MethodAttributes.Static, typeof(String), Type.EmptyTypes);
        var gen = test_method.GetILGenerator();
        var local = gen.DeclareLocal(typeof(int));
        gen.Emit(OpCodes.Ldloca, local);
        gen.Emit(OpCodes.Constrained, typeof(int));
        gen.Emit(OpCodes.Callvirt, typeof(int).GetMethod(nameof(int.ToString), Type.EmptyTypes));
        gen.Emit(OpCodes.Ret);
        myType.CreateType();
        myAssembly.Save(myAsmName.Name + ".dll");
    }
}

有内置工具,名为PEVerify (https://docs.microsoft.com/en-us/dotnet/framework/tools/peverify-exe-peverify-tool)。它有助于确定他们的 MSIL 代码和关联的元数据是否满足类型安全要求。我决定对其进行测试,在调用生成的程序集后显示以下错误消息:

[IL]: Error: [GenericEmitExample1.dll : Sample::test][offset 0x00000008] Callvirt on a value type method.

1 Error(s) Verifying GenericEmitExample1.dll

这样的报道让我很吃惊。这是生成类型的 IL 代码:

.class public auto ansi Sample
    extends [mscorlib]System.Object
{
    // Methods
    .method public static 
        string test () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 14 (0xe)
        .maxstack 1
        .locals init (
            [0] int32
        )

        IL_0000: ldloca.s 0
        IL_0002: constrained. [mscorlib]System.Int32
        IL_0008: callvirt instance string [mscorlib]System.Int32::ToString()
        IL_000d: ret
    } // end of method Sample::test

    .method public specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x206c
        // Code size 7 (0x7)
        .maxstack 2

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Sample::.ctor

} // end of class Sample

我没有看到任何禁止的 tricks/invalid IL 代码。 callvirtconstrained 前缀一起使用。文档 (https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.constrained?view=netframework-4.8) 验证了这个技巧。这是引用 III.2.1 constrained. – (prefix) invoke a member on a value of a variable type:

The constrained opcode allows IL compilers to make a call to a virtual function in a uniform way independent of whether ptr is a value type or a reference type. Although it is intended for the case where thisType is a generic type variable, the constrained prefix also works for nongeneric types and can reduce the complexity of generating virtual calls in languages that hide the distinction between value types and reference types.

那么,PEVerify 有什么问题?是错误吗?

来自 ECMA-335 的第 III.2.1 节(讨论 constrained 前缀)

Verifiability:

The ptr argument will be a managed pointer (&) to thisType. In addition all the normal verification rules of the callvirt instruction apply after the ptr transformation as described above. This is equivalent to requiring that a boxed thisType must be a subclass of the class which method belongs to.

我认为你违反了“这相当于要求盒装 thisType 必须是 class 的子class method属于".

method 在您的例子中是 Int32::ToString(),而不是 Object::ToString(),因此属于 int。但是,盒装 int 不是 int.

的子 class

为了在这里使用约束虚拟调用,您必须调用 Object::ToString(),而不是 Int32::ToString()

我已经通过将 callvirt 指令更改为:

来验证这一点
gen.Emit(OpCodes.Callvirt, typeof(object).GetMethod(nameof(object.ToString), Type.EmptyTypes));

这验证了。


另外:

I.12.1.6.2.4 Calling methods

Static methods on value types are handled no differently from static methods on an ordinary class: use a call instruction with a metadata token specifying the value type as the class of the method. Non-static methods (i.e., instance and virtual methods) are supported on value types, but they are given special treatment. A non-static method on a reference type (rather than a value type) expects a this pointer that is an instance of that class. This makes sense for reference types, since they have identity and the this pointer represents that identity. Value types, however, have identity only when boxed. To address this issue, the this pointer on a non-static method of a value type is a byref parameter of the value type rather than an ordinary by-value parameter.

A non-static method on a value type can be called in the following ways:

  • For unboxed instances of a value type, the exact type is known statically. The call instruction can be used to invoke the function, passing as the first parameter (the this pointer) the address of the instance. The metadata token used with the call instruction shall specify the value type itself as the class of the method.
  • Given a boxed instance of a value type, there are three cases to consider:
    • Instance or virtual methods introduced on the value type itself: unbox the instance and call the method directly using the value type as the class of the method.
    • Virtual methods inherited from a base class: use the callvirt instruction and specify the method on the System.Object, System.ValueType or System.Enum class as appropriate.
    • Virtual methods on interfaces implemented by the value type: use the callvirt instruction and specify the method on the interface type.

你是直接在值类型上调用方法(而不是在它的一个盒子上),所以你应该使用 call.