[PATCHv2 1/2] gdb: Add 'maint info inline-frames' command

Andrew Burgess aburgess@redhat.com
Tue Jul 30 12:31:17 GMT 2024


While reviewing a patch I wanted to view GDB's inline frame state.  I
don't believe there's currently a maintenance command to view this
information, so in this commit I've added one.

The new command is:

  maintenance info inline-frames
  maintenance info inline-frames ADDRESS

The command lists the inline frames that start at ADDRESS, or at the
current $pc if no ADDRESS is given.  The command also displays the
"outer" function in which the inline functions are present.

An example of the command output:

  (gdb) maintenance info inline-frames
  program counter = 0x401137
  skipped frames = 1
    bar
  > foo
    main
  (gdb)

This tells us that function 'main' called 'foo' which called 'bar'.
The functions 'foo' and 'bar' are both inline and both start at the
address 0x401137.  Currently GDB considers the inferior to be stopped
in frame 'foo' (note the '>' marker), this means that there is 1
skipped frame (function 'bar').

The function 'main' is the outer function.  The outer function might
not start at 0x401137, it is simply the function that contains the
inline functions.

If the user does a 'step' then GDB will not actually move the inferior
forward, but will instead simply tell the user that the inferior
entered 'bar'.  The output of 'maint info inline-frames' will change
like this:

  (gdb) step
  bar () at inline.c:6
  6	  ++global_counter;
  (gdb) maintenance info inline-frames
  program counter = 0x401137
  skipped frames = 0
  > bar
    foo
    main
  (gdb)

Now GDB is in function 'bar' and there are no skipped frames.

I've added a basic test for the new command.  Please excuse the file
name for the new test, in the next commit I'll be adding additional
tests and at that point the file name will make sense.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
---
 gdb/NEWS                                      |   7 +
 gdb/doc/gdb.texinfo                           | 108 ++++++++++++
 gdb/inline-frame.c                            | 163 ++++++++++++++++--
 .../maint-info-inline-frames-and-blocks.c     |  57 ++++++
 .../maint-info-inline-frames-and-blocks.exp   | 147 ++++++++++++++++
 5 files changed, 467 insertions(+), 15 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/maint-info-inline-frames-and-blocks.c
 create mode 100644 gdb/testsuite/gdb.base/maint-info-inline-frames-and-blocks.exp

diff --git a/gdb/NEWS b/gdb/NEWS
index 9a5543c0443..871c6d3dafa 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -17,6 +17,13 @@
   This may cause breakage when using an incompatible libc, like uclibc or
   newlib, or an older glibc.
 
+* New commands
+
+maintenance info inline-frames [ADDRESS]
+  New command which displays GDB's inline-frame information for the
+  current address, or for ADDRESS if specified.  The output identifies
+  inlined frames which start at the specified address.
+
 *** Changes in GDB 15
 
 * The MPX commands "show/set mpx bound" have been deprecated, as Intel
diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index 147b8d4c24e..355115f7fbf 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -41860,6 +41860,114 @@
 frame-id for frame #2: @{stack=0x7fffffffac90,code=0x000000000040111c,!special@}
 @end smallexample
 
+@item maint info inline-frames
+@itemx maint info inline-frames @var{address}
+@cindex frames of inlined functions
+@kindex maint info inline-frames
+Print information about inlined frames which start at the current
+address, or @var{address} if specified.
+
+In order to allow the user to correctly step into inlined functions,
+@value{GDBN} needs to identify which inlined functions start at a
+particular address, and @value{GDBN} also needs to track which of
+these functions was last displayed to the user as the current frame.
+
+Imagine a situation where function @code{main} calls @code{foo}, which
+then calls @code{bar}, something like this:
+
+@smallexample
+@group
+int
+main ()
+@{
+  /* Some interesting code here...  */
+
+  foo ();
+
+  /* More interesting code here... */
+@}
+
+void
+foo ()
+@{
+  bar ();
+@}
+
+void
+bar ()
+@{
+  /* Some interesting code here...  */
+@}
+@end group
+@end smallexample
+
+As both @code{foo} and @code{bar} are inlined within @code{main} then
+there could be one address within @code{main} which is also the start
+of @code{foo} and also the start of @code{bar}.  When the user stops
+at this address they will initially be told the inferior is in
+@code{main}, if the user does a @kbd{step} then @value{GDBN} doesn't
+actually step the inferior, instead the user is told the inferior
+entered @code{foo}.  After the next @kbd{step} the user is told the
+inferior entered @code{bar}.  The @kbd{maint info inline-frames}
+command can be used to view this internal @value{GDBN} state, like
+this:
+
+@smallexample
+@group
+(@value{GDBP}) step
+24	  foo ();
+(@value{GDBP}) maintenance info inline-frames
+program counter = 0x401137
+skipped frames = 2
+  bar
+  foo
+> main
+@end group
+@end smallexample
+
+Here the user is stopped in @code{main} at the call to @code{foo}.  The
+inline-frames information shows that at this address @value{GDBN} has
+found the start of inlined functions @code{bar} and @code{foo}, but
+currently @value{GDBN} has skipped 2 frames and considers @code{main}
+to be the current frame, this is indicated with the @samp{>}.
+
+If the user performs a @kbd{step} to enter @code{foo} then the
+situation is updated:
+
+@smallexample
+@group
+(@value{GDBP}) step
+foo () at inline.c:14
+14	  bar ();
+(@value{GDBP}) maintenance info inline-frames
+program counter = 0x401137
+skipped frames = 1
+  bar
+> foo
+  main
+@end group
+@end smallexample
+
+Notice that the program counter value @code{0x401137} hasn't change,
+but now @value{GDBN} considers @code{foo} to be the current frame, and
+it is marked as such with the @samp{>}.
+
+Finally, the user performs another @kbd{step} to enter @code{bar}:
+
+@smallexample
+@group
+(@value{GDBP}) step
+bar () at inline.c:6
+6	  ++global_counter;
+(@value{GDBP}) maintenance info inline-frames
+program counter = 0x401137
+skipped frames = 0
+> bar
+  foo
+  main
+@end group
+@end smallexample
+
 @kindex maint print registers
 @kindex maint print raw-registers
 @kindex maint print cooked-registers
diff --git a/gdb/inline-frame.c b/gdb/inline-frame.c
index f65f39be40d..96cea9eeb1e 100644
--- a/gdb/inline-frame.c
+++ b/gdb/inline-frame.c
@@ -27,6 +27,7 @@
 #include "regcache.h"
 #include "symtab.h"
 #include "frame.h"
+#include "cli/cli-cmds.h"
 #include <algorithm>
 
 /* We need to save a few variables for every thread stopped at the
@@ -336,25 +337,28 @@ stopped_by_user_bp_inline_frame (const block *frame_block, bpstat *stop_chain)
   return false;
 }
 
-/* See inline-frame.h.  */
+/* Gather information about inlined frames that start at THIS_PC.
+   STOP_CHAIN indicates where GDB has just stopped.  *SKIPPED_SYMS will
+   have the symbols for all skipped frames at THIS_PC appended too it.  If
+   OUTER_SYMBOL is not nullptr then *OUTER_SYMBOL is set to the outer
+   function symbol, this is the function that contains all of the skipped
+   functions.  If the outer function can't be established then
+   *OUTER_SYMBOL will be set to nullptr.  */
 
-void
-skip_inline_frames (thread_info *thread, bpstat *stop_chain)
+static int
+gather_inline_frame_info (CORE_ADDR this_pc, bpstat *stop_chain,
+			  std::vector<struct symbol *> *skipped_syms,
+			  const struct symbol **outer_symbol)
 {
-  const struct block *frame_block, *cur_block;
-  std::vector<struct symbol *> skipped_syms;
   int skip_count = 0;
 
-  /* This function is called right after reinitializing the frame
-     cache.  We try not to do more unwinding than absolutely
-     necessary, for performance.  */
-  CORE_ADDR this_pc = get_frame_pc (get_current_frame ());
-  frame_block = block_for_pc (this_pc);
+  if (outer_symbol != nullptr)
+    *outer_symbol = nullptr;
 
-  if (frame_block != NULL)
+  const struct block *cur_block = block_for_pc (this_pc);
+  if (cur_block != NULL)
     {
-      cur_block = frame_block;
-      while (cur_block->superblock ())
+      while (cur_block->superblock () != nullptr)
 	{
 	  if (cur_block->inlined_p ())
 	    {
@@ -370,18 +374,38 @@ skip_inline_frames (thread_info *thread, bpstat *stop_chain)
 		    break;
 
 		  skip_count++;
-		  skipped_syms.push_back (cur_block->function ());
+		  skipped_syms->push_back (cur_block->function ());
 		}
 	      else
 		break;
 	    }
-	  else if (cur_block->function () != NULL)
+	  else if (cur_block->function () != nullptr)
 	    break;
 
 	  cur_block = cur_block->superblock ();
 	}
     }
 
+  if (outer_symbol != nullptr && cur_block != nullptr)
+    *outer_symbol = cur_block->function ();
+
+  return skip_count;
+}
+
+/* See inline-frame.h.  */
+
+void
+skip_inline_frames (thread_info *thread, bpstat *stop_chain)
+{
+  std::vector<struct symbol *> skipped_syms;
+
+  /* This function is called right after reinitializing the frame
+     cache.  We try not to do more unwinding than absolutely
+     necessary, for performance.  */
+  CORE_ADDR this_pc = get_frame_pc (get_current_frame ());
+  int skip_count = gather_inline_frame_info (this_pc, stop_chain,
+					     &skipped_syms, nullptr);
+
   gdb_assert (find_inline_frame_state (thread) == NULL);
   inline_states.emplace_back (thread, skip_count, this_pc,
 			      std::move (skipped_syms));
@@ -460,3 +484,112 @@ frame_inlined_callees (const frame_info_ptr &this_frame)
 
   return inline_count;
 }
+
+/* The 'maint info inline-frames' command.  Takes an optional address
+   expression.  If an address is passed then list inline frames that start
+   at the given address.  If no address is given then list the current
+   thread's current inline frame state.  */
+
+static void
+maintenance_info_inline_frames (const char *arg, int from_tty)
+{
+  int skipped_frame_count;
+  std::vector<struct symbol *> *skipped_sym_vec;
+  CORE_ADDR addr;
+
+  /* With no argument then the user wants to know about the current inline
+     frame information.  This information is cached per-thread and can be
+     updated as the user steps between inline functions at the current
+     address.
+
+     If there is an argument then parse it as an address, the user is
+     asking about inline functions that start at that address.  */
+  if (arg == nullptr)
+    {
+      if (inferior_ptid == null_ptid)
+	error (_("no inferior thread"));
+
+      thread_info *thread = inferior_thread ();
+
+      auto it = std::find_if (inline_states.begin (), inline_states.end (),
+			      [thread] (const inline_state &istate)
+			      {
+				return thread == istate.thread;
+			      });
+
+      if (it == inline_states.end ())
+	{
+	  gdb_printf (_("No inline frame info for current thread.\n"));
+	  return;
+	}
+
+      skipped_frame_count = it->skipped_frames;
+      skipped_sym_vec = &it->skipped_symbols;
+      addr = it->saved_pc;
+    }
+  else
+    addr = parse_and_eval_address (arg);
+
+  /* The cached inline frame information only tracks skipped frames.  In
+     order to display the outer function (that contains the inline frames)
+     we always gather the complete information here, even if we are using
+     some details from the previous cached information.  */
+  bpstat stat;
+  std::vector<struct symbol *> skipped_syms;
+  const struct symbol *outer_symbol;
+  int skipped_frames = gather_inline_frame_info (addr, &stat, &skipped_syms,
+						 &outer_symbol);
+  if (arg != nullptr)
+    {
+      skipped_frame_count = skipped_frames;
+      skipped_sym_vec = &skipped_syms;
+    }
+
+  gdb_printf (_("program counter = %s\n"), core_addr_to_string_nz (addr));
+  gdb_printf (_("skipped frames = %d\n"), skipped_frame_count);
+
+  int i;
+  for (i = 0; i < skipped_syms.size (); ++i)
+    {
+      std::string tail;
+
+      if (i < skipped_sym_vec->size ()
+	  && (*skipped_sym_vec)[i] != skipped_syms[i])
+	tail = string_printf (_("\t(expected %s)"),
+			      (*skipped_sym_vec)[i]->print_name ());
+
+      gdb_printf (_("%c %s%s\n"), (i == skipped_frame_count ? '>' : ' '),
+		  skipped_syms[i]->print_name (),
+		  tail.c_str ());
+    }
+
+  if (outer_symbol != nullptr)
+    gdb_printf (_("%c %s\n"),
+		(i == skipped_frame_count ? '>' : ' '),
+		outer_symbol->print_name ());
+  else
+    gdb_printf (_("  Failed to find an outer function\n"));
+}
+
+
+
+void _initialize_inline_frame ();
+void
+_initialize_inline_frame ()
+{
+  add_cmd ("inline-frames", class_maintenance, maintenance_info_inline_frames,
+	   _("\
+Display inline frame information for current thread.\n\
+\n\
+Usage:\n\
+\n\
+  maintenance info inline-frames [ADDRESS]\n\
+\n\
+With no ADDRESS show all inline frames starting at the current program\n\
+counter address.  When ADDRESS is given, list all inline frames starting\n\
+at ADDRESS.\n\
+\n\
+The last frame listed might not start at ADDRESS, this is the frame that\n\
+contains the other inline frames."),
+	   &maintenanceinfolist);
+}
diff --git a/gdb/testsuite/gdb.base/maint-info-inline-frames-and-blocks.c b/gdb/testsuite/gdb.base/maint-info-inline-frames-and-blocks.c
new file mode 100644
index 00000000000..35b20648cd5
--- /dev/null
+++ b/gdb/testsuite/gdb.base/maint-info-inline-frames-and-blocks.c
@@ -0,0 +1,57 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2024 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/>.  */
+
+static void inline_func_a (void);
+static void inline_func_b (void);
+static void normal_func (void);
+
+volatile int global_var = 0;
+
+static void __attribute__((noinline))
+normal_func (void)
+{
+  /* Do some work.  */
+  ++global_var;
+  ++global_var;
+
+  /* Now the inline function.  */
+  inline_func_a ();
+
+  /* Do some work.  */
+  ++global_var;		/* After inline function.  */
+  ++global_var;
+}
+
+static inline void __attribute__((__always_inline__))
+inline_func_a (void)
+{
+  inline_func_b ();
+}
+
+static inline void __attribute__((__always_inline__))
+inline_func_b (void)
+{
+  ++global_var;
+  ++global_var;
+}
+
+int
+main ()
+{
+  normal_func ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/maint-info-inline-frames-and-blocks.exp b/gdb/testsuite/gdb.base/maint-info-inline-frames-and-blocks.exp
new file mode 100644
index 00000000000..f5c5fd2623e
--- /dev/null
+++ b/gdb/testsuite/gdb.base/maint-info-inline-frames-and-blocks.exp
@@ -0,0 +1,147 @@
+# Copyright (C) 2024 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 the 'maint info inline-frames' command.
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile} \
+	  {debug nopie}]} {
+    return -1
+}
+
+if {![runto normal_func]} {
+    return 0
+}
+
+# Next forward until we find the call to inline_func_a().  The hope is
+# that when we see the 'inline_func_a()' line this will be the start of
+# the inlined function.  This might not be the case on all
+# architectures if the compiler needs to perform some preamble.
+gdb_test_multiple "next" "next forward to inline_func_a" {
+    -re "^$decimal\\s+inline_func_a \\(\\);\r\n" {
+	# Consume the next prompt.
+	gdb_expect {
+	    -re "^$gdb_prompt $" {}
+	}
+	pass $gdb_test_name
+    }
+
+    -re "^$decimal\\s+\[^\r\n\]+After inline function\[^\r\n\]+\r\n" {
+	# We've gone too far!
+	fail $gdb_test_name
+    }
+
+    -re "^$decimal\\s+\[^\r\n\]+\r\n" {
+	send_gdb "next\n"
+	exp_continue
+    }
+
+    -re "^\[^\r\n\]+\r\n" {
+	exp_continue
+    }
+}
+
+# View the inline frame information.  This should display that we are
+# at the start of inline_func_a() within normal_func().
+gdb_test "maint info inline-frames" \
+    [multi_line \
+	 "^program counter = $hex" \
+	 "skipped frames = 2" \
+	 "  inline_func_b" \
+	 "  inline_func_a" \
+	 "> normal_func"] \
+    "check inline-frames state when in normal_func"
+
+# Step, we should now enter the inlined function.
+gdb_test "step" ".*" \
+    "step to enter inline_func"
+
+# And the inline-frames information should update.
+gdb_test "maint info inline-frames" \
+    [multi_line \
+	 "^program counter = $hex" \
+	 "skipped frames = 1" \
+	 "  inline_func_b" \
+	 "> inline_func_a" \
+	 "  normal_func"] \
+    "check inline-frames state when just entered inline_func_a"
+
+# Record the current program counter.
+set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN"]
+
+# Use the recorded $pc value to check inline frames.
+gdb_test "maint info inline-frames $pc" \
+    [multi_line \
+	 "^program counter = $hex" \
+	 "skipped frames = 2" \
+	 "  inline_func_b" \
+	 "  inline_func_a" \
+	 "> normal_func"] \
+    "check inline-frames state at recorded \$pc while at the \$pc"
+
+# Step again, we should now enter inlined_func_b().
+gdb_test "step" ".*" \
+    "step into inline_func_b"
+
+gdb_test "maint info inline-frames" \
+    [multi_line \
+	 "^program counter = $hex" \
+	 "skipped frames = 0" \
+	 "> inline_func_b" \
+	 "  inline_func_a" \
+	 "  normal_func"] \
+    "check inline-frames state when just entered inline_func_b"
+
+gdb_test "step" ".*" \
+    "step into the body of inline_func_b"
+
+# Now we are no longer at the start of the inlined function we should
+# no longer see normal_func() in the inline-frames information.
+gdb_test "maint info inline-frames" \
+    [multi_line \
+	 "^program counter = $hex" \
+	 "skipped frames = 0" \
+	 "> inline_func_b"] \
+    "check inline-frames state when within inline_func_b"
+
+# Use the recorded $pc value to check inline frames.
+gdb_test "maint info inline-frames $pc" \
+    [multi_line \
+	 "^program counter = $hex" \
+	 "skipped frames = 2" \
+	 "  inline_func_b" \
+	 "  inline_func_a" \
+	 "> normal_func"] \
+    "check inline-frames state at recorded \$pc"
+
+clean_restart $binfile
+
+# Use the recorded $pc value to check inline frames when the inferior
+# is not executing.
+gdb_test "maint info inline-frames $pc" \
+    [multi_line \
+	 "^program counter = $hex" \
+	 "skipped frames = 2" \
+	 "  inline_func_b" \
+	 "  inline_func_a" \
+	 "> normal_func"] \
+    "check inline-frames state at recorded \$pc before execution starts"
+
+# Trying to read the $pc from the current thread should fail if the
+# inferior is not yet running.
+gdb_test "maint info inline-frames" \
+    "^no inferior thread" \
+    "check inline-frames state of current thread before execution starts"
-- 
2.25.4



More information about the Gdb-patches mailing list