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

为方法调用生成代码。生成的C#代码显示了比IL代码中实际存在的更多声明的局部变量?,c#,reflection.emit,il,ilspy,C#,Reflection.emit,Il,Ilspy,我正在从DynamicMethod创建一个开放实例委托,以调用特定目标上的方法。代码通过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.Delegate

我正在从
DynamicMethod
创建一个开放实例委托,以调用特定目标上的方法。代码通过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
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;
}
DelegateForCall
返回一个打开的实例委托,以调用
Test
对象上给定一些参数的
ByRef
方法。因此我们可以推断出它的定义:

public delegate object MethodCaller(object target, object[] args);
但它实际上是强类型的(我处理强目标和弱目标),所以它实际上看起来是这样的:

public delegate TReturn MethodCaller<TTarget, TReturn>(TTarget target, object[] args);
不幸的是,在我在ILSpy中生成的测试程序集中查看生成的代码(用于调试目的),会显示以下C#代码:

我无法理解为什么它声明了
arg_39_0
arg_39_2
——在我的代码中,我声明了一个本地存储目标,并声明了一个本地从
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();
    }
请注意,它清楚地表明有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)
这意味着您正在传递对
x
z
的引用。现在,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代码(或类似的代码)开始,并尝试从该代码中获得意义。然而,要做到这一点并不容易。反编译器使用一组重复执行的“规则”,以使代码进入可读状态。这些“规则”是保守的:您必须保证规则后的代码与规则前的代码相等。要做到这一点,你总比后悔安全。引入额外的变量以确保这有时是必要的预防措施。

看看这篇博文:反编译基本上是一门不完善的科学,不同的工具会产生不同的结果。那些额外的
arg.
变量没有像你希望的那样做好。我明白了。我确实在dotPeek中得到了更奇怪的结果(添加到问题中)。这有点不方便,如果能看到C来验证生成的内容是否正确,那就太好了。我想我只需要相信我的IL是有效的和经过验证的,不要过多地考虑那些不准确的结果。
push arg0
push arg1
push arg2
call method
method(arg0,arg1,arg2)