前言:

项目紧赶慢赶总算在年前有了一些成绩,所以沉寂了几周之后,小匹夫也终于有时间写点东西了。以前匹夫写过一篇文章,对CIL做了一个简单地介绍,不过不知道各位看官看的是否过瘾,至少小匹夫觉得很不过瘾。所以决定写几篇关于CIL的文章,即和各位看官一起进行个交流,同时也是匹夫自己总结和巩固一下这些知识点。俗话说的好,“万事开头,Hello World”,那么作为匹夫总结CIL的第一篇文章,就从Hello World开始吧。当然,正式开始写CIL代码之前,我们还有点闲话要说,那就是运行时的选择为何是它?为何是CIL?而CIL为何又是基于堆栈的?内存或者寄存器难道不是更理想的选择吗?

为何是CIL?

开始正文内容之前,匹夫带领大家先回顾一下《Mono为何能跨平台?聊聊CIL(MSIL)》的简要内容:首先,用C#写的代码被C#的编译器编译成CIL(当然除了C#还有很多其他的语言,比如VB等等),之后再有JIT编译器在程序运行时即时编译或者AOT(或者NGEN)进行提前编译将CIL代码编译成对应平台的机器码,最后运行在平台上的便是机器码。小匹夫在那篇文章中提过,首先将各种不同的语言都统一编译成CIL,再由CIL编译成各个平台的机器码是跨平台的基础。那么仔细想想,一定有人会提出这样的疑问,直接从C#编译到机器码,省略掉“多余”的中间语言,是不是也可行呢?这个问题的确值得讨论,同时也为了小匹夫接下来的文章师出有名,所以首先聊聊CIL的“合法性”(用必要性这个词也许更好)问题就成了匹夫写这篇文章的头等大事。

论据一:考虑下“性价比”

首先提出我们的论据一,那就是使用CIL这套体系对实现跨平台的开销要小的多的多。

引入一个“多余的”中间语言和两个编译器(C#----->CIL------>机器码)听上去总是要比只使用一种编译器(C#-------->机器码)的实现代价高的多,因为我们的目的是C#代码能编译成机器能运行的机器码,显然一步到位是最直接有效的方式。相反,引入中间语言之后,我们就需要实现两种语言的分析和编译,看上起的确多此一举。但如果我们考虑到跨平台这个前提,就会发现中间语言是多么的重要。

假设你可以选择的语言有N种(比如C#, VB, F#, JScript .NET,Boo...),而我们的目标平台有M种(win,mac,linux,ios,android...)。那么如果我们采用最直接的编译方式,即从源代码直接编译成机器码,那么到底需要多少个编译器呢?

答案很直接咯:需要N*M种编译器。因为你需要为每一种语言针对每一个平台写一个编译器。

如果我们采用了中间语言呢?

我们只需要为N种语言写N种编译器,将它编译成CIL代码。再为M种平台写M种编译器,将上一步生成的CIL代码编译成M种平台的机器码。那么这次我们到底需要多少编译器呢?

答案也很明显:需要M+N种编译器。

所以,采用中间语言要比直接编译代码的开销小的多得多。

论据二:实现的难度

假设,匹夫对硬件语言一窍不通(当然事实上是这样的。。。),但却具备一种分析源代码语义的特殊天赋(瞎掰的)。那么要实现从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() + 

机器首先要为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 // 现在堆栈中包含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 Wolrd 你好,沃尔德

本文开篇就提到了那句名言:“万事开头,Hello World”。那么我们第一个CIL语言的程序,就从Hello World开始吧。因为匹夫使用的是mac机器,所以编译.il文件所使用的工具是mono的ilasm。

那么匹夫就先新建一个.il文件,起名就叫做chen.il好了。

与C#不同,CIL并不要求方法必须要属于一个类。所以,我们无需定义一个类,只需要声明一个主函数(按照C#的说法main)即可。其实在CIL中我们应该管这种函数叫做“entrypoint”,也就是入口函数。只要定义了“entrypoint”,函数叫不叫main都无关紧要,为了演示这一点,我们的函数名就叫做Fanyou好了。

那么小匹夫就这样写一下咯:

上面就是小匹夫的Fanyou方法的定义了。和一般的语言一样,包括方法签名和方法体。但是在CIL语言中,方法的定义有以下需要注意的地方:

  1. 方法的定义以.method作为标识,可以在类中声明,也可以在类外声明。
  2. 和C#一样,CIL程序的入口也必须是静态的,也就是意味着调用这个入口函数并不需要某个类的实例。当然,使用static关键字来标识。
  3. 入口的标识.entrypoint,这个标志表明了该方法是CIL程序的入口。所以咯,只有一个函数能拥有.entrypoint标识。
  4. .maxstack这个标识表明了预计使用的堆栈槽,这里是1,因为我们只是把“Hello World”这个字符串压栈。举个例子,如果像上文那样做2个数相加的加法,则需要2个堆栈槽,首先需要将2个数压栈,之后add操作符将2个数弹出并求和,最后将结果压栈。所以最多需要2个栈槽。
  5. ldstr操作符将“Hello World”压栈,供之后的WriteLine方法使用。
  6. call调用了mscorlib程序集中System.Console类的WriteLine方法。这里call指明了WriteLine完整的签名(void [mscorlib]System.Console::WriteLine(string))所以运行时可以选择WriteLine的正确地重载。
  7. ret操作符则将结果返回给调用者。在这里,作为入口函数的返回,也意味着应用运行的结束。
  8. 有一些同学可能也看过很多CIL语言的代码,是不是发现它们每一条语句之前往往有一个“IL_0000:”这样的东东?但是匹夫你写的代码里没有啊!是不是你写错了?NO,NO,那个IL_XXXX其实仅仅是行号,是不会影响程序的运行的。

好啦,一个简单地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,大功告成!

如果各位看官觉得文章写得还好,那么就容小匹夫跪求各位给点个“推荐”,谢啦~

装模作样的声明一下:本博文章若非特殊注明皆为原创,若需转载请保留原文链接http://www.cnblogs.com/murongxiaopifu/p/4257264.html)及作者信息慕容小匹夫

后记

CIL代码虽然号称不是很友好,但是作为C#程序员的确还是很有必要掌握一下。匹夫水平一般,能力有限,愿抛砖引玉和大家共同探讨,共同进步。

用CIL写程序:你好,沃尔德的更多相关文章

  1. 用CIL写程序:写个函数做加法

    前言: 上一篇文章小匹夫为CIL正名的篇幅比较多,反而忽略了写那篇文章初衷--即通过写CIL代码来熟悉它,了解它.那么既然有上一篇文章做基础(炮灰),想必各位对CIL的存在也就释然了,兴许也燃起了一点 ...

  2. 用CIL写程序:定义一个叫“慕容小匹夫”的类

    前文回顾: <用CIL写程序:你好,沃尔德> <用CIL写程序:写个函数做加法> 前言: 今天是乙未羊年的第一天,小匹夫先在这里给各位看官拜个年了.不知道各位看官是否和匹夫一样 ...

  3. 用CIL写程序:从“call vs callvirt”看方法调用

    前文回顾:<用CIL写程序系列> 前言: 最近的时间都奉献给了加班,距离上一篇文章也有半个多月了.不过在上一篇文章<用CIL写程序:定义一个叫“慕容小匹夫”的类>中,匹夫和各位 ...

  4. STM32用JLINK 烧写程序时出现NO Cortex-m device found in JTAG chain现象和解决方案

    现象 CPU: STM32107VC 用JLINK 烧写程序时出现NO Cortex-m device found in JTAG chain 如图无法查找到硬件就是CPU 提示1:NO Cortex ...

  5. 第一章-第四题(ACM 比赛的程序是软件么? “写程序” 和 ”做软件“ 有区别么?软件工程是不是教那些不怎么会写程序的人开发软件? 你怎么看?这个游戏团队, 有很好的软件,但是商业模式和其他软件之外的因素呢?有没有考虑到)--By梁旭晖

    引用 http://baike.baidu.com/link?url=z_phkcEO4_HjFG_Lt163dGFAubdb68IbfcfzWscTOrrZ55WbJEQKzyMQ5eMQKyatD ...

  6. 4.“写程序” 这个活动大多数情况下是个人行为。 我们听说的优秀程序员似乎都是单打独斗地完成任务。同学们在大学里也认识一些参加ACM 比赛的编程牛人, 他们写的ACM 比赛的程序是软件么? “写程序” 和 ”做软件“ 有区别么? 请采访这些学生。

    ACM的题库的编程都只能算做程序,不能算软件.写程序和做软件区别还是很大的.程序是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合.为实现预期目的而进行操作的一系列语句和指令.而软件是程 ...

  7. IntelliJ下使用Code/Live Template加快编码速度:程序员的工作不是写程序,而是写程序解决问题

    程序员的工作不是写程序,而是写程序解决问题. --- 某不知名程序员 我们每天都在写代码,有些代码有结构性的相似,但不是所有的代码都可以被抽成方法.在这种情况下,我们应该考虑使用template的方式 ...

  8. object-c cocos2d-x 写程序时注意调试的技巧

    (1)写程序时最好在类的init函数中显示类名,表明现在在执行哪个类,样例代码 CCLOG(@"cocos2d: Using Director Type:%@", [self cl ...

  9. 象写程序一样写博客:搭建基于github的博客

    象写程序一样写博客:搭建基于github的博客   前言 github 真是无所不能.其 Pages 功能 支持上传 html,并且在页面中显示.于是有好事者做了一个基于 github 的博客管理工具 ...

随机推荐

  1. JS核心系列:浅谈原型对象和原型链

    在Javascript中,万物皆对象,但对象也有区别,大致可以分为两类,即:普通对象(Object)和函数对象(Function). 一般而言,通过new Function产生的对象是函数对象,其他对 ...

  2. TODO:Laravel 使用blade标签布局页面

    TODO:Laravel 使用blade标签布局页面 本文主要介绍Laravel的标签使用,统一布局页面.主要用到到标签有@yield,@ stack,@extends,@section,@stop, ...

  3. 第一个shell脚本

    打开文本编辑器,新建一个文件,扩展名为sh(sh代表shell),扩展名并不影响脚本执行,见名知意就好. #!/bin/bash echo "Hello World !" &quo ...

  4. fiddler发送post请求

    1.指定为 post 请求,输入 url Content-Type: application/x-www-form-urlencoded;charset=utf-8 request body中的参数格 ...

  5. 开始学nodejs——net模块

    net模块的组成部分 详见 http://nodejs.cn/api/net.html 下面整理出了整个net模块的知识结构,和各个事件.方法.属性的用法 net.Server类 net.Socket ...

  6. mybatis_个人总结

    在使用mybatis框架开发数据访问层的过程中,我在这段时间遇到很多细节问题困住我,在这里我来分享一下我遇到的坑,希望能帮到大家. 一.mybatis动态代理方式开发的规范: 1.注意在mybatis ...

  7. 马里奥AI实现方式探索 ——神经网络+增强学习

    [TOC] 马里奥AI实现方式探索 --神经网络+增强学习 儿时我们都曾有过一个经典游戏的体验,就是马里奥(顶蘑菇^v^),这次里约奥运会闭幕式,日本作为2020年东京奥运会的东道主,安倍最后也已经典 ...

  8. 「译」JUnit 5 系列:条件测试

    原文地址:http://blog.codefx.org/libraries/junit-5-conditions/ 原文日期:08, May, 2016 译文首发:Linesh 的博客:「译」JUni ...

  9. css样式之border-radius

    border-radius 属性设置边框的园角 可能的值:像素,百分比 扩展延伸 html代码 <div></div> css代码 div { height: 200px; w ...

  10. phpstorm 配置 xdebug调试工具

    前言   php是脚本型程序 每次出错都要手动exit断点程序不是很方便 哪里有需求哪里就有生产,Xdebug可以实现对php的断点调试.下面将我个人的安装经历分享给大家. 运行环境 windows ...