工具 Dotnet IL Editor 推荐
Dotnet IL Editor是一款.NET平台反编译工具,可以反编译.NET程序集文件为IL代码,并且可以执行,调试反编译后生成的IL代码。它的设计出发点比较直观,新建一个项目,添加程序集文件,设置断点(F9),然后就可以调试反编译后的程序集文件,Step Into,Step Out均可,以此原理,可以找到系统的瓶颈代码,也可以深入的学习MSIL微软中间语言。
先建立一个C#控制台项目,设计一个数字相加的方法,并在Main方法中调用它
程序总共不到10行,把数字1和2相加,输出到控制台。
public class TestEditor { public static int Sum(int a, int b) { return a + b; } public static void Main(string [] args) { Console.WriteLine(Sum(1,2)); Console.ReadLine(); } }
执行程序Dile.exe, 新建一个项目,在项目浏览器中点右键添加程序集,一层层的展开该程序集,如下图所示
与Visual Studio的解决方案浏览器相似,它按照命名空间展开,列出了程序集中的方法,双击该方法,可以在编辑器中打开IL源代码,IL编辑器窗口是只读的。
探索.NET代码的奥秘
记得.NET教材中有一条原理知识,类型默认继承自object类,当没有为类型定义构造方法(ctor)时,.NET编译器会为它生成一个默认的构造方法,该方法不带参数。如果在阻止默认生成的构造方法,只需要为类型定义一个方法,即可阻止编译器的这个行为。
为什么不现在就试一下这条原理是否正缺,或是有什么遗漏的地方。于是修改类型定义的方法,给它加上一个带参数的构造方法,该方法为一个空方法,带参数以区别是.NET生成还是我们手动添加的。源代码是这样的
public class TestEditor { public TestEditor(string str) { } public static int Sum(int a, int b) { return a + b; } public static void Main(string [] args) { Console.WriteLine(Sum(1,2)); Console.ReadLine(); } }
TestEditor类型现在有一个自定义的构造方法,带一个字符串参数。再次到IL Editor中打开,看看它生成的IL代码
果然如此,生成的IL代码中,没有默认的构造方法,取而代之的是我们定义的构造方法。
再来验证一条知识点,const常量会被以常量的形式编译到程序集中,因而它的效率很高。再修改C#源代码,编译,IL Editor中打开,C#的源代码现在看起来是这样的
public class TestEditor { public const string ProductName="Enterprise Solution"; public TestEditor(string str) { } public static int Sum(int a, int b) { return a + b; } public static void Main(string [] args) { string productionName = ProductName; Console.WriteLine(productionName); Console.WriteLine(Sum(1,2)); Console.ReadLine(); } }
再来看生成的IL代码,它显示的结果如下
反编译后的结果与我们曾看到过的知识点一样,验证所学无误。
在源代码中,控制台主方法中,它调用1+2求和的方法,也是以常量的形式编译进程序集。
Console.WriteLine(Sum(1,2));
对应的.NET IL代码是
ldc.i4.1
ldc.i4.2
call int32 ILEditorLibrary.TestEditor::Sum(int32, int32)
call void [mscorlib]System.Console::WriteLine(int32)
IL是基于堆栈的语言,先压入值1,再压入值2,再对二者求和,最后调用方法显示到控制台上。
调试IL代码
IL Editor的调试功能是它的亮点之一,可以在打开的IL代码中直接设置断点(F9, Toogle breakpoint),然后点击工具栏中的Run,启动调试。调试程序的几个要点,看堆栈Stack, 看变量值Watch,IL Editor都可以做到。
IL堆栈窗口
IL参数窗口,可显示当前被调用的方法的传入参数
IL Watch窗口
表达式可以支持自定义的表达式,这个功能与VS中的即时窗口类似,输入表量或表达式,右边计算出结果。
IL Editor的作者列举出了一些经过测试,演示的表达式代码,他们是
5 * -6 1 + 2 * 3 - 10 / 5 * 5 (1 + 2 * 3 - 10 / 5 * 5).ToString() (-5).ToString() new object() + "a" "abc".Length.ToString() System.Type.GetType("System.String").GUID.ToByteArray() TestApplication.DebugTest.CreateOperatorTest4("op1") | true TestApplication.DebugTest.ParamsTest2() TestApplication.DebugTest.ParamsTest2(5, 6) System.String.Format("{0}{1}{2}{3}{4}", "a", "b", "c", "d", "e") new object[] {4, "a", 5} ((System.Exception){exception}).Message TestApplication.GenericClass<int, System.DateTime>.StaticMethod<string>("test") new TestApplication.TestClass<int, string>[] {new TestApplication.TestClass<int, string>(1, "one")} TestApplication.GenericClass<int, string>.NestedGenericClass<System.Type>.StaticMixedMethod<System.DateTime>(System.DateTime.Now, 5, null)
是的,你可以直接在窗口中输入,IL会对表达式求值,返回结果显示到右边。
最后看到执行结果
控制上显示输出值,与在VS中调试C#源代码的体验完全相同。
学习IL指令集
IL Editor的作者肯定想到IL语言不容易记住和熟悉,当你在IL Editor中移动鼠标时,随着鼠标当前行显示的IL代码的不同,在IL Instructions窗口中,会及时的显示它的方法注释,并显示MSDN地址,可以直接点击进入该IL方法的文档。
设计的非常贴心周到。
Click here直接挑到MSDN网页中。
http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.Nop.aspx
对于我们习惯的语言来说,还没有看到有相关的设置,可以用来设置跳转到中文版的MSDN上。
命令行调用
IL Editor支持命令行方式的调用,命令行的参数列表如下所示
dile [/p "Project name"] [/a "assembly path"] [/l "project name.dileproj"]
/p Optional. When DILE is loaded, a new project will be created with the given name.
/a Optional, can be repeated. When DILE is loaded, a new project will be created and the given assemblies will be added to it.
/l Optional. DILE will load the given dileproj file. If this parameter is given then /p and /a will be ignored. If a parameter is followed by a name/path which contains spaces then it should be written between quotes
创建一个Test project的项目
dile /p "Test project"
创建一个Test project的项目,并给它添加程序集
dile /p "Test project" /a TestAssembly.exe
创建一个新项目,并从两个不同的地方加载程序集
dile /a TestAssembly.exe /a "c:\assemblies\My test.dll"
加载一个现有的项目
dile /l TestProject.dileproj
最后,附上项目主页地址
项目主页: http://dile.sourceforge.net/
作者博客地址:http://pzsolt.blogspot.com
其实吧,内存泄露一直是个令人头疼的问题,在带有GC的语言中这个情况得到了很大的好转,但是仍然可能会有问题。
一、什么是内存泄露(memory leak)?
内存泄露不是指内存坏了,也不是值内存没插稳漏出来了,简单来说,内存泄露就是在你期待的时间内你程序所占用的内存没有按照你想象中的那样被释放。
因此什么是你期待的时间呢?明白这点很重要。如果一个对象占用内存的时间和包含这个对象的程序一样长,但是你并不期望是这样。那么就可以认为是内存泄露了。用具体例子来说明如下:
class Button { public void OnClick(object sender, EventArgs e) { ... } } class Program { static event EventHandler ButtonClick; static void Main(string[] args) { Button button = new Button(); ButtonClick += button.OnClick; } }
上面这段代码中,我们使用了一个静态的事件,而静态成员的生命周期是从AppDomain被加载开始,直到AppDomain被卸载,也就是说在通常情况下如果进程没被关闭,又忘记取消注册事件,那么ButtonClick事件包含的EventHandler委托所引用的对象会一直存在到进程结束为止,这就造成了内存泄露问题。这也是.NET中最常见的内存泄露问题的原因之一。后面我会接着说怎么解决这种事件造成的泄露问题。
二、内存回收的方式
1、引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个 变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办 法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
像原来IE6中Javascript中原生对象内存回收的方式就是通过检查对象是否有引用来判断一个对象是否是垃圾。IE9之前,其BOM和DOM中的对象是使用C++以COM对象的形式实现的,而COM对象的垃圾收集机制采用的也是引用计数策略。而这种方式通常会因为循环引用导致内存泄露,也就是A引用B的同时,B也引用者A。 在Object C中也会有这样的循环引用的问题。在Object C中的解决方案就是给一方标记为weak,介绍可以参看这里, 关于Object C中的委托模式的介绍。
2、标记清除法(mark-weep)
C#中采用的是标记法回收内存,全部对象都要标记,并且只标记一次就不再标记。判断一个对象是不是垃圾取决于是否有引用,而是取决是是否被root引用。
root的类型有寄存器中的变量,线程栈上的变量,静态变量等。
我们来看一幅通常情况下的对象图,图中有一个循环引用。
我们抽取其中一部分图说明
在采用标记清除策略的实现中,由于函数执行之后,local3出栈,离开了作用域,因此这种相互引用在标记清除法中不是个问题。
我们很容易看出,因为每一个对象都要mark,因此创建大量的小对象会给Mark阶段造成压力。值得注意的是,在GC的mark 和weep阶段,会挂起所有线程,因此创建大量的线程也是会对GC造成问题。这个问题我以后会再讨论。
三、弱引用解决一些问题
如前面所说,忘记取消注册事件通常是.NET中最常见的内存泄露问题,我们怎么自动化的解决这个问题呢?也就是说当方法所属的对象已经被标记为垃圾的时候,我们就在事件中取消注册这个方法。这时就可以通过弱引用来实现。
委托的本质就是一个类,包含了几个关键属性
1.指向原对象的Target属性(强引用)。
2.一个指向方法的ptr指针。
3.内部维护着一个集合(delegate是以链表结构实现)
因为.NET中的委托是强引用,我们要把它改成弱引用,我们可以抓住这个这些特征,创建一个自己的WeakDelegate类。
事件的本质就是一个访问器方法,和委托的关系类似于字段和属性,也就是控制外部对字段的访问。我们可以通过自定义add和remove方法来把外部的委托转换成我们自己定义的委托。
public class Button { private class WeakDelegate { public WeakReference Target; public MethodInfo Method; }
private List<WeakDelegate> clickSubscribers = new List<WeakDelegate>();
public event EventHandler Click { add { clickSubscribers.Add(new WeakDelegate { Target = new WeakReference(value.Target), Method = value.Method }); } remove
{ ..... } }
public void FireClick() { List<WeakDelegate> toRemove = new List<WeakDelegate>(); foreach (WeakDelegate subscriber in clickSubscribers) {
//第一个Target表示方法所属的对象,第二个Target表示这个对象是否被标记为垃圾,如果为null则表示为已经被标记为垃圾。 object target = subscriber.Target.Target; if (target == null) { toRemove.Add(subscriber); } else { subscriber.Method.Invoke(target, new object[] { this, EventArgs.Empty }); } } clickSubscribers.RemoveAll(toRemove); } }
弱引用还可以用来创建一个对象池,对象池就是通过管理少量的对象来减少内存和GC压力。我们可以通过强引用来表示对象池内最小的对象数量,通过弱引用来表示可以达到的最大的数量。
如果你觉得这篇文章有帮助,请点击一下右下角的推荐,欢迎讨论。