在.Net框架中,公共语言基础结构使用公共语言规范来绑定不同的语言。通过要求不同的语言至少要实现公共类型系统(CTS)包含在公共语言规范中的部分,公共语言基础结构允许不同的语言使用.Net框架。因此在.Net框架中,所有的语言(C#,VB.Net,Effil.Net等)最后都被转换为了一种通用语言:微软中间语言(MSIL)。
MSIL是将.Net代码转化为机器语言的一个中间过程。它是一种介于高级语言和基于Intel的汇编语言的伪汇编语言。当用户编译一个.Net程序时,编译器将源代码翻译成一组可以有效地转换为本机代码且独立于CPU 的指令。当执行这些指令时,实时(JIT)编译器将它们转化为CPU特定的代码。由于公共语言运行库支持多种实时编译器,因此同一段MSIL代码可以被不同的编译器实时编译并运行在不同的结构上。从理论上来说,MSIL将消除多年以来业界中不同语言之间的纷争。在.Net的世界中可能出现下面的情况:一部分代码可以用Effil实现,另一部分代码使用C#或VB完成的,但是最后这些代码都将被转换为中间语言。这给程序员提供了极大的灵活性,程序员可以选择自己熟悉的语言,并且再也不用为学习不断推出的新语言而烦恼了。 解密微软中间语言的系列文章将通过一些简单易懂的方式来揭示中间语言的复杂原理。这些原理通过详细的例子来阐述。
解密微软中间语言MSIL之中间语言概述
在.Net框架中,公共语言基础结构使用公共语言规范来绑定不同的语言。通过要求不同的语言至少要实现公共类型系统(CTS)包含在公共语言规范中的部分,公共语言基础结构允许不同的语言使用.Net框架。因此在.Net框架中,所有的语言(C#,VB.Net,Effil.Net等)最后都被转换为了一种通用语言:微软中间语言(MSIL)。 MSIL是将.Net代码转化为机器语言的一个中间过程。它是一种介于高级语言和基于Intel的汇编语言的伪汇编语言。当用户编译一个.Net程序时,编译器将源代码翻译成一组可以有效地转换为本机代码且独立于CPU 的指令。当执行这些指令时,实时(JIT)编译器将它们转化为CPU特定的代码。由于公共语言运行库支持多种实时编译器,因此同一段MSIL代码可以被不同的编译器实时编译并运行在不同的结构上。从理论上来说,MSIL将消除多年以来业界中不同语言之间的纷争。在.Net的世界中可能出现下面的情况:一部分代码可以用Effil实现,另一部分代码使用C#或VB完成的,但是最后这些代码都将被转换为中间语言。这给程序员提供了极大的灵活性,程序员可以选择自己熟悉的语言,并且再也不用为学习不断推出的新语言而烦恼了。 解密微软中间语言的系列文章将通过一些简单易懂的方式来揭示中间语言的复杂原理。这些原理通过详细的例子来阐述。在一些例子中同时给出了源代码和中间代码,通过比较源代码和中间代码,我们可以更好地理解编译器的局限性,指导我们编写出更好更快的代码。 微软中间语言概述 1.用中间语言编写的一个简单程序 让我们从经典的Hello World例子开始。首先在一个文本编辑器中输入以下的代码,并保存为HelloWorld.il: .assembly HelloWorldIL {} .method static void HelloWorld() { .entrypoint ldstr "Hello World." call void [mscorlib]System.Console::WriteLine(class System.String) ret } 在一个中间语言程序中,如果某一行以“.”开始,则代表这是一个传输给汇编工具的指令,该指令要求汇编工具执行某些操作,例如生成一个函数或类。而没有以“.”开始的行是中间语言的代码。在中间语言中方法通过汇编命令method来定义,汇编命令后跟方法的返回值、名称和参数。方法体被包含在{}中。例子中的ret代表该方法的结束。 一个中间语言文件可以包含很多函数,汇编工具没有办法分辨应该首先执行哪一个方法。在诸如C#或VB这一类高级语言中,程序的入口方法通常都有特定的名称,例如在C#中的public static void Main()。这就是上面的汇编工具发出错误提示的原因。在中间语言中,第一个被执行的方法被称为入口函数(EntryPoint Function)。为了告诉汇编工具HelloWorld是入口函数,我们需要在代码中增加一条汇编命令entrypoint,该命令可以放在方法体中的任何位置。需要注意的是在一个程序集中只能有一个入口函数。 中间语言代码通常被编译成一个模块,该模块隶属于一个程序集。在.Net中模块和程序集的概念非常重要,因此开发人员需要很清楚地了解它们。在后面的文章中我们将详细讨论.Net程序的结构。通过在代码中加入assembly命令,可以告诉汇编工具中间代码隶属于那个程序集。assembly命令的格式如下: .assembly <程序集名称> {} 需要注意在method命令后加入了static关键字,这是因为每个入口函数必须是静态的,例如在C#中我们将Main方法定义为public static void Main()。 接下来我们需要调用WriteLine方法将HelloWorld字符串输出到屏幕。通过使用call指令(Instruction)我们可以达到这个目的。指令的格式如下: call <return type> <namespace>.<class name>::<method name> 这里我们可以看到当调用一个方法时,中间语言和其他的编程语言有很大的区别。在中间语言中,如果需要调用一个方法,需要指定方法的全名,包括他的名称域(namespace)、类名、返回值类型和参数的数据类型。这样就保证了汇编工具能够找到正确的方法。 在调用WriteLine方法时需要一个字符串参数。所有传递给方法或函数的参数都被保存在内存的堆栈中。在中间语言中有一个指令ldstr可以从堆栈中加载一个字符串。(堆栈是内存中的一块区域,它被用于将参数传输给方法,在后面我们会详细讨论堆栈的问题)。所有的方法都从堆栈中获取它们的参数,因此ldstr指令是必不可少的。ldstr指令的格式如下所示: ldstr <parameter string> 我们可以用ILAsm.exe来编译这个程序。在运行ILAsm.exe之前,首先需要确认一下该程序已经包含在了Windows操作系统的Path环境变量中。ILAsm.exe 可在下面的路径中找到: %windir%/Microsoft.NET/Framework/v1.0.xxxx 其中xxxx是正在使用的.NET框架的内部版本号。例如我使用的版本号是3705,则应该如下设置Path环境变量: Set Path = %Path%;c:/Windows/Microsoft.NET/Framework/v1.0.3705 然后运行cmd.exe(开始->运行->输入cmd->按下确认键)。在弹出的命令窗口中输入: J:/Testcode>ilasm HelloWorld.il 汇编代码后运行程序就可以看到Hello World.的输出。 通过上面的例子,我们了解了中间语言的程序结构,一些命令和指令。同时需要提醒大家的是中间语言是区分大小写的。 2.改进的HelloWorld例子 在.Net中的所有语言都是面向对象的语言,但是上面的HelloWorld例子是一个结构化的例子。下面让我们来看一下如何将它转化为面向对象的代码。在面向对象的编程中,我们将操作定义在类中。为了将上面的HelloWorld例子转化为面向对象的代码,可以使用class命令: .class HelloWorld { } class命令后紧跟的是类的名称。类的名称在中间语言中是可选的。同时我们还需要为该指令添加一些属性,例如存取控制类在内存中的布局和互用性等。这样代码就变成了: .assembly HelloWorldIL {} .class public auto ansi HelloWorld extends [mscorlib]System.Object { .method public hidebysig static void HelloWorld() cil managed { .entrypoint ldstr "Hello World." call void [mscorlib]System.Console::WriteLine(class System.String) ret } .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ldarg.0 call instance void [mscorlib]System.Object::.ctor() ret } } 在代码中用到了三个属性: · public:public是访问控制属性,它表明了对于访问该类的成员没有限制。 · auto:auto属性表明了当类被加载到内存中时,在内存中的布局是由公共运行库而不是程序决定的。 · ansi:指定ansi属性是为了在没有被管理和被管理的代码之间实现无缝的转化。在.Net中,那些不可直接应用在公共语言基础设施之上的代码被称为没有被管理的代码,例如C、C++和VB6的代码。我们需要一个属性来处理被管理的代码和没有被管理的代码之间的互用性。在被管理的代码中,字符串用双字节的Unicode字符表示,而在被管理的代码中,字符串有可能用单字节的ANSI字符表示。指定了ansi属性就可以在不同的代码间转化字符串了。 我们知道在.Net框架中,所有的类都直接或间接地继承了System.Object类。在代码中我们明确指定了HelloWorld继承了System.Object。 在HelloWorld方法中加入了public、hidebysig、cil managed属性,下面是对这些属性的解释: · public:在C#或VB.Net中,当我们定义一个方法时,需要指定方法的访问修饰符。访问修饰符可以是public、protected、internal或private 。 · hidebysig:一个类可以继承其他的类,hidebysig属性保证当前类中的方法在作为父类时不会被子类继承。例如如果HelloWorldChild类继承了HelloWorld类,在HelloWorldChild中不会看到HelloWorld方法。 · cil managed:该属性将在后面讨论。 在高级语言中(C#,VB.Net等),每个类必须有构造函数,而且构造函数的第一行需要调用基类的构造函数。如果类中没有构造函数,基类的构造函数将被自动调用。通常这是由编译器自动完成的,现在我们要在的代码中加入构造函数,该构造函数通过.ctor命令调用基类的构造函数。 小结 本文我们从经典的Hello World例子开始,通过实例了解了微软中间语言的基本语法规则以及中间语言与其他开发语言的关系。在下一篇文章中,我们将在此基础上,运用实例程序讲述.net应用程序的格式和结构等内容。
解密微软中间语言MSIL之解析.Net应用程序
.Net应用程序由一个或多个可执行程序组成,每个可执行程序中都有元数据和可管理的代码。.Net应用程序通常被称为程序集。一个程序集由一个或多个部署在一起的文件组成,它通常保存一份清单,该清单确定程序集标识,指定组成程序集实现的文件,指定组成程序集的类型和资源,列举对其他程序集的编译时依赖项,并指定为保证程序集正确运行所需要的权限集。在运行时使用此信息来解析引用,强制版本绑定策略,并验证已加载的程序集的完整性。 不含程序集清单的中间语言文件被称为模块。程序集可以是单模块的,也可以是多模块的。每个程序集只能够有一个清单,该清单驻留在拥有入口函数的模块中。图一显示了一个单模块程序集的结构: ![]() 图一 单模块程序集的结构 从图一中我们可以看到程序集中包含了程序集标识段,元数据段和中间语言代码段。让我们来看一下HelloWorld中的代码,其中的assembly命令代表是程序集标识段,但是在其中没有包含版本、名称、区域性、安全性和模块信息。让我们在代码中加入下面的行(代码重用黑体标出): .assembly DemystifyingILChapter1 { .hash algorithm 0x00008004 .ver 1:0:0:0} .class public auto ansi HelloWorld extends [mscorlib]System.Object { … } 上面的代码扩充了assembly命令的内容。事实上assembly命令可以包含很多其它的命令,在上面的代码中使用了hash和ver命令。 · hash:该命令告诉VSE实现安全性所使用的哈希算法。数字0x00008004表示使用SHA1,这也是系统的缺省设置。 · ver:程序集的版本号,由四个32位整数组成。 前面在讨论.Net应用程序的格式的时候,曾提到在一个可执行的应用程序中可以包含对其它模块的引用。到目前为止我们还没有使用任何命令来告知程序集应该生成哪一个模块。在HelloWorld例子中我们引用了一个外部程序mscorlib。那么汇编工具正确编译了代码吗?答案是肯定的。ILAsm能够自动将HelloWorld中的代码定义为基本模块,并在其中引用mscorlib程序集。在下面的代码中我们将使用命令告诉编译器如何集成模块。我们使用的命令仍然是assembly,不过现在带上了extern属性。为了正确地引用一个程序集,至少需要创作者的公钥或公钥Token以及程序集的版本信息。公钥Token是SHA1哈希码的低八位字节,它能够唯一确定一个程序集。我们可以在C:/Winnt/assembly目录下找到程序集的相关信息(如图二所示)。 ![]() 图二 程序集的相关信息 在C:/Winnt/assembly中可以看到计算机上安装的mscorlib版本是1.0.3300.0。公钥Token是B77A5C561934E089。也许你计算机上安装了不同版本的mscorlib。在下面的代码中你需要用正确的版本号替代1.0.3300.0。 .module Hello.exe .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89) .ver 1:0:3300:0 } .assembly DemystifyingILChapter1 { … } .class public auto ansi HelloWorld extends [mscorlib]System.Object { … } 现在我们拥有了一个正确的.Net应用程序,该程序中提供了.Net框架所有必要的信息。下面我们将用C#和VB.Net中编写HelloWorld程序,并将它们编译成中间代码。 在高级语言中实现HelloWorld例子程序 我们将在C#和VB.Net中编写HelloWorld程序,将它们编译成可执行程序,然后用反汇编工具ildasm.exe将执行程序反汇编为中间代码,把它们和上面的例子进行比较。 C# public class HelloWorld { public static void Main() { System.Console.WriteLine("Hello World."); } } 当生成可执行文件后,用ildasm.exe来反汇编才生成的HelloWorld.exe。在命令行中输入: ildasm /out=HelloWorld.txt HelloWorld.exe 查看生成的HelloWorld.txt文件,我们可以看到: // Microsoft (R) .NET Framework IL Disassembler. Version 1.0.3705.0 // Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z/V.4.. .ver 1:0:3300:0 } .assembly HelloWorld { // --- The following custom attribute is added automatically, --- // --- do not uncomment ------- // .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute:: // .ctor(bool, bool) = ( 01 00 00 01 00 00 ) .hash algorithm 0x00008004 .ver 0:0:0:0 } .module HelloWorld.exe // MVID: {E63F9CA9-D4C4-4826-9BE1-2B0EE3694289} .imagebase 0x00400000 .subsystem 0x00000003 .file alignment 512 .corflags 0x00000001 // Image base: 0x03000000 // // ============== CLASS STRUCTURE DECLARATION ================== // .class public auto ansi beforefieldinit HelloWorld extends [mscorlib]System.Object { } // end of class HelloWorld // =============== CLASS MEMBERS DECLARATION =================== // note that class flags, 'extends' and 'implements' clauses // are provided here for information only .class public auto ansi beforefieldinit HelloWorld extends [mscorlib]System.Object { .method public hidebysig static void Main() cil managed { .entrypoint // Code size 11 (0xb) .maxstack 1 IL_0000: ldstr "Hello World." IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: ret } // end of method HelloWorld::Main .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // Code size 7 (0x7) .maxstack 1 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } // end of method HelloWorld::.ctor } // end of class HelloWorld // ============================================================= //*********** DISASSEMBLY COMPLETE *********************** // WARNING: Created Win32 resource file HelloWorld.res 如果仔细察看一下上面的文件,我们会发现其中大部分的命令和指令在前面已经作了阐述。如果用VB.Net来编写HelloWorld程序,编译器的输出基本上和C#一样。因此虽然使用的语言不一样,但是源代码最终会编译成相同的中间代码,因此由于语言之间的差别产生的种种问题在.Net中都不足为道了。 小结 本文我们延续使用了《解密微软中间语言MSIL之中间语言概述 》Hello World例子程序,详细分析了.net应用程序的格式和结构。接下来,我们将在下一篇文章中完成对程序的调试工作。
解密微软中间语言MSIL之调试程序(1)
没有程序员敢保证没有经过调试的代码绝对没有错误,无论他/她智商多么高,开发出来的代码总是或多或少带有一些错误(当然是无意的:-))。这些错误可能是简单的语法错误或者复杂的逻辑错误。因此和其他语言一样,我们需要中间语言的调试工具/方法。由于中间语言是比较底层的语言,因此调试工具/方法对于程序员来说更加重要。 最简单的调试方法莫过于在程序中加入WriteLine方法,但是在中间语言中使用这种方法非常繁琐,因为调用WriteLine方法需要三行代码: ldstr "Hello World" call void [mscorlib]System.Console::WriteLine(string) ret 如果你需要调试一个比较大的应用程序,显然上面的方法行不通。幸运的是,微软在.Net中提供了两个调试工具用于调试.Net的程序集。 调试工具 .Net提供了两个非常好的调试工具,分别是CLR调试器和运行库调试器: · CLR调试器(DbgCLR.exe):提供图形界面帮助开发者调试程序。 · 运行库调试器(Cordbg.exe):使用运行库调试API,通过命令行对程序进行调试。 初看这两个工具提供的是相同的功能。但是事实上它们的功能还是有所区别的。DbgCLR.exe是一个Windows应用程序,提供了用户界面,并且很容易定义断点和即时窗口;而Cordbg.exe使一个命令行工具,它允许开发人员通过调试脚本的方式来调试程序。本文侧重于介绍DgbCLR.exe。 CLR调试器 为了展示CLR调试器的功能,我们需要编写一个带有错误的程序。下面是一个C#的程序,编译后我们将通过利用ildasm.exe获得它的中间语言代码。 using System; namespace ErrorneousApp { class ErrorneousClass { [STAThread] static void Main(string[] args) { int operand1; int operand2; int sum; operand1 = int.Parse(args[0]); operand2 = int.Parse(args[1]); sum = Add(operand1 , operand2); Console.WriteLine(sum); } private static int Add(int op1, int op2) { // 很明显的逻辑错误 :-) return 5 * (op1 + op2); } } } 我们可以看到在这段代码中存在两个错误: · 逻辑错误:Add方法返回了不正确的值 · 方法错误:如果调用方法是没有提供两个参数,程序将报错。 需要提醒大家的是,CLR调试器能够调试的错误远远不止以上两种。现在编译这段代码,生成可执行文件,然后用反汇编工具生成中间语言,其中去除了一些不重要的代码: .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89) .ver 1:0:3300:0 } .assembly ErrorneousApp { .ver 1:0:1026:17140 } .module ConsoleApplication1.exe .namespace ErrorneousApp { .class private auto ansi beforefieldinit ErrorneousClass extends [mscorlib]System.Object { .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .locals init ([0] int32 operand1, [1] int32 operand2, [2] int32 sum) ldarg.0 ldc.i4.0 ldelem.ref call int32 [mscorlib]System.Int32::Parse(string) stloc.0 ldarg.0 ldc.i4.1 ldelem.ref call int32 [mscorlib]System.Int32::Parse(string) stloc.1 ldc.i4.5 ldloc.0 ldloc.1 add mul stloc.2 ldloc.2 call void [mscorlib]System.Console::WriteLine(int32) ret } // end of method ErrorneousClass::Main .method private hidebysig static int32 Add(int32 op1, int32 op2) cil managed { .locals init ([0] int32 CS$00000003$00000000) ldc.i4.5 ldarg.0 ldarg.1 add mul stloc.0 ldloc.0 ret } // end of method ErrorneousClass::Add .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ldarg.0 call instance void [mscorlib]System.Object::.ctor() ret } // end of method ErrorneousClass::.ctor } // end of class ErrorneousClass } // end of namespace ErrorneousApp 在使用代码以前先解释一下代码。我们使用了Consle.WriteLine和Int.Parse方法,这两个方法定义在外部程序集mscorlib中,因此我们需要创建一个对它们的引用。通过使用带external参数的assembly命令我们可以达到这个目的。 然后代码中通过class命令定义了ErrorneousClass类,并在该类中用method命令创建了Main方法,该方法是程序的入口方法。接着用local命令初始化了三个本地变量: .locals init ([0] int32 operand1, [1] int32 operand2, [2] int32 sum) 接下来需要给这些本地变量赋值。代码通过ldelm命令从程序的参数数组中提取相应的值赋给变量,但是在赋值之前需要用ldarg将参数(由指定索引值引用)加载到堆栈上,然后ldc命令将真正的值推送到计算堆栈上。ldc.i4.0的表示将0作为int32类型推送到计算堆栈上。接下来代码调用System.Int32.Parse方法将字符串转换为整数。当所有的变量都完成初始化后,代码调用Add方法计算operand1和operand2的和。最后通过调用System.Console.WriteLine方法来显示计算结果。 Add方法的实现也很简单。需要提醒大家的是由于MSIL工作在基于堆栈的内存结构上,因此最后使用的变量需要最先保存。由于算法是将两个数相加在乘以5,因此需要先用ldc.i4.5命令将5放入堆栈中,然后加载两个被操作的数,使用add命令计算它们的和(add命令自动将堆栈中最顶层的两个数相加),将得到的结果放回堆栈中。最后代码调用mul命令将两个数相乘。 如果仔细察看中间代码,我们会发现在Main方法中并没有直接调用Add方法。这是因为C#编译器用内嵌代码替代了对静态方法的调用。 调试MSIL代码 调试中间代码的同时我们需要程序数据库文件ErrorneuosApp.pdb,在该文件中包含了调试和工程状态信息。我们可以利用ilasm工具来获得该文件。 ilasm errorneousApp.il /debug 现在运行DbgClr.exe并打开ErroneousApp.il文件(如图一所示),然后设定需要调试的可执行文件(如图二所示)。现在就可以调试程序了。开发人员可以设定断点,查看寄存器和内存中的值等。下面让我们一一了解这些功能。 ![]() 图一 CLR调试器 ![]() 图二 选择要调试的程序
解密微软中间语言MSIL之调试程序(2)
中断程序的执行 CLR调试器最基本的目的是显示被调试程序的状态信息。有很多工具可以监视和修改程序的状态,但是大部分的工具在程序中断时才能够使用。调试器在程序运行到一个断点或遇到错误时将中断程序的运行。 开发人员可以点击中间代码中任何可执行的行左边的空白处就可以设置位置断点了;也可以通过菜单上调试->新断点来设置位置断点(如图三所示)。第二种方法允许开发人员设定带有条件的位置断点,开发人员可以设定三种类型的断点: · "函数断点"使程序在执行到达指定函数内的指定位置时中断。 · "文件断点"使程序在执行到达指定文件内的指定位置时中断。 · "地址断点"使程序在执行到达指定的内存地址时中断。 ![]() 图三 设置新断点 开发人员还可以根据设定条件判断是否需中断程序,或者根据断点的命中次数来确定是否在断点中断程序(点击次数是断点被点击的次数。对于位置断点,它是程序执行达到指定位置并满足可能有的断点条件的次数,命中次数决定执行中断前点击发生的次数)。当程序执行到断点位置时,调试器就会就会计算实现设定好的表达式。如果表达式的结果为真(在"断点条件"对话框中是"为真")或者表达式的值更改时(在"断点条件"对话框中为"已更改")则调试器点击断点,如果点击断点并且点击次数正确,调试器将中断程序执行。 局部变量窗口 在局部变量窗口中显示了在当前上下文关系中的局部变量变量和它们的值。开发人员可以通过选择调用堆栈或线程来切换上下文。使用局部变量窗口的好处在于能够在程序运行的时候改变某个变量的值,开发人员只需要双击变量的值,输入新的值就可以了。 ![]() 图四 局部变量窗口 快速监视窗口 开发人员可以通过快速监视窗口获得一个变量或者表达式的值。 监视窗口 监视窗口同快速监视窗口的功能类似,唯一不同的是快速监视窗口是模态窗口。 寄存器窗口 寄存器窗口中显示当前寄存器中的状态。最近被改变了的寄存器值用红色显示。 ![]() 图五 寄存器窗口 调用堆栈窗口 通过调用堆栈窗口,开发人员可以看到当前在堆栈中的方法或函数。除了方法或函数的名称外,窗口中还提供了诸如参数名称和类型、参数值、行号和偏移量等信息。 ![]() 图六 调用堆栈窗口 内存窗口 开发人员可以用内存窗口来监视大的缓冲区、字符串或者其他在监视窗口中无法完全显示的数据。如果开发人员希望跳转到内存的某个地址,只需要在地址栏填入相应的地址或者表达式即可。缺省情况下,内存窗口动态监视表达式,当表达式的结果发生变化时,地址窗口也会相应发生变化。 调试程序 现在我们有了这些调试工具,调试中间代码程序就变得很容易。首先打开局部变量窗口监视局部变量。如果我们输入的参数是3和4,当单步执行到sum的值从0变为35时,可以注意到代码中不仅有加法,而其还有乘法。我们可以去掉乘法来修正这个错误。修改的代码如下所示: .method private hidebysig static void Main(string[] args) cil managed { … //ldc.i4.5 ldloc.0 ldloc.1 add //mul stloc.2 … } // end of method ErrorneousClass::Main 调试库文件 在CRL调试器中,开发人员可以逐语句执行库文件。如果需要调试库文件的话,开发人员需要一个测试装置(Test Harness)文件,引用了库中的类或方法的可执行文件,以及可执行文件和库文件的程序数据库(.pdb)文件。开发人员需要在CLR调试器中打开测试装置文件,然后在调试时就可以进入需要调试的外部库的代码行。 小结 在这篇文章中我们介绍了用与调试中间语言程序的调试工具。尽管中间语言平时不会经常使用到,但是对中间语言的熟悉可以帮助程序员深刻理解.Net程序是如何工作的,并且根据.Net程序的特点编写出快速高效的代码。同时中间语言也可以应用到反向工程中,当然这涉及到知识产权的问题。 |
解密微软中间语言:MSIL
最新推荐文章于 2024-02-16 13:08:48 发布