mirror of
https://gitlab.gnome.org/GNOME/glib.git
synced 2025-02-24 03:02:10 +01:00
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:
parent
35db2ffe9c
commit
025a346728
@ -256,6 +256,69 @@ How to run and debug your GLib application
|
|||||||
</para>
|
</para>
|
||||||
</formalpara>
|
</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>
|
||||||
|
|
||||||
<refsect2 id="setlocale">
|
<refsect2 id="setlocale">
|
||||||
|
@ -125,6 +125,10 @@ volatile gboolean glib_on_error_halt = TRUE;
|
|||||||
* If "[P]roceed" is selected, the function returns.
|
* If "[P]roceed" is selected, the function returns.
|
||||||
*
|
*
|
||||||
* This function may cause different actions on non-UNIX platforms.
|
* 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
|
void
|
||||||
g_on_error_query (const gchar *prg_name)
|
g_on_error_query (const gchar *prg_name)
|
||||||
@ -207,6 +211,12 @@ g_on_error_query (const gchar *prg_name)
|
|||||||
* gdk_init().
|
* gdk_init().
|
||||||
*
|
*
|
||||||
* This function may cause different actions on non-UNIX platforms.
|
* 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
|
void
|
||||||
g_on_error_stack_trace (const gchar *prg_name)
|
g_on_error_stack_trace (const gchar *prg_name)
|
||||||
|
@ -286,6 +286,7 @@ DllMain (HINSTANCE hinstDLL,
|
|||||||
{
|
{
|
||||||
case DLL_PROCESS_ATTACH:
|
case DLL_PROCESS_ATTACH:
|
||||||
glib_dll = hinstDLL;
|
glib_dll = hinstDLL;
|
||||||
|
g_crash_handler_win32_init ();
|
||||||
g_clock_win32_init ();
|
g_clock_win32_init ();
|
||||||
#ifdef THREADS_WIN32
|
#ifdef THREADS_WIN32
|
||||||
g_thread_win32_init ();
|
g_thread_win32_init ();
|
||||||
@ -306,6 +307,7 @@ DllMain (HINSTANCE hinstDLL,
|
|||||||
if (lpvReserved == NULL)
|
if (lpvReserved == NULL)
|
||||||
g_thread_win32_process_detach ();
|
g_thread_win32_process_detach ();
|
||||||
#endif
|
#endif
|
||||||
|
g_crash_handler_win32_deinit ();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -36,6 +36,8 @@ void g_thread_win32_thread_detach (void);
|
|||||||
void g_thread_win32_init (void);
|
void g_thread_win32_init (void);
|
||||||
void g_console_win32_init (void);
|
void g_console_win32_init (void);
|
||||||
void g_clock_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;
|
extern HMODULE glib_dll;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
77
glib/gwin32-private.c
Normal file
77
glib/gwin32-private.c
Normal 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;
|
||||||
|
}
|
214
glib/gwin32.c
214
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
|
#endif
|
||||||
|
Loading…
x
Reference in New Issue
Block a user