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


CRITICAL_SECTION after


OVERLAPPED


Before


OVERLAPPED before


After


OVERLAPPED after


PROCESS_INFORMATION


Before


PROCESS_INFORMATION before


After


PROCESS_INFORMATION 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"


 

Comments (2)

  1. bcam37 says:

    Great article. Trying to use addin to display custom array elements which are stored in heap memory. As soon as I set my array object's pointer to the elements heap address, the ReadDebuggeeMemoryEx call fails to return my array object that contains the heap pointer. Prior to setting the pointer it read it fine. Thanks for any suggestions.

  2. Maxou83 says:

    Hello,

    this is  very interesting. Thus I met an issue I didn't solve yet, and I though you could help me about it (I already posted my issue on MSDN forums, no solution have been posted). I'm actually trying to implement an expression evaluator for Visual Studio 9 debug. It should make me able to visualize the contents of a variable whose class( still unknown ) is derived from an abstract class (say I2dBufferAccessor) . So the beginning of the code could be like:

    /*…*/

    I2dBufferAccessor* accessor;

    helper->ReadDebuggeeMemory(pHelper, pHelper->GetRealAddress(pHelper), sizeof(vlam::ia::I2dBufferAccessor*), accessor, &n_got));

    …unfortunately this code doesn't work… And I can't figure out how to do, since I can't instanciate an I2dBufferAccessor variable,  since it's an abstract class!

    I hope I've been clear and you will be able to help me. Thank you for your attention. (and sorry for my english)

    Maxime

Skip to main content