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

前言:

上一篇文章小匹夫为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行之前是上篇文章时写的代码):

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

此时眼尖的小伙伴一定会发现,好像是少了点什么呀?对,没有.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

结果如图:

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

输入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
}
上一篇:自用的基于Emit的C#下DataTable转实体类方法


下一篇:IL指令详细表