> From: Andrew Burgess <aburgess@redhat.com>
> Cc: Andrew Burgess <aburgess@redhat.com>
> Date: Mon, 22 Jul 2024 15:42:08 +0100
>
> gdb/NEWS | 7 +
> gdb/doc/gdb.texinfo | 97 +++++++++++
> gdb/inline-frame.c | 163 ++++++++++++++++--
> .../maint-info-inline-frames-and-blocks.c | 57 ++++++
> .../maint-info-inline-frames-and-blocks.exp | 147 ++++++++++++++++
> 5 files changed, 456 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
Thanks.
> diff --git a/gdb/NEWS b/gdb/NEWS
> index b56ba9b36ce..1cbc05c5a18 100644
> --- a/gdb/NEWS
> +++ b/gdb/NEWS
> @@ -13,6 +13,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.
This part is okay.
> +@kindex maint info inline-frames
> +@item maint info inline-frames
> +@itemx maint info inline-frames @var{address}
> +Print information about inlined frames which start at the current
> +address, or @var{address} if specified.
Maybe add an index entry here, something like
@cindex frames of inlined functions
> +In order to allow a user to correctly step into inlined function,
^^^^^^
"the user"
> +Imagine a situation where function @code{main} calls @code{foo}, which
> +then calls @code{bar}, something like this:
> +
> +@smallexample
> +int
> +main ()
> +@{
> + /* Some interesting code here... */
> +
> + foo ();
> +
> + /* More interesting code here... */
> +@}
> +
> +void
> +foo ()
> +@{
> + bar ();
> +@}
> +
> +void
> +bar ()
> +@{
> + /* Some interesting code here... */
> +@}
> +@end smallexample
Suggest to use "@group..@end group" to avoid breaking functions
between pages.
> + When the user stops
> +at this address then will initially be told they are in @code{main},
Something is wrong with this part of the sentence ("then" seems out of
place).
> +if the user does a @command{step} then @value{GDBN} doesn't actually
> +step the inferior, instead the user is now told they have entered
> +@code{foo}. After the next @command{step} the user is told they have
> +entered @code{bar}. The @command{maint info inline-frames} command
> +can be used to view this internal @value{GDBN} state, like this:
Please use @kbd, not @command for markup of commands the user types.
> +Here the user is stopped in @code{main} at the call to @code{foo}. The
^^
Two spaces there.
> +If the user performs a @command{step} to enter @code{foo} then the
> +situation is updated: ^^^^^^^^^^^^^^
@code{step}
> +Finally, the user performs another @command{step} to enter @code{bar}:
^^^^^^^^^^^^^^
@code{step}
Reviewed-By: Eli Zaretskii <eliz@gnu.org>
@@ -13,6 +13,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
@@ -41850,6 +41850,103 @@
frame-id for frame #2: @{stack=0x7fffffffac90,code=0x000000000040111c,!special@}
@end smallexample
+@kindex maint info inline-frames
+@item maint info inline-frames
+@itemx maint info inline-frames @var{address}
+Print information about inlined frames which start at the current
+address, or @var{address} if specified.
+
+In order to allow a user to correctly step into inlined function,
+@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
+int
+main ()
+@{
+ /* Some interesting code here... */
+
+ foo ();
+
+ /* More interesting code here... */
+@}
+
+void
+foo ()
+@{
+ bar ();
+@}
+
+void
+bar ()
+@{
+ /* Some interesting code here... */
+@}
+@end smallexample
+
+As both @code{foo} and @code{bar} are inlined within @code{main} then
+there will 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 then will initially be told they are in @code{main},
+if the user does a @command{step} then @value{GDBN} doesn't actually
+step the inferior, instead the user is now told they have entered
+@code{foo}. After the next @command{step} the user is told they have
+entered @code{bar}. The @command{maint info inline-frames} command
+can be used to view this internal @value{GDBN} state, like this:
+
+@smallexample
+(@value{GDBP}) step
+24 foo ();
+(@value{GDBP}) maintenance info inline-frames
+program counter = 0x401137
+skipped frames = 2
+ bar
+ foo
+> main
+@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 @command{step} to enter @code{foo} then the
+situation is updated:
+
+@smallexample
+(@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 smallexample
+
+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 @command{step} to enter @code{bar}:
+
+@smallexample
+(@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 smallexample
+
@kindex maint print registers
@kindex maint print raw-registers
@kindex maint print cooked-registers
@@ -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);
+}
new file mode 100644
@@ -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;
+}
new file mode 100644
@@ -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"