C# Assembly 与 Reflection
前言
前一阵子想利用闲余时间写一个 Unity 游戏的翻译工具,主要是用于翻译一些内嵌在代码中的文本,最初想偷懒看了一下网上的教学推荐说可以先利用DnSpy、ILSpy等工具反编译,直接修改反编译出来的代码中的字符串然后再重新编译,这样就只需要写一个提取和置换c#代码中所有文本的工具就行了。但在略微尝试一下后发现这些反编译工具并不能完美的生成可编译的代码,于是只能暂时搁置了。
刚好近期工作中在编写一些Debug工具,需要大量的利用 c# 的 Reflection 和 Mono.Cecil、ICSharpCode.Compiler等工具来读取程序集中的CIL,以及利用IL注入动态的生成一些Debug代码。在一段时间的学习后这些工具仿佛打开了新世界的大门,此前提到的翻译工具也不再是问题了,就想在这里分享一下成果,也算是对前段时间的所学做一些记录。
介绍
c#的代码在编译后,会先生成一个 Assembly 程序集(.exe, .dll),该程序集包含一些类似于汇编的中间语言(CIL)构成的托管代码,然后再通过类似于Java虚拟机的虚拟运行环境 CLR (Common Language Runtime) 来进行JIT (just-in-time) 编译成机器语言。而这篇文章以及之后整个专栏的目的就是介绍如何查看与编辑 c# 所编译出来的程序集中所包含的IL代码。
微软官方提供了 System.Reflection (反射) 来用于读取c#程序集中的内容,常常用于查看程序集中的内容并动态的生成其中类的实例,调用里面的方法。在不涉及太底层的分析时,反射能很好的达到目的并快速的应用。
使用反射(Reflection)读取程序集
反射作为c#官方提供的工具,本身使用起来非常的简单。你可以用它快速的读取当前程序的程序集,或者读取指定路径的程序集。
- 获取当前正在运行的程序集
Assembly currentAssem = Assembly.GetExecutingAssembly();
- 根据已有类读取
Assembly assembly = Typename.GetType().Assembly;
- 从文件中读取
Assembly assembly = Assembly.Loadfile("File path");
要注意的是,Assembly 只能读取指定版本的程序集,这个所谓的版本不单单是指的.Net Framework的版本,还有目标是x86架构还是x64。不同的架构可能会导致assembly读取失败。
生成类的动态实例,调用方法
当我们从 Assembly 中获得 Runtime Type 以及其方法信息以后,就可以利用Activator.CreateInstance() 来生成其动态的对象并调用其中方法了。
// 将该类生成的实例作为动态的对象, 直接调用方法
dynamic dynamicTypeInstance = Activator.CreateInstance(type);
// 可以在type后面插入object数组作为 constructor 参数传入
// 动态类可以直接调用方法并填入参数
dynamicTypeInstance.MethodName(1, 2);
// 将该类生成的实例作为 object, 通过 MethodInfo.Invoke 来调用方法
object typeInstance = Activator.CreateInstance(type);
method.Invoke(typeInstance, new object[] {1, 2, "string"});
可能有人(比如我)会觉得用 dynamic 来调用方法速度会比method.invoke(object) 慢,但经过一番测试后,结果却令我感到惊讶。我使用的测试方法先编写一个简单的GetSum方法计算两个参数的和并返回,然后用如下代码分别计算动态调用、通过Method.Invoke调用、以及直接调用该方法 10000 次的总时间消耗和最大单次时间消耗。
Stopwatch watch = new Stopwatch();
watch.Start();
long initialTime = watch.ElapsedMilliseconds;
dynamic dynamicTypeInstance = Activator.CreateInstance(typeof(Program));
long maxTime = 0;
long maxTimeId = 0;
// 动态调用方法一万次
for (int i = 0; i < 10000; i++)
{
var startTime = watch.ElapsedTicks;
// 动态类可以直接调用方法并填入参数
dynamicTypeInstance.GetSum(i, i+1);
var endTime = watch.ElapsedTicks;
var timeDifference = endTime - startTime;
if(timeDifference > maxTime)
{
maxTime = timeDifference;
maxTimeId = i;
}
}
Console.WriteLine("Dynamic");
Console.WriteLine("Max Time Id: " + maxTimeId);
Console.WriteLine("Max Time: " + maxTime);
Console.WriteLine("Total Time: " + (watch.ElapsedTicks - initialTime));
watch.Stop();
watch.Start();
initialTime = watch.ElapsedMilliseconds;
object typeInstance = Activator.CreateInstance(typeof(Program));
MethodInfo method = typeof(Program).GetMethod("GetSum");
maxTime = 0;
maxTimeId = 0;
// 通过 Method.Invoke 调用一万次
for (int i = 0; i < 10000; i++)
{
var startTime = watch.ElapsedTicks;
method.Invoke(typeInstance, new object[] { i, i + 1 });
var endTime = watch.ElapsedTicks;
var timeDifference = endTime - startTime;
if (timeDifference > maxTime)
{
maxTime = timeDifference;
maxTimeId = i;
}
}
Console.WriteLine("Method.Invoke");
Console.WriteLine("Max Time Id: " + maxTimeId);
Console.WriteLine("Max Time: " + maxTime);
Console.WriteLine("Total Time: " + (watch.ElapsedTicks - initialTime));
watch.Stop();
watch.Start();
Program program = new Program();
maxTime = 0;
maxTimeId = 0;
// 直接调用一万次
for (int i = 0; i < 10000; i++)
{
var startTime = watch.ElapsedTicks;
program.GetSum(i, i + 1);
var endTime = watch.ElapsedTicks;
var timeDifference = endTime - startTime;
if (timeDifference > maxTime)
{
maxTime = timeDifference;
maxTimeId = i;
}
}
Console.WriteLine("Direct Call");
Console.WriteLine("Max Time Id: " + maxTimeId);
Console.WriteLine("Max Time: " + maxTime);
Console.WriteLine("Total Time: " + (watch.ElapsedTicks - initialTime));
watch.Stop();
输出如下
Dynamic
Max Time Id: 0
Max Time: 2870643
Total Time: 2926522
Method.Invoke
Max Time Id: 7232
Max Time: 342
Total Time: 3009383
Direct Call
Max Time Id: 1342
Max Time: 23
Total Time: 3040620
由此可见,动态方法在第一次调用时效率极低,但之后的重复调用对比另外两种方案却没有太明显的差别,甚至有着一定的优势。
使用反射来向Assembly中获取或注入IL代码
Reflection本身也有提供查看和注入IL代码的方法,但其在这两方面都有很大的限制。
查看IL代码
Reflection 可以通过
MethodInfo.GetMethodBody().GetILAsByteArray();
来获取方法中的IL代码。但正如你所见,IL是以Byte数组的形式被读出来,你往往需要自己对读出的ByteArray进行额外的包装和翻译处理才能做有意义的应用,而这需要对 CIL 这门中间语言有相当程度的理解才能做到。
示范
下面我会举一个例子示范。
首先假设我们在Program类中,除了main以外还有一个方法
// 这是一个用于计算1 + 2 + ... + x 的方法,作为示范
public int SumOf1ToNum(int num)
{
if(num < 1)
{
Console.WriteLine("Sum = 0;");
return 0;
}
int sum = 0;
for(int i = 1; i < num; i++)
{
sum += i;
Console.Write(i + " + ");
}
sum += num;
Console.WriteLine(num + " = " + sum);
return sum;
}
然后在main函数中,利用Reflection.Asssembly直接读取当前Program类所在的程序集(该示范是直接获取当前程序的程序集作为演示,大多数情况我们会用上面提到的方法获取其他程序集,这里就偷个懒了)
// 下面这两步其实是画蛇添足的,可以直接通过 typeof(Program).GetMethod() 来获取对应的MethodInfo,
// 而这里是在演示从 Assembly 中获取方法,为了偷懒没有创建额外的程序集,也顺便演示一下可以这么做
Assembly assembly = typeof(Program).Assembly;
// 这里注意类名需要输入全名,也就是要包含 namespace
MethodInfo sumOf1ToNumMethod = assembly.GetType("AssemblyExample.Program").GetMethod("SumOf1ToNum");
byte[] ilByteArr = sumOf1ToNumMethod.GetMethodBody().GetILAsByteArray();
// BitConverter 也是C#中非常实用的工具,常用于各个基本类型与二进制数据间的转换
Console.WriteLine(BitConverter.ToString(ilByteArr));
用DnSpy或ILSpy等工具查看,你将看到
上面这段代码的Output为:
00-03-17-FE-04-0B-07-2C-10-00-72-49-00-00-70-28-16-00-00-0A-00-16-0C-2B-54-16-0A-17-0D-2B-20-00-06-09-58-0A-09-8C-19-00-00-01-72-5B-00-00-70-28-18-00-00-0A-28-19-00-00-0A-00-00-09-17-58-0D-09-03-FE-04-13-04-11-04-2D-D6-06-03-58-0A-03-8C-19-00-00-01-72-63-00-00-70-06-8C-19-00-00-01-28-1A-00-00-0A-28-16-00-00-0A-00-06-0C-2B-00-08-2A
嗯。。。你或许已经发现,这串代码根本不是给人读的,但 Reflection 似乎只打算给你看这个(至少我只找到这个,欢迎指正),Thanks, Microsoft!
动态生成 IL 代码
Reflection 在 Reflection.Emit 的命名空间下提供了各种 Builder 类用于动态的输出 IL 代码,这些只能动态的生成新的 Runtime Type 以及新的 Assembly,而并不能直接修改已经存在的 Assembly,如此一来能应用到的方面就非常局限了(不过好歹比 GetILAsByteArray() 实用一点)。
利用反射动态生成代码最常用的两种方式为下:
- 生成动态方法,并利用托管或接口调用
DynamicMethod dynamicSum = new DynamicMethod("GetSum",typeof(int), new Type[] {typeof(int), typeof(int)}, typeof(Program).Module);
ILGenerator generator = dynamicSum.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Add);
generator.Emit(OpCodes.Ret);
var getSum = (GetSumDelegate)dynamicSum.CreateDelegate(typeof(GetSumDelegate));
Console.WriteLine(getSum(1, 3));
- 生成 Runtime 程序集,然后动态调用方法
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("RuntimeAssembly"), AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
TypeBuilder typeBuilder = moduleBuilder.DefineType("MyRuntimeType", TypeAttributes.Public);
MethodBuilder methodBuilder = typeBuilder.DefineMethod("RuntimeSum", MethodAttributes.Public | MethodAttributes.Static);
methodBuilder.SetParameters(new Type[] {typeof(int), typeof(int)});
methodBuilder.SetReturnType(typeof(int));
ILGenerator generator = methodBuilder.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Add);
generator.Emit(OpCodes.Ret);
Type dynamicType = typeBuilder.CreateType();
var dynamicMethod = dynamicType.GetMethod("RuntimeSum", BindingFlags.Public | BindingFlags.Static);
assemblyBuilder.SetEntryPoint(dynamicMethod);
Console.WriteLine("Sum of 1 + 2 = " + assemblyBuilder.EntryPoint.Invoke(null, new object[] { 1, 2 }));
Console.WriteLine("Sum of 1 + 2 = " + dynamicMethod.Invoke(null, new object[] { 1, 2}));
如果需要将程序集保存在本地,你需要在定义 Assembly 的时候使用
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder module = assembly.DefineDynamicModule("MainModule", "NewAssembly.dll");
然后在结尾加上
assemblyBuilder.Save("NewAssembly.exe"); // 生成exe或者是dll拓展名的c#程序集
动态生成的方法在第一次运行时也会有一定的速度上的overhead,这是因为运行前还没有经过JIT compiler的编译。之后的重复运行便不会有任何速度劣势了。
总结
总体来说,Reflection 非常适合从整体结构上分析 Assembly 中的类,查看其中的 Field, Property 及其属性 Attributes,同时也能够让开发者很方便的动态的使用其中的类和方法,但它在提供方法中的IL代码的方式上真的一言难尽。其IL的获得与注入方式注定了对方法中内容难以进行具体或复杂的分析和生成,也无法做到动态的将 IL 代码注入到已有的程序集代码中,这也是我们为什么会需要 Mono.Cecil 等第三方工具的主要原因,这个我会在下一篇中介绍。
参考
Assembly:
https://docs.microsoft.com/en-us/dotnet/standard/assembly/
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly?view=net-5.0
Reflection:
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/reflection
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbody.getilasbytearray?view=net-5.0
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit?view=net-5.0
https://www.codeproject.com/articles/121568/dynamic-type-using-reflection-emit
https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-define-and-execute-dynamic-methods
https://flylib.com/books/en/4.453.1.58/1/
https://www.codeproject.com/Questions/494701/Can-27tplusfigureplusoutpluswhyplusthisplusDynamic