Custom Debugger Auto-Expansion Tips

There is a fair amount of existing information (plus the official “EEAddIn” sample) on the subject of adding custom tool tips when hovering over various data types in the debugger. But lately I’ve run across a bunch of people who have never heard of it, so I figured I would post some details and a few of the extensions for standard Win32 types which I have in my collection.

 

For those new to the topic, there is some extensibility built into the VC debugger which allows you to supply custom text to the tool-tips which popup when you hover over a variable. For example, when you hover over a structure, the debugger will pop up a balloon which looks like:

WIN32_FIND_DATA before

 

Using a custom handler, we can query the object and provide our own output to make it look like:

WIN32_FIND_DATA after

 

The main idea is to turn something which might not contain very useful or actionable information into something with more context and meaning; thus making it easier to debug your application.

 

The above mentioned sample shows how to do this for the SYSTEMTIME and FILETIME structures, so here I will provide some functions which can be used to display custom text for the WIN32_FIND_DATAA, PROCESS_INFORMATION, CRITICAL_SECTION(1), and OVERLAPPED structs.

 

Struct

CRITICAL_SECTION

Before

CRITICAL_SECTION before

After

OVERLAPPED

Before

After

PROCESS_INFORMATION

Before

After

 

To start with, we will get the helper functions out of the way. Since I’m a nice guy, I support the “nBase” parameter which allows the debugger’s user to get back information in either base 10 or base 16 depending on their preference. The below function does the number to text formatting depending on the base. The return value is the number of bytes written to the string, which allows us too easily (and performantly(2)) concatenate strings. I have a handful of sprint_type functions, but I will just supply one of them as a sample, the others (which you will see used below) should be fairly self explanatory:

size_t sprint_int(char *buffer, size_t maxLen, int val, unsigned base)

    {

    if(base == 8)

        return sprintf_s(buffer, maxLen, "%o", val);

    else if(base == 10)

        return sprintf_s(buffer, maxLen, "%d", val);

    else if(base == 16)

        return sprintf_s(buffer, maxLen, "0x%08x", val);

    else

        return sprintf_s(buffer, maxLen, "%d", val);

    }

 

WIN32_FIND_DATAA

HRESULT __stdcall OS_WIN32_FIND_DATAA(DWORD dwAddress, DEBUGHELPER *pHelper, int nBase,

  BOOL fReserved1, char *pResult, size_t max, DWORD dwReserved2)

    {

    WIN32_FIND_DATAA findData = {0};

    DWORD bytesRead = 0;

    HRESULT hr = S_OK;

 

    hr = pHelper->ReadDebuggeeMemoryEx(pHelper, pHelper->GetRealAddress(pHelper), sizeof(findData), &findData, &bytesRead);

    if(FAILED(hr))

        {

        return hr;

        }

 

    if(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)

        {

        sprintf_s(pResult, max, "\"%s\" (DIR)", findData.cFileName);

        }

    else

        {

        // package up the file size into a single 64bit value

        LARGE_INTEGER size;

        size.HighPart = findData.nFileSizeHigh;

        size.LowPart = findData.nFileSizeLow;

        double dblSize = (double)size.QuadPart;

 

        // write out the filename + the file size using appropriate units

        if(size.QuadPart > 1073741824)

            sprintf_s(pResult, max, "\"%s\" (%.2lf GB)", findData.cFileName, dblSize / 1073741824.0);

        else if(size.QuadPart > 1048576)

            sprintf_s(pResult, max, "\"%s\" (%.2lf MB)", findData.cFileName, dblSize / 1048576.0);

        else if(size.QuadPart > 1024)

            sprintf_s(pResult, max, "\"%s\" (%.2lf KB)", findData.cFileName, dblSize / 1024.0);

        else

            sprintf_s(pResult, max, "\"%s\" (%I64u B)", findData.cFileName, size.QuadPart);

        }

   

    return S_OK;

    }

 

Feel free (if needed) to expand on this to add in the file’s attributes and a timestamp.

CRITICAL_SECTION

HRESULT __stdcall OS_CRITICAL_SECTION(DWORD dwAddress, DEBUGHELPER *pHelper, int nBase,

  BOOL fReserved1, char *pResult, size_t max, DWORD dwReserved2)

    {

    CRITICAL_SECTION cs = {0};

    RTL_CRITICAL_SECTION_DEBUG csDebug = {0};

    DWORD bytesRead = 0;

    HRESULT hr = S_OK;

    unsigned slen = 0;

 

    hr = pHelper->ReadDebuggeeMemoryEx(pHelper, pHelper->GetRealAddress(pHelper), sizeof(cs), &cs, &bytesRead);

    if(FAILED(hr))

        {

        return hr;

        }

 

    // write out the primary count values

    slen += sprintf_s(pResult+slen, max-slen, "lock=");

    slen += sprint_int(pResult+slen, max-slen, cs.LockCount, nBase);

    slen += sprintf_s(pResult+slen, max-slen, " recursion=");

    slen += sprint_int(pResult+slen, max-slen, cs.RecursionCount, nBase);

    slen += sprintf_s(pResult+slen, max-slen, " spin=");

    slen += sprint_unsigned(pResult+slen, max-slen, cs.SpinCount, nBase);

 

    // if the debugging information looks valid, grab some information from

    // the debug struct

    if(cs.DebugInfo && !(cs.SpinCount&RTL_CRITICAL_SECTION_FLAG_NO_DEBUG_INFO))

        {

        hr = pHelper->ReadDebuggeeMemory(pHelper, (DWORD)cs.DebugInfo, sizeof(csDebug), &csDebug, &bytesRead);

        if(SUCCEEDED(hr) && (bytesRead == sizeof(csDebug)))

            {

            slen += sprintf_s(pResult+slen, max-slen, " [entry=");

            slen += sprint_unsigned(pResult+slen, max-slen, csDebug.EntryCount, nBase);

            slen += sprintf_s(pResult+slen, max-slen, " contention=");

            slen += sprint_unsigned(pResult+slen, max-slen, csDebug.ContentionCount, nBase);

            slen += sprintf_s(pResult+slen, max-slen, "]");

  }

        }

 

    return S_OK;

    }

 

OVERLAPPED

HRESULT __stdcall OS_OVERLAPPED(DWORD dwAddress, DEBUGHELPER *pHelper, int nBase,

  BOOL fReserved1, char *pResult, size_t max, DWORD dwReserved2)

    {

    OVERLAPPED over = {0};

    DWORD bytesRead = 0;

    HRESULT hr = S_OK;

    unsigned slen = 0;

 

    hr = pHelper->ReadDebuggeeMemoryEx(pHelper, pHelper->GetRealAddress(pHelper), sizeof(over), &over, &bytesRead);

    if(FAILED(hr))

        {

        return hr;

        }

 

    // package up the offset values into a single 64bit value

    LARGE_INTEGER offset = {0};

    offset.LowPart = over.Offset;

    offset.HighPart = over.OffsetHigh;

 

    slen += sprintf_s(pResult+slen, max-slen, "offset=");

    slen += sprint_u64(pResult+slen, max-slen, offset.QuadPart, nBase);

    slen += sprintf_s(pResult+slen, max-slen, " event=0x%08x", over.hEvent);

 

    return S_OK;

    }

 

PROCESS_INFORMATION

HRESULT __stdcall OS_PROCESS_INFORMATION(DWORD dwAddress, DEBUGHELPER *pHelper, int nBase,

  BOOL fReserved1, char *pResult, size_t max, DWORD dwReserved2)

    {

    PROCESS_INFORMATION procInfo = {0};

    DWORD bytesRead = 0;

    HRESULT hr = S_OK;

    unsigned slen = 0;

 

    hr = pHelper->ReadDebuggeeMemoryEx(pHelper, pHelper->GetRealAddress(pHelper), sizeof(procInfo), &procInfo, &bytesRead);

    if(FAILED(hr))

        {

        return hr;

        }

 

    // first see if the process still exists

    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procInfo.dwProcessId);

    if(hProc == NULL)

        {

        sprintf_s(pResult, max, "handle=%p PID=%u (exited)", procInfo.hProcess, procInfo.dwProcessId);

        return S_OK;

        }

 

    // attempt to query the filename of the process

    char exeName[MAX_PATH+1] = {0};

    if(GetProcessImageFileName(hProc, exeName, MAX_PATH))

        {

        char *filePart = strrchr(exeName, '\\');

        if(filePart) // strip off the leading device\path information

            slen += sprintf_s(pResult+slen, max-slen, "\"%s\"", filePart+1);

        else

  slen += sprintf_s(pResult+slen, max-slen, "\"%s\"", exeName);

        }

    else

        {

        // couldn't get the filename, so display the handle value instead

        slen += sprintf_s(pResult+slen, max-slen, "handle=%p", procInfo.hProcess);

        }

 

    // if the process has exited, but there are still outstanding handles to

    // it, then we can still get exit code status

    DWORD exitCode = 0;

    GetExitCodeProcess(hProc, &exitCode);

 

    // write out the PID and the status

    if(exitCode == STILL_ACTIVE)

        slen += sprintf_s(pResult+slen, max-slen, " PID=%u (running)", procInfo.dwProcessId);

    else

        slen += sprintf_s(pResult+slen, max-slen, " PID=%u (exited: %lu)", procInfo.dwProcessId, exitCode);

 

  CloseHandle(hProc);

    return S_OK;

    }

 

Setup

And as shown in the EEAddIn sample, you will want to use a .DEF file to specify the function exports; this allows you to use non-mangled names in the autoexp.dat file.

 

debugTypes.def

EXPORTS

    OS_WIN32_FIND_DATAA

    OS_PROCESS_INFORMATION

    OS_CRITICAL_SECTION

    OS_OVERLAPPED

 

Now just copy the built DLL into a directory where the IDE can find it (3) and add the following entries to the [AutoExpand] section of the autoexp.dat file, which should be located in the visual studio install path under "\Common7\Packages\Debugger". Since my DLL was named DebugTypes.dll, you will want to replace the filename below with your DLL’s name.

 

autoexp.dat

_WIN32_FIND_DATAA=$ADDIN(DebugTypes.dll,OS_WIN32_FIND_DATAA)

_PROCESS_INFORMATION=$ADDIN(DebugTypes.dll,OS_PROCESS_INFORMATION)

_RTL_CRITICAL_SECTION=$ADDIN(DebugTypes.dll,OS_CRITICAL_SECTION)

_OVERLAPPED=$ADDIN(DebugTypes.dll,OS_OVERLAPPED)

 

Footnotes

(1) A CRITICAL_SECTION is really just a typedef for the RTL_CRITICAL_SECTION struct. So you will see "_RTL_CRITICAL_SECTION" in the autoexp.dat file instead.

(2) By keeping track of the length as you go, you avoid the performance hit typically associated with a strcat() type of function call. And no, I don't think that "performantly" is a real word.

(3) I use a Post-Build event in my project to automatically copy the DLL into the Visual Studio install path:
copy "$(TargetPath)" "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE"