Fix backtrace showing ?? or wrong name for solib frame at function entry

Message ID CAAMY7r4gqd0YR2-SB3W4-tvH2pzY8ZagSoW20OoBx_5M3F2rdA@mail.gmail.com
State New
Headers
Series Fix backtrace showing ?? or wrong name for solib frame at function entry |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 fail Patch failed to apply
linaro-tcwg-bot/tcwg_gdb_build--master-arm fail Patch failed to apply

Commit Message

Dmitry Shafranov March 30, 2026, 9:21 a.m. UTC
  Pinging

---------- Forwarded message ---------
От: Дима <shafrnv@gmail.com>
Date: сб, 7 мар. 2026 г. в 22:48
Subject: [PATCH] Fix backtrace showing ?? or wrong name for solib
frame at function entry
To: <gdb-patches@sourceware.org>


When the resume PC is exactly at a function entry in a shared library
without debug info (e.g. a trampoline that does push return-address +
jmp), the backtrace shows "??" or the previous function name for that
frame.  Reason: get_frame_address_in_block gives pc-1 for
non-innermost frames, and lookup_minimal_symbol_by_pc(pc-1) then finds
nothing or the wrong symbol.

The fix: in find_frame_funname, if lookup_minimal_symbol_by_pc(pc)
fails, try pc+1; and when a symbol is found, also check pc+1 and
prefer the symbol whose address is exactly pc+1 (the actual resume PC
at function entry).

Tested on x86_64 Linux (-m32).  Added gdb.base/solib-bt-trampoline.exp.

Reproducer: gdb-solib-bt-bug-repro.tar.gz — see README inside for
build and run steps.

Patch below:
From 935853aaf1e2b4260b026ca36f5cfb9096c6b40c Mon Sep 17 00:00:00 2001
From: Dmitry Shafranov <shafrnv@gmail.com>
Date: Sat, 7 Mar 2026 22:14:22 +0300
Subject: [PATCH] Fix backtrace showing ?? or wrong name for solib frame at
 function entry

For frames without full symbols (e.g. solib with no debug info),
find_frame_funname uses get_frame_address_in_block and then
lookup_minimal_symbol_by_pc.  For non-innermost frames we get pc-1.
When the resume PC is exactly at a function entry (e.g. a trampoline
that does push+jmp), pc-1 is before that function, so the lookup
fails (backtrace shows "??") or finds the previous function in the
same object.

Add a fallback: if lookup_minimal_symbol_by_pc(pc) returns NULL, try
lookup_minimal_symbol_by_pc(pc+1).  This recovers the correct symbol
for the "PC at function entry" case without changing unwinding or
get_frame_address_in_block.
---
 gdb/stack.c                                   | 13 ++++
 .../gdb.base/solib-bt-trampoline-lib.S        | 24 ++++++
 gdb/testsuite/gdb.base/solib-bt-trampoline.c  | 21 ++++++
 .../gdb.base/solib-bt-trampoline.exp          | 74 +++++++++++++++++++
 4 files changed, 132 insertions(+)
 create mode 100644 gdb/testsuite/gdb.base/solib-bt-trampoline-lib.S
 create mode 100644 gdb/testsuite/gdb.base/solib-bt-trampoline.c
 create mode 100644 gdb/testsuite/gdb.base/solib-bt-trampoline.exp

+    }
+    -re "$gdb_prompt $" {
+ fail $gdb_test_name
+    }
+}
--
2.53.0
  

Patch

diff --git a/gdb/stack.c b/gdb/stack.c
index aa0257902e1..f60258fa9f2 100644
--- a/gdb/stack.c
+++ b/gdb/stack.c
@@ -1311,6 +1311,19 @@  find_frame_funname (const frame_info_ptr
&frame, enum language *funlang,
  return funname;

       bound_minimal_symbol msymbol = lookup_minimal_symbol_by_pc (pc);
+      if (msymbol.minsym == NULL)
+ {
+  /* pc may be at function entry (get_frame_address_in_block uses
pc-1); try pc+1.  */
+  msymbol = lookup_minimal_symbol_by_pc (pc + 1);
+ }
+      else
+ {
+  /* Prefer symbol at pc+1 (resume PC) when it is exactly at function
entry.  */
+  bound_minimal_symbol at_pc_plus_one = lookup_minimal_symbol_by_pc (pc + 1);
+  if (at_pc_plus_one.minsym != NULL
+      && at_pc_plus_one.value_address () == pc + 1)
+    msymbol = at_pc_plus_one;
+ }
       if (msymbol.minsym != NULL)
  {
   funname = make_unique_xstrdup (msymbol.minsym->print_name ());
diff --git a/gdb/testsuite/gdb.base/solib-bt-trampoline-lib.S
b/gdb/testsuite/gdb.base/solib-bt-trampoline-lib.S
new file mode 100644
index 00000000000..0273e1b471d
--- /dev/null
+++ b/gdb/testsuite/gdb.base/solib-bt-trampoline-lib.S
@@ -0,0 +1,24 @@ 
+/* Trampoline: run_then_exit + run_then_exit_return in one object.
+   When return address is exactly at run_then_exit_return entry, GDB
+   looks up pc-1 and without the fix shows "run_then_exit" or "??".  */
+ .text
+ .globl run_then_exit
+ .type  run_then_exit,@function
+ .globl run_then_exit_return
+ .type  run_then_exit_return,@function
+ .extern exit
+run_then_exit:
+ push   %ebp
+ mov    %esp, %ebp
+ call   .Lpic
+.Lpic:
+ pop    %eax
+ add    $(run_then_exit_return - .Lpic), %eax
+ push   12(%ebp)
+ push   %eax
+ jmp    *8(%ebp)
+ .size run_then_exit, .-run_then_exit
+run_then_exit_return:
+ push   $0
+ call   exit@PLT
+ .size run_then_exit_return, .-run_then_exit_return
diff --git a/gdb/testsuite/gdb.base/solib-bt-trampoline.c
b/gdb/testsuite/gdb.base/solib-bt-trampoline.c
new file mode 100644
index 00000000000..96e150e2279
--- /dev/null
+++ b/gdb/testsuite/gdb.base/solib-bt-trampoline.c
@@ -0,0 +1,21 @@ 
+/* Test program for run_then_exit trampoline.  Stops in user callback so that
+   "bt" in GDB shows the trampoline return frame: frame #1 should be
+   run_then_exit_return (not "??" or "run_then_exit").  */
+
+#include <stddef.h>
+
+extern void run_then_exit (void (*fn) (void *), void *arg);
+
+static void
+inner_func (void *arg)
+{
+  (void) arg;
+  return;  /* Break here and run "bt".  */
+}
+
+int
+main (void)
+{
+  run_then_exit (inner_func, NULL);
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/solib-bt-trampoline.exp
b/gdb/testsuite/gdb.base/solib-bt-trampoline.exp
new file mode 100644
index 00000000000..18418e8f121
--- /dev/null
+++ b/gdb/testsuite/gdb.base/solib-bt-trampoline.exp
@@ -0,0 +1,74 @@ 
+# 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/>.
+
+# Test that backtrace shows the correct function name for a solib frame
+# when the resume PC is exactly at function entry (trampoline that does
+# push return-address + jmp).  Without the fix, frame #1 shows "??" or
+# the previous function (run_then_exit) instead of run_then_exit_return.
+
+require allow_shlib_tests
+require {is_any_target "i?86-*-*" "x86_64-*-*"}
+
+standard_testfile solib-bt-trampoline.c
+set libsrc ${srcdir}/${subdir}/solib-bt-trampoline-lib.S
+set libname "solib-bt-trampoline-lib"
+set libfile [standard_output_file ${libname}.so]
+
+# Shared library: assembly only, no debug info (so minimal symbols are used).
+set lib_opts [list nodebug ldflags=-nostdlib libs=-lc]
+if {![istarget "i386-*-*"]} {
+    lappend lib_opts "additional_flags=-m32"
+}
+
+if {[gdb_compile_shlib $libsrc $libfile $lib_opts] != ""} {
+    untested "failed to compile shared library (need 32-bit toolchain
on x86_64?)"
+    return -1
+}
+
+set exec_opts [list debug shlib=$libfile
"additional_flags=-fno-omit-frame-pointer"]
+if {![istarget "i386-*-*"]} {
+    lappend exec_opts "additional_flags=-m32"
+}
+
+if {[gdb_compile $srcdir/$subdir/$srcfile $binfile executable
$exec_opts] != ""} {
+    untested "failed to compile executable"
+    return -1
+}
+
+clean_restart $testfile
+gdb_load_shlib $libfile
+
+if {![runto_main]} {
+    return -1
+}
+
+gdb_breakpoint "inner_func"
+# Pattern must match to end of line (e.g. solib-bt-trampoline.c:13)
so \r\n is next.
+set location_pattern "inner_func\[^\r\n\]*|solib-bt-trampoline\[^\r\n\]*"
+gdb_test_multiple "continue" "continue to breakpoint: inner_func" {
+    -re "(?:Breakpoint|Temporary breakpoint) .* (at|in)
$location_pattern\[\r\n\]+.*$gdb_prompt $" {
+ pass $gdb_test_name
+    }
+}
+
+# Frame #1 must be run_then_exit_return (not "??" or "run_then_exit").
+gdb_test_multiple "bt" "backtrace shows run_then_exit_return in frame 1" {
+    -re "#1\[^\r\n\]*run_then_exit_return" {
+ pass $gdb_test_name