[3/6] Improve "info program"

Message ID 20221203211338.2264994-4-pedro@palves.net
State New
Headers
Series Eliminate infrun_thread_thread_exit observer |

Commit Message

Pedro Alves Dec. 3, 2022, 9:13 p.m. UTC
  With gdb.base/catch-follow-exec.exp, we currently see:

~~~~~~~~~~~~~~~
 (gdb)
 continue
 Continuing.
 process 693251 is executing new program: /usr/bin/ls
 [New inferior 2]
 [New process 693251]
 [Switching to process 693251]

 Thread 2.1 "ls" hit Catchpoint 2 (exec'd /usr/bin/ls), 0x00007ffff7fd0100 in _start () from /lib64/ld-linux-x86-64.so.2
 (gdb)
 info prog
 No selected thread.
~~~~~~~~~~~~~~~

Note the "No selected thread" output.  That is totally bogus, because
there _is_ a selected thread.  What GDB really means, is that it can't
find the thread that had the latest (user-visible) stop.  And that
happens because "info program" gets that info from
get_last_target_status, and the last target status has been cleared.

However, GDB also checks if there is a selected thread, here:

  if (ptid == null_ptid || ptid == minus_one_ptid)
    error (_("No selected thread."));

.. the null_ptid part.  That is also bogus, because what matters is
the thread that last reported a stop, not the current thread:

 - in all-stop mode, "info program" displays info about the last stop.
   That may have happened on a thread different from the selected
   thread.

 - in non-stop mode, because all threads are controlled individually,
   "info program" shows info about the last stop of the selected
   thread.

The current code already behaves this way, though in a poor way.  This
patch reimplements it, such that the all-stop version now finds the
thread that last reported an event via the 'previous_thread' strong
reference.  Being a strong reference means that if that thread has
exited since the event was reported, 'previous_thread' will still
point to it, so we can say that the thread exited meanwhile.

The patch also extends "info program" output a little, to let the user
know which thread we are printing info for.  For example, for the
gdb.base/catch-follow-exec.exp case we shown above, we now get:

 (gdb) info prog
 Last stopped for thread 2.1 (process 710867).
	 Using the running image of child process 710867.
 Program stopped at 0x7ffff7fd0100.
 It stopped at breakpoint 2.
 Type "info stack" or "info registers" for more information.
 (gdb)

while in non-stop mode, we get:

 (gdb) info prog
 Selected thread 2.1 (process 710867).
	 Using the running image of child process 710867.
 Program stopped at 0x7ffff7fd0100.
 It stopped at breakpoint 2.
 Type "info stack" or "info registers" for more information.
 (gdb)

In both cases, the first line of output is new.

The existing code considered these running/exited cases as an error,
but I think that that's incorrect, since this is IMO just plain
execution info as well.  So the patch makes those cases regular
prints, not errors.

If the thread is running, we get, in non-stop mode:

 (gdb) info prog
 Selected thread 2.1 (process 710867).
 Selected thread is running.

... and in all-stop:

 (gdb) info prog
 Last stopped for thread 2.1 (process 710867).
 Thread is now running.

If the thread has exited, we get, in non-stop mode:

 (gdb) info prog
 Selected thread 2.1 (process 710867).
 Selected thread has exited.

... and in all-stop:

 (gdb) info prog
 Last stopped for thread 2.1 (process 710867).
 Thread has since exited.

The gdb.base/info-program.exp testcase was much extended to test
all-stop/non-stop and single-threaded/multi-threaded.

Change-Id: I51d9d445f772d872af3eead3449ad4aa445781b1
---
 gdb/infcmd.c                                 |  79 ++++++++---
 gdb/infrun.c                                 |  13 +-
 gdb/infrun.h                                 |   3 +
 gdb/testsuite/gdb.base/catch-follow-exec.exp |  17 ++-
 gdb/testsuite/gdb.base/info-program.c        |  66 ++++++++++
 gdb/testsuite/gdb.base/info-program.exp      | 131 ++++++++++++++++---
 6 files changed, 263 insertions(+), 46 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/info-program.c
  

Patch

diff --git a/gdb/infcmd.c b/gdb/infcmd.c
index bffe8d554e7..744587504b3 100644
--- a/gdb/infcmd.c
+++ b/gdb/infcmd.c
@@ -1937,37 +1937,74 @@  finish_command (const char *arg, int from_tty)
 static void
 info_program_command (const char *args, int from_tty)
 {
-  bpstat *bs;
-  int num, stat;
-  ptid_t ptid;
-  process_stratum_target *proc_target;
+  scoped_restore_current_thread restore_thread;
 
-  if (!target_has_execution ())
-    {
-      gdb_printf (_("The program being debugged is not being run.\n"));
-      return;
-    }
+  thread_info *tp;
+
+  /* In non-stop, since every thread is controlled individually, we'll
+     show execution info about the current thread.  In all-stop, we'll
+     show execution info about the last stop.  */
 
   if (non_stop)
     {
-      ptid = inferior_ptid;
-      proc_target = current_inferior ()->process_target ();
+      if (!target_has_execution ())
+	{
+	  gdb_printf (_("The program being debugged is not being run.\n"));
+	  return;
+	}
+
+      if (inferior_ptid == null_ptid)
+	error (_("No selected thread."));
+
+      tp = inferior_thread ();
+
+      gdb_printf (_("Selected thread %s (%s).\n"),
+		  print_thread_id (tp),
+		  target_pid_to_str (tp->ptid).c_str ());
+
+      if (tp->state == THREAD_EXITED)
+	{
+	  gdb_printf (_("Selected thread has exited.\n"));
+	  return;
+	}
+      else if (tp->state == THREAD_RUNNING)
+	{
+	  gdb_printf (_("Selected thread is running.\n"));
+	  return;
+	}
     }
   else
-    get_last_target_status (&proc_target, &ptid, nullptr);
+    {
+      tp = get_previous_thread ();
+
+      if (tp == nullptr)
+	{
+	  gdb_printf (_("The program being debugged is not being run.\n"));
+	  return;
+	}
 
-  if (ptid == null_ptid || ptid == minus_one_ptid)
-    error (_("No selected thread."));
+      switch_to_thread (tp);
 
-  thread_info *tp = find_thread_ptid (proc_target, ptid);
+      gdb_printf (_("Last stopped for thread %s (%s).\n"),
+		  print_thread_id (tp),
+		  target_pid_to_str (tp->ptid).c_str ());
 
-  if (tp->state == THREAD_EXITED)
-    error (_("Invalid selected thread."));
-  else if (tp->state == THREAD_RUNNING)
-    error (_("Selected thread is running."));
+      if (tp->state == THREAD_EXITED)
+	{
+	  gdb_printf (_("Thread has since exited.\n"));
+	  return;
+	}
+
+      if (tp->state == THREAD_RUNNING)
+	{
+	  gdb_printf (_("Thread is now running.\n"));
+	  return;
+	}
+    }
 
-  bs = tp->control.stop_bpstat;
-  stat = bpstat_num (&bs, &num);
+  int num;
+  bpstat *bs = tp->control.stop_bpstat;
+  int stat = bpstat_num (&bs, &num);
 
   target_files_info ();
   gdb_printf (_("Program stopped at %s.\n"),
diff --git a/gdb/infrun.c b/gdb/infrun.c
index a936f18fbb1..252f6e5eca5 100644
--- a/gdb/infrun.c
+++ b/gdb/infrun.c
@@ -149,8 +149,9 @@  show_step_stop_if_no_debug (struct ui_file *file, int from_tty,
 }
 
 /* proceed and normal_stop use this to notify the user when the
-   inferior stopped in a different thread than it had been running
-   in.  */
+   inferior stopped in a different thread than it had been running in.
+   It can also be used to find for which thread normal_stop last
+   reported a stop.  */
 static thread_info_ref previous_thread;
 
 /* See infrun.h.  */
@@ -164,6 +165,14 @@  update_previous_thread ()
     previous_thread = thread_info_ref::new_reference (inferior_thread ());
 }
 
+/* See infrun.h.  */
+
+thread_info *
+get_previous_thread ()
+{
+  return previous_thread.get ();
+}
+
 /* If set (default for legacy reasons), when following a fork, GDB
    will detach from one of the fork branches, child or parent.
    Exactly which branch is detached depends on 'set follow-fork-mode'
diff --git a/gdb/infrun.h b/gdb/infrun.h
index acaec9b5064..18dd3c2ff60 100644
--- a/gdb/infrun.h
+++ b/gdb/infrun.h
@@ -121,6 +121,9 @@  extern enum exec_direction_kind execution_direction;
    inferior_thread, or at nullptr, if there's no selected thread.  */
 extern void update_previous_thread ();
 
+/* Get a weak reference to 'previous_thread'.  */
+extern thread_info *get_previous_thread ();
+
 extern void start_remote (int from_tty);
 
 /* Clear out all variables saying what to do when inferior is
diff --git a/gdb/testsuite/gdb.base/catch-follow-exec.exp b/gdb/testsuite/gdb.base/catch-follow-exec.exp
index 08aab086b60..e2e19981c0f 100644
--- a/gdb/testsuite/gdb.base/catch-follow-exec.exp
+++ b/gdb/testsuite/gdb.base/catch-follow-exec.exp
@@ -31,16 +31,25 @@  proc catch_follow_exec { } {
 	return -1
     }
 
-    gdb_test "catch exec" \
-	{Catchpoint [0-9]+ \(exec\)}
+    set bpnum ""
+    gdb_test_multiple "catch exec" "" {
+	-wrap -re "Catchpoint ($::decimal) \\\(exec\\\)" {
+	    set bpnum $expect_out(1,string)
+	}
+    }
+    if {$bpnum == ""} {
+	return
+    }
 
     gdb_test_no_output "set follow-exec-mode new"
 
     gdb_test "continue" \
-	".*hit Catchpoint.*"
+	"Thread 2.1 .*hit Catchpoint $bpnum.*"
+
+    set any "\[^\r\n\]*"
 
     gdb_test "info prog" \
-	"No selected thread."
+	"Last stopped for thread 2.1 \\\($any\\\)\\..*It stopped at breakpoint $bpnum\\..*"
 }
 
 catch_follow_exec
diff --git a/gdb/testsuite/gdb.base/info-program.c b/gdb/testsuite/gdb.base/info-program.c
new file mode 100644
index 00000000000..fcb3c614f5f
--- /dev/null
+++ b/gdb/testsuite/gdb.base/info-program.c
@@ -0,0 +1,66 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2022 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/>.  */
+
+#ifdef USE_THREADS
+
+#include <pthread.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+pthread_barrier_t barrier;
+pthread_t child_thread;
+
+void *
+child_function (void *arg)
+{
+  pthread_barrier_wait (&barrier);
+
+  while (1)
+    usleep (100);
+
+  pthread_exit (NULL);
+}
+
+#endif /* USE_THREADS */
+
+static void
+done (void)
+{
+}
+
+int
+main (void)
+{
+#ifdef USE_THREADS
+  int res;
+
+  alarm (300);
+
+  pthread_barrier_init (&barrier, NULL, 2);
+
+  res = pthread_create (&child_thread, NULL, child_function, NULL);
+  pthread_barrier_wait (&barrier);
+#endif /* USE_THREADS */
+
+  done ();
+
+#ifdef USE_THREADS
+  pthread_join (child_thread, NULL);
+#endif /* USE_THREADS */
+
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/info-program.exp b/gdb/testsuite/gdb.base/info-program.exp
index 1496ea518dd..a1c779354b6 100644
--- a/gdb/testsuite/gdb.base/info-program.exp
+++ b/gdb/testsuite/gdb.base/info-program.exp
@@ -13,31 +13,124 @@ 
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-standard_testfile normal.c
+# Test "info program".
+#
+# We build both single-threaded and multi-threaded programs so that if
+# the target doesn't support multi-threading, we still exercise the
+# command.
+#
+# With the multi-threaded program, we test that in all-stop mode, GDB
+# prints information about the last thread that stopped, not the
+# current thread.  In non-stop mode, the command always prints info
+# about the selected thread, so we test that.
 
-if { [prepare_for_testing "failed to prepare" $testfile $srcfile] } {
-    return -1
-}
+standard_testfile
 
-if { ![runto_main] } {
-    return -1
-}
+# Run the test with the given parameters:
+#
+#   - THREADS: threads flavor, either single-threaded or
+#              multi-threaded.
+#   - NON-STOP: "set non-stop" value, "on" or "off".
+
+proc do_test { threads non-stop } {
+    save_vars { ::GDBFLAGS } {
+	append ::GDBFLAGS " -ex \"set non-stop ${non-stop}\""
+	clean_restart $::binfile-$threads
+    }
+
+    gdb_test "info program" \
+	"The program being debugged is not being run." \
+	"info program before run"
+
+    if { ![runto done] } {
+	return -1
+    }
+
+    if {${non-stop} == "on"} {
+	set thread_line "Selected"
+    } else {
+	set thread_line "Last stopped for"
+    }
 
-gdb_test "info program" "Program stopped at $hex\.\r\nIt stopped at breakpoint $decimal\.\r\nType \"info stack\" or \"info registers\" for more information\." \
-    "info program after run to main"
+    gdb_test "info program" \
+	[multi_line \
+	     "$thread_line thread 1 (\[^\r\n\]+)\\." \
+	     ".*" \
+	     "Program stopped at $::hex\." \
+	     "It stopped at breakpoint $::decimal\." \
+	     "Type \"info stack\" or \"info registers\" for more information\."] \
+	"info program after run to main"
 
-# We don't really care where this step lands, so long as GDB reports
-# that the inferior stopped  due to a step in the subsequent test.
-gdb_test "next" ".*" "step before info program"
+    # We don't really care where this step lands, so long as GDB reports
+    # that the inferior stopped  due to a step in the subsequent test.
+    gdb_test "next" ".*" "step before info program"
 
-gdb_test "info program" "Program stopped at $hex\.\r\nIt stopped after being stepped\.\r\nType \"info stack\" or \"info registers\" for more information\." \
-    "info program after next"
+    gdb_test "info program" \
+	[multi_line \
+	     "$thread_line thread 1 (\[^\r\n\]+)\\." \
+	     ".*" \
+	     "Program stopped at $::hex\." \
+	     "It stopped after being stepped\." \
+	     "Type \"info stack\" or \"info registers\" for more information\."] \
+	"info program after next"
 
-if {![runto_main]} {
-    return -1
+    if {$threads == "mt"} {
+	gdb_test "thread 2" "\\\[Switching to thread 2 .*"
+
+	if {${non-stop} == "on"} {
+	    gdb_test "info program" \
+		[multi_line \
+		     "$thread_line thread 2 (\[^\r\n\]+)\\." \
+		     "Selected thread is running\\."] \
+		"info program after next, other thread"
+	} else {
+	    gdb_test "info program" \
+		[multi_line \
+		     "$thread_line thread 1 (\[^\r\n\]+)\\." \
+		     ".*" \
+		     "Program stopped at $::hex\." \
+		     "It stopped after being stepped\." \
+		     "Type \"info stack\" or \"info registers\" for more information\."] \
+		"info program after next, other thread"
+	}
+    }
+
+    gdb_test "kill" "" "kill program" \
+	"Kill the program being debugged.*y or n. $" "y"
+
+    gdb_test "info program" "The program being debugged is not being run." \
+	"info program, after kill"
+
+    if { ![runto done] } {
+	return -1
+    }
+
+    delete_breakpoints
+
+    gdb_test "info program" \
+	[multi_line \
+	     "$thread_line thread 1 (\[^\r\n\]+)\\." \
+	     ".*" \
+	     "Program stopped at $::hex\." \
+	     "It stopped at a breakpoint that has since been deleted\." \
+	     "Type \"info stack\" or \"info registers\" for more information\."] \
+	"info program after deleting all breakpoints"
 }
 
-delete_breakpoints
+# Build executables and test them, one for each
+# single-thread/multi-thread flavor.
+foreach_with_prefix threads {st mt} {
+    set opts {debug}
+    if {$threads == "mt"} {
+	lappend opts pthreads "additional_flags=-DUSE_THREADS"
+    }
 
-gdb_test "info program" "Program stopped at $hex\.\r\nIt stopped at a breakpoint that has since been deleted\.\r\nType \"info stack\" or \"info registers\" for more information\." \
-    "info program after deleting all breakpoints"
+    if { [build_executable "failed to prepare $threads" \
+	    ${testfile}-${threads} ${srcfile} $opts] } {
+	continue
+    }
+
+    foreach_with_prefix non-stop {on off} {
+	do_test ${threads} ${non-stop}
+    }
+}