Why does PEVerify not recognize valid code?

1

I created simple program, which dynamically generates GenericEmitExample1.dll assembly. Such assembly defines following type:

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

Here is source code of such program:

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");
    }
}

There is builtin tool, named PEVerify (https://docs.microsoft.com/en-us/dotnet/framework/tools/peverify-exe-peverify-tool). It helps to determine whether their MSIL code and associated metadata meet type safety requirements. I decided to test it, after its calling on generated assembly it shows following error message:

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

1 Error(s) Verifying GenericEmitExample1.dll

Such report surprised me. Here is IL code of generated type:

.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

I don't see any forbiden tricks/invalid IL code. callvirt was used with constrained prefix. Documentation (https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.constrained?view=netframework-4.8) valids this trick. Here is quote 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.

So, what's the problem with PEVerify? Is it bug?

c#
reflection
cil
reflection.emit
asked on Stack Overflow Nov 18, 2019 by LmTinyToon • edited Jun 20, 2020 by Community

1 Answer

2

From Section III.2.1 of ECMA-335 (which talks about the constrained prefix)

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.

I think you're falling foul of "This is equivalent to requiring that a boxed thisType must be a subclass of the class which method belongs to".

method in your case is Int32::ToString(), not Object::ToString(), and so belongs to int. However a boxed int is not a subclass of int.

In order to use a constrained virtual call here, you would have to call Object::ToString(), not Int32::ToString().

I've verified this by changing the callvirt instruction to:

gen.Emit(OpCodes.Callvirt, typeof(object).GetMethod(nameof(object.ToString), Type.EmptyTypes));

This verifies.


In addition:

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.

You're calling a method directly on the value type (and not on a box of it), so you should use call.

answered on Stack Overflow Nov 18, 2019 by canton7 • edited Nov 18, 2019 by canton7

User contributions licensed under CC BY-SA 3.0