[PATCHv2 2/7] gdb: handle empty ranges for inline subroutines

Andrew Burgess
2025-08-01T09:03:33Z

The work in this patch is based on changes found in this series:

https://inbox.sourceware.org/gdb-patches/AS1PR01MB946510286FBF2497A6F03E83E4922@AS1PR01MB9465.eurprd01.prod.exchangelabs.com

That series has the fixes here merged along with other changes, and
takes a different approach for how to handle the issue addressed here.

Credit for identifying the original issue belongs with Bernd, the
author of the original patch, who I have included as a co-author on
this patch. A brief description of how the approach taken in this
patch differs from the approach Bernd took can be found at the end of
this commit message.

When compiling with optimisation, it can often happen that gcc will
emit an inline function instance with an empty range associated. This
can happen in two ways. The inline function might have a DW_AT_low_pc
and DW_AT_high_pc, where the high-pc is an offset from the low-pc, but
the high-pc offset is given as 0 by gcc.

Alternatively, the inline function might have a DW_AT_ranges, and one
of the sub-ranges might be empty, though usually in this case, other
ranges will be non-empty.

The second case is made worse in that sometimes gcc will specify a
DW_AT_entry_pc value which points to the address of the empty
sub-range.

My understanding of the DWARF spec is that empty ranges as seen in
these examples indicate that no instructions are associated with the
inline function, and indeed, this is how GDB handles these cases,
rejecting blocks and sub-ranges which are empty.

DWARF-5, 2.17.2, Contiguous Address Range:
The value of the DW_AT_low_pc attribute is the address of the
first instruction associated with the entity. If the value of the
DW_AT_high_pc is of class address, it is the address of the first
location past the last instruction associated with the entity...

DWARF-5, 2.17.3, Non-Contiguous Address Ranges:
A bounded range entry whose beginning and ending address offsets
are equal (including zero) indicates an empty range and may be
ignored.

As a consequence, an attempt by the user to place a breakpoint on an
inline function with an empty low/high address range will trigger
GDB's pending breakpoint message:

(gdb) b foo
Function "foo" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n

While, having the entry-pc point at an empty range forces GDB to
ignore the given entry-pc and select a suitable alternative.

If instead of ignoring these empty ranges, we instead teach GDB to
treat these as non-empty, what we find is that, in all the cases I've
seen, the debug experience is improved.

As a minimum, in the low/high case, GDB now knows about the inline
function, and can place breakpoints that will be hit. Further, in
most cases, local variables from the inline function can be accessed.

If we do start treating empty address ranges as non-empty then we are
deviating from the DWARF spec. It is not clear if we are working
around a gcc bug (I suspect so), or if gcc actually considers the
inline function gone, and we're just getting lucky that the debug
experience seems improved.

My proposed strategy for handling these empty address ranges is to
only perform this work around if the compiler is gcc, so far I've not
seen this issue with Clang (the only other compiler I've tested),
though extending this to other compilers in the future would be
trivial.

Additionally, I only apply the work around for
DW_TAG_inlined_subroutine DIEs, as I've only seen the issue for
inline functions.

If we find a suitable empty address range then the fix-up is to give
the address range a length of 1 byte.

Now clearly, in most cases, 1 byte isn't even going to cover a single
instruction, but so far this doesn't seem to be a problem. An
alternative to using a 1-byte range would be to try and disassemble
the code at the given address, calculate the instruction length, and
use that, the length of one instruction. But this means that the
DWARF parser now needs to make use of the disassembler, which feels
like a big change that I'd rather avoid if possible.

The other alternative is to allow blocks to be created with zero
length address ranges and then change the rest of GDB to allow for
lookup of zero sized blocks to succeed. This is the approach taken by
the original patch series that I linked above.

The results achieved by the original patch are impressive, and Bernd,
the original patch author, makes a good argument that at least some of
the problems relating to empty ranges are a result of deficiencies in
the DWARF specification rather than issues with gcc.

However, I remain unconvinced. But even if I accept that the issue is
with DWARF itself rather than gcc, the question still remains; should
we fix the problem by synthesising new DWARF attributes and/or accept
non-standard DWARF during the dwarf2/read.c phase, and then update GDB
to handle the new reality, or should we modify the incoming DWARF as
we read it to make it fit GDB's existing algorithms.

The original patch, I believe, took the former approach, while I
favour the later, and so, for now, I propose that the single byte
range proposal is good enough, at least until we find counter examples
where this doesn't work.

This leaves just one question: what about the remaining work in the
original patch. That work deals with problems around the end address
of non-empty ranges. The original patch handled that case using the
same algorithm changes, which is neat, but I think there are
alternative solutions that should be investigated. If the
alternatives don't end up working out, then it's trivial to revert
this patch in the future and adopt the original proposal.

For testing I have two approaches, C/C++ test compiled with
optimisation that show the problems discussed. These are good because
they show that these issues do crop up in compiled code. But they are
bad in that the next compiler version might change the way the test is
optimised such that the problem no longer shows.

And so I've backed up the real code tests with DWARF assembler tests
which reproduce each issue.

The DWARF assembler tests are not really impacted by which gcc version
is used, but I've run all of these tests using gcc versions 8.4.0,
9.5.0, 10.5.0, 11.5.0, 12.2.0, and 14.2.0. I see failures in all of
the new tests when using an unpatched GDB, and no failures when using
a patched GDB.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=25987
Co-Authored-By: Bernd Edlinger <bernd.edlinger@hotmail.de>
---
gdb/dwarf2/read.c | 70 ++++-
gdb/testsuite/gdb.cp/step-and-next-inline.exp | 145 +++++-----
.../gdb.dwarf2/dw2-empty-inline-low-high.c | 39 +++
.../gdb.dwarf2/dw2-empty-inline-low-high.exp | 128 +++++++++
.../gdb.dwarf2/dw2-empty-inline-ranges.c | 54 ++++
.../gdb.dwarf2/dw2-empty-inline-ranges.exp | 262 ++++++++++++++++++
.../gdb.dwarf2/dw2-unexpected-entry-pc.exp | 75 +++--
gdb/testsuite/gdb.opt/empty-inline-cxx.cc | 65 +++++
gdb/testsuite/gdb.opt/empty-inline-cxx.exp | 95 +++++++
gdb/testsuite/gdb.opt/empty-inline.c | 40 +++
gdb/testsuite/gdb.opt/empty-inline.exp | 130 +++++++++
11 files changed, 1018 insertions(+), 85 deletions(-)
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-empty-inline-low-high.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-empty-inline-low-high.exp
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.exp
create mode 100644 gdb/testsuite/gdb.opt/empty-inline-cxx.cc
create mode 100644 gdb/testsuite/gdb.opt/empty-inline-cxx.exp
create mode 100644 gdb/testsuite/gdb.opt/empty-inline.c
create mode 100644 gdb/testsuite/gdb.opt/empty-inline.exp

diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 634d67a9eba..6f3bd97e5bc 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -9038,6 +9038,16 @@ read_variable (struct die_info *die, struct dwarf2_cu *cu)
}
}

+/* Return true if an empty range associated with an entry of type TAG in
+ CU should be "fixed", that is, converted to a single byte, non-empty
+ range. */
+
+static bool
+dwarf_fixup_empty_range (struct dwarf2_cu *cu, dwarf_tag tag)
+{
+ return tag == DW_TAG_inlined_subroutine && cu->producer_is_gcc ();
+}
+
/* Call CALLBACK from DW_AT_ranges attribute value OFFSET
reading .debug_rnglists.
Callback's type should be:
@@ -9200,7 +9210,12 @@ dwarf2_rnglists_process (unsigned offset, struct dwarf2_cu *cu,

/* Empty range entries have no effect. */
if (range_beginning == range_end)
- continue;
+ {
+ if (dwarf_fixup_empty_range (cu, tag))
+ range_end = (unrelocated_addr) ((CORE_ADDR) range_end + 1);
+ else
+ continue;
+ }

/* Only DW_RLE_offset_pair needs the base address added. */
if (rlet == DW_RLE_offset_pair)
@@ -9322,7 +9337,12 @@ dwarf2_ranges_process (unsigned offset, struct dwarf2_cu *cu, dwarf_tag tag,

/* Empty range entries have no effect. */
if (range_beginning == range_end)
- continue;
+ {
+ if (dwarf_fixup_empty_range (cu, tag))
+ range_end = (unrelocated_addr) ((CORE_ADDR) range_end + 1);
+ else
+ continue;
+ }

range_beginning = (unrelocated_addr) ((CORE_ADDR) range_beginning
+ (CORE_ADDR) *base);
@@ -9540,9 +9560,24 @@ dwarf2_get_pc_bounds (struct die_info *die, unrelocated_addr *lowpc,
if (ret == PC_BOUNDS_NOT_PRESENT || ret == PC_BOUNDS_INVALID)
return ret;

- /* partial_die_info::read has also the strict LOW < HIGH requirement. */
+ /* These LOW and HIGH values will be used to create a block. A block's
+ high address is the first address after the block's address range, so
+ if 'high <= low' then the block has no code associated with it. */
if (high <= low)
- return PC_BOUNDS_INVALID;
+ {
+ /* In some cases though, when the blocks LOW / HIGH were defined with
+ the DW_AT_low_pc and DW_AT_high_pc, we see some compilers create
+ an empty block when we can provide a better debug experience by
+ having a non-empty block. We do this by "fixing" the block to be
+ a single byte in length. See dwarf_fixup_empty_range for when
+ this fixup is performed. */
+ if (high == low
+ && ret == PC_BOUNDS_HIGH_LOW
+ && dwarf_fixup_empty_range (cu, die->tag))
+ high = (unrelocated_addr) (((ULONGEST) low) + 1);
+ else
+ return PC_BOUNDS_INVALID;
+ }

/* When using the GNU linker, .gnu.linkonce. sections are used to
eliminate duplicate copies of functions and vtables and such.
@@ -9823,8 +9858,33 @@ dwarf2_record_block_ranges (struct die_info *die, struct block *block,

CORE_ADDR low = per_objfile->relocate (unrel_low);
CORE_ADDR high = per_objfile->relocate (unrel_high);
+
fixup_low_high_pc (cu, die, &low, &high);
- cu->get_builder ()->record_block_range (block, low, high - 1);
+
+ /* Blocks where 'high < low' should be rejected earlier in the
+ process, e.g. see dwarf2_get_pc_bounds. */
+ gdb_assert (high >= low);
+
+ /* The value of HIGH is the first address past the end, but
+ GDB stores ranges with the high value as last inclusive
+ address, so in most cases we need to decrement HIGH here.
+
+ Blocks where 'high == low' represent an empty block (i.e. a
+ block with no associated code).
+
+ When 'high == low' and dwarf_fixup_empty_range returns true we
+ "fix" the empty range into a single byte range, which we can
+ do by leaving HIGH untouched. Otherwise we decrement HIGH,
+ which might result in 'high < low'. */
+ if (high > low || !dwarf_fixup_empty_range (cu, die->tag))
+ high -= 1;
+
+ /* If the above decrement resulted in 'high < low' then this
+ represents an empty range. There's little point storing this
+ in GDB's internal structures, it's just more to search
+ through, and it will never match any address. */
+ if (high >= low)
+ cu->get_builder ()->record_block_range (block, low, high);
}

attr = dwarf2_attr (die, DW_AT_ranges, cu);
diff --git a/gdb/testsuite/gdb.cp/step-and-next-inline.exp b/gdb/testsuite/gdb.cp/step-and-next-inline.exp
index 492a46fa166..effff2721fa 100644
--- a/gdb/testsuite/gdb.cp/step-and-next-inline.exp
+++ b/gdb/testsuite/gdb.cp/step-and-next-inline.exp
@@ -86,18 +86,16 @@ proc do_test { use_header } {
}

gdb_test "bt" "\\s*\\#0\\s+main.*" "in main"
- set line1 {\t\{}
- set line2 {\t if \(t != NULL}
- gdb_test_multiple "step" "step into get_alias_set" {
- -re -wrap $line1 {
- gdb_test "next" $line2 $gdb_test_name
- }
- -re -wrap $line2 {
- pass $gdb_test_name
- }
- }
+ gdb_test "step" \
+ [multi_line \
+ "get_alias_set \\(t=\[^\r\n\]+\\) at \[^\r\n\]+:$::decimal" \
+ "$::decimal\\s+if \\(t != NULL\\s*"] \
+ "step into get_alias_set"
gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
"not in inline 1"
+ gdb_test "next" ".*TREE_TYPE.*" "next step 1"
+ gdb_test "bt" "#0\\s+get_alias_set\[^\r\n\]*${srcfile}:.*" \
+ "not in inline 2"

# It's possible that this first failure (when not using a header
# file) is GCC's fault, though the remaining failures would best
@@ -107,27 +105,6 @@ proc do_test { use_header } {
# having location view support, so for now it is tagged as such.
set have_kfail [expr [test_compiler_info gcc*] && !$use_header]

- set ok 1
- gdb_test_multiple "next" "next step 1" {
- -re -wrap "if \\(t->x != i\\)" {
- set ok 0
- send_gdb "next\n"
- exp_continue
- }
- -re -wrap ".*TREE_TYPE.* != 1" {
- if { $ok } {
- pass $gdb_test_name
- } else {
- if { $have_kfail } {
- setup_kfail "*-*-*" symtab/25507
- }
- fail $gdb_test_name
- }
- }
- }
- gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
- "not in inline 2"
-
set ok 1
gdb_test_multiple "next" "next step 2" {
-re -wrap "return x;" {
@@ -196,12 +173,49 @@ proc do_test { use_header } {
"\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
"not in inline 5"

- if {!$use_header} {
- # With the debug from GCC 10.x (and earlier) GDB is currently
- # unable to successfully complete the following tests when we
- # are not using a header file.
- kfail symtab/25507 "stepping tests"
- return
+ # Clang at least upto v16 doesn't include line number information
+ # for anything but the first line of the 'tree_check' inline
+ # function. As a result, these tests will fail.
+ if { ![test_compiler_info "clang-*" "c++"] } {
+ clean_restart ${executable}
+
+ if ![runto_main] {
+ return
+ }
+
+ gdb_test "bt" "\\s*\\#0\\s+main.*" "in main pass 2"
+ gdb_test "step" \
+ [multi_line \
+ "get_alias_set \\(t=\[^\r\n\]+\\) at \[^\r\n\]+:$::decimal" \
+ "$::decimal\\s+if \\(t != NULL\\s*"] \
+ "step into get_alias_set, pass 2"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "in get_alias_set pass 2"
+ gdb_test "step" ".*TREE_TYPE.*" "step 1"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 1 pass 2"
+ gdb_test "step" ".*if \\(t->x != i\\).*" "step 2"
+ gdb_test "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "in inline 1 pass 2"
+ gdb_test "step" ".*return x.*" "step 3"
+ gdb_test "step" ".*TREE_TYPE.*" "step 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 2 pass 2"
+ gdb_test "step" ".*if \\(t->x != i\\).*" "step 5"
+ gdb_test "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "in inline 2 pass 2"
+ gdb_test "step" ".*return x.*" "step 6"
+ gdb_test "step" ".*TREE_TYPE.*" "step 7"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 3 pass 2"
+ gdb_test "step" ".*if \\(t->x != i\\).*" "step 8"
+ gdb_test "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "in inline 3 pass 2"
+ gdb_test "step" "return x.*" "step 9"
+ gdb_test "step" "return 0.*" "step 10"
+ gdb_test "bt" \
+ "\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
+ "not in inline 4 pass 2"
}

clean_restart ${executable}
@@ -210,32 +224,37 @@ proc do_test { use_header } {
return
}

- gdb_test "bt" "\\s*\\#0\\s+main.*" "in main pass 2"
- gdb_test "step" ".*" "step into get_alias_set pass 2"
- gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
- "in get_alias_set pass 2"
- gdb_test "step" ".*TREE_TYPE.*" "step 1"
- gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
- "not in inline 1 pass 2"
- gdb_test "step" ".*if \\(t->x != i\\).*" "step 2"
- gdb_test "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
- "in inline 1 pass 2"
- gdb_test "step" ".*TREE_TYPE.*" "step 3"
- gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
- "not in inline 2 pass 2"
- gdb_test "step" ".*if \\(t->x != i\\).*" "step 4"
- gdb_test "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
- "in inline 2 pass 2"
- gdb_test "step" ".*TREE_TYPE.*" "step 5"
- gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
- "not in inline 3 pass 2"
- gdb_test "step" ".*if \\(t->x != i\\).*" "step 6"
- gdb_test "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
- "in inline 3 pass 2"
- gdb_test "step" "return 0.*" "step 7"
- gdb_test "bt" \
- "\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
- "not in inline 4 pass 2"
+ gdb_test "bt" "#0\\s+main.*" "in main pass 3"
+ gdb_test "step" \
+ [multi_line \
+ "get_alias_set \\(t=\[^\r\n\]+\\) at \[^\r\n\]+:$::decimal" \
+ "$::decimal\\s+if \\(t != NULL\\s*"] \
+ "step into get_alias_set, pass 3"
+ gdb_test "bt" "#0\\s+get_alias_set\[^\r\n\]*${srcfile}:.*" \
+ "in get_alias_set pass 3"
+ gdb_test "step" ".*TREE_TYPE.*" "step 1 pass 3"
+ gdb_test "bt" "#0\\s+get_alias_set\[^\r\n\]*${srcfile}:.*" \
+ "not in inline 1 pass 3"
+ gdb_test "step" ".*if \\(t->x != i\\).*" "step 2 pass 3"
+ gdb_test "bt" "#0\\s+\[^\r\n\]*tree_check\[^\r\n\]*${hdrfile}:.*" \
+ "in inline 1 pass 3"
+ gdb_test_multiple "p t->x = 2" "change value pass 3" {
+ -re ".*value has been optimized out.*$::gdb_prompt $" {
+ gdb_test "p xx.x = 2" ".* = 2.*" $gdb_test_name
+ }
+ -re ".* = 2.*$::gdb_prompt $" {
+ pass $gdb_test_name
+ }
+ }
+
+ # Clang at least upto v16 doesn't include line number information
+ # for anything but the first line of the 'tree_check' inline
+ # function. As a result, this test will fail.
+ if { ![test_compiler_info "clang-*" "c++"] } {
+ gdb_test "step" ".*abort.*" "step 3, pass 3"
+ gdb_test "bt" "#0\\s+\[^\r\n\]*tree_check\[^\r\n\]*${hdrfile}:.*" \
+ "abort from inline 1 pass 3"
+ }
}
}

diff --git a/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-low-high.c b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-low-high.c
new file mode 100644
index 00000000000..2e77e28822e
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-low-high.c
@@ -0,0 +1,39 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ 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/>. */
+
+volatile int global_var = 0;
+
+int
+main (void)
+{ /* main decl line */
+ asm ("main_label: .globl main_label");
+ ++global_var;
+
+ asm ("main_0: .globl main_0");
+ ++global_var;
+
+ asm ("main_1: .globl main_1");
+ ++global_var; /* foo call line */
+
+ asm ("main_2: .globl main_2");
+ ++global_var;
+
+ asm ("main_3: .globl main_3");
+ ++global_var;
+
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-low-high.exp b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-low-high.exp
new file mode 100644
index 00000000000..67a21f03bde
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-low-high.exp
@@ -0,0 +1,128 @@
+# 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/>.
+
+# Define an inline function `foo` within the function `main`. The
+# function `foo` uses DW_AT_low_pc and DW_AT_high_pc to define its
+# range, except that DW_AT_high_pc is the constant 0.
+#
+# This should indicate that there is no code associated with `foo`,
+# however, with gcc versions at least between 8.x and 14.x (latest at
+# the time of writing this comment), it is observed that when these
+# empty inline functions are created, if GDB stops at the address
+# given in DW_AT_low_pc, then locals associated with the inline
+# function can usually be read.
+#
+# At the very least, stopping at the location of the inline function
+# means that the user can place a breakpoint on the inline function
+# and have GDB stop in a suitable location, that alone is helpful.
+#
+# This test defines an inline function, places a breakpoint, and then
+# runs and expects GDB to stop, and report the stop as being inside
+# the inline function.
+#
+# We then check that the next outer frame is `main` as expected, and
+# that the block for `foo` has been extended to a single byte, which
+# is how GDB gives the previously empty block some range.
+
+load_lib dwarf.exp
+
+require dwarf2_support
+
+standard_testfile .c .S
+
+# Lines we reference in the generated DWARF.
+set main_decl_line [gdb_get_line_number "main decl line"]
+set foo_call_line [gdb_get_line_number "foo call line"]
+
+get_func_info main
+
+set asm_file [standard_output_file $srcfile2]
+Dwarf::assemble $asm_file {
+ upvar entry_label entry_label
+
+ declare_labels lines_table inline_func
+
+ cu { } {
+ compile_unit {
+ {producer "GNU C 14.1.0"}
+ {language @DW_LANG_C}
+ {name $::srcfile}
+ {comp_dir /tmp}
+ {low_pc 0 addr}
+ {DW_AT_stmt_list $lines_table DW_FORM_sec_offset}
+ } {
+ inline_func: subprogram {
+ {name foo}
+ {inline @DW_INL_declared_inlined}
+ }
+ subprogram {
+ {name main}
+ {decl_file 1 data1}
+ {decl_line $::main_decl_line data1}
+ {decl_column 1 data1}
+ {low_pc $::main_start addr}
+ {high_pc $::main_len data4}
+ {external 1 flag}
+ } {
+ inlined_subroutine {
+ {abstract_origin %$inline_func}
+ {call_file 1 data1}
+ {call_line $::foo_call_line data1}
+ {low_pc main_1 addr}
+ {high_pc 0 data4}
+ }
+ }
+ }
+ }
+
+ lines {version 2} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+ }
+}
+
+if {[prepare_for_testing "failed to prepare" $testfile \
+ [list $srcfile $asm_file] {nodebug}]} {
+ return
+}
+
+if {![runto_main]} {
+ return
+}
+
+gdb_breakpoint foo
+gdb_test "continue" \
+ "Breakpoint $decimal, $hex in foo \\(\\)" \
+ "continue to b/p in foo"
+
+set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+set foo_end [get_hexadecimal_valueof "&main_1 + 1" "*UNKNOWN*" \
+ "get address of foo end"]
+
+gdb_test "maintenance info blocks" \
+ [multi_line \
+ "\\\[\\(block \\*\\) $hex\\\] $foo_start\\.\\.$foo_end" \
+ " entry pc: $foo_start" \
+ " inline function: foo" \
+ " symbol count: $decimal" \
+ " is contiguous"] \
+ "block for foo has some content"
+
+gdb_test "frame 1" \
+ [multi_line \
+ "#1 main \\(\\) at \[^\r\n\]+/$srcfile:$foo_call_line" \
+ "$foo_call_line\\s+\[^\r\n\]+/\\* foo call line \\*/"] \
+ "frame 1 is for main"
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.c b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.c
new file mode 100644
index 00000000000..24af7ba94e3
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.c
@@ -0,0 +1,54 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ 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/>. */
+
+volatile int global_var = 0;
+
+int
+main (void)
+{ /* main decl line */
+ asm ("main_label: .globl main_label");
+ ++global_var;
+
+ asm ("main_0: .globl main_0");
+ ++global_var;
+
+ asm ("main_1: .globl main_1");
+ ++global_var; /* foo call line */
+
+ asm ("main_2: .globl main_2");
+ ++global_var;
+
+ asm ("main_3: .globl main_3");
+ ++global_var;
+
+ asm ("main_4: .globl main_4");
+ ++global_var;
+
+ asm ("main_5: .globl main_5");
+ ++global_var;
+
+ asm ("main_6: .globl main_6");
+ ++global_var;
+
+ asm ("main_7: .globl main_7");
+ ++global_var;
+
+ asm ("main_8: .globl main_9");
+ ++global_var;
+
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.exp b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.exp
new file mode 100644
index 00000000000..5fc0b1833f6
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.exp
@@ -0,0 +1,262 @@
+# 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/>.
+
+# Define an inline function `foo` within the function `main`. The
+# function `foo` uses DW_AT_ranges to define its ranges. One of the
+# sub-ranges for foo will be empty.
+#
+# An empty sub-rnage should indicate that there is no code associated
+# with `foo` at that address, however, with gcc versions at least
+# between 8.x and 14.x (latest at the time of writing this comment),
+# it is observed that when these empty sub-ranges are created for an
+# inline function, if GDB treats the sub-range as non-empty, and stops
+# at that location, then this generally gives a better debug
+# experience. It is often still possible to read local variables at
+# that address.
+#
+# This function defines an inline function, places a breakpoint on its
+# entry-pc, and then runs and expects GDB to stop, and report the stop
+# as being inside the inline function.
+#
+# We then check that the next outer frame is `main` as expected, and
+# that the block for `foo` has the expected sub-ranges.
+#
+# We compile a variety of different configurations, broadly there are
+# two variables, the location of the empty sub-range, and whether the
+# entry-pc points at the empty sub-range or not.
+#
+# The the empty sub-range location, the empty sub-range can be the
+# sub-range at the lowest address, highest address, or can be
+# somewhere between a blocks low and high addresses.
+
+load_lib dwarf.exp
+
+require dwarf2_support
+
+standard_testfile .c .S
+
+# Lines we reference in the generated DWARF.
+set main_decl_line [gdb_get_line_number "main decl line"]
+set foo_call_line [gdb_get_line_number "foo call line"]
+
+get_func_info main
+
+# Compile the source file and load the executable into GDB so we can
+# extract some addresses needed for creating the DWARF.
+if { [prepare_for_testing "failed to prepare" ${testfile} \
+ [list ${srcfile}]] } {
+ return -1
+}
+
+if {![runto_main]} {
+ return -1
+}
+
+# Some addresses that we need when generating the DWARF.
+for { set i 0 } { $i < 9 } { incr i } {
+ set main_$i [get_hexadecimal_valueof "&main_$i" "UNKNOWN" \
+ "get address for main_$i"]
+}
+
+# Create the DWARF assembler file into ASM_FILE. Using DWARF_VERSION
+# to define which style of ranges to create. FUNC_RANGES is a list of
+# 6 entries, each of which is an address, used to create the ranges
+# for the inline function DIE. The ENTRY_PC is also an address and is
+# used for the DW_AT_entry_pc of the inlined function.
+proc write_asm_file { asm_file dwarf_version func_ranges entry_pc } {
+ Dwarf::assemble $asm_file {
+ upvar entry_label entry_label
+ upvar dwarf_version dwarf_version
+ upvar func_ranges func_ranges
+ upvar entry_pc entry_pc
+
+ declare_labels lines_table inline_func ranges_label
+
+ cu { version $dwarf_version } {
+ compile_unit {
+ {producer "GNU C 14.1.0"}
+ {language @DW_LANG_C}
+ {name $::srcfile}
+ {comp_dir /tmp}
+ {low_pc 0 addr}
+ {DW_AT_stmt_list $lines_table DW_FORM_sec_offset}
+ } {
+ inline_func: subprogram {
+ {name foo}
+ {inline @DW_INL_declared_inlined}
+ }
+ subprogram {
+ {name main}
+ {decl_file 1 data1}
+ {decl_line $::main_decl_line data1}
+ {decl_column 1 data1}
+ {low_pc $::main_start addr}
+ {high_pc $::main_len data4}
+ {external 1 flag}
+ } {
+ inlined_subroutine {
+ {abstract_origin %$inline_func}
+ {call_file 1 data1}
+ {call_line $::foo_call_line data1}
+ {entry_pc $entry_pc addr}
+ {ranges $ranges_label DW_FORM_sec_offset}
+ }
+ }
+ }
+ }
+
+ lines {version 2} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+ }
+
+ if { $dwarf_version == 5 } {
+ rnglists {} {
+ table {} {
+ ranges_label: list_ {
+ start_end [lindex $func_ranges 0] [lindex $func_ranges 1]
+ start_end [lindex $func_ranges 2] [lindex $func_ranges 3]
+ start_end [lindex $func_ranges 4] [lindex $func_ranges 5]
+ }
+ }
+ }
+ } else {
+ ranges { } {
+ ranges_label: sequence {
+ range [lindex $func_ranges 0] [lindex $func_ranges 1]
+ range [lindex $func_ranges 2] [lindex $func_ranges 3]
+ range [lindex $func_ranges 4] [lindex $func_ranges 5]
+ }
+ }
+ }
+ }
+}
+
+# Gobal used to give each generated binary a unique name.
+set test_id 0
+
+proc run_test { dwarf_version empty_loc entry_pc_type } {
+ incr ::test_id
+
+ set this_testfile $::testfile-$::test_id
+
+ set asm_file [standard_output_file $this_testfile.S]
+
+ if { $empty_loc eq "start" } {
+ set ranges [list \
+ main_1 main_1 \
+ main_3 main_4 \
+ main_6 main_7]
+ set entry_pc_choices [list main_1 main_3]
+ } elseif { $empty_loc eq "middle" } {
+ set ranges [list \
+ main_1 main_2 \
+ main_4 main_4 \
+ main_6 main_7]
+ set entry_pc_choices [list main_4 main_1]
+ } elseif { $empty_loc eq "end" } {
+ set ranges [list \
+ main_1 main_2 \
+ main_4 main_5 \
+ main_7 main_7]
+ set entry_pc_choices [list main_7 main_1]
+ } else {
+ error "unknown location for empty range '$empty_loc'"
+ }
+
+ if { $entry_pc_type eq "empty" } {
+ set entry_pc_label [lindex $entry_pc_choices 0]
+ } elseif { $entry_pc_type eq "non_empty" } {
+ set entry_pc_label [lindex $entry_pc_choices 1]
+ } else {
+ error "unknown entry-pc type '$entry_pc_type'"
+ }
+
+ write_asm_file $asm_file $dwarf_version $ranges $entry_pc_label
+
+ if {[prepare_for_testing "failed to prepare" $this_testfile \
+ [list $::srcfile $asm_file] {nodebug}]} {
+ return
+ }
+
+ if {![runto_main]} {
+ return
+ }
+
+ # Continue until we stop in 'foo'.
+ gdb_breakpoint foo
+ gdb_test "continue" \
+ "Breakpoint $::decimal, $::hex in foo \\(\\)" \
+ "continue to b/p in foo"
+
+ # Check we stopped at the entry-pc.
+ set pc [get_hexadecimal_valueof "\$pc" "*UNKNOWN*" \
+ "get \$pc at breakpoint"]
+ set entry_pc [set ::$entry_pc_label]
+ gdb_assert { $pc == $entry_pc } "stopped at entry-pc"
+
+ # The block's expected overall low/high addresses.
+ set block_start [set ::[lindex $ranges 0]]
+ set block_end [set ::[lindex $ranges 5]]
+
+ # Setup variables r{0,1,2}s, r{0,1,2}e, to represent ranges start
+ # and end addresses. These are extracted from the RANGES
+ # variable. However, RANGES includes the empty ranges, so spot
+ # the empty ranges and update the end address as GDB does.
+ #
+ # Also, if the empty range is at the end of the block, then the
+ # block's overall end address also needs adjusting.
+ for { set i 0 } { $i < 3 } { incr i } {
+ set start [set ::[lindex $ranges [expr $i * 2]]]
+ set end [set ::[lindex $ranges [expr $i * 2 + 1]]]
+
+ if { $start == $end } {
+ set end [format "0x%x" [expr $end + 1]]
+ }
+ if { $block_end == $start } {
+ set block_end $end
+ }
+ set r${i}s $start
+ set r${i}e $end
+ }
+
+ # Check the block 'foo' has the expected ranges.
+ gdb_test "maintenance info blocks" \
+ [multi_line \
+ "\\\[\\(block \\*\\) $::hex\\\] $block_start\\.\\.$block_end" \
+ " entry pc: $entry_pc" \
+ " inline function: foo" \
+ " symbol count: $::decimal" \
+ " address ranges:" \
+ " $r0s\\.\\.$r0e" \
+ " $r1s\\.\\.$r1e" \
+ " $r2s\\.\\.$r2e"] \
+ "block for foo has some content"
+
+ # Check the outer frame is 'main' as expected.
+ gdb_test "frame 1" \
+ [multi_line \
+ "#1 main \\(\\) at \[^\r\n\]+/$::srcfile:$::foo_call_line" \
+ "$::foo_call_line\\s+\[^\r\n\]+/\\* foo call line \\*/"] \
+ "frame 1 is for main"
+}
+
+foreach_with_prefix dwarf_version { 4 5 } {
+ foreach_with_prefix empty_loc { start middle end } {
+ foreach_with_prefix entry_pc_type { empty non_empty } {
+ run_test $dwarf_version $empty_loc $entry_pc_type
+ }
+ }
+}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-unexpected-entry-pc.exp b/gdb/testsuite/gdb.dwarf2/dw2-unexpected-entry-pc.exp
index d8b738d38c7..c8ebd148d20 100644
--- a/gdb/testsuite/gdb.dwarf2/dw2-unexpected-entry-pc.exp
+++ b/gdb/testsuite/gdb.dwarf2/dw2-unexpected-entry-pc.exp
@@ -18,7 +18,7 @@
#
# Within the function's ranges, create an empty sub-range, many
# versions of gcc (8.x to at least 14.x) do this, and point the
-# DW_AT_entry_pc at this empty sub-range (at last 8.x to 9.x did
+# DW_AT_entry_pc at this empty sub-range (at least 8.x to 9.x did
# this).
#
# Now place a breakpoint on the inline function and run to the
@@ -51,11 +51,14 @@ if ![runto_main] {
}

# Some label addresses, needed to match against the output later.
-foreach foo {foo_1 foo_2 foo_3 foo_4 foo_5 foo_6} {
+foreach foo {foo_1 foo_2 foo_3 foo_4 foo_5 foo_6 foo_7} {
set $foo [get_hexadecimal_valueof "&$foo" "UNKNOWN" \
"get address for $foo label"]
}

+set foo_3_end [get_hexadecimal_valueof "&foo_3 + 1" "UNKNOWN" \
+ "get address for 'foo_3 + 1'"]
+
# Some line numbers needed in the generated DWARF.
set foo_decl_line [gdb_get_line_number "foo decl line"]
set bar_call_line [gdb_get_line_number "bar call line"]
@@ -85,24 +88,40 @@ if [is_ilp32_target] {
# generated which covers some parts of the inlined function. This
# makes most sense when being tested with the 'foo_6' label, as that
# label is all about handling the end of the inline function case.
+#
+# The PRODUCER is the string used to control the DW_AT_producer string
+# in the CU. When PRODUCER is 'gcc' then a string is used that
+# represents the gcc compiler. When PRODUCER is 'other' then a string
+# that will not be interpreted as gcc is used. The gcc compiler will
+# sometimes generate empty ranges for inline functions (from at least
+# gcc 8.x through to the currently latest release 14.x), and so GDB
+# has code in place to convert empty ranges to non-empty. This fix is
+# not applied to other compilers at this time.

-proc run_test { entry_label dwarf_version with_line_table } {
- set dw_testname "${::testfile}-${dwarf_version}-${entry_label}"
+proc run_test { producer entry_label dwarf_version with_line_table } {
+ set dw_testname "${::testfile}-${producer}-${dwarf_version}-${entry_label}"

if { $with_line_table } {
set dw_testname ${dw_testname}-lt
}

+ if { $producer eq "other" } {
+ set producer_str "ACME C 1.0.0"
+ } else {
+ set producer_str "GNU C 10.0.0"
+ }
+
set asm_file [standard_output_file "${dw_testname}.S"]
Dwarf::assemble $asm_file {
upvar dwarf_version dwarf_version
upvar entry_label entry_label
+ upvar producer_str producer_str

declare_labels lines_table inline_func ranges_label

cu { version $dwarf_version } {
compile_unit {
- {producer "gcc"}
+ {producer $producer_str}
{language @DW_LANG_C}
{name $::srcfile}
{comp_dir /tmp}
@@ -157,6 +176,10 @@ proc run_test { entry_label dwarf_version with_line_table } {
line 2
DW_LNS_copy

+ DW_LNE_set_address foo_3
+ line 3
+ DW_LNS_copy
+
DW_LNE_set_address foo_6
line 10
DW_LNS_copy
@@ -170,7 +193,13 @@ proc run_test { entry_label dwarf_version with_line_table } {
line $::bar_call_line
DW_LNS_copy

+ DW_LNE_set_address foo_7
+ DW_LNS_negate_stmt
+ line [expr $::bar_call_line + 1]
+ DW_LNS_copy
+
DW_LNE_set_address "$::foo_start + $::foo_len"
+ line [expr $::bar_call_line + 2]
DW_LNE_end_sequence
}
}
@@ -206,6 +235,16 @@ proc run_test { entry_label dwarf_version with_line_table } {
return false
}

+ if { $producer eq "gcc" } {
+ set entry_pc $::foo_3
+ set empty_range_re "\r\n $::foo_3\\.\\.$::foo_3_end"
+ set line_num 3
+ } else {
+ set entry_pc $::foo_1
+ set empty_range_re ""
+ set line_num 1
+ }
+
# Place a breakpoint on `bar` and run to the breakpoint. Use
# gdb_test as we want full pattern matching against the stop
# location.
@@ -215,8 +254,8 @@ proc run_test { entry_label dwarf_version with_line_table } {
if { $with_line_table } {
set re \
[multi_line \
- "Breakpoint $::decimal, bar \\(\\) at \[^\r\n\]+/$::srcfile:1" \
- "1\\s+\[^\r\n\]+"]
+ "Breakpoint $::decimal, bar \\(\\) at \[^\r\n\]+/$::srcfile:$line_num" \
+ "$line_num\\s+\[^\r\n\]+"]
} else {
set re "Breakpoint $::decimal, $::hex in bar \\(\\)"
}
@@ -230,21 +269,23 @@ proc run_test { entry_label dwarf_version with_line_table } {
gdb_test "maint info blocks" \
[multi_line \
"\\\[\\(block \\*\\) $::hex\\\] $::foo_1\\.\\.$::foo_6" \
- " entry pc: $::foo_1" \
+ " entry pc: $entry_pc" \
" inline function: bar" \
" symbol count: $::decimal" \
- " address ranges:" \
+ " address ranges:$empty_range_re" \
" $::foo_1\\.\\.$::foo_2" \
" $::foo_5\\.\\.$::foo_6"]
}

-foreach_with_prefix dwarf_version { 4 5 } {
- # Test various labels without any line table present.
- foreach_with_prefix entry_label { foo_3 foo_4 foo_2 foo_6 } {
- run_test $entry_label $dwarf_version false
- }
+foreach_with_prefix producer { other gcc } {
+ foreach_with_prefix dwarf_version { 4 5 } {
+ # Test various labels without any line table present.
+ foreach_with_prefix entry_label { foo_3 foo_4 foo_2 foo_6 } {
+ run_test $producer $entry_label $dwarf_version false
+ }

- # Now test what happens if we use the end address of the block,
- # but also supply a line table. Does GDB do anything different?
- run_test foo_6 $dwarf_version true
+ # Now test what happens if we use the end address of the block,
+ # but also supply a line table. Does GDB do anything different?
+ run_test $producer foo_6 $dwarf_version true
+ }
}
diff --git a/gdb/testsuite/gdb.opt/empty-inline-cxx.cc b/gdb/testsuite/gdb.opt/empty-inline-cxx.cc
new file mode 100644
index 00000000000..f2e163d4646
--- /dev/null
+++ b/gdb/testsuite/gdb.opt/empty-inline-cxx.cc
@@ -0,0 +1,65 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ 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/>. */
+
+#include "attributes.h"
+
+/* A global to do some work on. This being volatile is important. Without
+ this the compiler might optimise the whole program away. */
+volatile int global = 0;
+
+__attribute__((noinline)) ATTRIBUTE_NOCLONE void
+breakpt ()
+{
+ /* Some filler work. */
+ global++;
+}
+
+struct MyClass;
+
+struct ptr
+{
+ /* The following line is a single line to aid matching in the test
+ script. Sometimes the DWARF will point GDB at the '{' and sometimes
+ at the body of the function. We don't really care for this test, so
+ placing everything on one line removes this variability. */
+ MyClass* get_myclass () { return t; }
+
+ MyClass* t;
+};
+
+struct MyClass
+{
+ void call();
+};
+
+void
+MyClass::call ()
+{
+ breakpt (); /* Final breakpoint. */
+}
+
+static void
+intermediate (ptr p)
+{
+ p.get_myclass ()->call ();
+}
+
+int
+main ()
+{
+ intermediate (ptr {new MyClass});
+}
diff --git a/gdb/testsuite/gdb.opt/empty-inline-cxx.exp b/gdb/testsuite/gdb.opt/empty-inline-cxx.exp
new file mode 100644
index 00000000000..1ead9ed7c5e
--- /dev/null
+++ b/gdb/testsuite/gdb.opt/empty-inline-cxx.exp
@@ -0,0 +1,95 @@
+# 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/>.
+
+standard_testfile .cc
+
+require {expr ![test_compiler_info gcc* c++] \
+ || [supports_statement_frontiers] }
+
+set options {c++ debug optimize=-Og}
+lappend_include_file options $srcdir/lib/attributes.h
+if {[supports_statement_frontiers]} {
+ lappend options additional_flags=-gstatement-frontiers
+}
+
+# Some line numbers we need for the test.
+set get_myclass_line [gdb_get_line_number "MyClass* get_myclass ()"]
+set call_get_line [gdb_get_line_number "p.get_myclass ()"]
+set final_bp_line [gdb_get_line_number "Final breakpoint"]
+
+# Build the test executable adding "-OPT_LEVEL" to the compilation
+# flags. The break on the small function which is likely to have been
+# inlined, check we stop where we expect, and that the backtrace looks
+# correct.
+#
+# Then return from the inline function and call to another function,
+# check the backtrace from this second function also looks good,
+# specifically, we're checking that the backtrace doesn't incorrectly
+# place frame #1 on the line for the inline function.
+proc run_test { opt_level } {
+
+ set opts $::options
+ lappend opts "additional_flags=-${opt_level}"
+
+ if { [prepare_for_testing "failed to prepare" "$::testfile-$opt_level" \
+ $::srcfile $opts] } {
+ return
+ }
+
+ if { ![runto_main] } {
+ return
+ }
+
+ gdb_test "bt" "#0\\s+main \\(\\) \[^\r\n\]+/$::srcfile:$::decimal" \
+ "backtrace in main"
+
+ # Break at the empty inline function ptr::get_myclass.
+ gdb_breakpoint get_myclass
+ gdb_continue_to_breakpoint "continue to get_myclass" \
+ [multi_line \
+ ".*/$::srcfile:$::get_myclass_line" \
+ "$::get_myclass_line\\s+MyClass\\* get_myclass \\(\\) \[^\r\n\]+"]
+
+ # Backtrace.
+ gdb_test "bt" \
+ [multi_line \
+ "#0\\s+ptr::get_myclass\[^\r\n\]+/$::srcfile:$::get_myclass_line" \
+ "#1\\s+intermediate\[^\r\n\]+/$::srcfile:$::call_get_line" \
+ "#2\\s+\[^\r\n\]+main \\(\\) \[^\r\n\]+/$::srcfile:$::decimal"] \
+ "at get_myclass"
+
+ # Print a class member variable, this should be in scope, but is often
+ # reported as optimised out.
+ gdb_test "p t" \
+ "(?:\\\$1 = \\(MyClass \\*\\) $::hex|value has been optimized out)" \
+ "print ptr::t"
+
+ gdb_breakpoint $::srcfile:$::final_bp_line
+ gdb_continue_to_breakpoint "continue to final breakpoint"
+
+ # 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.
+ gdb_test "bt" \
+ [multi_line \
+ "#0\\s+MyClass::call\[^\r\n\]+/$::srcfile:$::final_bp_line" \
+ "#1\\s+\[^\r\n\]+ intermediate\[^\r\n\]+/$::srcfile:$::call_get_line" \
+ "#2\\s+\[^\r\n\]+ main \\(\\) \[^\r\n\]+/$::srcfile:$::decimal"] \
+ "at call"
+}
+
+foreach_with_prefix opt_level { Og O0 O1 O2 } {
+ run_test ${opt_level}
+}
diff --git a/gdb/testsuite/gdb.opt/empty-inline.c b/gdb/testsuite/gdb.opt/empty-inline.c
new file mode 100644
index 00000000000..91ad6f01b70
--- /dev/null
+++ b/gdb/testsuite/gdb.opt/empty-inline.c
@@ -0,0 +1,40 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ 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/>. */
+
+#include "attributes.h"
+
+static int
+test0 (void)
+{
+ asm (""); /* First line of test0. */
+ return 1; /* Second line of test0. */
+}
+
+int __attribute__((noinline)) ATTRIBUTE_NOCLONE
+test1 (int x)
+{
+ asm ("");
+ return x + 1; /* Second line of test1. */
+}
+
+int
+main (void)
+{
+ test1 (test0 ()); /* First line of main. */
+ test1 (test0 ()); /* Second line of main. */
+ return 0; /* Third line of main. */
+}
diff --git a/gdb/testsuite/gdb.opt/empty-inline.exp b/gdb/testsuite/gdb.opt/empty-inline.exp
new file mode 100644
index 00000000000..af29431a3fd
--- /dev/null
+++ b/gdb/testsuite/gdb.opt/empty-inline.exp
@@ -0,0 +1,130 @@
+# 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/>.
+
+# Create a test file with an inline function for which gcc (at least)
+# will, on some architectures, create a funciton with a zero byte
+# range. The inline function is something pretty trivial, e.g. a
+# function that just returns a constant integer. On x86-64 gcc will
+# make the integer an operand of an instruction within the caller. As
+# a result the inline function doesn't even get a full instruction
+# (it's just one operand) and gcc generates DWARF that gives the
+# inline function a zero byte range.
+#
+# The problem with this is that we can no longer step into the inline
+# function.
+#
+# GDB will expand the range of inline functions to be at least a
+# single byte, this allows the user to step into inline functions.
+
+standard_testfile
+
+require {expr ![test_compiler_info gcc* c++] \
+ || [supports_statement_frontiers] }
+
+set options {debug nowarnings optimize=-O2}
+lappend_include_file options $srcdir/lib/attributes.h
+if {[supports_statement_frontiers]} {
+ lappend options additional_flags=-gstatement-frontiers
+}
+
+# Some line numbers we need.
+set lineno_main_1 [gdb_get_line_number "First line of main"]
+set lineno_main_2 [gdb_get_line_number "Second line of main"]
+set lineno_main_3 [gdb_get_line_number "Third line of main"]
+set lineno_test0_1 [gdb_get_line_number "First line of test0"]
+set lineno_test0_2 [gdb_get_line_number "Second line of test0"]
+set lineno_test1_2 [gdb_get_line_number "Second line of test1"]
+
+# Step into some very small functions that could (at some optimisation
+# levels) be inlined. Check the backtrace at various points to
+# confirm that GDB thinks it is in the right place.
+#
+# OPT_LEVEL should be a string 'O0', 'O1', etc, and is passed to the
+# compiler in the build flags.
+proc run_test { opt_level } {
+
+ set opts $::options
+ lappend opts "additional_flags=-${opt_level}"
+
+ if { [prepare_for_testing "failed to prepare" "$::testfile-$opt_level" \
+ $::srcfile $opts] } {
+ return
+ }
+
+ if { ![runto_main] } {
+ return
+ }
+
+ gdb_test "frame 0" \
+ [multi_line \
+ "#0\\s+main \[^\r\n\]+/$::srcfile:$::lineno_main_1" \
+ "$::lineno_main_1\\s+\[^\r\n\]+"] \
+ "frame 0 while in main"
+
+ gdb_test_multiple "step" "step into test0" {
+ -re -wrap ".*test0.*$::srcfile:$::lineno_test0_1.*" {
+ gdb_test "step" "^$::lineno_test0_2\\s+\[^\r\n\]+" $gdb_test_name
+ }
+ -re -wrap ".*test0.*$::srcfile:$::lineno_test0_2.*" {
+ pass $gdb_test_name
+ }
+ }
+
+ gdb_test "frame 1" \
+ [multi_line \
+ "#1\\s+main \[^\r\n\]+/$::srcfile:$::lineno_main_1" \
+ "$::lineno_main_1\\s+\[^\r\n\]+"] \
+ "inspect frame 1, main"
+
+ # Step into test1() function.
+ gdb_test "step" \
+ [multi_line \
+ "test1 \\(\[^)\]+\\) at \[^\r\n\]+/$::srcfile:$::lineno_test1_2" \
+ "$::lineno_test1_2\\s+\[^\r\n\]+"] \
+ "step into test1"
+
+ # 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.
+ gdb_test "frame 1" \
+ [multi_line \
+ "#1\\s+\[^\r\n\]*main \\(\\) \[^\r\n\]+/$::srcfile:$::lineno_main_1" \
+ "$::lineno_main_1\\s+\[^\r\n\]+"] \
+ "inspect frame 1 again, still main"
+
+ # Step from the last line of test1 back into main.
+ gdb_test "step" \
+ [multi_line \
+ "main \\(\\) at \[^\r\n\]+/$::srcfile:$::lineno_main_2" \
+ "$::lineno_main_2\\s+\[^\r\n\]+"] \
+ "step back to main"
+
+ # Use next to step to the last line of main. This skips over the inline
+ # call to test0, and the non-inline call to test1.
+ gdb_test "next" \
+ "$::lineno_main_3\\s+return 0;\\s+\[^\r\n\]+" \
+ "step over test0+1"
+
+ # Sanity check that we are in main like we expect.
+ gdb_test "frame 0" \
+ [multi_line \
+ "#0\\s+main \[^\r\n\]+/$::srcfile:$::lineno_main_3" \
+ "$::lineno_main_3\\s+return 0;\\s+\[^\r\n\]+"] \
+ "confirm expected frame in main"
+}
+
+foreach_with_prefix opt_level { Og O0 O1 O2 } {
+ run_test ${opt_level}
+}
--
2.47.1