前言:

上一篇文章小匹夫为CIL正名的篇幅比较多,反而忽略了写那篇文章初衷--即通过写CIL代码来熟悉它,了解它。那么既然有上一篇文章做基础(炮灰),想必各位对CIL的存在也就释然了,兴许也燃起了一点探索它,掌握它的欲望。那么小匹夫就继续扯一扯CIL,接下来的几篇文章也都以上一篇文章中的那个CIL实现的Hello Wolrd程序为基础,继续通过写CIL代码实现一些功能的方式来和各位探讨交流,同时也加深自己对CIL的掌握和印象。

人生就是做加法

"我的肩上搭着她得衣裳,我嗅着她留在衣服上的体香......"。匹夫偶尔不经意间地看到年轻时候写的东西,总会感觉那时自己的幼稚。是啊,转眼就从17,8到25,6了,除了一直单身这一点,匹夫人生的其余部分简直就是加法,怎能不让人感到唏嘘。为了缓解这种彷徨忧伤的情绪,匹夫决定用CIL代码实现一个把2个数相加的功能,让自己的情愫谱写在代码的字里行间里。

好啦,言归正传,其实选择实现一个加法功能是因为小匹夫上一篇文章中举过一个关于加法的例子,同时上一篇文章也大体上介绍了一下CIL中如何声明一个函数。所以趁热打铁,一鼓作气,直接用CIL实现一个做加法的函数既是对上一篇文章的一个呼应,也能达到本篇文章的目的---聊聊如何用CIL实现函数。

那么这个函数的功能呢?简单的分一下各个功能点,匹夫想到的大概就是这些了:

  1. 显示“请输入第一个加数”,并获取输入的值,记为num1。显示“请输入第二个加数”,并获取输入的值,并记为num2。
  2. 将输入的两个数,num1和num2相加求和,结果记为result。
  3. 要将结果显示给用户,所以我们做的更彻底一点:显示整个算式:num1+num2 = result。

局部变量

说到函数,就不得不提局部变量了,因为你总得操作一些东西吧?而局部变量正是那些在函数中被你操作的家伙。

假设我们要实现的函数叫做AddLife好啦。AddLife需要实现将我们输入的2个数字相加,并且将“和”作为结果返回。所以局部变量的个数是3个。

我们的局部变量的声明如下:

.locals init (int32 num1,
int32 num2,
int32 result)

在这里:

  1. 我们使用.locals指令标识我们要声明的是局部变量。
  2. init标识我们所声明的3个int32型变量都被初始化为int32型的默认值,当然如果我们声明的不是int32型的变量,则被初始化为该变量相应类型的初始值。
  3. int32,代表变量的类型
  4. 当然,在CIL代码中我们也可以不写变量的名字num1,num2以及result。因为你可以通过它们的索引获得它们,这里写它们的名字仅仅是为了易读。

OK,我们声明了函数中的局部变量。可之后呢?这毕竟不是C#或者别的高级语言啊,否则也不会有很多人讨厌去读它更别说写它了。所以,这里小匹夫要对CIL的执行做个小说明,以便各位不和匹夫产生较大的分歧。

基于堆栈记心头

首先的一点,就是CIL是基于堆栈的。也就是说它的执行离不开堆栈。所以要搞清楚CIL的执行,就是要搞清楚CIL到底是如何使用堆栈的。

因为是基于堆栈的,所以CIL指令所取的值来自堆栈。也就是说,值的传递要经过堆栈这个“桥梁”。所以当你想要为某个CIL指令传值(值类型),所传的就必须要先入栈,之后该指令再将这个值出栈进而使用。

OK,那让我更进一步。假如我的CIL指令是call(或者callvirt,二者区别以后会聊)呢?这需要调用一个函数,那call是如何使用堆栈的呢?

2种情况。

首先,你调用的是某个类的实例函数,那么就要把调用的某个类的实例的引用压栈。然后呢?当然如果需要的话,这个函数所需要的参数也要压栈。在调用函数的过程中,这个实例的引用以及它的参数都会出栈,供CIL指令使用。

其二,你需要用一个引用类型作为参数传入某个方法中。同样,这个引用类型的参数在被使用之前,也要把它的引用压栈。

那么,一直说压栈和出栈。可处理压栈和出栈的CIL指令是啥呢?2个最基本也是本文要用到的:(更具体的可以看匹夫之前的文章)

  1. ldloc,用来做压栈操作。将变量的值压入堆栈中。
  2. stloc,用来做出栈操作。将堆栈的值传入变量中。

之后,各位可能还会注意到上面匹夫标红的几个字,如果对所谓的值类型和引用类型有一个简单的概念的话,是不是觉得恍然大悟呢?

  1. 值类型的值直接存在堆栈上。(当然引用类型,比如某个类的实例中的值类型字段不再此列,它也会和该类的实例一起出现在堆上,不过这并非我们今天要探讨的主要内容)
  2. 引用类型的实例并非存在堆栈上,堆栈上存的是它的引用。

第一个功能:显示提示输入加数,并获取输入的值

好啦。来到我们要实现的第一个功能了,那就是要显示“请输入第一个加数”这样一个字符串,同时要读取用户输入的数值,然后赋给我们的变量num1。

饭要一口一口吃,码要一行一行写。所以就从显示字符串开始吧。其实小匹夫上一篇文章介绍如何输出一个Hello World的时候,就已经实现了字符串的输出。那么让我们依样画葫芦,输出“请输入第一个加数”这样的语句吧。

//在屏幕上显示“请输入第一个加数”
ldstr "请输入第一个加数"
call void [mscorlib]System.Console::WriteLine(string)

首先将“请输入第一个加数”压栈,然后使用call来调用mscorlib程序集中System.Console类的WriteLine方法,此时屏幕上会显示“请输入第一个加数”。

第二步,我们要获取用户输入的值。

//获取用户的输入值
call string [mscorlib]System.Console::ReadLine()

调用mscorlib程序集中System.Console类的ReadLine方法,并返回一个字符串。因为返回的是一个字符串,所以我们在将正确的值赋给变量num1之前,还需要对这个字符串进行转化,转化成int32的过程如下:

//将输入的字符串转化成int
call int32 [mscorlib]System.Int32::Parse(string)

注意,以上的三步,虽然看上去只有一个ldstr将字符串压栈,但是其实每一步,每一行都伴随着压栈或出栈的过程。简单的叙述下这个过程是这样的:

  1. 将“请输入第一个加数”压栈。
  2. call调用Write方法,同时会将“请输入第一个加数”出栈,作为WriteLine的参数。
  3. call调用ReadLine方法,该方法从用户处获得一个输入,并且将该值作为一个string型压栈。
  4. 由于这个从用户处得到的string型已经在堆栈中了,所以在最后一个call调用[mscorlib]System.Int32::Parse之前,无需对那个string型压栈。反而是直接从堆栈中弹出该string值,作为[mscorlib]System.Int32::Parse的参数。之后在将已经转化为int型的值压栈。

简单描述了下过程。在这里,匹夫只想说:记住,只要涉及到数据,就要用到堆栈。

好啦,完成上面的过程,也就是完成了从用户处获取值的过程,此时的值已经躺在堆栈中了。之后,我们还要将这个值赋给变量num1:

//值出栈,赋给局部变量num1
stloc num1

之后获取第二个加数的过程就是上面几步的重复。各位可以自己实现下。

第二个功能:相爱相杀,不对,应该是相爱相加...

好啦,用户输入的数值我们已经搞到手了,那么是不是就该实现这个方法最核心的功能,对2个数相加求和呢?答案是yes。

不过这里我们会涉及到这篇文章已经介绍过的一个指令----ldloc。顾名思义,ldloc?不就是loadlocal嘛~所以其作用也就十分明了了:使用ldloc我们可以将局部变量num1,num2中的值压入堆栈,这样才能供之后使用。

所以我们将值压栈的语句就是:

//将值从变量中压入堆栈
ldloc num1
ldloc num2

当然在本文一开头就说过,局部变量什么的作为我们人类也可以不给它们起名字,只需要使用它们的索引就可以了。所以你也可以这样写来实现数值压栈的过程:

//如果不写变量名
ldloc.
ldloc.

反正小匹夫是挺讨厌这种不是给人看的写法的。

变量已经躺进堆栈了,那么下一步呢?求和呗,CIL可是带求和指令的哦~没错:add

//求和
add

add指令会将刚刚压入堆栈的2个值弹出,然后计算和,最后将结果在压入堆栈中。可是我们还有一个局部变量result没用呢,所以我们还要将结果赋值给result变量。

//将结果赋值给result
stloc result

最后一个功能,关键的其实是装箱

好啦好啦,求和这个事情其实已经做完了,但是我们总的输出一点东西好让用户看到我们的确已经求过和了。那么如何实现文章一开始时,匹夫定下的最后一条功能呢?也就是按照”num1 + num2 = result“这个格式显示结果呢?

首先,我们把显示的字符串的格式规定好:

//显示的格式
ldstr "{0} + {1} = {2}"

其次我们要把格式中的{0},{1},{2}替换成具体的数值,或者说”object“。因为我们要调用WriteLine方法,所以这里就会涉及到一个值类型到object的装箱的过程。首先我们还是将变量中的值压栈,之后再对栈中的值进行装箱。

//将num1,num2,result装箱,供之后的writeLine使用。
ldloc num1
box int32
ldloc num2
box int32
ldloc result
box int32

这里终于聊到了装箱这个话题,所以小匹夫继续扯一扯装箱,也就是box指令在CIL中的执行过程吧:

  1. 首先将压栈的值弹出。
  2. 同时在堆上构造一个新的object,并且这个object包含该值类型的值的拷贝
  3. 最后将这个新的object的引用压栈。

所以,各位是不是觉得C#中的一些概念通过CIL来看更加直观呢?其实这也是小匹夫对CIL感兴趣的一个原因。

最后一步,就是输出结果咯,因为3个值类型的值已经装箱了,所以我们就这样写:

//将算式显示出来
call void [mscorlib]System.Console::WriteLine(string, object, object, object)

[mscorlib]System.Console::WriteLine(string, object, object, object)中的第一个参数string代表第一个被压栈的格式字符串"{0} + {1} = {2}",之后依次是经过装箱的num1的值,num2的值,result的值。

此时,以上一篇文章中实现Hello World输出的chen.il文件为基础,再加入我们的AddLife函数之后,大体上就长这个样子了(26行之前是上篇文章时写的代码):

此时眼尖的小伙伴一定会发现,好像是少了点什么呀?对,没有.entrypoint。因为.entrypoint还在上一篇文章中的Fanyou()这个方法里呢。然后呢?还少了.maxstack,因为匹夫光顾着看逻辑了(04/02/15-23:35刚写完。。。)所以,没有提前考虑到底需要多大的堆栈槽。那么到底最多到底需要使用多少堆栈槽呢?答案是4,使用最多堆栈槽的地方就是我们最后显示算式的时候,WriteLine要用4个参数。

现在我们把.entrypoint和.maxstack 4加入到现在的AddLife方法,编译并运行它。

和上文一样的顺序:

//编译chen.il
ilasm chen.il

运行生成的chen.exe

//运行生成的chen.exe
mono chen.exe

结果如图:

输入1和2,最后显示结果为:1 + 2 = 3。

OK,大功告成。

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

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

后记

上一篇文章:《用CIL写程序:你好,沃尔德

chen.il所有的内容如下:

.assembly extern mscorlib
{
.ver :::
.publickeytoken = (B7 7A 5C E0 ) // .z\V.4..
}
.assembly 'HelloWorld'
{
.custom instance void class [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::'.ctor'() = (
4E 6F 6E // ....T..WrapNonEx
6F 6E 6F ) // ceptionThrows. .hash algorithm 0x00008004
.ver :::
}
.module HelloWorld.exe // GUID = {EF275948-881F-4408-AE6B-F9130972ECD5}
.method static void Fanyou()
{
.maxstack ldstr "Hello World!"
call void [mscorlib]System.Console::WriteLine(string) ret
}
.method static void AddLife()
{
.entrypoint
.maxstack
//局部变量
.locals init (int32 num1,
int32 num2,
int32 result)
//第一个功能:显示提示输入加数,并获取输入的值
//在屏幕上显示“请输入第一个加数”
ldstr "请输入第一个加数"
call void [mscorlib]System.Console::WriteLine(string)
//获取用户的输入值
call string [mscorlib]System.Console::ReadLine()
//将输入的字符串转化成int
call int32 [mscorlib]System.Int32::Parse(string)
//值出栈,赋给局部变量num1
stloc num1
//num2
ldstr "请输入第二个加数"
call void [mscorlib]System.Console::WriteLine(string)
call string [mscorlib]System.Console::ReadLine()
call int32 [mscorlib]System.Int32::Parse(string)
stloc num2 //第二个功能:相爱相杀,不对,应该是相爱相加...
//将值从变量中压入堆栈
ldloc num1
ldloc num2
//求和
add
//将结果赋值给result
stloc result
//最后一个功能,关键的其实是装箱
//显示的格式
ldstr "{0} + {1} = {2}"
//将num1,num2,result装箱,供之后的writeLine使用。
ldloc num1
box int32
ldloc num2
box int32
ldloc result
box int32
//将算式显示出来
call void [mscorlib]System.Console::WriteLine(string, object, object, object)
ret
}

用CIL写程序:写个函数做加法的更多相关文章

  1. 循序渐进做项目系列(1):最简单的C/S程序——让服务器来做加法

    (本文是专门针对未接触过C/S开发的初学者而写的,C/S开发高手请自动忽略啊~~) 还在写“Hello world!”式的单机程序吗?还在各种拖控件吗?是否自己都觉得有点low呢?来个质的飞跃吧!看看 ...

  2. 最简单的C/S程序——让服务器来做加法

    还在写“Hello world!”式的单机程序吗?还在各种拖控件吗?是否自己都觉得有点low呢?来个质的飞跃吧!看看怎么让服务器帮咱做加法! 所谓C/S程序就是Client/Server程序,自然既包 ...

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

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

  4. 开发(ASP.NET程序)把写代码写至最有面向对象味道

    前几天,搬房子时又拿起<重构----改善既有代码的设计>这本书来随便翻来看下,重构Refactoring在开发时,是时常也经常会使用得到. 她确实教我们怎样把写程序写简洁,清楚 好明白,好 ...

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

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

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

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

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

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

  8. .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换

    .NET/C# 程序从 Main 函数开始执行,基本上各种书籍资料都是这么写的.不过,我们可以写多个 Main 函数,然后在项目文件中设置应该选择哪一个 Main 函数. 你可能会觉得这样没有什么用, ...

  9. 大湿教我写程序(2)之走向AV之路

    一.大摆庆功宴 上一篇博文<大湿教我写程序(1)之菜单导航篇>中讲到了我撸码到晚上两点多,整出了一个还算是高端大气上档次的demo.半夜回到家里打算着可以好好睡上一个懒觉,到时候直接到客户 ...

随机推荐

  1. 在.NET Core 里使用 BouncyCastle 的DES加密算法

    .NET Core上面的DES等加密算法要等到1.2 才支持,我们可是急需这个算法的支持,文章<使用 JavaScriptService 在.NET Core 里实现DES加密算法>需要用 ...

  2. ABP文档 - Javascript Api - Message

    本节内容: 显示信息 确认 Message API给用户显示一个信息,或从用户那里获取一个确认信息. Message API默认使用sweetalert实现,为使sweetalert正常工作,你应该包 ...

  3. solr_架构案例【京东站内搜索】(附程序源代码)

    注意事项:首先要保证部署solr服务的Tomcat容器和检索solr服务中数据的Tomcat容器,它们的端口号不能发生冲突,否则web程序是不可能运行起来的. 一:solr服务的端口号.我这里的sol ...

  4. WebApi - 路由

    这段时间的博客打算和大家一起分享下webapi的使用和心得,主要原因是群里面有朋友说希望能有这方面的文章分享,随便自己也再回顾下:后面将会和大家分不同篇章来分享交流心得,希望各位多多扫码支持和点赞,谢 ...

  5. js数组学习整理

    原文地址:js数组学习整理 常用的js数组操作方法及原理 1.声明数组的方式 var colors = new Array();//空的数组 var colors = new Array(3); // ...

  6. 【干货分享】流程DEMO-出差申请单

    流程名: 出差申请  业务描述: 员工出差前发起流程申请,流程发起时,会检查预算,如果预算不够,将不允许发起费用申请,如果预算够用,将发起流程,同时占用相应金额的预算,但撤销流程会释放相应金额的预算. ...

  7. 基于jQuery左右滑动切换特效 附源码

    分享一款基于脚jQuery左右滑动切换特效.这是一款鼠标点击左右箭头按钮图片滚动切换,鼠标移到图片上显示透明边框特效.   效果图如下:   废话不多说,代码奉上!   html代码: <div ...

  8. Mono 3.2.3 Socket功能迎来一稳定的版本

    由于兴趣自己业余时间一直在搞.net下面的通讯应用,mono的存在得以让.NET程序轻松运行在Linux之下.不过经过多尝试Socket相关功能在Mono下的表现并不理想.不管性能还是吞吐能力方面离我 ...

  9. 程序猿是如何解决SQLServer占CPU100%的

    文章目录 遇到的问题 使用SQLServer Profiler监控数据库 SQL1:查找最新的30条告警事件 SQL2:获取当前的总报警记录数 有哪些SQL语句会导致CPU过高? 查看SQL的查询计划 ...

  10. Microsoft Azure Web Sites应用与实践【4】—— Microsoft Azure网站的“后门”

    Microsoft Azure Web Sites应用与实践 系列: [1]—— 打造你的第一个Microsoft Azure Website [2]—— 通过本地IIS 远程管理Microsoft ...