Elementary, my dear Watson! (03 Jul 2007)

When an application crashes under Windows Vista, it will contact Microsoft's Windows Error Reporting servers; only if those servers request more data about the problem, crashdump files will be generated. Bad luck if you're a developer talking to an enraged customer who just lost his data, and you first have to go through a lengthy process of registering your application on Winqual, mapping the right application versions, and waiting until the first customer crashdumps show up on the server. Chances are that your customer may have taken his money elsewhere in the meantime while waiting for you to fix his problem.

In Think globally, dump locally, I listed the following workarounds:

  • Using the ForceQueue registry entry to force WER to always produce a crashdump (at the price of losing UI interaction)
  • Disable the Internet connection before the crash occurs
  • (Ab-)Using the CorporateWERServer registry entry
  • Deploy Microsoft Operations Manager 2007, including Agentless Exception Monitoring (which replaces the older "Corporate Error Reporting" servers)
  • Copy Dr. Watson from XP to the Vista system and install it as system JIT debugger
  • Install a top-level crash filter in your application using SetUnhandledExceptionFilter

In Don't dump Vista just yet..., I presented a little-known Task Manager option which creates a user dump file from any running process.

And in this installment of the seemingly never-ending series on Vista crashdumps, we'll explore yet another option. What if we had a system JIT debugger which can be easily installed on a customer system and automatically produces minidump files for any crashing application? Basically a homebrew version of good ol' Dr. Watson, stripped down to bare essentials?

mydearwatson_install.png

The following code illustrates this approach. This skeleton application is called mydearwatson and installs as a JIT debugger. When an app crashes, it attaches to it and asks the user what kind of minidump (normal or full dump) should be generated. The resulting crashdump file goes to the current user's desktop folder, readily available for sending it off to the developer of the application.

// mydearwatson
//
// Minimal JIT debugger; attaches to a crashing process 
// and generates minidump information. Intended to be used
// as the rough skeleton for a poor man's Dr. Watson 
// replacement on Vista.
//
// Written by Claus Brod, http://www.clausbrod.de/Blog

#include <windows.h>
#include <DbgHelp.h>
#pragma comment(lib, "DbgHelp.lib")
#include <Psapi.h>
#include <shlobj.h>
#include <atlbase.h>

#include <stdio.h>
#include <string.h>

#define MSGHDR "mydearwatson\n" \
               "(C) 2007 Claus Brod, http://www.clausbrod.de/Blog\n\n"

bool uninstall(void)
{
  CRegKey key;
  if (ERROR_SUCCESS != key.Open(HKEY_LOCAL_MACHINE,
    "Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug\\",
    KEY_READ | KEY_WRITE))
    return false;

  // check for old debugger registration
  char debuggerCommandLine[MAX_PATH+256];
  ULONG nChars = _countof(debuggerCommandLine);
  LONG ret = key.QueryStringValue("PreMydearwatsonDebugger",
    debuggerCommandLine, &nChars);
  if (ret == ERROR_SUCCESS) {
    ret = key.SetStringValue("Debugger", debuggerCommandLine);
    if (ret == ERROR_SUCCESS) {
      ret = key.DeleteValue("PreMydearwatsonDebugger");
    }
  }

  return ret == ERROR_SUCCESS;
}

bool install(char *programName)
{
  CRegKey key;
  if (ERROR_SUCCESS != key.Open(HKEY_LOCAL_MACHINE,
    "Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug\\",
    KEY_READ | KEY_WRITE))
    return false;

  char debuggerCommandLine[MAX_PATH+256];
  ULONG nChars = _countof(debuggerCommandLine);
  if (ERROR_SUCCESS == key.QueryStringValue("Debugger", debuggerCommandLine, &nChars)) {
    _strlwr_s(debuggerCommandLine, _countof(debuggerCommandLine));
    if (!strstr(debuggerCommandLine, "mydearwatson")) {
      // save command line for previously installed debugger
      key.SetStringValue("PreMydearwatsonDebugger", debuggerCommandLine);
    }
  }

  char debuggerPath[MAX_PATH];
  strcpy_s(debuggerPath, programName);  // preset with default
  ::GetModuleFileName(GetModuleHandle(0), debuggerPath, _countof(debuggerPath));

  _snprintf_s(debuggerCommandLine, _countof(debuggerCommandLine), _TRUNCATE,
    "\"%s\" -p %%ld -e %%ld", debuggerPath);
  return ERROR_SUCCESS == key.SetStringValue("Debugger", debuggerCommandLine);
}

char *getMinidumpPath(void)
{
  static char dumpPath[MAX_PATH];
  if (dumpPath[0] == 0) {
    SHGetSpecialFolderPath(NULL, dumpPath, CSIDL_DESKTOPDIRECTORY, FALSE);
  }
  return dumpPath;
}

char *getMinidumpFilename(DWORD pid)
{
  static char minidumpFilename[MAX_PATH];
  if (!minidumpFilename[0]) {
    _snprintf_s(minidumpFilename, MAX_PATH, _TRUNCATE,
      "%s\\mydearwatson_pid%d.mdmp", getMinidumpPath(), pid);
  }
  return minidumpFilename;
}

bool dumpHelper(HANDLE hDumpFile,
                HANDLE processHandle, DWORD pid,
                HANDLE threadHandle, DWORD tid,
                EXCEPTION_RECORD *exc_record, MINIDUMP_TYPE miniDumpType)
{
  bool ret = false;

  CONTEXT threadContext;
  threadContext.ContextFlags = CONTEXT_ALL;
  if (::GetThreadContext(threadHandle, &threadContext)) {
    __try {
      MINIDUMP_EXCEPTION_INFORMATION exceptionInfo;
      exceptionInfo.ThreadId = tid;
      EXCEPTION_POINTERS exc_ptr;
      exc_ptr.ExceptionRecord = exc_record;
      exc_ptr.ContextRecord = &threadContext;
      exceptionInfo.ExceptionPointers = &exc_ptr;
      exceptionInfo.ClientPointers = FALSE;

      if (MiniDumpWriteDump(processHandle,
        pid, hDumpFile, miniDumpType, &exceptionInfo, NULL, NULL))
        ret = true;
    } __except(EXCEPTION_EXECUTE_HANDLER) { }
  }

  return ret;
}

struct HandleOnStack
{
  HANDLE m_h;
  HandleOnStack(HANDLE h) : m_h(h) { }
  ~HandleOnStack() { if (m_h && m_h != INVALID_HANDLE_VALUE) CloseHandle(m_h); }
  operator HANDLE() { return m_h; }
};

bool createMinidump(HANDLE processHandle, DWORD pid, DWORD tid,
                    EXCEPTION_RECORD *exc_record)
{
  HandleOnStack hDumpFile(CreateFile(getMinidumpFilename(pid), GENERIC_WRITE, 0, NULL,
    CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
  if (hDumpFile == INVALID_HANDLE_VALUE)
    return false;

  HandleOnStack
    threadHandle(OpenThread(THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME, 0, tid));
  if (!threadHandle)
    return false;

  if (-1 == SuspendThread(threadHandle))
    return false;

  MINIDUMP_TYPE miniDumpType = MiniDumpNormal;
  if (IDYES == MessageBox(NULL, MSGHDR
    "By default, minimal crashdump information is generated.\n"
    "Do you want full crashdump information instead?",
    "mydearwatson", MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2)) {
      miniDumpType = MiniDumpWithFullMemory;
  }

  bool ret = dumpHelper(hDumpFile, processHandle, pid,
    threadHandle, tid, exc_record, miniDumpType);
  if (ret) {
    char buf[1024];
    _snprintf_s(buf, _countof(buf), _TRUNCATE, MSGHDR
      "Minidump information has been written to\n%s.\n",
      getMinidumpFilename(pid));
    MessageBox(NULL, buf, "mydearwatson", MB_OK|MB_ICONINFORMATION);
  }

  ResumeThread(threadHandle);
  return ret;
}

int debuggerLoop(DWORD pid, HANDLE eventHandle)
{
  // attach to debuggee
  if (!DebugActiveProcess(pid)) {
    fprintf(stderr, "Could not attach to process %d\n", pid);
    return 1;
  }

  HANDLE processHandle = 0;
  while (1) {
    DEBUG_EVENT de;
    if (WaitForDebugEvent(&de, INFINITE)) {
      switch(de.dwDebugEventCode)
      {
      case CREATE_PROCESS_DEBUG_EVENT:
        processHandle = de.u.CreateProcessInfo.hProcess;
        printf("Attaching to process %x...\n", processHandle);
        break;

      case EXCEPTION_DEBUG_EVENT:
        printf("Exception reported: code=%x, dwFirstChance=%d\n",
          de.u.Exception.ExceptionRecord.ExceptionCode, de.u.Exception.dwFirstChance);

        if (de.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT) {
          SetEvent(eventHandle);
        } else {
          createMinidump(processHandle, de.dwProcessId, de.dwThreadId,
            &de.u.Exception.ExceptionRecord);
          ContinueDebugEvent(de.dwProcessId, de.dwThreadId,
            DBG_EXCEPTION_NOT_HANDLED); // required?
          DebugActiveProcessStop(pid);
          printf("Detached from process, terminating debugger...\n");
          return 0;
        }
        break;

      default:
        // printf("debug event code = %d\n", de.dwDebugEventCode);
        break;
      }

      ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
    }
  } // while (1)

  return 1;
}


void usage(char *programName)
{
  fprintf(stderr, "To install as JIT debugger:\n");
  fprintf(stderr, "  %s -i\n", programName);
  fprintf(stderr, "  %s\n", programName);
  fprintf(stderr, "To uninstall:\n");
  fprintf(stderr, "  %s -u\n", programName);
  fprintf(stderr, "Call as JIT debugger:\n");
  fprintf(stderr, "  %s -p pid -e eventhandle\n", programName);
}

int main(int argc, char *argv[])
{
  DWORD pid = 0;
  HANDLE eventHandle = (HANDLE)0;
  bool uninstallationMode = false;
  bool installationMode = false;

  if (argc == 1) {
    if (IDYES == MessageBox(NULL, MSGHDR
      "Do you want to install mydearwatson as the system JIT debugger?",
      "mydearwatson", MB_YESNO|MB_ICONQUESTION)) {
        installationMode = true;
    }
  }

  for (int i=1; i<argc; i++) {
    if (!_stricmp(argv[i], "-p")) {
      pid = atol(argv[i+1]);
    }
    if (!_stricmp(argv[i], "-e")) {
      eventHandle = (HANDLE)atol(argv[i+1]);
    }
    if (!_stricmp(argv[i], "-i")) {
      installationMode = true;
      uninstallationMode = false;
    }
    if (!_stricmp(argv[i], "-u")) {
      uninstallationMode = true;
      installationMode = false;
    }
  }

  if (installationMode) {
    if (!install(argv[0])) {
      fprintf(stderr, "Could not register as a JIT debugger.\n");
      return 1;
    }
    return 0;
  }

  if (uninstallationMode) {
    if (!uninstall()) {
      fprintf(stderr, "Could not uninstall.\n");
      return 1;
    }
    return 0;
  }

  if (!pid || !eventHandle) {
    usage(argv[0]);
    return 2;
  }

  return debuggerLoop(pid, eventHandle);
}

To compile and build this code, open a Visual Studio command prompt and enter

  cl mydearwatson.cpp

To install mydearwatson, run the executable in elevated mode and confirm the installation message box. Now configure Windows Error Reporting to always ask the user before submitting crash data: "Problem Reports and Solutions/Change Settings/Ask me to check if a problem occurs". Alternatively, create a registry value called Auto (REG_SZ) in HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AeDebug and set its value to "1".

mydearwatson_minidump.jpg The next time an application crashes, the usual WER dialog will appear; click the "Debug" option in that dialog. Another message box will be displayed asking what kind of crashdump information should be written. Make your choice, and the crashdump file will magically appear on your desktop.

To uninstall, run mydearwatson with the -u option. mydearwatson tries to remember which JIT debugger was installed before, and will reinstall that JIT debugger. The mechanism for doing this is far from perfect, though.

If you look at the code, you'll notice that it basically implements a minimal debugger, using Win32 debugging APIs such as DebugActiveProcess or WaitForDebugEvent. I've never written a debugger before, so I'd assume there are a few subtleties and bugs hidden in this code, but it did work for me on both XP and Vista systems. Test results most welcome.



Previous month: Click here.

Revision: r1.3 - 03 Jul 2007 - 16:09 - ClausBrod
Blog > DefinePrivatePublic200707
Copyright © 1999-2024 by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki? Send feedback