关注底层:IL部分
2009-06-02 11:08 横刀天笑 阅读(4466) 评论(20) 编辑 收藏 举报为了达成共识,我们先对一些概念回忆一下:
CPU只能执行机器码,不能执行IL
这个应该没有什么疑问吧。机器码就是传说的0、1的组合,虽然今天的CPU运算速度已经非常非常快,而且非常非常智能,但它和上个世纪的CPU还是一样,还只能认识0、1的组合的这种机器码。
说到IL就应该提一提编译器的前端和后端。众所周知,微软的.NET平台上有众多的语言,大家熟悉的有C#、VB.NET、Jscript.NET,而这些不同语法的“高级”语言,经过CSC.Exe(C#编译器)、VBC.Exe(VB.NET编译器)等编译后得到IL,这之前的部分我们称之为前端,实际上事情并没有到这里结束,当CLR加载托管程序集,运行某一个方法时,CLR发现这个方法还没有被即时编译(JIT),这个时候就会调用JIT编译器对这个方法的IL编译,编译的结果就是我们的目标代码(目标代码可以是汇编代码或机器码,这里不加以区分)。而这之后的JIT编译等过程我们可以认为是编译器的后端。
有了上面这段描述,各位同学大脑中应该有这样一幅画面:
通过这幅图我们看到,微软通过实现不同的前端,而共一个后端实现了一个平台,多种语言的目标。这也就是为什么你用VB.NET写的组件,我用C#可以直接使用,甚至是
我用C#写的类直接派生自一个VB.NET写的类。
因为IL相对于机器码来说相对简单,因为IL不能操作寄存器。所以你甚至可以自己定义一个语法,然后实现一个编译器的“前端”,将你自己的语言加入到.NET这个大家族
中(貌似园子里的装配脑袋正在做这方面的工作)。这样你自己的语言也可以享受.NET的类库了,.NET的垃圾回收机制了。
从这里我们了解到IL起一个桥梁的作用。那好,我们学习IL到底可以干些什么?
1:探究C#这些编译器内部所作所为:
记得以前博客园有一场对访问集合对象时,使用for好还是foreach好的大讨论,那我就用这个例子来用IL说明一下使用for访问
{
ArrayList arr = new ArrayList();
for (int i = 0; i < arr.Count; i++)
{
object o = arr[i];
}
}
对应IL代码如下:
{
//标明这是本程序的入口点
.entrypoint
// Code size 32 (0x20)
.maxstack 2
//声明两个局部变量,一个ArrayList类型的arr,一个用在for循环中的整型i
.locals init ([0] class [mscorlib]System.Collections.ArrayList arr,
[1] int32 i)
//调用ArrayList的构造函数,实例化对象,并将对象的引用放到IL的运算栈上
IL_0000: newobj instance void [mscorlib]System.Collections.ArrayList::.ctor()
//将IL运算栈上顶部的一项弹出,赋值给本方法的第一个局部变量
IL_0005: stloc.0
//将一个四字节的整型0放到IL的运算栈上
IL_0006: ldc.i4.0
//将IL运算栈顶部的一项弹出,复制给本方法的第二个局部变量
IL_0007: stloc.1
//跳转到IL_0016处
IL_0008: br.s IL_0016
//将本方法的第一个局部变量加载到运算栈顶部
IL_000a: ldloc.0
//将本方法第二个局部变量加载到运算栈顶部
IL_000b: ldloc.1
//弹出运算栈最顶部项,在这里就是arr,调用arr的get_Item(int32)方法,并且将结果放//到运算栈顶部
IL_000c: callvirt instance object [mscorlib]System.Collections.ArrayList::get_Item(int32)
//弹出运算栈顶部,在这里注意到,C#的代码将arr[i]赋值给了object对象,但为什么没有//对应IL代码呢,原来C#编译器发现这个object对象o并没有什么用,所以就直接丢弃了,//呵呵,还挺智能的
IL_0011: pop
//加载本方法第二个局部变量到运算栈顶部
IL_0012: ldloc.1
//加载四字节整型1到运算栈顶部
IL_0013: ldc.i4.1
//弹出运算栈顶部两项,相加,将相加后的结果放到运算栈顶部
IL_0014: add
//将运算栈顶部一项赋值给本方法的第二个局部变量(聪明的你从这里应该可以猜测得到,//这里就是for循环中的i++了)
IL_0015: stloc.1
//将本方法的第二个局部变量加载到IL运算栈
IL_0016: ldloc.1
//将本方法的第一个局部变量加载到IL运算栈
IL_0017: ldloc.0
//弹出运算栈顶部一项,调用该项的get_Count()方法,从上面代码可以看出运算栈顶部一//项就是本方法的第一个局部变量,也就是arr,方法调用后的结果会保存到运算栈顶部(从//这里也可以看出读取C#里的Count属性原来就是调用get_Count()方法,这也是IL的一个作//用,你现在应该明白了为什么大家都说.NET中的属性其实是两个方法吧)
IL_0018: callvirt instance int32 [mscorlib]System.Collections.ArrayList::get_Count()
//弹出运算栈顶部的两项,比较大小,在这里就是arr.Count和i,如果i小于arr.Count,则//跳转到IL_000a
IL_001d: blt.s IL_000a
//不用多说,退出方法
IL_001f: ret
}
上面就是Main这个方法对应的IL代码,从注释中可以看出,IL是基于一个运算栈的,所有的操作数就是从这运算栈里倒来倒去,运算栈也是一个虚拟的概念,
不要想着把它与物理计算机里的内存或寄存器相对应起来,因为IL本来就是运行在CLR这个虚拟机之上的。看了使用for访问的代码,我们来看看使用foreach访问的代码
(与上面相同的部分我就不注释了):
{
.entrypoint
// Code size 50 (0x32)
.maxstack 1
//声明四个局部变量啊
.locals init ([0] class [mscorlib]System.Collections.ArrayList arr,
[1] object item,
[2] class [mscorlib]System.Collections.IEnumerator CS$5$0000,
[3] class [mscorlib]System.IDisposable CS$0$0001)
IL_0000: newobj instance void [mscorlib]System.Collections.ArrayList::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
//注意,调用ArrayList的GetEnumerator()方法,获取ArrayList的迭代器
IL_0007: callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Collections.ArrayList::GetEnumerator()
IL_000c: stloc.2
//呵呵,还加了一个try,为的是能在finally的时候调用Dispose()方法
.try
{
IL_000d: br.s IL_0016
IL_000f: ldloc.2
//不再是使用ArrayList的get_Item(int32)方法了,是使用迭代器的get_Current()
IL_0010: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
IL_0015: stloc.1
IL_0016: ldloc.2
//调用迭代器的MoveNext方法
IL_0017: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001c: brtrue.s IL_000f
IL_001e: leave.s IL_0031
} // end .try
finally
{
IL_0020: ldloc.2
IL_0021: isinst [mscorlib]System.IDisposable
IL_0026: stloc.3
IL_0027: ldloc.3
IL_0028: brfalse.s IL_0030
IL_002a: ldloc.3
IL_002b: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0030: endfinally
} // end handler
IL_0031: ret
}
哦,这样一比较,for和foreach的不同点就自然而然的明白了。对于这些“语法糖”,我们只要打开IL一切都明明白白,C#
3.0的语法糖为数众多,为什么很多
人知道他到底是怎样实现的呢,还有很多人叫嚷着,这没什么,语法糖而已,因为大家都明白IL没变啥,只是编译器使坏。
2.加密解密
加密解密在Win32时代就有之,不过自从.NET的出现这块就更容易了,很多加密的方法,只需要打开IL,然后改一点东西,再用微软提供的ILAsmer汇编回去,这样就破解了。
这块内容很复杂,园子里也有很多文章。
那学IL有没有用呢?当然有用。学习IL你可以看到表面上看不到的东西,如果你是一位痴迷于技术的同学,那IL就应该好好学学了,把CLR看做“计算机”,那IL就是CLR的“本地代码”
(实际上不正确,CLR自己并不直接运行IL,CLR还是需要将IL编译为真正的本地代码然后执行)。
.NET的水很深,实际上针对编译器前端这部分水不是很深。内核或原理部分都是在CLR这部分,比如程序集如何加载?方法如何编译?多态的实现方法?那要探究这部分的原理怎么办?这个
时候从IL就无法知晓了,当然你可以google之或百度之,你也可以买很多大牛的书籍看看。不过你想不想自己弄明白呢?毕竟如果不自己加以试验得出结论,那些东西我觉得还是书本上的,
我们获得的知识也是靠记忆获得的,如果你能使用一个调试工具,亲自打开JIT之后的结果,你就知道多态到底是咋回事?
要知后事,请听下回分解!
本来想写三篇的:
关注底层(上):IL部分
关注底层(中):汇编 for .NETer
关注底层(下):Why and How
不过发现我想讨论的内容老赵已经说了,而且说得更好,所以觉得后面两篇也没有必要出了。所以就留此一篇作为纪念吧。