Pull to refresh

Введение в postmortem debugging

Reading time7 min
Views14K
Падение программы — это очень гадкая вещь. К сожалению все мы не идеальны и даже применяя наиболее безопасные методы разработки (например TDD) мы не застрахованы от того что программа свалится. Особенно плохо, если она свалится уже у заказчика. Но на пути к идеалу у нас всегда есть инструментарий, который может помочь расследовать падение программ, выявить ошибки, а главное исправить их.
Одно грустно, что многие, даже очень опытные разработчики не знакомы с этим инструментарием и многие компании не включают данную практику в свою работу. Я говорю о postmortem отладке.
В данной статье я хочу показать азы работы с данным зверем и возможно подтолкну этим самым разработчиков на расширение своих познаний в области оладки. Итак, приглашаются к чтению C++ Windows разработчики, тим-лиды, ну и руководителям отделов разработки будет неплохо ознакомиться.


Ломаем программу


Начнем с того что напишем маленькую программу которая свалится при попытке её запустить,
для этого используем VS2008 и создадим консольное приложение на C++ со следующим кодом:
#include "stdafx.h"

int _tmain(int argc, _TCHAR* argv[])
{
  int* p = NULL;
  p[0] = 10;
  return 0;
}


* This source code was highlighted with Source Code Highlighter.

как видите, мы создали пустой указатель и пытаемся туда запихать значение. Скомпилируем программу в конфигурации Release и запустим её из-под проводника. Мы увидим что-то типа такого:
image
Если мы нажмем кнопку «Отладить программу», то с высокой долей вероятности поднимется отладчик и покажет вам место, где произошла ошибка. Данный проект был с дефолтными настройками, по этому у меня поднялась студия и показала в исходном коде место ошибки.
Если вы по какой-то причине не знаете, как это студии удалось сделать, то загляните в настройки проекта:
image
По умолчанию были установлены подсвеченые значения. Это значит, что вместе с программой создается pdb файл содержащий отладочную информацию о нашей программе. Благодаря тому, что мы запустили программу на том-же компьютере, где её и написали, при отладке Visual Studio подцепил этот pdb файл, и по хранящимся в нем путям к исходникам смог их открыть.Далее Visual Studio развернула нам стек падения и показала строку кода, которая вызвала исключительную ситуацию. А если падение произошло на чужом компьютере, где нет ни pdb-файла, ни Visual Studio, ни исходников? В этом случае мы сами должны отловить исключительную ситуацию и собрать все возможные данные о том, из-за чего она возникла.

Устанавливаем обработчик unhandled исключений


Перепишем программу следующим образом:
#include "stdafx.h"
#include <windows.h>
#include <io.h>

LONG WINAPI CustomUnhandledExceptionFilter( PEXCEPTION_POINTERS pExInfo )
{
  _tprintf( TEXT( "Exception!" ) );
  return EXCEPTION_EXECUTE_HANDLER;
}

int _tmain(int argc, _TCHAR* argv[])
{
  LPTOP_LEVEL_EXCEPTION_FILTER hOldFilter = SetUnhandledExceptionFilter( CustomUnhandledExceptionFilter );

  int* p = NULL;
  p[0] = 10;

  SetUnhandledExceptionFilter( hOldFilter );

  return 0;
}


* This source code was highlighted with Source Code Highlighter.

Как видим, мы тут добавили функцию CustomUnhandledExceptionFilter, которая всего лишь печатает в консоль слово «Exception!». А в основной функции мы устанавливаем фильтр необрабатываемых исключений вначале и возвращаем на место старый в конце.
Компилируем программу, запускаем… всё! больше нет сообщений об ошибке. Теперь в консоль только выводится слово «Exception!».
Перейдем к следующему шагу, сохранинии информации о падении.

Да будет свет DbgHelp.lib


Вот теперь начинается самое интересное. Идем на сайт www.microsoft.com/whdc/DevTools/Debugging/default.mspx
и скачиваем оттуда Debugging Tools For Windows для вашей системы. Это инсталлятор отладчика WinDBG и отладочного SDK.
Устанавливаем всё это на компьютер и следим за тем, чтобы в студии оказались прописаны пути к SDK который шел в Debugging Tools For Windows.

Далее в зависимости проекта прописываем библиотеку DbgHelp.lib и модифицируем функцию CustomUnhandledExceptionFilter следующим образом:
#include <dbghelp.h>

LONG WINAPI CustomUnhandledExceptionFilter( PEXCEPTION_POINTERS pExInfo )
{
  HANDLE hFile;

  hFile = CreateFile( TEXT("c:\\minidump.dmp"), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );

  if( NULL == hFile || INVALID_HANDLE_VALUE == hFile )
     return EXCEPTION_EXECUTE_HANDLER;

  MINIDUMP_EXCEPTION_INFORMATION eInfo;
  eInfo.ThreadId = GetCurrentThreadId();
  eInfo.ExceptionPointers = pExInfo;
  eInfo.ClientPointers = FALSE;

  MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), hFile,
            MiniDumpNormal, &eInfo, NULL, NULL);
  CloseHandle( hFile );

  return EXCEPTION_EXECUTE_HANDLER;
}


* This source code was highlighted with Source Code Highlighter.

Здесь мы подключили заголовочный файл dbghelp.h и сделали вызов функции MinidumpWriteDump для сохранения дампа падения приложения.
Остается только скопировать файл dbghelp.dll из папки Debugging Tools For Windows в папку Release нашего приложения и запустить его. На диске «c:\» появится файл minidump.dmp — это трупик нашего приложения, будем его препарировать.

Паталогоанатомия приложения


Запустим отладчик WinDbg из пакета Debugging Tools For Windows. Идем в меню File->Open Crash Dump… открываем файл minidump.dmp и отрицательно отвечаем на вопрос о сохранении рабочего пространства. Далее идем в меню File->Symbol File Path… там вводим путь к папке Release нашего проекта, аналогичный путь вводим в окне File->Image File Path… В окне File->Source File Path… прописываем папку с исходниками нашего проекта. Вот мы и готовы к препарированию.
И пишем в командной строке отладчика заветную команду: !analyze -v
и через пару секунд получим листинг содержащий что-то типа такого:

FAULTING_IP:
CrashHandler!wmain+11 [d:\work\crashhandler\crashhandler.cpp @ 35]
00401091 c7010a000000 mov dword ptr [ecx],0Ah

EXCEPTION_RECORD: ffffffff — (.exr 0xffffffffffffffff)
ExceptionAddress: 00401091 (CrashHandler!wmain+0x00000011)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00000000
Attempt to write to address 00000000

PROCESS_NAME: CrashHandler.exe

ADDITIONAL_DEBUG_TEXT:
Use '!findthebuild' command to search for the target build information.
If the build information is available, run '!findthebuild -s; .reload' to set symbol path and load symbols.

FAULTING_MODULE: 75bf0000 kernel32

DEBUG_FLR_IMAGE_TIMESTAMP: 4bb07e29

MODULE_NAME: CrashHandler

ERROR_CODE: (NTSTATUS) 0xc0000005 — EXCEPTION_CODE: (NTSTATUS) 0xc0000005 — EXCEPTION_PARAMETER1: 00000001

EXCEPTION_PARAMETER2: 00000000

WRITE_ADDRESS: 00000000

FOLLOWUP_IP:
CrashHandler!wmain+11 [d:\work\crashhandler\crashhandler.cpp @ 35]
00401091 c7010a000000 mov dword ptr [ecx],0Ah

FAULTING_THREAD: 0000109c

BUGCHECK_STR: APPLICATION_FAULT_NULL_POINTER_WRITE_WRONG_SYMBOLS

PRIMARY_PROBLEM_CLASS: NULL_POINTER_WRITE

DEFAULT_BUCKET_ID: NULL_POINTER_WRITE

LAST_CONTROL_TRANSFER: from 0040120d to 00401091

STACK_TEXT:
0018ff44 0040120d 00000001 003c2dc0 003c3c90 CrashHandler!wmain+0x11 [d:\work\crashhandler\crashhandler.cpp @ 35]
0018ff88 75c03677 7efde000 0018ffd4 773b9d72 CrashHandler!__tmainCRTStartup+0x10f [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 583]
WARNING: Stack unwind information not available. Following frames may be wrong.
0018ff94 773b9d72 7efde000 6819a792 00000000 kernel32!BaseThreadInitThunk+0x12
0018ffd4 773b9d45 00401355 7efde000 00000000 ntdll!RtlInitializeExceptionChain+0x63
0018ffec 00000000 00401355 7efde000 00000000 ntdll!RtlInitializeExceptionChain+0x36

STACK_COMMAND: ~0s; .ecxr; kb

FAULTING_SOURCE_CODE:
31: int* p = NULL;
32: p[0] = 10;
33:
34: SetUnhandledExceptionFilter( hOldFilter );
> 35:
36: return 0;
37: }
38:

SYMBOL_STACK_INDEX: 0

SYMBOL_NAME: CrashHandler!wmain+11

FOLLOWUP_NAME: MachineOwner

IMAGE_NAME: CrashHandler.exe

BUCKET_ID: WRONG_SYMBOLS

FAILURE_BUCKET_ID: NULL_POINTER_WRITE_c0000005_CrashHandler.exe!wmain


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

Теперь вы можете взять exe-шник вашей программы и файл dbghelp.dll, перенести его на другой комьютер, там запустить. Взять оттуда файл minidump.dmp и опять разобрать этот файл на своем рабочем компьютере. И результат будет тот-же. Это и есть postmortem debugging.
В следующем разделе я дам немного советов.

Дальнейшее развитие

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

  1. Здравый смысл нам диктует, что непосредственно обработчик unhandled исключений следует вынести в отдельную dll для повторного испрользования в ваших программах. Для регистрации обработчика хорошо бы сделать класс, экземпляр которого создается на стеке в точке входа вашего приложения (тогда в конструкторе мы сможем установить свой обработчик, а в деструкторе убрать
  2. Помимо дампа можно создавать ещё и файл с информацие об исключении вручную разбирая структуры данных в колбеке функции DumpWriteDump (почитайте хелп к этой функции для ясности). И вообще вы можете даже прикрутить к сборщику дампов архиватор, чтобы всё пожать, но не забудте вставить в обработчик защиту от реентерабильности (на случай если исключение у вас возникнет во время обработки исключения)
  3. Для получения достоверных данных о стеке падения нам необходимо, чтобы от всех модулей, которые в были загружены приложением, у нас были отладочные символы. Для этого следует развернуть сервер отладочной информации, о том как это сделать я писал в своей статье: habrahabr.ru/blogs/development/89094/#habracut
  4. Библиотека dbghelp.dbg с давних пор идет в комплекте с Windows, с Visual Studio и т.п. поэтому, чтобы не городить dll-hell, распространяйте её вместе со своим приложением и подгружайте именно нужный вам экземпляр.


Куда копать дальше

Если данная статья вам понравилась и вы захотели взять на вооружение Postmortem Debugging то вам следует изучить следующее:
  • Хелп от Debugging Tools For Windows, от SDK и от отладчика WinDbg
  • Блог Олега Стародумова, debuginfo.com, там есть полезные статьи и утилиты.
  • Посмотрите старую, но очень полезную реализацию библиотеки сбора дампов www.codeproject.com/KB/debug/XCrashReportPt1.aspx (там 4 части)
  • Читайте книги Джона Роббинса по отладке, а также его блог (его ник Bugslayer).

Ну и фантазируйте. Например один мой бывший коллега — очень грамотный специалист, сделал библиотеку которая собирала дамы, отсылала их на специальный почтовый ящик компании. На этом почтовом ящике висел робот, который разбирал дампы, искал в стеке знакомые компоненты и пересылал письма с дампами программистам, ответственным за эти модули. Если не ошибаюсь по этой методе он даже писал статью в RSDN Journal.

Удачи и безбажного кода вам, если возникли вопросы — задавайте, подискутируем.
Tags:
Hubs:
Total votes 15: ↑12 and ↓3+9
Comments18

Articles