为方法调用生成代码。生成的 C# 代码显示声明的局部变量比 IL 代码中实际有更多?
Generating code for method call. Generated C# code shows more declared local variables then there actually is in the IL code?
我正在从 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
DelegateForCall
returns 一个开放实例委托,在给定一些参数的情况下调用 Test
对象上的 ByRef
方法。所以可以推导出它的定义:
public delegate object MethodCaller(object target, object[] args);
但它实际上是强类型的(我同时处理强目标和弱目标)所以它实际上看起来像这样:
public delegate TReturn MethodCaller<TTarget, TReturn>(TTarget target, object[] args);
代码按预期工作。我将向您展示我用来生成调用方委托的代码,但首先让我展示一下我期望它生成的内容。 DelegateForCall
基本上 returns 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_0
和 arg_39_2
- 在我的代码中,我声明了一个 local 来存储目标,而 locals 从 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)
这意味着您正在传递对 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)
现在您有时可以进行交错计算。因此,您将某些东西压入堆栈,然后将其弹出以执行某些操作,等等。很难跟踪哪个变量位于何处以及它是否仍然具有与原始值相同的值。通过在反编译过程中使用"new variables",你确定你没有做错任何事情。
短版:
您始终必须首先生成对值的引用。由于它们的类型不同于 int
(int
是 不等于 int&
),反编译器决定使用新变量。但是反编译从来都不是完美的。有无数的程序可以产生相同的 IL 代码。
反编译器应该是保守的:您从 IL 代码(或类似的东西)开始,并尝试理解该代码。然而,要做到这一点并不容易。反编译器使用一组重复执行的 "rules" 来使代码进入可读状态。这些"rules"是保守的:你必须保证规则之后的代码和之前的代码是等价的。要做到这一点,安全总比后悔好。引入额外的变量以确保有时是必要的预防措施。
我正在从 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
DelegateForCall
returns 一个开放实例委托,在给定一些参数的情况下调用 Test
对象上的 ByRef
方法。所以可以推导出它的定义:
public delegate object MethodCaller(object target, object[] args);
但它实际上是强类型的(我同时处理强目标和弱目标)所以它实际上看起来像这样:
public delegate TReturn MethodCaller<TTarget, TReturn>(TTarget target, object[] args);
代码按预期工作。我将向您展示我用来生成调用方委托的代码,但首先让我展示一下我期望它生成的内容。 DelegateForCall
基本上 returns 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_0
和 arg_39_2
- 在我的代码中,我声明了一个 local 来存储目标,而 locals 从 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)
这意味着您正在传递对 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)
现在您有时可以进行交错计算。因此,您将某些东西压入堆栈,然后将其弹出以执行某些操作,等等。很难跟踪哪个变量位于何处以及它是否仍然具有与原始值相同的值。通过在反编译过程中使用"new variables",你确定你没有做错任何事情。
短版:
您始终必须首先生成对值的引用。由于它们的类型不同于 int
(int
是 不等于 int&
),反编译器决定使用新变量。但是反编译从来都不是完美的。有无数的程序可以产生相同的 IL 代码。
反编译器应该是保守的:您从 IL 代码(或类似的东西)开始,并尝试理解该代码。然而,要做到这一点并不容易。反编译器使用一组重复执行的 "rules" 来使代码进入可读状态。这些"rules"是保守的:你必须保证规则之后的代码和之前的代码是等价的。要做到这一点,安全总比后悔好。引入额外的变量以确保有时是必要的预防措施。