一步步教你读懂NET中IL(图文详解)
接触.NET已经一年有余,对其内部实现的始终让我着迷。我认为,深入了解这些底层实现对于提高编程技能有着极大的帮助。今天,我想通过一个实际的例子来让大家了解堆叠式VM的运作原理,并对.NET IL有一个基本的了解。
让我们来看看.NET CLR和Java VM。它们都是采用堆叠式虚拟机器(Stack-Based VM)的方式运行。这意味着它们的指令集都是基于堆叠运算的,执行时的数据都会先放在堆叠中,再进行运算。Java VM有大约200个指令,每个指令都是1byte的操作码,后面跟着不同数量的参数。而.NET CLR有超过220个指令,其中一些指令使用相同的操作码,因此操作码的数目略少于指令数。值得注意的是,.NET的操作码长度并不固定,大部分操作码长度为1byte,少部分为2byte。
接下来,让我们通过一个简单的C代码示例来了解.NET IL。代码如下:
```csharp
using System;
public class Test
{
public static void Main(String[] args)
{
int i = 1;
int j = 2;
int k = 3;
int answer = i + j + k;
Console.WriteLine("i+j+k=" + answer);
}
}
```
将这个源代码编译后,会得到一个EXE程序。我们可以通过ILDASM.EXE来反编译EXE以查看IL。将Main()的IL反编译后,我们得到了十八条IL指令。其中,有的指令(如ldstr和box)需要接参数,有的(如ldc.i4.1和add)则不需要。
在执行此程序时,涉及到三种关键的内存:Managed Heap、Call Stack和Evaluation Stack。Managed Heap是动态配置的记忆体,由Garbage Collector(GC)自动管理。Call Stack是.NET CLR在执行时自动管理的内存,每个Thread都有自己的Call Stack。每当调用一个method时,Call Stack上就会多一个Record Frame,调用结束后,这个Record Frame会被丢弃。Evaluation Stack也是由.NET CLR在执行时自动管理的内存,同样每个Thread都有自己专属的Evaluation Stack。前面提到的堆叠式虚拟机器就是指的这个堆叠。
为了更直观地解释这三种内存的变化,后面附有一系列示意图。在进入Main()后,尚未执行任何指令前,内存的状况如图1所示。接着,执行第一道指令ldc.i4.1,该指令的意思是在Evaluation Stack上置入一个4byte的常数,其值为1。
希望通过这个实际的例子,大家能对堆叠式VM的运作原理和.NET IL有一个基本的了解。在未来的和实践中,这些知识点将为你提供更深层次的帮助和理解。经过执行一系列指令后,程序中的记忆体发生了相应的变化。通过`ldc.i4.1`指令,将值1加载到堆栈中,这是初始化过程的一部分。随后,`stloc.0`指令将栈顶的值存储到第0号变量(即i)中。这个过程以及其他类似指令的执行,都会在内存中有相应的变化,如图2至图7所示。
紧接着,第七道指令`ldloc.0`和第八道指令`ldloc.1`分别将V0(即i)和V1(即j)的值放到计算堆栈上,这是为了准备进行加法运算。这个过程如图8和图9所示。
随后,`add`指令从计算堆栈中取出两个值(即i和j),将它们相加,并将结果放回计算堆栈中。这个过程如图10所示。
接下来,第十道指令`ldloc.2`将V2(即k)的值放到计算堆栈上,为第二次加法运算做准备。然后,第十一道`add`指令将k的值与之前的和相加,得到i+j+k的最终结果。
第十二道指令`stloc.3`将计算堆栈中的最终结果存储到第3号变量(即answer)中。这个过程如图13所示。
之后,第十三道指令`ldstr "i+j+k="`将字符串"i+j+k="的引用放入计算堆栈中。紧接着,第十四道指令`ldloc.3`将V3(即answer)的值放入计算堆栈中,为输出结果做好准备。至此,程序完成了对变量i、j、k的加法运算,并将结果存储在了answer变量中。
整个过程生动地展示了如何通过一系列指令在程序中实现数据的加载、存储、加法运算以及结果的存储与输出。这些指令的执行导致了内存状态的变化,最终得到了预期的运算结果。揭开内存变化的神秘面纱:一次深入.NET指令级的之旅
当我们执行一道指令后,内存的变化如同舞台上的灯光,照亮了我们平时难以察觉的幕后世界。让我们跟随这次指令级的之旅,深入理解IL(中间语言)在.NET程序中的运作机制。
我们迎来了第十五道指令——box [mscorlib]System.Int32。这一步将值类型(Value Type)包装为引用类型(Reference Type)。这个过程被称为装箱操作,它在int到string的转换中尤为关键。装箱操作意味着性能损失,在编码过程中尽量减少装箱操作是提高性能的有效手段。执行这道指令后,内存中的变化如“图15”所示。
紧接着是第十六道指令——call string [mscorlib] System.String::Concat(object, object)。此指令从评价栈(Evaluation Stack)中取出两个值,调用System.String的Concat()方法进行字符串拼接。值得注意的是,由于String.Concat()是静态方法,这里使用的是call指令而非callvirt指令。完成此指令后,内存如“图16”所示发生了改变。一些不再被引用的对象变成了垃圾,等待垃圾回收器(GC)的处理。
随后是第十七道指令——call void [mscorlib] System.Console::WriteLine(string)。此指令将拼接后的字符串显示在控制台窗口上。完成此操作后,内存状态如“图17”所示。
我们迎来了第十八道指令——ret。这是结束此次调用的指令。此时会检查评价栈内的剩余数据,由于Main()方法不需要返回值(void),所以评价栈内必须为空。在符合这种情况后,此次调用才能顺利结束,程序也随之结束。完成此指令后,内存如“图18”所示发生变化。随着程序的结束,评价栈的清空标志着此次指令执行旅程的终结。
通过这次范例,读者应该能够对IL有了基本的认识。对IL感兴趣的读者可以进一步阅读Serge Lidin所著的《Inside Microsoft .NET IL Assembler》,深入了解IL的每一条指令的作用。作为.NET程序员,了解IL是每个开发者必备的知识。虽然可能并不需要直接使用IL Assembly写程序,但至少应该能看懂ILDASM反编译出的IL代码。
平面设计师
- 一步步教你读懂NET中IL(图文详解)
- 解析数组非数字键名引号的必要性
- ASP.NET MVC使用ActionFilterAttribute实现权限限制的方法
- jQuery+css实现炫目的动态块漂移效果
- 微信开发之微信jssdk录音功能开发示例
- jQuery插件zTree实现清空选中第一个节点所有子节点
- Yii模型操作之criteria查找数据库的方法
- JS表单数据验证的正则表达式(常用)
- asp中日期时间函数介绍
- laravel5.4生成验证码的代码
- 很实用的js选项卡切换效果
- Servlet+Jsp实现图片或文件的上传功能具体思路及代
- 在AngularJS应用中实现一些动画效果的代码
- php实现过滤UBB代码的类
- vue最简单的前后端交互示例详解
- vue项目实现记住密码到cookie功能示例(附源码)