Debugging with Visual Studio 2005/2008, Part 4: Setting up Code for the Debugger

by Patrick Mancier

In the fourth part of the series on debugging in Visual Studio, we will discuss how to set up your code for the debugger.

Debug break

A direct call to a debug break can be used to stop the execution of a program. Why is this different than a breakpoint and why would you use it? Generally you would use it if you are not going to launch the debug session from Visual Studio, you are going to launch it after the fact. So if we want to try this how is a debug break called in code?



Calling __debugbreak() can be used to generate a software breakpoint. This function is designed to be portable between platforms. You could also use the Win32 version DebugBreak() but it is not portable. So once you call __debugbreak() what happens from there? An exception is thrown and your program stops execution. This gives the programmer the opportunity to use Visual Studio to 'attach' to the process that is running the executable.

To attach to a running process and use the Visual Studio debugger, go to Tools and then select 'Attach to Process'. If you're running locally there is no need to change any of the dialog settings. However, if you are trying to run a debugger session remotely, you will need to change the Transport to Remote, and then the Qualifier to the remote PC you are trying to reach. Either way, once you have the process list simply find the process that you want Visual Studio to attach to and click OK. Visual Studio will then attach to this process and your debug session will begin.

A practical example of using __debugbreak() would be to debug a service. You cannot launch a service directly from Visual Studio; it must be launched with the Service Control Manager. So there is no way to directly debug it from the IDE. You must 'attach' to the process in order to debug it. The problem with debugging a service using this method is that you cannot debug the service from launch; it will simply launch and begin its run state.

Assertions

An assertion in code is a test of a condition and if the condition fails the execution of the program halts. It is designed to only be when the program is complied as debug. When an assert() is called it is accompanied by a message indicating which expression failed, the line number and module where the assertion took place. In a Windows application the assertion will come in a message box. In a console application it will go to the console screen.

In the following example, you can see that the string is tested for NULL. If the char array being passed in is NULL, without the assert in the debug build the memory doesn't exist and an exception would be thrown. Instead, the assert would fail and tell the programmer that something went wrong with this function.

void TestFunc(char *pszName)
{
        assert(pszName==NULL);
        strcpy(pszName,"Tom Smith");
}

The assert macros _ASSERT, _ASSERTE are used during debugging and are only available if the _DEBUG macro is defined. _ASSERTE is used to properly print out any Unicode characters that are in the expression passed to the macro. When either of these assert, that amounts to the report that the assertion failed, it lists the condition that failed and the source code module and line number it failed on. By default, in a Windows application the assertion will come in a message box and in a console application it will go to the console screen.

#define _DEBUG
void TestFunc(char *pszName)
{
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDOUT);

_ASSERT(pszName==NULL);
strcpy(pszName,"Tom Smith");
}

It may appear on the surface that assert() and _ASSERT, _ASSERTE do the same thing. As far as behavior of halting execution on a failed Boolean condition they do. However the macros are only called when _DEBUG macro is defined. The other difference is that you can define the desired behavior of the assert through a series of calls to _CrtSetReportMode(). This is because the macros invoke a _CrtDbgReportW report message and this mechanism is designed to allow a programmer to track progress of a debug build. You can define the macro assert to report to a debug file, to only use the console window instead of a message box, etc.

Exception Handling

Exception handling is essentially a way for the programmer to do error handling on parts of code that may fail under certain conditions without halting the program. For example, if a character array is access without having its memory defined it would normally throw a first chance exception and the operating system would halt the program. With exception handling this is caught and can be handled by the programmer.

Here is an example of exception handling in code. Assume that the *pszName character array passed into this function is NULL. You can see from this example that the program doesn't actually halt, what happens is when the strcpy call is 'tried', the exception is thrown by the operating system but is 'caught' by the catch block. So instead of halting the program, the program reports the error and returns FALSE instead of TRUE from this function. The syntax catch(.) means that ANY exception will be caught and handled.

BOOL TestFunc(char *pszName)
{
        try
        {
            strcpy(pszName,"Tom Smith");
        } 
        catch(.)
        {
            printf("ERROR: Cannot copy string into pszName!\n");
            return FALSE;
        }

        return TRUE;
}

void main()
{
char *pszMyName = NULL;

        if(TestFunc(pszMyName)==FALSE)
        {
          printf("ERROR: Forgot to instantiate array, aborting");
          exit(-1);
        }

        exit(0);
}

Catching any exception is ok for very quick generic stuff but is not all that useful in tracking what the specific issue might be. How do we narrow down the exception handling to specific items? This is easily handled by using a throw and defining the specific exception in the catch block we need. Here the exception of char * is the only thing that will be checked for an exception. When the check for NULL is made on the variable and we force the exception with the throw call, we can tell the exception handler exactly what caused the problem.

BOOL TestFunc(char *pszName)
{
    try
    {
        if(pszName == NULL)
        {
           throw "ERROR: pszName parameter was NULL!";
        }
        strcpy(pszName,"Tom Smith");
        } 
        catch(char * error)
        {
            printf("ERROR: %s, pszName == NULL, cannot copy name into array\n",error);
            return FALSE;
        }

        return TRUE;
}

void main()
{
        char *pszMyName = NULL;

        if(TestFunc(pszMyName)==FALSE)
        {
            printf("ERROR: Forgot to instantiate array, aborting");
            exit(-1);
        }

        exit(0);
}

The items that are put into the catch function are not limited to just variable types. A class can be passed that is used to hold exception data. Below is an example of this type of class exception. You can see that there are now two types of exceptions that are throw using the class, we pass the enumeration into the constructor in order for the class to track what the exception is.

enum ExceptionErrors
{
    ArrayIsNull=1,
    ArrayNotBigEnough
};

class CMyException
{
        int m_iErrorCode;

public:
    void ShowDescription()
    {
       if(m_iErrorCode & ArrayIsNull)
       {
          printf("ERROR: Array variable was NULL\n");
       }
 
       if(m_iErrorCode & ArrayNotBigEnough)
       {
          printf("ERROR: Array size to small\n");
       }
    }

    CMyException(int iError){m_iErrorCode = iError;}
};

BOOL TestFunc(char *pszName, int iNameLength)
{
        try
        {
                if(pszName == NULL)
                {
                        throw CMyException(ArrayIsNull);
                }

                if(iNameLength %lt 20) 
                {
                        throw CMyException(ArrayNotBigEnough);
                }
        }
        catch(const CMyException& ex) {
            ex.ShowDescription();
        }
}

Miscellaneous Debugging Calls

There are other calls that can be made to perform some types of debugging. The reasons to use these types vary but generally these are all called to halt execution of a running program due to some error condition.

  • abort - Halts the current program whenever this line is called
  • raise - Halts the program with a specific error, abnormal termination, floating point error, illegal instruction, CTRL+C interrupt, illegal storage access and a request to terminate.
  • signal - This is similar in concept to raise with the difference being you must define your own error handler. The general idea is you raise the normal signal and then perform post process error handling.
Prev: Part 3: Using Breakpoints Effectively
Next: Part 5: Using Trace and Log Messages

Related articles

Debugging with Visual Studio Part 1: Debugging Concepts

Debugging with Visual Studio Part 2: Setting up the Debugger

Debugging with Visual Studio Part 3: Using Breakpoints Effectively

Debugging with Visual Studio Part 5: Using Trace and Log Messages

Debugging with Visual Studio Part 6: Remote Debugging

Main debugging page

GDB tutorial

bug prevention, debugging strategies, tips, and gotchas

hunting segmentation faults and pointer errors.

Valgrind

Skip Stepping Into Functions with Visual Studio's NoStepInto Option.