Зачем и для чего


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


Перехват исключений


Для того, чтобы программа перед падением могла сообщить нам (вне отладчика) в чём проблема, можно воспользоваться функцией обработки сигналов.


Несколько примеров таких сигналов:


    signal(SIGABRT, abort_handler); // Аварийное завершение, стандартные исключения с nullptr и прочим 
    signal(SIGFPE, floating_point_handler); // Ошибки арифметики с плавающей запятой 
    signal(SIGILL, illegal_instruction_handler); // Ошибки при работе с intrinsics (SSE/AVX/Other) 

Обработчики сигналов



inline void abort_handler(int signal)
{
    handler_base("Application is aborting");
}

inline void floating_point_handler(int signal)
{
    handler_base("Floating point error");
}

inline void illegal_instruction_handler(int signal)
{
    if (!CPU::Info.hasFeature(CPUFeature::SSE42))
        handler_base("SSE4.2 and AVX instructions isn't legal on your CPU");
    else if (!CPU::Info.hasFeature(CPUFeature::AVX)) 
        handler_base("AVX instructions isn't legal on your CPU");
    else
        handler_base("Illegal instruction");
}

Так же есть обработчик недопустимых параметров (_set_invalid_parameter_handler), некорректного выделения памяти (_set_new_handler) и подобные им.


Проблемное место: Получение callstack'a


Помимо самого исключения, нужно узнать, где именно проблема. К примеру, CallStack. Для этого можно воспользоваться готовым решением: StackWalker


Проблемное место: Получения dump-файла


Если вам нужно получить более детальную информацию о проблеме, то следует сгенерировать Dump файл.


using MINIDUMPWRITEDUMP = BOOL(WINAPI*)(HANDLE hProcess, DWORD dwPid, HANDLE hFile, MINIDUMP_TYPE DumpType,
    CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
    CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
    CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam
    );

LONG WriteMinidump(struct _EXCEPTION_POINTERS* pExceptionInfo)
{
    long retval = EXCEPTION_CONTINUE_SEARCH;

    // firstly see if dbghelp.dll is around and has the function we need
    // look next to the EXE first, as the one in System32 might be old
    // (e.g. Windows 2000)
    HMODULE hDll = nullptr;
    string_path szDbgHelpPath;

    if (GetModuleFileName(nullptr, szDbgHelpPath, _MAX_PATH))
    {
        char* pSlash = strchr(szDbgHelpPath, '\\');
        if (pSlash)
        {
            const char dbgHelpStr[] = "DBGHELP.DLL";
            xr_strcpy(pSlash + 1, sizeof(dbgHelpStr), dbgHelpStr);
            hDll = LoadLibraryA(szDbgHelpPath);
        }
    }

    if (!hDll)
    {
        // Загружаем dbgHelp 
        hDll = LoadLibraryA("DBGHELP.DLL");
    }

    LPCSTR szResult = nullptr;

    if (hDll)
    {
        MINIDUMPWRITEDUMP pDump = (MINIDUMPWRITEDUMP)::GetProcAddress(hDll, "MiniDumpWriteDump");
        if (pDump)
        {
            string512 ErrorString = { NULL };
            DWORD ErrorSysCode = NULL;
            DWORD ErrorStringSize = NULL;

            string_path szDumpPath = { 0 };
            string_path szFilename = { 0 };
            string_path szScratch = { 0 };
            string64 t_stemp = { 0 };

            timestamp(t_stemp);
            xr_strcat(szDumpPath, DumpFilePath);

            // Генерируем название файла : App Name, User Name, Time, Ext
            xr_strcat(szFilename, Core.ApplicationName);
            xr_strcat(szFilename, "_");
            xr_strcat(szFilename, Core.UserName);
            xr_strcat(szFilename, "_");
            xr_strcat(szFilename, t_stemp);
            xr_strcat(szFilename, ".mdmp");

            xr_strcat(szDumpPath, szFilename);

            // Создаём файл
            HANDLE hFile = CreateFileA(szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
            if (INVALID_HANDLE_VALUE == hFile)
            {
                // Попробуем записать в директорию, из которой запущено приложение
                ZeroMemory(szDumpPath, sizeof(szDumpPath));
                xr_strcat(szDumpPath, szFilename);
                hFile = CreateFileA(szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
            }
            if (hFile != INVALID_HANDLE_VALUE)
            {
                _MINIDUMP_EXCEPTION_INFORMATION ExInfo;

                ExInfo.ThreadId = GetCurrentThreadId();
                ExInfo.ExceptionPointers = pExceptionInfo;
                ExInfo.ClientPointers = NULL;

                // Записываем Dump 
                MINIDUMP_TYPE dump_flags = MINIDUMP_TYPE(MiniDumpNormal | MiniDumpFilterMemory | MiniDumpScanMemory);

                // Сообщение ошибки 
                char* logFileContent = nullptr;
                DWORD logFileContentSize = 0;

/////////////////////////////////////
                // Вставьте ваш код генерации сообщения об ошибке (logFileContent)
/////////////////////////////////////

        MINIDUMP_USER_STREAM_INFORMATION UserStreamsInfo = { 0 };
                MINIDUMP_USER_STREAM LogFileUserStream = { 0 };

                if (logFileContent)
                {
                    UserStreamsInfo.UserStreamCount = 1;
                    LogFileUserStream.Buffer = logFileContent; // Передаём ваше сообщение
                    LogFileUserStream.BufferSize = logFileContentSize; // Размер сообщения
                    LogFileUserStream.Type = MINIDUMP_STREAM_TYPE::CommentStreamA; // Тип
                    UserStreamsInfo.UserStreamArray = &LogFileUserStream;
                }

                BOOL bOK = pDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, dump_flags, &ExInfo, &UserStreamsInfo, nullptr);
                if (bOK)
                {
                    xr_sprintf(szScratch, "Saved dump file to '%s'", szDumpPath);
                    szResult = szScratch;
                    retval = EXCEPTION_EXECUTE_HANDLER;
                }
                else
                {
                    ErrorSysCode = GetLastError();
                    ErrorStringSize = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, ErrorSysCode, 0, ErrorString, sizeof(ErrorString) - 1, nullptr);

                    if (!!ErrorString && ErrorSysCode && ErrorStringSize)
                        xr_sprintf(szScratch, "Failed to save dump file to '%s' (error %d '%s')", szDumpPath, ErrorSysCode, ErrorString);
                    else
                        xr_sprintf(szScratch, "Failed to save dump file to '%s' (No system error)", szDumpPath);
                    szResult = szScratch;

                }
                CloseHandle(hFile);
            }
            else
            {
                ErrorSysCode = GetLastError();
                ErrorStringSize = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, ErrorSysCode, 0, ErrorString, sizeof(ErrorString) - 1, nullptr);

                if (!!ErrorString && ErrorSysCode && ErrorStringSize)
                    xr_sprintf(szScratch, "Failed to create dump file '%s' (error %d '%s')", szDumpPath, ErrorSysCode, ErrorString);
                else
                    xr_sprintf(szScratch, "Failed to create dump file '%s' (No system error)", szDumpPath);
                szResult = szScratch;
            }
        }
        else
            szResult = "DBGHELP.DLL too old";
    }
    else
        szResult = "DBGHELP.DLL not found";

    return retval;
}

Сообщим пользователю о проблеме


Для вывода сообщений о проблемах, как исключений, так и внутренних (скриптов, конфигов, неправильного поведения программы), мы написали небольшую утилиту, которая выглядит следующим образом:
image
Написана она на C# + WPF, исходный код доступен


Функционал достаточно примитивен:


  • Продолжить выполнение программы (если это внутренняя ошибка)
  • Сообщить о проблеме (стек улетает в буфер обмена, пользователь переходит на сервер дискорда в нужный топик)
  • Прервать выполнение программы