gdb: print backtrace on fatal SIGSEGV

This commit adds a new maintenance feature, the ability to print
a (limited) backtrace if GDB dies due to a fatal signal.

The backtrace is produced using the backtrace and backtrace_symbols_fd
functions which are declared in the execinfo.h header, and both of
which are async signal safe.  A configure check has been added to
check for these features, if they are not available then the new code
is not compiled into GDB and the backtrace will not be printed.

The motivation for this new feature is to aid in debugging GDB in
situations where GDB has crashed at a users site, but the user is
reluctant to share core files, possibly due to concerns about what
might be in the memory image within the core file.  Such a user might
be happy to share a simple backtrace that was written to stderr.

The production of the backtrace is on by default, but can switched off
using the new commands:

  maintenance set backtrace-on-fatal-signal on|off
  maintenance show backtrace-on-fatal-signal

Right now, I have hooked this feature in to GDB's existing handling of
SIGSEGV only, but this will be extended to more signals in a later
commit.

One additional change I have made in this commit is that, when we
decide GDB should terminate due to the fatal signal, we now
raise the same fatal signal rather than raising SIGABRT.

Currently, this is only effecting our handling of SIGSEGV.  So,
previously, if GDB hit a SEGV then we would terminate GDB with a
SIGABRT.  After this commit we will terminate GDB with a SIGSEGV.

This feels like an improvement to me, we should still get a core dump,
but in many shells, the user will see a more specific message once GDB
exits, in bash for example "Segmentation fault" rather than "Aborted".

Finally then, here is an example of the output a user would see if GDB
should hit an internal SIGSEGV:

  Fatal signal: Segmentation fault
  ----- Backtrace -----
  ./gdb/gdb[0x8078e6]
  ./gdb/gdb[0x807b20]
  /lib64/libpthread.so.0(+0x14b20)[0x7f6648c92b20]
  /lib64/libc.so.6(__poll+0x4f)[0x7f66484d3a5f]
  ./gdb/gdb[0x1540f4c]
  ./gdb/gdb[0x154034a]
  ./gdb/gdb[0x9b002d]
  ./gdb/gdb[0x9b014d]
  ./gdb/gdb[0x9b1aa6]
  ./gdb/gdb[0x9b1b0c]
  ./gdb/gdb[0x41756d]
  /lib64/libc.so.6(__libc_start_main+0xf3)[0x7f66484041a3]
  ./gdb/gdb[0x41746e]
  ---------------------
  A fatal error internal to GDB has been detected, further
  debugging is not possible.  GDB will now terminate.

  This is a bug, please report it.  For instructions, see:
  <https://www.gnu.org/software/gdb/bugs/>.

  Segmentation fault (core dumped)

It is disappointing that backtrace_symbols_fd does not actually map
the addresses back to symbols, this appears, in part, to be due to GDB
not being built with -rdynamic as the manual page for
backtrace_symbols_fd suggests, however, even when I do add -rdynamic
to the build of GDB I only see symbols for some addresses.

We could potentially look at alternative libraries to provide the
backtrace (e.g. libunwind) however, the solution presented here, which
is available as part of glibc is probably a good baseline from which
we might improve things in future.
This commit is contained in:
Andrew Burgess
2021-06-10 16:57:24 +01:00
parent 270135645b
commit 6aa4f97c2b
9 changed files with 398 additions and 11 deletions

View File

@@ -3,6 +3,13 @@
*** Changes since GDB 11
maint set backtrace-on-fatal-signal on|off
maint show backtrace-on-fatal-signal
This setting is 'on' by default. When 'on' GDB will print a limited
backtrace to stderr in the situation where GDB terminates with a
fatal signal. This only supported on some platforms where the
backtrace and backtrace_symbols_fd functions are available.
*** Changes in GDB 11
* The 'set disassembler-options' command now supports specifying options

View File

@@ -162,6 +162,12 @@
/* Define to 1 if your system has the etext variable. */
#undef HAVE_ETEXT
/* Define to 1 if execinfo.h backtrace functions are available. */
#undef HAVE_EXECINFO_BACKTRACE
/* Define to 1 if you have the <execinfo.h> header file. */
#undef HAVE_EXECINFO_H
/* Define to 1 if you have the `fdwalk' function. */
#undef HAVE_FDWALK

51
gdb/configure vendored
View File

@@ -12315,6 +12315,18 @@ fi
done
for ac_header in execinfo.h
do :
ac_fn_c_check_header_mongrel "$LINENO" "execinfo.h" "ac_cv_header_execinfo_h" "$ac_includes_default"
if test "x$ac_cv_header_execinfo_h" = xyes; then :
cat >>confdefs.h <<_ACEOF
#define HAVE_EXECINFO_H 1
_ACEOF
fi
done
# ------------------------- #
# Checks for declarations. #
@@ -16484,6 +16496,45 @@ fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $found" >&5
$as_echo "$found" >&6; }
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether execinfo.h backtrace is available" >&5
$as_echo_n "checking whether execinfo.h backtrace is available... " >&6; }
if ${gdb_cv_execinfo_backtrace+:} false; then :
$as_echo_n "(cached) " >&6
else
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
/* end confdefs.h. */
#include <execinfo.h>
int
main ()
{
int f;
void *b[2];
f = backtrace (b, 2);
backtrace_symbols_fd (b, f, 2);
;
return 0;
}
_ACEOF
if ac_fn_c_try_link "$LINENO"; then :
gdb_cv_execinfo_backtrace=yes
else
gdb_cv_execinfo_backtrace=no
fi
rm -f core conftest.err conftest.$ac_objext \
conftest$ac_exeext conftest.$ac_ext
fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $gdb_cv_execinfo_backtrace" >&5
$as_echo "$gdb_cv_execinfo_backtrace" >&6; }
if test "$gdb_cv_execinfo_backtrace" = yes; then
$as_echo "#define HAVE_EXECINFO_BACKTRACE 1" >>confdefs.h
fi
if test "${build}" = "${host}" -a "${host}" = "${target}" ; then
case ${host_os} in

View File

@@ -1291,6 +1291,7 @@ AC_CHECK_HEADERS(term.h, [], [],
AC_CHECK_HEADERS([sys/socket.h])
AC_CHECK_HEADERS([ws2tcpip.h])
AC_CHECK_HEADERS([execinfo.h])
# ------------------------- #
# Checks for declarations. #
@@ -1682,6 +1683,27 @@ fi
AC_SUBST(RDYNAMIC)
AC_MSG_RESULT($found)
AC_CACHE_CHECK(
[whether execinfo.h backtrace is available],
gdb_cv_execinfo_backtrace,
[AC_LINK_IFELSE(
[AC_LANG_PROGRAM(
[
#include <execinfo.h>
],
[
int f;
void *b[[2]];
f = backtrace (b, 2);
backtrace_symbols_fd (b, f, 2);
])],
[gdb_cv_execinfo_backtrace=yes],
[gdb_cv_execinfo_backtrace=no])])
if test "$gdb_cv_execinfo_backtrace" = yes; then
AC_DEFINE(HAVE_EXECINFO_BACKTRACE, 1,
[Define to 1 if execinfo.h backtrace functions are available.])
fi
dnl For certain native configurations, we need to check whether thread
dnl support can be built in or not.
dnl

View File

@@ -39716,6 +39716,24 @@ These are representative commands for each @var{kind} of setting type
@value{GDBN} supports. They are used by the testsuite for exercising
the settings infrastructure.
@kindex maint set backtrace-on-fatal-signal
@kindex maint show backtrace-on-fatal-signal
@item maint set backtrace-on-fatal-signal [on|off]
@itemx maint show backtrace-on-fatal-signal
When this setting is @code{on}, if @value{GDBN} itself terminates with
a fatal signal (e.g.@: SIGSEGV), then a limited backtrace will be
printed to stderr. This backtrace can be used to help diagnose
crashes within @value{GDBN} in situations where a user is unable to
share a corefile with the @value{GDBN} developers.
If the functionality to provide this backtrace is not available for
the platform on which GDB is running then this feature will be
@code{off} by default, and attempting to turn this feature on will
give an error.
For platforms that do support creating the backtrace this feature is
@code{on} by default.
@kindex maint with
@item maint with @var{setting} [@var{value}] [-- @var{command}]
Like the @code{with} command, but works with @code{maintenance set}

View File

@@ -46,6 +46,10 @@
#include "readline/readline.h"
#include "readline/history.h"
#ifdef HAVE_EXECINFO_H
# include <execinfo.h>
#endif /* HAVE_EXECINFO_H */
/* readline defines this. */
#undef savestring
@@ -96,6 +100,38 @@ bool exec_done_display_p = false;
run again. */
int call_stdin_event_handler_again_p;
/* When true GDB will produce a minimal backtrace when a fatal signal is
reached (within GDB code). */
static bool bt_on_fatal_signal
#ifdef HAVE_EXECINFO_BACKTRACE
= true;
#else
= false;
#endif /* HAVE_EXECINFO_BACKTRACE */
/* Implement 'maintenance show backtrace-on-fatal-signal'. */
static void
show_bt_on_fatal_signal (struct ui_file *file, int from_tty,
struct cmd_list_element *cmd, const char *value)
{
fprintf_filtered (file, _("Backtrace on a fatal signal is %s.\n"), value);
}
/* Implement 'maintenance set backtrace-on-fatal-signal'. */
static void
set_bt_on_fatal_signal (const char *args, int from_tty, cmd_list_element *c)
{
#ifndef HAVE_EXECINFO_BACKTRACE
if (bt_on_fatal_signal)
{
bt_on_fatal_signal = false;
error (_("support for this feature is not compiled into GDB"));
}
#endif
}
/* Signal handling variables. */
/* Each of these is a pointer to a function that the event loop will
invoke if the corresponding signal has received. The real signal
@@ -846,6 +882,84 @@ gdb_readline_no_editing_callback (gdb_client_data client_data)
}
/* Attempt to unblock signal SIG, return true if the signal was unblocked,
otherwise, return false. */
static bool
unblock_signal (int sig)
{
#if HAVE_SIGPROCMASK
sigset_t sigset;
sigemptyset (&sigset);
sigaddset (&sigset, sig);
gdb_sigmask (SIG_UNBLOCK, &sigset, 0);
return true;
#endif
return false;
}
/* Called to handle fatal signals. SIG is the signal number. */
static void ATTRIBUTE_NORETURN
handle_fatal_signal (int sig)
{
#ifdef HAVE_EXECINFO_BACKTRACE
const auto sig_write = [] (const char *msg) -> void
{
gdb_stderr->write_async_safe (msg, strlen (msg));
};
if (bt_on_fatal_signal)
{
sig_write ("\n\n");
sig_write (_("Fatal signal: "));
sig_write (strsignal (sig));
sig_write ("\n");
/* Allow up to 25 frames of backtrace. */
void *buffer[25];
int frames = backtrace (buffer, ARRAY_SIZE (buffer));
sig_write (_("----- Backtrace -----\n"));
if (gdb_stderr->fd () > -1)
{
backtrace_symbols_fd (buffer, frames, gdb_stderr->fd ());
if (frames == ARRAY_SIZE (buffer))
sig_write (_("Backtrace might be incomplete.\n"));
}
else
sig_write (_("Backtrace unavailable\n"));
sig_write ("---------------------\n");
sig_write (_("A fatal error internal to GDB has been detected, "
"further\ndebugging is not possible. GDB will now "
"terminate.\n\n"));
sig_write (_("This is a bug, please report it."));
if (REPORT_BUGS_TO[0] != '\0')
{
sig_write (_(" For instructions, see:\n"));
sig_write (REPORT_BUGS_TO);
sig_write (".");
}
sig_write ("\n\n");
gdb_stderr->flush ();
}
#endif /* HAVE_EXECINF_BACKTRACE */
/* If possible arrange for SIG to have its default behaviour (which
should be to terminate the current process), unblock SIG, and reraise
the signal. This ensures GDB terminates with the expected signal. */
if (signal (sig, SIG_DFL) != SIG_ERR
&& unblock_signal (sig))
raise (sig);
/* The above failed, so try to use SIGABRT to terminate GDB. */
#ifdef SIGABRT
signal (SIGABRT, SIG_DFL);
#endif
abort (); /* ARI: abort */
}
/* The SIGSEGV handler for this thread, or NULL if there is none. GDB
always installs a global SIGSEGV handler, and then lets threads
indicate their interest in handling the signal by setting this
@@ -887,7 +1001,7 @@ handle_sigsegv (int sig)
install_handle_sigsegv ();
if (thread_local_segv_handler == nullptr)
abort (); /* ARI: abort */
handle_fatal_signal (sig);
thread_local_segv_handler (sig);
}
@@ -1160,16 +1274,7 @@ async_sigtstp_handler (gdb_client_data arg)
char *prompt = get_prompt ();
signal (SIGTSTP, SIG_DFL);
#if HAVE_SIGPROCMASK
{
sigset_t zero;
sigemptyset (&zero);
gdb_sigmask (SIG_SETMASK, &zero, 0);
}
#elif HAVE_SIGSETMASK
sigsetmask (0);
#endif
unblock_signal (SIGTSTP);
raise (SIGTSTP);
signal (SIGTSTP, handle_sigtstp);
printf_unfiltered ("%s", prompt);
@@ -1320,4 +1425,17 @@ Control whether to show event loop-related debug messages."),
set_debug_event_loop_command,
show_debug_event_loop_command,
&setdebuglist, &showdebuglist);
add_setshow_boolean_cmd ("backtrace-on-fatal-signal", class_maintenance,
&bt_on_fatal_signal, _("\
Set whether to produce a backtrace if GDB receives a fatal signal."), _("\
Show whether GDB will produce a backtrace if it receives a fatal signal."), _("\
Use \"on\" to enable, \"off\" to disable.\n\
If enabled, GDB will produce a minimal backtrace if it encounters a fatal\n\
signal from within GDB itself. This is a mechanism to help diagnose\n\
crashes within GDB, not a mechanism for debugging inferiors."),
set_bt_on_fatal_signal,
show_bt_on_fatal_signal,
&maintenance_set_cmdlist,
&maintenance_show_cmdlist);
}

View File

@@ -0,0 +1,22 @@
/* Copyright 2021 Free Software Foundation, Inc.
This file is part of GDB.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
int
main (void)
{
return 0;
}

View File

@@ -0,0 +1,134 @@
# Copyright 2021 Free Software Foundation, Inc.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Test the 'maint set backtrace-on-fatal-signal' behaviour. Start up
# GDB, turn on backtrace-on-fatal-signal, then send fatal signals to
# GDB and ensure we see the backtrace.
standard_testfile
# The logic for sending signals to GDB might now work when using a
# remote host (will the signal go to GDB, or the program that
# established the connection to the remote host?), so just skip this
# test for remote host setups.
if {[is_remote host]} {
untested $testfile
return -1
}
if {[prepare_for_testing "failed to prepare" $testfile $srcfile]} {
return -1
}
# Check we can run to main. If this works this time then we just
# assume that it will work later on (when we repeatedly restart GDB).
if ![runto_main] then {
untested $testfile
return -1
}
# Check that the backtrace-on-fatal-signal feature is supported. If
# this target doesn't have the backtrace function available then
# trying to turn this on will give an error, in which case we just
# skip this test.
gdb_test_multiple "maint set backtrace-on-fatal-signal on" "" {
-re "support for this feature is not compiled into GDB" {
untested $testfile
return -1
}
-re "$gdb_prompt $" {
pass $gdb_test_name
}
}
# Now the actual test loop.
foreach test_data {{SEGV "Segmentation fault"}} {
set sig [lindex ${test_data} 0]
set msg [lindex ${test_data} 1]
with_test_prefix ${sig} {
# Restart GDB.
clean_restart $binfile
# Capture the pid of GDB.
set testpid [spawn_id_get_pid $gdb_spawn_id]
# Start the inferior.
runto_main
# Turn on the backtrace-on-fatal-signal feature.
gdb_test_no_output "maint set backtrace-on-fatal-signal on"
# Flags for various bits of the output we expect to see, we
# check for these in the gdb_test_multiple below.
set saw_fatal_msg false
set saw_bt_start false
set saw_bt_end false
set internal_error_msg_count 0
# Send the fatal signal to GDB.
remote_exec host "kill -${sig} ${testpid}"
# Scan GDB's output for the backtrace. As the output we get
# here includes the standard "internal error" message, which
# gdb_test_multiple will usually handle, we are forced to make
# extensive use of the "-early" flag here so that all our
# patterns are applied before gdb_test_multiple can check for
# the internal error pattern.
gdb_test_multiple "" "scan for backtrace" {
-early -re "^\r\n" {
exp_continue
}
-early -re "^Fatal signal: ${msg}\r\n" {
set saw_fatal_msg true
exp_continue
}
-early -re "^----- Backtrace -----\r\n" {
set saw_bt_start true
exp_continue
}
-early -re ".+\r\n---------------------\r\n" {
set saw_bt_end true
exp_continue
}
-early -re "^A fatal error internal to GDB has been detected, further\r\n" {
incr internal_error_msg_count
exp_continue
}
-early -re "^debugging is not possible. GDB will now terminate\\.\r\n" {
incr internal_error_msg_count
exp_continue
}
eof {
# Catch the eof case as this indicates that GDB has
# gone away, which in this case, is what we expect to
# happen.
gdb_assert { $saw_fatal_msg }
gdb_assert { $saw_bt_start }
gdb_assert { $saw_bt_end }
gdb_assert { [expr $internal_error_msg_count == 2] }
}
-re "$gdb_prompt $" {
# GDB should terminate, we should never get back to
# the prompt.
fail $gdb_test_name
}
}
# GDB should be dead and gone by this point, but just to be
# sure, force an exit.
gdb_exit
}
}

View File

@@ -83,6 +83,11 @@ public:
virtual void flush ()
{}
/* If this object has an underlying file descriptor, then return it.
Otherwise, return -1. */
virtual int fd () const
{ return -1; }
};
typedef std::unique_ptr<ui_file> ui_file_up;
@@ -195,6 +200,10 @@ public:
bool can_emit_style_escape () override;
/* Return the underlying file descriptor. */
int fd () const override
{ return m_fd; }
private:
/* Sets the internal stream to FILE, and saves the FILE's file
descriptor in M_FD. */