Cómo manejar Win32 Unhandled Exception en Release

Probablemente se hayan topado con este escenario: una aplicación Windows, que usa C++ nativo/no manejado, funciona perfecto en la máquina de desarrollo. Pero en las máquinas de los clientes a veces sale un message box similar al siguiente:

A veces, esa excepción puede capturarse con un bloque try-catch. Pero en ciertos casos como Access violation, división por cero y similares, no se puede capturar. Una alternativa específica de Windows es usar un bloque __try - __except, pero tiene sus limitaciones. Concretamente, sólo puede capturar excepciones de tipo SEH. Y las mismas deben estar habilitadas a nivel proyecto.

Una mejor alternativa, que es la que estudiaremos a continuación, consiste en usar la función SetUnhandledExceptionFilter de la biblioteca Kernel32.lib. Esto nos permite capturar la excepción, ¿pero cómo obtener más información? Es posible obtener un stack trace usando unas funciones auxiliares de las bibliotecas psapi.lib y dbghlp.lib. De este modo:

Ejemplo de uso (StackTracing es un namespace, no una clase):

void Init(){
   // ...
   // Registro handler
   ::SetUnhandledExceptionFilter(OnUnhandledException);
   // ...
}

// Defino handler (disculpen el Singleton, es código legacy)
LONG WINAPI OnUnhandledException(  _In_  struct _EXCEPTION_POINTERS *pExceptionInfo ){
   string stackTrace;
   LONG result = StackTracing::DumpStackTrace( pExceptionInfo, stackTrace );
   ::GetErrorLoggerInstance()->Log(
      LOG_TYPE_ERROR,
      "StackTracing",
      "OnUnhandledException",
      stackTrace
   );
   ::MessageBoxA(
      NULL,
      "Fatal error encountered, application will shut down. Please check log for more information.",
      "DRA", MB_OK
   );
   return result;
}
Implementación de las funciones definidas arriba:
// Obtengo stack trace
#include <windows.h>
#include <string>
#include <sstream>
#include <vector>
#include <Psapi.h>
#include <algorithm>
#include <iterator>

#pragma comment(lib, "psapi.lib")
#pragma comment(lib, "dbghelp.lib")

// Some versions of imagehlp.dll lack the proper packing directives themselves
// so we need to do it.
#pragma pack( push, before_imagehlp, 8 )
#include <imagehlp.h>
#pragma pack( pop, before_imagehlp )

struct module_data {
    std::string image_name;
    std::string module_name;
    void *base_address;
    DWORD load_size;
};

// Reemplazar con tu propia función
DWORD DumpStackTrace( EXCEPTION_POINTERS *ep );
extern void YourMessage(const char* title, const char *fmt, ...);

class symbol { 
    typedef IMAGEHLP_SYMBOL64 sym_type;
    sym_type *sym;
    static const int max_name_len = 1024;

public:
    symbol(HANDLE process, DWORD64 address) : sym((sym_type *)::
    operator new(sizeof(*sym) + max_name_len)) {
        memset(sym, '\0', sizeof(*sym) + max_name_len);
        sym->SizeOfStruct = sizeof(*sym);
        sym->MaxNameLength = max_name_len;
        DWORD64 displacement;

        if (!SymGetSymFromAddr64(process, address, &displacement, sym))
            throw(std::logic_error("Bad symbol"));
    }

    std::string name() { return std::string(sym->Name); }
    std::string undecorated_name() { 
        std::vector<char> und_name(max_name_len);
        UnDecorateSymbolName(sym->Name, &und_name[0], max_name_len, UNDNAME_COMPLETE);
        return std::string(&und_name[0], strlen(&und_name[0]));
    }
};


class get_mod_info {
    HANDLE process;
    static const int buffer_length = 4096;
public:
    get_mod_info(HANDLE h) : process(h) {}

    module_data operator()(HMODULE module) { 
        module_data ret;
        char temp[buffer_length];
        MODULEINFO mi;

        GetModuleInformation(process, module, &mi, sizeof(mi));
        ret.base_address = mi.lpBaseOfDll;
        ret.load_size = mi.SizeOfImage;

        GetModuleFileNameExA(process, module, temp, sizeof(temp));
        ret.image_name = temp;
        GetModuleBaseNameA(process, module, temp, sizeof(temp));
        ret.module_name = temp;
        std::vector<char> img(ret.image_name.begin(), ret.image_name.end());
        std::vector<char> mod(ret.module_name.begin(), ret.module_name.end());
        SymLoadModule64(process, 0, &img[0], &mod[0], (DWORD64)ret.base_address, ret.load_size);
        return ret;
    }
};


// if you use C++ exception handling: install a translator function
// with set_se_translator(). In the context of that function (but *not*
// afterwards), you can either do your stack dump, or save the CONTEXT
// record as a local copy. Note that you must do the stack dump at the
// earliest opportunity, to avoid the interesting stack-frames being gone
// by the time you do the dump.
DWORD StackTracing::DumpStackTrace( EXCEPTION_POINTERS *ep, std::string& stackTrace ){
    HANDLE process = GetCurrentProcess();
    HANDLE hThread = GetCurrentThread();
    int frame_number=0;
    DWORD offset_from_symbol=0;
    IMAGEHLP_LINE64 line = {0};
    std::vector<module_data> modules;
    DWORD cbNeeded;
    std::vector<HMODULE> module_handles(1);

    // Load the symbols:
    if (!SymInitialize(process, NULL, false)) 
        throw(std::logic_error("Unable to initialize symbol handler"));
    DWORD symOptions = SymGetOptions();
    symOptions |= SYMOPT_LOAD_LINES | SYMOPT_UNDNAME;
    SymSetOptions(symOptions);
    EnumProcessModules(
       process,
       &module_handles[0],
       module_handles.size() * sizeof(HMODULE), &cbNeeded
    );
    module_handles.resize(cbNeeded/sizeof(HMODULE));
    EnumProcessModules(
       process,
       &module_handles[0],
       module_handles.size() * sizeof(HMODULE), &cbNeeded
    );
    std::transform(
        module_handles.begin(), module_handles.end(),
        std::back_inserter(modules), get_mod_info(process)
    );
    void *base = modules[0].base_address;

    // Setup stuff:
    CONTEXT* context = ep->ContextRecord;
#ifdef _M_X64
    STACKFRAME64 frame;
    frame.AddrPC.Offset = context->Rip;
    frame.AddrPC.Mode = AddrModeFlat;
    frame.AddrStack.Offset = context->Rsp;
    frame.AddrStack.Mode = AddrModeFlat;
    frame.AddrFrame.Offset = context->Rbp;
    frame.AddrFrame.Mode = AddrModeFlat;
#else
    STACKFRAME64 frame;
    frame.AddrPC.Offset = context->Eip;
    frame.AddrPC.Mode = AddrModeFlat;
    frame.AddrStack.Offset = context->Esp;
    frame.AddrStack.Mode = AddrModeFlat;
    frame.AddrFrame.Offset = context->Ebp;
    frame.AddrFrame.Mode = AddrModeFlat;
#endif
    line.SizeOfStruct = sizeof line;
    IMAGE_NT_HEADERS *h = ImageNtHeader(base);
    DWORD image_type = h->FileHeader.Machine;
    int n = 0;

    // Build the string:
    std::ostringstream builder;
    do {
        if ( frame.AddrPC.Offset != 0 ) {
            std::string fnName = symbol(process, frame.AddrPC.Offset).undecorated_name();
            builder << fnName;
            if (SymGetLineFromAddr64( process, frame.AddrPC.Offset, &offset_from_symbol, &line)) 
                builder << "  " /*<< line.FileName*/ << "(" << line.LineNumber << ")\n";
            else builder << "\n";
            if (fnName == "main")
                break;
            if (fnName == "RaiseException") {
                // This is what we get when compiled in Release mode:
                //YourMessage("Crash", "Your program has crashed.\n\n");
                return EXCEPTION_EXECUTE_HANDLER;
            }
        }
        else
            builder << "(No Symbols: PC == 0)";
        if (!StackWalk64(image_type, process, hThread, &frame, context, NULL, 
                            SymFunctionTableAccess64, SymGetModuleBase64, NULL))
            break;
        if (++n > 10)
            break;
    } while (frame.AddrReturn.Offset != 0);
    //return EXCEPTION_EXECUTE_HANDLER;
    SymCleanup(process);

    // Display the string:
    //::MessageBoxA( NULL, builder.str().c_str(), "Stack Trace", MB_OK );
    stackTrace = builder.str();
    return EXCEPTION_EXECUTE_HANDLER;
}

Comments

Popular posts from this blog

VB.NET: Raise base class events from a derived class

Apache Kafka - I - High level architecture and concepts

Upgrading Lodash from 3.x to 4.x