[ 0x68.com Home ] [ Short Code Snippets ] [ Debugging FAQ ] [ How to Avoid Debugging ]

Basic Debugging: Visual C++

Carl Corcoran Feb. 7, 2002

This article describes basic debugging techniques using Microsoft Visual C++ debugger.  The scope of this article is limited to only the fundamentals. 

Index

  1. Why do you need to read this
  2. Overview of Debuggers
  3. Basic debugger operation / terminology
  4. Exercises
  5. Scenario 1: Introduction to the debugger
  6. Scenario 2: A simple application
  7. Scenario 3: An application that "hangs"

Why do you need to read this?

Remember taking that test in math class - and trying to use the "guess and test" method?  Well as you now know, it took much longer to solve the problem than if you had just understood how to do it the good-bear way.  You probably find yourself using the "guess and test" method often in your own applications, saying to yourself, "Maybe it doesn't work because that variable is zero - let me change it and try it out!"  Most of the time, though, this method doesn't work well.

Just as a good mathematician cannot rely on "guess and test", you cannot be a good developer without being a good debugger.  No developer can write an application with absolutely no bugs, and unfortunately most of the time the dev is left to debug his/her own code.

In fact, you most likely find yourself actually coding only 5% of the time - the other 95% of the time is spent debugging, guessing, testing, and scratching your head.  See Figure A.

 

Figure A: The Developer's Schedule

Because of the amount of time spent debugging, it's important to make it productive.  What many beginning debuggers don't realize is that debugging is a complex skill, just as developing is.  There are many articles and books on debugging techniques and strategies.  When you sharpen your debugging skills, your developing will move along quicker and you will get more of a sense of accomplishment.

After you have learned just a few basic debugging techniques, you will no longer need to rely on the "guess and test" method; you will be able to actually make a plan of attack and fix bugs in an orderly fashion.

Overview of debuggers

What is a debugger?

Other than a person who debugs, a debugger is an application that "latches" on to another application so it can have heightened privileges and access to the target application.  In addition to being a text/hex editor, compiler, and linker, Microsoft Visual C++ (msdev) is also a debugger.  When msdev runs your application, msdev acts as a debugger.  Thus, it can access all of your application's resources, and even halt / continue its execution.

Debuggers also get notices on certain events, such as when a DLL is loaded, or when an exception is not handled (crash).

The basic theory is that the debugger is between your application and the operating system, so you can catch stuff in the debugger before the OS throws a fit about it.

Basic debugger operation / terminology

Before we delve into the example application, let's start with some basic techniques and vocabulary.  These are techniques that you will need to know in order to get through this article, and, ultimately, life in general.

Breaking and Breakpoints

This is a big one.  "Breaking into the debugger" refers to the debugger halting your program.  Your program is then essentially frozen, in that no code is being executed - even the processor's registers are frozen in time, unless the debugger changes them.  There are several ways to break into the debugger:

Method 1: Your application crashes.  Any un-handled exception will cause your application to break into the debugger.  This includes accessing invalid memory (access violation), using the 'throw' keyword outside of a try/catch block, and other predefined conditions.

Method 2: Using the assembly instruction  int 3.  This throws an exception that will cause your debugger to break.  If no debugger is present, then your application will crash (Figure B), so it's important not to use this technique in "released" products.


Figure B.  This is what happens when you perform an int 3 without a debugger

Method 3: By hitting CTRL + Break.  This throws another "safer" exception that causes the debugger to break.  This seems to be available only to console mode applications, though.

Method 4: Using an explicit breakpoint.  Breakpoints are the most common way to break into the debugger.  There are many types of breakpoints, although the most common is a "break on execution".  A break on execution will break your application into the debugger when the application's execution reaches the point of the breakpoint.  This can be accomplished in msdev by pressing F9.  The breakpoint is indicated by a big red dot next to the line where your cursor is.

Method 5: Selecting Debug->Break from the msdev menu while your application is running.  This will cause the application to be forced to break immediately, if possible.  Most likely your application was in the middle of a system call at the point of the break, so the context information might look a little foreign.  This method is used mostly to determine where a "hang" is occurring.

While your application is "broken" into the debugger, you are free to examine all sorts of details about your application like memory, the stack and the processor's registers.  This brings me to my next topic.

Watches

When your application breaks into the debugger, you will most likely want to take a look at some variables.  There are many ways to view memory, but in msdev, the easiest way to view memory is to use the watch windows and the tooltips.


Figure C: Different ways to "watch" memory in msdev.

For Figure C, I set a breakpoint on the "g_nGlobal = ..." line, ran the application, and when msdev broke on that line, and took my screen shot.  Let me describe all the points of interest:

  1. This really doesn't have anything to do with watches, but the yellow arrow is the current instruction pointer.  The red circle behind it indicates a breakpoint.
  2. One way to view a variable's value is simply to hover the mouse over it.  In this example, I demonstrate how to see the value of a whole expression.  You do this by selecting the whole expression would like to view, and then hovering over the selected text.
  3. This is the "variables" window.  There are three tabs in this window, each show you "convenient" data.  The "Auto" tab shows variables that are accessed near the instruction pointer (the yellow arrow), the "Locals" tab shows you the values of all the local variables in the current function, and the "this" tab shows the values of the member variables of the 'this' pointer.
  4. This is the main "Watch" window.  By entering symbol names into these lists, you can view the values.  Note here that you can also enter expressions, like g_nGlobal + 1, and you can use C-style casts to cast values to other types.

The Stack

No, I'm not referring to what you did with empty pop cans in college.  I'm referring to a special block of memory in your application devoted to keeping track of where we are.  The stack (commonly known as the "call stack") stores arguments to functions, local variables, and return addresses.

Let's look at a small example:

int add(int n1, int n2)
{
    return n1 + n2;
}

int subtract(int n1, int n2)
{
    return add(n1, -n2);
}

int main()
{
    return subtract(13, 8);
}

As you can see, by the time we are in add(), we are three functions "deep".  If we were to look at the call stack while we are in add(), it would look something like this:

main(void)
subtract(13,  8)
add(13, -8)

So the call stack is just a first-in-last-out data structure that keeps track of a program's flow of execution.  MS Visual C++ displays the stack in the "context" drop-down combo-box when your application is being debugged (look at Figure C and you will see it just above "3.").

You use the stack to figure out where you are in execution.  If you wrote your own strcpy() function that crashes only every once in a while, you would need to use the call stack to determine the context in which it crashes, vs. the context in which is succeeds. 

Stepping through code

When your application is halted by the debugger, I mentioned that you can examine variables and memory.  What if you want to watch how a variable changes over time.  Using stepping techniques, you can trace through code line-by-line to closely examine its execution.  By "stepping", the debugger acts as if it sets a breakpoint on every line of code.

Sometimes just stepping through a function will reveal behaviors that you never expected.

This technique is a great way to find answers about a mysterious piece of code.  Stepping allows you to watch how a variable changes over time, see the "flow" of a function, or even just to confirm that a function performs its duty correctly.

Let me describe the different types of stepping in msdev (and their shortcut keys):

Exercises

Here are some things you might want to try, before moving on to the sample scenarios

Open up MS Visual C++ and create a new "Win32 Console Application" project.  Choose "A Simple Application" when the wizard prompts you.  Then modify main() to look like this:

int main(int argc, char* argv[])
{
    int a = 2;
    int b = 3;
    a = b * a;
    b = a * 2;
    a = b / 3;
    return 0;
}

Now try setting breakpoints on different lines and using the watch windows to view variables.  Also try stepping through the code using F10 or F11.

Try modifying the code to include some functions, and take a look at how the watch windows behave as you move in and out of functions.  Take a look at the stack to see how it changes also.

Scenario 1: Introduction to the debugger

I've always believed that the best way to learn something is to first examine its most basic form.  The purpose of this example is to simply get our feet wet in the debugger.  We'll explore how to debug the most basic of problems using the topics I've described above.

Open up MS Visual C++ and create a new "Win32 Console Application" project.  Choose "A Simple Application" when the wizard prompts you.

Now modify the main() function to look like this:

int main(int argc, char* argv[])
{
    char* a = 0;
    *a = 1;
    return 0;
}

Let's examine this code first.  We create a pointer called 'a' that we set to 0.  Then we try to put a value in the memory pointed to by a.  Obviously since a is zero, this memory is invalid, and the program will crash.

So compile the application by hitting F7.  The compiler will say it's finished compiling, and we are ready to examine the bug.

First, though, let's see what happens when this application is run without a debugger.  To do this, navigate to the exe and run it from Explorer or the command prompt.  Without a debugger, when your application crashes all you see is something similar to one of the windows shown in Figure D.  It explains that the code at address 0x0040af32 tried to write to memory at 0x00000000.  That warning doesn't provide much help, however.  In even a small project, this information would be nearly useless.




Figure D: Various access violation windows

Let's try running this application in the debugger.  To do this just press F5 in msdev.  When the crash occurs, you are presented with much more meaningful context information.  Figure E shows you what msdev will present you with upon the crash.  Immediately you are presented with the following important details:

  1. The line of code that cause the crash (indicated by a yellow arrow)
  2. The context in which the crash happened (the stack)
  3. Values of variables that are most likely related to the crash (displayed in watch windows)


Figure E: This is what msdev looks like when your application crashes.

Also note that at this point that the application has automatically broken into the debugger, ready for us to start searching for clues.

We should start our investigation by looking at the line that caused the fault.  In this case, it was the following line:

*a = 1;

By looking at this line you can see that the fault is probably caused by a bad pointer.  To continue on this strategy, let's examine the value of the pointer.  In the "Auto" tab on the variable window, you can see that the value of 'a' is 0x00000000.  Our theory is correct, so the next order of business would be to figure out why that pointer is invalid.  For this example, though, we are finished debugging.

Scenario 2: A simple application

Click here to download the required project

After unzipping the required project, open degrees.dsw in MS Visual C++, then compile and run the application.

When you've tested the application a few times, you'll see that what was supposed to be a Fahrenheit to Celsius converter turned out to be a Fahrenheit to ZERO converter.  There is obviously something wrong in the code, so let's do some debugging.

Let's try to figure out where to start.  In a larger project this is a fairly large decision.  In this case, we know that somewhere along the way our numbers are getting miscalculated.  Because this example is small enough, let's start at the beginning - where the user inputs the data.

If you look at main.cpp, you will see that the user inputs data with the _tscanf() function.  The number they input is stored in a variable called 'd'.  To see how this number changes through the code, let's set a breakpoint on the next line by moving the text cursor to line 27, and pressing F9.  You should see the red dot pop up, like this:

Hit F5 to run the program again.  This time enter the boiling point, "212", and hit enter.  Your application then will break into the debugger.

If you look at the "Auto" tab on the variable window, you see that 'd' is equal to 212.0000.  That's good news so far, so let's continue by hitting F10 to "Step Over" to the next line.  This will perform just the Subtract() function.

Now we can see that the subtract function works correctly, because 'd' now equals 180.0000.  We haven't hit our problematic area yet, so let's continue by performing the Multiply() function.  Do this by hitting F10 again.  This should multiply 180 by 5/9ths, for a desired result of 100.

Uh oh... Now 'd' equals 0.0000, so we have found our problem area.  Double checking the code, it looks OK, though, so let's investigate further into what Multiply() is doing to the number.

Because we're past the point of failure, let's start over again.  You can stop your program now buy going to the menu: Debug -> Stop Debugging, or by hitting Shift + F5.  Then start the program back up by hitting F5.  Enter "212" again, hit enter, and you should break into the debugger just like before.

Now skip over the Subtract() line by pressing F10.

Now we're at the crime scene, with Multiply() queued up.  This time, let's step into this function by pressing F11.  Now we can examine the two arguments, 'n1' and 'n2'.  This also shows us a problem - n2 should be 5/9, or .555555556.

Hmm.  So the bug is that 5/9 is being passed into Multiply() as 0.  Well with a little research you would find that it needs to be 5.0/9.0 in order to be treated like a floating-point number.

If you modify the code to reflect this, and take away the breakpoint, you will see that the program now works as expected.

Scenario 3: An application that "hangs"

(coming soon, after enough people nag me to write this part)