浅谈 C# Assembly 与 IL (一):C# Assembly 与 Reflection
作者:Compasslg
前言
前一阵子想利用闲余时间写一个 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
浅谈 C# Assembly 与 IL (一):C# Assembly 与 Reflection的更多相关文章
- 【小白学C#】浅谈.NET中的IL代码
一.前言 前几天群里有位水友提问:”C#中,当一个方法所传入的参数是一个静态字段的时候,程序是直接到静态字段拿数据还是从复制的函数栈中拿数据“.其实很明显,这和方法参数的传递方式有关,如果是引用传递的 ...
- 【Unity游戏开发】浅谈Lua和C#中的闭包
一.前言 目前在Unity游戏开发中,比较流行的两种语言就是Lua和C#.通常的做法是:C#做些核心的功能和接口供Lua调用,Lua主要做些UI模块和一些业务逻辑.这样既能在保持一定的游戏运行效率的同 ...
- 浅谈.NET编译时注入(C#-->IL)
原文:浅谈.NET编译时注入(C#-->IL) .NET是一门多语言平台,这是我们所众所周知的,其实现原理在于因为了MSIL(微软中间语言)的一种代码指令平台.所以.NET语言的编译就分为了两部 ...
- 浅谈 .NET 中的对象引用、非托管指针和托管指针 理解C#中的闭包
浅谈 .NET 中的对象引用.非托管指针和托管指针 目录 前言 一.对象引用 二.值传递和引用传递 三.初识托管指针和非托管指针 四.非托管指针 1.非托管指针不能指向对象引用 2.类成员指针 五 ...
- 浅谈WebService的版本兼容性设计
在现在大型的项目或者软件开发中,一般都会有很多种终端, PC端比如Winform.WebForm,移动端,比如各种Native客户端(iOS, Android, WP),Html5等,我们要满足以上所 ...
- 浅谈Hex编码算法
一.什么是Hex 将每一个字节表示的十六进制表示的内容,用字符串来显示. 二.作用 将不可见的,复杂的字节数组数据,转换为可显示的字符串数据 类似于Base64编码算法 区别:Base64将三个字节转 ...
- 【转】Android Canvas的save(),saveLayer()和restore()浅谈
Android Canvas的save(),saveLayer()和restore()浅谈 时间:2014-12-04 19:35:22 阅读:1445 评论:0 收藏: ...
- [C#]6.0新特性浅谈
原文:[C#]6.0新特性浅谈 C#6.0出来也有很长一段时间了,虽然新的特性和语法趋于稳定,但是对于大多数程序猿来说,想在工作中用上C#6.0估计还得等上不短的一段时间.所以现在再来聊一聊新版本带来 ...
- 【ThinkingInC++】8、说明,浅谈数据类型的大小
/** * 特征:说明.浅谈数据类型的大小 * 时刻:2014年8一个月10日本11:02:02 * 笔者:cutter_point */ #include<iostream> using ...
随机推荐
- vue项目配置 `webpack-obfuscator` 进行代码加密混淆
背景 公司代码提供给第三方使用,为了不完全泄露源码,需要对给出的代码进行加密混淆,前端代码虽然无法做到完全加密混淆,但是通过使用 webpack-obfuscator 通过增加随机废代码段.字符编码转 ...
- 微信小程序:block标签
代码中存在block标签,但是渲染的时候会移除掉. 例子: 如果将view改为block: 当你要渲染某些数据时,如果不想额外的加一层外边的标签,此时可以使用block标签来进行占位.
- pytorch(13)卷积层
卷积层 1. 1d/2d/3d卷积 Dimension of Convolution 卷积运算:卷积核在输入信号(图像)上滑动,相应位置上进行乘加 卷积核:又称为滤波器,过滤器,可认为是某种模式,某种 ...
- spring 整合kafka监听消费
前言 最近项目里有个需求,要消费kafka里的数据.之前也手动写过代码去消费kafka数据.但是转念一想.既然spring提供了消费kafka的方法.就没必要再去重复造轮子.于是尝试使用spring的 ...
- C语言入门--初来乍到
Hi,我是fish-studio,这是我写的第一篇博客,接下来我会以萌新的角度来与大家一起学习C语言,我也不是什么大佬,在我写的教程中会尽量详细的把我遇到的问题写出来,也会结合一些网上的文章进行编写, ...
- BZOJ_2844 albus就是要第一个出场 【线性基】
一.题目 albus就是要第一个出场 二.分析 非常有助于理解线性基的一题. 构造线性基$B$后,如果$|A| > |B|$,那么就意味着有些数可以由$B$中的数异或出来,而多的数可以取或者不取 ...
- java mvc 及其缓存
使用Spring框架的好处是什么? - 轻量:Spring 是轻量的,基本的版本大约2MB. - 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们. ...
- python-类的隐藏和封装
7 """ 8 封装是面对对象的三大特征之一(另外两个是集成和多态),它指的是将对象> 的信息隐藏在对象的内部,不允许外部程序直接访问对象内部信息,而是通> ...
- [系统重装日志2]win10系统安装pytorch0.4.1(gpu版本)
目录 0,资源整理 1,安装最新版的显卡驱动 2,安装visual studio 3,安装cuda 4,安装cudnn,配置环境变量 5,安装pytorch 6,安装torchvision 7,验证 ...
- CentOS7使用NTP搭建时间同步服务器
前言 为什么要搭建时间同步服务器呢?场景是这样的. 我们有两台CentOS服务器,其中一台是可以连接外网的,下文中我们称它为A服务器,另一台不能连接外网,但是与A服务器在同一局域网中,下文中我们称它为 ...