VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > 汇编语言 >
  • IL汇编语言介绍(译)(2)

 

在这一部分,我将向你们演示怎样在IL汇编语言中创建一个类的对象并使用它。在这之前,你必须知道怎样在IL汇编语言中创建你自己的命名空间和类。但是如果不使用它,那么创建的这些东西就没有用,所以我们来开始创建一个简单的类并使用它。

我们来在IL汇编语言中创建我们自己的一个类库。这个简单的类库只包含一个公有的方法,这个方法接受一个变量并且返回这个变量的平方。简单的比较容易理解,我们来看代码:

   1: .assembly extern mscorlib {}
   2: .assembly MathLib
   3: {
   4:     .ver 1:0:1:0
   5: }
   6:  
   7: .module MathLib.dll
   8:  
   9: .namespace HangamaHouse
  10: {    
  11:     .class public ansi auto MathClass extends [mscorlib]System.Object
  12:     {
  13:         
  14:         .method public int32 GetSquare(int32) cil managed
  15:         {
  16:             .maxstack 3
  17:             ldarg.0        //Load the Object's 'this' pointer on the stack
  18:             ldarg.1
  19:             ldarg.1
  20:             mul
  21:             ret        
  22:  
  23:         }
  24:     }
  25: }

图1.11求数学平方的类库

注意:把上面的代码编译成DLL文件,用ILAsm MathLib.il /dll 指令

上面的代码看起来很简单,它定义了一个命名空间HangamaHouse,然后在命名空间里面定义了一个MathClass类,这个类和上面的代码(图1.10)中定义的类一样继承于System命名空间里面的Object类。在这个类里面我们定义了一个需要传入一个int32类型参数的方法GetSquare。 我们把maxstack的大小定义为3,然后加载第0个参数。接下来我们重复的加载两次第二个参数。等等,我们在这里只接受了一个参数,但是我们却加载了参数0和参数1(总共两个参数)。这怎么可能?确实可以,实际上第一个参数(ldarg.0)是这个对象的this指针的引用,因为每个对象的实例都会把自己在内存中的地址也传进来。所以实际上我们的参数是从索引1处开始的。我们加载第一个参数两次去为了后面执行mul指令把这两个数相乘。最后的结果将会放在栈的顶部,然后在我们调用ret指令的时候,返回给调用这个方法的地方去。

这个类库编译没有问题,我们来看看一个使用这个类库的例子

   1: .assembly extern mscorlib {}
   2: .assembly extern MathLib {.ver 1:0:1:0}
   3: //
   4: //rest code here
   5: //
   6: .method static void    Main() cil managed
   7: {
   8:     .maxstack 2
   9:     .entrypoint
  10:     .locals init (valuetype [MathLib]HangamaHouse.MathClass mclass) 
  11:             
  12:     ldloca mclass
  13:     ldc.i4 5
  14:     call instance int32 [MathLib]HangamaHouse.MathClass::GetSquare(int32)
  15:     ldstr "The Square of 5 Returned : "
  16:     call void [mscorlib]System.Console::Write(string)
  17:     call void [mscorlib]System.Console::WriteLine(int32)
  18:  
  19:     ret
  20: }
    图1.12使用类库中的MathClass类

这个方法里面最前面的两行很简单。我们看到第三行定义了一个MathClass类型的局部变量(HangamaHouse命名空间中的,注意我们已经在导入mscorlib 类库的时候导入了这个类库)。我们也提供了这个类库的版本信息,尽管可以不需要,因为我们在前面引用外部类库mscorlib类库的时候没有提供版本信息。另外在我们要创建的对象类型前面,我们加了关键字valuetype,我们也提供了这个类的完整签名包括类库的名字。接下来我们加载了局部变量mclass的地址到栈里面。然后加载了一个整型数5到栈里面,接着调用了MathClass类中的GetSquare方法。之所以能够这样做,是因为我们在之前已经加载了mclass类的对象,这个对象的内存引用已经加载到栈里面去了。当我们调用MathClass类的GetSquare方法时,它会在栈上面找这个对象的引用以及所要传入的参数。如果找到了,它就使用这个引用变量去调用那个方法。另外一个我们还注意的就是在调用方法的时候我们用到了一个我们以前从来没有用到的关键字instance,instance关键字告诉编译器我们将会以对象的实例来调用方法,它不是静态的方法。执行完这些指令后,GetSquare方法返回一个int32类型的数并把它存入栈里面,我们后面就以字符串的形式在控制台把这个数打印出来。

所以在这里最重要的事就是用.locals 指令和valuetype以及完整的签名包括类库名来声明一个类的对象,第二就是调用这个类里面的方法,它首先会加载这个类对象的引用,然后加载要传递给这个方法的任何变量,最后调用这个方法的时候加上关键字instance。

同样的,我们可以在类里面用属性和构造函数,并且在外面的代码里面使用它们。我这篇文章的下一部分将会介绍怎样在IL汇编语言中创建私有字段,构造函数和属性,然后用IL汇编代码去调用它们。

创建构造函数和属性

构造函数在高级语言中是在创建对象时会调用的一个方法,但是在低级语言中,像IL汇编语言,你需要人工的去调用它,它是不返回任何东西的一个方法。下面的示例代码演示了怎样创建构造函数。我只是把需要的代码拿出来了,这篇文章的源代码里面包括了这一部分的所有代码。在阅读这一部分的时候,一定要集中注意力,因为这一部分将会教给你很多东西。

   1: .class public MathClass extends [mscorlib]System.ValueType
   2: {        
   3:     .field private int32 mValue
   4:     //Other code goes here
   5:  
   6:     .method public specialname rtspecialname 
   7:         instance void .ctor() cil managed
   8:     {        
   9:             
  10:         ldarg.0
  11:         ldc.i4.s    15
  12:         stfld int32 HangamaHouse.MathClass::mValue
  13:         ret
  14:     }
  15:     //Other code goes here.

图1.13 MathLib类的构造函数

第一个你会注意到的就是我创建的类是继承于System.ValyeType而不是继承于Object。在.NET里面,一个类实际上也是一种类型所以它继承于ValueType。在这之前我的类是继承于Object类,但是现在不是时候去讨论这两者的区别了,因为我们要创造一个完整的类(有构造函数,属性等等),如果你不想利用类的这些特性,你可以让你的类继承于任何类。

在声明了类之后,我定义了一个私有字段mValue(高级语言中的私有变量)。然后我用.method 指令声明了一个构造函数。记住,构造函数也是一个方法。现在你会对用.ctor代替类名(在高级语言像c++中也是如此)觉得吃惊。是的,在IL汇编语言中.ctor就代表构造函数。这是一个默认的构造函数,因为它没有任何参数。我们在这里做的就是用ldarg.0语句来加载对象的引用,然后我们加载一个常量15,赋值给类中的私有变量mValue。stfld语句可以用来对任何字段赋值。我们提供了这个字段的完整签名。我想你现在应该不会吃惊我们为什么要这样做。最后我们从这个方法(构造函数)中返回。

你可能也注意到了我们在声明构造函数的时候用了一系列关键字,包括specialname和rtspecilaname.实际上,这些关键字告诉运行时把这些方法的名字当作一种特殊的名字。你可以在声明构造函数或属性的使用它,但是这些不是必要的。

不像高级语言那样,在IL汇编语言中,构造函数不是自动调用的,而是需要你显示的调用它,下面的一个代码片断演示了怎样调用一个构造函数去初始化类中的变量。

   1: .method public static void Main() cil managed
   2: {
   3:     .maxstack 2
   4:     .entrypoint
   5:     .locals init (valuetype [MathLib]HangamaHouse.MathClass mclass) 
   6:             
   7:     ldloca mclass            
   8:     call instance void [MathLib]HangamaHouse.MathClass::.ctor()

上面的代码创建一个MathClass类型的局部变量mclass,这个类型是在HangamaHouse命名空间里面的。然后我们加载这个对象变量的地址到材里面去,然后调用构造函数(.ctro方法),如果你仔细观察,你就会发现它和我们在IL汇编语言中调用其它普通方法一样,没有区别,同样的,你可以定义重载的构造函数当你在定义.ctor方法的时候,让它接受几个参数。然后像我们调用默认构造函数一样去调用它。

现在来讨论属性,其实属性实质上也是方法,看一下它的特性,我们就会完全明白。

   1: .method  specialname public instance int32 get_Value() cil managed
   2: {
   3:     ldarg.0
   4:     ldfld int32 HangamaHouse.MathClass::mValue
   5:     ret
   6: }
   7: .method specialname public instance void set_Value(int32 ) cil managed
   8: {
   9:     ldarg.0
  10:     ldarg.1
  11:     stfld int32 HangamaHouse.MathClass::mValue
  12:     
  13:     ret
  14: }        
  15: //Define the property, Value
  16: .property int32 Value()
  17: {
  18:     .get instance int32 get_Value()
  19:     .set instance void set_Value(int32 )
  20: }

图1.15属性

你可以看一下上面的代码,你会肯定的说,这和方法的代码一样。但是你在这可以看到另外一个东西,那就是.property 指令,在里面它定义了两个东西,一个是属性get,一个是属性set。说明这两个方法属于属性的一部分。我们可以说这两个方法在上面都定义了,一个是get_Value 方法一个是set_Value方法。这些方法就像在文章中出现的一些普通方法,调用属性很简单,因为它们就像方法一样。

   1: .maxstack 2 .locals
   2: init (valuetype [MathLib]HangamaHouse.MathClass tclass) 
   3:  
   4: ldloca tclass
   5: ldc.i4 25 
   6: call instance void [MathLib]HangamaHouse.MathClass::set_Value(int32) 
   7: ldloca tclass 
   8: call instance int32 [MathLib]HangamaHouse.MathClass::get_Value()
   9: ldstr "Propert Value Set to : " 
  10: call void [mscorlib]System.Console::Write(string) 
  11: call void [mscorlib]System.Console::WriteLine(int32)

图1.16 使用属性,GetSquare方法也被调用了

不要吃惊,我们创建了一个类的实例,然后调用了set_Value方法(实际上是一个属性,我们准备去修改这个属性的值)。然后为了证实一下值是否被修改了,我们重新读取了一下这个属性,然后把它打印出来。

到现在为止,我们已经讨论了IL汇编语言中的大部分内容。但是还有很重要的一部分,那就是错误和调试。(译者注:老外这好像写错了,因为调试是在最后一部分,下面实际上是讲如何创建窗体)

创建窗体

这部分内容告诉我们怎样联系上面所讲的内容创建一个简单的GUI,Windows窗体。在这个应用程序里面,我从System.Windows.Forms.Form 类中继承创建了一个简单的窗体,它没有包含任何控件,但是我修改了一些它的属性,例如BackColor,Text和WindowState。代码一步一步的比较简单。大家一起来看一下我结束这篇文章之前的最后一段代码。

   1: .namespace MyForm
   2: {
   3:   .class public TestForm extends 
   4:        [System.Windows.Forms]System.Windows.Forms.Form
   5:   {
   6:     .field private class [System]System.ComponentModel.IContainer components
   7:     .method public static void Main() cil managed
   8:     {
   9:       .entrypoint
  10:       .maxstack  1
  11:             
  12:       //Create New Object of TestForm Class and Call the Constructor     
  13:       newobj     instance void MyForm.TestForm::.ctor()
  14:       call       void [System.Windows.Forms]
  15:           System.Windows.Forms.Application::Run(
  16:           class [System.Windows.Forms]System.Windows.Forms.Form)
  17:       ret
  18:     }

图1.17Windows Form的入口函数

这是整个应用程序的入口函数,首先(在MyForm命名空间里面创建类TestForm后),我们定义了一个IContainer的局部变量(字段)。注意在定义这种字段之前,我们加上了关键字class 。然后在主函数里,我们用newobj指令创建了一个TestForm 类的对象。然后我们调用Application.Run 方法去运行这个应用程序。如果你把它与高级语言相比,你就会发现,它和我们现在用的手法是一样的。现在我们来看看我们类(TestForm)的构造函数。

   1: .method public specialname rtspecialname instance 
   2:        void  .ctor() cil managed
   3:    {     
   4:      .maxstack  4
   5:            
   6:      ldarg.0
   7:     //Call Base Class Constructor
   8:      call       instance void [System.Windows.Forms]
   9:                        System.Windows.Forms.Form::.ctor()
  10:      
  11:      //Initialize the Components
  12:      ldarg.0
  13:      newobj   instance void [System]System.ComponentModel.Container::.ctor()
  14:      stfld    class [System]System.ComponentModel.IContainer 
  15:                        MyForm.TestForm::components
  16:  
  17:      //Set the Title of the Window (Form)
  18:      ldarg.0
  19:      ldstr      "This is the Title of the Form...."
  20:      call   instance void [System.Windows.Forms]
  21:                       System.Windows.Forms.Control::set_Text(string)
  22:      
  23:      //Set the Back Color of the Form
  24:      ldarg.0
  25:      ldc.i4 0xff
  26:      ldc.i4 0
  27:      ldc.i4 0
  28:      call valuetype [System.Drawing]System.Drawing.Color 
  29:                     [System.Drawing]System.Drawing.Color::FromArgb(
  30:                     int32, int32, int32)
  31:  
  32:      call   instance void [System.Windows.Forms]
  33:                     System.Windows.Forms.Control::set_BackColor(
  34:                     valuetype [System.Drawing]System.Drawing.Color)
  35:      
  36:      
  37:      //Maximize the Form using WindowState Property
  38:      ldarg.0
  39:      ldc.i4 2        //2 for Maximize
  40:      call instance void [System.Windows.Forms]
  41:                     System.Windows.Forms.Form::set_WindowState(
  42:                     valuetype [System.Windows.Forms]
  43:                     System.Windows.Forms.FormWindowState)
  44:      ret
  45:    }

图1.18TestForm中的.ctor方法(构造函数)

非常简单,我们只是调用了基类的.ctor方法(构造函数)。然后我们创建一个Container 类的对象,把它当作我们的一个组件对象(类中的字段),窗体初始化到此就完成了。接下来我们为我们的新窗体设置一些属性。首先设置窗体的标题(Text属性)。我们加载一个字符串到栈里面去,然后调用Control类的 set_Text方法(因为Text属性是从Control继承而来的),设置了Text属性后,我们开始去设置BackColor属性。我们调用FromArgb方法从红,绿,蓝中获取颜色值。我们首先加载了三个值到栈里面然后调用Color.FromArgb方法去得到一个Color类的对象,然后把这个Color值赋给BackColor属性。我们跟前面设置Text属性一样的方式来设置BackColor属性。最后我们把WindowState 属性设置为Maximized(最大化),用同样的方式。你可能注意到了我们加载了一个常量到栈里面去,这个常量是一个FormWindowState 的枚举值,每个枚举变量都已经提前定义好了值。

尽管创建窗体的代码已经完成了,但是我们在这里也定义了一个Dispose事件(窗体的析构函数),通过这个事件我们可以移除不需要的对象以此来清理内存。如果你对Dispose事件的代码感兴趣,那么看一下几行代码。

   1: .method family virtual instance void Dispose(bool disposing) cil managed
   2:   {
   3:     .maxstack  2
   4:  
   5:     ldarg.0
   6:     ldfld      class [System]System.ComponentModel.IContainer 
   7:                  MyForm.TestForm::components
   8:     callvirt   instance void [mscorlib]System.IDisposable::Dispose()
   9:       
  10:       //Call Base Class's Dispose Event
  11:     ldarg.0
  12:     ldarg.1
  13:     call       instance void [System.Windows.Forms]
  14:                   System.Windows.Forms.Form::Dispose(bool)
  15:     ret
  16:   }

这个Dispose方法是重载的,所以它被声明为虚方法。我们只需要加载这个对象的引用(this),加载一个组件(component)字段,然后通过IDisposable调用Dispose方法,然后调用我们窗体的Dispose方法就可以了。

所以创建一个用户界面(UI)不是一件非常困难的事(尽管有一点)。从现在开始,你可以在你的窗体里面加上其它的控件比如textBox,lable等等,然后响应它们的事件,你能吗?

   错误和调试

每门程序语言里面都有错误,IL汇编语言中也不例外。像其它语言中的错误一样,在IL汇编语言中,你也可以遇到编译错误(语法错误),运行错误,逻辑错误。我不打算去详细的讲解这些错误是什么,因为你们都非常熟悉。这部分的目的是介绍一些帮助你调试程序的工具和技巧。首先在你编译程序的时候可以生成一个debug文件,在用ILAsm.exe编译代码时加上/debug分支即可,如下

   1: ILAsm.exe Test.il /debug

它将会产生一个叫Text.exe的exe文件和一个叫Test.pdb的调试信息文件,在后面调试的过程中,你将会用到这个文件。

你可以用一个工具去检验一下你的应用程序(实际上是程序集),就是PE 验证(peverify.exe),.NET Framework SDK 自带的一个工具,你可以在C:\Program Files\Microsoft .NET\Framework SDK\Bin 目录下(默认的)找到它。peverify工具并不是在源代码中去验证程序集,而是通过exe文件来验证编译器是否产生了无效的代码。在某些情况下,你需要去验证一下你的程序集,比如你使用的第三编译工具来编译你的代码,你就需要验证一下。这个工具用起来很简单,看下面的一个例子:

   1: peverify.exe Test.exe 

要想知道更多的用法,你可以用peverify.exe /?来查看peverify.exe 其它的用法。

你可以通过ILDasm.exe来得到任何编译过的Exe或DLL文件的IL代码,ILDasm.exe是另外一个非常有用的.net自带的工具,它可以帮助你在底层分析你的代码。如果你在高级语言中编写代码,而你又想看一下编译器产生的IL代码,那么这个工具也非常有用。你可以在和peverify.exe相同的目录下找到它。你可以用这种方法得到任何.NET下exe文件的IL代码。

   1: ILDasm.exe SomeProject.exe /out:SomeProject.il 

还有其它很多可以用来调试分析.NET应用程序的工具,比如DbgClr.exe, CorDbg.exe。你可以在MSDN,.NET Framework SDK 或其它第三方网站上找到许多有关的资料。

总结

在这篇文章里面,我们学习了IL汇编语言然后用IL汇编语言写了一些程序。我们从IL汇编语言的基础开始。写了一个在控制台输出一个字符串的简单程序。然后学习了一点评估堆栈的知识,用一些简单的代码(两数相加)来演示了它是怎样工作的。接着我们学习了IL数据类型,用这些数据类型声明变量,用来进行条件判断,写循环等等。我们也学会了定义了方法,在那之后,我们转向了创建命名空间和类。我们创建了我们自己的类库,用别的程序调用它。然后又在我们的类里面创建了构造函数和属性。

结论

用IL汇编语言写代码并不是一件简单的事情。还有很多东西在这篇文章里面没有讨论,比如数组,异常处理等等。但是当你对IL汇编语言熟悉之后,你可以用它做很多事情。如果你想从底层来分析你的代码,或者计划为.NET开发一个编译器,那么IL汇编语言将会非常有用。如果你是一个.NET的初学者,那么我不建议你去学习它因为学习它之前,你必须对.NET平台有一个很好的理解。但是从另一方面说,它又可以帮助你去了解.NET的运行时(CLR)在幕后是怎样工作的。

源码下载
https://files-cdn.cnblogs.com/files/xiaoxiangfeizi/ilasmcode.zip

相关教程