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
This commit is contained in:
Руслан Ижбулатов 2019-01-13 11:21:04 +00:00
parent 35db2ffe9c
commit 025a346728
6 changed files with 368 additions and 0 deletions

View File

@ -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">

View File

@ -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)

View File

@ -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:

View File

@ -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

77
glib/gwin32-private.c Normal file
View File

@ -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;
}

View File

@ -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