Debugging a
project starts with the
coding and ends only after the product is delivered; and,
irrespective of project success or failure,
debugging cannot be
avoided. However, familiarity with good debugging methods,
best practices, and use of debugging tools are prerequisite
for a successful project. In this
article, I have listed a few good practices for
debugging that I have
learned during my development career. If you are looking for
quick debugging
tips, specifically for
Windows-based projects,
refer to the General Tips section.
These
debugging tips are directly applicable to
C and
C++ projects. However,
in general, these are also applicable for any software
project.
Compiler Warnings
Make sure your
code is free from compiler warnings.
Even a single warning
has potential of consuming several hours of
debugging. If you have
access to lint, use it. It will improve the quality of the
code tremendously. If you are using
VC++, use the /W4
switch to work the compiler
for you to give extended warnings.
Study each extended warning
carefully to make sure that it is not a potential bug.
Using
Asserts
Assert is a MACRO which,
when it fails, stops the program execution (it also may
raise an exception or break in the debugger, depending on
the configuration and the programming language being used).
It is enabled only in the Debug
version of the build and does not have any effect in the
released builds.
Assert can be used as an
effective debugging aid.
Use Assert to check for
inconsistencies, preconditions, null pointer checks, and so
on. It can save you a lot of time by locating most of the
programming mistakes and
logic errors during unit testing.
Usually, there
is confusion regarding the use of
Assert versus error handling and people use both
interchangeably. The simple rule is that
Assert should be used to
check for programming
errors, logical
mistakes, and precondition checks, but not for run time
error checks. For example, if I have written a function that
takes a pointer as input and I do not expect it to be NULL,
I should use Assert to check that the pointer is not null.
The user of this function should do error handling so that
he does not pass a null pointer to this function. Consistent
use of this principle results in code that is simple and
readable, avoids bloating of code due to unnecessary error
handling, and produces a robust product. It is explained in
the following example.
char* MakeString(void)
{
char *pszStr = NULL;
pszStr = (char*)malloc(STRING_SIZE);
if (pszStr == NULL)
{
return NULL;
}
FillString(pszStr, STRING_SIZE);
return pszStr;
}
void FillString(char* pszFillStr, unsigned int uSize)
{
Assert(pszFillStr != NULL);
...
return;
}
VC++ defines
Assert as
ASSERT.
VC++ also defines
VERIFY MACRO, which is
the equivalent of ASSERT
but also works in Released builds.
Trace Logging
Trace logging is a
mechanism to log program messages to a file, console, or
both.
Debuggers are
good at locating the source of errors during the early phase
of development. But, it
is not wise to depend only on a debugger as the project
moves towards its later phases. There are several reasons
for this. For example,
sometimes it is more efficient to look at the trace messages
and find the problem. In my experience, if a program logs
proper tracing messages, in general, I find using trace
messages more efficient in locating the source of the
problem. However, putting proper tracing messages takes time
but pays off tremendously after the coding is completed. The
second reason for using tracing is that quite often, the
problem reported may not be reproduced when debugger is
used. It happens mainly when the program is multi threaded
or its execution flow depends on other asynchronous events,
such as input from network devices, and so forth. The third
reason is that it is very helpful in locating the problem
source of the client-reported problems. When your client
reports a problem to you, most often this is the only trick
that is going to help you. And believe me, if you have
proper tracing mechanism built into the product, you and
your client are going to be happy about it. Due to these
reasons, it is a de facto standard to have some kind of
trace logging functionality in the product.
However, to
make the tracing useful, it should fulfill the following
requirements:
-
It should
produce detailed diagnostic messages when needed.
-
It should
be possible to filter out non-interesting messages.
The first
requirement is based on the fact that you need detailed
information to find the source of problem. However, as the
program produces more and more trace logs, it becomes more
and more difficult to find interesting messages. It is my
experience that if a program produces indiscriminate tracing
logs, it becomes ineffective in problem solving, if proper
filtering mechanism is not present. The previous two
requirements seem to contradict each other and there should
be some mechanism to balance them. These are fulfilled by
the following two principles:
-
It should
be possible to disable and enable tracing at the module
level. It means that I can disable the tracing for other
modules I am not interested in. This also can be
achieved by logging trace messages for modules in
different files or consoles.
-
There
should be a facility to filter tracing based on some
generic categories. For example, in the early phase of
module development, I might be interested only in flow
of my module and later only in errors. I use following
filtering categories in my projects:
-
TRACE—Enabled when I want to just see the flow
of program. It prints messages at entry and exit of
a function.
-
INFO—Used to print important events in program.
-
DEBUG—Used to
enable debug-tracing statements. These are usually
printed to see the state of the program.
-
ERROR—Used to print error conditions in the program.
-
FATAL—Used to print a fatal event. This usually
results in program termination.
To best
utilize this mechanism, it should be possible to configure
trace filtering at run time or at least at the start of the
program. It is a time killer if the program needs to be
compiled to reconfigure the filtering mechanism.
MS Windows has
functionality for sending trace outputs to Debug Monitors
such as DBMON and DebugView. This is done either by using
the DebugOutputString
API or the VC++ TRACE
macro. However, beware that TRACE
is enabled only in DEBUG
builds. DebugOutputSting
and TRACE do not
implement any filtering mechanism; you need to implement a
filtering mechanism, if needed. Using this has the following
two disadvantages:
-
To capture
the trace output,
Debug Monitor should
be running.
-
It usually
slows down the performance of the application, when the
Debug Monitor is capturing the
debug trace.
It has the
advantage that you do not need to implement trace-logging
functionality and there are good freeware Debug Monitors
available for use. This mechanism is not suited for large
projects and I advise you to implement a file trace logging
mechanism for medium to large projects.
Master Your
Debugger
Chances are
that you are going to use the
debugger very often. Today's debuggers are very
powerful and can save you a lot of time if used properly.
The VC++ debugger has
numerous options, including advanced options to check memory
leaks, finding memory corruptions, and so on. Check MSDN for
details.
Use
Memory Tools
Use of tools
can improve productivity and quality tremendously. For
C/C++ programs, it is
very difficult to find memory leaks
and corruptions without
using proper tools and can take days whereas tools can
detect them instantly. There are a lot of commercial and
freely available tools that can be used. I have used MPATROL,
an open source library, satisfactorily.
Incremental
Testing
Write and test
your programs incrementally. Try to write small procedures
and test them immediately if possible. Working with small
increments reduces complexity. Also, use a unit testing
tool/framework from start. Using a unit testing
tool/framework simplifies incremental
testing. There are good open source unit test
frameworks available for both C (CuTest)
and C++ (CppUnit).
General Tips
These tips do
not fall in any particular category, but are useful in
debugging.
-
If you
need to trace an error value returned by
GetLastError() in
VC++, use the ERR
pseudo register. For this, set the trace watch for @ERR.
It avoids calling GetLastError()
to get the error value.
-
After
freeing the heap allocated memory pointer, set it to
null. Similarly, set any handles that have been
deallocated to NULL of INVALID_HANDLE, as appropriate.
-
How many
times did you face problems due to using '=' in place of
'=='? This is a typo error and can happen quite often.
Some compilers may detect this as a warning, depending
on the situation. Here is the trick to avoid it; the
compiler will find it for you.
use
instead of
-
Use the
Windows NT Tucancode.netManager to check
memory usage
of your application. For this, add a 'Memory Usage
Delta' column using the process tab on the Tucancode.netManager.
Now you can check memory usage during usage by selecting
the update speed or pausing the update speed and later
doing the refresh.
-
Use the
reserved variable names __FILE__, __LINE__, and
__MODULE__ can give information about the location of
source code in a project. These can be invaluable while
logging program trace messages.
-
Use the /GF
switch in VC++ to
put all static strings into read-only memory. It can be
used to detect memory corruption if this memory is being
overwritten. For example, suppose you have char msg[] =
"Some static string" and it is passed to a function that
uses it and corrupts this memory. These types of errors
can be detected by using this flag.
-
MACROs are
very difficult to debug and are potential source of
errors. If you suspect that there could be a problem in
MACRO, expand it to locate the problem.
VC++ supports the /P
switch and gnu compilers support the -E switch for this
purpose.
-
To detect
memory leaks in MFC applications, use the
CMemoryState class.
CMemoryState class
has a couple of members, but to track a memory leak in
an application only, the CheckPoint and
DumpAllObjectsSince methods need to be used. To use
CMemoryState, use
DEBUG_NEW instead of new for memory allocations. It's
better to define new as DEBUG_NEW for the debug version
of the build. Following is an example to dump the memory
leaks in an simple fictitious function:
VOID fictitiousFun()
{
CMemoryState ms;
ms1.Checkpoint();
ms1.DumpAllObjectsSince();
}
It should
be noted that to use
CMemoryState of a memory leak, use
DEBUG_NEW instead of
new operator; it does not detects memory leaks due to
other memory functions such as malloc, HeapAlloc, and so
on.
-
Detecting
memory leaks due to the use of
C memory functions is easy in
VC++.
VC++ implements a
debug version of all C memory functions (malloc, calloc,
realloc, and free) to facilitate
memory debugging.
To enable memory debugging support,
_DEBUG (debug build)
and _CRTDBG_MAP_ALLOC should be defined. In the
following code, I have just explained to detect
memory leaks using
an example.
#define _CRTDBG_MAP_ALLOC
#include <stdio.h>
#include <crtdbg.h>
int main()
{
malloc(100);
malloc(200);
malloc(300);
malloc(400);
malloc(500);
malloc(600);
_CrtDumpMemoryLeaks();
return 1;
}
It is a
very powerful mechanism and supports much more
functionality than shown here. I suggest you to refer to
MSDN for details.
-
TRACE
Output: Enable TRACE
output to display messages about the internal operation
of the MFC library
as well as warnings and errors if something goes wrong
in your application. TRACE output works only in DEBUG
build of the application and the afxTraceEnabled flag
need to be enabled. The easiest way to enable the
afxTraceEnabled flag is by using the Tracer utility
supplied with Visual Studio. Run Tracer from Start,
Programs, Microsoft Visual Studio, Tools, Tracer and
enable the tracing options that you need. You can also
do the same by modifying the afx.ini file, which is
usually located at c:\winnt.