c# - 生成方法调用的代码。生成的 C# 代码显示的声明局部变量比 IL 代码中实际存在的变量多?

标签 c# reflection.emit il ilspy

我正在从 DynamicMethod 创建一个开放实例委托(delegate)在某个目标上调用方法。该代码通过 ref 参数以及静态方法进行处理。

请参阅以下内容:

public class Test
{
    public void ByRef(ref int x, int y, out int z) { x = y = z = -1; }
}

var type = typeof(Test);
var method = type.GetMethod("ByRef");
var caller = method.DelegateForCall();
var args = new object [] { 1, 2, 3 };
var inst = new Test();
caller(inst, args);
Console.WriteLine(args[0]); // -1
Console.WriteLine(args[1]); // 2
Console.WriteLine(args[2]); // -1

DelegateForCall返回一个开放实例委托(delegate)来调用 ByRef Test 上的方法给定一些参数的对象。因此我们可以推断出它的定义:

public delegate object MethodCaller(object target, object[] args);

但它实际上是强类型的(我处理强目标和弱目标)所以它实际上看起来像这样:

public delegate TReturn MethodCaller<TTarget, TReturn>(TTarget target, object[] args);

代码按预期工作。我将向您展示我用来生成调用者委托(delegate)的代码,但首先让我展示我期望它生成的内容。 DelegateForCall基本上返回DelegateForCall<object, object>所以它是弱类型的,在这种情况下我希望它生成以下内容:

public static object MethodCaller(object target, object[] args)
{
   Test tmp = (Test)target;
   int arg0 = (int)args[0];
   int arg1 = (int)args[1];
   int arg2 = (int)args[2];
   tmp.ByRef(ref arg0, arg1, out arg2);
   args[0] = arg0;
   args[2] = arg2;
   return null;
}

不幸的是,在 ILSpy 中查看我生成的测试程序集中生成的代码(用于调试目的),显示了以下 C# 代码:

public static object MethodCaller(object target, object[] args)
{
    Program.Test test = (Program.Test)target;
    Program.Test arg_39_0 = test;
    int num = (int)args[0];
    int num2 = (int)args[1];
    int arg_39_2 = num2;
    int num3 = (int)args[2];
    arg_39_0.ByRef(ref num, arg_39_2, ref num3);
    args[0] = num;
    args[2] = num3;
    return null;
}

我无法理解为什么它声明 arg_39_0arg_39_2 - 在我的代码中,我声明一个 local 来存储目标,并声明一个 local 来从 args 获取值。大批。所以总共我们应该会看到 4 个本地人。

这是我正在使用的代码:

    static void GenerateMethodInvocation<TTarget>(MethodInfo method)
    {
        var weaklyTyped = typeof(TTarget) == typeof(object);

        // push target if not static (instance-method. in that case first arg0 is always 'this')
        if (!method.IsStatic)
        {
            var targetType = weaklyTyped ? method.DeclaringType : typeof(TTarget);
            emit.declocal(targetType);
            emit.ldarg0();
            if (weaklyTyped)
                emit.unbox_any(targetType);
            emit.stloc0()
                .ifclass_ldloc_else_ldloca(0, targetType);
        }

        // push arguments in order to call method
        var prams = method.GetParameters();
        for (int i = 0, imax = prams.Length; i < imax; i++)
        {
            emit.ldarg1()       // push array
                .ldc_i4(i)      // push index
                .ldelem_ref();  // pop array, index and push array[index]

            var param = prams[i];
            var dataType = param.ParameterType;

            if (dataType.IsByRef)
                dataType = dataType.GetElementType();

            var tmp = emit.declocal(dataType);
            emit.unbox_any(dataType)
                .stloc(tmp)
                .ifbyref_ldloca_else_ldloc(tmp, param.ParameterType);
        }

        // perform the correct call (pushes the result)
        emit.callorvirt(method);

        // assign byref values back to the args array
        // if method wasn't static that means we declared a temp local to load the target
        // that means our local variables index for the arguments start from 1
        int localVarStart = method.IsStatic ? 0 : 1;
        for (int i = 0; i < prams.Length; i++)
        {
            var paramType = prams[i].ParameterType;
            if (paramType.IsByRef)
            {
                var byRefType = paramType.GetElementType();
                emit.ldarg1()
                    .ldc_i4(i)
                    .ldloc(i + localVarStart);
                if (byRefType.IsValueType)
                    emit.box(byRefType);
                emit.stelem_ref();
            }
        }

        if (method.ReturnType == typeof(void))
            emit.ldnull();
        else if (weaklyTyped)
            emit.ifvaluetype_box(method.ReturnType);

        emit.ret();
    }

'emit' 基本上是我用来发出操作码的助手 ( source )

最后,这是 ILSpy 中所示的 IL 代码,它似乎与我期望的 C# 更一致,而不是它实际生成的 C#(带有两个额外冗余局部变量的代码)

.method public hidebysig static 
    object MethodCaller (
        object target,
        object[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 100 (0x64)
    .maxstack 5
    .locals init (
        [0] class [CustomSerializer]CustomSerializer.Program/Test,
        [1] int32,
        [2] int32,
        [3] int32
    )

    IL_0000: ldarg.0
    IL_0001: unbox.any [CustomSerializer]CustomSerializer.Program/Test
    IL_0006: stloc.0
    IL_0007: ldloc 0
    IL_000b: nop
    IL_000c: nop
    IL_000d: ldarg.1
    IL_000e: ldc.i4 0
    IL_0013: ldelem.ref
    IL_0014: unbox.any [mscorlib]System.Int32
    IL_0019: stloc.1
    IL_001a: ldloca.s 1
    IL_001c: ldarg.1
    IL_001d: ldc.i4 1
    IL_0022: ldelem.ref
    IL_0023: unbox.any [mscorlib]System.Int32
    IL_0028: stloc.2
    IL_0029: ldloc.2
    IL_002a: ldarg.1
    IL_002b: ldc.i4 2
    IL_0030: ldelem.ref
    IL_0031: unbox.any [mscorlib]System.Int32
    IL_0036: stloc.3
    IL_0037: ldloca.s 3
    IL_0039: call instance void [CustomSerializer]CustomSerializer.Program/Test::ByRef(int32&, int32, int32&)
    IL_003e: ldarg.1
    IL_003f: ldc.i4 0
    IL_0044: ldloc 1
    IL_0048: nop
    IL_0049: nop
    IL_004a: box [mscorlib]System.Int32
    IL_004f: stelem.ref
    IL_0050: ldarg.1
    IL_0051: ldc.i4 2
    IL_0056: ldloc 3
    IL_005a: nop
    IL_005b: nop
    IL_005c: box [mscorlib]System.Int32
    IL_0061: stelem.ref
    IL_0062: ldnull
    IL_0063: ret
} // end of method Test::MethodCaller

请注意,它明确指出有 4 个局部变量,但 ILSpy C# 显示有 6 个!

注意生成的程序集通过 peverify验证。

为什么 ILSpy 中的 C# 看起来不像我想象的那样?为什么显示有 6 个局部变量,而实际上只有 4 个?

编辑:这是 dotPeek 显示的内容,更奇怪......

  public static object MethodCaller(object target, object[] args)
  {
    Program.Test test = (Program.Test) target;
    int num1 = (int) args[0];
    // ISSUE: explicit reference operation
    // ISSUE: variable of a reference type
    int& x = @num1;
    int y = (int) args[1];
    int num2 = (int) args[2];
    // ISSUE: explicit reference operation
    // ISSUE: variable of a reference type
    int& z = @num2;
    test.ByRef(x, y, z);
    args[0] = (object) num1;
    args[2] = (object) num2;
    return (object) null;
  }

最佳答案

int& x = @num1; 语句生成对 num1引用。这样做是为了通过 ref 调用执行方法调用。

如果你调用一个方法:

public void ByRef(ref int x, int y, out int z)

这意味着您正在传递对 xz 的引用。现在,C# 允许您在代码级别上非常简洁地完成此操作,但在 IL 级别上,这不太明显,因为只有有限的指令集。结果,ByRef 方法被翻译为:

public void ByRef(int& x, int y, int& z)

您首先需要计算引用。现在,反编译器总是很难理解正在发生的事情,尤其是在代码经过优化的情况下。尽管对于人类来说,这可能看起来是一个简单的模式,但对于机器来说,这通常要困难得多。


声明新变量的另一个原因是,通常当生成参数列表时,它们会被推送到调用堆栈上。所以你做类似的事情:

push arg0
push arg1
push arg2
call method

做某事:

method(arg0,arg1,arg2)

现在您有时可以进行交错计算。因此,您将某些内容压入堆栈,然后将其弹出以执行某些操作等。很难跟踪哪个变量位于何处以及它是否仍具有与原始变量相同的值。通过在反编译过程中使用“新变量”,您可以确定自己没有做错任何事情。


简短版本:

您始终必须首先生成对值的引用。由于它们的类型与 int 不同(int 不等于 int&),反编译器决定使用新变量。但反编译从来都不是完美的。有无数的程序可以生成相同的 IL 代码。

反编译器应该是保守的:您从 IL 代码(或等效的代码)开始,并尝试理解该代码。然而,做到这一点并不容易。反编译器使用一组重复执行的“规则”以使代码进入可读状态。这些“规则”是保守的:您必须保证规则之后的代码与之前的代码等效。为此,安全总比后悔好。引入额外的变量以确保有时是必要的预防措施。

关于c# - 生成方法调用的代码。生成的 C# 代码显示的声明局部变量比 IL 代码中实际存在的变量多?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28795057/

相关文章:

c# - 将 BitArray 转换为小字节数组

c# - 使用 CustomAttributeBuilder 的参数不匹配(装箱十进制?)

c# - Fody Async MethodDecorator 来处理异常

c# - MVC 核心获取 Controller ViewModel 结果

c# - 为什么 WriteEntry 不适用于此 EventLog 源?

c# - 使用 Toolkit for Windows Phone 中的 ExpanderView 自定义 header

c# - 动态创建类型并调用基类的构造函数

c# - 从托管代码调用非托管方法

.net - 为什么 IL 中的 const long 会转换为 Short?

.net - CLR 是否知道有关事件的任何信息?