Падение программы — это очень гадкая вещь. К сожалению все мы не идеальны и даже применяя наиболее безопасные методы разработки (например TDD) мы не застрахованы от того что программа свалится. Особенно плохо, если она свалится уже у заказчика. Но на пути к идеалу у нас всегда есть инструментарий, который может помочь расследовать падение программ, выявить ошибки, а главное исправить их.
Одно грустно, что многие, даже очень опытные разработчики не знакомы с этим инструментарием и многие компании не включают данную практику в свою работу. Я говорю о postmortem отладке.
В данной статье я хочу показать азы работы с данным зверем и возможно подтолкну этим самым разработчиков на расширение своих познаний в области оладки. Итак, приглашаются к чтению C++ Windows разработчики, тим-лиды, ну и руководителям отделов разработки будет неплохо ознакомиться.
Начнем с того что напишем маленькую программу которая свалится при попытке её запустить,
для этого используем VS2008 и создадим консольное приложение на C++ со следующим кодом:
как видите, мы создали пустой указатель и пытаемся туда запихать значение. Скомпилируем программу в конфигурации Release и запустим её из-под проводника. Мы увидим что-то типа такого:
Если мы нажмем кнопку «Отладить программу», то с высокой долей вероятности поднимется отладчик и покажет вам место, где произошла ошибка. Данный проект был с дефолтными настройками, по этому у меня поднялась студия и показала в исходном коде место ошибки.
Если вы по какой-то причине не знаете, как это студии удалось сделать, то загляните в настройки проекта:
По умолчанию были установлены подсвеченые значения. Это значит, что вместе с программой создается pdb файл содержащий отладочную информацию о нашей программе. Благодаря тому, что мы запустили программу на том-же компьютере, где её и написали, при отладке Visual Studio подцепил этот pdb файл, и по хранящимся в нем путям к исходникам смог их открыть.Далее Visual Studio развернула нам стек падения и показала строку кода, которая вызвала исключительную ситуацию. А если падение произошло на чужом компьютере, где нет ни pdb-файла, ни Visual Studio, ни исходников? В этом случае мы сами должны отловить исключительную ситуацию и собрать все возможные данные о том, из-за чего она возникла.
Перепишем программу следующим образом:
Как видим, мы тут добавили функцию CustomUnhandledExceptionFilter, которая всего лишь печатает в консоль слово «Exception!». А в основной функции мы устанавливаем фильтр необрабатываемых исключений вначале и возвращаем на место старый в конце.
Компилируем программу, запускаем… всё! больше нет сообщений об ошибке. Теперь в консоль только выводится слово «Exception!».
Перейдем к следующему шагу, сохранинии информации о падении.
Да будет
Вот теперь начинается самое интересное. Идем на сайт www.microsoft.com/whdc/DevTools/Debugging/default.mspx
и скачиваем оттуда Debugging Tools For Windows для вашей системы. Это инсталлятор отладчика WinDBG и отладочного SDK.
Устанавливаем всё это на компьютер и следим за тем, чтобы в студии оказались прописаны пути к SDK который шел в Debugging Tools For Windows.
Далее в зависимости проекта прописываем библиотеку DbgHelp.lib и модифицируем функцию CustomUnhandledExceptionFilter следующим образом:
Здесь мы подключили заголовочный файл 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… прописываем папку с исходниками нашего проекта. Вот мы и готовы к препарированию.
И пишем в командной строке отладчика заветную команду:
и через пару секунд получим листинг содержащий что-то типа такого:
Одно грустно, что многие, даже очень опытные разработчики не знакомы с этим инструментарием и многие компании не включают данную практику в свою работу. Я говорю о 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 и запустим её из-под проводника. Мы увидим что-то типа такого:
Если мы нажмем кнопку «Отладить программу», то с высокой долей вероятности поднимется отладчик и покажет вам место, где произошла ошибка. Данный проект был с дефолтными настройками, по этому у меня поднялась студия и показала в исходном коде место ошибки.
Если вы по какой-то причине не знаете, как это студии удалось сделать, то загляните в настройки проекта:
По умолчанию были установлены подсвеченые значения. Это значит, что вместе с программой создается 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.
В следующем разделе я дам немного советов.
Дальнейшее развитие
В вышестоящем примере мы сделали примитивную программу, которая при падении собирает свой дамп и смогли этот дамп разобрать, чтобы получить стек падения. А теперь несколько здравых мыслей по организации работы с подобного рода вещами.
- Здравый смысл нам диктует, что непосредственно обработчик unhandled исключений следует вынести в отдельную dll для повторного испрользования в ваших программах. Для регистрации обработчика хорошо бы сделать класс, экземпляр которого создается на стеке в точке входа вашего приложения (тогда в конструкторе мы сможем установить свой обработчик, а в деструкторе убрать
- Помимо дампа можно создавать ещё и файл с информацие об исключении вручную разбирая структуры данных в колбеке функции DumpWriteDump (почитайте хелп к этой функции для ясности). И вообще вы можете даже прикрутить к сборщику дампов архиватор, чтобы всё пожать, но не забудте вставить в обработчик защиту от реентерабильности (на случай если исключение у вас возникнет во время обработки исключения)
- Для получения достоверных данных о стеке падения нам необходимо, чтобы от всех модулей, которые в были загружены приложением, у нас были отладочные символы. Для этого следует развернуть сервер отладочной информации, о том как это сделать я писал в своей статье: habrahabr.ru/blogs/development/89094/#habracut
- Библиотека 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.
Удачи и безбажного кода вам, если возникли вопросы — задавайте, подискутируем.