il中间语言-张银-博客园


il中间语言-张银-博客园
2012年01月14日
  一、IL与汇编语言
  IL(Intermediate Language)是微软.NET平台上衍生出的一门中间语言,.NET平台上的各种高级语言(如C#,VB,F#)的编译器会将各自的代码转化为IL。,其中包含了.NET平台上的各种元素,如"范型"、"类"、"接口"、"模块"、"属性"等等。值得注意的是,各种高级语言本身可能根本没有这些"概念"在里头,如IronScheme是一个在.NET平台上的Scheme语言实现,其中根本没有前面提到的这些IL--亦或说是.NET平台上的名词。IL本身并不知道自己是由哪种高级语言转化而来的,哪种语言中有哪些特性,IL也根本不会关心。
  各种语言的编译器将: 高级语言 => IL。
  汇编是让CPU直接使用的"语言",请注意"直接"二字:一条汇编指令便是让CPU作一件事情(如寄存器的复制,从内存中读取数据等等),毫无二义。不同族CPU拥有不同的指令集,但是它们都有一样的特征:指令的数量相对较少,每个指令功能都简单之至。
  由于CPU只认识汇编代码(机器码和汇编其实也是一一对应的,您可以这样理解:汇编是机器码的文字表现形式,提供了一些方便人们记忆的"助记符"),因此就算是IL也需要再次进行转化,才能被CPU执行。这次转化便由 "JIT Compiler"( 即时编译器)完成。CLR加载了IL之后,当每个方法--请注意这是IL中的概念--第一次被执行时,就会使用JIT将IL代码进行编译为机器码。与IL不同的是,CLR,JIT都是真正了解CPU的,对于同样的IL,JIT会把它为不同的CPU架构(如x86/IA64等等)生成不同的机器码。这也是 Java/.NET中"Compile Once,Run Everywhere"这一口号的技术基础:它们为不同的CPU架构提供了不同的"IL转化器",仅此而已。与高级语言到IL的转化类似,CPU也完全不知道自己在执行的指令是从哪里来的,可能是JIT从IL转化而来,可能是JVM从Java Bytecode转化而来,也有可能是C语言编译得来,也有可能是由MIT/GNU Scheme解释而来。
  这就是.NET平台上的高级语言在机器上运行的第二次转化:IL => 汇编(机器码)。
  因此,IL和汇编的区别是显著的。IL拥有各种高级特性,它知道什么是范型,什么是类和方法(以及它们的"名称"),什么是继承,什么是字符串,布尔值,什么是User对象。而CPU只知道寄存器,地址,内存,01010101。与汇编相比,IL简直太高级了,几乎完全是一个高级语言,比C语言还要高级。因此,您会看到.NET Reflector几乎可以把IL代码"一五一十"地反编译为可读性良好的C#代码,包括类,属性,方法等等;而从汇编只能勉勉强强地反编译为C语言-- 而且其中的"方法名"等信息已经完全不可恢复了,更别说"模块"等高级抽象的内容。您想要把汇编反编译成C#代码?相信在将来这是可行的,不过现在这还是天方夜谭。 
  二、IL并不是万能的,CLR还有很多内容IL都无法看到
  示例一:探究泛型在某些情况下的性能问题 namespace TestConsole
  {
  publicclass MyArrayList
  {
  public MyArrayList(int length)
  {
  this.m_items = newobject[length];
  } 
  privateobject[] m_items; 
  publicobjectthis[int index]
  {
  [MethodImpl(MethodImplOptions.NoInlining)]
  get
  {
  returnthis.m_items[index];
  }
  [MethodImpl(MethodImplOptions.NoInlining)]
  set
  {
  this.m_items[index] = value;
  }
  }
  } 
  publicclass MyList
  {
  public MyList(int length)
  {
  this.m_items = new T[length];
  } 
  private T[] m_items; 
  public T this[int index]
  {
  [MethodImpl(MethodImplOptions.NoInlining)]
  get
  {
  returnthis.m_items[index];
  }
  [MethodImpl(MethodImplOptions.NoInlining)]
  set
  {
  this.m_items[index] = value;
  }
  }
  } 
  class Program
  {
  staticvoid Main(string[] args)
  {
  MyArrayList arrayList = new MyArrayList(1);
  arrayList[0] = arrayList[0] ?? newobject(); 
  MyList list = new MyList(1);
  list[0] = list[0] ?? newobject();
  Console.WriteLine("Here comes the testing code."); 
  var a = arrayList[0];
  var b = list[0];
  Console.ReadLine();
  }
  }
  }
  示例目的是证明".NET中,就算在使用Object作为泛型类型的时候,也不会比直接使用Object类型性能差"。类MyList泛型容器,类MyArrayList直接使用Object类型的容器。在Main方法中将对 MyList和MyArrayList的下标索引进行访问。至此,便出现了一些疑问,为泛型容器使用Object类型,是否比直接使用Object类型性能要差?
  看MyArrayList.get_Item和MyList.get_Item两个方法的IL代码get操作: // MyArrayList的get_Item方法
  .methodpublichidebysigspecialnameinstanceobject get_Item(int32 index) cilmanaged noinlining
  {
  .maxstack8
  L_0000:ldarg.0
  L_0001:ldfldobject[] TestConsole.MyArrayList::m_items
  L_0006:ldarg.1
  L_0007:ldelem.ref
  L_0008:ret
  }
  // MyList的get_Item方法
  .methodpublichidebysigspecialnameinstance !T get_Item(int32 index) cilmanaged noinlining
  {
  .maxstack8
  L_0000:ldarg.0
  L_0001:ldfld !0[] TestConsole.MyList`1::m_items
  L_0006:ldarg.1
  L_0007: ldelem.any !T
  L_000c:ret
  }
  这两个方法的区别只在于红色的两句。我们"默认"ldfld指令的功能在两段代码中产生的效果完全相同(毕竟是相同的指令嘛),但是您觉得ldelem.ref指令和ldelem.any两条指令的效果如何,它们是一样的吗?我们通过查阅一些资料可以了解到说,ldelem.any的作用是加载一个泛型向量或数组中的元素。不过它的性能如何?您能得出结果说,它就和ldelem.ref指令一样吗?
  除非您了解到JIT对待这两个指令的具体方式,否则您是无法得出其中性能高低的。因为IL还是过于高级,您看到了一条IL指令,您可以知道它的作用,但是您还是不知道它最终造成了何种结果。您还是无法证明"Object泛型集合的性能不会低于直接存放Object的非泛型集合"。因此,比较MyArrayList.get_Item方法和MyList.get_Item方法的汇编代码,最后得出结果是"毫无二致"。由于汇编代码和机器代码一一对应,因此观察汇编代码就可以完全了解CPU是如何执行这两个方法的。汇编代码一模一样,就意味着CPU对待这两个方法的方式一模一样,它们的性能怎么会有不同呢?
  结论:.NET的Object泛型容器的性能不会低于直接使用 Object的容器,因为CLR在处理Object泛型的时候,会生成与直接使用Object类型时一模一样的类型,因此性能是不会降低的。但是您是通过学习IL可以了解这些吗?显然不是,如果您只是学习了IL,最终还是要"听别人说"才能知道这些,而即使您不学IL,在"听别人说"了之后您也了解了这些 --同时也不会因为不了解IL而变得"易忘"等等。
  同样道理,IL的call指令和callvirt指令的区别是什么呢?"别人会告诉你"call指令直接就去调用了那个方法,而 callvirt还需要去虚方法表里去"寻找"那个真正的方法;"别人可能还会告诉你",查找虚方法是靠方法表地址加偏移量;《Essential .NET》还会将方法表的实现结构告诉给你,而这些都是IL不会告诉您的。您就算了解再多IL,也不如"别人告诉你"的这些来得重要。您要了解"别人告诉你"的东西,也不需要了解多少IL。
  示例二:只有经过调用的方法才能获得其汇编代码吗?
  许多资料都告诉我们,在一个方法被第一次调用之前,它是不会被JIT的。也就是说,直到第一次调用时它才会被转化为机器码。不过,这个真是这样吗?我们还是准备一段简单的C#代码:
  ......
  IL代码多容易懂呀,这段IL代码基本上就和我们的C#一样。没错,这就是IL的作用。IL和C#一样,都是用于表现程序逻辑。C#使用 if...else、while、for等等丰富语法,而在IL中就会变成判断+跳转语句。但是,您从一段几十行的IL语句中,看出一句十几行的 while逻辑--收获在哪里?除此之外,C#分配一个变量,IL也分配一个。C#调用一个方法,IL就call或callvirt一下。C#里new一个,IL中就newobj一下(自然也会有一些特殊,例如可以使用jmp或tail call一个方法--是为尾递归,但也只是及其特殊的情况)。可以发现IL的功能大部分就是C#可以表现的功能。而C#隐藏掉的一些细节,在IL这里同样没有显示出来!
  那么我们又该如何发现一些细节呢?例如"书本"告诉我们的JIT的工作方式:方法第一次调用之后才会生成机器码。
  这段程序会打印三行文字,在打印出Before JITed和After JITed字样之后都会有一次停止,需要用户按回车之后才能继续。在进行试验的时候,您可以在程序暂停的时候使用WinDbg的File - Attach to Process命令附加到TestConsole.exe进程中,或者在两次暂停时各生成一个dump文件,这样便可不断地重现一些过程。否则的话,应用程序两次启动所生成的地址很可能会完全不同--因为JIT的工作是动态的,有时候很难提前把握。
  好,我们已经进入了第一个Console.ReadLine暂停,在点击回车继续下去之前。我们先使用WinDbg进行调试。以下是Main方法的汇编代码:
  ......
  这是什么,不像是SomeMethod的内容阿,SomeMethod是会调用Console.WriteLine方法的,怎么变成了一些跳转了呢?于是我们想起书本(例如《CLR via C#》)中的话来,在方法第一次调用时,将会跳转到JIT的指令处,对方法的IL代码进行编译。再想想书中的示意图,于是恍然大悟,原来这段代码的作用是"让JIT编译IL"啊。那么在JIT后,同样的调用会产生什么结果呢? 
  我们在WinDbg中Debug - Detach Debuggee,让程序继续运行。单击回车,您会发现屏幕上出现了Hello Word和After JIT的字样。于是我们继续Attach to Process,重复上面的命令。由于Main方法已经被编译好了,它的汇编代码不会改变,因此在调用SomeMethod方法时的步骤还是不变:先去内存172FF8中读取目标地址,再call至目标地址。 
  
  于是我们发现,虽然步骤没有变,但是由于地址172FF8中的值改变了,因此call的目标也变了。新的目标中包含了SomeMethod方法的IL代码编译后的机器码,而我们现在看到便是这个机器码的汇编表现形式。
  示例三:泛型方法是为每个类型各生成一份代码吗?
  IL和我们平时用的C#程序代码不一样,其中使用了各种指令,而不是像C#那样有类似于英语的关键字,甚至是语法。但是有一点是类似的,它的主要目的是表现程序逻辑,而他们表现得逻辑也大都是相同的,接近的。你创建对象那么我也创建,你调用方法那么我也调用。因此才可以有.NET Reflector帮我们把IL反编译为比IL更高级的C#代码。如果IL把太多细节都展开了,把太多信息都丢弃了,那么怎么可以如此容易就恢复呢?例如,您可以把一篇Word文章转化为图片,那么又如何才能把图片再转回为Word格式呢?C => 汇编、汇编 => C,此类例子数不胜数。 
  这……怎么和我们的C#代码如此接近。嗯,谁让IL清清楚楚明明白白地知道什么叫做"泛型",于是直接使用这个特性就可以了。所以我们还是用别的办法吧。
  从这里我们可以看到,CLR为引用类型(string/object/Program)生成共享的机器码,它们都实际上在调用一个GenericMethod所生成的代码。而对于每个不同的值类型(int/DateTime /double),CLR则会为每种类型各生成一份。自然,您有充分的理由说:"调用的目标地址不一样,但是可能机器码是相同的"。此外,CLR的"泛型共享机器码"特性也并非如此简单,如果有多个泛型参数(且引用和值类型"混搭")呢?如果虽然有泛型参数,但是确没有使用呢?关于这些,您可以自行进行验证。本文的目的在于说明一些问题,并非是要把这一细节给深究到底。
  总结
  以上三个示例都是用IL无法说明的,而这样的问题其实还有很多,例如:
  引用类型和值类型是怎么分配的
  GC是怎么分代,怎么工作的
  Finalizer做什么的,对GC有什么影响
  拆箱装箱到底做了些什么
  CLR是怎么验证强签名程序集的
  跨AppDomain通信是怎么Marshal by ref或by value的
  托管代码是怎么做P/Invoke的
  ……
  您会发现,这些东西虽然无法用IL说明,却其中大部分可以说是最最基本的一些.NET/CLR工作方式的常识,更别说一些细节(数组存放方式,方法表结构)了。它们依旧需要别人来告诉您,您就算学会了IL指令,学会了IL表现逻辑的方式,您还是无法自己知道这些。 
  IL还是太高级了,太高级了,太高级了……CLR作为承载IL的平台,负担的还是太多。与CPU相比,CLR就像一个溺爱孩子的父母,操办了孩子生活所需要的一切。这个孩子一嚷嚷"我要吃苹果",则父母就会拿过来一个苹果。您咋看这个孩子,都还是无法了解父母是如何获得苹果的(new一个 Apple对象),怎么为孩子收拾残局的(GC)。虽然这些经常是所谓的"成年人(.NET 程序员)必知必会"。而您如果盯着孩子看了半天,耐心分析他吃苹果的过程(使用IL编写的逻辑),最后终于看懂了,可惜发现--tmd老子自己也会吃苹果啊(从C#等高级语言中也能看出端倪来)!不过这一点,还是由下一篇文章来分析和论证吧。 
  这也是为什么各种.NET相关的书,即使是《CLR via C#》或《Essential .NET》此类偏重"内幕"的书,也只是告诉您什么是IL,它能做什么。然后大量的篇幅都是在使用各种示意图配合高级语言进行讲解,然后通过试验来进行验证,不会盯着IL捉摸不停。同理,我们可以看到《CLR via C#》,《CLR via VB.NET》和《CLR via CLI/C++》,但从来没有过《CLR via IL》。IL还是对应于高级语言,直接对应着.NET特性,而不是CLR的内部实现--既然IL无法说明比高级语言更多的东西,那么为什么要"via IL"?同样的例子还有,MSDN Magazine的CLR Inside Out专栏也没有使用IL来讲解内容,Mono甚至使用了与MS CLR不同实现方式来"编译"相同的IL(Mono是不能参考任何CLR和.NET的代码的,一行都看不得)。你要了解CLR?那么多看看Rotor,多看看Mono--看IL作用不大,它既不是您熟悉CLR的必要条件也不是充分条件,因为您关注的不是对IL的读取,甚至不是IL到机器码的转换方式,而是 CLR各处所使用的方案。 
  最后,本文全篇在使用WinDbg进行探索,这并非要以了解IL作为基础,您完全可以不去关心IL那些缤纷复杂的指令的作用是什么。甚至于您完全忽略IL的存在,极端地"认为"是C#直接编译出的机器码,也不妨碍您来使用本文的做法来一探究竟--细节上会有不同,但是看到的东西是一样的。
  ……
  "大胆的推测"和"认为是,应该是"并非一个意思。大胆的推测是根据已知现象,运用逻辑进行判断,从而前进,而最终这些推测要通过事实进行确定。正所谓"大胆推测,小心求证"。 
  以上这些是您"自行进行探索"所需要的条件,而如果您只是要"看懂"某个探索过程的话,就要看"描述"者的表达情况了。一般来说,看懂一个探索过程的要求会低很多,相信只要您有耐心,并且有一些基本概念(与这些条件有关,与IL无关),想要看懂如上的探索过程,以及吸收最后的结论应该不是一件困难的事情。 
  三、IL可以看到的东西,其实大都也可以用C#来发现
  我们使用工具.NET Reflector来完成这部分知识的学习,从.NET 1.x开始,.NET Reflector就是一个探究.NET框架(主要是BCL)内部实现的有力工具,它可以把一个程序集高度还原成C#等高级语言的代码。在它的帮助下,几乎所有程序集实现都变得一目了然,这大大方便了我们的工作。在某段不算短的时间内,使用.NET Reflector阅读过的代码数量远远超过了自己编写的代码。与此相反的是,几乎没有使用IL探索过.NET框架下的任何问题。这可能还涉及到方式方法和个人做事方式,但是如果这真有效果的话,为什么要舍近求远呢?希望您看过了这篇文章,也可以像我一样摆脱IL,投入.NET Reflector的怀抱。
  这只是一个示例,我并不是说这种作法是所谓的"最佳实践"。任何办法一旦遭到滥用也肯定不会有好处,您要根据当前情况判断是否应该采取某种作法。刚才的演示只是为了说明,我们应该如何从其他语言中吸取优势思想,改进我们的编程工作。当然,您使用IL来探索新的语言也没有太大问题,C#能看到的东西用IL也可以看到。但是请您回想一下,即使您平时学习IL,您想过直接使用IL来写程序吗?您学习和探索新语言的目的,只是为了搞清楚它的IL表现形式吗?为什么您不使用简单易懂的C#,却要纠缠于IL中那些纷繁复杂的指令呢?
  示例三:性能相关
  学习IL对写出高性能的.NET程序有帮助吗? 
  记得以前在学习"计算机系统概论"课程时,有一个实验就是为几段C程序进行优化。当时的手段可谓无所不用其极,例如内联一个子过程以避免 call指令的消耗,或把一段C代码使用汇编进行替换等等。从结果上看,它们都能对性能有"明显"的提高。不过,那些都是为了加深概念而进行的练习,并不是说在现代程序中应该使用这种方式进行优化。现在早已不是在"指令级别"进行性能优化的时期了,连操作系统内核也只是在一些对性能要求非常高的地方,如内存管理,线程调度中的细微方面使用汇编来编写,其余部分也都是用C语言来完成。这并不是仅仅是因为"可维护性"等考虑,也有部分原因是因为在目前编译技术的发展下,一些极端的做法已经很难产生有效的优化效果了(例如一般来说来,程序员写出的C代码的性能会优于他写的汇编代码)。 
  此外,在您不知道JIT究竟作了什么事情的情况下,观察IL这样一种高度抽象的语言,您还是无法真正判断出一个程序从微观上的性能如何。不过这并不是说,现代程序不应该"主动"追究性能,而是说,现代程序在性能优化问题上并非如此简单,它涉及到的东西会更多,需要更加合适的手段。例如,即使您内联了一个子过程,也只是减少了call指令的所带来的消耗,但是这与这个子过程本身"一长串"指令相比,所带来的提高是微乎其微的。而如果您一旦破坏了Locality或造成了False Sharing,或造成了资源竞争等等,这可能就会造成数倍甚至更多的性能损耗。换句话说,影响现代应用程序的性能的因素大都是"宏观"的,用通俗的话来说,一般都是"写法"上造成的问题。 
  这也是为什么说"Make clean code fast"远比"Make fast code clean"来的容易,现代程序更注重的是"清晰"而并非是"性能"。因为程序清晰,更容易让人发现性能瓶颈究竟在何处,可以进行有针对性地优化(即使是那种在极端性能要求下故意进行的"丑陋"写法,也是为了高性能而"丑陋",而不是因为"丑陋"而高性能,分清这一点很重要)。换句话说,如果我们有一种更清晰地方式来查看同样的程序实现,不也降低了探索程序性能瓶颈的难度吗?那么,同样一段程序,您会通过C#进行观察,还是使用IL呢? 
  有朋友可能会说:即使无法把握JIT对于IL的优化,但是从IL中可以看出高级语言,如C#的编译器的优化效果啊。这话本没有错,但问题还是在于,C#的编译器优化效果,是否在"反编译"回来之后就无法观察到了呢?"优化过程"往往都是不可逆的,它会造成信息丢失,导致我们很难从"优化结果"中看出"原始模样",这一点在上一篇文章中也有过论述。换句话说,我们通过C# => IL => C#这一系列"转化"之后,几乎都可以清楚地发现C#编译器做过哪些优化。这里还是使用经典的foreach作为示例,您知道以下两个方法的性能高低如何?
  不过,判断两者性能高低,最简单,也最直接的方式还是进行性能测试。例如您可以使用CodeTimer来比较DoArray和DoEnumerable方法的性能,一目了然。 
  值得一提的是,如果要进行性能优化,需要做的事情有很多,而"阅读代码"在其中的重要性其实并不高,而且它也最容易误入歧途的一种。"阅读代码"充其量是一种人工的"静态分析",而程序的运行效果是"动态"的。这篇文章解释了为什么使用foreach对ArrayList进行遍历的性能会比List低,其中使用了Profiler来说明问题。 Profiler能告诉我们很多难以观察到的事情,例如在遍历中究竟是ArrayList哪个方法消耗时间最长。此外它还发现了ArrayList在遍历时创建了大量的对象,这种对于内存资源的消耗,几乎不可能从一小段代码中观察得出。此外,不同环境下,同样的代码可能执行效果会有不同。如果没有 Profiler,我们可能会选择把一段执行了100遍的代码性能提升1秒钟,却不会把一段执行100000遍的代码性能提升100毫秒。性能优化的关键是"有的放矢",如果没有Profiler帮我们指明道路,做到这一点相当困难。 
  其实对于性能方面说的这些,可以大致归纳为以下三点:
  .关注IL,对于从微观角度观察程序性能很难有太大帮助,因为您很难具体指出JIT对IL的编译方式。
  .关注IL,对于从宏观角度观察程序性能同样很难有太大帮助,因为它的表述能力不会比C#来的直观清晰。
  .性能优化,最关键的一点是使用Profiler来找出性能瓶颈,有的放矢。
  所以,如果您问:"学习IL,对写出高性能的.NET程序有帮助吗?"回答:"有,肯定有啊"。 
  但是,如果您问:"我想写出高性能的.NET程序,应该学习IL吗?"回答:"别,别学IL"。
  总结
  feilng在前文留下的一些评论,我认为说得非常有道理: 
  IL只是在CLR的抽象级别上说明干什么,而不是怎么干……重要的是要清楚在现实条件下,需要进入那个层次才能获取足够的信息,掌握接口的完整语义和潜在副作用。 
  IL的确比C#等高级语言来的所谓"底层",但是很明显,IL本身也是一种高级抽象。而即使是机器码,它也可以说是基于CPU的抽象,CPU上如流水线,并行,内存模型,Cache Lock等东西对于汇编/机器码来说也可以说是一种"封装"。从不同层次可以获得不同信息,我们追求"底层"的目的肯定也不是"底层"这两个字,而是一种收获。了解自身需要什么,然后能够选择一个合理的层次进入,并得到更好的收益,这本身也是一种能力。追求IL的做法,本身并没有错,只是追求IL一定是当前情况下的最优选择吗?这是一个值得不断讨论的问题,我的这篇文章也只是表达了我个人对某些问题的看法。
  1、如何看到元件的中间语言吗?
  Microsoft 提供了一个称为 Ildasm 的工具,它可以用来查看元件的 metadata 和IL。
  2、能否通过反向工程从 IL 中获得源代码?
  是的。相对而言,从 IL 来重新生成高级语言源代码 (例如 C#) 通常是很简单的。
  3、如何防止别人通过反向工程获得我的代码?
  目前唯一的办法是运行带有 /owner 选项的 ilasm。这样生成的元件的 IL 不能通过 ildasm 来查看。然而,意志坚定的代码破译者能够破解 ildasm 或者编写自己的ildasm 版本,所以这种方法只能吓唬那些业余的破译者。
  不幸的事,目前的 .NET 编译器没有 /owner 选项,所以要想保护你的 C# 或VB.NET 元件,你需要像下面那样做:(aidd2008:好像没用啊!)
  csc helloworld.cs
  ildasm /out=temp.il helloworld.exe
  ilasm /owner temp.il
  (这个建议是 Hany Ramadan 贴到 DOTNET 上的。) >What happened to it?
  It was removed before the first RTM release since it didn't provide any real protection anyway.
  >Will it be added when 2005 is released?
  No
  >Is there a way to protect my code (besides obfuscation) from being disassemble with ildasm.exe or Reflector tools?
  No (except not distributing the executable).
  看起来过一段时间能有 IL 加密工具 (无论来自 Microsoft 或第三方)。这些工具会以这样的方式来"优化" IL:使反向工程变得更困难。
  当然,如果你是在编写 Web 服务,反向工程看起来就不再是一个问题,因为客户不能访问你的 IL。
  4、我能直接用 IL 编程吗?
  是的。Peter Drayton 在 DOTNET 邮件列表里贴出了这个简单的例子:
  程序代码 将其放入名为 hello.il 的文件中,然后运行 ilasm hello.il,将产生一个 exe 元件。
  5、IL 能做到 C# 中做不到的事吗?
  是的。一些简单的例子是:你能抛出不是从 SystemException 导出的异常,另外你能使用非以零起始的数组。

猜你喜欢

转载自ldi543lc.iteye.com/blog/1364188