[glib: 1/3] W32: Add a simple exception handler



commit 025a346728d50371073cfe8c976ee80e7c01182a
Author: Руслан Ижбулатов <lrn1986 gmail com>
Date:   Sun Jan 13 11:21:04 2019 +0000

    W32: Add a simple exception handler
    
    Install a Vectored Exception Handler[0]. Its sole purpose is to catch
    some exceptions (access violations, stack overflows, illegal
    instructions and debug breaks - by default, but it can be made to catch
    any exception for which a code is known) and run a debugger in response.
    
    This allows W32 glib applications to be run without a debugger,
    but at the same time allows a debugger to be attached in case
    something happens.
    
    The debugger is run with a new console, unless an environment variable
    is set to allow it to inherit the console of the crashing process.
    
    The short list of handleable exceptions is there to ensure that
    this handler won't run a debugger to "handle" utility exceptions,
    such as the one that is used to communicate thread names to a debugger.
    
    The handler is installed to be called last, and shouldn't interfere
    with any user-installed handlers.
    
    There's nothing fancy about the way it runs a debugger (it doesn't even
    support unicode in paths), and it deliberately avoids using glib code.
    
    The handler will also print a bit of information about the exception
    that it caught, and even more information for well-known exceptions,
    such as access violation.
    
    The whole scheme is similar to AeDebug[1] and, in fact, the signal-event
    gdb command was originally implemented for this very purpose.
    
    [0]: https://docs.microsoft.com/en-us/windows/desktop/debug/vectored-exception-handling
    [1]: https://docs.microsoft.com/en-us/windows/desktop/debug/configuring-automatic-debugging

 docs/reference/glib/running.xml |  63 ++++++++++++
 glib/gbacktrace.c               |  10 ++
 glib/glib-init.c                |   2 +
 glib/glib-init.h                |   2 +
 glib/gwin32-private.c           |  77 +++++++++++++++
 glib/gwin32.c                   | 214 ++++++++++++++++++++++++++++++++++++++++
 6 files changed, 368 insertions(+)
---
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
   </para>
 </formalpara>
 
+<formalpara id="G_DEBUGGER">
+  <title><envar>G_DEBUGGER</envar></title>
+
+  <para>
+    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 <literal>%p</literal> and <literal>%e</literal> 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 <literal>%e</literal> is absent, or if the debugger is not able to
+    signal events, GLib will resume execution after 60 seconds.
+    If <literal>%p</literal> is absent, the debugger won't know which process to attach to,
+    and GLib will also resume execution after 60 seconds.
+  </para>
+  <para>
+    Additionally, even if <envar>G_DEBUGGER</envar> is not set, GLib would still
+    try to print basic exception information (code and address) into
+    stderr.
+  </para>
+  <para>
+    By default the debugger gets a new console allocated for it.
+    Set the <envar>G_DEBUGGER_OLD_CONSOLE</envar> 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.
+  </para>
+  <para>
+    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 <envar>G_DEBUGGER</envar>.
+  </para>
+  <para>
+    See also <link linkend="G_VEH_CATCH"><envar>G_VEH_CATCH</envar></link>.
+  </para>
+</formalpara>
+
+<formalpara id="G_VEH_CATCH">
+  <title><envar>G_VEH_CATCH</envar></title>
+
+  <para>
+    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.
+  </para>
+  <para>
+    The <envar>G_VEH_CATCH</envar> environment variable augments
+    <ulink url="https://docs.microsoft.com/en-us/windows/desktop/debug/vectored-exception-handling";>Vectored 
Exception Handling</ulink>
+    on Windows (see <link linkend="G_DEBUGGER"><envar>G_DEBUGGER</envar></link>), allowing GLib to catch more
+    exceptions. Set this variable to a comma-separated list of
+    hexademical exception codes that should additionally be caught.
+  </para>
+  <para>
+    By default GLib will only catch Access Violation, Stack Overflow and
+    Illegal Instruction <ulink 
url="https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_exception_record";>exceptions</ulink>.
+  </para>
+</formalpara>
+
 </refsect2>
 
 <refsect2 id="setlocale">
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 <http://www.gnu.org/licenses/>.
+ */
+
+/* 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


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]