代码改变世界

关注底层:IL部分

2009-06-02 11:08  横刀天笑  阅读(4463)  评论(20编辑  收藏  举报
园子里两个大牛正争的如火如荼,小生不才,借一下两个名人的名气也来谈一下Microsoft intermediate language MSIL,就是大家口里的IL)和ASM(这里指针对X86汇编,排除其他一切“高级汇编”)。

为了达成共识,我们先对一些概念回忆一下:

CPU只能执行机器码,不能执行IL

这个应该没有什么疑问吧。机器码就是传说的01的组合,虽然今天的CPU运算速度已经非常非常快,而且非常非常智能,但它和上个世纪的CPU还是一样,还只能认识01的组合的这种机器码。

说到IL就应该提一提编译器的前端和后端。众所周知,微软的.NET平台上有众多的语言,大家熟悉的有C#VB.NETJscript.NET,而这些不同语法的“高级”语言,经过CSC.ExeC#编译器)、VBC.ExeVB.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访问

 static void Main(string[] args)
        {
            ArrayList arr 
= new ArrayList();
            
for (int i = 0; i < arr.Count; i++)
            { 
                
object o = arr[i];
            }
}

对应IL代码如下:

.method private hidebysig static void  Main(string[] args) cil managed
{
//标明这是本程序的入口点
  .entrypoint
  
// Code size       32 (0x20)
  .maxstack  2
 
//声明两个局部变量,一个ArrayList类型的arr,一个用在for循环中的整型i
  .locals init ([0class [mscorlib]System.Collections.ArrayList arr,
           [
1int32 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访问的代码
(与上面相同的部分我就不注释了):

.method private hidebysig static void  Main(string[] args) cil managed
{
  
.entrypoint
  
// Code size       50 (0x32)
  .maxstack  1
//声明四个局部变量啊
  .locals init ([0class [mscorlib]System.Collections.ArrayList arr,
           [
1object item,
           [
2class [mscorlib]System.Collections.IEnumerator CS$5$0000,
           [
3class [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自己并不直接运行ILCLR还是需要将IL编译为真正的本地代码然后执行)。

 

.NET的水很深,实际上针对编译器前端这部分水不是很深。内核或原理部分都是在CLR这部分,比如程序集如何加载?方法如何编译?多态的实现方法?那要探究这部分的原理怎么办?这个
时候从
IL就无法知晓了,当然你可以google之或百度之,你也可以买很多大牛的书籍看看。不过你想不想自己弄明白呢?毕竟如果不自己加以试验得出结论,那些东西我觉得还是书本上的,
我们获得的知识也是靠记忆获得的,如果你能使用一个调试工具,亲自打开
JIT之后的结果,你就知道多态到底是咋回事?

要知后事,请听下回分解!

 

 本来想写三篇的:

关注底层(上):IL部分

关注底层(中):汇编 for .NETer

关注底层(下):Why and How

不过发现我想讨论的内容老赵已经说了,而且说得更好,所以觉得后面两篇也没有必要出了。所以就留此一篇作为纪念吧。