diff --git a/gdb/symtab.c b/gdb/symtab.c
index ba421267b9a..20f4aeb7a58 100644
--- a/gdb/symtab.c
+++ b/gdb/symtab.c
@@ -3271,14 +3271,23 @@ find_pc_sect_line (CORE_ADDR pc, struct obj_section *section, int notcurrent)
 	  best = prev;
 	  best_symtab = iter_s;
 
-	  /* If during the binary search we land on a non-statement entry,
-	     scan backward through entries at the same address to see if
-	     there is an entry marked as is-statement.  In theory this
-	     duplication should have been removed from the line table
-	     during construction, this is just a double check.  If the line
-	     table has had the duplication removed then this should be
-	     pretty cheap.  */
-	  if (!best->is_stmt)
+	  /* If NOTCURRENT is false then the address we are looking for is
+	     the address the inferior is currently stopped at.  In this
+	     case our preference is to report a stop at a line marked as
+	     is_stmt.  If BEST is not marked as a statement then scan
+	     backwards through entries at this address looking for one that
+	     is marked as a statement; if one is found then use that.
+
+	     If NOTCURRENT is true then the address we're looking for is
+	     not the inferior's current address, but is an address from a
+	     previous stack frame (i.e. frames 1, 2, 3, ... etc).  In this
+	     case scanning backwards for an is_stmt line table entry is not
+	     the desired behaviour.  If an inline function terminated at
+	     this address then the last is_stmt line will be within the
+	     inline function, while the following non-statement line will
+	     be for the outer function.  When looking up the stack we
+	     expect to see the outer function.  */
+	  if (!best->is_stmt && !notcurrent)
 	    {
 	      const linetable_entry *tmp = best;
 	      while (tmp > first
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c b/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c
new file mode 100644
index 00000000000..1321726ff9a
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c
@@ -0,0 +1,79 @@
+/* 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/>.  */
+
+/* Used to insert labels with which we can build a fake line table.  */
+#define LL(N) asm ("line_label_" #N ": .globl line_label_" #N)
+
+/* The following non-compiled code exists for the generated line table to
+   point at.  */
+
+#if 0
+
+volatile int global = 0;
+
+__attribute__((noinline, noclone)) void
+foo (int arg)
+{			/* foo prologue */
+  asm ("");
+  global += arg;
+}
+
+inline __attribute__((always_inline)) int
+bar (void)
+{
+  return 1;		/* bar body */
+}
+
+int
+main (void)
+{			/* main prologue */
+  foo (bar ());		/* call line */
+  return 0;
+}
+
+#endif	/* 0 */
+
+volatile int var;
+
+/* Generate some code to take up some space.  */
+#define FILLER do { \
+    var = 99;	    \
+} while (0)
+
+void
+func (void)
+{
+  asm ("func_label: .globl func_label");
+  FILLER;
+  LL (1);
+  FILLER;
+  LL (2);
+  return;
+}
+
+int
+main (void)
+{
+  asm ("main_label: .globl main_label");
+  FILLER;
+  LL (4);
+  FILLER;
+  LL (5);
+  func ();
+  FILLER;
+  LL (6);
+  FILLER;
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp b/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp
new file mode 100644
index 00000000000..3e237fc4cf2
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp
@@ -0,0 +1,227 @@
+# 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/>.
+
+# Setup a line table where:
+#
+# |      |      |      | Func | Func | Func |
+# | Addr | Line | Stmt | main | foo  | bar  |
+# |------|------|------|------|------|------|
+# | 1    | 28   | Y    |      | X    |      |
+# | 2    | 30   | Y    |      | X    |      |
+# | 3    | 31   | N    |      | X    |      |
+# | 4    | 41   | Y    | X    |      |      |
+# | 5    | 42   | Y    | X    |      |      |
+# | 5    | 36   | Y    | X    |      | X    |
+# | 5    | 42   | N    | X    |      |      |
+# | 6    | 43   | Y    | X    |      |      |
+# | 7    | END  | Y    | X    |      |      |
+# |------|------|------|------|------|------|
+#
+#
+# The function 'bar' is inline within 'main' while 'foo' is not
+# inline.  Function 'foo' is called from 'main' immediately after the
+# inlined call to bar.  The C code can be found within a '#if 0' block
+# inside the test's .c file.  The line table is similar to that
+# generated by compiling the source code at optimisation level -Og.
+#
+# Place a breakpoint in 'foo', run to the breakpoint, and then examine
+# frame #1, that is, the frame for 'main'.  At one point, bugs in GDB
+# meant that the user would be shown the inline line from 'bar' rather
+# than the line from 'main'.  In the example above the user expects to
+# see line 42 from 'main', but instead would be shown line '36'.
+#
+# The cause of the bug is this: to find the line for frame #1 GDB
+# first finds an address in frame #1 by unwinding frame #0.  This
+# provides the return address in frame #1.  GDB subtracts 1 from this
+# address and looks for a line matching this address.  In this case
+# that would be line 42.
+#
+# However, buggy GDB would then scan backward through the line table
+# looking for a line table entry that is marked as is-stmt.  In this
+# case, the first matching entry is that for line 36, and so that is
+# what is reported.  This backward scan makes sense for frame #0, but
+# not for outer frames.
+#
+# This has now been fixed to prevent the backward scan for frames
+# other than frame #0.
+
+load_lib dwarf.exp
+
+# This test can only be run on targets which support DWARF-2 and use
+# gas.
+require dwarf2_support
+
+standard_testfile .c .S
+
+# Lines in the source code that we need to reference.
+set call_line [gdb_get_line_number "call line" $srcfile]
+set foo_prologue [gdb_get_line_number "foo prologue" $srcfile]
+set main_prologue [gdb_get_line_number "main prologue" $srcfile]
+set bar_body [gdb_get_line_number "bar body" $srcfile]
+
+# We need the return address in 'main' after the call to 'func' so
+# that we can build the line table.  Compile the .c file with debug,
+# and figure out the address.  This works so long as the only
+# difference in build flags between this compile and the later compile
+# is that this is debug on, and the later compile is debug off.
+if { [prepare_for_testing "failed to prepare" $testfile $srcfile] } {
+    return
+}
+
+if {![runto func]} {
+    return
+}
+
+set func_call_line [gdb_get_line_number "func ();"]
+gdb_test "up" \
+    [multi_line \
+	 "#1\\s*$hex in main \\(\\) at \[^\r\n\]+" \
+	 "$func_call_line\\s+ func \\(\\);"] \
+    "move up from func to main"
+
+set return_addr_in_main [get_hexadecimal_valueof "\$pc" "*UNKNOWN*" \
+			     "get pc after return from func"]
+
+# Prepare and run the test.  Placed into a proc in case we ever want
+# to parameterise this test in the future.
+
+proc do_test { } {
+    set build_options {nodebug}
+
+    set asm_file [standard_output_file $::srcfile2]
+    Dwarf::assemble $asm_file {
+	upvar build_options build_options
+
+	declare_labels lines_label foo_label bar_label
+
+	get_func_info main $build_options
+	get_func_info func $build_options
+
+	cu {} {
+	    compile_unit {
+		{producer "gcc" }
+		{language @DW_LANG_C}
+		{name $::srcfile}
+		{low_pc 0 addr}
+		{stmt_list ${lines_label} DW_FORM_sec_offset}
+	    } {
+		foo_label: subprogram {
+		    {external 1 flag}
+		    {name foo}
+		    {low_pc $func_start addr}
+		    {high_pc "$func_start + $func_len" addr}
+		}
+		bar_label: subprogram {
+		    {external 1 flag}
+		    {name bar}
+		    {inline 3 data1}
+		}
+		subprogram {
+		    {external 1 flag}
+		    {name main}
+		    {low_pc $main_start addr}
+		    {high_pc "$main_start + $main_len" addr}
+		} {
+		    inlined_subroutine {
+			{abstract_origin %$bar_label}
+			{low_pc line_label_4 addr}
+			{high_pc line_label_5 addr}
+			{call_file 1 data1}
+			{call_line $::call_line data1}
+		    }
+		}
+	    }
+	}
+
+	lines {version 2 default_is_stmt 1} lines_label {
+	    include_dir "${::srcdir}/${::subdir}"
+	    file_name "$::srcfile" 1
+
+	    program {
+		DW_LNE_set_address func
+		line $::foo_prologue
+		DW_LNS_copy
+
+		DW_LNE_set_address line_label_1
+		DW_LNS_advance_line 2
+		DW_LNS_copy
+
+		DW_LNE_set_address line_label_2
+		DW_LNS_advance_line 1
+		DW_LNS_negate_stmt
+		DW_LNS_copy
+
+		DW_LNE_set_address main
+		DW_LNS_advance_line [expr $::main_prologue - $::foo_prologue - 3]
+		DW_LNS_negate_stmt
+		DW_LNS_copy
+
+		DW_LNE_set_address line_label_4
+		DW_LNS_advance_line 1
+		DW_LNS_copy
+
+		DW_LNE_set_address line_label_4
+		line $::bar_body
+		DW_LNS_copy
+
+		DW_LNE_set_address line_label_4
+		line $::call_line
+		DW_LNS_negate_stmt
+		DW_LNS_copy
+
+		# Skip line_label_5, this is used as the end of `bar`
+		# the inline function.
+
+		DW_LNE_set_address $::return_addr_in_main
+		DW_LNS_advance_line 1
+		DW_LNS_negate_stmt
+		DW_LNS_copy
+
+		DW_LNE_set_address "$main_start + $main_len"
+		DW_LNE_end_sequence
+	    }
+	}
+    }
+
+    if { [prepare_for_testing "failed to prepare" $::testfile \
+	      [list $::srcfile $asm_file] $build_options] } {
+	return
+    }
+
+    if ![runto foo] {
+	return
+    }
+
+    # For this backtrace we don't really care which line number in foo
+    # is reported.  We might get different line numbers depending on
+    # how the architectures skip prologue function works.  This test
+    # is all about how frame #1 is reported.
+    set foo_body_1 [expr $::foo_prologue + 1]
+    set foo_body_2 [expr $::foo_prologue + 2]
+    gdb_test "bt" \
+	[multi_line \
+	     "^#0\\s+foo \\(\\) at \[^\r\n\]+$::srcfile:(?:$::foo_prologue|$foo_body_1|$foo_body_2)" \
+	     "#1\\s+$::hex in main \\(\\) at \[^\r\n\]+$::srcfile:$::call_line"] \
+	"backtrace show correct line number in main"
+
+    gdb_test "frame 1" \
+	[multi_line \
+	     "^#1\\s+$::hex in main \\(\\) at \[^\r\n\]+$::srcfile:$::call_line" \
+	     "$::call_line\\s+foo \\(bar \\(\\)\\);\[^\r\n\]+"] \
+	"correct lines are shown for frame 1"
+}
+
+# Run the test.
+do_test
diff --git a/gdb/testsuite/gdb.opt/empty-inline-cxx.exp b/gdb/testsuite/gdb.opt/empty-inline-cxx.exp
index 1de41428f38..fcff66659b1 100644
--- a/gdb/testsuite/gdb.opt/empty-inline-cxx.exp
+++ b/gdb/testsuite/gdb.opt/empty-inline-cxx.exp
@@ -82,7 +82,6 @@ proc run_test { opt_level } {
     # Backtrace.  Check frame #1 looks right.  Bug gdb/25987 would report
     # frame #1 as being the correct function, but would report the line for
     # ptr::get_myclass(), which is not correct.
-    setup_xfail *-*-* gdb/25987
     gdb_test "bt" \
 	[multi_line \
 	     "#0\\s+MyClass::call\[^\r\n\]+/$::srcfile:$::final_bp_line" \
diff --git a/gdb/testsuite/gdb.opt/empty-inline.exp b/gdb/testsuite/gdb.opt/empty-inline.exp
index 4b3758a05ad..bcd5956f355 100644
--- a/gdb/testsuite/gdb.opt/empty-inline.exp
+++ b/gdb/testsuite/gdb.opt/empty-inline.exp
@@ -83,7 +83,6 @@ proc run_test { opt_level } {
     # Check frame #1 looks right.  Bug gdb/25987 would report frame #1 as
     # being the correct function, but would report the line for a nearby
     # inlined function.
-    setup_xfail *-*-* gdb/25987
     gdb_test "frame 1" \
 	[multi_line \
 	     "#1\\s+\[^\r\n\]*main \\(\\) \[^\r\n\]+/$::srcfile:$::lineno_main_1" \
diff --git a/gdb/testsuite/gdb.opt/inline-bt.c b/gdb/testsuite/gdb.opt/inline-bt.c
index 3999104dfeb..d4192b3130b 100644
--- a/gdb/testsuite/gdb.opt/inline-bt.c
+++ b/gdb/testsuite/gdb.opt/inline-bt.c
@@ -13,6 +13,8 @@
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
+#include "attributes.h"
+
 /* This is only ever run if it is compiled with a new-enough GCC, but
    we don't want the compilation to fail if compiled by some other
    compiler.  */
@@ -39,6 +41,30 @@ inline ATTR int func2(void)
   return x * func1 (1);
 }
 
+inline ATTR int
+return_one (void)
+{
+  /* The following empty asm() statement prevents older (< 11.x) versions
+     of gcc from completely optimising away this function.  And for newer
+     versions of gcc (>= 11.x) this ensures that we have two line table
+     entries in main for the inline call to this function, with the second
+     of these lines being a non-statement, which is critical for this
+     test.  These two behaviours have been checked for versions of gcc
+     between 8.4.0 and 14.2.0.  */
+  asm ("");
+  return 1;
+}
+
+volatile int global = 0;
+
+__attribute__((noinline)) ATTRIBUTE_NOCLONE void
+not_inline_func (int count)
+{
+  global += count;
+  global += count;	/* b/p in not_inline_func */
+  global += count;
+}
+
 int main (void)
 {
   int val;
@@ -53,5 +79,7 @@ int main (void)
   val = func2 ();
   result = val;
 
+  not_inline_func (return_one ());	/* bt line in main */
+
   return 0;
 }
diff --git a/gdb/testsuite/gdb.opt/inline-bt.exp b/gdb/testsuite/gdb.opt/inline-bt.exp
index 9e1fb195f9b..9228edefc3f 100644
--- a/gdb/testsuite/gdb.opt/inline-bt.exp
+++ b/gdb/testsuite/gdb.opt/inline-bt.exp
@@ -15,9 +15,11 @@
 
 standard_testfile .c inline-markers.c
 
+set opts {debug additional_flags=-Winline}
+lappend_include_file opts $srcdir/lib/attributes.h
+
 if {[prepare_for_testing "failed to prepare" $testfile \
-	 [list $srcfile $srcfile2] \
-	 {debug additional_flags=-Winline}]} {
+	 [list $srcfile $srcfile2] $opts]} {
     return -1
 }
 
@@ -29,40 +31,87 @@ if { [skip_inline_frame_tests] } {
     return
 }
 
-set line1 [gdb_get_line_number "set breakpoint 1 here" ${srcfile2}]
-gdb_breakpoint $srcfile2:$line1
-
-gdb_test "continue" ".*set breakpoint 1 here.*" "continue to bar, 1"
-gdb_test "backtrace" "#0  bar.*#1  .*main.*" "backtrace from bar, 1"
-gdb_test "info frame" ".*called by frame.*" "bar not inlined"
-
-gdb_test "continue" ".*set breakpoint 1 here.*" "continue to bar, 2"
-gdb_test "backtrace" "#0  bar.*#1  .*func1.*#2  .*main.*" \
-    "backtrace from bar, 2"
-gdb_test "up" "#1  .*func1.*" "up from bar, 2"
-gdb_test "info frame" ".*inlined into frame.*" "func1 inlined, 2"
-
-gdb_test "continue" ".*set breakpoint 1 here.*" "continue to bar, 3"
-gdb_test "backtrace" "#0  bar.*#1  .*func1.*#2  .*func2.*#3  .*main.*" \
-    "backtrace from bar, 3"
-gdb_test "up" "#1  .*func1.*" "up from bar, 3"
-gdb_test "info frame" ".*inlined into frame.*" "func1 inlined, 3"
-gdb_test "up" "#2  .*func2.*" "up from func1, 3"
-gdb_test "info frame" ".*inlined into frame.*" "func2 inlined, 3"
-
-# A regression test for having a backtrace limit that forces unwinding
-# to stop after an inline frame.  GDB needs to compute the frame_id of
-# the inline frame, which requires unwinding past all the inline
-# frames to the real stack frame, even if that means bypassing the
-# user visible backtrace limit.  See PR backtrace/15558.
-#
-# Set a backtrace limit that forces an unwind stop after an inline
-# function.
-gdb_test_no_output "set backtrace limit 2"
-# Force flushing the frame cache.
-gdb_test "maint flush register-cache" "Register cache flushed."
-gdb_test "up" "#1  .*func1.*" "up from bar, 4"
-gdb_test "info frame" ".*in func1.*" "info frame still works"
-# Verify the user visible limit works as expected.
-gdb_test "up" "Initial frame selected; you cannot go up." "up hits limit"
-gdb_test "backtrace" "#0  bar.*#1  .*func1.*" "backtrace hits limit"
+# Run inline function backtrace tests, compile with binary with OPT_LEVEL
+# optimisation level.  OPT_LEVEL should be a string like 'O0', 'O1', etc.
+# No leading '-' is needed on OPT_LEVEL, that is added in this proc.
+proc run_test { opt_level } {
+
+    set local_opts $::opts
+    lappend local_opts "additional_flags=-$opt_level"
+
+    if {[prepare_for_testing "failed to prepare" ${::testfile}-${opt_level} \
+	     [list $::srcfile $::srcfile2] $local_opts]} {
+	return
+    }
+
+    runto_main
+
+    set line1 [gdb_get_line_number "set breakpoint 1 here" ${::srcfile2}]
+    gdb_breakpoint $::srcfile2:$line1
+
+    with_test_prefix "first stop at bar" {
+	gdb_continue_to_breakpoint "continue to bar" \
+	    ".*set breakpoint 1 here.*"
+	gdb_test "backtrace" "#0  bar.*#1  .*main.*" "backtrace from bar"
+	gdb_test "info frame" ".*called by frame.*" "bar not inlined"
+    }
+
+    with_test_prefix "second stop at bar" {
+	gdb_continue_to_breakpoint "continue to bar" \
+	    ".*set breakpoint 1 here.*"
+	gdb_test "backtrace" "#0  bar.*#1  .*func1.*#2  .*main.*" \
+	    "backtrace from bar"
+	gdb_test "up" "#1  .*func1.*" "up from bar"
+	gdb_test "info frame" ".*inlined into frame.*" "func1 inlined"
+    }
+
+    with_test_prefix "third stop at bar" {
+	gdb_continue_to_breakpoint "continue to bar" \
+	    ".*set breakpoint 1 here.*"
+	gdb_test "backtrace" "#0  bar.*#1  .*func1.*#2  .*func2.*#3  .*main.*" \
+	    "backtrace from bar"
+	gdb_test "up" "#1  .*func1.*" "up from bar"
+	gdb_test "info frame" ".*inlined into frame.*" "func1 inlined"
+	gdb_test "up" "#2  .*func2.*" "up from func1"
+	gdb_test "info frame" ".*inlined into frame.*" "func2 inlined"
+    }
+
+    # A regression test for having a backtrace limit that forces unwinding
+    # to stop after an inline frame.  GDB needs to compute the frame_id of
+    # the inline frame, which requires unwinding past all the inline
+    # frames to the real stack frame, even if that means bypassing the
+    # user visible backtrace limit.  See PR backtrace/15558.
+    #
+    # Set a backtrace limit that forces an unwind stop after an inline
+    # function.
+    gdb_test_no_output "set backtrace limit 2"
+    # Force flushing the frame cache.
+    gdb_test "maint flush register-cache" "Register cache flushed."
+    gdb_test "up" "#1  .*func1.*" "up from bar"
+    gdb_test "info frame" ".*in func1.*" "info frame still works"
+    # Verify the user visible limit works as expected.
+    gdb_test "up" "Initial frame selected; you cannot go up." "up hits limit"
+    gdb_test "backtrace" "#0  bar.*#1  .*func1.*" "backtrace hits limit"
+
+    set line2 [gdb_get_line_number "b/p in not_inline_func" $::srcfile]
+    set line3 [gdb_get_line_number "bt line in main" $::srcfile]
+
+    gdb_breakpoint $::srcfile:$line2
+
+    gdb_continue_to_breakpoint "stop in not_inline_func" \
+	".*b/p in not_inline_func.*"
+    gdb_test "bt" \
+	[multi_line \
+	     "^#0\\s+not_inline_func \\(\[^)\]+\\) at \[^\r\n\]+$::srcfile:$line2" \
+	     "#1\\s+$::hex in main \\(\\) at \[^\r\n\]+$::srcfile:$line3"] \
+	"bt from not_inline_func to main"
+    gdb_test "frame 1" \
+	[multi_line \
+	     "^#1\\s+$::hex in main \\(\\) at \[^\r\n\]+$::srcfile:$line3" \
+	     "$line3\\s+not_inline_func \\(return_one \\(\\)\\);\[^\r\n\]+"] \
+	"select frame for main from not_inline_func"
+}
+
+foreach_with_prefix opt_level { O0 Og O1 O2 } {
+    run_test $opt_level
+}
