Welcome to Atalasoft Community Sign in | Help

Debugging Fu, Part 2

This is a continuation on Debugging Fu, Part 1, where I talk about techniques for tracking down the cause of a bug.

I've been doing a lot of reflection on how I actually track down particular bugs and in most cases, it comes down to Divide and Conquer. I first ran into this as a formal approach with a very experienced developer at Adobe.  He was a proponent of reading code outside the debugger first.  He was extremely fast and the reason why was that he was extremely good at D&C.  For the most part, his approach centered around if-else statements.  He would read the condition and if he believed that the condition was correctly written, he would only ever look in branch taken.  He totally dismissed the previous branch and moved on.  He used this technique because at the time, a source level debugger was a luxury and the compile-link-execute cycle was much slower.

Divide and conquer can be enhanced by laying down traps.  Traps can be done either in your source code or using the debugger.

To lay a trap in your source code,  you can use the Debug class in System.Diagnostics.  For these purposes Debug.Assert() is probably the most useful.  Let's say that you have a condition that indicates that a bug has happened and you have a chain of method calls like this:

methodOne();
methodTwo();
methodThree();
methodFour();

One way to do debug this is to set Debug.Assert traps before and after each method call:

Debug.Assert(IsBugCondition());
methodOne();
Debug.Assert(IsBugCondition());
methodTwo();
Debug.Assert(IsBugCondition());
methodThree();
Debug.Assert(IsBugCondition());
methodFour();
Debug.Assert(IsBugCondition());

The line before the failing Assert is responsible.  While I don’t fully like this, as it dirties up the code, it’s good as an instrument to inject into your code.  Some people like the style where preconditions and postconditions are asserted always.  I personally don’t care for it outside of debugging.  The good news is that the Debug class is a special case to the compiler and any expression that involves its methods is removed in a Release build (in C#, at least), so it is almost safe to leave them in.  I say almost because of the possibility of bugs related to race conditions.  Anything that changes timing significantly will change how race conditions happen (or not!).

Another approach is to use conditional break points.  In Visual Studio, set a break point as you normally would then right click on the breakpoint indicator and select “Condition…” that gives you this dialog:

image which lets you type in a C# expression to evaluate at this point.  The down side to doing this is that it slows down execution immensely.

In either case, you can narrow the cause down to a single method call.  Start reading that method and repeat the D&C there.

In earlier times, I did this with code to output text – don’t give up on this technique.  It’s useful too.  I’ve done a fair amount of work with other people’s APIs and I’ve found bugs in their code, usually in edge conditions.  As an API published, the best support cases are the ones where our clients hand us a concise app or code snippet the decisively demonstrates a bug.  I can’t always do that, so I find it helpful to wrap an existing API with one that has switches to turn on code generation.  By that I mean that I might write the following:

APISTATUS APIWrapper::APIInit(LPSTR pszBasePath, LPSTR pszTempPath, LPSTR pszCustomerID)
{
	APISTATUS ret = ::APIInit(pszBasePath, pszTempPath, pszCustomerID);
#if _DEBUG
	DebugArgs("APIInit", "dsss", pszBasePath, pszTempPath, pszCustomerID, ret);
#endif
	return ret;
}

DebugArgs() is a routine that given a name and a spec of argument types, will output a chunk of C++ code that will nearly always compile correctly.  This means that I’m taking all my calls into an API and logging them with their input values.  The output of DebugArgs is a log file like looks like this:

APIInit("somepath", "somepath", "customer id"); /* returnval */

The format string is a single character that represents the printf character for that type.  The first character is the return type and is also allowed to be v (void).  I’m not fully happy with the implementation, but the end result gives me an source text that is a short bit of editing from a concise example I can send to the owner of an API that is completely separate from my code base.

These simple techniques get you to the cause of most bugs.  Next time, I’ll talk about the defensive coding and paths of last resort.

Published Tuesday, December 16, 2008 2:07 PM by Steve Hawley

Comments

No Comments
Anonymous comments are disabled