Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Отладка приложений на C++. Часть 5. Minidumps


DevDoc home page
 
   
 

Выпуск №7

Записывать и анализировать дампы памяти процесса (minidump) при возникновении сбоя проще, чем многие себе это представляют. При этом это очень мощный инструмент поиска ошибок, когда нет возможности запустить программу под отладчиком. Статья ниже, поможет Вам разобраться в тонкостях этого процесса.

Все материалы доступны на сайте http://www.devdoc.ru Наш девиз - новые статьи каждую неделю. Ресурс находится в постоянном развитии. Если у Вас есть интересный материал, вы можете опубликовать его на сайте.

Пожалуйста, присылайте свои вопросы и пожелания к темам статей на sub12@devdoc.ru

Если вам нравиться эта рассылка рекомендуйте ее своим друзьям. Подписаться можно по адресу http://subscribe.ru/catalog/comp.soft.prog.devdoc


Постоянная ссылка на статью: http://www.devdoc.ru/index.php/content/view/debugging_p5.htm

Автор: Кудинов Александр
Последняя модификация: 2007-02-09 22:23:22

Отладка приложений на C++. Часть 5

Введение

Когда мы говорим об отладке, подразумевается, что программист компилирует программу, запускает ее под отладчиком и пользуется теми методами, которые были рассмотрены в предыдущих частях. Это типичная ситуация, и я допускаю,что 90% всех задач сводится к этой схеме. Тем не менее, иногда возникают ситуации, когда запустить программу под отладчиком просто невозможно. Это может быть связано с тем, что программа находится у заказчика на другом конце света, или ошибка проявляется на каком-то одном компьютере, на котором нет среды разработки. Ситуация усугубляется тем, что зачастую почти невозможно получить описание от пользователя, в результате чего возникла ошибка. Ответ один: «я работал, а программа упала». Не рассчитывайте, что пользователь будет осваивать тестирование приложений, чтобы дать вам исчерпывающий отчет. Все не так уж и плохо, потому что в распоряжении профессиональных программистов есть пара трюков.

Первый - это протоколирование работы приложения. В следующей части этот метод будет описан детально и приведен один из вариантов решения.

Второй трюк, который можно применить,– это создание Crash dump. Это очень мощный инструмент, который позволяет находить места, в которых приложение падает.

Исключения

Вы наверняка знаете про исключения языка C++, которые реализуются блоками try/catch. Дополнительно существует еще один уровень обработки исключений, который поддерживается операционной системой. В Visual Studio этот механизм поддерживается через ключевые слова __try/ __except/__finally.

Использовать оба типа исключений одновременно в программе настоятельно не рекомендуется. Можете пропустить это мимо ушей, если точно знаете, что вы делаете.

При возникновении ошибочных ситуаций, таких, как деление на ноль, обращение по нулевому указателю и т.п. – возникает исключение. Ваша программа может иметь развитый механизм перехвата исключений и стратегию восстановления после сбоя. Однако большинство приложений не нуждается в такой сложной схеме, и такие исключения игнорируются.

Что это означает для пользователя? Все очень просто: если исключение не было перехвачено программой, то ОС сделает это за нее и покажет стандартное окно об ошибке. Это сообщение приводит пользователей программы в восторг, и они спешат поделиться своим счастьем со службой поддержки.

Что это означает для программиста? Тут тоже все очень просто. Он получит малоинформативное сообщение от пользователя и задание от руководства исправить ошибку за час. При этом нет ни информации о том, какие действия спровоцировали ошибку, ни о том, в каком месте программы она произошла.

Давайте подробнее рассмотрим, что происходит, когда ОС перехватывает необработанное исключение. Во-первых, система показывает диалог об ошибке. В Windows XP этот диалог содержит кнопку для отправки отчета разработчикам… Windows. Это Вам никак не поможет. Если согласиться на отправку отчета – система создаст специальный дамп файл, который содержит снимок памяти процесса и отправит его в Microsoft.

Хорошая новость в том, что файл с дампом имеет стандартный формат и может быть открыт с помощью MS Visual Studio .NET 2003 для анализа.

Minidump

Файл со снимком памяти процесса может содержать очень много ценной информации. Помимо всего содержимого памяти туда может входить информация об открытых дескрипторах, потоках, текущих счетчиках команд для каждого потока и т.п.

Т.е., имея такой файл, можно узнать не только адрес инструкции, на которой произошел сбой, но и содержимое всех переменных, а также стеков вызова для всех потоков.

Для того, чтобы полноценно использовать этот файл, Вам надо иметь символьную информацию для вашей программы. Если вы выполнили рекомендации Части 2, то у Вас наверняка есть такой файл, а также архив с исходными кодами для нужной версии программы.

Без символьной информации и исходников вы будете вынуждены ковыряться в ассемблерном коде. К таким радикальным мерам надо прибегать только в крайних случаях. В большинстве случаев будет быстрее найти ошибку, пользуясь другими методиками, даже если у вас нет дампа процесса.

Если все перечисленное у Вас в наличии – вы просто размещаете все файлы на нужные места:

  • Исходники в папку, из которой собирался проект.
  • Исполняемые файлы в те же папки, что и на компьютере пользователя

Потом Вы просто открываете дамп-файл с помощью Visual Studio и запускаете с помощью меню Debug->Start. Процесс похож на отладку приложения в обычном режиме, за исключением того, что отладчик сразу остановится в точке, в которой произошел сбой. А во-вторых, вы не сможете запустить программу на выполнение. Если с символьной информацией и исходниками все в порядке – вы увидите Ваш исходный код.

Давайте разберемся, можно ли заполучить этот замечательный источник информации об умершем процессе.

Как умирают процессы

Существует ли способ узнать, когда в приложении произошел сбой? Да! Более того, приложение может сделать это самостоятельно. Т.е. оно может предпринять некоторые действия для обработки ошибки.

Делается это не просто, а очень просто с помощью функции API SetUnhandledExceptionFilter.

LONG MiniDumper::TopLevelFilter( struct _EXCEPTION_POINTERS *pExceptionInfo )
{
  ...
}
 
void SetExceptionHook()
{
 ::SetUnhandledExceptionFilter( TopLevelFilter );
}

В качестве параметра SetUnhandledExceptionFilter указывается функция, которая будет вызываться каждый раз, как будет возникать необработанное исключение. Она будет вызываться до того, как ОС возьмет процесс управления исключением в свои руки. Эта функция называется фильтром, т.к. одновременно может быть установлено несколько перехватчиков, каждый из которых может отдать исключение следующему. Последним в цепочке стоит перехватчик ОС, который и показывает стандартное сообщение об ошибке. Перехват исключений происходит для всех потоков процесса.

Обратите внимание, что необработанные исключения будут перехватываться только если приложение не находится под отладчиком, т.к. отладчик имеет наивысший приоритет в обработке таких исключений.

Фильтр может вернуть одно из следующих значений в процессе своей работы:

  • EXCEPTION_EXECUTE_HANDLER – будет выполнена обработка по умолчанию. Обычно это приводит к завершению приложения.
  • EXCEPTION_CONTINUE_EXECUTION – программа продолжит свое выполнение с прерванного места. Фильтр может модифицировать информацию об исключении и попытаться сделать восстановление.
  • EXCEPTION_CONTINUE_SEARCH – будет сделана попытка найти следующий обработчик исключений. В результате пользователь может увидеть стандартное окно системы об ошибке.

Обратите внимание, что фильтр выполняется в контексте того потока, который вызвал исключение.

Используя собственный обработчик исключений, можно заменить стандартное сообщение Windows своим. Например, можно показать пользователю подсказку, как связаться с Вашей службой поддержки.

Наибольшего эффекта можно достичь, если предложить пользователю отправить автоматически подготовленный отчет об ошибке разработчикам. Такой отчет может содержать результат протоколирования работы программы, а также Crash Dump. О протоколировании читайте в следующей части.

Я постоянно рассказываю о файле с дампом памяти процесса, но пока ни слова не сказал о том, где его взять. Приступим!

Начиная с Windows XP и Windows Server 2003 появилась API функция MiniDumpWriteDump, которая умеет полностью формировать файл с дампом процесса. Функция находится в файле Dbghelp.dll, о котором я еще расскажу в последующем, т.к. внутри еще масса полезных функций. Сама библиотека не зависит от ОС, поэтому вы можете включить ее в поставку вашего приложения и пользоваться ей на более ранних версиях Windows.

Эту библиотеку лучше всего загружать динамически. Давайте рассмотрим пример, как это работает.

LONG TopLevelFilter( struct _EXCEPTION_POINTERS *pExceptionInfo )
{
 //по умолчанию завершим процесс стандартным образом
 LONG retval = EXCEPTION_CONTINUE_SEARCH;
 
 // Сначала попробуем загрузить библиотеку рядом с EXE, т.к.
 // в System32 может быть старая версия.
 HMODULE hDll = NULL;
 TCHAR szDbgHelpPath[_MAX_PATH];
 
 if (GetModuleFileName( NULL, szDbgHelpPath, _MAX_PATH ))
 {
  TCHAR *pSlash = _tcsrchr( szDbgHelpPath, '\\' );
  if (pSlash)
  {
   _tcscpy( pSlash+1, _T("DBGHELP.DLL") );
   hDll = ::LoadLibrary( szDbgHelpPath );
  }
 }
 
 if (hDll==NULL)
 {
  // Если загрузка не удалась - пробуем загрузить любую
  // доступную версию
  hDll = ::LoadLibrary( _T("DBGHELP.DLL") );
 }
 
 LPCTSTR szResult = NULL;
 
 if (hDll)
 {
  //если библиотека загружена - получаем адрес MiniDumpWriteDump()
  MINIDUMPWRITEDUMP pDump = (MINIDUMPWRITEDUMP)::GetProcAddress( hDll, "MiniDumpWriteDump" );
  if (pDump)
  {
   TCHAR szDumpPath[_MAX_PATH];
   TCHAR szScratch [_MAX_PATH];
 
   // будем записывать файл во временную папку
   if (!GetTempPath( _MAX_PATH, szDumpPath ))
    _tcscpy( szDumpPath, _T("c:\\temp\\") );
 
   _tcscat( szDumpPath, _T("MyApp") );
   _tcscat( szDumpPath, _T(".dmp") );
 
   // Сообщаем пользователю, что процесс на грани смерти и
   // предлагаем сохранить дамп.
   if (::MessageBox( NULL,
    _T("Приложение умирает, вы хотите записать диагностический файл для службы поддержки?"), 
    _T("MyApp"), MB_YESNO )==IDYES)
   {
    // Создать файл
    HANDLE hFile = ::CreateFile( szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, 
                                CREATE_ALWAYS,
           FILE_ATTRIBUTE_NORMAL, NULL );
 
    if (hFile!=INVALID_HANDLE_VALUE)
    {
     _MINIDUMP_EXCEPTION_INFORMATION ExInfo;
 
     ExInfo.ThreadId = ::GetCurrentThreadId();
     ExInfo.ExceptionPointers = pExceptionInfo;
     ExInfo.ClientPointers = NULL;
 
     // И записать в него дамп
     BOOL bOK = pDump( GetCurrentProcess(), GetCurrentProcessId(), 
                                        hFile, MiniDumpNormal, &ExInfo, NULL, NULL );
     if (bOK)
     {
      _stprintf( szScratch, _T("Файл сохранен в: '%s'"), szDumpPath );
      szResult = szScratch;
      retval = EXCEPTION_EXECUTE_HANDLER;
     }
     else
     {
      _stprintf( szScratch, _T("Ошибка сохранения '%s' (код %d)"), szDumpPath, GetLastError() );
      szResult = szScratch;
     }
     ::CloseHandle(hFile);
    }
    else
    {
     _stprintf( szScratch, _T("Ошибка создания диагностического файла '%s' (код %d)"), 
                                     szDumpPath, GetLastError() );
     szResult = szScratch;
    }
   }
  }
  else
  {
   szResult = _T("DBGHELP.DLL старая");
  }
 }
 else
 {
  szResult = _T("DBGHELP.DLL не найдена");
 }
 
 if (szResult)
  ::MessageBox( NULL, szResult, _T("MyApp"), MB_OK );
 
 return retval;
}

Вот собственно и все. С минимальными затратами мы получили в свои руки инструмент, который позволяет в разы увеличить эффективность поиска ошибок и уменьшить затраты на их исправление.

В заключение хочу сказать несколько слов о API функции MiniDumpWriteDump, которая выполняет большую часть работы. Она может записывать дамп только в уже созданный файл. Объем отладочной информации регулируется 4-м параметром. В примере он равен MiniDumpNormal. Его достаточно для захвата стеков вызова для всех потоков. В большинстве случаев этого вполне достаточно. Файл дампа при этом получается приемлемых размеров. Если указать параметр MiniDumpWithFullMemory, то в файл будет записана вся память процесса. Файлы при этом получаются очень большими и позволяют проводить самый детальный анализ. Я не рекомендую без надобности использовать этот режим, т.к. для пользователя может оказаться проблемой передать полученный файл через медленные каналы связи.

Заключение

Я сделал обзор только базовых вещей при формировании дампов процессов и перехвату исключений. Тема эта очень сложная и многогранная и физически невозможно уместить все в одну статью такого формата.

Напоследок хочу подкинуть пару идей, как можно развить эту идею.

  • Сделать автоматическую отправку отчетов по электронной почте разработчикам.
  • Если приведенный код обернуть в класс – можно существенно автоматизировать процесс подключения кода к новому проекту. Достаточно будет создать новый глобальный объект, чтобы конструктор класса выполнил всю инициализацию и установил обработчик исключений.
  • Написать код для сбора информации о системе: версию ОС, установленные сервис-паки, библиотеки и т.п.

Все свои отзывы и рекомендации присылайте на email: alexander {at} devdoc.ru или воспользуйтесь ссылкой на мой профиль вверху статьи.

Copyright (C) Kudinov Alexander, 2006-2007

Перепечатка материалов с данного сайта запрещена без писменного разрешения автора. При перепечатке обязательно указывать ссылку на оригинал.


В избранное