Debugging Tips

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);

        // Do error handling on pszStr, not Assert
        if (pszStr == NULL)
        {
            return NULL;
        }

        FillString(pszStr, STRING_SIZE);
        return pszStr;
    }

    void FillString(char* pszFillStr, unsigned int uSize)
    {
        // Do Assert on pszFillStr, not error handling. Passing
        // pszFillStr is a programming mistake, not an error.
        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:

  1. It should produce detailed diagnostic messages when needed.
  2. 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.

  1. 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.
  2. 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.
  3. 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.
  4. use

        if (10 == iValue) {...}

    instead of

        if (iValue == 10) {...}
  5. Use the Windows NT TaskManager to check memory usage of your application. For this, add a ‘Memory Usage Delta’ column using the process tab on the TaskManager. Now you can check memory usage during usage by selecting the update speed or pausing the update speed and later doing the refresh.
  6. 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.
  7. 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.
  8. 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.
  9. 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:
  10. VOID fictitiousFun()
    {
        CMemoryState ms;
    
        ms1.Checkpoint();
        //
        // do operations here
        //
    
        // Dump Memory that is allocated but not freed after the
        // CheckPoint call
        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.

  11. 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();  // dump memory leaks
      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.

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

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read