[PATCHv3 1/7] gdb: improve line number lookup around inline functions

Andrew Burgess
2025-10-16T17:52:20Z

This commit aims to fix an issue where GDB would report the wrong line
for frames other than #0 if a previous frame had just left an inline
function.

Consider this example which is compiled at -Og:

volatile int global = 0;

static inline int bar (void) { asm (""); return 1; }

static void foo (int count)
{ global += count; }

int main (void)
{
foo (bar ());
return 0;
}

Used in this GDB session:

(gdb) break foo
Breakpoint 1 at 0x401106: file test.c, line 6.
(gdb) run
Starting program: /tmp/inline-bt/test.x

Breakpoint 1, foo (count=count@entry=1) at test.c:6
6 { global += count; }
(gdb) frame 1
#1 0x0000000000401121 in main () at test.c:3
3 static inline int bar (void) { asm (""); return 1; }

Notice that GDB incorrectly reports frame #1 as being at line 3 when
it should really be reporting this line:

foo (bar ());

The cause of this problem is in find_pc_sect_line (symtab.c). This
function is passed a PC for which GDB must find the symtab_and_line
information. The function can be called in two modes based on the
NOTCURRENT argument.

When NOTCURRENT is false then we are looking for information about the
current PC, i.e. the PC at which the inferior is currently stopped
at.

When NOTCURRENT is true we are looking for information about a PC that
it not the current PC, but is instead the PC for a previous frame.
The interesting thing in this case is that the PC passed in will be
the address after the address we actually want to lookup information
for, this is because as we unwind the program counter from frame #0
what we get is the return address in frame #1. The return address is
often (or sometimes) on the line after the calling line, and so in
find_pc_sect_line, when NOTCURRENT is true, we subtract 1 from PC and
then proceed as normal looking for information about this new PC
value.

Now lets look at the x86-64 disassembly for 'main' from the above
example. The location marker (=>) represents the return address in
'main' after calling 'foo':

(gdb) run
Starting program: /tmp/inline-bt/test.x

Breakpoint 1, foo (count=count@entry=1) at test.c:6
6 { global += count; }
#0 foo (count=count@entry=1) at test.c:6
#1 0x000000000040111f in main () at test.c:3
(gdb) up
#1 0x000000000040111f in main () at test.c:3
3 static inline int bar (void) { asm (""); return 1; }
(gdb) disassemble
Dump of assembler code for function main:
0x0000000000401115 <+0>: mov $0x1,%edi
0x000000000040111a <+5>: call 0x401106 <foo>
=> 0x000000000040111f <+10>: mov $0x0,%eax
0x0000000000401124 <+15>: ret
End of assembler dump.

And the corresponding line table:

(gdb) maintenance info line-table
objfile: /tmp/inline-bt/test.x ((struct objfile *) 0x59405a0)
compunit_symtab: test.c ((struct compunit_symtab *) 0x53ad320)
symtab: /tmp/inline-bt/test.c ((struct symtab *) 0x53ad3a0)
linetable: ((struct linetable *) 0x53adc90):
INDEX LINE REL-ADDRESS UNREL-ADDRESS IS-STMT PROLOGUE-END EPILOGUE-BEGIN
0 6 0x0000000000401106 0x0000000000401106 Y
1 6 0x0000000000401106 0x0000000000401106 Y
2 6 0x0000000000401106 0x0000000000401106
3 6 0x0000000000401114 0x0000000000401114
4 9 0x0000000000401115 0x0000000000401115 Y
5 10 0x0000000000401115 0x0000000000401115 Y
6 3 0x0000000000401115 0x0000000000401115 Y
7 3 0x0000000000401115 0x0000000000401115 Y
8 3 0x0000000000401115 0x0000000000401115 Y
9 10 0x0000000000401115 0x0000000000401115
10 11 0x000000000040111f 0x000000000040111f Y
11 12 0x000000000040111f 0x000000000040111f
12 END 0x0000000000401125 0x0000000000401125 Y

When looking for the line information of frame #1 we start with the
return address 0x40111f, however, as this is not the current program
counter value we subtract one and look for line information for
0x40111e.

We will find the entry at index 9, this is the last entry with an
address less than the address we're looking for, the next entry has an
address greater than the one we're looking for. The entry at index 9
is for line 10 which is the correct line, but GDB reports line 3, so
what's going on?

Having found a matching entry GDB checks to see if the entry is marked
as is-stmt (is statement). In our case index 9 (line 10) is not a
statement, and so GDB looks backwards for entries at the same address,
if any of these are marked is-stmt then GDB will use the last of these
instead. In our case the previous entry at index 8 is marked is-stmt,
and so GDB uses that. The entry at index 8 is for line 3, and that is
why GDB reports the wrong line. So why perform the backward is-stmt
check?

When NOTCURRENT is false (not our case) the backward scan makes
sense. If the inferior has just stopped at some new location, and we
want to report that location to the user, then it is better (I think)
to select an is-stmt entry. In this way we will report a line number
for a line which the inferior is just about to start executing, and
non of the side effects of that line have yet taken place. The line
GDB prints will correspond with the reported line, and if the user
queries the inferior state, the inferior should (assuming the compiler
emitted correct is-stmt markers) correspond to the line in question
having not yet been started.

However, in our case NOTCURRENT is true. We're looking back to
previous frames that are currently in-progress. If upon return to the
previous frame we are about to execute the next line then (it seems to
me) that this indicates we must be performing the very last action
from the previous line. As such, looking back through the line table
in order to report a line that has not yet started is the wrong thing
to do. We really want to report the very last line table entry for
the previous address as this is (I think) most likely to represent the
previous line that is just about to complete.

Further, in the NOTCURRENT case, we should care less about reporting
an is-stmt line. When a user looks back to a previous frame I don't
think they expect the line being reported to have not yet started. In
fact I think the expectation is the reverse ... after all, the
previous line must have executed enough to call the current frame.

So my proposal is that the backward scan of the line table looking for
an is-stmt entry should not be performed when NOTCURRENT is true. In
the case above this means we will report the entry at index 9, which
is for line 10, which is correct.

For testing this commit I have:

1. Extended the existing gdb.opt/inline-bt.exp test. I've extended
the source code to include a test similar to the example above. I
have also extended the script so that the test is compiled at a
variety of optimisation levels (O0, Og, O1, O2).

2. Added a new DWARF assembler test which hard codes a line table
similar to the example given above. My hope is that even if test
case (1) changes (due to compiler changes) this test will continue to
test the specific case I'm interested in.

I have tested the gdb.opt/inline-bt.exp test with gcc versions 8.4.0,
9.3.1, 10.5.0, 11.5.0, 12.2.0, and 14.2.0, in each case the test will
fail (with the expected error) without this patch applied, and will
pass with this patch applied.

I was inspired to write this patch while reviewing these patches:

https://inbox.sourceware.org/gdb-patches/AS8P193MB1285C58F6F09502252CEC16FE4DF2@AS8P193MB1285.EURP193.PROD.OUTLOOK.COM
https://inbox.sourceware.org/gdb-patches/AS8P193MB12855708DFF59A5309F5B19EE4DF2@AS8P193MB1285.EURP193.PROD.OUTLOOK.COM

though this patch only covers one of the issues addressed by these
patches, and the approach taken is quite different. Still, those
patches are worth reading for the history of this fix.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=25987
---
gdb/symtab.c | 25 ++-
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c | 79 +++++++
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp | 227 +++++++++++++++++++++
gdb/testsuite/gdb.opt/inline-bt.c | 28 +++
gdb/testsuite/gdb.opt/inline-bt.exp | 119 +++++++----
5 files changed, 435 insertions(+), 43 deletions(-)
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp

diff --git a/gdb/symtab.c b/gdb/symtab.c
index b0cea7a06b5..0e09f475004 100644
--- a/gdb/symtab.c
+++ b/gdb/symtab.c
@@ -3108,14 +3108,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..bc38bc07f46
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c
@@ -0,0 +1,79 @@
+/* Copyright 2024-2025 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..0a36e0c9dac
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp
@@ -0,0 +1,227 @@
+# Copyright 2024-2025 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 {} {
+ DW_TAG_compile_unit {
+ DW_AT_producer "gcc"
+ DW_AT_language @DW_LANG_C
+ DW_AT_name $::srcfile
+ DW_AT_low_pc 0 addr
+ DW_AT_stmt_list ${lines_label} DW_FORM_sec_offset
+ } {
+ foo_label: subprogram {
+ DW_AT_external 1 flag
+ DW_AT_name foo
+ DW_AT_low_pc $func_start addr
+ DW_AT_high_pc "$func_start + $func_len" addr
+ }
+ bar_label: subprogram {
+ DW_AT_external 1 flag
+ DW_AT_name bar
+ DW_AT_inline 3 data1
+ }
+ subprogram {
+ DW_AT_external 1 flag
+ DW_AT_name main
+ DW_AT_low_pc $main_start addr
+ DW_AT_high_pc "$main_start + $main_len" addr
+ } {
+ inlined_subroutine {
+ DW_AT_abstract_origin %$bar_label
+ DW_AT_low_pc line_label_4 addr
+ DW_AT_high_pc line_label_5 addr
+ DW_AT_call_file 1 data1
+ DW_AT_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/inline-bt.c b/gdb/testsuite/gdb.opt/inline-bt.c
index a020bd71573..6be036e16f0 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 79c48e832cf..9dd96b33650 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
+# 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 } {

-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"
+ set local_opts $::opts
+ lappend local_opts "additional_flags=-$opt_level"

-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"
+ if {[prepare_for_testing "failed to prepare" ${::testfile}-${opt_level} \
+ [list $::srcfile $::srcfile2] $local_opts]} {
+ return
+ }

-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"
+ runto_main

-# 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"
+ 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
+}
--
2.47.1