A DBWin32 Debugger for Windows



October 01, 1996
URL:http://drdobbs.com/a-dbwin32-debugger-for-windows/184403245

October 1996/A DBWin32 Debugger

If you mourn the passing of DBWin with the advent of 32-bit Windows environments, here's some cause for rejoicing.


Introduction

Most computer users, unless they are command-line junkies, will probably say that Windows programs are easier to use than character-based programs. But Windows applications are generally more difficult to program. This is especially true in the area of debugging. Although interactive debuggers are available for most compilers, they are sometimes awkward or inconvenient to use. Also, certain types of programs, such as device drivers, TSRs, thread routines, and NT services, do not work well with interactive debuggers.

printf, or one of its close cousins, makes a great tool for simple debugging jobs under MS-DOS. But printf's equivalents are not so great under Windows. Since the screen is now a hierarchical series of display windows, which one should display the debug trace statements? It's possible to create application-specific solutions, such as special trace windows that pop up when the code is compiled and run with debug information. What we really need, however, is a system-wide solution that all applications can use.

Working with Microsoft's Solutions

The Windows API addressed this problem, starting with v3.0, by providing the OutputDebugString API, which allowed you to send a text string to an external monochrome monitor or have it displayed in a window while running an interactive debugger. Although this solution worked, it was not pretty. It was usually inconvenient to acquire and set up the monitor or run the program under the debugger when trace statements were needed.

Visual C++ 1.5 addressed this problem with DBWin. DBWin captures the text from any program calling OutputDebugString and displays it in a graphical window, or redirects it to a COM port or monochrome monitor. DBWin also allows the user to save the text to a disk file as well copy and paste to any other application. In short, DBWin addressed all of the previous shortcomings and gave Windows programmers a useful debugging tool.

With the release of Windows NT and Visual C++ 2.0 however, things reverted to their previous state. DBWin did not work with any Win32 programs, although it remained useful for 16-bit programs running on a Win32 platform. The loss of DBWin frustrated me, until I found the DBMON example on the Win32 SDK. This code documents how OutputDebugString makes its text string available to programs using Win32 interprocess communication (IPC) mechanisms, and implements a simple command-line display of this text. Although DBMON worked fine, I was unsatisfied with its character mode interface and found myself wishing for the features and usefulness of DBWin. Thus, I decided to merge the Win32 IPC of DBMON and the interface of DBWin -- the result was DBWin32.

In researching the best way to implement DBWin32, I discovered that the full source to DBWin is available on the Visual C++ 1.5 CD-ROM. This made the implementation of DBWin32 much easier, since I didn't have to start from scratch. With only a couple of hours of work, I had the best of both worlds -- all the features of DBWin running under Windows NT. With the release of Windows 95 in August of 1995, I assumed that everything would work similarly. Unfortunately, it did not, and it took some creative use of the C preprocessor to get everything to work smoothly.

NT vs. Win95

When the Windows NT version of OutputDebugString is called, it creates a Win32 memory-mapped file to make the text available to other programs, and two auto reset events to notify other programs that the text is available. Memory-mapped files and events are just two of the many IPC features that the Win32 API provides. (See chapters 7 and 9 of [1]

for an in-depth discussion of these.) DBWin32 utilizes these points of communication to successfully display the OutputDebugString text. Unfortunately, for some reason, the Windows 95 development team decided not to provide this communication from their version of OutputDebugString. Thus I was faced with creating these connections myself to allow DBWin32 to work identically under NT and Windows 95.

Implementation

I experimented with several different ways of injecting the IPC code into Windows 95 programs with minimal changes required to the original source. Using the preprocessor seemed to be the most straightforward and efficient method. To use the preprocessor, I supply two source files with DBWin32: W95TRACE.CPP and W95TRACE.H. W95TRACE.H, shown in Listing 1, uses a #define to substitute OutputDebugStringW95 for the standard call to OutputDebugString as well as for the Microsoft Foundation Classes (MFC) standard TRACE macros for compatibility with existing MFC code. OutputDebugStringW95 mimics the NT version of OutputDebugString by initiating the proper IPC links.

As shown in Listing 2, W95TRACE.CPP implements the function OutputDebugStringW95. After declaring the necessary local variables, OutputDebugStringW95 builds the text string from the variable parameter list. Using a variable parameter list instead of just using an LPCSTR, as OutputDebugString does, is necessary to remain compatible with the MFC TRACE macros. It also frees users from having to build the buffer themselves. The drawback, though, is that it becomes possible to overrun the buffer with a string longer than the size of the local achBuffer variable.

The next few lines of code call the OutputDebugString API to ensure that the text is shown in the output window of the debugger if one is being used. Had I skipped this step, the text would only show up in DBWin32 and never in the output window of a debugger, which is not what the user would expect. I use the ifdef to avoid infinite recursion caused by W95TRACE.H substituting OutputDebugStringW95 for the call to OutputDebugString.

Following the call to OutputDebugString, the code checks the operating system version number. If it is running under NT v3.x, then the call to OutputDebugString has already posted the necessary IPC information, so it simply returns. Neither Windows 95 nor NT v4.0 Beta 2 fire the appropriate IPC events, so code running under these systems must generate them. Next, the function obtains handles to the necessary IPC objects, DBWIN_BUFFER_READY, DBWIN_DATA_READY, and DBWIN_BUFFER. DBWIN_BUFFER_READY is an auto reset event that serializes the displaying of the debug text to one thread at a time. Similarly, DBWIN_DATA_READY synchronizes the end of writing to the memory-mapped file, DBWIN_BUFFER, and tells DBWin32 that there is new text to be read.

If DBWin32 is not running or one of the attempts to obtain a handle returns an error, the thread simply returns. Assuming all handles were obtained without error, the function waits for DBWin32 buffer access to become available, by calling WaitForSingleObject on DBWIN_BUFFER_READY (represented by the handle heventDBWIN). Once it has access to DBWin32, the function writes the process ID and the text buffer to the memory-mapped file, signals that new data is available with DBWIN_DATA_READY, closes the IPC object handles and returns.

DBWin32 uses a worker thread to wait for the debug event to fire and handle the event appropriately. The worker thread routine is shown in Listing 3. The first few lines declare some local variables, then the thread creates the two synchronization events and the memory-mapped file. The startup code that executes before this worker thread is launched checks for a currently running instance of DBWin32, using the FindWindow API. If an existing instance is found, DBWin32 brings that instance to the foreground and then exits. This ensures that only one copy of DBWin32 is running at any time. For this reason, the worker thread just returns if the DBWIN_BUFFER_READY event already exists. After the thread has retrieved a pointer to the memory-mapped file with MapViewOfFile, it sets up pointers for writing the process ID and the text, and sends a DBWIN_BUFFER_READY signal to the system, via a call to SetEvent(AckEvent).

The thread then goes into an infinite loop, waiting for the DBWIN_DATA_READY event to become signaled. When the call to WaitForSingleObject returns, there is new debug text in the memory-mapped file. To properly display the text in the output window, the thread keeps track of the process ID of the last debug text and whether that text ended in a newline. If the previous line was from a different process or if the previous line was from the same process and ended with a newline, the thread prepends the process ID to the output buffer. The fOutput and fDisplayPID variables are global values, configurable through menu items, that allow the user to completely disable the screen output or just disable the process ID output, respectively.

Once it has determined the newline and process ID values, the thread uses the FixNewLines routine to ensure that the text will display properly in the edit box on the main screen. FixNewLines just makes sure that all \n characters in the string are preceded by a \r character, as required by the Windows edit box, adding characters as necessary. FixNewLines returns a pointer to a newly allocated buffer, which the caller must free.

The worker thread then sends the debug text to the main window thread by posting the user defined message WM_DEBUGTEXT to the main window, and signals DBWIN_BUFFER_READY that it is ready for another debug string. When the main window thread receives this message, it appends the passed text to the edit box, and then frees the pointer to the text string. The worker thread terminates with a call to CloseHandle when DBWin32 is closed.

Limitations and Possible Improvements

Though the user interface of DBWin32 is identical to that of DBWin, DBWin32 cannot redirect the text output to a COM port or a monochrome monitor. These features could be added. Also, the Win95 support could be improved to handle the debug macros of other application frameworks, such as Borland's OWL, as well as the C++ ostream operators for the afxTrace object in MFC.

Reference

[1] Jeffrey Richter. Advanced Windows: The Developer's Guide to the Win32 API for Windows NT 3.5 and Windows 95 (Microsoft Press, 1995).

Andrew Tucker is a software engineer in Seattle, currently developing a document management system with Visual C++ for Windows NT/95. He has a BS degree in computer science from Seattle Pacific University and three years programming experience. His programming interests include databases, compilers, and operating systems.

October 1996/A DBWin32 Debugger/Listing 1

Listing 1: Declarations for Win95 tracing facility


#ifdef _DEBUG

#ifndef __TRACEW95__
#define __TRACEW95__

// redefine all the MFC macros to point to us

#undef  TRACE
#define TRACE   OutputDebugStringW95

#undef  TRACE0
#define TRACE0   OutputDebugStringW95

#undef  TRACE1
#define TRACE1   OutputDebugStringW95

#undef  TRACE2
#define TRACE2   OutputDebugStringW95

#undef  TRACE3
#define TRACE3   OutputDebugStringW95

// redefine OutputDebugString so that it works with
// API calls
#undef OutputDebugString
#define OutputDebugString   OutputDebugStringW95

// function declarations
void OutputDebugStringW95( LPCTSTR lpOutputString, ... );

#endif  //__TRACEW95__

#endif  // _DEBUG

/* End of File */

October 1996/A DBWin32 Debugger/Listing 2

Listing 2: Implementation of Win95 tracing facility to mimic that of NT


#include "windows.h"
#include <stdio.h>
#include <stdarg.h>
#include <process.h>
#include "w95trace.h"

#ifdef _DEBUG

void OutputDebugStringW95( LPCTSTR lpOutputString, ...)
{

    HANDLE heventDBWIN;  /* DBWIN32 synchronization object */
    HANDLE heventData;   /* data passing synch object */
    HANDLE hSharedFile;  /* memory mapped file shared data */
    LPSTR lpszSharedMem;
    char achBuffer[500];

    /* create the output buffer */
    va_list args;
    va_start(args, lpOutputString);
    vsprintf(achBuffer, lpOutputString, args);
    va_end(args);
    /*

        Do a regular OutputDebugString so that the output is
        still seen in the debugger window if it exists.

        This ifdef is necessary to avoid infinite recursion
        from the inclusion of W95TRACE.H
    */
#ifdef _UNICODE
    ::OutputDebugStringW(achBuffer);
#else
    ::OutputDebugStringA(achBuffer);
#endif
    /* bail if it's not Win95 */
    {
        OSVERSIONINFO VerInfo;
        VerInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
        GetVersionEx(&VerInfo);
        if ( VerInfo.dwPlatformId != VER_PLATFORM_WIN32_WINDOWS )
            return;
    }

    /* make sure DBWIN is open and waiting */
    heventDBWIN = OpenEvent(EVENT_MODIFY_STATE,
                            FALSE, "DBWIN_BUFFER_READY");
    if ( !heventDBWIN )
    {
        return;
    }

    /* get a handle to the data synch object */
    heventData = OpenEvent(EVENT_MODIFY_STATE,
                           FALSE, "DBWIN_DATA_READY");
    if ( !heventData )
    {
        // MessageBox(NULL, "DBWIN_DATA_READY nonexistent",
                      NULL, MB_OK);
        CloseHandle(heventDBWIN);
        return;
    }

    hSharedFile = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE,
                                    0, 4096, "DBWIN_BUFFER");
    if (!hSharedFile)
    {
        CloseHandle(heventDBWIN);
        CloseHandle(heventData);
        return;
    }

    lpszSharedMem = (LPSTR)MapViewOfFile(hSharedFile, FILE_MAP_WRITE,
                                         0, 0, 512);
    if (!lpszSharedMem)
    {
        CloseHandle(hSharedFile);
        CloseHandle(heventDBWIN);
        CloseHandle(heventData);
        return;
    }
    /* wait for buffer event */
    WaitForSingleObject(heventDBWIN, INFINITE);

    /* write it to the shared memory */
    *((LPDWORD)lpszSharedMem) = _getpid();
    wsprintf(lpszSharedMem + sizeof(DWORD), "%s", achBuffer);

    /* signal data ready event */
    SetEvent(heventData);
    
    /* clean up handles */
    CloseHandle(hSharedFile);
    CloseHandle(heventData);
    CloseHandle(heventDBWIN);

    return;
}

#endif

/* End of File */

October 1996/A DBWin32 Debugger/Listing 3

Listing 3: Worker thread routine


/*
   The main gist of this routine comes from the MSDN DBMON
   example.
*/
void EventThreadRoutine(void *pvParam)
{
    HANDLE AckEvent;
    HANDLE ReadyEvent;
    HANDLE SharedFile;
    LPVOID SharedMem;
    LPSTR  String;
    LPSTR  lpszDebugText;
    DWORD  ret;
    DWORD  LastPid;
    LPDWORD pThisPid;
    BOOL   fDidCR = TRUE;

    AckEvent = CreateEvent(NULL, FALSE, FALSE,
                           "DBWIN_BUFFER_READY");
    if (!AckEvent)
    {
        MessageBox(NULL, 
            "DBWIN32: Unable to create obj DBWIN_BUFFER_READY", 
            "Error", MB_OK);
        return;
    }

    if (GetLastError() == ERROR_ALREADY_EXISTS)
    {
        MessageBox(NULL, 
                   "DBWIN32: already running",
                   "Error", MB_OK);
        return;
    }

    ReadyEvent = CreateEvent(NULL, FALSE, FALSE,
                             "DBWIN_DATA_READY");
    if (!ReadyEvent)
    {
        MessageBox(NULL, 
            "DBWIN32: Unable to create object DBWIN_DATA_READY",
            "Error", MB_OK);
        return;
    }

    SharedFile = CreateFileMapping((HANDLE)-1, NULL, 
        PAGE_READWRITE, 0, 4096, "DBWIN_BUFFER");
    if (!SharedFile)
    {
        MessageBox(NULL, 
            "DBWIN32: Unable to create file object DBWIN_BUFFER",
            "Error", MB_OK);
        return;
    }

    SharedMem = MapViewOfFile(SharedFile, FILE_MAP_READ,
                              0, 0, 512);
    if (!SharedMem)
    {
        MessageBox(NULL, "DBWIN32: Unable to map shared memory",
            "Error", MB_OK);
        return;
    }

    String = (LPSTR)SharedMem + sizeof(DWORD);
    pThisPid = SharedMem;
    LastPid = 0xffffffff;
    SetEvent(AckEvent);

    for (;;) {

        ret = WaitForSingleObject(ReadyEvent, INFINITE);


        if (ret != WAIT_OBJECT_0)
        {
            MessageBox(NULL, "DBWIN32: wait failed",
                       "Error", MB_OK);
            return;
        }
        else
        {
            // only output if it is requested
            if ( fOutput )
            {
                LPSTR lpszScratch;
                BOOL fWantNewLine = FALSE;
                if (LastPid != *pThisPid)
                {
                    LastPid = *pThisPid;
                    if ( !fDidCR )
                    {
                        fWantNewLine = TRUE;
                        fDidCR = TRUE;
                    }
                }

                // fix up the newlines
                lpszScratch = FixNewLines(String);

                // allocate display buffer
                lpszDebugText = malloc(strlen(lpszScratch) + 20);
                if ( !lpszDebugText )
                {
                    MessageBox(NULL, 
                       "DBWIN32: malloc failed", "Error", MB_OK);
                    continue;
                }

                // make it empty or a newline
                if ( fWantNewLine )
                    strcpy(lpszDebugText, "\r\n");
                else
                    *lpszDebugText = 0;

                // prepend the PID necessary
                if ( fDisplayPID && fDidCR )
                {
                    // avoid overwriting the \r\n if it exists
                    int iLen = strlen(lpszDebugText);
                    wsprintf(&lpszDebugText[iLen], "%3lu: ",
                             LastPid);
                }

                // build display buffer
                strcat(lpszDebugText, lpszScratch);

                // record newline status
                fDidCR =
                    (*lpszScratch &&
                     (lpszScratch[strlen(lpszScratch)-1]=='\n'));

                // cleanup from FixNewLines
                free(lpszScratch);

                // send the display string to the window
                PostMessage(hwndDBWin, WM_DEBUGTEXT,
                            (WPARAM)lpszDebugText, 0);
            }

            // ready for new event
            SetEvent(AckEvent);
        }
    }

    return;
}
/* End of File */

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.