diff --git a/docs/reference/glib/running.xml b/docs/reference/glib/running.xml index 8e4ffeca3..86765c868 100644 --- a/docs/reference/glib/running.xml +++ b/docs/reference/glib/running.xml @@ -256,6 +256,69 @@ How to run and debug your GLib application + + <envar>G_DEBUGGER</envar> + + + When running on Windows, if set to a non-empty string, GLib will + try to interpret the contents of this environment variable as + a command line to a debugger, and run it if the process crashes. + The debugger command line should contain %p and %e substitution + tokens, which GLib will replace with the process ID of the crashing + process and a handle to an event that the debugger should signal + to let GLib know that the debugger successfully attached to the + process. If %e is absent, or if the debugger is not able to + signal events, GLib will resume execution after 60 seconds. + If %p is absent, the debugger won't know which process to attach to, + and GLib will also resume execution after 60 seconds. + + + Additionally, even if G_DEBUGGER is not set, GLib would still + try to print basic exception information (code and address) into + stderr. + + + By default the debugger gets a new console allocated for it. + Set the G_DEBUGGER_OLD_CONSOLE environment variable to any + non-empty string to make the debugger inherit the console of + the crashing process. Normally this is only used by the GLib + testsuite. + + + The exception handler is written with the aim of making it as + simple as possible, to minimize the risk of it invoking + buggy functions or running buggy code, which would result + in exceptions being raised recursively. Because of that + it lacks most of the amenities that one would expect of GLib. + Namely, it does not support Unicode, so it is highly advisable + to only use ASCII characters in G_DEBUGGER. + + + See also G_VEH_CATCH. + + + + + <envar>G_VEH_CATCH</envar> + + + Catching some exceptions can break the program, since Windows + will sometimes use exceptions for execution flow control and + other purposes other than signalling a crash. + + + The G_VEH_CATCH environment variable augments + Vectored Exception Handling + on Windows (see G_DEBUGGER), allowing GLib to catch more + exceptions. Set this variable to a comma-separated list of + hexademical exception codes that should additionally be caught. + + + By default GLib will only catch Access Violation, Stack Overflow and + Illegal Instruction exceptions. + + + diff --git a/glib/gbacktrace.c b/glib/gbacktrace.c index e83079985..34cb1ce46 100644 --- a/glib/gbacktrace.c +++ b/glib/gbacktrace.c @@ -125,6 +125,10 @@ volatile gboolean glib_on_error_halt = TRUE; * If "[P]roceed" is selected, the function returns. * * This function may cause different actions on non-UNIX platforms. + * + * On Windows consider using the `G_DEBUGGER` environment + * variable (see [Running GLib Applications](glib-running.html)) and + * calling g_on_error_stack_trace() instead. */ void g_on_error_query (const gchar *prg_name) @@ -207,6 +211,12 @@ g_on_error_query (const gchar *prg_name) * gdk_init(). * * This function may cause different actions on non-UNIX platforms. + * + * When running on Windows, this function is *not* called by + * g_on_error_query(). If called directly, it will raise an + * exception, which will crash the program. If the `G_DEBUGGER` environment + * variable is set, a debugger will be invoked to attach and + * handle that exception (see [Running GLib Applications](glib-running.html)). */ void g_on_error_stack_trace (const gchar *prg_name) diff --git a/glib/glib-init.c b/glib/glib-init.c index 6ffe7933b..ed800dca1 100644 --- a/glib/glib-init.c +++ b/glib/glib-init.c @@ -286,6 +286,7 @@ DllMain (HINSTANCE hinstDLL, { case DLL_PROCESS_ATTACH: glib_dll = hinstDLL; + g_crash_handler_win32_init (); g_clock_win32_init (); #ifdef THREADS_WIN32 g_thread_win32_init (); @@ -306,6 +307,7 @@ DllMain (HINSTANCE hinstDLL, if (lpvReserved == NULL) g_thread_win32_process_detach (); #endif + g_crash_handler_win32_deinit (); break; default: diff --git a/glib/glib-init.h b/glib/glib-init.h index 695dc044b..f44b9a4d2 100644 --- a/glib/glib-init.h +++ b/glib/glib-init.h @@ -36,6 +36,8 @@ void g_thread_win32_thread_detach (void); void g_thread_win32_init (void); void g_console_win32_init (void); void g_clock_win32_init (void); +void g_crash_handler_win32_init (void); +void g_crash_handler_win32_deinit (void); extern HMODULE glib_dll; #endif diff --git a/glib/gwin32-private.c b/glib/gwin32-private.c new file mode 100644 index 000000000..917bcd1b5 --- /dev/null +++ b/glib/gwin32-private.c @@ -0,0 +1,77 @@ +/* gwin32-private.c - private glib functions for gwin32.c + * + * Copyright 2019 Руслан Ижбулатов + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +/* Copy @cmdline into @debugger, and substitute @pid for `%p` + * and @event for `%e`. + * If @debugger_size (in bytes) is overflowed, return %FALSE. + * Also returns %FALSE when `%` is followed by anything other + * than `e` or `p`. + */ +static gboolean +_g_win32_subst_pid_and_event (char *debugger, + gsize debugger_size, + const char *cmdline, + DWORD pid, + guintptr event) +{ + gsize i = 0, dbg_i = 0; +/* These are integers, and they can't be longer than 20 characters + * even when they are 64-bit and in decimal notation. + * Use 30 just to be sure. + */ +#define STR_BUFFER_SIZE 30 + char pid_str[STR_BUFFER_SIZE] = {0}; + gsize pid_str_len; + char event_str[STR_BUFFER_SIZE] = {0}; + gsize event_str_len; +#undef STR_BUFFER_SIZE + snprintf (pid_str, G_N_ELEMENTS (pid_str), "%lu", pid); + pid_str[G_N_ELEMENTS (pid_str) - 1] = 0; + pid_str_len = strlen (pid_str); + snprintf (event_str, G_N_ELEMENTS (pid_str), "%Iu", event); + event_str[G_N_ELEMENTS (pid_str) - 1] = 0; + event_str_len = strlen (event_str); + + while (cmdline[i] != 0 && dbg_i < debugger_size) + { + if (cmdline[i] != '%') + debugger[dbg_i++] = cmdline[i++]; + else if (cmdline[i + 1] == 'p') + { + gsize j = 0; + while (j < pid_str_len && dbg_i < debugger_size) + debugger[dbg_i++] = pid_str[j++]; + i += 2; + } + else if (cmdline[i + 1] == 'e') + { + gsize j = 0; + while (j < event_str_len && dbg_i < debugger_size) + debugger[dbg_i++] = event_str[j++]; + i += 2; + } + else + return FALSE; + } + if (dbg_i < debugger_size) + debugger[dbg_i] = 0; + else + return FALSE; + + return TRUE; +} diff --git a/glib/gwin32.c b/glib/gwin32.c index 8a7ab3aeb..6d97e4c0e 100644 --- a/glib/gwin32.c +++ b/glib/gwin32.c @@ -1018,4 +1018,218 @@ g_console_win32_init (void) } } +/* This is a handle to the Vectored Exception Handler that + * we install on library initialization. If installed correctly, + * it will be non-NULL. Only used to later de-install the handler + * on library de-initialization. + */ +static void *WinVEH_handle = NULL; + +#include "gwin32-private.c" + +/** + * Handles exceptions (useful for debugging). + * Issues a DebugBreak() call if the process is being debugged (not really + * useful - if the process is being debugged, this handler won't be invoked + * anyway). If it is not, runs a debugger from G_DEBUGGER env var, + * substituting first %p in it for PID, and the first %e for the event handle - + * that event should be set once the debugger attaches itself (otherwise the + * only way out of WaitForSingleObject() is to time out after 1 minute). + * For example, G_DEBUGGER can be set to the following command: + * ``` + * gdb.exe -ex "attach %p" -ex "signal-event %e" -ex "bt" -ex "c" + * ``` + * This will make GDB attach to the process, signal the event (GDB must be + * recent enough for the signal-event command to be available), + * show the backtrace and resume execution, which should make it catch + * the exception when Windows re-raises it again. + * The command line can't be longer than MAX_PATH (260 characters). + * + * This function will only stop (and run a debugger) on the following exceptions: + * * EXCEPTION_ACCESS_VIOLATION + * * EXCEPTION_STACK_OVERFLOW + * * EXCEPTION_ILLEGAL_INSTRUCTION + * To make it stop at other exceptions one should set the G_VEH_CATCH + * environment variable to a list of comma-separated hexademical numbers, + * where each number is the code of an exception that should be caught. + * This is done to prevent GLib from breaking when Windows uses + * exceptions to shuttle information (SetThreadName(), OutputDebugString()) + * or for control flow. + * + * This function deliberately avoids calling any GLib code. + */ +static LONG __stdcall +g_win32_veh_handler (PEXCEPTION_POINTERS ExceptionInfo) +{ + EXCEPTION_RECORD *er; + char debugger[MAX_PATH + 1]; + const char *debugger_env = NULL; + const char *catch_list; + gboolean catch = FALSE; + STARTUPINFO si; + PROCESS_INFORMATION pi; + HANDLE event; + SECURITY_ATTRIBUTES sa; + + if (ExceptionInfo == NULL || + ExceptionInfo->ExceptionRecord == NULL) + return EXCEPTION_CONTINUE_SEARCH; + + er = ExceptionInfo->ExceptionRecord; + + switch (er->ExceptionCode) + { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_STACK_OVERFLOW: + case EXCEPTION_ILLEGAL_INSTRUCTION: + case EXCEPTION_BREAKPOINT: /* DebugBreak() raises this */ + break; + default: + catch_list = getenv ("G_VEH_CATCH"); + + while (!catch && + catch_list != NULL && + catch_list[0] != 0) + { + unsigned long catch_code; + char *end; + errno = 0; + catch_code = strtoul (catch_list, &end, 16); + if (errno != NO_ERROR) + break; + catch_list = end; + if (catch_list != NULL && catch_list[0] == ',') + catch_list++; + if (catch_code == er->ExceptionCode) + catch = TRUE; + } + + if (catch) + break; + + return EXCEPTION_CONTINUE_SEARCH; + } + + if (IsDebuggerPresent ()) + { + /* This shouldn't happen, but still try to + * avoid recursion with EXCEPTION_BREAKPOINT and + * DebugBreak(). + */ + if (er->ExceptionCode != EXCEPTION_BREAKPOINT) + DebugBreak (); + return EXCEPTION_CONTINUE_EXECUTION; + } + + fprintf (stderr, + "Exception code=0x%lx flags=0x%lx at 0x%p", + er->ExceptionCode, + er->ExceptionFlags, + er->ExceptionAddress); + + switch (er->ExceptionCode) + { + case EXCEPTION_ACCESS_VIOLATION: + fprintf (stderr, + ". Access violation - attempting to %s at address 0x%p\n", + er->ExceptionInformation[0] == 0 ? "read data" : + er->ExceptionInformation[0] == 1 ? "write data" : + er->ExceptionInformation[0] == 8 ? "execute data" : + "do something bad", + (void *) er->ExceptionInformation[1]); + break; + case EXCEPTION_IN_PAGE_ERROR: + fprintf (stderr, + ". Page access violation - attempting to %s at address 0x%p with status %Ix\n", + er->ExceptionInformation[0] == 0 ? "read from an inaccessible page" : + er->ExceptionInformation[0] == 1 ? "write to an inaccessible page" : + er->ExceptionInformation[0] == 8 ? "execute data in page" : + "do something bad with a page", + (void *) er->ExceptionInformation[1], + er->ExceptionInformation[2]); + break; + default: + fprintf (stderr, "\n"); + break; + } + + fflush (stderr); + + debugger_env = getenv ("G_DEBUGGER"); + + if (debugger_env == NULL) + return EXCEPTION_CONTINUE_SEARCH; + + /* Create an inheritable event */ + memset (&si, 0, sizeof (si)); + memset (&pi, 0, sizeof (pi)); + memset (&sa, 0, sizeof (sa)); + si.cb = sizeof (si); + sa.nLength = sizeof (sa); + sa.bInheritHandle = TRUE; + event = CreateEvent (&sa, FALSE, FALSE, NULL); + + /* Put process ID and event handle into debugger commandline */ + if (!_g_win32_subst_pid_and_event (debugger, G_N_ELEMENTS (debugger), + debugger_env, GetCurrentProcessId (), + (guintptr) event)) + { + CloseHandle (event); + return EXCEPTION_CONTINUE_SEARCH; + } + + /* Run the debugger */ + debugger[MAX_PATH] = '\0'; + if (0 != CreateProcessA (NULL, + debugger, + NULL, + NULL, + TRUE, + getenv ("G_DEBUGGER_OLD_CONSOLE") != NULL ? 0 : CREATE_NEW_CONSOLE, + NULL, + NULL, + &si, + &pi)) + { + CloseHandle (pi.hProcess); + CloseHandle (pi.hThread); + /* If successful, wait for 60 seconds on the event + * we passed. The debugger should signal that event. + * 60 second limit is here to prevent us from hanging + * up forever in case the debugger does not support + * event signalling. + */ + WaitForSingleObject (event, 60000); + } + + CloseHandle (event); + + /* Now the debugger is present, and we can try + * resuming execution, re-triggering the exception, + * which will be caught by debugger this time around. + */ + if (IsDebuggerPresent ()) + return EXCEPTION_CONTINUE_EXECUTION; + + return EXCEPTION_CONTINUE_SEARCH; +} + +void +g_crash_handler_win32_init (void) +{ + if (WinVEH_handle != NULL) + return; + + WinVEH_handle = AddVectoredExceptionHandler (0, &g_win32_veh_handler); +} + +void +g_crash_handler_win32_deinit (void) +{ + if (WinVEH_handle != NULL) + RemoveVectoredExceptionHandler (WinVEH_handle); + + WinVEH_handle = NULL; +} + #endif diff --git a/glib/tests/meson.build b/glib/tests/meson.build index d54fc41fa..2b3f902ab 100644 --- a/glib/tests/meson.build +++ b/glib/tests/meson.build @@ -140,6 +140,9 @@ if host_machine.system() == 'windows' }, } endif + glib_tests += { + 'win32' : {}, + } else glib_tests += { 'include' : {}, diff --git a/glib/tests/win32.c b/glib/tests/win32.c new file mode 100644 index 000000000..d4b1fc523 --- /dev/null +++ b/glib/tests/win32.c @@ -0,0 +1,173 @@ +/* Unit test for VEH on Windows + * Copyright (C) 2019 Руслан Ижбулатов + * + * This work is provided "as is"; redistribution and modification + * in whole or in part, in any medium, physical or electronic is + * permitted without restriction. + * + * This work is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * In no event shall the authors or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + */ + +#include "config.h" + +#include +#include +#include +#include + +static char *argv0 = NULL; + +#include "../gwin32-private.c" + +static void +test_subst_pid_and_event (void) +{ + const gchar not_enough[] = "too long when %e and %p are substituted"; + gchar debugger_3[3]; + gchar debugger_not_enough[G_N_ELEMENTS (not_enough)]; + gchar debugger_enough[G_N_ELEMENTS (not_enough) + 1]; + gchar debugger_big[65535] = {0}; + gchar *output; + DWORD be = 0xFFFFFFFF; + guintptr bp = (guintptr) G_MAXSIZE; + + /* %f is not valid */ + g_assert_false (_g_win32_subst_pid_and_event (debugger_3, G_N_ELEMENTS (debugger_3), + "%f", 0, 0)); + + g_assert_false (_g_win32_subst_pid_and_event (debugger_3, G_N_ELEMENTS (debugger_3), + "string longer than 10", 0, 0)); + /* 200 is longer than %e, so the string doesn't fit by 1 byte */ + g_assert_false (_g_win32_subst_pid_and_event (debugger_not_enough, G_N_ELEMENTS (debugger_not_enough), + "too long when %e and %p are substituted", 10, 200)); + + /* This should fit */ + g_assert_true (_g_win32_subst_pid_and_event (debugger_enough, G_N_ELEMENTS (debugger_enough), + "too long when %e and %p are substituted", 10, 200)); + g_assert_cmpstr (debugger_enough, ==, "too long when 200 and 10 are substituted"); + + g_assert_true (_g_win32_subst_pid_and_event (debugger_big, G_N_ELEMENTS (debugger_big), + "multipl%e big %e %entries and %pids are %provided here", bp, be)); + output = g_strdup_printf ("multipl%lu big %lu %luntries and %lluids are %llurovided here", be, be, be, (guint64) bp, (guint64) bp); + g_assert_cmpstr (debugger_big, ==, output); + g_free (output); +} + +/* Crash with access violation */ +static void +test_access_violation (void) +{ + int *integer = NULL; + /* Use SEM_NOGPFAULTERRORBOX to prevent an error dialog + * from being shown. + */ + DWORD dwMode = SetErrorMode (SEM_NOGPFAULTERRORBOX); + SetErrorMode (dwMode | SEM_NOGPFAULTERRORBOX); + *integer = 1; + SetErrorMode (dwMode); +} + +/* Crash with illegal instruction */ +static void +test_illegal_instruction (void) +{ + DWORD dwMode = SetErrorMode (SEM_NOGPFAULTERRORBOX); + SetErrorMode (dwMode | SEM_NOGPFAULTERRORBOX); + RaiseException (EXCEPTION_ILLEGAL_INSTRUCTION, 0, 0, NULL); + SetErrorMode (dwMode); +} + +static void +test_veh_crash_access_violation (void) +{ + g_unsetenv ("G_DEBUGGER"); + /* Run a test that crashes */ + g_test_trap_subprocess ("/win32/subprocess/access_violation", 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("Exception code=0xc0000005*"); +} + +static void +test_veh_crash_illegal_instruction (void) +{ + g_unsetenv ("G_DEBUGGER"); + /* Run a test that crashes */ + g_test_trap_subprocess ("/win32/subprocess/illegal_instruction", 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("Exception code=0xc000001d*"); +} + +static void +test_veh_debug (void) +{ + /* Run a test that crashes and runs a debugger */ + g_test_trap_subprocess ("/win32/subprocess/debugee", 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("Exception code=0xc0000005*Debugger invoked, attaching to*"); +} + +static void +test_veh_debugee (void) +{ + /* Set up a debugger to be run on crash */ + gchar *command = g_strdup_printf ("%s %s", argv0, "%p %e"); + g_setenv ("G_DEBUGGER", command, TRUE); + /* Because the "debugger" here is not really a debugger, + * it can't write into stderr of this process, unless + * we allow it to inherit our stderr. + */ + g_setenv ("G_DEBUGGER_OLD_CONSOLE", "1", TRUE); + g_free (command); + /* Crash */ + test_access_violation (); +} + +static void +veh_debugger (int argc, char *argv[]) +{ + char *end; + DWORD pid = strtoul (argv[1], &end, 10); + guintptr event = (guintptr) _strtoui64 (argv[2], &end, 10); + /* Unfreeze the debugee and announce ourselves */ + SetEvent ((HANDLE) event); + CloseHandle ((HANDLE) event); + g_fprintf (stderr, "Debugger invoked, attaching to %lu and signalling %" G_GUINTPTR_FORMAT, pid, event); +} + +int +main (int argc, + char *argv[]) +{ + argv0 = argv[0]; + + g_test_init (&argc, &argv, NULL); + + if (argc > 2) + { + veh_debugger (argc, argv); + return 0; + } + + g_test_add_func ("/win32/substitute-pid-and-event", test_subst_pid_and_event); + + g_test_add_func ("/win32/veh/access_violation", test_veh_crash_access_violation); + g_test_add_func ("/win32/veh/illegal_instruction", test_veh_crash_illegal_instruction); + g_test_add_func ("/win32/veh/debug", test_veh_debug); + + g_test_add_func ("/win32/subprocess/debugee", test_veh_debugee); + g_test_add_func ("/win32/subprocess/access_violation", test_access_violation); + g_test_add_func ("/win32/subprocess/illegal_instruction", test_illegal_instruction); + + return g_test_run(); +}