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();
这样我们在执行可执行程序时就能够看到程序的输出了.
下面是代码片段的列表:
- PrintString—把字符串输出到控制台.
- XequalN—把一个值赋给一个int变量,并把它输出到控制台.
- Operations—从控制台读入两个数并进行加减乘操作,然后将结果输出到控制台.
- Array1—声明一个int数值,给数值的每个元素赋值; 把数值的每个元素和数组的长度输出到控制台.
- Compare—输入两个数,并把较小的一个打印出来.
- Array2—在循环中填充数组并打印数组的元素.
- Unsafe—使用unsafe的指针访问数组元素.
- PInvoke—calls Win32 API.
- Classes—works with classes.
- Exception—handles exceptions.
我建议大家按照我列的顺序来阅读这些代码, 在每个代码片段的描述中,我将介绍一些新的IL命令的使用方法.
PrintString 是一个用IL写的Hello, World 程序.
在代码中直接使用的IL指令包括:
- .entrypoint—定义程序的入口(当程序启动时被.Net运行时调用的函数).
- .maxstack—定义被函数使用的堆栈的最大深度. C#编译器会为每个函数设定一个精确的深度. 在示例中我把它设为8.
MSIL 命令:
- ldstr string—将字符串压入堆栈
- call function(parameters)—调用静态函数. 在调用这个函数之前应该把它的参数压入堆栈.
- pop—将一个值从堆栈中弹出. 当我们不需要将这个值存到变量里的时候,调用这个命令.
- ret—函数返回.
调用静态函数其实很简单. 我们把参数压入堆栈, 然后调用函数,然后从堆栈中读取函数的结果(如果函数不是void). Console.WriteLine 就是这样一个函数.
代码如下:
Code
.assembly PrintString {}
/**//*
Console.WriteLine("Hello, World)"
*/
.method static public void main() il managed
{
.entrypoint // this function is the application
// entry point
.maxstack 8
// *****************************************************
// Console.WriteLine("Hello, World)";
// *****************************************************
ldstr "Hello, World" // load string onto stack
// Call static System.Console.Writeline function
// (function pops string from the stack)
call void [mscorlib]System.Console::WriteLine
(class System.String)
// *****************************************************
ldstr "Press Enter to continue"
call void [mscorlib]System.Console::WriteLine
(class System.String)
// Call the System.Console.Read function
call int32 [mscorlib]System.Console::Read()
// The pop instruction removes the top element from the stack.
// (remove number returned by Read() function)
pop
// *****************************************************
ret
}
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
.assembly XequalN {}
// int x;
// x = 7;
// Console.WriteLine(x);
.method static public void main() il managed
{
.entrypoint
.maxstack 8
.locals init ([0] int32 x) // Allocate local variable
// *****************************************************
// x = 7;
// *****************************************************
ldc.i4.7 // load constant onto stack
stloc.0 // store value from stack to
// var. 0
// *****************************************************
// Console.WriteLine(x);
// *****************************************************
ldloc.0 // load var.0 onto stack
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
Operations—从控制台读入两个数并进行加减乘操作,然后将结果输出到控制台.
命令:
add—将两个数值相加.在调用这个命令之前, 应先将两个操作数送入堆栈. 该命令会从堆栈中将两个操作数弹出,然后将计算结果压入堆栈
sub—将两个数值相减
mul—将两个数相乘
Code
.assembly Operations {}
/**//*
// This program works as C# code:
int x, y, z;
string s;
Console.WriteLine("Enter x:");
s = Console.ReadLine();
x = Int32.Parse(s);
Console.WriteLine("Enter y:");
s = Console.ReadLine();
y = Int32.Parse(s);
z = x + y;
Console.Write("x + y = ");
Console.Write(z);
Console.WriteLine("");
z = x - y;
Console.Write("x - y = ");
Console.Write(z);
Console.WriteLine("");
z = x * y;
Console.Write("x * y = ");
Console.Write(z);
Console.WriteLine("");
*/
.method static public void main() il managed
{
.entrypoint
.maxstack 8
.locals init ([0] int32 x,
[1] int32 y,
[2] int32 z,
[3] string s)
// *****************************************************
// Console.WriteLine("Enter x:");
// *****************************************************
ldstr "Enter x:" // load string onto stack
call void [mscorlib]System.Console::WriteLine(string)
// *****************************************************
// s = Console.ReadLine();
// *****************************************************
call string [mscorlib]System.Console::ReadLine()
stloc.3 // store value to var. 3
// *****************************************************
// x = Int32.Parse(s);
// *****************************************************
ldloc.3 // load variable 3 onto stack
// Call System.Int32::Parse(string)
// Function pops string from stack and pushes to stack
// int32 value - result of parsing.
call int32 [mscorlib]System.Int32::Parse(string)
stloc.0 // store value to var. 0
// *****************************************************
// Same operations with variable y
// *****************************************************
ldstr "Enter y:"
// load string
call void [mscorlib]System.Console::WriteLine(string)
// call
call string [mscorlib]System.Console::ReadLine()
// call
stloc.3
// store to var. 3
ldloc.3
// load var. 3
call int32 [mscorlib]System.Int32::Parse(string)
// call
stloc.1
// store to var. 1
// *****************************************************
// z = x + y;
// *****************************************************
ldloc.0 // load variable 0 onto stack
ldloc.1 // load variable 1 onto stack
// pop two values from the stack, add them and push result
// onto stack
add
stloc.2 // store to variable 2
// *****************************************************
// Console.Write("x + y = ");
// *****************************************************
ldstr "x + y = " // load string onto stack
call void [mscorlib]System.Console::Write(string)
// *****************************************************
// Console.Write(z);
// *****************************************************
ldloc.2 // load variable 2 onto stack
call void [mscorlib]System.Console::Write(int32)
// *****************************************************
// Console.WriteLine("");
// *****************************************************
ldstr "" // load string onto stack
call void [mscorlib]System.Console::WriteLine(string)
// Same operations with subtraction and multiplication
ret
}
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++中那样)以及函数的参数压入堆栈. 在示例代码中我们使用ldemema和ldloca两个命令来将地址压入堆栈.
Code
.assembly Array1 {}
/**//*
// This program works as C# code:
int[] x = new int[5];
x[0] = 10;
x[1] = 20;
Console.WriteLine("x[0] = " + x[0].ToString());
Console.WriteLine("x[1] = " + x[1].ToString());
Console.WriteLine("Array length = " + x.Length.ToString());
*/
.method static public void main() il managed
{
.entrypoint
.maxstack 8
.locals init ([0] int32[] x,
[1] int32 tmp) // generated by compiler
// *****************************************************
// x = new int[5];
// *****************************************************
ldc.i4.5 // load constant onto stack
// create array and store reference onto stack
newarr [mscorlib]System.Int32
// Store (pop) value from the stack and place it to local
// variable 0.
stloc.0
// *****************************************************
// x[0] = 10;
// *****************************************************
ldloc.0 // Load local variable 0 onto stack (array)
ldc.i4.0 // Load constant 0 to the stack (index)
ldc.i4.s 10 // Load constant 10 to the stack (value)
stelem.i4 // array[index] = value
// The same operations with element number 1
// ***************************************************
// Console.WriteLine("x[0] = " + x[0].ToString());
// ***************************************************
ldstr "x[0] = " // load string onto stack
// STACK: "x[0] = " (stack is shown from local
// variables)
ldloc.0 // load variable 0 onto stack
ldc.i4.0 // load constant 0 onto stack
// STACK: "x[0] = " -> x -> 0
// Load address of array element onto stack.
ldelema [mscorlib]System.Int32
// STACK: "x[0] = " -> pointer to Int32 instance
// 10
// Call non-static function System.Int32::ToString().
call instance string [mscorlib]System.Int32::ToString()
// STACK: "x[0] = " -> "10"
// call static System.String::Concat(string, string)
call string [mscorlib]System.String::Concat
(string, string)
// STACK: "x[0] = 10"
// call static System.Console::WriteLine(string)
call void [mscorlib]System.Console::WriteLine(string)
// STACK: empty
// The same operations with element number 1
// *****************************************************
// Console.WriteLine("Array length = " + x.Length.ToString());
// *****************************************************
ldstr "Array length = "
// load string onto stack
// STACK: "Array length = "
ldloc.0
// load variable 0 to stack
// STACK: "Array length = " -> x
ldlen
// push the length of array onto stack
// STACK: "Array length = " -> 5
conv.i4
// Convert to int32, pushing int32 onto stack
// STACK: "Array length = " -> 5
stloc.1
// store to local variable 1 (tmp)
// STACK: "Array length = "
ldloca.s tmp
// load address of variable tmp onto stack
// STACK: "Array length = " -> &tmp
call instance string [mscorlib]System.Int32::ToString()
// STACK: "Array length = " -> "5"
call string [mscorlib]System.String::Concat
(string, string)
// STACK: "Array length = 5"
call void [mscorlib]System.Console::WriteLine(string)
// STACK: empty
ret
}
Compare—输入两个数,并把较小的一个打印出来.
命令:
- bge.s label—如果value1的值大于等于value2的值, 跳转到 label处执行. 在调用这个命令之前value1和value2必需提前压入堆栈.
- br.s label—跳转到label处继续执行.
- box value type—将值类型转换成对象类型, 并将对象的引用压入堆栈
这段代码中的装箱操作是由于这个条语句引起的: Console.WriteLine("{0:d}", z);
如果写成如下形式,就不会有装箱操作了: Console.WriteLine(z.ToString());
Code
.assembly Compare {}
/**//*
int x, y, z;
string s;
Console.WriteLine("Enter x:");
s = Console.ReadLine();
x = Int32.Parse(s);
Console.WriteLine("Enter y:");
s = Console.ReadLine();
y = Int32.Parse(s);
if ( x < y )
z = x;
else
z = y;
Console.WriteLine("{0:d}", z);
*/
.method static public void main() il managed
{
.entrypoint
.maxstack 8
.locals init ([0] int32 x,
[1] int32 y,
[2] int32 z,
[3] string s)
// *****************************************************
// Console.WriteLine("Enter x:");
// *****************************************************
ldstr "Enter x:" // load string onto stack
call void [mscorlib]System.Console::WriteLine(string)
// *****************************************************
// s = Console.ReadLine();
// *****************************************************
call string [mscorlib]System.Console::ReadLine()
stloc.3 // store to var. 3
// *****************************************************
// x = Int32.Parse(s);
// *****************************************************
ldloc.3 // load var. 3 onto stack
call int32 [mscorlib]System.Int32::Parse(string)
stloc.0 // store to var. 0
// The same operations for y
// *****************************************************
// branch
// if ( x >= y ) goto L_GR;
// *****************************************************
ldloc.0 // load x onto stack (value 1)
ldloc.1 // load y onto stack (value 2)
bge.s L_GR // goto L_GR if value1 is greater
// than or equal to value2
// *****************************************************
// z = x
// *****************************************************
ldloc.0 // load variable 0 onto stack
stloc.2 // store to variable 2
br.s L_CONTINUE // goto L_CONTINUE
L_GR:
// *****************************************************
// z = y
// *****************************************************
ldloc.1 // load variable 1 onto stack
stloc.2 // store to variable 2
L_CONTINUE:
// *****************************************************
// Console.WriteLine("{0:d}", z);
// NOTE: this line causes boxing.
// *****************************************************
ldstr "{0:d}" // load string onto stack
ldloc.2 // load variable 2 to stack (z)
box [mscorlib]System.Int32 // convert Int32 to Object
call void [mscorlib]System.Console::WriteLine(string, object)
ret
}
Array2—在循环中填充数组并打印数组的元素.
命令:
- blt.s label—如果value1的值小于value2的值,那么跳转到label处继续执行. 在调用这个命令前要将value1和value2压入堆栈.
- ldelem.i4—将数组元素压入堆栈. 调用之前要将数组对象的引用以及数组元素的索引压入堆栈.
- ldarga.s argument—将函数参数的地址压入堆栈
在这段代码中我们可以看到, 在MSIL总For循环是使用 label实现的.
Code
.assembly Array2 {}
/**//*
int[] px = new int[100];
int i;
for ( i = 1; i < 100; i++ )
{
px[i] = i + 1;
}
ShowNumber(px[5]);
ShowNumber(px[10]);
static void ShowNumber(int n)
{
Console.WriteLine(n.ToString());
}
*/
.method static public void main() il managed
{
.entrypoint
.maxstack 8
.locals init ([0] int32[] px,
[1] int32 i)
// *****************************************************
// x = new int[100]
// *****************************************************
ldc.i4.s 100 // load constant onto
// stack
newarr [mscorlib]System.Int32 // allocate Int32
stloc.0 // store to variable 0
// *****************************************************
// i = 1
// *****************************************************
ldc.i4.1 // load constant onto stack
stloc.1 // store to variable 1
br.s CHECK_COUNTER // goto CHECK_COUNTER
START_LOOP:
// *****************************************************
// px[i] = i + 1;
// *****************************************************
ldloc.0 // load variable 0 to stack
// STACK: px
ldloc.1 // load variable 1 to stack
// STACK; px -> i
ldloc.1 // load variable 1 to stack
// STACK: px -> i -> i
ldc.i4.1 // load constant to stack
// STACK: px -> i -> i -> 1.
add // add last two values
// STACK: px -> i -> i+1
// (array,index,value)
stelem.i4 // store value to array element:
// array[index] = value
// STACK: empty
// *****************************************************
// i = i + 1
// *****************************************************
ldloc.1 // load variable 1 onto stack
ldc.i4.1 // load constant onto stack
add // add
stloc.1 // store to variable 1
CHECK_COUNTER:
// *****************************************************
// if i < 100 goto start f loop
// *****************************************************
ldloc.1 // load variable 1 onto stack
ldc.i4.s 100 // load constant onto stack
blt.s START_LOOP // if value1 < value2 go to
// START_LOOP
// *****************************************************
// ShowNumber(px[5]
// *****************************************************
ldloc.0 // load variable 0 onto stack
// (array)
ldc.i4.5 // load constant onto stack
// (index)
ldelem.i4 // load array element to stack
call void ShowNumber(int32) // call ShowNumber
// *****************************************************
// ShowNumber(px[10]
// *****************************************************
ldloc.0
ldc.i4.s 10
ldelem.i4
call void ShowNumber(int32)
ret
}
.method static public void ShowNumber(int32 n) il managed
{
.maxstack 1
ldarga.s n // load to stack address of argument n
call instance string [mscorlib]System.Int32::ToString()
call void [mscorlib]System.Console::WriteLine(string)
ret
}