]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdb: fix 'info frame' for tail calls with no debug information
authorAndrew Burgess <aburgess@redhat.com>
Mon, 12 Jan 2026 10:44:35 +0000 (10:44 +0000)
committerAndrew Burgess <aburgess@redhat.com>
Thu, 22 Jan 2026 10:13:02 +0000 (10:13 +0000)
If the inferior stack contains a tail call function.  And if the CU
containing the tail call function doesn't have any debug information.
And if the user uses 'info frame' to examine the tail call frame, then
GDB will report the wrong function name, for example:

  Breakpoint 1, 0x000000000040110a in callee ()
  (gdb) bt
  #0  0x000000000040110a in callee ()
  #1  0x0000000000401116 in caller ()
  #2  0x0000000000401140 in main ()
  (gdb) up
  #1  0x0000000000401116 in caller ()
  (gdb) frame
  #1  0x0000000000401116 in caller ()
  (gdb) info frame
  Stack level 1, frame at 0x7fffffffa440:
   rip = 0x401116 in dummy_func; saved rip = 0x401140
   called by frame at 0x7fffffffa450, caller of frame at 0x7fffffffa430
   Arglist at 0x7fffffffa430, args:
   Locals at 0x7fffffffa430, Previous frame's sp is 0x7fffffffa440
   Saved registers:
    rbp at 0x7fffffffa430, rip at 0x7fffffffa438
  (gdb)

Notice that 'info frame' claims that the current frame is 'dummy_func'
rather than 'caller', as the 'backtrace', 'up', and 'frame' commands
claim.

This is because 'backtrace', 'up', and 'frame' all uses print_frame to
print the frame details, which in turn uses find_frame_funname to get
the frame's function name.

In contrast, 'info_frame_command_core' contains an inlined copy of
'find_frame_funname' with one key difference.  The code in
info_frame_command_core uses get_frame_pc_if_available while
find_frame_funname uses get_frame_address_in_block_if_available.  The
latter function returns '$pc - 1' if the frame in question could be a
tail call function, while get_frame_pc_if_available always returns
$pc.  This difference means that, for a tail call function, GDB will
lookup the wrong msymbol.

Fix this by updating info_frame_command_core to use
find_frame_funname.  We end up still keeping the call to
get_frame_pc_if_available as 'info frame' still needs to print this
address.  There should be no other noticeable changes after this
commit.

There's also a test in which I have tried to create a tail call
function in a (relatively) target agnostic way.  I compile a test
program, pull some addresses from it, then recompile the test to
assembly, and augment the assembler output, changing one symbol size,
and adding an entirely new function symbol.  The modified assembly
file is then compiled, without debug information, to create the actual
test executable.  This gives GDB the impression that the test contains
a tail call function.

Approved-By: Tom Tromey <tom@tromey.com>
gdb/stack.c
gdb/testsuite/gdb.base/tailcall-msym.c [new file with mode: 0644]
gdb/testsuite/gdb.base/tailcall-msym.exp [new file with mode: 0644]

index a0abf4cda186a4ae82b080b43b38d899b885cc82..732d525083e4cb7bbb5220bf144f30058c5c7217 100644 (file)
@@ -1500,40 +1500,13 @@ info_frame_command_core (const frame_info_ptr &fi, bool selected_frame_p)
       pc_regname = "pc";
     }
 
-  const char *funname = nullptr;
-  enum language funlang = language_unknown;
-  std::optional<CORE_ADDR> frame_pc = get_frame_pc_if_available (fi);
-  struct symbol *func = get_frame_function (fi);
-  symtab_and_line sal = find_frame_sal (fi);
-  struct symtab *s = sal.symtab;
-  gdb::unique_xmalloc_ptr<char> func_only;
-  if (func != nullptr)
-    {
-      funname = func->print_name ();
-      funlang = func->language ();
-      if (funlang == language_cplus)
-       {
-         /* It seems appropriate to use print_name() here,
-            to display the demangled name that we already have
-            stored in the symbol table, but we stored a version
-            with DMGL_PARAMS turned on, and here we don't want to
-            display parameters.  So remove the parameters.  */
-         func_only = cp_remove_params (funname);
-
-         if (func_only != nullptr)
-           funname = func_only.get ();
-       }
-    }
-  else if (frame_pc.has_value ())
-    {
-      bound_minimal_symbol msymbol = lookup_minimal_symbol_by_pc (*frame_pc);
-      if (msymbol.minsym != nullptr)
-       {
-         funname = msymbol.minsym->print_name ();
-         funlang = msymbol.minsym->language ();
-       }
-    }
-  frame_info_ptr calling_frame_info = get_prev_frame (fi);
+  /* FUNC and FUNLANG are always initialized by the call to
+     find_frame_funname, even if it is just to NULL and language_unknown
+     respectively.  */
+  struct symbol *func;
+  enum language funlang;
+  gdb::unique_xmalloc_ptr<char> funname
+    = find_frame_funname (fi, &funlang, &func);
 
   if (selected_frame_p && frame_relative_level (fi) >= 0)
     {
@@ -1547,6 +1520,9 @@ info_frame_command_core (const frame_info_ptr &fi, bool selected_frame_p)
   fputs_styled (paddress (gdbarch, get_frame_base (fi)),
                address_style.style (), gdb_stdout);
   gdb_puts (":\n");
+
+  symtab_and_line sal = find_frame_sal (fi);
+  std::optional<CORE_ADDR> frame_pc = get_frame_pc_if_available (fi);
   gdb_printf (" %s = ", pc_regname);
   if (frame_pc.has_value ())
     fputs_styled (paddress (gdbarch, get_frame_pc (fi)),
@@ -1555,10 +1531,10 @@ info_frame_command_core (const frame_info_ptr &fi, bool selected_frame_p)
     fputs_styled ("<unavailable>", metadata_style.style (), gdb_stdout);
 
   gdb_stdout->wrap_here (3);
-  if (funname != nullptr)
+  if (funname.get () != nullptr)
     {
       gdb_puts (" in ");
-      fputs_styled (funname, function_name_style.style (), gdb_stdout);
+      fputs_styled (funname.get (), function_name_style.style (), gdb_stdout);
     }
   gdb_stdout->wrap_here (3);
   if (sal.symtab != nullptr)
@@ -1604,6 +1580,7 @@ info_frame_command_core (const frame_info_ptr &fi, bool selected_frame_p)
                  address_style.style (), gdb_stdout);
   gdb_puts ("\n");
 
+  frame_info_ptr calling_frame_info = get_prev_frame (fi);
   if (calling_frame_info == nullptr)
     {
       enum unwind_stop_reason reason;
@@ -1636,7 +1613,7 @@ info_frame_command_core (const frame_info_ptr &fi, bool selected_frame_p)
   if (get_next_frame (fi) != nullptr || calling_frame_info != nullptr)
     gdb_puts ("\n");
 
-  if (s != nullptr)
+  if (struct symtab *s = sal.symtab; s != nullptr)
     gdb_printf (" source language %s.\n",
                language_str (s->language ()));
 
diff --git a/gdb/testsuite/gdb.base/tailcall-msym.c b/gdb/testsuite/gdb.base/tailcall-msym.c
new file mode 100644 (file)
index 0000000..aae0e88
--- /dev/null
@@ -0,0 +1,43 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2026 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/>.  */
+
+volatile int global_var = 0;
+
+void
+callee (void)
+{
+  /* Nothing.  */
+}
+
+void
+caller (void)
+{
+  callee ();
+
+  /* Filler so that there is some code after the call.  We manually add
+     symbols to this executable, and create a fake 'dummy_func' that
+     overlaps this code.  */
+  ++global_var;
+  ++global_var;
+}
+
+int
+main (void)
+{
+  caller ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/tailcall-msym.exp b/gdb/testsuite/gdb.base/tailcall-msym.exp
new file mode 100644 (file)
index 0000000..d6cd06f
--- /dev/null
@@ -0,0 +1,162 @@
+# Copyright 2026 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/>.
+
+# This test checks GDB's ability to display the correct function name
+# in both a 'backtrace' and in the 'info frame' output, for a tailcall
+# frame, when there is no debug info, and we are relying on minimal
+# symbols.
+#
+# We create a "fake" tailcall frame by analysing a normal C program,
+# and then editing the generated .s file to override the symbol sizes,
+# and to create a fake function symbol that sits immediately after the
+# tail call function.
+
+standard_testfile
+
+# Compile with debug info first so we can find the instruction boundaries.
+if { [prepare_for_testing "prepare with debug" ${testfile} ${srcfile}] } {
+    return
+}
+
+if {![runto callee]} {
+    return
+}
+
+# At this point the stack is main->caller->callee, and the inferior is
+# stopped in 'callee'.  Move up to the 'caller' function so we can
+# find all the addresses we need.
+gdb_test "up" ".*caller.*" "go up to caller"
+
+set func_start -1
+set func_end -1
+set return_addr -1
+
+# Get the start address of 'caller'.
+gdb_test_multiple "info address caller" "get caller start address" {
+    -re -wrap "Symbol \"caller\" is a function at address ($hex)\." {
+       set func_start $expect_out(1,string)
+       pass $gdb_test_name
+    }
+}
+
+# Get the return address within 'caller' (current PC in this frame).
+gdb_test_multiple "print/x \$pc" "get return address" {
+    -re -wrap " = ($hex)" {
+       set return_addr $expect_out(1,string)
+       pass $gdb_test_name
+    }
+}
+
+gdb_test_multiple "disassemble caller" "get caller end address" {
+    -re "^\\s+($hex) \[^\r\n\]+\r\n" {
+       set func_end $expect_out(1,string)
+       exp_continue
+    }
+    -re "^$gdb_prompt $" {
+       gdb_assert { $func_end != -1 } $gdb_test_name
+    }
+    -re "^\[^\r\n\]*\r\n" {
+       exp_continue
+    }
+}
+
+# Check both addresses were found.
+if { $func_start == -1 || $func_end == -1 || $return_addr == -1} {
+    fail "could not determine addresses"
+    return
+}
+
+# Calculate the reduced length we want the 'caller' function to be.
+# This will stop the function immediately after the call instruction,
+# as if the function was a tailcall.
+set fake_len [expr {$return_addr - $func_start}]
+
+# Calculate the length of the dummy function.  This is almost the
+# remainder of the original 'caller' function.  The FUNC_END is
+# actually the address of the last instruction in 'caller', so this
+# new length will be one instruction short, but finding the actual end
+# is more complex, and really isn't necessary.  This is good enough.
+set dummy_len [expr {$func_end - $return_addr}]
+
+# Recompile the source file into an assembly file.  We're going to
+# modify this assembly file later.
+set asm_file [standard_output_file ${testfile}.s]
+if { [gdb_compile "${srcdir}/${subdir}/${srcfile}" "$asm_file" assembly {}] != "" } {
+    untested "failed to compile to assembly"
+    return
+}
+
+# Create and modify some symbols within the assembler file.  There are
+# two things we want to do.  First, reduce the length of the function
+# 'caller' such that the call instruction (the one going to 'callee')
+# is the last instruction in the function, this appears to make
+# 'caller' a tail call function.  Second, create a new function called
+# 'dummy_func' immediately after 'caller', this means that if GDB gets
+# things wrong it will report 'dummy_func' in the backtrace rather
+# than 'caller'.
+set fd [open $asm_file a]
+puts $fd ""
+puts $fd "/* Artificial symbols added by testsuite.  */"
+
+# Create 'dummy_func'.  The length here is short to avoid overlapping
+# other functions.
+puts $fd ".global dummy_func"
+puts $fd ".type dummy_func, %function"
+puts $fd "dummy_func = caller + $fake_len"
+puts $fd ".size dummy_func, $dummy_len"
+
+# Emit a new size for function 'caller', the assembler seems happy
+# enough to just use this new length instead of the original length
+# the compiler emitted.
+#
+# If this is ever a problem then we'll need to parse through the
+# assembler file and remove the original .size directive.
+puts $fd ".size caller, $fake_len"
+close $fd
+
+# Rebuild the test executable from the modified assembler file.  Don't
+# include debug as we want GDB to use the msymbols.
+if { [prepare_for_testing "prepare" ${testfile}-updated $asm_file {nodebug}] } {
+    return
+}
+
+if {![runto callee]} {
+    return
+}
+
+# Test that the backtrace shows the correct function name for each
+# frame.
+gdb_test "bt" \
+    [multi_line \
+        "#0\[^\r\n\]+callee \\(\\)" \
+        "#1\[^\r\n\]+caller \\(\\)" \
+        "#2\[^\r\n\]+main \\(\\)"] \
+    "backtrace shows correct caller, not dummy_func"
+
+# Test that 'info frame' displays the correct function name.  Also
+# check that the correct function name is shown after the 'up'
+# command.
+gdb_test "info frame" ".*in callee;.*" \
+    "info frame in frame 0"
+gdb_test "up" \
+    "#1\\s+\[^\r\n\]+ in caller \\(\\)" \
+    "up to frame 1"
+gdb_test "info frame" ".*in caller;.*" \
+    "info frame in frame 1"
+gdb_test "up" \
+    "#2\\s+\[^\r\n\]+ in main \\(\\)" \
+    "up to frame 2"
+gdb_test "info frame" ".*in main;.*" \
+    "info frame in frame 2"