MSIL Tutorial (上)


MSIL 教程


原文地址:http://www.codeguru.com/Csharp/.NET/net_general/il/article.php/c4635#PrintString

介绍

MSIL是微软公司一系列的编译器的输出(如C#, VB, .NET, 等等),它也是一种语言, 我们可以用MSIL直接编写程序,但是太麻烦了:). 

ILDasm.exe是和.Net FrameWork SDK一起发布的一个工具程序, 用它我们可以查看任何可执行程序(exe和dll)的IL代码.    我们也可以使用ILAsm.exe把IL代码编译成可执行程序, 我们可以从类似于WINNT\Microsoft.NET\Framework\vn.nn.nn 的路径下找到它.

任何一个c++程序员在开始使用.Net进行开发时, 肯定会想知道.Net Framework在底层到底是怎样工作的. 通过对IL的学习,可以让使用C#的程序员有机会了解那些深埋于底层的工作机制, 加深理解. 实际上,我们没有必要直接用IL来写程序, 但是对于一些特殊的情况, 用ILDasm来看看某些功能是如何实现的是很有用的.

我认为学习IL最好的方法就是用它写几个小程序. 这也是为什么我在本书中提供了几个MSIL写的几个代码片段(实际上不是我手工写的,是C#编译器生成, 我只是进行了微小的改的, 并添加了大量的注释).增加这些代码片段有助于读者理解IL, 并在读者需要读IL的时候能够轻松读懂它.

 

一般信息

在IL中,所有的操作都是在堆栈上完成的. 当我们调用一个函数的时候, 首先要为函数参数和本地变量在堆栈上分配空间,并将他们的值加载到堆栈中. 然后函数代码开始运行,运行过程中,它可能会往堆栈写入数据或者操作堆栈中的数据或者从堆栈读取数据.
调用函数或调用MSIL的命令一般分为三步:
1. 把命令的操作数或者函数的参数送入堆栈中.
2. 执行MSIL命令或调用函数. 命令或函数从堆栈中读取操作数或参数,然后把结果在送入堆栈
3. 从堆栈中读取结果

其中第一和第三步是可选的, 譬如无返回值的函数就不需要去读结果.
堆栈中可以存放值类型和引用类型的引用, 引用类型实际上存放在托管堆中.

我们把那些用来把值压入堆栈的IL命令叫做ld...(load), 把那些用来把值从堆栈中取出来的IL命令叫做st...(store), 所以我们把压栈操作叫做loading, 把出栈操作叫做storing.


示例代码
这篇文章中包含了很多个用IL编写的控制台程序. 在用ILAsm把他们编译成可执行程序前,一定要保证ILAsm所在的路径已经包含在PATH中了. 我们可以用VS的文本编辑器来编辑这些代码.  在每个代码片段的最后我都加入了如下的代码:

Console.WriteLine("Press Enter to continue");
Console::Read();

这样我们在执行可执行程序时就能够看到程序的输出了.

下面是代码片段的列表:

  1. PrintString—把字符串输出到控制台.
  2. XequalN—把一个值赋给一个int变量,并把它输出到控制台.
  3. Operations—从控制台读入两个数并进行加减乘操作,然后将结果输出到控制台.
  4. Array1—声明一个int数值,给数值的每个元素赋值; 把数值的每个元素和数组的长度输出到控制台.
  5. Compare—输入两个数,并把较小的一个打印出来.
  6. Array2—在循环中填充数组并打印数组的元素.
  7. Unsafe—使用unsafe的指针访问数组元素.
  8. PInvoke—calls Win32 API.
  9. Classes—works with classes.
  10. Exception—handles exceptions.


我建议大家按照我列的顺序来阅读这些代码, 在每个代码片段的描述中,我将介绍一些新的IL命令的使用方法.


PrintString—把字符串输出到控制台.

PrintString 是一个用IL写的Hello, World 程序.

在代码中直接使用的IL指令包括:

  • .entrypoint—定义程序的入口(当程序启动时被.Net运行时调用的函数).
  • .maxstack—定义被函数使用的堆栈的最大深度. C#编译器会为每个函数设定一个精确的深度. 在示例中我把它设为8.

MSIL 命令:

  • ldstr string—将字符串压入堆栈
  • call function(parameters)—调用静态函数. 在调用这个函数之前应该把它的参数压入堆栈.
  • pop—将一个值从堆栈中弹出. 当我们不需要将这个值存到变量里的时候,调用这个命令.
  • ret—函数返回.

调用静态函数其实很简单. 我们把参数压入堆栈, 然后调用函数,然后从堆栈中读取函数的结果(如果函数不是void). Console.WriteLine 就是这样一个函数.

代码如下:

Code




XequalN—把一个值赋给一个int变量,并把它输出到控制台

将整数或浮点数常量压入计算堆栈。

  • ldc.i4, ldc.i4.s : 将 4-byte int32 压入堆栈
  • ldc.i4.m1 : 将 -1 压入堆栈
  • ldc.i4.0 ... ldc.i4.8 : 将整数 0...8 压入堆栈
  • ldc.i8 : 将 8-byte int64 压入堆栈
  • ldc.r4 : 将 4-byte float32 压入堆栈
  • ldc.r8 : 将 8-byte float64 压入堆栈

stloc.n 从堆栈中取出最顶部的值,并把它保存到第n个变量中
ldloc.n 将第n个变量中的值压入堆栈

Code




Operations—从控制台读入两个数并进行加减乘操作,然后将结果输出到控制台.

命令:

add—将两个数值相加.在调用这个命令之前, 应先将两个操作数送入堆栈. 该命令会从堆栈中将两个操作数弹出,然后将计算结果压入堆栈
sub—将两个数值相减
mul—将两个数相乘

Code




Array1—声明一个int数值,给数值的每个元素赋值; 把数值的每个元素和数组的长度输出到控制台.

命令: 

  • newarr type创建Type类型的数组, 在调用这个命令之前要先把数组的大小压入堆栈. 把对数组对象的引用压入堆栈(数组实际上存在于托管堆上)
  • stelem.i4把一个32位的整型值赋给数组元素.在调用这个命令前需要将数组对象的引用, 索引值和要赋给数组元素的值都压入堆栈.
  • ldelema type把数组元素的地址压入堆栈. 在调用这个命令之前要将数组对象的引用以及索引值压入堆栈. 压入堆栈的地址可以用来对非静态函数进行调用.
  • ldlen将数组的长度压入堆栈. 调用这个命令之前要将数组对象的引用压入堆栈
  • ldloca.s variable将变量的地址压入堆栈
  • ldc.i4.s value将一个32位整型常量压入堆栈 (用于将比8大的的值压入堆栈).
  • conv.i4把从堆栈中取出的值转换成32位整型值,然后将结果压入堆栈
  • call instance function(arguments)调用非静态函数. 在调用非静态函数之前, 我们需要将对象的地址 (作为函数的第一个隐藏参数, 就像c++中那样)以及函数的参数压入堆栈. 在示例代码中我们使用ldememaldloca两个命令来将地址压入堆栈.

Code




 

Compare输入两个数,并把较小的一个打印出来.

命令:

  • bge.s label如果value1的值大于等于value2的值, 跳转到 label处执行. 在调用这个命令之前value1value2必需提前压入堆栈.
  • br.s label跳转到label处继续执行.
  • box value type将值类型转换成对象类型, 并将对象的引用压入堆栈

这段代码中的装箱操作是由于这个条语句引起的: Console.WriteLine("{0:d}", z);

如果写成如下形式,就不会有装箱操作了: Console.WriteLine(z.ToString());

Code




 

Array2在循环中填充数组并打印数组的元素.

命令:

  • blt.s label如果value1的值小于value2的值,那么跳转到label处继续执行. 在调用这个命令前要将value1value2压入堆栈.
  • ldelem.i4将数组元素压入堆栈. 调用之前要将数组对象的引用以及数组元素的索引压入堆栈.
  • ldarga.s argument将函数参数的地址压入堆栈

在这段代码中我们可以看到, MSILFor循环是使用 label实现的.

Code





 

posted @ 2008-07-01 16:35  Simon.guo  阅读(330)  评论(0编辑  收藏  举报