项目紧赶慢赶总算在年前有了一些成绩,因此沉寂了几周以后,小匹夫也终于有时间写点东西了。之前匹夫写过一篇文章,对CIL作了一个简单地介绍,不过不知道各位看官看的是否过瘾,至少小匹夫以为很不过瘾。因此决定写几篇关于CIL的文章,即和各位看官一块儿进行个交流,同时也是匹夫本身总结和巩固一下这些知识点。俗话说的好,“万事开头,Hello World”,那么做为匹夫总结CIL的第一篇文章,就从Hello World开始吧。固然,正式开始写CIL代码以前,咱们还有点闲话要说,那就是运行时的选择为什么是它?为什么是CIL?而CIL为什么又是基于堆栈的?内存或者寄存器难道不是更理想的选择吗?html
开始正文内容以前,匹夫带领你们先回顾一下《Mono为什么能跨平台?聊聊CIL(MSIL)》的简要内容:首先,用C#写的代码被C#的编译器编译成CIL(固然除了C#还有不少其余的语言,好比VB等等),以后再有JIT编译器在程序运行时即时编译或者AOT(或者NGEN)进行提早编译将CIL代码编译成对应平台的机器码,最后运行在平台上的即是机器码。小匹夫在那篇文章中提过,首先将各类不一样的语言都统一编译成CIL,再由CIL编译成各个平台的机器码是跨平台的基础。那么仔细想一想,必定有人会提出这样的疑问,直接从C#编译到机器码,省略掉“多余”的中间语言,是否是也可行呢?这个问题的确值得讨论,同时也为了小匹夫接下来的文章师出有名,因此首先聊聊CIL的“合法性”(用必要性这个词也许更好)问题就成了匹夫写这篇文章的头等大事。linux
首先提出咱们的论据一,那就是使用CIL这套体系对实现跨平台的开销要小的多的多。android
引入一个“多余的”中间语言和两个编译器(C#----->CIL------>机器码)听上去老是要比只使用一种编译器(C#-------->机器码)的实现代价高的多,由于咱们的目的是C#代码能编译成机器能运行的机器码,显然一步到位是最直接有效的方式。相反,引入中间语言以后,咱们就须要实现两种语言的分析和编译,看上起的确画蛇添足。但若是咱们考虑到跨平台这个前提,就会发现中间语言是多么的重要。ios
假设你能够选择的语言有N种(好比C#, VB, F#, JScript .NET,Boo...),而咱们的目标平台有M种(win,mac,linux,ios,android...)。那么若是咱们采用最直接的编译方式,即从源代码直接编译成机器码,那么到底须要多少个编译器呢?程序员
答案很直接咯:须要N*M种编译器。由于你须要为每一种语言针对每个平台写一个编译器。函数
若是咱们采用了中间语言呢?工具
咱们只须要为N种语言写N种编译器,将它编译成CIL代码。再为M种平台写M种编译器,将上一步生成的CIL代码编译成M种平台的机器码。那么此次咱们到底须要多少编译器呢?post
答案也很明显:须要M+N种编译器。url
因此,采用中间语言要比直接编译代码的开销小的多得多。spa
假设,匹夫对硬件语言一窍不通(固然事实上是这样的。。。),但却具有一种分析源代码语义的特殊天赋(瞎掰的)。那么要实现从C#到各个平台机器码一步到位的编译,匹夫就要去啃各类目标芯片的说明,将C#代码转化成对应芯片的机器码。这听上去就像是一条不归路,由于你并不擅长这个领域并且工做量巨大,同时因为不擅长带来的隐患难以估量。
换言之,这个难度太大了。
可是若是咱们经过对C#进行语义分析,能十分容易的就生成一份和芯片无关的CIL代码,那么实现的难度相比直接从C#到机器码那但是大大的下降了。由于CIL语言自己就十分简单(至少匹夫这种粗人都能看懂),因此从源代码到CIL的编译器实现就十分容易。同时,也是由于CIL语言自己十分简单,因此从CIL到机器码的编译器也十分简单。
并且即使有新的平台出现,你也不须要为每种语言都写一个针对新平台的编译器,而只须要实现一个从CIL到新平台机器码的编译器就能够了。
因此能够看到,CIL中间语言的出现,大大下降了跨平台的实现难度。
《Mono为什么能跨平台?聊聊CIL(MSIL)》这篇文章中,小匹夫也给各位列举了一些CIL的代码,同时作了一些解释,文中在介绍CIL不依托cpu的寄存器时写了这样一句话:
不错,CIL是基于堆栈的,也就是说CIL的VM(mono运行时)是一个栈式机。
那么不知道各位看官是否也有这样的疑问呢?那就是~~~~~~~
终于要聊聊小匹夫也以为挺有趣的一个话题了。对啊,为何CIL基于堆栈呢?那么咱们首先就来聊聊什么是“栈式机”。
假如让你来设计一种机器语言,同时实现一个简单地加法功能,简单到什么程度呢?好比a+b等于c这样好了。那么思路是什么呢?
方案一:使用内存
add [a的地址], [b的地址], [结果的地址也就是c的地址]
当机器遇到add操做符时,它就会去寻找a的地址和b的地址这两个地址中存放的值,而后用balabala的方式将它们求和,并将结果存放在c的地址。
方案二:使用寄存器
固然匹夫也是一个学过汇编的汉子,也了解一点点单片机的知识,知道有一个叫作累加器的东西。累加器就属于寄存器了,它主要用来储存计算所产生的中间结果,最后将其转存到其它寄存器或内存中。因此使用累加器的思路也很简单,一开始将累加器设定为0,每一个数字依序地被加到累加器中,当全部的数字都被加入后,结果才写回到主内存中。
方案三:使用堆栈
等等,这个部分介绍的不是栈式机吗?怎么感受有点跑题呢?好吧,拉回思绪,让咱们再来考虑下使用堆栈如何实现这个简单地加法功能呢?
push a
push b
add
pop c
add操做符首先将a,b弹出堆栈,而后将两者相加,再将结果压栈。那么,使用了这种方案的虚拟机,就被称为“栈式机”。
因此若是要回答为什么CIL的选择是使用堆栈,那么就绕不过堆栈和另外两种方案的比较。
首先看一下咱们作这种简单加法时,硬件须要为咱们提供一些什么呢?对,就是存放这些值的临时空间。所谓的临时空间,就是说存储这个值的空间只有在须要这个值的时候才有用,其他的时候你并不须要关心这个空间或者说它的地址究竟是什么。假设咱们已经定义了一些操做符,好比Allocate用来分配内存,Call用来调用函数,Add用来求和,Store则是用来存储数据。
首先咱们直接使用内存来运行CIL,那么遇到这样的表达式:
x = A() + B() + C() + 100
机器首先要为A()在内存上分配空间用来保存它的返回值,而后调用A()并将A()的返回值保存在以前分配给它的地址中,咱们就管它叫作ret1好了。以后为B()在内存上分配空间来保存B()的返回值,接着调用B(),一样将B()的返回值保存在刚才分配给它的内存中,咱们暂时称呼它ret2。这时,咱们遇到了第一个“+”号,因此此时会为ret1和ret2相加的结果在内存上分配一个空间,而且将ret1和ret2相加,并将结果保存在刚刚分配的内存中(咱们称为sum1),以后的过程以此类推。
Allocate ret1 //为A()的返回值分配临时空间ret1 Call A(),ret1 //调用A()并将结果保存在ret1 Allocate ret2 //为B()的返回值分配临时空间ret2 Call B(),ret2 //调用B()并将结果保存在ret2 Allocate sum1 //为第一次相加的结果分配临时空间sum1 Add ret1,ret2,sum1 //使用Add操做符将ret1和ret2中的内容相加,并将结果保存在sum1中。 ...
能够看到这样的CIL代码在每一步真正的逻辑执行以前,都会先在内存上分配一块临时空间,用来存储咱们此时须要的数据。若是使用堆栈,这个步骤是不须要,由于你将你须要的数据存储在了堆栈之中,而非在内存上临时去分配空间。因此,使用堆栈时,CIL代码看上去也许像是这样的:
push x的地址 // 将x的地址压栈 call A() // 如今堆栈中包含x的地址和A()的返回值ret1 call B() // 如今堆栈中包换x的地址,ret1,B()的返回值ret2 add // 如今堆栈中包含x的地址,ret1 + ret2的结果sum1 call C() // 如今堆栈中包含x的地址,sum1和C()的返回值ret3 add // 如今堆栈中包含x的地址, ret1+ret2+ret3的返回值sum2 push 100 // 如今堆栈中包含x的地址,sum2,以及100 add // 如今堆栈中包含x的地址, ret1+ret2+ret3+100的和sum3 store //将sum3存在x的地址中。
同时,咱们还能够看到若是CIL直接使用内存的话,因为在内存上的空间是临时分配的,因此CIL代码在运行时须要带上它的操做数地址以及返回地址,好比上例中的Add ret1,ret2,sum1,由于若是不告诉它这些地址,它就不知道该从何处获得数据,并将返回的数据放在何处。
因此直接使用内存来运行CIL代码,会使得CIL代码变得十分的臃肿不堪,并且要作不少多余的工做。因此不直接使用内存,而是使用堆栈的缘由就是由于:若是咱们仅仅只是为了临时存储一些值,而在使用完这些值以后咱们就再也不关心这块空间如何如何,显然使用堆栈要比直接使用内存方便的多,简洁的多。
至于为什么不使用寄存器,小匹夫在上文提到的文章中已经解释过了。简单的讲就是由于简单。
好啦,到此为CIL正名的过程就结束啦。那么下面就开始首尾呼应,结尾点题,从Hello World开始踏上咱们的CIL语言的征程吧~~
本文开篇就提到了那句名言:“万事开头,Hello World”。那么咱们第一个CIL语言的程序,就从Hello World开始吧。由于匹夫使用的是mac机器,因此编译.il文件所使用的工具是mono的ilasm。
那么匹夫就先新建一个.il文件,起名就叫作chen.il好了。
与C#不一样,CIL并不要求方法必需要属于一个类。因此,咱们无需定义一个类,只须要声明一个主函数(按照C#的说法main)便可。其实在CIL中咱们应该管这种函数叫作“entrypoint”,也就是入口函数。只要定义了“entrypoint”,函数叫不叫main都可有可无,为了演示这一点,咱们的函数名就叫作Fanyou好了。
那么小匹夫就这样写一下咯:
上面就是小匹夫的Fanyou方法的定义了。和通常的语言同样,包括方法签名和方法体。可是在CIL语言中,方法的定义有如下须要注意的地方:
好啦,一个简单地Hello World的确能带来一些最基本的知识点,可是这个.il文件编译以后能运行吗?答案是NO。由于上面的第6点也说了,调用了mscorlib程序集。可是咱们貌似没有引入什么程序集啊?因此咱们还要加入一些程序集的信息才能够哦。那么完整的代码以下了:
而后,让咱们编译而且运行一下,看看咱们写的实现了Fanyou方法,输出Hello World的CIL代码究竟是否能够运行吧!
运行结果:
首先
ilasm chen.il
对chen.il这个CIL文件进行编译,生成的结果是chen.exe
以后再运行chen.exe
mono chen.exe
能够看到屏幕上输出了“Hello World”。
OK,大功告成!
若是各位看官以为文章写得还好,那么就容小匹夫跪求各位给点个“推荐”,谢啦~
CIL代码虽然号称不是很友好,可是做为C#程序员的确仍是颇有必要掌握一下。匹夫水平通常,能力有限,愿抛砖引玉和你们共同探讨,共同进步。