Saturday, May 21, 2011

Becoming a Console Programmer : Extending The Watch Window (EEAddin)

So we established that its possible to re-format what a Watch Window shows you, make things look better and therefore easier to consume. What we cannot do with that method tho is any real processing... but we can with a little more work.

consider the example

// blogtest.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdio.h>
#include <intrin.h>

enum e_union_test_case
{
    k_unset,
    k_type_string,
    k_type_int,
    k_type_float,
};

struct u_test_level1
{
    int m_type;
    union u_data
    {
        const char *m_string;
        int m_count;
        float m_value;
    }m_data;

    void set(const char *string)
    {
        m_type= k_type_string;
        m_data.m_string= string;
    }

    void set(int int_value)
    {
        m_type= k_type_int;
        m_data.m_count= int_value;
    }

    void set(float float_value)
    {
        m_type= k_type_float;
        m_data.m_value= float_value;
    }

    void print(void)
    {
        if (m_type == k_unset)
        {
            printf("unset\n");
        }

        if (m_type == k_type_string)
        {
            printf("%s \n",m_data.m_string);
        }
        if (m_type == k_type_int)
        {
            printf("%d \n",m_data.m_count);
        }
        if (m_type == k_type_float)
        {
            printf("%f \n",m_data.m_value);
        }
    }
};

struct s_test_level2
{
    static const int k_num_elements= 32;

    u_test_level1 m_elements[k_num_elements];
};


int _tmain(int argc, _TCHAR* argv[])
{
    s_test_level2 test;

    memset(&test,0,sizeof(test));

    test.m_elements[0].set("my foo");
    test.m_elements[1].set(7);
    test.m_elements[2].set(3.14159265358f);    // everyone loves Pi

    return 0;
}


similar to before setup a basic console project and drop in this code. Compile and run to the breakpoint and drop "test" into your watch window opening up the m_elements member... you should see something like.

not very readable at all... the print function shows what we "might" do to display these elements by using "m_type" to change the formatting options...

EEAddin is an option here, (Expression Evaluation Addin). This allows the autoexp.dat [AutoExpand] section to call into a dll for the preview display string. The dll can output any string it desires into the provided char buffer (obeying the limits of course).

you can find an example EEAddin within the VS2010 install, assuming you have the samples extracted (most installs will have a zip file) you will find the solution at

C:\Program Files (x86)\Microsoft Visual Studio 10.0\Samples\1033\VC2010Samples\C++\Debugging\EEaddin

NOTE: the example does not work out of the box, you must edit "ADDIN_API" and make it
#define ADDIN_API __declspec(dllexport)  __stdcall

then move the HRESULT return to the opposite side of the ADDIN_API on use. Optionally you could achieve the same results by altering the project compiler options.
now paste the code


enum e_union_test_case
{
    k_unset,
    k_type_string,
    k_type_int,
    k_type_float,
};

struct u_test_level1
{
    int m_type;
    union u_data
    {
        const char *m_string;
        int m_count;
        float m_value;
    }m_data;

    void print(char *result_buffer, int result_size)
    {
        if (m_type == k_unset)
        {
            sprintf_s(result_buffer, result_size,"unset\n");
        }
        if (m_type == k_type_string)
        {
            sprintf_s(result_buffer, result_size,"str:\n");
        }
        if (m_type == k_type_int)
        {
            sprintf_s(result_buffer, result_size,"int:%d \n",m_data.m_count);
        }
        if (m_type == k_type_float)
        {
            sprintf_s(result_buffer, result_size,"flt:%f \n",m_data.m_value);
        }

    }
};

HRESULT ADDIN_API AddIn_blogtest( DWORD dwAddress, DEBUGHELPER *pHelper, int nBase, BOOL bUniStrings, char *pResult, size_t max, DWORD reserved )
{
    DWORD nGot;

    u_test_level1 example;

    // read file time from debuggee memory space
    if (pHelper->ReadDebuggeeMemoryEx(pHelper, pHelper->GetRealAddress(pHelper), sizeof(u_test_level1 ), &example, &nGot) != S_OK)
        return E_FAIL;
    if (nGot != sizeof(u_test_level1))
        return E_FAIL;

    

    example.print(pResult, max);

    return S_OK;
}



Into the existing "timeaddin.cpp" file and add the export to both the header (timeaddin.h) and the def file (eeaddin.def).

Compile this and it will generate EEaddin.dll, copy this dll to your VS2010 IDE folder (C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE).

Add the line

u_test_level1=$ADDIN(eeaddin.dll,AddIn_blogtest)

to your autoexp.dat right below the previous example, below the [AutoExpand] tag.

debug your application... your new display should be

each element now displays using the m_type correctly. We have an issue however, the string is not immediately available. This is due to the EEAddin working in a different memory space than the executable it is accessing. You'll note that the EEaddin DLL reads memory from the application manually using the supplied address AND by dint of the autoexp.dat line we know the size of the type we're representing. We must therefore pull any pointed at string data over manually in order to display it.

the new code is


enum e_union_test_case
{
    k_unset,
    k_type_string,
    k_type_int,
    k_type_float,
};

struct u_test_level1
{
    int m_type;
    union u_data
    {
        const char *m_string;
        int m_count;
        float m_value;
    }m_data;

    void print(char *result_buffer, int result_size)
    {
        if (m_type == k_unset)
        {
            sprintf_s(result_buffer, result_size,"unset\n");
        }
        if (m_type == k_type_string)
        {
            sprintf_s(result_buffer, result_size,"str:%s\n",m_data.m_string);
        }
        if (m_type == k_type_int)
        {
            sprintf_s(result_buffer, result_size,"int:%d \n",m_data.m_count);
        }
        if (m_type == k_type_float)
        {
            sprintf_s(result_buffer, result_size,"flt:%f \n",m_data.m_value);
        }

    }
};

HRESULT ADDIN_API AddIn_blogtest( DWORD dwAddress, DEBUGHELPER *pHelper, int nBase, BOOL bUniStrings, char *pResult, size_t max, DWORD reserved )
{
    DWORD nGot;

    u_test_level1 example;

    // read file time from debuggee memory space
    if (pHelper->ReadDebuggeeMemoryEx(pHelper, pHelper->GetRealAddress(pHelper), sizeof(u_test_level1 ), &example, &nGot) != S_OK)
        return E_FAIL;
    if (nGot != sizeof(u_test_level1))
        return E_FAIL;

    static const int local_buffer_size= 1024;
    char local_buffer[local_buffer_size];

    if (example.m_type == k_type_string && example.m_data.m_string != 0)
    {
        // pull over the string to our local buffer
        pHelper->ReadDebuggeeMemoryEx(pHelper, (DWORDLONG)example.m_data.m_string, local_buffer_size, local_buffer, &nGot);

        example.m_data.m_string= local_buffer;
    }

    example.print(pResult, max);

    return S_OK;
}


this code uses the pointer within the original "example" and pulls over an arbitrary length data stream (1kb) relying upon the string being null terminated. It then points our local copy of example.m_data.m_string to that local buffer before calling our new print(...) function which will now output the string as before. Our new display is:

which is exactly what we need.

Notes:
  • EEAddin is not forgiving
    • if you do something heinous within an EEAddin entrypoint it WILL take out the devenv that loaded it. Within Addins you MUST program defensively at all points, assume you will be sent bad data, assume your strings will not be null terminated, assume that the pointers you need to follow are bad and take you into no mans land. Verify everything. You'll note that i did not do this in my example but that is merely for the purposes of keeping the code samples short and sweet.
  • Debugging the EEAddin
    • load up the EEAddin project, Attach to the instance of devenv (debug it) that your Main project runs in, put breakpoints into your Addin entrypoint then hit debug on your application. Each time your Watch Window updates your EEAddin dll is loaded/called/unloaded so expect a lot of calls in the array case.
  • Global Symbols are not accessible
    • Accessing client memory requires the physical address of said client memory, if this address isn't accessible from the element you're debugging then you have no way to get to that address. An example of this limitation would be a handle into a global manager. The client code can call the manager to get the object, the debugger can only display the value of the handle, EEAddin can only access the internals OF the handle. It does not have knowledge of the manager and thus it cannot access the object itself.
  • Not all Endianess was made equal
    • if your target is not the same endianess as PC (little) such as xbox 360 then you must switch the endianess of data before reading it.
  • Alignment & Pointer size
    • you will have to manually handle both alignment differences (target <=> client) and pointer size differences. In my support thus far i have chosen 2 pathways. Some structures i have made cross platform entirely by supplying a construct that is 64bit and aware of its pointer size. For other types where this isn't possible i have used an element of the next topic to disclose to the DLL the size of our pointer.

No comments: