Balloon Tooltips

In the Windows XP login screen, the password text box will warn you with a balloon tooltip if you accidentally turn Caps Lock on. The balloon tooltip appears to be a Windows tooltip common control with the TTS_BALLOON style.

To replicate this functionality, I decided to write a function called ShowMsgBalloon() which, given a control and the various balloon tooltip parameters, creates and shows the balloon tooltip below the control.

The key insight to making ShowMsgBallon() work as intended was to use the TTF_TRACK option to create a tracking tooltip. This will immediately show the tooltip without requiring the user to position the mouse over the control. The main downside to using TTF_TRACK is that the tooltip will not move with the control if the window is moved; you need to manually move the tooltip using TTM_TRACKPOSITION as required. One could probably make this automatic by subclassing the tooltip’s parent control and handling WM_WINDOWPOSCHANGED messages.

Here is the source code to ShowMsgBalloon(). When you are done with the balloon, call DestroyWindow() on the returned HWND. Note: you may want your application to use comctl32.dll version 6 as it will lead to a nicer visual style, including a close button.

#include <windows.h>
#include <commctrl.h>
     
// Options to ShowMsgBallon() (see dwOpts parameter).  These are the
// standard icon types for balloon tooltips.
#define SMB_ICON_INFO    (1 << 0)
#define SMB_ICON_WARNING (1 << 1)
#define SMB_ICON_ERROR   (1 << 2)
     
// Given the options passed to ShowMsgBalloon(), determine what
// parameter to send to TTM_SETTITLE for the balloon tooltip’s icon.
static DWORD
GetTitleIcon(DWORD dwOpts)
{
    if (dwOpts &amp; SMB_ICON_INFO)
        return TTI_INFO;
    else if (dwOpts &amp; SMB_ICON_WARNING)
        return TTI_WARNING;
    else if (dwOpts &amp; SMB_ICON_ERROR)
        return TTI_ERROR;
    else
        return 0;
}
     
// Create and show a balloon tooltip immediately below the control
// hwndCtrl with the given title, message, and options.
HWND
ShowMsgBalloon(HWND hwndCtrl, LPCTSTR szTitle, LPCTSTR szMsg,
               DWORD dwOpts)
{
    HWND hwndRet = NULL;
    HWND hwndTT = NULL;
    TOOLINFO ti = { 0 };
    RECT rc;
     
    // Even though TTS_CLOSE is always specified, a close button will
    // only be shown if your application has a manifest that requires
    // comctl32.dll version 6.
    hwndTT = CreateWindow
        (
        TOOLTIPS_CLASS,
        TEXT(""),
        WS_POPUP | TTS_NOPREFIX | TTS_BALLOON | TTS_CLOSE,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        hwndCtrl,
        NULL,
        NULL,
        NULL
        );
    if (hwndTT == NULL)
        goto Cleanup;
     
    // By using TTTOOLINFO_V1_SIZE rather than sizeof(TOOLINFO),
    // we don’t require users to be using comctl32 version 6.
    ti.cbSize = TTTOOLINFO_V1_SIZE;
    ti.uFlags = TTF_TRACK;
    ti.hwnd = hwndCtrl;
    ti.lpszText = const_cast<lptstr>(szMsg);
    if (!SendMessage(hwndTT, TTM_ADDTOOL, 0, (LPARAM) &amp;ti))
        goto Cleanup;
    if (!SendMessage(hwndTT, TTM_SETTITLE, GetTitleIcon(dwOpts),
                     (LPARAM) szTitle))
        goto Cleanup;
     
    // Position the tooltip below the control
    if (!GetWindowRect(hwndCtrl, &amp;rc))
        goto Cleanup;
    SendMessage(hwndTT, TTM_TRACKPOSITION, 0,
                MAKELONG(rc.left + 10, rc.bottom));
     
    // Show the tooltip
    if (!SendMessage(hwndTT, TTM_TRACKACTIVATE, TRUE, (LPARAM) &amp;ti))
        goto Cleanup;
     
    hwndRet = hwndTT;
    hwndTT = NULL;
     
Cleanup:
    if (hwndTT != NULL)
        DestroyWindow(hwndTT);
     
    return hwndRet;
}

Update 2008-11-01 3:08PM: If you are targeting comctl32.dll version 6 or later, I recommend using the EM_SHOWBALLOONTIP message. Comctl32.dll version 6 or later also automatically shows the caps lock warning balloon for edit boxes with the ES_PASSWORD window style.

STL Objects and Win32 Module Boundaries

Let’s say you have the following function:

void AppendChar(std::string& s, char ch)
{
    s += ch;
}

What happens if this function is exported as an ordinal function from a DLL (not an inlined piece of code inside a header) and you call it from an EXE?

It works most of the time. When it doesn’t, it corrupts your heap and causes a spectacular mess.

In Windows you must free memory with the same allocator that allocated it. However, your EXE may not share the same allocator as the DLL. Perhaps the two modules are linked against different versions of libc, or perhaps one of the modules is using a static version of libc. If your EXE and DLL do not share an allocator and if AppendChar resizes the string s, you will almost certainly cause a heap corruption.

The STL performs a lot of reallocations behind the scenes for you; this is one of its major benefits. Unfortunately, if you are writing a general-purpose DLL these behind-the-scene allocations are deadly. You cannot know or dictate what version of libc your clients will use.

Therefore, I reiterate my previous recommendation:

Avoid passing STL objects as parameters to DLLs.

Custom-Drawn Win32 Tooltips

Like many common controls, the tooltip control supports custom drawing for maximum flexibility. This is a quick tutorial on how to use the tooltip custom draw facility.

First, start with the following scratch program (which is a slightly modified version of Raymond Chen’s scratch program):

#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <tchar.h>
     
#define WND_CLASS_NAME TEXT("Scratch")
     
HINSTANCE g_hinst;
     
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
    return TRUE;
}

void OnDestroy(HWND hwnd)
{
    PostQuitMessage(0);
}
     
LRESULT CALLBACK WndProc(HWND hwnd, UINT uiMsg, WPARAM wParam,
                         LPARAM lParam)
{
    switch (uiMsg)
    {
    HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
    HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
    }
     
    return DefWindowProc(hwnd, uiMsg, wParam, lParam);
}
     
BOOL RegisterWindowClass()
{
    WNDCLASS wc;
    ATOM atom;
     
    wc.style = 0;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = g_hinst;
    wc.hIcon = NULL;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = WND_CLASS_NAME;
     
    atom = RegisterClass(&wc);
    return (atom != 0);
}
     
int WINAPI _tWinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                     LPTSTR lpCmdLine, int nCmdShow)
{
    INITCOMMONCONTROLSEX icc;
    int ret = EXIT_FAILURE;
     
    g_hinst = hinst;
     
    // We will need the tooltip common control
    icc.dwSize = sizeof(icc);
    icc.dwICC = ICC_WIN95_CLASSES;
    if (InitCommonControlsEx(&icc))
    {
        if (RegisterWindowClass())
        {
            HWND hwnd = CreateWindow
                (
                WND_CLASS_NAME,
                TEXT("Scratch"),
                WS_OVERLAPPEDWINDOW,
                CW_USEDEFAULT, CW_USEDEFAULT,
                CW_USEDEFAULT, CW_USEDEFAULT,
                NULL,
                NULL,
                hinst,
                0
                );
            if (hwnd != NULL)
            {
                MSG msg;
     
                (void) ShowWindow(hwnd, nCmdShow);
                while (GetMessage(&msg, NULL, 0, 0)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
     
                ret = EXIT_SUCCESS;
            }
        }
    }
     
    return ret;
}

Next, we’ll add a tooltip to this window. We’re not going to do anything fancy like tooltip multiplexing so we’ll use TTF_SUBCLASS.

HWND g_hwndTT;
     
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
    BOOL ret = FALSE;
     
    // Create the tooltip window
    g_hwndTT = CreateWindow
        (
        TOOLTIPS_CLASS,
        NULL,
        TTS_NOPREFIX,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        hwnd,
        NULL,
        g_hinst,
        NULL
        );
    if (g_hwndTT != NULL)
    {
        // Tell the tooltip to register itself using the entire scratch
        // window’s client area as the active region.
        TOOLINFO ti = { sizeof(ti) };
        ti.uFlags = TTF_SUBCLASS;
        ti.hwnd = hwnd;
        ti.hinst = g_hinst;
        ti.uId = 0;
        ti.lpszText = TEXT("Hello world!");
        if (GetClientRect(hwnd, &ti.rect))
        {
            if (SendMessage(g_hwndTT, TTM_ADDTOOL, 0, (LPARAM)&ti))
            {
                ret = TRUE;
            }
        }
    }
     
    // If there were any failures, clean up the allocated objects
    if (!ret)
    {
        if (g_hwndTT)
            DestroyWindow(g_hwndTT);
    }
     
    return ret;
}

If you compile and run the program, you should see a tooltip with the text “Hello World!” pop up.

To use custom draw, we must handle the NM_CUSTOMDRAW message. First we’ll write the WM_NOTIFY handler to forward the message to our function.

LRESULT OnToolTipCustomDraw(NMTTCUSTOMDRAW* pcd)
{
    // Perform the default action (i.e. have the tooltip control draw
    // "Hello World!" itself)
    return CDRF_DODEFAULT;
}
     
LRESULT OnNotify(HWND hwnd, int idFrom, NMHDR* pnm)
{
    if (pnm->hwndFrom == g_hwndTT)
    {
        switch (pnm->code)
        {
        case NM_CUSTOMDRAW:
            return OnToolTipCustomDraw((NMTTCUSTOMDRAW*) pnm);
        }
    }
     
    return 0;
}
     
LRESULT CALLBACK WndProc(HWND hwnd, UINT uiMsg, WPARAM wParam,
                         LPARAM lParam)
{
    switch (uiMsg)
    {
    HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
    HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
    HANDLE_MSG(hwnd, WM_NOTIFY, OnNotify);
    }
     
    return DefWindowProc(hwnd, uiMsg, wParam, lParam);
}

Now we’ll implement the custom draw function. For simplicity’s sake, we will simply write the text “Hello World!” just as the tooltip did before we used custom draw.

It is important to note that the tooltip control will continue to draw the static text even if we are using custom draw. To get around this problem, we’ll have the tooltip control draw the text in the same color as the background, effectively making it invisible.

// Draw the contents of the tooltip within the rectangle prc
void DrawToolTipContent(HDC hdc, RECT* prc)
{
    SetTextColor(hdc, RGB(0, 0, 0));
    TextOut(hdc, prc->left, prc->top, TEXT("Hello World!"), 12);
}
     
LRESULT OnToolTipCustomDraw(NMTTCUSTOMDRAW* pcd)
{
    switch (pcd->nmcd.dwDrawStage)
    {
    case CDDS_PREPAINT:
        {
            // Set the text and back colors of default text so it
            // becomes invisible
            COLORREF clrBg = (COLORREF) SendMessage(g_hwndTT,
                                                    TTM_GETTIPBKCOLOR,
                                                    0, 0);
            SetTextColor(pcd->nmcd.hdc, clrBg);
            SetBkColor(pcd->nmcd.hdc, clrBg);
            return CDRF_NOTIFYPOSTPAINT;
        }
    case CDDS_POSTPAINT:
        {
            DrawToolTipContent(pcd->nmcd.hdc, &pcd->nmcd.rc);
            return CDRF_SKIPDEFAULT;
        }
    }
     
    return CDRF_DODEFAULT;
}

We now can use the full range of GDI functions to render the content of the tooltip, including multiple fonts, lines, ellipses, etc. However, drawing is limited to the size of the tooltip’s client window, and this is determined based on the text passed to the tooltip window in OnCreate(). If we want to control the size of the tooltip, we must handle the TTN_SHOW message:

LPCTSTR g_szTooltipMsg = TEXT("Hello World!");
     
// Determine the required size of the client area of the tooltip
BOOL GetToolTipContentSize(SIZE* psz)
{
    BOOL ret = FALSE;
     
    HDC hdc = GetDC(g_hwndTT);
    if (hdc != NULL)
    {
        HFONT hfontTT = (HFONT) SendMessage(g_hwndTT, WM_GETFONT, 0, 0);
        HFONT hfontTTOld = (HFONT) SelectObject(hdc, hfontTT);
        if (hfontTTOld != NULL)
        {
            SIZE szText;
            if (GetTextExtentPoint32(hdc, g_szTooltipMsg,
                                     lstrlen(g_szTooltipMsg), &szText))
            {
                psz->cx = szText.cx;
                psz->cy = szText.cy;
                ret = TRUE;
            }
     
            SelectObject(hdc, hfontTTOld);
        }
     
        ReleaseDC(g_hwndTT, hdc);
    }
     
    return ret;
}
     
// Determine the required client rectangle of the tooltip to fit the
// text
BOOL GetToolTipContentRect(RECT* prc)
{
    BOOL ret = FALSE;
     
    SIZE sz;
    if (GetToolTipContentSize(&sz))
    {
        if (GetWindowRect(g_hwndTT, prc))
        {
            prc->right = prc->left + sz.cx;
            prc->bottom = prc->top + sz.cy;
            ret = TRUE;
        }
    }
     
    return ret;
}
     
// When the tooltip is being shown, size it to fit the content
LRESULT OnToolTipShow(NMHDR* pnm)
{
    LRESULT ret = 0;
    RECT rc;
     
    if (GetToolTipContentRect(&rc))
    {
        // Adjust the rectangle to be the proper size to contain the
        // content
        if (SendMessage(g_hwndTT, TTM_ADJUSTRECT, TRUE, (LPARAM) &rc))
        {
            // Resize and move the tooltip accordingly
            if (SetWindowPos(g_hwndTT, NULL, rc.left, rc.top,
                             rc.right - rc.left, rc.bottom - rc.top,
                             SWP_NOZORDER | SWP_NOACTIVATE))
            {
                ret = TRUE;
            }
        }
    }
     
    return ret;
}
     
LRESULT OnNotify(HWND hwnd, int idFrom, NMHDR* pnm)
{
    if (pnm->hwndFrom == g_hwndTT)
    {
        switch (pnm->code)
        {
        case TTN_SHOW:
            return OnToolTipShow(pnm);
        case NM_CUSTOMDRAW:
            return OnToolTipCustomDraw((NMTTCUSTOMDRAW*) pnm);
        }
    }
     
    return 0;
}
     
void DrawToolTipContent(HDC hdc, RECT* prc)
{
    SetTextColor(hdc, RGB(0, 0, 0));
    TextOut(hdc, prc->left, prc->top, g_szTooltipMsg,
            lstrlen(g_szTooltipMsg));
}

We now have a sizable, custom-drawable tooltip control.

Vista Does Not Virtualize Creation Of Shell Links

Windows Vista developers beware: Vista does not perform file virtualization on the creation of shell links. Consider the following code:

// Creates a shell link (a.k.a. shortcut) located at swzLinkFile that points to
// szTargetFile with a description of szDescription.
BOOL CreateLink(LPCTSTR szTargetFile, LPCTSTR szDescription, LPCOLESTR swzLinkFile)
{
    BOOL bRet = FALSE;

    IShellLink* psl;
    HRESULT hr = ::CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
                                    IID_IShellLink, (void**) &psl);
    if (SUCCEEDED(hr))
    {
        IPersistFile* ppf;
        hr = psl->QueryInterface(IID_IPersistFile, (void**) &ppf);
        if (SUCCEEDED(hr))
        {
            hr = psl->SetPath(szTargetFile);
            if (SUCCEEDED(hr))
            {
                hr = psl->SetDescription(szDescription);
                if (SUCCEEDED(hr))
                {
                    hr = ppf->Save(swzLinkFile, TRUE);
                    if (SUCCEEDED(hr))
                    {
                        bSuccess = TRUE;
                    }
                }
            }
            ppf->Release();
        }
        psl->Release();
    }
    return bSuccess;
}

// NOTE: Hardcoding C:\WINDOWS and C:\Program Files is a bad practice.  Use
// something like ::SHGetFolderPath().
BOOL bSuccess = CreateLink
    (
    _T("C:\\WINDOWS\\SYSTEM32\\SOL.EXE"),
    _T("Shortcut to SOL.EXE"),
    L"C:\\Program Files\\sol.lnk")
    );

One might expect that the creation of the file C:\Program Files\sol.lnk would be silently redirected by Vista using file virtualization and CreateLink() would succeed, but it doesn’t — the call to IPersistFile::Save() returns E_ACCESSDENIED.

For more information about developing on Vista, see the document Windows Vista Application Development Requirements for User Account Control Compatibility.

Debugging Crashes in Windows Applications: The Null Pointer Dereference

Windows C++ developers remain all too familiar with the standard Windows crash dialog. This post is an attempt to teach developers how to understand the data the crash dialog reports to diagnose difficult issues. A basic understanding of assembly language is assumed; for more background on these topics please read Matt Pietrek’s “Under The Hood” articles in the Microsoft Systems Journal February 1998 and June 1998 issues.

To begin with, let’s write an application that crashes with a null pointer dereference:

// DerefNullPointer.c: Crash by dereferencing the null pointer.

static void DoCrash()
{
    *((char*) 0) = 1;
}

int main(int argc, char* argv[])
{
    DoCrash();
    return 0;
}

I placed the null pointer dereference in its own function for reasons that will later become clear. Now, let’s compile the application from the command line:

C:\Proj\Crashes> cl /Od /nologo /c DerefNullPtr.c
C:\Proj\Crashes> link /SUBSYSTEM:CONSOLE /OUT:DerefNullPtr.exe DerefNullPtr.obj

Now run the application:

C:\Proj\Crashes> DerefNullPtr.exe

The standard Windows crash dialog pops up.

Normally users will click “Send Error Report” or “Don’t Send”. If you have access to the machine and a debugger installed you can click “Debug”. Alternatively, you can view the error report details right away by clicking “click here.” If you do, an error report dialog will pop up.

The most important piece of information from the error report dialog is the offset of the instruction which caused the crash. Note that this value is an offset — it’s the address of the crashing instruction relative to where DerefNullPtr.exe is loaded into memory.

We now have enough information to see the instruction which is causing the crash. One way to do this is to use Doron Holan’s windbg1 dump file trick:

C:\Proj\Crashes> windbg -z DerefNullPtr.exe

Microsoft (R) Windows Debugger  Version 6.6.0003.5
Copyright (c) Microsoft Corporation. All rights reserved.

Loading Dump File [C:\Proj\Crashes\DerefNullPtr.exe]
Symbol search path is: SRV*C:\websymbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00408000   C:\Proj\Crashes\DerefNullPtr.exe
eax=00000000 ebx=00000000 ecx=00000000 edx=00000000 esi=00000000 edi=00000000
eip=00401041 esp=00000000 ebp=00000000 iopl=0         nv up di pl nz na pe nc
cs=0000  ss=0000  ds=0000  es=0000  fs=0000  gs=0000             efl=00000000
*** WARNING: Unable to verify checksum for DerefNullPtr.exe
*** ERROR: Module load completed but symbols could not be loaded for DerefNullPtr.exe
DerefNullPtr+0x1041:
00401041 6a18             push    0x18
0:000> u DerefNullPtr+0×1013
DerefNullPtr+0×1013:
00401013 c6050000000001   mov     byte ptr [00000000],0×1
0040101a 5d               pop     ebp
0040101b c3               ret
0040101c 833dc872400002   cmp  dword ptr [DerefNullPtr+0×72c8 (004072c8)],0×2
00401023 7405             jz      DerefNullPtr+0×102a (0040102a)
00401025 e8f0040000       call    DerefNullPtr+0×151a (0040151a)
0040102a ff742404         push    dword ptr [esp+0×4]
0040102e e870030000       call    DerefNullPtr+0×13a3 (004013a3)

The crashing instruction is mov byte ptr [00000000],0x1. Clearly the bug is a null pointer dereference.

Had we produced a map file at link time, we could have done even better. Let me show you:

C:\Proj\Crashes> link /SUBSYSTEM:CONSOLE /MAP:DerefNullPtr.map /OUT:DerefNullPtr.exe DerefNullPtr.obj

The map file looks like:

DerefNullPtr

Timestamp is 462fae49 (Wed Apr 25 14:38:49 2007)

Preferred load address is 00400000

Start         Length     Name                   Class
0001:00000000 00003b68H .text                   CODE
0002:00000000 000000c4H .idata$5                DATA
...
0003:00000030 00000274H .data                   DATA
0003:000002c0 00000598H .bss                    DATA

Address         Publics by Value              Rva+Base     Lib:Object

0000:00000000       __except_list              00000000     <absolute>
0000:00000002       ___safe_se_handler_count   00000002     <absolute>
0001:00000000       _main                      00401000 f   DerefNullPtr.obj
0001:0000001c       __amsg_exit                0040101c f   LIBC:crt0.obj
0001:00000041       _mainCRTStartup            00401041 f   LIBC:crt0.obj
...
0003:00000850       __FPinit                   00407850     <common>
0003:00000854       __acmdln                   00407854     <common>

entry point at        0001:00000041

Static symbols

0001:00000010       _DoCrash                   00401010 f   DerefNullPtr.obj
0001:000002a2       _doexit                    004012a2 f   LIBC:crt0dat.obj
0001:0000078b       _parse_cmdline             0040178b f   LIBC:stdargv.obj

The first thing to note is that all addresses in the map file are relative to the preferred load address, 0x00400000. Therefore, we’re looking for the function which contains the address 0x00400000 + 0x00001013 = 0x00401013. To do so, we should look for the function which has the largest address less than or equal to the address of the crash. The function _DoCrash() is at the address 0x00401010, so the crash appears to be at the address _DoCrash+0x3.

Better still is to create a PDB file alongside your retail builds, as in:

C:\Proj\Crashes> cl /Zi /Od /nologo /c DerefNullPtr.c
C:\Proj\Crashes> link /DEBUG /SUBSYSTEM:CONSOLE /PDB:DerefNullPtr.pdb /MAP:DerefNullPtr.map /OUT:DerefNullPtr.exe DerefNullPtr.obj

Note that the /DEBUG option may be a little confusing — it really should named /CREATEPDB.

Unfortunately, this change will invalidate the previous crash address. (This wouldn’t be a problem if building retail PDBs was part of your normal process) However, we can reproduce the bug within the debugger:

C:\Proj\Crashes> windbg DerefNullPtr.exe

Microsoft (R) Windows Debugger  Version 6.6.0003.5
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: DerefNullPtr.exe
Symbol search path is: SRV*C:\websymbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 0040d000   DerefNullPtr.exe
ModLoad: 7c900000 7c9b0000   ntdll.dll
ModLoad: 7c800000 7c8f4000   C:\WINDOWS\system32\kernel32.dll
(3e4.7f4): Break instruction exception - code 80000003 (first chance)
eax=00241eb4 ebx=7ffd9000 ecx=00000000 edx=00000001 esi=00241f48 edi=00241eb4
eip=7c901230 esp=0012fb20 ebp=0012fc94 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!DbgBreakPoint:
7c901230 cc               int     3
0:000> g
(3e4.7f4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=003214a8 ebx=7ffd9000 ecx=00000001 edx=7c90eb94 esi=00000a28 edi=00000000
eip=00401023 esp=0012fedc ebp=0012fedc iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
*** WARNING: Unable to verify checksum for DerefNullPtr.exe
DerefNullPtr!DoCrash+0×3:
00401023 c6050000000001   mov     byte ptr [00000000],0×1 ds:0023:00000000=??
0:000> kb
ChildEBP RetAddr  Args to Child
0012fedc 00401018 0012ffc0 00401245 00000001 DerefNullPtr!DoCrash+0×3 [c:\proj\crashes\derefnullptr.c @ 3]
0012fee4 00401245 00000001 00321470 003214a8 DerefNullPtr!main+0×8 [c:\proj\crashes\derefnullptr.c @ 9]
0012ffc0 7c816fd7 00090000 00a7fa9c 7ffd9000 DerefNullPtr!mainCRTStartup+0×173 [f:\vs70builds\3077\vc\crtbld\crt\src\crt0.c @ 259]
0012fff0 00000000 004010d2 00000000 78746341 kernel32!BaseProcessStart+0×23

Using PDBs, you have the ability to map instruction addresses to a file and line number, even with retail builds. I recommend producing PDBs as part of your build process and placing them somewhere accessible by all developers on your team.

1. Windbg is an extremely powerful debugger included with Microsoft’s free Debugging Tools for Windows.