четверг, 13 марта 2008 г.

Post mortem

Этот пост начинает серию о диагностике windows приложений. Традиционно, для того что-бы найти ошибки в работающей программе, используются лог файлы. Программа пишет себе в лог, а в случае возникновения проблем разработчик по логам может найти баг. Здесь есть несколько проблем, главная - нельзя писать много, во первых сложно анализировать, во вторых возникает оверхед, особенно, когда одновременно работает множество программ работающих с диском... Ну и конечно проблема с тем, что писать, а что нет. Значения переменных, вызовы функций, все это записывается только за тем, что-бы в случае возникновения проблем, можно бы было приблизительно восстановить состояние стека. Есть еще один путь, использование средств ОС для диагностирования программы. Самое вкусное, на мой взгляд, отладочный дамп. Для создания дампа нужно использовать средства библиотеки DbgHelp. А именно функцию MiniDumpWriteDump. Функция получает на вход структуру EXCEPTION_POINTERS, которая содержит контекст в котором произошло исключение. Эту структуру в принципе можно получить при обработке исключения, с помощю функции GetExceptionInformation (эту функцию можно вызывать только внутри filter expression(__except) обработчика исключения), а так-же при срабатывании фильтра необработанных исключений (поставить можно функцией SetUnhandledExceptionFilter). Так же функция принимает хэндл открытого для записи файла, идентификатор и хэндл процесса. В опциях проекта нужно сказать компоновщику, что нужно генерировать отладочную информацию (указать *.pdb файл). код:
LONG WINAPI UExceptFilter(_EXCEPTION_POINTERS* ExceptionInfo)
{
    char block[_MAX_PATH];
    SYSTEMTIME st;
    GetLocalTime(&st);
    _snprintf(block, _MAX_PATH, "CrashDump-%04d%02d%02d%02d%02d%02d%03d.dmp", 
        st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
    HANDLE hProc = GetCurrentProcess();
    HANDLE hFile = CreateFile(
                        block, 
                        GENERIC_READ|GENERIC_WRITE, 
                        FILE_SHARE_READ, 
                        NULL,
                        CREATE_ALWAYS, 
                        FILE_ATTRIBUTE_NORMAL, 
                        NULL
                        );
    MINIDUMP_EXCEPTION_INFORMATION eInfo;
    eInfo.ExceptionPointers = ExceptionInfo;
    eInfo.ThreadId = ::GetCurrentThreadId();
    eInfo.ClientPointers = NULL;
    BOOL ret = MiniDumpWriteDump(    
                        hProc, 
                        GetProcessId(hProc), 
                        hFile,
                        MiniDumpWithFullMemory,
                        ExceptionInfo ? &eInfo : NULL,
                        NULL,NULL
                );
    CloseHandle(hFile);
    return EXCEPTION_CONTINUE_SEARCH;
} 

int _tmain(int argc, _TCHAR* argv[])
{
    SetUnhandledExceptionFilter(&UExceptFilter);
    //.........
}
Далее, после возникновения ошибки, просим клиента отправить нам отладочный дамп, можно это дело автоматизировать, и отправлять средствами самой программы. Потом открываем его студией (OpenSolution, потом выбираем тип файла memory dump), и запускаем отладчик. Нужно что-бы в папке с дампом находился исполняемый файл, файл с отладочной информацией (*.pdb), и исходники - все это должно быть той-же версии что и экзешник записавший дамп, желательно, что-бы версии используемых библиотек так-же совпадали (хотя есть подозрение что будет работать в любом случае). После запуска можно будет в окне call stack просматривать состояние стека так, как-будто запущена программа, можно так-же смотреть значения локальных переменных. Но есть одна проблема, место возникновения исключения нельзя узнать точно из за оптимизаций компилятора, который может инлайнить многие функции. Самое сложное в этом - хранить исходники, exe и pdb файлы для каждого релиза :)