aboutsummaryrefslogtreecommitdiff
path: root/gdb
diff options
context:
space:
mode:
Diffstat (limited to 'gdb')
-rw-r--r--gdb/breakpoint.c6
-rw-r--r--gdb/python/py-breakpoint.c3
-rw-r--r--gdb/python/py-finishbreakpoint.c33
-rw-r--r--gdb/python/python-internal.h1
-rw-r--r--gdb/testsuite/gdb.mi/mi-thread-bp-deleted.c88
-rw-r--r--gdb/testsuite/gdb.mi/mi-thread-bp-deleted.exp290
-rw-r--r--gdb/testsuite/gdb.threads/thread-bp-deleted.c88
-rw-r--r--gdb/testsuite/gdb.threads/thread-bp-deleted.exp210
-rw-r--r--gdb/thread.c2
9 files changed, 709 insertions, 12 deletions
diff --git a/gdb/breakpoint.c b/gdb/breakpoint.c
index 36c53e5..a42d26f 100644
--- a/gdb/breakpoint.c
+++ b/gdb/breakpoint.c
@@ -3249,14 +3249,10 @@ remove_threaded_breakpoints (struct thread_info *tp, int silent)
{
if (b->thread == tp->global_num && user_breakpoint_p (b))
{
- b->disposition = disp_del_at_next_stop;
-
gdb_printf (_("\
Thread-specific breakpoint %d deleted - thread %s no longer in the thread list.\n"),
b->number, print_thread_id (tp));
-
- /* Hide it from the user. */
- b->number = 0;
+ delete_breakpoint (b);
}
}
}
diff --git a/gdb/python/py-breakpoint.c b/gdb/python/py-breakpoint.c
index ecf52a4..880f1b5 100644
--- a/gdb/python/py-breakpoint.c
+++ b/gdb/python/py-breakpoint.c
@@ -1199,6 +1199,9 @@ gdbpy_breakpoint_deleted (struct breakpoint *b)
gdbpy_ref<gdbpy_breakpoint_object> bp_obj (bp->py_bp_object);
if (bp_obj != NULL)
{
+ if (bp_obj->is_finish_bp)
+ bpfinishpy_pre_delete_hook (bp_obj.get ());
+
if (!evregpy_no_listeners_p (gdb_py_events.breakpoint_deleted))
{
if (evpy_emit_event ((PyObject *) bp_obj.get (),
diff --git a/gdb/python/py-finishbreakpoint.c b/gdb/python/py-finishbreakpoint.c
index 159164e..7122fa8 100644
--- a/gdb/python/py-finishbreakpoint.c
+++ b/gdb/python/py-finishbreakpoint.c
@@ -349,16 +349,19 @@ bpfinishpy_out_of_scope (struct finish_breakpoint_object *bpfinish_obj)
if (meth_result == NULL)
gdbpy_print_stack ();
}
-
- delete_breakpoint (bpfinish_obj->py_bp.bp);
}
/* Callback for `bpfinishpy_detect_out_scope'. Triggers Python's
- `B->out_of_scope' function if B is a FinishBreakpoint out of its scope. */
+ `B->out_of_scope' function if B is a FinishBreakpoint out of its scope.
+
+ When DELETE_BP is true then breakpoint B will be deleted if B is a
+ FinishBreakpoint and it is out of scope, otherwise B will not be
+ deleted. */
static void
bpfinishpy_detect_out_scope_cb (struct breakpoint *b,
- struct breakpoint *bp_stopped)
+ struct breakpoint *bp_stopped,
+ bool delete_bp)
{
PyObject *py_bp = (PyObject *) b->py_bp_object;
@@ -379,7 +382,11 @@ bpfinishpy_detect_out_scope_cb (struct breakpoint *b,
if (b->pspace == current_inferior ()->pspace
&& (!target_has_registers ()
|| frame_find_by_id (initiating_frame) == NULL))
- bpfinishpy_out_of_scope (finish_bp);
+ {
+ bpfinishpy_out_of_scope (finish_bp);
+ if (delete_bp)
+ delete_breakpoint (finish_bp->py_bp.bp);
+ }
}
catch (const gdb_exception &except)
{
@@ -390,6 +397,17 @@ bpfinishpy_detect_out_scope_cb (struct breakpoint *b,
}
}
+/* Called when gdbpy_breakpoint_deleted is about to delete a breakpoint. A
+ chance to trigger the out_of_scope callback (if appropriate) for the
+ associated Python object. */
+
+void
+bpfinishpy_pre_delete_hook (struct gdbpy_breakpoint_object *bp_obj)
+{
+ breakpoint *bp = bp_obj->bp;
+ bpfinishpy_detect_out_scope_cb (bp, nullptr, false);
+}
+
/* Attached to `stop' notifications, check if the execution has run
out of the scope of any FinishBreakpoint before it has been hit. */
@@ -399,7 +417,8 @@ bpfinishpy_handle_stop (struct bpstat *bs, int print_frame)
gdbpy_enter enter_py;
for (breakpoint *bp : all_breakpoints_safe ())
- bpfinishpy_detect_out_scope_cb (bp, bs == NULL ? NULL : bs->breakpoint_at);
+ bpfinishpy_detect_out_scope_cb
+ (bp, bs == NULL ? NULL : bs->breakpoint_at, true);
}
/* Attached to `exit' notifications, triggers all the necessary out of
@@ -411,7 +430,7 @@ bpfinishpy_handle_exit (struct inferior *inf)
gdbpy_enter enter_py (target_gdbarch ());
for (breakpoint *bp : all_breakpoints_safe ())
- bpfinishpy_detect_out_scope_cb (bp, nullptr);
+ bpfinishpy_detect_out_scope_cb (bp, nullptr, true);
}
/* Initialize the Python finish breakpoint code. */
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index 55bbe78..258f5c4 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -777,6 +777,7 @@ extern const struct value_print_options *gdbpy_current_print_options;
void bpfinishpy_pre_stop_hook (struct gdbpy_breakpoint_object *bp_obj);
void bpfinishpy_post_stop_hook (struct gdbpy_breakpoint_object *bp_obj);
+void bpfinishpy_pre_delete_hook (struct gdbpy_breakpoint_object *bp_obj);
extern PyObject *gdbpy_doc_cst;
extern PyObject *gdbpy_children_cst;
diff --git a/gdb/testsuite/gdb.mi/mi-thread-bp-deleted.c b/gdb/testsuite/gdb.mi/mi-thread-bp-deleted.c
new file mode 100644
index 0000000..5ac23b4
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-thread-bp-deleted.c
@@ -0,0 +1,88 @@
+/* Copyright 2023 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/>. */
+
+#include <pthread.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#define NTHREAD 1
+
+/* Barrier for synchronising threads. */
+static pthread_barrier_t barrier;
+
+/* Wrapper around pthread_barrier_wait that aborts if the wait returns an
+ error. */
+static void
+barrier_wait (pthread_barrier_t *barrier)
+{
+ int res = pthread_barrier_wait (barrier);
+ if (res != PTHREAD_BARRIER_SERIAL_THREAD && res != 0)
+ abort ();
+}
+
+/* GDB can set this to 0 to force the 'spin' function to return. */
+volatile int do_spin = 1;
+
+void
+breakpt ()
+{
+ /* Nothing. */
+}
+
+/* Spin for a reasonably long while (this lets GDB run some commands in
+ async mode), but return early if/when do_spin is set to 0 (which is done
+ by GDB). */
+
+void
+spin ()
+{
+ int i;
+
+ for (i = 0; i < 300 && do_spin; ++i)
+ sleep (1);
+}
+
+void *
+thread_worker (void *arg)
+{
+ barrier_wait (&barrier);
+ return NULL;
+}
+
+int
+main ()
+{
+ pthread_t thr[NTHREAD];
+ int i;
+
+ if (pthread_barrier_init (&barrier, NULL, NTHREAD + 1) != 0)
+ abort ();
+
+ for (i = 0; i < NTHREAD; ++i)
+ pthread_create (&thr[i], NULL, thread_worker, NULL);
+
+ breakpt (); /* First breakpoint. */
+
+ barrier_wait (&barrier);
+
+ for (i = 0; i < NTHREAD; ++i)
+ pthread_join (thr[i], NULL);
+
+ spin ();
+
+ breakpt ();
+}
diff --git a/gdb/testsuite/gdb.mi/mi-thread-bp-deleted.exp b/gdb/testsuite/gdb.mi/mi-thread-bp-deleted.exp
new file mode 100644
index 0000000..0ebca92
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-thread-bp-deleted.exp
@@ -0,0 +1,290 @@
+# Copyright 2023 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/>.
+
+# Check that the =breakpoint-deleted notification for a thread-specific
+# breakpoint is sent as soon as the related thread exits, and not when the
+# inferior next stops.
+#
+# This test is based on gdb.threads/thread-bp-deleted.exp.
+
+load_lib mi-support.exp
+set MIFLAGS "-i=mi"
+
+# We need to do things a little differently when using the remote protocol.
+set is_remote \
+ [expr [target_info exists gdb_protocol] \
+ && ([string equal [target_info gdb_protocol] "remote"] \
+ || [string equal [target_info gdb_protocol] "extended-remote"])]
+
+standard_testfile
+
+if { [build_executable "failed to prepare" $testfile $srcfile \
+ {debug pthreads}] } {
+ return -1
+}
+
+foreach_mi_ui_mode mode {
+ mi_gdb_exit
+
+ if {$mode eq "separate"} {
+ set start_ops "separate-mi-tty"
+ } else {
+ set start_ops ""
+ }
+
+ # Restart, but enable non-stop mode, we need it for background
+ # execution.
+ save_vars { GDBFLAGS } {
+ append GDBFLAGS " -ex \"maint set target-non-stop on\""
+ append GDBFLAGS " -ex \"set mi-async on\""
+ mi_clean_restart $binfile $start_ops
+ }
+
+ mi_runto_main
+
+ if {![mi_detect_async]} {
+ unsupported "async-mode is required"
+ continue
+ }
+
+ mi_delete_breakpoints
+
+ # Place a breakpoint on 'breakpt' and run to this breakpoint.
+ mi_create_breakpoint "breakpt" "place breakpoint on breakpt"
+ set breakpt_num [mi_get_valueof "/d" "\$bpnum" "INVALID" \
+ "get number for breakpt breakpoint"]
+ mi_execute_to "exec-continue" "breakpoint-hit" "breakpt" "" \
+ ".*" ".*" {"" "disp=\"keep\""} \
+ "continue to breakpoint in breakpt"
+
+ # Now drain all the pending output from the CLI if we are using a separate
+ # UI.
+ if {$mode eq "separate"} {
+ with_spawn_id $gdb_main_spawn_id {
+ gdb_test_multiple "" "drain CLI output upto breakpoint" {
+ -re "Thread 1 \[^\r\n\]+ hit Breakpoint $decimal,\
+ breakpt \\(\\) at\
+ \[^\r\n\]+\r\n$decimal\\s+\[^\r\n\]+\r\n" {
+ pass $gdb_test_name
+ }
+ }
+ }
+ }
+
+ # This is just for convenience, this refers to the second thread the
+ # inferior spawns.
+ set worker_thread 2
+
+ # Create a thread-specific breakpoint.
+ mi_create_breakpoint "-p $worker_thread main" \
+ "place thread breakpoint on main" \
+ -thread "$worker_thread"
+ set bpnum [mi_get_valueof "/d" "\$bpnum" "INVALID" \
+ "get number for thread-specific breakpoint"]
+
+ set loc1 [mi_make_breakpoint -number "$breakpt_num"]
+ set loc2 [mi_make_breakpoint -number "$bpnum" -thread "$worker_thread"]
+ set table_2_locs [mi_make_breakpoint_table [list $loc1 $loc2]]
+ set table_1_locs [mi_make_breakpoint_table [list $loc1]]
+
+ mi_gdb_test "-break-info" \
+ "\\^done,$table_2_locs" \
+ "-break-info, expecting two locations"
+
+ # Resume the inferior, at this point the inferior will spin while
+ # we interact with it.
+ mi_send_resuming_command "exec-continue" "continue"
+
+ # Look for the thread-exited notification and the breakpoint-deleted
+ # notification. When using a single UI we see both the MI and CLI
+ # messages. When using a separate MI UI we only see the MI messages.
+ set saw_cli_thread_exited false
+ set saw_mi_thread_exited false
+ set saw_cli_bp_deleted false
+ set saw_mi_bp_deleted false
+
+ # When running with a remote target, the thread-exited event doesn't
+ # appear to be pushed from the target to GDB; instead GDB has to fetch the
+ # thread list from the target and spot that a thread exited.
+ #
+ # In order to achieve this, when running with a remote target we run the
+ # '-thread-info 99' command. There isn't a thread 99, but GDB doesn't
+ # know that until it fetches the thread list. By fetching the thread list
+ # GDB will spot that the thread we are interested in has exited.
+ if {$is_remote} {
+ set cmd "-thread-info 99"
+ set attempt_count 5
+ } else {
+ set cmd ""
+ set attempt_count 0
+ }
+
+
+ gdb_test_multiple $cmd "collect thread exited output" \
+ -prompt "$::mi_gdb_prompt$" {
+
+ -re "^~\"\\\[Thread \[^\r\n\]+ exited\\\]\\\\n\"\r\n" {
+ set saw_cli_thread_exited true
+ exp_continue
+ }
+
+ -re "^~\"Thread-specific breakpoint $bpnum deleted -\
+ thread $worker_thread no longer in the thread list\\.\\\\n\"\r\n" {
+ set saw_cli_bp_deleted true
+ exp_continue
+ }
+
+ -re "^=thread-exited,id=\"$worker_thread\",group-id=\"i1\"\r\n" {
+ set saw_mi_thread_exited true
+
+ # The order of the MI notifications depends on the order in which
+ # the observers where registered within GDB. If we have not seen
+ # the other MI notification yet then keep looking.
+ #
+ # Additionally, for remote targets, we're going to wait for the
+ # output of the '-thread-info 99' command before we check the
+ # results.
+ if {!$saw_mi_bp_deleted || $is_remote} {
+ exp_continue
+ }
+
+ # We get here with a native target; check we saw all the output
+ # that we expected.
+ if {$mode eq "separate"} {
+ gdb_assert { $saw_mi_thread_exited && $saw_mi_bp_deleted \
+ && !$saw_cli_thread_exited \
+ && !$saw_cli_bp_deleted } \
+ $gdb_test_name
+ } else {
+ gdb_assert { $saw_mi_thread_exited && $saw_mi_bp_deleted \
+ && $saw_cli_thread_exited \
+ && $saw_cli_bp_deleted } \
+ $gdb_test_name
+ }
+ }
+
+ -re "^=breakpoint-deleted,id=\"3\"\r\n" {
+ set saw_mi_bp_deleted true
+
+ # The order of the MI notifications depends on the order in which
+ # the observers where registered within GDB. If we have not seen
+ # the other MI notification yet then keep looking.
+ #
+ # Additionally, for remote targets, we're going to wait for the
+ # output of the '-thread-info 99' command before we check the
+ # results.
+ if {!$saw_mi_thread_exited || $is_remote} {
+ exp_continue
+ }
+
+ # We get here with a native target; check we saw all the output
+ # that we expected.
+ if {$mode eq "separate"} {
+ gdb_assert { $saw_mi_thread_exited && $saw_mi_bp_deleted \
+ && !$saw_cli_thread_exited \
+ && !$saw_cli_bp_deleted } \
+ $gdb_test_name
+ } else {
+ gdb_assert { $saw_mi_thread_exited && $saw_mi_bp_deleted \
+ && $saw_cli_thread_exited \
+ && $saw_cli_bp_deleted } \
+ $gdb_test_name
+ }
+ }
+
+ -re "^-thread-info 99\r\n" {
+ if {!$is_remote} {
+ fail "$gdb_test_name (unexpected output)"
+ }
+ # This is the command being echoed back, ignore it.
+ exp_continue
+ }
+
+ -re "^\\^done,threads=\\\[\\\]\r\n$::mi_gdb_prompt$" {
+
+ # This is the result of the '-thread-info 99' trick, which is only
+ # used in remote mode. If we see this in native mode then
+ # something has gone wrong.
+ if {!$is_remote} {
+ fail "$gdb_test_name (unexpected output)"
+ }
+
+ # If we've not seen any of the expected output yet then maybe the
+ # remote thread just hasn't exited yet. Wait a short while and
+ # try again.
+ if { !$saw_mi_thread_exited && !$saw_mi_bp_deleted \
+ && !$saw_cli_thread_exited && !$saw_cli_bp_deleted \
+ && $attempt_count > 0 } {
+ sleep 1
+ incr attempt_count -1
+ send_gdb "$cmd\n"
+ exp_continue
+ }
+
+ # The output has arrived! Check how we did. There are other bugs
+ # that come into play here which change what output we'll see.
+ if { $saw_mi_thread_exited && $saw_mi_bp_deleted \
+ && $saw_cli_thread_exited \
+ && $saw_cli_bp_deleted } {
+ kpass "gdb/30129" $gdb_test_name
+ } elseif { $saw_mi_thread_exited && $saw_mi_bp_deleted \
+ && !$saw_cli_thread_exited \
+ && $saw_cli_bp_deleted } {
+ kfail "gdb/30129" $gdb_test_name
+ } else {
+ fail "$gdb_test_name"
+ }
+ }
+ }
+
+ # When the MI is running on a separate UI the CLI message will be seen
+ # over there, but only if we are not running remote. When we are running
+ # remote then the thread-exited event will only be triggered as a result
+ # of user triggering a refresh of the thread list (hence the '-thread-info
+ # 99' trick above). By typing a command we change the current UI to the
+ # terminal we are typing at, as a result these CLI style message will
+ # actually appear on the MI when using a remote target.
+ if {$mode eq "separate" && !$is_remote} {
+ with_spawn_id $gdb_main_spawn_id {
+ set saw_thread_exited false
+ gdb_test_multiple "" "collect cli thread exited output" {
+ -re "\\\[Thread \[^\r\n\]+ exited\\\]\r\n" {
+ set saw_thread_exited true
+ exp_continue
+ }
+
+ -re "^Thread-specific breakpoint $bpnum deleted -\
+ thread $worker_thread no longer in the thread list\\.\r\n" {
+ gdb_assert { $saw_thread_exited } \
+ $gdb_test_name
+ }
+ }
+ }
+ }
+
+ mi_gdb_test "-break-info" \
+ "\\^done,$table_1_locs" \
+ "-break-info, expecting one location"
+
+ # Set 'do_spin' to zero, this allows the inferior to progress again; we
+ # should then hit the breakpoint in 'breakpt' again.
+ mi_gdb_test "set var do_spin = 0" \
+ [multi_line \
+ ".*=memory-changed,thread-group=\"i${decimal}\".addr=\"${hex}\",len=\"${hex}\"" \
+ "\\^done"] \
+ "set do_spin variable in inferior, inferior should now finish"
+ mi_expect_stop "breakpoint-hit" "breakpt" ".*" ".*" "$::decimal" \
+ {"" "disp=\"keep\""} "stop in breakpt at the end of the test"
+}
diff --git a/gdb/testsuite/gdb.threads/thread-bp-deleted.c b/gdb/testsuite/gdb.threads/thread-bp-deleted.c
new file mode 100644
index 0000000..5ac23b4
--- /dev/null
+++ b/gdb/testsuite/gdb.threads/thread-bp-deleted.c
@@ -0,0 +1,88 @@
+/* Copyright 2023 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/>. */
+
+#include <pthread.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#define NTHREAD 1
+
+/* Barrier for synchronising threads. */
+static pthread_barrier_t barrier;
+
+/* Wrapper around pthread_barrier_wait that aborts if the wait returns an
+ error. */
+static void
+barrier_wait (pthread_barrier_t *barrier)
+{
+ int res = pthread_barrier_wait (barrier);
+ if (res != PTHREAD_BARRIER_SERIAL_THREAD && res != 0)
+ abort ();
+}
+
+/* GDB can set this to 0 to force the 'spin' function to return. */
+volatile int do_spin = 1;
+
+void
+breakpt ()
+{
+ /* Nothing. */
+}
+
+/* Spin for a reasonably long while (this lets GDB run some commands in
+ async mode), but return early if/when do_spin is set to 0 (which is done
+ by GDB). */
+
+void
+spin ()
+{
+ int i;
+
+ for (i = 0; i < 300 && do_spin; ++i)
+ sleep (1);
+}
+
+void *
+thread_worker (void *arg)
+{
+ barrier_wait (&barrier);
+ return NULL;
+}
+
+int
+main ()
+{
+ pthread_t thr[NTHREAD];
+ int i;
+
+ if (pthread_barrier_init (&barrier, NULL, NTHREAD + 1) != 0)
+ abort ();
+
+ for (i = 0; i < NTHREAD; ++i)
+ pthread_create (&thr[i], NULL, thread_worker, NULL);
+
+ breakpt (); /* First breakpoint. */
+
+ barrier_wait (&barrier);
+
+ for (i = 0; i < NTHREAD; ++i)
+ pthread_join (thr[i], NULL);
+
+ spin ();
+
+ breakpt ();
+}
diff --git a/gdb/testsuite/gdb.threads/thread-bp-deleted.exp b/gdb/testsuite/gdb.threads/thread-bp-deleted.exp
new file mode 100644
index 0000000..019bddde
--- /dev/null
+++ b/gdb/testsuite/gdb.threads/thread-bp-deleted.exp
@@ -0,0 +1,210 @@
+# Copyright (C) 2023 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/>.
+
+# Check that 'maint info breakpoints' succeeds in the period of time
+# between a thread-specific breakpoint being deleted, and GDB next
+# stopping.
+#
+# There used to be a bug where GDB would try to lookup the thread for
+# the thread-specific breakpoint, the thread of course having been
+# deleted, couldn't be found, and GDB would end up dereferencing a
+# nullptr.
+
+standard_testfile
+
+if {[build_executable "failed to prepare" $testfile $srcfile \
+ {debug pthreads}] == -1} {
+ return -1
+}
+
+# We need to do things a little differently when using the remote protocol.
+set is_remote \
+ [expr [target_info exists gdb_protocol] \
+ && ([string equal [target_info gdb_protocol] "remote"] \
+ || [string equal [target_info gdb_protocol] "extended-remote"])]
+
+# This test requires background execution, which relies on non-stop mode.
+save_vars { GDBFLAGS } {
+ append GDBFLAGS " -ex \"maint set target-non-stop on\""
+ clean_restart ${binfile}
+}
+
+if {![runto_main]} {
+ return -1
+}
+
+# Check we hace non-stop mode. We do try to force this on above, but maybe
+# the target doesn't support non-stop mode, in which case (hopefully)
+# non-stop mode will still show as off, and this test should not be run.
+if {![is_target_non_stop]} {
+ unsupported "required non-stop mode"
+ return -1
+}
+
+delete_breakpoints
+
+gdb_breakpoint "breakpt"
+gdb_continue_to_breakpoint "continue to first breakpt call"
+set breakpt_num [get_integer_valueof "\$bpnum" "INVALID" \
+ "get number for breakpoint in breakpt"]
+
+# Check info threads just to confirm the thread numbering. The rest
+# of this script just assumes we have threads numbered 1 and 2.
+gdb_test "info threads" \
+ [multi_line \
+ "\\* 1\\s+Thread \[^\r\n\]+" \
+ " 2\\s+Thread \[^\r\n\]+"]
+
+set main_thread 1
+set worker_thread 2
+
+# Check the 'info breakpoints' output for the thread-specific breakpoint
+# numbered BPNUM. If EXPECTED is true then the breakpoint is expected to be
+# present, otherwise, the breakpoint is expected not to be present.
+
+proc check_for_thread_specific_breakpoint { testname bpnum expected } {
+ set saw_thread_specific_bp false
+ gdb_test_multiple "info breakpoints" $testname {
+ -re "^(\[^\r\n\]+)\r\n" {
+ set line $expect_out(1,string)
+ if { [regexp "$bpnum\\s+breakpoint\[^\r\n\]+ $::hex in main\
+ at \[^\r\n\]+" $line] } {
+ set saw_thread_specific_bp true
+ }
+ exp_continue
+ }
+ -re "^$::gdb_prompt $" {
+ set result [expr $expected ? $saw_thread_specific_bp \
+ : !$saw_thread_specific_bp]
+ gdb_assert { $result } $gdb_test_name
+ }
+ }
+}
+
+# Create a thread-specific breakpoint. This will never actually be hit; we
+# don't care, we just want to see GDB auto-delete this breakpoint.
+gdb_breakpoint "main thread $worker_thread" \
+ "create a thread-specific breakpoint"
+set bpnum [get_integer_valueof "\$bpnum" "INVALID" \
+ "get number for thread-specific breakpoint"]
+
+# Check the thread-specific breakpoint is present in 'info breakpoints'.
+check_for_thread_specific_breakpoint \
+ "check for thread-specific b/p before thread exit" $bpnum true
+
+# Continue in async mode. After this the worker thread will exit.
+# The -no-prompt-anchor is needed here as sometimes the exit of the
+# worker thread will happen so quickly that expect will see the
+# 'thread exited' message immediately after the prompt, which breaks
+# the normal gdb_test prompt anchoring.
+gdb_test -no-prompt-anchor "continue&" "Continuing\\."
+
+if {$is_remote} {
+ # Collect the output from GDB telling us that the thread exited.
+ # Unfortunately in the remote protocol the thread-exited event doesn't
+ # appear to be pushed to GDB, instead we rely on GDB asking about the
+ # threads (which isn't great).
+ #
+ # So, what we do here is ask about thread 99, which hopefully shouldn't
+ # exist, however, in order to answer that question GDB has to grab the
+ # thread list from the remote, at which point GDB will spot that one of
+ # the threads has exited, and will tell us about it.
+ #
+ # However, we might be too quick sending the 'info threads 99' command,
+ # so, if we see the output of that command without any thread exited
+ # text, we wait for a short while and try again. We wait for upto 5
+ # seconds (5 tries). However, this might mean on a _really_ slow
+ # machine that the thread still hasn't exited. I guess if we start
+ # seeing that then we can just update ATTEMPT_COUNT below.
+ set saw_thread_exited false
+ set saw_bp_deleted false
+ set attempt_count 5
+ gdb_test_multiple "info threads 99" "collect thread exited output" {
+ -re "info threads 99\r\n" {
+ exp_continue
+ }
+
+ -re "^\\\[Thread \[^\r\n\]+ exited\\\]\r\n" {
+ set saw_thread_exited true
+ exp_continue
+ }
+
+ -re "^Thread-specific breakpoint $bpnum deleted -\
+ thread $worker_thread no longer in the thread list\\.\r\n" {
+ set saw_bp_deleted true
+ exp_continue
+ }
+
+ -re "No threads match '99'\\.\r\n$gdb_prompt $" {
+ if {!$saw_thread_exited && !$saw_bp_deleted && $attempt_count > 0} {
+ sleep 1
+ incr attempt_count -1
+ send_gdb "info threads 99\n"
+ exp_continue
+ }
+
+ # When PR gdb/30129 is fixed then this can all be collapsed down
+ # into a single gdb_assert call. This is split out like this
+ # because the SAW_BP_DELETED part is working, and we want to
+ # spot if that stops working.
+ if { $saw_thread_exited && $saw_bp_deleted } {
+ kpass "gdb/30129" $gdb_test_name
+ } elseif {!$saw_thread_exited && $saw_bp_deleted} {
+ kfail "gdb/30129" $gdb_test_name
+ } else {
+ fail $gdb_test_name
+ }
+ }
+ }
+} else {
+ # Collect the output from GDB telling us that the thread exited.
+ set saw_thread_exited false
+ gdb_test_multiple "" "collect thread exited output" {
+ -re "\\\[Thread \[^\r\n\]+ exited\\\]\r\n" {
+ set saw_thread_exited true
+ exp_continue
+ }
+
+ -re "^Thread-specific breakpoint $bpnum deleted -\
+ thread $worker_thread no longer in the thread list\\.\r\n" {
+ gdb_assert { $saw_thread_exited } \
+ $gdb_test_name
+ }
+ }
+}
+
+# Check the thread-specific breakpoint is no longer present in 'info
+# breakpoints' output.
+check_for_thread_specific_breakpoint \
+ "check for thread-specific b/p before after exit" $bpnum false
+
+# Check the thread-specific breakpoint doesn't show up in the 'maint
+# info breakpoints' output. And also that this doesn't cause GDB to
+# crash, which it did at one point.
+gdb_test_lines "maint info breakpoints" "" ".*" \
+ -re-not "breakpoint\\s+keep\\s+y\\s+$hex\\s+in main at "
+
+# Set the do_spin variable in the inferior. This will cause it to drop out
+# of its spin loop and hit the next breakpoint. Remember, at this point the
+# inferior is still executing.
+gdb_test "print do_spin = 0" "\\\$$decimal = 0"
+
+# Collect the notification that the inferior has stopped.
+gdb_test_multiple "" "wait for stop" {
+ -re "Thread $main_thread \[^\r\n\]+ hit Breakpoint ${breakpt_num},\
+ breakpt \\(\\) \[^\r\n\]+\r\n$decimal\\s+\[^\r\n\]+\r\n" {
+ pass $gdb_test_name
+ }
+}
diff --git a/gdb/thread.c b/gdb/thread.c
index 1a852f7..5b47215 100644
--- a/gdb/thread.c
+++ b/gdb/thread.c
@@ -1431,6 +1431,8 @@ show_inferior_qualified_tids (void)
const char *
print_thread_id (struct thread_info *thr)
{
+ gdb_assert (thr != nullptr);
+
char *s = get_print_cell ();
if (show_inferior_qualified_tids ())