没有程序员敢保证没有经过调试的代码绝对没有错误,无论他/她智商多么高,开发出来的代码总是或多或少带有一些错误(当然是无意的:-))。这些错误可能是简单的语法错误或者复杂的逻辑错误。因此和其他语言一样,我们需要中间语言的调试工具/方法。由于中间语言是比较底层的语言,因此调试工具/方法对于程序员来说更加重要。
最简单的调试方法莫过于在程序中加入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调试器
图二 选择要调试的程序
1
2
下一页>>