Article Options
Premium Sponsor
Premium Sponsor

 »  Home  »  .NET Framework  »  Demystifying Microsoft Intermediate Language. Part 3 - Debugging
Demystifying Microsoft Intermediate Language. Part 3 - Debugging
by Kamran Qamar | Published  10/26/2002 | .NET Framework | Rating:
Kamran Qamar

Kamran Qamar is independent consultant with extensive experience in all facets of software development life cycle (SDLC). He specializes in web-enabled application development using Microsoft tools and technologies. He has spent last seven years architecting and developing web based telemetry and management applications around the world. Recently he delivered Bridge Management System for Ukrainian government built on .NET technologies.

Kamran also enjoys teaching and presenting on .NET technologies. He has Electronic Engineering background and has Master degree in computer sciences.

 

View all articles by Kamran Qamar...
Demystifying Microsoft Intermediate Language. Part 3 - Debugging

Who can claim that wrote a program without any errors on the first attempt? The human beings are error prone and no matter how smart or great programmers we are, we always end up writing buggy code, unintentionally of course :). That error could be as simple as syntax error or as complex as logical error. In any case we need a way to debug our program. This need increases when you are writing program in low level language like Intermediate Language, which is hard to debug.

You can debug your program either inserting number of WriteLine statements, but this method is very tedious for MSIL program. You wouldn't write one line but three lines of code like this:

ldstr      "Hello World"
call       void [mscorlib]System.Console::WriteLine(string)
ret

Imagine a practical situation where you need to insert number line of code for debugging. Of course it is tedious, but required. Fortunately, .NET SDK comes with two tools for debugging IL, more correctly any .NET assembly. In this article, we will explore the power of these tools. This insight in debugging IL code will help us in future when we will write more complex applications using MSIL.

Debugging Tools

.NET SDK has two great debugging utilities in its tool box, they are:

  1. Microsoft CLR Debugger (DbgCLR.exe)
    Provides debugging services with a graphical interface to help application developers find and fix bugs in programs that target the runtime.
  2. Runtime Debugger (Cordbg.exe)
    Provides command-line debugging services using the common language runtime Debug API. Used to find and fix bugs in programs that target the runtime.

On surface both the utilities seem to solve same problem i.e. find and fix bugs in programs targeted for .NET runtime. However, they differ a little in their functionalities. DbgCLR.exe is a window application which provides you with visual interface and ease of defining break points and watches, where as Cordbg.exe is a command line tool which let you write debugging script. In this article, I will concentrate on the windows application i.e. DbgCLR.exe.

Microsoft CLR Debugger (DbgCLR.exe)

The Microsoft CLR Debugger (DbgCLR.exe) provides debugging services with a graphical interface to help application developers find and fix bugs in programs that target the common language runtime.

In order to understand the capabilities of Microsoft CLR Debugger, we need an erroneous program. Unfortunately, we are not advance enough to write one by our self in MSIL. To make thing clear and more understandable, I will write an erroneous program in C#. Then we will get its MSIL equivalent using ildasm.exe.

Erroneous Program in C#

Open your favorite code editor and punch in the following C# program. Name the file ErroneousApp.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)
        {
            // obvious error, I am multiplying by 5 along with adding 
            // two numbers :)
            return 5 * (op1 + op2);
        }
    }
}

You can identify, with this small executable program I can potentially generate two kind of errors:

  1. Logical Error - Add function return incorrect results.
  2. Functional Error - if I don't provide two arguments as program expect, this will crash

By the way, don't take this approach as only way to debug. Microsoft CLR debugger is capable of debugging any kind of bugs. Here we need code that we can compile and then retrieve MSIL code from it using ildasm.exe. So let's go ahead and compile this code using csc.exe. Next thing we need is MSIL code, I will run ildasm.exe to extract this code, and you already know how to do it. Therefore, I would not go in that detail. Here is the MSIL code for this application.

.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

I have slashed out all the unnecessary code and comments generated by ildasm.exe.

Explanation of the MSIL code

Before we start using this code let me explain some of the above. We are using Console.WriteLine, and Int.Parse functions defined in external assembly mscorlib, so we need to create a reference for it. This is done using directive assembly with attribute external. Further we named our assembly ErrorneousApp using the assembly directive again.

Next we defined our class as ErrorneousClass using class directive. Then we created a static function named Main using directive method, we also set this method as entrypoint method in the application. And we initialized three of our local fields using local directive:

.locals init ([0] int32 operand1, [1] int32 operand2, [2] int32 sum)

Now we need to assign values to these local fields. We will to extract the values from argument array, that is done using ldelm, which stands for load an element of array. However, to instruct CLR which argument correspond to which local field we need to provide it a array element reference number that is accomplished by pushing the arguments using ldarg opcode (The ldarg num instruction pushes the num'th incoming argument, where arguments are numbered 0 onwards onto the evaluation stack.) The ldc command pushes the actual number in stack. ldc.i4.0 means pushing zero as int32 type in stack. Next we called System.Int32.Parse method to convert string values into integer. Once both the local fields are initialized we call local method Add to add operand1 to operand2. And finally we displayed the result using System.Console.WriteLine method.

Implementation of the static method Add is quite straightforward. You need to remember that field value which we are going to use at the end should be stored first because MSIL works on stack based memory model. Since our intention is to add two numbers and then multiply them to 5, therefore we will first load 5 into stack using ldc.i4.5 command. Next we will load two operands, add them using add command that will automatically pop last two values from the stack, add them and push the result back. Finally we use mul command to multiply the two operands.

Notice, in the Main function we do not have direct call to static Add function, this is because C# compiler substitutes call to a static function with inline code for that function - interesting revelation!

I don't expect you to understand all this code at this point. Remember our focus here is to understand how to debug IL code and more specifically how Microsoft CLR debugger works. In the future articles I will lead you step by step in writing IL programs.

Let me emphasize again, our aim is to understand how to debug MSIL code, since I have not showed you how to write a MSIL application yet, I have to take a detour and use ildasm.exe to create one functional but erroneous application. I will use this IL code to explore Microsoft CLR Debugger functionalities and eventually fix this application.

Debugging MSIL Code

To debug MSIL code we need debug information file - ErrorneuosApp.pdb. A PDB (program database) file holds debugging and project state information that allows incremental linking of a Debug configuration of your program. We can obtain this file by compiling our ErrorneuosApp.il source code using ilasm.exe with switch debug.

> ilasm errorneousApp.il /debug

This will create ErroneousApp.pdb file for us.

Now run DbgClr.exe and open ErroneousApp.il file using File | Open menu. You will have solution like this:

Next step is to set the program to run. This you can do using Debug | Program To Debug option from main menu. This will pop up the following window:

Now you all set to start debugging your program and you have all the power of debugger of high level language, you can set break points, put watches, peek into memory and even see register values. Let me walk you through these options one by one and see how we can use them in the current context.

Breaking Execution

The primary purpose of CLR debugger is to display information about the state of the program being debugged. There are variety of tools for inspecting and modifying the state of program. Most of these tools function only in a break mode. The debugger breaks execution of the program when execution reaches a breakpoint or when an exception occurs.

We can set break points in two ways:

  1. Clicking on right margin of any executable line of code mark that line as break point.
  2. Using Debug | New Breakpoint menu option.

Second method enables you to set conditional breakpoints. You can set break points using one of the following methods:
  • Setting the function name where execution should halt,
  • Setting Line and Character number with in a specific file, or
  • Setting a specific address

Remember, data breakpoints are not supported for IL code debugging. This approach also let us narrows down our breaking condition by specifying condition and break point hit count. When breakpoint location is reached the expression set as condition is evaluated, if the expression is either true or "has changed" execution stops. Each time execution is stopped, it is termed as a hit and debugger keep a internal count of this hit. You can set breaking condition based on this hit counter value e.g. you can set stop only when hit count is multiple of five.

In current example we can set break points only for Addresses and File.

The Visual Studio debugger provides a variety of tools for inspecting and modifying the state of our program. Most of these tools function only in break mode. Once we reached in break mode using any of the method defined above, we can use these tools. Let me explain them one by one.

Local Window

The Locals window displays variables local to the current context. You can always change the context by selecting new context from Call Stack, Thread or Debug Location window.

The great advantage of this tool is that you can change value of local variable on the fly. Simply double click on the value for field you want to change and update it with new value.

Quick Watch

As the name implies, we can use the QuickWatch dialog box to quickly evaluate a variable or expression e.g. I am checking if Operand1 has a value greater than 2 or not.

I can use this same window to change the value of the operand1 also. I can even change the value of a register. QuickWatch is a modal dialog box. You cannot leave it open to watch a variable or expression while you step through your program. If you need to do that, you can add the variable or expression to the Watch window.

Watch Window

Once watches are added we can keep debugging the code and see the results in watch window. Watch window is quite powerful in the sense that we can change values of fields.

The QuickWatch dialog box provides a quicker, simpler way of evaluating or editing a single variable or expression. Whereas, Watch window provide a consolidated view for all the watches.

Registers Window

The Registers window displays register contents. If you keep the Registers window open as you step through your program, you can see register values change as your code executes. Values that have changed recently appear in red.

Call Stack Window

Using the Call Stack window, you can view the function or procedure calls that are currently on the stack. The Call Stack window displays name of each function. The function or procedure name may be accompanied by optional information, such as module name, line number, byte offset, and parameter names, types, and values. The display of this optional information can be turned on or off using context menu

Disassembly Window

The Disassembly window shows assembly code corresponding to the instructions created by the compiler. If you are debugging managed code, these assembly instructions correspond to the native code created by the JIT compiler, not the intermediate language generated by the Visual Studio compiler.

Memory Window

You can use a Memory window to view large buffers, strings, and other data that do not display well in the Watch or Variables window. If you want to move instantly to a selected location in memory, you can do so by using drag and drop or editing the value in the Address box. The Address box accepts expressions as well as numeric values. By default, the Memory window treats an Address expression as a live expression, which is reevaluated as your program executes.

Moving on with debugging...

Now that we are equipped with the power of Microsoft CLR debugger we can fix our program easily. Let's run it in debug mode and keep a watch on local variables. As we move step by step we notice that sum value changes from zero to 15. We also notice that we have multiplication statement in addition to add statement. We can rip that statement off to fix the problem. Here is the updated code:

.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 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

Debugging a Library File

We cannot open and run step by step a library file in CLR Debugger. In order to debug a library file you will need a test harness, executable application that reference this library and make use of one of its classes and methods. You will also need to have program database (.pdb) files for both test harness executable and library file. Then and only then you can open test harness file in CLR debugger and it will automatically take you to code lines in external code library you actually need to debug.

Applying Debugging Techniques

Suppose you have just received one assembly that does some fancy work and display result in console window. For simplicity, let's assume this assembly (FancyGreetings.dll) has a property named Name which take users name and write out on console a greeting like this:

Welcome Mr. <Name>

You easy see the problem with the output. The output assumes that name will always be of a male. This is not the case in your application. You don't have access to source code and you cannot wait for the new release to arrive. You can use the power of ildasm.exe along with debugger to fix this bug.

You can create MSIL Code using ildasm.exe, update the name method and recompile it using ilasm.exe. If this FancyGreetings.dll assembly is quite big and complex you wouldn't be able to understand where you actually need to fix the bug. In this scenario DbgClr.exe become handy. This utility will let you step through the code to find the problematic area. Remember, debugger need program database (.pdb) to run and that you can generate using debug switch.

This example brings us to one very important topic i.e. reverse engineering and protection of intellectual rights. MSIL not only provides a common code base it also opens door for reverse engineering. Fortunately, there are obfuscators available on the market that hide code at some extent or at least make reverse engineering quite difficult (How? This is out of the scope of this article). Most of the products you buy come under certain license agreements which normally forbids you to do any kind of reverse engineering.

In this article you learned how to debug MSIL application. This knowledge will be useful in future when you will follow the examples provided in future articles during our journey in the MSIL world. Don't forget to post your comments and ideas about what you like to read about IL in future articles. You can always reach me at kamran@kenlogix.com


Related devCity.NET articles:

How would you rate the quality of this article?
1 2 3 4 5
Poor Excellent
Tell us why you rated this way (optional):

Article Rating
The average rating is: No-one else has rated this article yet.

Article rating:3.06451612903226 out of 5
 31 people have rated this page
Article Score30213
Sponsored Links