* [PATCH 0/7] Inline Function Optimised Code Debug Improvements
@ 2025-07-20 10:20 Andrew Burgess
2025-07-20 10:20 ` [PATCH 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
` (8 more replies)
0 siblings, 9 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-07-20 10:20 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
This series presents some improvements to debugging inline functions,
especially in optimised code.
The first two patches of this series have been posted previously here:
https://inbox.sourceware.org/gdb-patches/cover.1736865029.git.aburgess@redhat.com
This series replaces that earlier work, though the patches are
basically unchanged.
This entire series is an alternative solution to the patches that were
posted here:
https://inbox.sourceware.org/gdb-patches/AS1PR01MB946510286FBF2497A6F03E83E4922@AS1PR01MB9465.eurprd01.prod.exchangelabs.com/
My reasons for writing an alternative patch set can be found in the
commit messages of patches (2) and (7), I'm not going to repeat them
here. However, Bernd, the original patch author, did amazing work
identifying the problems GDB was having, and these patches likely
wouldn't exist without the original patch series, so a big thanks
there.
For reviewing, this series can be considered in 3 parts:
+ Patches (1) and (2) are independent from the rest of the series,
though related in theme. Both these patches could be approved and
merged ahead of remaining work, and do offer an improvement to GDB
on their own.
+ Patches (3), (4), (5), and (6) are refactoring work to support the
last patch. These can be reviewed, but shouldn't be merged until
the last patch is approved.
+ Patch (7) covers the final parts of this work, which deal with
non-empty inline functions.
All thoughts and feedback are welcome.
Thanks,
Andrew
---
Andrew Burgess (7):
gdb: improve line number lookup around inline functions
gdb: handle empty ranges for inline subroutines
gdb: split dwarf line table parsing in two
gdb: move block range recording into its own function
gdb: create address map after parsing all DIE
gdb: record block end addresses while parsing DIEs
gdb: fix-up truncated inline function block ranges
gdb/buildsym.c | 42 +-
gdb/buildsym.h | 13 +-
gdb/dwarf2/cu.h | 7 +
gdb/dwarf2/read.c | 288 +++++++--
gdb/symtab.c | 25 +-
gdb/testsuite/gdb.cp/step-and-next-inline.exp | 171 ++++--
.../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 | 260 ++++++++
.../gdb.dwarf2/dw2-extend-inline-block.c | 78 +++
.../gdb.dwarf2/dw2-extend-inline-block.exp | 574 ++++++++++++++++++
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c | 79 +++
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp | 227 +++++++
.../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 ++++
gdb/testsuite/gdb.opt/inline-bt.c | 28 +
gdb/testsuite/gdb.opt/inline-bt.exp | 119 ++--
21 files changed, 2342 insertions(+), 195 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.dwarf2/dw2-extend-inline-block.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.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
base-commit: cfbf9925c1c34f9e9d47c8b29d165866557663e3
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH 1/7] gdb: improve line number lookup around inline functions
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
@ 2025-07-20 10:20 ` Andrew Burgess
2025-07-20 10:20 ` [PATCH 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
` (7 subsequent siblings)
8 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-07-20 10:20 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
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 7d1a0b066c7..cacc3d21386 100644
--- a/gdb/symtab.c
+++ b/gdb/symtab.c
@@ -3304,14 +3304,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..bc77d39d0c9
--- /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 {} {
+ 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/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
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH 2/7] gdb: handle empty ranges for inline subroutines
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-07-20 10:20 ` [PATCH 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
@ 2025-07-20 10:20 ` Andrew Burgess
2025-07-20 10:20 ` [PATCH 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
` (6 subsequent siblings)
8 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-07-20 10:20 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess, Bernd Edlinger
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 | 260 ++++++++++++++++++
.../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, 1016 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 5e18e452061..02641588f96 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..917ab8bdcda
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-empty-inline-ranges.exp
@@ -0,0 +1,260 @@
+# 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 [lindex $entry_pc_choices 0]
+ } elseif { $entry_pc_type eq "non_empty" } {
+ set entry_pc [lindex $entry_pc_choices 1]
+ } else {
+ error "unknown entry-pc type '$entry_pc_type'"
+ }
+
+ write_asm_file $asm_file $dwarf_version $ranges $entry_pc
+
+ 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"]
+ gdb_assert { $pc == $entry_pc } "stopped at entry-pc"
+
+ # The block's expected overall low/high addresses.
+ set block_start [lindex $ranges 0]
+ set block_end [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 [lindex $ranges [expr $i * 2]]
+ set end [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
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH 3/7] gdb: split dwarf line table parsing in two
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-07-20 10:20 ` [PATCH 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
2025-07-20 10:20 ` [PATCH 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
@ 2025-07-20 10:20 ` Andrew Burgess
2025-07-20 10:20 ` [PATCH 4/7] gdb: move block range recording into its own function Andrew Burgess
` (5 subsequent siblings)
8 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-07-20 10:20 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
A later commit in this series, that improves GDB's ability to debug
optimised code, wants to use the line table information in order to
"fix" inline blocks with a truncated address range. For the reasoning
behind wanting to do that, please read ahead in the series.
Assuming that we accept for now the need to use the line table
information to adjust the block ranges, then why is this commit
needed?
GDB splits the line table data info different symtabs, adding end of
sequence markers as we move between symtabs. This seems to work fine
for GDB, but causes a problem for me in this case.
What I will want to do is this: scan the line table and spot line
table entries that corresponds to the end addresses of an inline
block's address range. If the block meets certain requirements, then
the end address of the block is adjusted to be that of the next line
table entry.
The way that GDB currently splits the line table entries between
symtabs makes this harder. I will have the set of blocks end
addresses which I know might be fixable, but to find the line table
entry corresponding to that address requires searching through all the
symtabs. Having found the entry for the end address, I then need to
find the next line table entry. For some blocks this is easy, it's
the next entry in the same symtab. But for other blocks the next
entry might be in a different symtab, which requires yet another full
search.
I did try implementing this approach, but the number of full symtab
searches is significant, and it had a significant impact on GDB's
debug parsing performance. The impact was such that an operation that
currently takes ~7seconds would take ~3minutes or more. Now I could
possibly improve that 3 minutes figure by optimising the code some,
but I think that would add unnecessary complexity.
By deferring building the line table until after we have parsed the
DIEs it becomes simple to spot when a line table entry corresponds to
a block end address, and finding the next entry is always trivial, as,
at this point, the next entry is just the next entry which we will
process. With this approach I see no noticable impact on DWARF
parsing performance.
This patch is just the refactoring. There's no finding block end
addresses and "fixing" being done here. This just sets things up for
the later commits.
There should be no user visible changes after this commit.
---
gdb/dwarf2/read.c | 111 +++++++++++++++++++++++++---------------------
1 file changed, 61 insertions(+), 50 deletions(-)
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 02641588f96..46f87c097b4 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -787,9 +787,7 @@ static line_header_up dwarf_decode_line_header (sect_offset sect_off,
struct dwarf2_cu *cu,
const char *comp_dir);
-static void dwarf_decode_lines (struct line_header *,
- struct dwarf2_cu *,
- unrelocated_addr, int decode_mapping);
+static void dwarf_decode_lines (struct dwarf2_cu *cu, unrelocated_addr lowpc);
static void dwarf2_start_subfile (dwarf2_cu *cu, const file_entry &fe,
const line_header &lh);
@@ -5866,29 +5864,55 @@ find_file_and_directory (struct die_info *die, struct dwarf2_cu *cu)
return *cu->per_cu->fnd;
}
-/* Handle DW_AT_stmt_list for a compilation unit.
- DIE is the DW_TAG_compile_unit die for CU.
- COMP_DIR is the compilation directory. LOWPC is passed to
- dwarf_decode_lines. See dwarf_decode_lines comments about it. */
+/* Ensure that every file_entry within the line_table of CU has a symtab
+ allocated for it. */
static void
-handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
- const file_and_directory &fnd, unrelocated_addr lowpc,
- bool have_code) /* ARI: editCase function */
+create_symtabs_from_cu_line_table (struct dwarf2_cu *cu)
{
- dwarf2_per_objfile *per_objfile = cu->per_objfile;
- struct attribute *attr;
- hashval_t line_header_local_hash;
- void **slot;
- int decode_mapping;
+ /* Make sure a symtab is created for every file, even files
+ which contain only variables (i.e. no code with associated
+ line numbers). */
+ buildsym_compunit *builder = cu->get_builder ();
+ struct compunit_symtab *cust = builder->get_compunit_symtab ();
- gdb_assert (! cu->per_cu->is_debug_types);
+ struct line_header *lh = cu->line_header;
+ gdb_assert (lh != nullptr);
- attr = dwarf2_attr (die, DW_AT_stmt_list, cu);
+ for (auto &fe : lh->file_names ())
+ {
+ dwarf2_start_subfile (cu, fe, *lh);
+ subfile *sf = builder->get_current_subfile ();
+
+ if (sf->symtab == nullptr)
+ sf->symtab = allocate_symtab (cust, sf->name.c_str (),
+ sf->name_for_id.c_str ());
+
+ fe.symtab = sf->symtab;
+ }
+}
+
+
+/* Handle DW_AT_stmt_list for a compilation unit. DIE is the
+ DW_TAG_compile_unit die for CU. FND is used to access the compilation
+ directory. This function will decode the line table header and create
+ symtab objects for the files referenced in the line table. The line
+ table itself though is not processed by this function. If there is no
+ line table, or there's a problem decoding the header, then CU will not
+ be updated. */
+
+static void
+decode_line_header_for_cu (struct die_info *die, struct dwarf2_cu *cu,
+ const file_and_directory &fnd)
+{
+ gdb_assert (!cu->per_cu->is_debug_types);
+
+ struct attribute *attr = dwarf2_attr (die, DW_AT_stmt_list, cu);
if (attr == NULL || !attr->form_is_unsigned ())
return;
sect_offset line_offset = (sect_offset) attr->as_unsigned ();
+ dwarf2_per_objfile *per_objfile = cu->per_objfile;
/* The line header hash table is only created if needed (it exists to
prevent redundant reading of the line table for partial_units).
@@ -5906,8 +5930,9 @@ handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
xcalloc, xfree));
}
+ void **slot;
line_header line_header_local (line_offset, cu->per_cu->is_dwz);
- line_header_local_hash = line_header_hash (&line_header_local);
+ hashval_t line_header_local_hash = line_header_hash (&line_header_local);
if (per_objfile->line_header_hash != NULL)
{
slot = htab_find_slot_with_hash (per_objfile->line_header_hash.get (),
@@ -5960,12 +5985,8 @@ handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
then this is what we want as well. */
gdb_assert (die->tag != DW_TAG_partial_unit);
}
- decode_mapping = (die->tag != DW_TAG_partial_unit);
- /* The have_code check is here because, if LOWPC and HIGHPC are both 0x0,
- then there won't be any interesting code in the CU, but a check later on
- (in lnp_state_machine::check_line_address) will fail to properly exclude
- an entry that was removed via --gc-sections. */
- dwarf_decode_lines (cu->line_header, cu, lowpc, decode_mapping && have_code);
+
+ create_symtabs_from_cu_line_table (cu);
}
/* Process DW_TAG_compile_unit or DW_TAG_partial_unit. */
@@ -6017,10 +6038,12 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
scoped_restore restore_sym_cu
= make_scoped_restore (&per_objfile->sym_cu, cu);
- /* Decode line number information if present. We do this before
- processing child DIEs, so that the line header table is available
- for DW_AT_decl_file. */
- handle_DW_AT_stmt_list (die, cu, fnd, unrel_low, unrel_low != unrel_high);
+ /* Decode the line header if present. We do this before processing child
+ DIEs, so that information is available for DW_AT_decl_file. We defer
+ parsing the actual line table until after processing the child DIEs,
+ this allows us to fix up some of the inline function blocks as the
+ line table is read. */
+ decode_line_header_for_cu (die, cu, fnd);
/* Process all dies in compilation unit. */
for (die_info *child_die : die->children ())
@@ -6028,6 +6051,12 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
per_objfile->sym_cu = nullptr;
+ /* If we actually have code, then read the line table now. */
+ if (unrel_low != unrel_high
+ && die->tag != DW_TAG_partial_unit
+ && cu->line_header != nullptr)
+ dwarf_decode_lines (cu, unrel_low);
+
/* Decode macro information, if present. Dwarf 2 macro information
refers to information in the line number info statement program
header, so we can only read it if we've read the header
@@ -16552,29 +16581,11 @@ dwarf_decode_lines_1 (struct line_header *lh, struct dwarf2_cu *cu,
table is read in. */
static void
-dwarf_decode_lines (struct line_header *lh, struct dwarf2_cu *cu,
- unrelocated_addr lowpc, int decode_mapping)
+dwarf_decode_lines (struct dwarf2_cu *cu, unrelocated_addr lowpc)
{
- if (decode_mapping)
- dwarf_decode_lines_1 (lh, cu, lowpc);
+ gdb_assert (cu->line_header != nullptr);
- /* Make sure a symtab is created for every file, even files
- which contain only variables (i.e. no code with associated
- line numbers). */
- buildsym_compunit *builder = cu->get_builder ();
- struct compunit_symtab *cust = builder->get_compunit_symtab ();
-
- for (auto &fe : lh->file_names ())
- {
- dwarf2_start_subfile (cu, fe, *lh);
- subfile *sf = builder->get_current_subfile ();
-
- if (sf->symtab == nullptr)
- sf->symtab = allocate_symtab (cust, sf->name.c_str (),
- sf->name_for_id.c_str ());
-
- fe.symtab = sf->symtab;
- }
+ dwarf_decode_lines_1 (cu->line_header, cu, lowpc);
}
/* Start a subfile for DWARF. FILENAME is the name of the file and
@@ -16839,7 +16850,7 @@ new_symbol (struct die_info *die, struct type *type, struct dwarf2_cu *cu,
if (file_cu->line_header == nullptr)
{
file_and_directory fnd (nullptr, nullptr);
- handle_DW_AT_stmt_list (file_cu->dies, file_cu, fnd, {}, false);
+ decode_line_header_for_cu (file_cu->dies, file_cu, fnd);
}
if (file_cu->line_header != nullptr)
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH 4/7] gdb: move block range recording into its own function
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (2 preceding siblings ...)
2025-07-20 10:20 ` [PATCH 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
@ 2025-07-20 10:20 ` Andrew Burgess
2025-07-20 10:20 ` [PATCH 5/7] gdb: create address map after parsing all DIE Andrew Burgess
` (4 subsequent siblings)
8 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-07-20 10:20 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Like the previous commit, this is a refactor that makes a later commit
in this series easier. The later commit improves GDB's ability to
debug optimised code. To do this I propose to "fix" the address
ranges of some inline blocks.
In order to know which blocks to fix, I need to record the end address
of inline blocks.
And so, I'd like to create a single common function where block ranges
are recorded, in a later commit I can then hook into this function to
record the block's end address(es). This commit sets up this single
common function.
The new function I'm adding dwarf2_record_single_block_range, takes a
currently unused argument unrel_high. This argument will be needed in
the later commit. I've added it now as this will allow the later
commit to be smaller and more focused. I only plan to push this
commit at the same time as the later, so I don't think adding
the (currently) unused argument is too much of a problem.
There should be no user visible change after this commit.
---
gdb/dwarf2/read.c | 21 ++++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 46f87c097b4..47fbccbdb08 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -9845,6 +9845,20 @@ dwarf2_record_block_entry_pc (struct die_info *die, struct block *block,
}
}
+/* Helper function for dwarf2_record_block_ranges. This function records
+ the address range for a single BLOCK. LOW and HIGH are the block's
+ range, these addresses are inclusive, so LOW is the first address in
+ the range, and HIGH is the last address inside the range. UNREL_HIGH
+ is the unrelocated version of HIGH. */
+
+static void
+dwarf2_record_single_block_range (struct dwarf2_cu *cu, struct block *block,
+ CORE_ADDR low, CORE_ADDR high,
+ unrelocated_addr unrel_high)
+{
+ cu->get_builder ()->record_block_range (block, low, high);
+}
+
/* Record the address ranges for BLOCK, offset by BASEADDR, as given
in DIE. Also set the entry PC for BLOCK. */
@@ -9913,7 +9927,8 @@ dwarf2_record_block_ranges (struct die_info *die, struct block *block,
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);
+ dwarf2_record_single_block_range (cu, block, low, high,
+ unrel_high);
}
attr = dwarf2_attr (die, DW_AT_ranges, cu);
@@ -9943,8 +9958,8 @@ dwarf2_record_block_ranges (struct die_info *die, struct block *block,
{
CORE_ADDR abs_start = per_objfile->relocate (start);
CORE_ADDR abs_end = per_objfile->relocate (end);
- cu->get_builder ()->record_block_range (block, abs_start,
- abs_end - 1);
+ dwarf2_record_single_block_range (cu, block, abs_start,
+ abs_end - 1, end);
blockvec.emplace_back (abs_start, abs_end);
});
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH 5/7] gdb: create address map after parsing all DIE
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (3 preceding siblings ...)
2025-07-20 10:20 ` [PATCH 4/7] gdb: move block range recording into its own function Andrew Burgess
@ 2025-07-20 10:20 ` Andrew Burgess
2025-07-20 10:20 ` [PATCH 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
` (3 subsequent siblings)
8 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-07-20 10:20 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Continuing the work done in the last two commits, this commit defers
building the addrmap for a block vector until after all the DIE have
been read, and the line table processed.
The benefit of this is that any changes to a block's ranges done
during line table processing (see the next commit) will be reflected
in the blockvector's addrmap.
The alternative to this is to build the addrmap as we initially see
each block, but then adjust the addrmap if we later decide to modify a
block. I think defering the addrmap creation is cleaner, and is less
work overall.
The addrmap requires that we add the most inner blocks first. I
achieve this by walking the blockvector backward, as we always add
parent blocks before their more inner child blocks.
There should be no user visible changes after this commit.
---
gdb/buildsym.c | 42 +++++++++++++++++++++++++++++++++++++-----
gdb/buildsym.h | 13 ++++---------
2 files changed, 41 insertions(+), 14 deletions(-)
diff --git a/gdb/buildsym.c b/gdb/buildsym.c
index 6dc079f29b1..ef50a3d8c18 100644
--- a/gdb/buildsym.c
+++ b/gdb/buildsym.c
@@ -412,8 +412,6 @@ buildsym_compunit::record_block_range (struct block *block,
if (start != block->start ()
|| end_inclusive + 1 != block->end ())
m_pending_addrmap_interesting = true;
-
- m_pending_addrmap.set_empty (start, end_inclusive, block);
}
struct blockvector *
@@ -449,9 +447,43 @@ buildsym_compunit::make_blockvector ()
/* If we needed an address map for this symtab, record it in the
blockvector. */
if (m_pending_addrmap_interesting)
- blockvector->set_map
- (new (&m_objfile->objfile_obstack) addrmap_fixed
- (&m_objfile->objfile_obstack, &m_pending_addrmap));
+ {
+ struct addrmap_mutable pending_addrmap;
+ int num_blocks = blockvector->num_blocks ();
+
+ /* If M_PENDING_ADDRMAP_INTERESTING is true then we must have seen
+ an interesting block. If we see one block, then we should at a
+ minimum have a global block, and a static block. */
+ gdb_assert (num_blocks > 1);
+
+ /* Assert our understanding of how the blocks are laid out. */
+ gdb_assert (blockvector->block (0)->is_global_block ());
+ gdb_assert (blockvector->block (1)->is_static_block ());
+
+ /* The 'J > 1' here is so that we don't place the global block into
+ the map. For CU with gaps, the static block will reflect the
+ gaps, while the global block will just reflect the full extent of
+ the range. */
+ for (int j = num_blocks; j > 1; )
+ {
+ --j;
+ struct block *b = blockvector->block (j);
+
+ gdb_assert (!b->is_global_block ());
+
+ if (b->is_contiguous ())
+ pending_addrmap.set_empty (b->start (), (b->end () - 1), b);
+ else
+ {
+ for (const auto &br : b->ranges ())
+ pending_addrmap.set_empty (br.start (), (br.end () - 1), b);
+ }
+ }
+
+ blockvector->set_map
+ (new (&m_objfile->objfile_obstack) addrmap_fixed
+ (&m_objfile->objfile_obstack, &pending_addrmap));
+ }
else
blockvector->set_map (nullptr);
diff --git a/gdb/buildsym.h b/gdb/buildsym.h
index 8f38131ec55..4fd9b61fb5d 100644
--- a/gdb/buildsym.h
+++ b/gdb/buildsym.h
@@ -418,15 +418,10 @@ struct buildsym_compunit
struct subfile *m_current_subfile = nullptr;
- /* The mutable address map for the compilation unit whose symbols
- we're currently reading. The symtabs' shared blockvector will
- point to a fixed copy of this. */
- struct addrmap_mutable m_pending_addrmap;
-
- /* True if we recorded any ranges in the addrmap that are different
- from those in the blockvector already. We set this to false when
- we start processing a symfile, and if it's still false at the
- end, then we just toss the addrmap. */
+ /* If there are gaps in the address range of any block associated with
+ this buildsym_compunit, then we need to create an address map, this
+ flag is set true to indicate the addrmap must be created. If this
+ remains false, then no addrmap will be created. */
bool m_pending_addrmap_interesting = false;
/* An obstack used for allocating pending blocks. */
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH 6/7] gdb: record block end addresses while parsing DIEs
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (4 preceding siblings ...)
2025-07-20 10:20 ` [PATCH 5/7] gdb: create address map after parsing all DIE Andrew Burgess
@ 2025-07-20 10:20 ` Andrew Burgess
2025-07-20 10:20 ` [PATCH 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
` (2 subsequent siblings)
8 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-07-20 10:20 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Continuing to work towards the goal of improving GDB's ability to
debug optimised code, this commit stores a map from the end address
of a block (or a block's sub-range) to the block pointer. This
information is collected while parsing the DIEs.
This new map is required after the last commit deferred building the
address map. The optimised code fix ups require that we can map from
an address back to a block, something the address map is perfect for.
However, the optimised code fix ups will then adjust the block ranges,
which means the address map is out of date.
So, I saw a couple of choices, I could build the address map while
parsing the DIES (as if the previous commit had not occurred), use the
address map to lookup inline blocks, and then either update, or
rebuild the full address map. Or, I could defer building the address
map, and then create this "partial" address map that only holds the
interesting, inline, blocks. I felt that maintaining this
alternative, partial address map was probably cheaper, take a look at
how new entries are added to the addrmap class compared to this
gdb::unordered_map.
Currently, nothing is done with this information, the information is
recorded as the block ranges are recorded, and then discarded after
the line table has been built. But in the next commit, this will be
used to help adjust the ranges of some inline blocks, and this will
improve GDB's ability to debug optimised code.
There should be no user visible changes after this commit.
---
gdb/dwarf2/cu.h | 7 +++++++
gdb/dwarf2/read.c | 8 ++++++++
2 files changed, 15 insertions(+)
diff --git a/gdb/dwarf2/cu.h b/gdb/dwarf2/cu.h
index 69f396c774a..c1c0d9fcaa2 100644
--- a/gdb/dwarf2/cu.h
+++ b/gdb/dwarf2/cu.h
@@ -269,6 +269,13 @@ struct dwarf2_cu
return m_producer;
}
+ /* The end addresses for some inline blocks. For blocks with multiple
+ sub-ranges, this is the end address of every sub-range within the
+ block. These are the inclusive end addresses, that is, these are the
+ last addresses inside the block's ranges. Only the first block that
+ ends at any given address will be recorded. */
+ gdb::unordered_map<unrelocated_addr, struct block *> inline_block_ends;
+
private:
const char *m_producer = nullptr;
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 47fbccbdb08..992cf008da0 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -6057,6 +6057,10 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
&& cu->line_header != nullptr)
dwarf_decode_lines (cu, unrel_low);
+ /* We no longer need to track the inline block end addresses. Release
+ memory associated with this. */
+ cu->inline_block_ends.clear ();
+
/* Decode macro information, if present. Dwarf 2 macro information
refers to information in the line number info statement program
header, so we can only read it if we've read the header
@@ -9856,6 +9860,10 @@ dwarf2_record_single_block_range (struct dwarf2_cu *cu, struct block *block,
CORE_ADDR low, CORE_ADDR high,
unrelocated_addr unrel_high)
{
+ /* If this is the end of an inline block, then record its end address. */
+ if (block->inlined_p () && block->function () != nullptr)
+ cu->inline_block_ends.insert ({unrel_high, block});
+
cu->get_builder ()->record_block_range (block, low, high);
}
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH 7/7] gdb: fix-up truncated inline function block ranges
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (5 preceding siblings ...)
2025-07-20 10:20 ` [PATCH 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
@ 2025-07-20 10:20 ` Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-08-01 15:41 ` [PATCH " Sam James
8 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-07-20 10:20 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess, Bernd Edlinger
This commit aims to improve GDB's handling of inline functions. There
are two mechanisms which can tell GDB, or the user of GDB, that the
inferior is within an inline function, these are the block range
associated with an inline instance of a function, and also the line
table, which associates addresses with source lines in the program.
Currently, gcc truncates the address range for, at least some, inline
function blocks, such that a given address is considered outside the
inline function. However, the line table maps that same address to a
line within the inline function.
A consequence of this, is that, when using 'next' to move the inferior
forward, GDB will often stop the inferior believing that the inferior
has left an inline function, and indeed, GDB will claim that the
inferior is in the outer, non-inline function, but GDB will then
display a source line from the inline function as the current location.
An example of this problem can be seen with the test
gdb.cp/step-and-next-inline.exp. Using the
step-and-next-inline-no-header binary that is built as part of the
test:
(gdb) file ./gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Reading symbols from ./gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header...
(gdb) break get_alias_set
Breakpoint 1 at 0x401160: file /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc, line 51.
(gdb) run
Starting program: /tmp/build/gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Breakpoint 1, get_alias_set (t=t@entry=0x404038 <xx>)
at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:51
51 if (t != NULL
(gdb) next
52 && TREE_TYPE (t).z != 1
(gdb) next
43 return x; <------------- Problem line.
(gdb) bt
#0 get_alias_set (t=t@entry=0x404038 <xx>) at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:43
#1 0x000000000040105e in main () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:64
(gdb)
I've labelled the issue as 'Problem line'. After the second 'next'
GDB stopped thinking it was in get_alias_set, but printed a line from
the inline function behind the TREE_TYPE macro. The 'Problem line'
should have been line 53, not line 43.
The $pc at which GDB stopped is 0x40116f. If we then use 'objdump
--dwarf=decodedline' to view the line table for the executable, this
is what we see:
File name Line number Starting address View Stmt
...
step-and-next-inline.cc 38 0x401165 x
step-and-next-inline.cc 40 0x401165 1 x
step-and-next-inline.cc 40 0x401165 2
step-and-next-inline.cc 40 0x401167
step-and-next-inline.cc 42 0x40116f x
step-and-next-inline.cc 43 0x40116f 1 x
step-and-next-inline.cc 43 0x40116f 2
step-and-next-inline.cc 52 0x40116f 3
step-and-next-inline.cc 52 0x401172
step-and-next-inline.cc 38 0x401177 x
step-and-next-inline.cc 40 0x401177 1 x
step-and-next-inline.cc 40 0x401177 2
...
NOTE: I use objdump to view the line table, not 'maintenance info
line-table' as GDB drops some line table entries that it sees as
irrelevant. Using objdump give a complete view of the line table.
We can see that address 0x40116f is associated with three line
numbers, 42, and 43 are both within the inline function, and 52 is the
line from which the inline function was called. Notice too that 52 is
a non-statement line.
If we now look at the block structure for the previous $pc
value 0x40116e (i.e. $pc - 1), then we see this:
(gdb) maintenance info blocks 0x40116e
...
[(block *) 0x3c62900] 0x401040..0x4011a6
entry pc: 0x401160
function: get_alias_set(tree*)
symbol count: 1
address ranges:
0x401160..0x4011a6
0x401040..0x401046
[(block *) 0x3c61260] 0x401041..0x40116f
entry pc: 0x401165
inline function: tree_check(tree*, int)
symbol count: 4
address ranges:
0x401165..0x40116f
0x401041..0x401046
(gdb)
Here we see 'tree_check', the inline function that backs the TREE_TYPE
macro, this is the inline function we have just stepped out of. This
makes sense as the end-address for the tree_check block is 0x40116f,
and as the block's end address is not inclusive, that means that
0x40116f is the first address outside the block, which, is the current
$pc value.
And so, we can see what's going on. When the 'next' starts GDB is in
get_alias_set, GDB steps forward, entering tree_check. GDB then uses
the extent of the block to figure out where the inline function ends,
and steps forward to that address (0x40116f). At this point, GDB
looks up the current line in the line table (43), and reports a stop
at this line.
In this commit, the fix I propose is to look for the line table
pattern seen above, a sequence of line table entries, that end with a
non-statement entry for the calling line of an inline function,
located at the exact end address of an inline function block.
When such a pattern is found, then we can extend the inline function's
address range to the next line table address, so long as doing so does
not extend the inline function beyond the extent of the containing,
non-inline, function.
In the above example, the block for the tree_check function would be
extended to end at 0x401172. With this fix in place, and with the
same test binary, GDB now behaves like this:
(gdb) break get_alias_set
Breakpoint 1 at 0x401160: file /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc, line 51.
(gdb) run
Starting program: /tmp/build/gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Breakpoint 1, get_alias_set (t=t@entry=0x404038 <xx>)
at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:51
51 if (t != NULL
(gdb) next
52 && TREE_TYPE (t).z != 1
(gdb) next
53 && TREE_TYPE (t).z != 2
(gdb)
The block for the inline function has been updated, like this:
(gdb) maintenance info blocks 0x40116e
...
[(block *) 0x4965530] 0x401040..0x4011a6
entry pc: 0x401160
function: get_alias_set(tree*)
symbol count: 1
address ranges:
0x401160..0x4011a6
0x401040..0x401046
[(block *) 0x49640b0] 0x401040..0x401172
entry pc: 0x401165
inline function: tree_check(tree*, int)
symbol count: 4
address ranges:
0x401165..0x401172
0x401040..0x401041
0x401041..0x401046
This is my alternative to the patch presented here:
https://inbox.sourceware.org/gdb-patches/AS1PR01MB946510286FBF2497A6F03E83E4922@AS1PR01MB9465.eurprd01.prod.exchangelabs.com/
This original patch series from Bernd Edlinger contains a number of
different fixes, some have already been split out and merged into GDB,
but the core idea for how to improve inline function handling by
extending the inline block range is the same, however, the mechanism
Bernd uses is significantly different.
In the above series, the approach taken is to mark the line table at
the end address of an inline function, and a few addresses beyond
that (see the is-weak flag in the above series). Then, when looking
up the block for a given address, if the address is within this marked
region, then we actually return the previous (inline function) block.
I believe that the above is a fair high-level summary of how the above
patch solves the inline function range problem. Any differences are
down to my misunderstanding the above patch, for which I apologise.
My problem with the above patch is that it breaks what I think should
be an invariant of GDB, that when looking up a block for a given
address, the block returned must contain the address within its
ranges. I feel that, if we start to break this invariant, then we
risk introducing bugs within, e.g. the stepping control code.
In contrast, my approach solves this problem during the DWARF parsing,
where problem cases are identified, and the DWARF "fixed" by extending
the block ranges. After this, no additional changes are needed in
GDB, the address to block mapping can work as normal, and the stepping
logic can continue to work just as it always has.
The test changes for gdb.cp/step-and-next-inline.exp have been taken
from Bernd's original patch series, and I've added a Co-Author tag for
Bernd to reflect this, as well as for the inspiration that I took from
his original series when creating this alternative proposal.
If/when this patch is merged, I plan to follow up with some cleanup to
the test case gdb.cp/step-and-next-inline.exp. I think this test
should really be moved to gdb.opt/, it's really testing optimisation
debug, not C++ features, but also the structure of the test file is a
bit of a mess. I think with some restructuring we could make the test
more readable, and also, maybe, test some additional compiler
flags (e.g. more optimisation levels). I've not done the refactoring
in this patch in order to make it clearer what new tests I've added,
and also, I want to leave the test similar to what's in Bernd's
original series, to make comparison easier.
The gdb.cp/step-and-next-inline.exp test was originally added by me
back in 2019, so the problems with it are of my own making.
For testing I've obviously run the entire test suite, but of
particular interest are these tests:
gdb.cp/step-and-next-inline.exp
gdb.dwarf2/dw2-inline-bt.exp
gdb.opt/empty-inline-cxx.exp
gdb.opt/empty-inline.exp
gdb.opt/inline-bt.exp
I've run these tests with a range of different gcc versions: 9.5.0,
10.5.0, 11.5.0, 12.2.0, 13.3.0, 14.2.0, 15.1.0. These tests all
relate to optimised debug of inline functions, and all passed with all
compiler versions listed here.
Co-Authored-By: Bernd Edlinger <bernd.edlinger@hotmail.de>
---
gdb/dwarf2/read.c | 80 +++
gdb/testsuite/gdb.cp/step-and-next-inline.exp | 38 +-
.../gdb.dwarf2/dw2-extend-inline-block.c | 78 +++
.../gdb.dwarf2/dw2-extend-inline-block.exp | 574 ++++++++++++++++++
4 files changed, 763 insertions(+), 7 deletions(-)
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 992cf008da0..89e3efe5013 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -16237,6 +16237,79 @@ dwarf_finish_line (struct gdbarch *gdbarch, struct subfile *subfile,
dwarf_record_line_1 (gdbarch, subfile, 0, address, LEF_IS_STMT, cu);
}
+/* Look for an inline block that finishes at ORIGINAL_ADDRESS. If a block
+ is found, then search up the block hierarchy looking for a suitable
+ inline block to extend, a suitable block will be called from LINE. If a
+ block is found then update its end address to EXTENDED_ADDRESS. */
+
+static void
+dwarf_find_and_extend_inline_block_range (dwarf2_cu *cu,
+ unrelocated_addr original_address,
+ unrelocated_addr extended_address,
+ unsigned int line)
+{
+ /* Is there an inline block that ends at ORIGINAL_ADDRESS? */
+ auto it = cu->inline_block_ends.find (original_address);
+ if (it == cu->inline_block_ends.end ())
+ return;
+
+ /* Walk back up the block structure until we find the first inline block
+ that occurs after a non-inline block. This is our candidate for
+ extending. */
+ struct block *block = nullptr;
+ for (const struct block *b = it->second;
+ b != nullptr;
+ b = b->superblock ())
+ {
+ if (b->function () != nullptr && b->inlined_p ())
+ {
+ if (b->superblock () != nullptr
+ && b->superblock ()->function () != nullptr
+ && !b->superblock ()->inlined_p ())
+ {
+ block = const_cast<struct block *> (b);
+ break;
+ }
+ }
+ }
+
+ /* If we didn't find a block, of the line table doesn't indicate that the
+ block should be extended, then we're done. Maybe we should try harder
+ to look for the block that matches LINE, but this would require us to
+ possibly extended more blocks, adding more complexity. Currently,
+ this works enough for simple cases, we can possibly improve the logic
+ here later on. */
+ if (block == nullptr || block->function ()->line () != line)
+ return;
+
+ /* Sanity check. We should have an inline block, which should have a
+ valid super block. */
+ gdb_assert (block->inlined_p ());
+ gdb_assert (block->superblock () != nullptr);
+
+ CORE_ADDR extended_end = cu->per_objfile->relocate (extended_address);
+
+ /* The proposed new end of BLOCK is outside of the ranges of BLOCK's
+ superblock. If we tried to extend BLOCK then this would create an
+ invalid block structure; BLOCK would no longer be fully nested within
+ its superblock. Don't do that. */
+ if (extended_end > block->superblock ()->end ())
+ return;
+
+ CORE_ADDR original_end = cu->per_objfile->relocate (original_address);
+
+ /* Now find the part of BLOCK that ends at ORIGINAL_END, and extend it
+ out to EXTENDED_END. */
+ for (blockrange &br : block->ranges ())
+ {
+ if (br.end () == original_end)
+ br.set_end (extended_end);
+ }
+
+ if (block->end () == original_end)
+ block->set_end (extended_end);
+}
+
void
lnp_state_machine::record_line (bool end_sequence)
{
@@ -16255,6 +16328,13 @@ lnp_state_machine::record_line (bool end_sequence)
(end_sequence ? "\t(end sequence)" : ""));
}
+ if (m_address != m_last_address
+ && m_stmt_at_address
+ && m_cu->producer_is_gcc ()
+ && (m_flags & LEF_IS_STMT) == 0)
+ dwarf_find_and_extend_inline_block_range (m_cu, m_last_address,
+ m_address, m_line);
+
file_entry *fe = current_file ();
if (fe == NULL)
diff --git a/gdb/testsuite/gdb.cp/step-and-next-inline.exp b/gdb/testsuite/gdb.cp/step-and-next-inline.exp
index effff2721fa..4ed82deda32 100644
--- a/gdb/testsuite/gdb.cp/step-and-next-inline.exp
+++ b/gdb/testsuite/gdb.cp/step-and-next-inline.exp
@@ -24,13 +24,6 @@ if {[test_compiler_info gcc*] && ![supports_statement_frontiers] } {
proc do_test { use_header } {
global srcfile testfile
- if { $use_header } {
- # This test will not pass due to poor debug information
- # generated by GCC (at least up to 10.x). See
- # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94474
- return
- }
-
set options {c++ debug nowarnings optimize=-O2}
if { [supports_statement_frontiers] } {
lappend options additional_flags=-gstatement-frontiers
@@ -198,6 +191,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 1 pass 2"
gdb_test "step" ".*TREE_TYPE.*" "step 4"
gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
"not in inline 2 pass 2"
@@ -205,6 +200,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 2 pass 2"
gdb_test "step" ".*TREE_TYPE.*" "step 7"
gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
"not in inline 3 pass 2"
@@ -212,6 +209,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 3 pass 2"
gdb_test "step" "return 0.*" "step 10"
gdb_test "bt" \
"\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
@@ -255,6 +254,31 @@ proc do_test { use_header } {
gdb_test "bt" "#0\\s+\[^\r\n\]*tree_check\[^\r\n\]*${hdrfile}:.*" \
"abort from inline 1 pass 3"
}
+
+ clean_restart ${executable}
+
+ if ![runto_main] {
+ return
+ }
+
+ gdb_test "bt" "\\s*\\#0\\s+main.*" "in main pass 4"
+ gdb_test "skip tree_check" ".*" "skip tree_check pass 4"
+ gdb_test "step" ".*" "step into get_alias_set pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "in get_alias_set pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 1 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 1 pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 2 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 2 pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 3 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 3 pass 4"
+ gdb_test "step" "return 0.*" "step 4 pass 4"
+ gdb_test "bt" \
+ "\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
+ "not in inline 4 pass 4"
}
}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
new file mode 100644
index 00000000000..d6becf5d66b
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
@@ -0,0 +1,78 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 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;
+
+/* The follow code exists only to be referenced from the generated line
+ table. */
+#if 0
+static inline void
+foo (void)
+{
+ /* foo:1 */
+ /* foo:2 */
+ /* foo:3 */
+}
+
+int
+main (void)
+{ /* main decl line */
+ /* main:1 */
+ /* main:2 */
+ /* main:3 */ foo (); /* foo call line */
+ /* main:4 */
+ /* main:5 */
+ /* main:6 */
+}
+#endif
+
+
+int
+main (void)
+{
+ asm ("main_label: .globl main_label");
+ ++global_var;
+
+ asm ("main_0: .globl main_0");
+ ++global_var;
+
+ asm ("main_1: .globl main_1");
+ ++global_var;
+
+ 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_8");
+ ++global_var;
+
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
new file mode 100644
index 00000000000..963410ce057
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
@@ -0,0 +1,574 @@
+# Copyright 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/>.
+
+# When compiling optimised code, GCC will sometimes truncate the address
+# range of an inline function, usually by a single instruction.
+#
+# It is possible to detect when this has happened by looking at the line
+# table, GCC will create two non-statement line table entries associated
+# with the call-line of the inline function, but the end address of the
+# inline function will be set to be the address of the first of these line
+# table entries.
+#
+# The problem here is that block end addresses are not inclusive, which
+# means the block ends before either of these line table entries.
+#
+# What we find is that we get a better debug experience if we extend the
+# inline function to actually end at the second line table entry, that is
+# the first line table entry becomes part of the inline function, while the
+# second entry remains outside the inline function.
+#
+# This test tries to create this situation using the DWARF assembler, and
+# then checks that GDB correctly extends the inline function to include the
+# first line table entry.
+
+load_lib dwarf.exp
+
+require dwarf2_support
+
+standard_testfile .c
+
+# Lines numbers we reference in the generated DWARF.
+set main_decl_line [gdb_get_line_number "main decl line"]
+set main_line_1 [gdb_get_line_number "main:1"]
+set main_line_4 [gdb_get_line_number "main:4"]
+set foo_call_line [gdb_get_line_number "foo call line"]
+set foo_line_1 [gdb_get_line_number "foo:1"]
+
+get_func_info main
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with a contiguous address range that needs extending.
+
+proc build_dwarf_for_contiguous_block { asm_file } {
+ Dwarf::assemble $asm_file {
+ 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 main_3 addr}
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+ }
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_contiguous_block'.
+
+proc check_contiguous_block {} {
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_4" "*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 expected content"
+}
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with two ranges, and it is the first range that needs extending.
+
+proc build_dwarf_for_first_block_range { asm_file dwarf_version } {
+ Dwarf::assemble $asm_file {
+ upvar dwarf_version dwarf_version
+ 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}
+ {ranges $ranges_label DW_FORM_sec_offset}
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+
+ if { $dwarf_version == 5 } {
+ rnglists {} {
+ table {} {
+ ranges_label: list_ {
+ start_end main_1 main_3
+ start_end main_7 main_8
+ }
+ }
+ }
+ } else {
+ ranges { } {
+ ranges_label: sequence {
+ range main_1 main_3
+ range main_7 main_8
+ }
+ }
+ }
+ }
+}
+
+# Wrapper around 'build_dwarf_for_first_block_range', creates DWARF 4 range
+# information.
+
+proc build_dwarf_for_first_block_range_4 { asm_file } {
+ build_dwarf_for_first_block_range $asm_file 4
+}
+
+# Wrapper around 'build_dwarf_for_first_block_range', creates DWARF 5 range
+# information.
+
+proc build_dwarf_for_first_block_range_5 { asm_file } {
+ build_dwarf_for_first_block_range $asm_file 5
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_first_block_range'.
+
+proc check_for_block_ranges_1 {} {
+
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_8" "*UNKNOWN*" \
+ "get address of foo end"]
+
+ set main_4 [get_hexadecimal_valueof "&main_4" "*UNKNOWN*" \
+ "get address of main_4 label"]
+ set main_7 [get_hexadecimal_valueof "&main_7" "*UNKNOWN*" \
+ "get address of main_7 label"]
+
+ gdb_test "maintenance info blocks" \
+ [multi_line \
+ "\\\[\\(block \\*\\) $::hex\\\] $foo_start\\.\\.$foo_end" \
+ " entry pc: $foo_start" \
+ " inline function: foo" \
+ " symbol count: $::decimal" \
+ " address ranges:" \
+ " $foo_start\\.\\.$main_4" \
+ " $main_7\\.\\.$foo_end"] \
+ "block for foo has expected content"
+}
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with two ranges, and it is the second range that needs extending.
+
+proc build_dwarf_for_last_block_range { asm_file dwarf_version } {
+ Dwarf::assemble $asm_file {
+ upvar dwarf_version dwarf_version
+ 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}
+ {ranges $ranges_label DW_FORM_sec_offset}
+ {entry_pc main_1 addr}
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+
+ if { $dwarf_version == 5 } {
+ rnglists {} {
+ table {} {
+ ranges_label: list_ {
+ start_end main_7 main_8
+ start_end main_1 main_3
+ }
+ }
+ }
+ } else {
+ ranges { } {
+ ranges_label: sequence {
+ range main_7 main_8
+ range main_1 main_3
+ }
+ }
+ }
+ }
+}
+
+# Wrapper around 'build_dwarf_for_last_block_range', creates DWARF 4 range
+# information.
+
+proc build_dwarf_for_last_block_range_4 { asm_file } {
+ build_dwarf_for_last_block_range $asm_file 4
+}
+
+# Wrapper around 'build_dwarf_for_last_block_range', creates DWARF 5 range
+# information.
+
+proc build_dwarf_for_last_block_range_5 { asm_file } {
+ build_dwarf_for_last_block_range $asm_file 5
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_last_block_range'.
+
+proc check_for_block_ranges_2 {} {
+
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_8" "*UNKNOWN*" \
+ "get address of foo end"]
+
+ set main_4 [get_hexadecimal_valueof "&main_4" "*UNKNOWN*" \
+ "get address of main_4 label"]
+ set main_7 [get_hexadecimal_valueof "&main_7" "*UNKNOWN*" \
+ "get address of main_7 label"]
+
+ gdb_test "maintenance info blocks" \
+ [multi_line \
+ "\\\[\\(block \\*\\) $::hex\\\] $foo_start\\.\\.$foo_end" \
+ " entry pc: $foo_start" \
+ " inline function: foo" \
+ " symbol count: $::decimal" \
+ " address ranges:" \
+ " $main_7\\.\\.$foo_end" \
+ " $foo_start\\.\\.$main_4"] \
+ "block for foo has expected content"
+}
+
+# Buidl ASM_FILE, along with the global SRCFILE into an executable called
+# TESTFILE. Place a breakpoint in 'foo', run to the breakpoint, and use
+# BLOCK_CHECK_FUNC to ensure the block for 'foo' is correct.
+#
+# Then step through 'foo' and back into 'main'.
+
+proc run_test { asm_file testfile block_check_func } {
+ if {[prepare_for_testing "failed to prepare" $testfile \
+ [list $::srcfile $asm_file] {nodebug}]} {
+ return
+ }
+
+ if {![runto_main]} {
+ return
+ }
+
+ gdb_breakpoint foo
+ gdb_test "continue" \
+ [multi_line \
+ "Breakpoint $::decimal, foo \\(\\) \[^\r\n\]+:$::foo_line_1" \
+ "$::foo_line_1\\s+/\\* foo:1 \\*/"] \
+ "continue to b/p in foo"
+
+ # Check that the block for `foo` has been extended.
+ $block_check_func
+
+ 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"
+
+ gdb_test "step" \
+ "^[expr $::foo_line_1 + 1]\\s+/\\* foo:2 \\*/" \
+ "step to second line of foo"
+
+ gdb_test "step" \
+ "^[expr $::foo_line_1 + 2]\\s+/\\* foo:3 \\*/" \
+ "step to third line of foo"
+
+ gdb_test "step" \
+ [multi_line \
+ "^main \\(\\) at \[^\r\n\]+:$::main_line_4" \
+ "$::main_line_4\\s+/\\* main:4 \\*/"] \
+ "set back to main"
+
+ gdb_test "step" \
+ "^[expr $::main_line_4 + 1]\\s+/\\* main:5 \\*/" \
+ "step again in main"
+}
+
+# Test specifications, items are:
+# 1. Prefix string used to describe the test.
+# 2. Proc to call that builds the DWARF.
+# 3. Proc to call that runs 'maint info blocks' when stopped at the entry
+# $pc for 'foo' (the inline function), and checks that the block details
+# for 'foo' are correct.
+set test_list \
+ [list \
+ [list "block with ranges, extend first range, dwarf 4" \
+ build_dwarf_for_first_block_range_4 \
+ check_for_block_ranges_1] \
+ [list "block with ranges, extend first range, dwarf 5" \
+ build_dwarf_for_first_block_range_5 \
+ check_for_block_ranges_1] \
+ [list "block with ranges, extend last range, dwarf 4" \
+ build_dwarf_for_last_block_range_4 \
+ check_for_block_ranges_2] \
+ [list "block with ranges, extend last range, dwarf 5" \
+ build_dwarf_for_last_block_range_4 \
+ check_for_block_ranges_2] \
+ [list "contiguous block" \
+ build_dwarf_for_contiguous_block \
+ check_contiguous_block] \
+ ]
+
+# Run all the tests.
+set suffix 0
+foreach test_spec $test_list {
+ incr suffix
+
+ set prefix [lindex $test_spec 0]
+ set build_dwarf_func [lindex $test_spec 1]
+ set check_block_func [lindex $test_spec 2]
+
+ with_test_prefix $prefix {
+ set asm_file [standard_output_file ${testfile}-${suffix}.S]
+ $build_dwarf_func $asm_file
+ run_test $asm_file ${testfile}-${suffix} $check_block_func
+ }
+}
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (6 preceding siblings ...)
2025-07-20 10:20 ` [PATCH 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
@ 2025-08-01 8:58 ` Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
` (7 more replies)
2025-08-01 15:41 ` [PATCH " Sam James
8 siblings, 8 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-08-01 8:58 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
In v2:
- Fixes to the test added in patch (2) to address failures reported
from Linaro CI. The issues all relate to compiling the DWARF
assembler test with `-pie` which is on by default for the Linaro
CI machines. The issues were all DWARF generation issues.
- Rebased series to more recent upstream/master.
---
This series presents some improvements to debugging inline functions,
especially in optimised code.
The first two patches of this series have been posted previously here:
https://inbox.sourceware.org/gdb-patches/cover.1736865029.git.aburgess@redhat.com
This series replaces that earlier work, though the patches are
basically unchanged.
This entire series is an alternative solution to the patches that were
posted here:
https://inbox.sourceware.org/gdb-patches/AS1PR01MB946510286FBF2497A6F03E83E4922@AS1PR01MB9465.eurprd01.prod.exchangelabs.com/
My reasons for writing an alternative patch set can be found in the
commit messages of patches (2) and (7), I'm not going to repeat them
here. However, Bernd, the original patch author, did amazing work
identifying the problems GDB was having, and these patches likely
wouldn't exist without the original patch series, so a big thanks
there.
For reviewing, this series can be considered in 3 parts:
+ Patches (1) and (2) are independent from the rest of the series,
though related in theme. Both these patches could be approved and
merged ahead of remaining work, and do offer an improvement to GDB
on their own.
+ Patches (3), (4), (5), and (6) are refactoring work to support the
last patch. These can be reviewed, but shouldn't be merged until
the last patch is approved.
+ Patch (7) covers the final parts of this work, which deal with
non-empty inline functions.
All thoughts and feedback are welcome.
Thanks,
Andrew
---
Andrew Burgess (7):
gdb: improve line number lookup around inline functions
gdb: handle empty ranges for inline subroutines
gdb: split dwarf line table parsing in two
gdb: move block range recording into its own function
gdb: create address map after parsing all DIE
gdb: record block end addresses while parsing DIEs
gdb: fix-up truncated inline function block ranges
gdb/buildsym.c | 42 +-
gdb/buildsym.h | 13 +-
gdb/dwarf2/cu.h | 7 +
gdb/dwarf2/read.c | 288 +++++++--
gdb/symtab.c | 25 +-
gdb/testsuite/gdb.cp/step-and-next-inline.exp | 171 ++++--
.../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-extend-inline-block.c | 78 +++
.../gdb.dwarf2/dw2-extend-inline-block.exp | 574 ++++++++++++++++++
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c | 79 +++
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp | 227 +++++++
.../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 ++++
gdb/testsuite/gdb.opt/inline-bt.c | 28 +
gdb/testsuite/gdb.opt/inline-bt.exp | 119 ++--
21 files changed, 2344 insertions(+), 195 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.dwarf2/dw2-extend-inline-block.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.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
base-commit: c7c272f06d2d456d1d99762325fd0dd7db31d540
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv2 1/7] gdb: improve line number lookup around inline functions
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
@ 2025-08-01 8:58 ` Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
` (6 subsequent siblings)
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-08-01 8:58 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
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 302f4ebf274..754147cac5f 100644
--- a/gdb/symtab.c
+++ b/gdb/symtab.c
@@ -3304,14 +3304,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..bc77d39d0c9
--- /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 {} {
+ 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/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
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv2 2/7] gdb: handle empty ranges for inline subroutines
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
@ 2025-08-01 8:58 ` Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
` (5 subsequent siblings)
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-08-01 8:58 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess, Bernd Edlinger
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
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv2 3/7] gdb: split dwarf line table parsing in two
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
@ 2025-08-01 8:58 ` Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 4/7] gdb: move block range recording into its own function Andrew Burgess
` (4 subsequent siblings)
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-08-01 8:58 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
A later commit in this series, that improves GDB's ability to debug
optimised code, wants to use the line table information in order to
"fix" inline blocks with a truncated address range. For the reasoning
behind wanting to do that, please read ahead in the series.
Assuming that we accept for now the need to use the line table
information to adjust the block ranges, then why is this commit
needed?
GDB splits the line table data info different symtabs, adding end of
sequence markers as we move between symtabs. This seems to work fine
for GDB, but causes a problem for me in this case.
What I will want to do is this: scan the line table and spot line
table entries that corresponds to the end addresses of an inline
block's address range. If the block meets certain requirements, then
the end address of the block is adjusted to be that of the next line
table entry.
The way that GDB currently splits the line table entries between
symtabs makes this harder. I will have the set of blocks end
addresses which I know might be fixable, but to find the line table
entry corresponding to that address requires searching through all the
symtabs. Having found the entry for the end address, I then need to
find the next line table entry. For some blocks this is easy, it's
the next entry in the same symtab. But for other blocks the next
entry might be in a different symtab, which requires yet another full
search.
I did try implementing this approach, but the number of full symtab
searches is significant, and it had a significant impact on GDB's
debug parsing performance. The impact was such that an operation that
currently takes ~7seconds would take ~3minutes or more. Now I could
possibly improve that 3 minutes figure by optimising the code some,
but I think that would add unnecessary complexity.
By deferring building the line table until after we have parsed the
DIEs it becomes simple to spot when a line table entry corresponds to
a block end address, and finding the next entry is always trivial, as,
at this point, the next entry is just the next entry which we will
process. With this approach I see no noticable impact on DWARF
parsing performance.
This patch is just the refactoring. There's no finding block end
addresses and "fixing" being done here. This just sets things up for
the later commits.
There should be no user visible changes after this commit.
---
gdb/dwarf2/read.c | 111 +++++++++++++++++++++++++---------------------
1 file changed, 61 insertions(+), 50 deletions(-)
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 6f3bd97e5bc..a68b1434e53 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -787,9 +787,7 @@ static line_header_up dwarf_decode_line_header (sect_offset sect_off,
struct dwarf2_cu *cu,
const char *comp_dir);
-static void dwarf_decode_lines (struct line_header *,
- struct dwarf2_cu *,
- unrelocated_addr, int decode_mapping);
+static void dwarf_decode_lines (struct dwarf2_cu *cu, unrelocated_addr lowpc);
static void dwarf2_start_subfile (dwarf2_cu *cu, const file_entry &fe,
const line_header &lh);
@@ -5866,29 +5864,55 @@ find_file_and_directory (struct die_info *die, struct dwarf2_cu *cu)
return *cu->per_cu->fnd;
}
-/* Handle DW_AT_stmt_list for a compilation unit.
- DIE is the DW_TAG_compile_unit die for CU.
- COMP_DIR is the compilation directory. LOWPC is passed to
- dwarf_decode_lines. See dwarf_decode_lines comments about it. */
+/* Ensure that every file_entry within the line_table of CU has a symtab
+ allocated for it. */
static void
-handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
- const file_and_directory &fnd, unrelocated_addr lowpc,
- bool have_code) /* ARI: editCase function */
+create_symtabs_from_cu_line_table (struct dwarf2_cu *cu)
{
- dwarf2_per_objfile *per_objfile = cu->per_objfile;
- struct attribute *attr;
- hashval_t line_header_local_hash;
- void **slot;
- int decode_mapping;
+ /* Make sure a symtab is created for every file, even files
+ which contain only variables (i.e. no code with associated
+ line numbers). */
+ buildsym_compunit *builder = cu->get_builder ();
+ struct compunit_symtab *cust = builder->get_compunit_symtab ();
- gdb_assert (! cu->per_cu->is_debug_types);
+ struct line_header *lh = cu->line_header;
+ gdb_assert (lh != nullptr);
- attr = dwarf2_attr (die, DW_AT_stmt_list, cu);
+ for (auto &fe : lh->file_names ())
+ {
+ dwarf2_start_subfile (cu, fe, *lh);
+ subfile *sf = builder->get_current_subfile ();
+
+ if (sf->symtab == nullptr)
+ sf->symtab = allocate_symtab (cust, sf->name.c_str (),
+ sf->name_for_id.c_str ());
+
+ fe.symtab = sf->symtab;
+ }
+}
+
+
+/* Handle DW_AT_stmt_list for a compilation unit. DIE is the
+ DW_TAG_compile_unit die for CU. FND is used to access the compilation
+ directory. This function will decode the line table header and create
+ symtab objects for the files referenced in the line table. The line
+ table itself though is not processed by this function. If there is no
+ line table, or there's a problem decoding the header, then CU will not
+ be updated. */
+
+static void
+decode_line_header_for_cu (struct die_info *die, struct dwarf2_cu *cu,
+ const file_and_directory &fnd)
+{
+ gdb_assert (!cu->per_cu->is_debug_types);
+
+ struct attribute *attr = dwarf2_attr (die, DW_AT_stmt_list, cu);
if (attr == NULL || !attr->form_is_unsigned ())
return;
sect_offset line_offset = (sect_offset) attr->as_unsigned ();
+ dwarf2_per_objfile *per_objfile = cu->per_objfile;
/* The line header hash table is only created if needed (it exists to
prevent redundant reading of the line table for partial_units).
@@ -5906,8 +5930,9 @@ handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
xcalloc, xfree));
}
+ void **slot;
line_header line_header_local (line_offset, cu->per_cu->is_dwz);
- line_header_local_hash = line_header_hash (&line_header_local);
+ hashval_t line_header_local_hash = line_header_hash (&line_header_local);
if (per_objfile->line_header_hash != NULL)
{
slot = htab_find_slot_with_hash (per_objfile->line_header_hash.get (),
@@ -5960,12 +5985,8 @@ handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
then this is what we want as well. */
gdb_assert (die->tag != DW_TAG_partial_unit);
}
- decode_mapping = (die->tag != DW_TAG_partial_unit);
- /* The have_code check is here because, if LOWPC and HIGHPC are both 0x0,
- then there won't be any interesting code in the CU, but a check later on
- (in lnp_state_machine::check_line_address) will fail to properly exclude
- an entry that was removed via --gc-sections. */
- dwarf_decode_lines (cu->line_header, cu, lowpc, decode_mapping && have_code);
+
+ create_symtabs_from_cu_line_table (cu);
}
/* Process DW_TAG_compile_unit or DW_TAG_partial_unit. */
@@ -6017,10 +6038,12 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
scoped_restore restore_sym_cu
= make_scoped_restore (&per_objfile->sym_cu, cu);
- /* Decode line number information if present. We do this before
- processing child DIEs, so that the line header table is available
- for DW_AT_decl_file. */
- handle_DW_AT_stmt_list (die, cu, fnd, unrel_low, unrel_low != unrel_high);
+ /* Decode the line header if present. We do this before processing child
+ DIEs, so that information is available for DW_AT_decl_file. We defer
+ parsing the actual line table until after processing the child DIEs,
+ this allows us to fix up some of the inline function blocks as the
+ line table is read. */
+ decode_line_header_for_cu (die, cu, fnd);
/* Process all dies in compilation unit. */
for (die_info *child_die : die->children ())
@@ -6028,6 +6051,12 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
per_objfile->sym_cu = nullptr;
+ /* If we actually have code, then read the line table now. */
+ if (unrel_low != unrel_high
+ && die->tag != DW_TAG_partial_unit
+ && cu->line_header != nullptr)
+ dwarf_decode_lines (cu, unrel_low);
+
/* Decode macro information, if present. Dwarf 2 macro information
refers to information in the line number info statement program
header, so we can only read it if we've read the header
@@ -16565,29 +16594,11 @@ dwarf_decode_lines_1 (struct line_header *lh, struct dwarf2_cu *cu,
table is read in. */
static void
-dwarf_decode_lines (struct line_header *lh, struct dwarf2_cu *cu,
- unrelocated_addr lowpc, int decode_mapping)
+dwarf_decode_lines (struct dwarf2_cu *cu, unrelocated_addr lowpc)
{
- if (decode_mapping)
- dwarf_decode_lines_1 (lh, cu, lowpc);
+ gdb_assert (cu->line_header != nullptr);
- /* Make sure a symtab is created for every file, even files
- which contain only variables (i.e. no code with associated
- line numbers). */
- buildsym_compunit *builder = cu->get_builder ();
- struct compunit_symtab *cust = builder->get_compunit_symtab ();
-
- for (auto &fe : lh->file_names ())
- {
- dwarf2_start_subfile (cu, fe, *lh);
- subfile *sf = builder->get_current_subfile ();
-
- if (sf->symtab == nullptr)
- sf->symtab = allocate_symtab (cust, sf->name.c_str (),
- sf->name_for_id.c_str ());
-
- fe.symtab = sf->symtab;
- }
+ dwarf_decode_lines_1 (cu->line_header, cu, lowpc);
}
/* Start a subfile for DWARF. FILENAME is the name of the file and
@@ -16852,7 +16863,7 @@ new_symbol (struct die_info *die, struct type *type, struct dwarf2_cu *cu,
if (file_cu->line_header == nullptr)
{
file_and_directory fnd (nullptr, nullptr);
- handle_DW_AT_stmt_list (file_cu->dies, file_cu, fnd, {}, false);
+ decode_line_header_for_cu (file_cu->dies, file_cu, fnd);
}
if (file_cu->line_header != nullptr)
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv2 4/7] gdb: move block range recording into its own function
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (2 preceding siblings ...)
2025-08-01 8:58 ` [PATCHv2 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
@ 2025-08-01 8:58 ` Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 5/7] gdb: create address map after parsing all DIE Andrew Burgess
` (3 subsequent siblings)
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-08-01 8:58 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Like the previous commit, this is a refactor that makes a later commit
in this series easier. The later commit improves GDB's ability to
debug optimised code. To do this I propose to "fix" the address
ranges of some inline blocks.
In order to know which blocks to fix, I need to record the end address
of inline blocks.
And so, I'd like to create a single common function where block ranges
are recorded, in a later commit I can then hook into this function to
record the block's end address(es). This commit sets up this single
common function.
The new function I'm adding dwarf2_record_single_block_range, takes a
currently unused argument unrel_high. This argument will be needed in
the later commit. I've added it now as this will allow the later
commit to be smaller and more focused. I only plan to push this
commit at the same time as the later, so I don't think adding
the (currently) unused argument is too much of a problem.
There should be no user visible change after this commit.
---
gdb/dwarf2/read.c | 21 ++++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index a68b1434e53..e553a47e970 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -9845,6 +9845,20 @@ dwarf2_record_block_entry_pc (struct die_info *die, struct block *block,
}
}
+/* Helper function for dwarf2_record_block_ranges. This function records
+ the address range for a single BLOCK. LOW and HIGH are the block's
+ range, these addresses are inclusive, so LOW is the first address in
+ the range, and HIGH is the last address inside the range. UNREL_HIGH
+ is the unrelocated version of HIGH. */
+
+static void
+dwarf2_record_single_block_range (struct dwarf2_cu *cu, struct block *block,
+ CORE_ADDR low, CORE_ADDR high,
+ unrelocated_addr unrel_high)
+{
+ cu->get_builder ()->record_block_range (block, low, high);
+}
+
/* Record the address ranges for BLOCK, offset by BASEADDR, as given
in DIE. Also set the entry PC for BLOCK. */
@@ -9913,7 +9927,8 @@ dwarf2_record_block_ranges (struct die_info *die, struct block *block,
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);
+ dwarf2_record_single_block_range (cu, block, low, high,
+ unrel_high);
}
attr = dwarf2_attr (die, DW_AT_ranges, cu);
@@ -9943,8 +9958,8 @@ dwarf2_record_block_ranges (struct die_info *die, struct block *block,
{
CORE_ADDR abs_start = per_objfile->relocate (start);
CORE_ADDR abs_end = per_objfile->relocate (end);
- cu->get_builder ()->record_block_range (block, abs_start,
- abs_end - 1);
+ dwarf2_record_single_block_range (cu, block, abs_start,
+ abs_end - 1, end);
blockvec.emplace_back (abs_start, abs_end);
});
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv2 5/7] gdb: create address map after parsing all DIE
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (3 preceding siblings ...)
2025-08-01 8:58 ` [PATCHv2 4/7] gdb: move block range recording into its own function Andrew Burgess
@ 2025-08-01 8:58 ` Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
` (2 subsequent siblings)
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-08-01 8:58 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Continuing the work done in the last two commits, this commit defers
building the addrmap for a block vector until after all the DIE have
been read, and the line table processed.
The benefit of this is that any changes to a block's ranges done
during line table processing (see the next commit) will be reflected
in the blockvector's addrmap.
The alternative to this is to build the addrmap as we initially see
each block, but then adjust the addrmap if we later decide to modify a
block. I think defering the addrmap creation is cleaner, and is less
work overall.
The addrmap requires that we add the most inner blocks first. I
achieve this by walking the blockvector backward, as we always add
parent blocks before their more inner child blocks.
There should be no user visible changes after this commit.
---
gdb/buildsym.c | 42 +++++++++++++++++++++++++++++++++++++-----
gdb/buildsym.h | 13 ++++---------
2 files changed, 41 insertions(+), 14 deletions(-)
diff --git a/gdb/buildsym.c b/gdb/buildsym.c
index 6dc079f29b1..ef50a3d8c18 100644
--- a/gdb/buildsym.c
+++ b/gdb/buildsym.c
@@ -412,8 +412,6 @@ buildsym_compunit::record_block_range (struct block *block,
if (start != block->start ()
|| end_inclusive + 1 != block->end ())
m_pending_addrmap_interesting = true;
-
- m_pending_addrmap.set_empty (start, end_inclusive, block);
}
struct blockvector *
@@ -449,9 +447,43 @@ buildsym_compunit::make_blockvector ()
/* If we needed an address map for this symtab, record it in the
blockvector. */
if (m_pending_addrmap_interesting)
- blockvector->set_map
- (new (&m_objfile->objfile_obstack) addrmap_fixed
- (&m_objfile->objfile_obstack, &m_pending_addrmap));
+ {
+ struct addrmap_mutable pending_addrmap;
+ int num_blocks = blockvector->num_blocks ();
+
+ /* If M_PENDING_ADDRMAP_INTERESTING is true then we must have seen
+ an interesting block. If we see one block, then we should at a
+ minimum have a global block, and a static block. */
+ gdb_assert (num_blocks > 1);
+
+ /* Assert our understanding of how the blocks are laid out. */
+ gdb_assert (blockvector->block (0)->is_global_block ());
+ gdb_assert (blockvector->block (1)->is_static_block ());
+
+ /* The 'J > 1' here is so that we don't place the global block into
+ the map. For CU with gaps, the static block will reflect the
+ gaps, while the global block will just reflect the full extent of
+ the range. */
+ for (int j = num_blocks; j > 1; )
+ {
+ --j;
+ struct block *b = blockvector->block (j);
+
+ gdb_assert (!b->is_global_block ());
+
+ if (b->is_contiguous ())
+ pending_addrmap.set_empty (b->start (), (b->end () - 1), b);
+ else
+ {
+ for (const auto &br : b->ranges ())
+ pending_addrmap.set_empty (br.start (), (br.end () - 1), b);
+ }
+ }
+
+ blockvector->set_map
+ (new (&m_objfile->objfile_obstack) addrmap_fixed
+ (&m_objfile->objfile_obstack, &pending_addrmap));
+ }
else
blockvector->set_map (nullptr);
diff --git a/gdb/buildsym.h b/gdb/buildsym.h
index 8f38131ec55..4fd9b61fb5d 100644
--- a/gdb/buildsym.h
+++ b/gdb/buildsym.h
@@ -418,15 +418,10 @@ struct buildsym_compunit
struct subfile *m_current_subfile = nullptr;
- /* The mutable address map for the compilation unit whose symbols
- we're currently reading. The symtabs' shared blockvector will
- point to a fixed copy of this. */
- struct addrmap_mutable m_pending_addrmap;
-
- /* True if we recorded any ranges in the addrmap that are different
- from those in the blockvector already. We set this to false when
- we start processing a symfile, and if it's still false at the
- end, then we just toss the addrmap. */
+ /* If there are gaps in the address range of any block associated with
+ this buildsym_compunit, then we need to create an address map, this
+ flag is set true to indicate the addrmap must be created. If this
+ remains false, then no addrmap will be created. */
bool m_pending_addrmap_interesting = false;
/* An obstack used for allocating pending blocks. */
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv2 6/7] gdb: record block end addresses while parsing DIEs
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (4 preceding siblings ...)
2025-08-01 8:58 ` [PATCHv2 5/7] gdb: create address map after parsing all DIE Andrew Burgess
@ 2025-08-01 8:58 ` Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-08-01 8:58 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Continuing to work towards the goal of improving GDB's ability to
debug optimised code, this commit stores a map from the end address
of a block (or a block's sub-range) to the block pointer. This
information is collected while parsing the DIEs.
This new map is required after the last commit deferred building the
address map. The optimised code fix ups require that we can map from
an address back to a block, something the address map is perfect for.
However, the optimised code fix ups will then adjust the block ranges,
which means the address map is out of date.
So, I saw a couple of choices, I could build the address map while
parsing the DIES (as if the previous commit had not occurred), use the
address map to lookup inline blocks, and then either update, or
rebuild the full address map. Or, I could defer building the address
map, and then create this "partial" address map that only holds the
interesting, inline, blocks. I felt that maintaining this
alternative, partial address map was probably cheaper, take a look at
how new entries are added to the addrmap class compared to this
gdb::unordered_map.
Currently, nothing is done with this information, the information is
recorded as the block ranges are recorded, and then discarded after
the line table has been built. But in the next commit, this will be
used to help adjust the ranges of some inline blocks, and this will
improve GDB's ability to debug optimised code.
There should be no user visible changes after this commit.
---
gdb/dwarf2/cu.h | 7 +++++++
gdb/dwarf2/read.c | 8 ++++++++
2 files changed, 15 insertions(+)
diff --git a/gdb/dwarf2/cu.h b/gdb/dwarf2/cu.h
index 69f396c774a..c1c0d9fcaa2 100644
--- a/gdb/dwarf2/cu.h
+++ b/gdb/dwarf2/cu.h
@@ -269,6 +269,13 @@ struct dwarf2_cu
return m_producer;
}
+ /* The end addresses for some inline blocks. For blocks with multiple
+ sub-ranges, this is the end address of every sub-range within the
+ block. These are the inclusive end addresses, that is, these are the
+ last addresses inside the block's ranges. Only the first block that
+ ends at any given address will be recorded. */
+ gdb::unordered_map<unrelocated_addr, struct block *> inline_block_ends;
+
private:
const char *m_producer = nullptr;
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index e553a47e970..cb3dace9fea 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -6057,6 +6057,10 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
&& cu->line_header != nullptr)
dwarf_decode_lines (cu, unrel_low);
+ /* We no longer need to track the inline block end addresses. Release
+ memory associated with this. */
+ cu->inline_block_ends.clear ();
+
/* Decode macro information, if present. Dwarf 2 macro information
refers to information in the line number info statement program
header, so we can only read it if we've read the header
@@ -9856,6 +9860,10 @@ dwarf2_record_single_block_range (struct dwarf2_cu *cu, struct block *block,
CORE_ADDR low, CORE_ADDR high,
unrelocated_addr unrel_high)
{
+ /* If this is the end of an inline block, then record its end address. */
+ if (block->inlined_p () && block->function () != nullptr)
+ cu->inline_block_ends.insert ({unrel_high, block});
+
cu->get_builder ()->record_block_range (block, low, high);
}
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv2 7/7] gdb: fix-up truncated inline function block ranges
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (5 preceding siblings ...)
2025-08-01 8:58 ` [PATCHv2 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
@ 2025-08-01 8:58 ` Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-08-01 8:58 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess, Bernd Edlinger
This commit aims to improve GDB's handling of inline functions. There
are two mechanisms which can tell GDB, or the user of GDB, that the
inferior is within an inline function, these are the block range
associated with an inline instance of a function, and also the line
table, which associates addresses with source lines in the program.
Currently, gcc truncates the address range for, at least some, inline
function blocks, such that a given address is considered outside the
inline function. However, the line table maps that same address to a
line within the inline function.
A consequence of this, is that, when using 'next' to move the inferior
forward, GDB will often stop the inferior believing that the inferior
has left an inline function, and indeed, GDB will claim that the
inferior is in the outer, non-inline function, but GDB will then
display a source line from the inline function as the current location.
An example of this problem can be seen with the test
gdb.cp/step-and-next-inline.exp. Using the
step-and-next-inline-no-header binary that is built as part of the
test:
(gdb) file ./gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Reading symbols from ./gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header...
(gdb) break get_alias_set
Breakpoint 1 at 0x401160: file /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc, line 51.
(gdb) run
Starting program: /tmp/build/gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Breakpoint 1, get_alias_set (t=t@entry=0x404038 <xx>)
at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:51
51 if (t != NULL
(gdb) next
52 && TREE_TYPE (t).z != 1
(gdb) next
43 return x; <------------- Problem line.
(gdb) bt
#0 get_alias_set (t=t@entry=0x404038 <xx>) at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:43
#1 0x000000000040105e in main () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:64
(gdb)
I've labelled the issue as 'Problem line'. After the second 'next'
GDB stopped thinking it was in get_alias_set, but printed a line from
the inline function behind the TREE_TYPE macro. The 'Problem line'
should have been line 53, not line 43.
The $pc at which GDB stopped is 0x40116f. If we then use 'objdump
--dwarf=decodedline' to view the line table for the executable, this
is what we see:
File name Line number Starting address View Stmt
...
step-and-next-inline.cc 38 0x401165 x
step-and-next-inline.cc 40 0x401165 1 x
step-and-next-inline.cc 40 0x401165 2
step-and-next-inline.cc 40 0x401167
step-and-next-inline.cc 42 0x40116f x
step-and-next-inline.cc 43 0x40116f 1 x
step-and-next-inline.cc 43 0x40116f 2
step-and-next-inline.cc 52 0x40116f 3
step-and-next-inline.cc 52 0x401172
step-and-next-inline.cc 38 0x401177 x
step-and-next-inline.cc 40 0x401177 1 x
step-and-next-inline.cc 40 0x401177 2
...
NOTE: I use objdump to view the line table, not 'maintenance info
line-table' as GDB drops some line table entries that it sees as
irrelevant. Using objdump give a complete view of the line table.
We can see that address 0x40116f is associated with three line
numbers, 42, and 43 are both within the inline function, and 52 is the
line from which the inline function was called. Notice too that 52 is
a non-statement line.
If we now look at the block structure for the previous $pc
value 0x40116e (i.e. $pc - 1), then we see this:
(gdb) maintenance info blocks 0x40116e
...
[(block *) 0x3c62900] 0x401040..0x4011a6
entry pc: 0x401160
function: get_alias_set(tree*)
symbol count: 1
address ranges:
0x401160..0x4011a6
0x401040..0x401046
[(block *) 0x3c61260] 0x401041..0x40116f
entry pc: 0x401165
inline function: tree_check(tree*, int)
symbol count: 4
address ranges:
0x401165..0x40116f
0x401041..0x401046
(gdb)
Here we see 'tree_check', the inline function that backs the TREE_TYPE
macro, this is the inline function we have just stepped out of. This
makes sense as the end-address for the tree_check block is 0x40116f,
and as the block's end address is not inclusive, that means that
0x40116f is the first address outside the block, which, is the current
$pc value.
And so, we can see what's going on. When the 'next' starts GDB is in
get_alias_set, GDB steps forward, entering tree_check. GDB then uses
the extent of the block to figure out where the inline function ends,
and steps forward to that address (0x40116f). At this point, GDB
looks up the current line in the line table (43), and reports a stop
at this line.
In this commit, the fix I propose is to look for the line table
pattern seen above, a sequence of line table entries, that end with a
non-statement entry for the calling line of an inline function,
located at the exact end address of an inline function block.
When such a pattern is found, then we can extend the inline function's
address range to the next line table address, so long as doing so does
not extend the inline function beyond the extent of the containing,
non-inline, function.
In the above example, the block for the tree_check function would be
extended to end at 0x401172. With this fix in place, and with the
same test binary, GDB now behaves like this:
(gdb) break get_alias_set
Breakpoint 1 at 0x401160: file /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc, line 51.
(gdb) run
Starting program: /tmp/build/gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Breakpoint 1, get_alias_set (t=t@entry=0x404038 <xx>)
at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:51
51 if (t != NULL
(gdb) next
52 && TREE_TYPE (t).z != 1
(gdb) next
53 && TREE_TYPE (t).z != 2
(gdb)
The block for the inline function has been updated, like this:
(gdb) maintenance info blocks 0x40116e
...
[(block *) 0x4965530] 0x401040..0x4011a6
entry pc: 0x401160
function: get_alias_set(tree*)
symbol count: 1
address ranges:
0x401160..0x4011a6
0x401040..0x401046
[(block *) 0x49640b0] 0x401040..0x401172
entry pc: 0x401165
inline function: tree_check(tree*, int)
symbol count: 4
address ranges:
0x401165..0x401172
0x401040..0x401041
0x401041..0x401046
This is my alternative to the patch presented here:
https://inbox.sourceware.org/gdb-patches/AS1PR01MB946510286FBF2497A6F03E83E4922@AS1PR01MB9465.eurprd01.prod.exchangelabs.com/
This original patch series from Bernd Edlinger contains a number of
different fixes, some have already been split out and merged into GDB,
but the core idea for how to improve inline function handling by
extending the inline block range is the same, however, the mechanism
Bernd uses is significantly different.
In the above series, the approach taken is to mark the line table at
the end address of an inline function, and a few addresses beyond
that (see the is-weak flag in the above series). Then, when looking
up the block for a given address, if the address is within this marked
region, then we actually return the previous (inline function) block.
I believe that the above is a fair high-level summary of how the above
patch solves the inline function range problem. Any differences are
down to my misunderstanding the above patch, for which I apologise.
My problem with the above patch is that it breaks what I think should
be an invariant of GDB, that when looking up a block for a given
address, the block returned must contain the address within its
ranges. I feel that, if we start to break this invariant, then we
risk introducing bugs within, e.g. the stepping control code.
In contrast, my approach solves this problem during the DWARF parsing,
where problem cases are identified, and the DWARF "fixed" by extending
the block ranges. After this, no additional changes are needed in
GDB, the address to block mapping can work as normal, and the stepping
logic can continue to work just as it always has.
The test changes for gdb.cp/step-and-next-inline.exp have been taken
from Bernd's original patch series, and I've added a Co-Author tag for
Bernd to reflect this, as well as for the inspiration that I took from
his original series when creating this alternative proposal.
If/when this patch is merged, I plan to follow up with some cleanup to
the test case gdb.cp/step-and-next-inline.exp. I think this test
should really be moved to gdb.opt/, it's really testing optimisation
debug, not C++ features, but also the structure of the test file is a
bit of a mess. I think with some restructuring we could make the test
more readable, and also, maybe, test some additional compiler
flags (e.g. more optimisation levels). I've not done the refactoring
in this patch in order to make it clearer what new tests I've added,
and also, I want to leave the test similar to what's in Bernd's
original series, to make comparison easier.
The gdb.cp/step-and-next-inline.exp test was originally added by me
back in 2019, so the problems with it are of my own making.
For testing I've obviously run the entire test suite, but of
particular interest are these tests:
gdb.cp/step-and-next-inline.exp
gdb.dwarf2/dw2-inline-bt.exp
gdb.opt/empty-inline-cxx.exp
gdb.opt/empty-inline.exp
gdb.opt/inline-bt.exp
I've run these tests with a range of different gcc versions: 9.5.0,
10.5.0, 11.5.0, 12.2.0, 13.3.0, 14.2.0, 15.1.0. These tests all
relate to optimised debug of inline functions, and all passed with all
compiler versions listed here.
Co-Authored-By: Bernd Edlinger <bernd.edlinger@hotmail.de>
---
gdb/dwarf2/read.c | 80 +++
gdb/testsuite/gdb.cp/step-and-next-inline.exp | 38 +-
.../gdb.dwarf2/dw2-extend-inline-block.c | 78 +++
.../gdb.dwarf2/dw2-extend-inline-block.exp | 574 ++++++++++++++++++
4 files changed, 763 insertions(+), 7 deletions(-)
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index cb3dace9fea..fd6fffcb780 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -16250,6 +16250,79 @@ dwarf_finish_line (struct gdbarch *gdbarch, struct subfile *subfile,
dwarf_record_line_1 (gdbarch, subfile, 0, address, LEF_IS_STMT, cu);
}
+/* Look for an inline block that finishes at ORIGINAL_ADDRESS. If a block
+ is found, then search up the block hierarchy looking for a suitable
+ inline block to extend, a suitable block will be called from LINE. If a
+ block is found then update its end address to EXTENDED_ADDRESS. */
+
+static void
+dwarf_find_and_extend_inline_block_range (dwarf2_cu *cu,
+ unrelocated_addr original_address,
+ unrelocated_addr extended_address,
+ unsigned int line)
+{
+ /* Is there an inline block that ends at ORIGINAL_ADDRESS? */
+ auto it = cu->inline_block_ends.find (original_address);
+ if (it == cu->inline_block_ends.end ())
+ return;
+
+ /* Walk back up the block structure until we find the first inline block
+ that occurs after a non-inline block. This is our candidate for
+ extending. */
+ struct block *block = nullptr;
+ for (const struct block *b = it->second;
+ b != nullptr;
+ b = b->superblock ())
+ {
+ if (b->function () != nullptr && b->inlined_p ())
+ {
+ if (b->superblock () != nullptr
+ && b->superblock ()->function () != nullptr
+ && !b->superblock ()->inlined_p ())
+ {
+ block = const_cast<struct block *> (b);
+ break;
+ }
+ }
+ }
+
+ /* If we didn't find a block, of the line table doesn't indicate that the
+ block should be extended, then we're done. Maybe we should try harder
+ to look for the block that matches LINE, but this would require us to
+ possibly extended more blocks, adding more complexity. Currently,
+ this works enough for simple cases, we can possibly improve the logic
+ here later on. */
+ if (block == nullptr || block->function ()->line () != line)
+ return;
+
+ /* Sanity check. We should have an inline block, which should have a
+ valid super block. */
+ gdb_assert (block->inlined_p ());
+ gdb_assert (block->superblock () != nullptr);
+
+ CORE_ADDR extended_end = cu->per_objfile->relocate (extended_address);
+
+ /* The proposed new end of BLOCK is outside of the ranges of BLOCK's
+ superblock. If we tried to extend BLOCK then this would create an
+ invalid block structure; BLOCK would no longer be fully nested within
+ its superblock. Don't do that. */
+ if (extended_end > block->superblock ()->end ())
+ return;
+
+ CORE_ADDR original_end = cu->per_objfile->relocate (original_address);
+
+ /* Now find the part of BLOCK that ends at ORIGINAL_END, and extend it
+ out to EXTENDED_END. */
+ for (blockrange &br : block->ranges ())
+ {
+ if (br.end () == original_end)
+ br.set_end (extended_end);
+ }
+
+ if (block->end () == original_end)
+ block->set_end (extended_end);
+}
+
void
lnp_state_machine::record_line (bool end_sequence)
{
@@ -16268,6 +16341,13 @@ lnp_state_machine::record_line (bool end_sequence)
(end_sequence ? "\t(end sequence)" : ""));
}
+ if (m_address != m_last_address
+ && m_stmt_at_address
+ && m_cu->producer_is_gcc ()
+ && (m_flags & LEF_IS_STMT) == 0)
+ dwarf_find_and_extend_inline_block_range (m_cu, m_last_address,
+ m_address, m_line);
+
file_entry *fe = current_file ();
if (fe == NULL)
diff --git a/gdb/testsuite/gdb.cp/step-and-next-inline.exp b/gdb/testsuite/gdb.cp/step-and-next-inline.exp
index effff2721fa..4ed82deda32 100644
--- a/gdb/testsuite/gdb.cp/step-and-next-inline.exp
+++ b/gdb/testsuite/gdb.cp/step-and-next-inline.exp
@@ -24,13 +24,6 @@ if {[test_compiler_info gcc*] && ![supports_statement_frontiers] } {
proc do_test { use_header } {
global srcfile testfile
- if { $use_header } {
- # This test will not pass due to poor debug information
- # generated by GCC (at least up to 10.x). See
- # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94474
- return
- }
-
set options {c++ debug nowarnings optimize=-O2}
if { [supports_statement_frontiers] } {
lappend options additional_flags=-gstatement-frontiers
@@ -198,6 +191,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 1 pass 2"
gdb_test "step" ".*TREE_TYPE.*" "step 4"
gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
"not in inline 2 pass 2"
@@ -205,6 +200,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 2 pass 2"
gdb_test "step" ".*TREE_TYPE.*" "step 7"
gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
"not in inline 3 pass 2"
@@ -212,6 +209,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 3 pass 2"
gdb_test "step" "return 0.*" "step 10"
gdb_test "bt" \
"\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
@@ -255,6 +254,31 @@ proc do_test { use_header } {
gdb_test "bt" "#0\\s+\[^\r\n\]*tree_check\[^\r\n\]*${hdrfile}:.*" \
"abort from inline 1 pass 3"
}
+
+ clean_restart ${executable}
+
+ if ![runto_main] {
+ return
+ }
+
+ gdb_test "bt" "\\s*\\#0\\s+main.*" "in main pass 4"
+ gdb_test "skip tree_check" ".*" "skip tree_check pass 4"
+ gdb_test "step" ".*" "step into get_alias_set pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "in get_alias_set pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 1 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 1 pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 2 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 2 pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 3 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 3 pass 4"
+ gdb_test "step" "return 0.*" "step 4 pass 4"
+ gdb_test "bt" \
+ "\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
+ "not in inline 4 pass 4"
}
}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
new file mode 100644
index 00000000000..d6becf5d66b
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
@@ -0,0 +1,78 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 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;
+
+/* The follow code exists only to be referenced from the generated line
+ table. */
+#if 0
+static inline void
+foo (void)
+{
+ /* foo:1 */
+ /* foo:2 */
+ /* foo:3 */
+}
+
+int
+main (void)
+{ /* main decl line */
+ /* main:1 */
+ /* main:2 */
+ /* main:3 */ foo (); /* foo call line */
+ /* main:4 */
+ /* main:5 */
+ /* main:6 */
+}
+#endif
+
+
+int
+main (void)
+{
+ asm ("main_label: .globl main_label");
+ ++global_var;
+
+ asm ("main_0: .globl main_0");
+ ++global_var;
+
+ asm ("main_1: .globl main_1");
+ ++global_var;
+
+ 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_8");
+ ++global_var;
+
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
new file mode 100644
index 00000000000..963410ce057
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
@@ -0,0 +1,574 @@
+# Copyright 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/>.
+
+# When compiling optimised code, GCC will sometimes truncate the address
+# range of an inline function, usually by a single instruction.
+#
+# It is possible to detect when this has happened by looking at the line
+# table, GCC will create two non-statement line table entries associated
+# with the call-line of the inline function, but the end address of the
+# inline function will be set to be the address of the first of these line
+# table entries.
+#
+# The problem here is that block end addresses are not inclusive, which
+# means the block ends before either of these line table entries.
+#
+# What we find is that we get a better debug experience if we extend the
+# inline function to actually end at the second line table entry, that is
+# the first line table entry becomes part of the inline function, while the
+# second entry remains outside the inline function.
+#
+# This test tries to create this situation using the DWARF assembler, and
+# then checks that GDB correctly extends the inline function to include the
+# first line table entry.
+
+load_lib dwarf.exp
+
+require dwarf2_support
+
+standard_testfile .c
+
+# Lines numbers we reference in the generated DWARF.
+set main_decl_line [gdb_get_line_number "main decl line"]
+set main_line_1 [gdb_get_line_number "main:1"]
+set main_line_4 [gdb_get_line_number "main:4"]
+set foo_call_line [gdb_get_line_number "foo call line"]
+set foo_line_1 [gdb_get_line_number "foo:1"]
+
+get_func_info main
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with a contiguous address range that needs extending.
+
+proc build_dwarf_for_contiguous_block { asm_file } {
+ Dwarf::assemble $asm_file {
+ 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 main_3 addr}
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+ }
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_contiguous_block'.
+
+proc check_contiguous_block {} {
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_4" "*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 expected content"
+}
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with two ranges, and it is the first range that needs extending.
+
+proc build_dwarf_for_first_block_range { asm_file dwarf_version } {
+ Dwarf::assemble $asm_file {
+ upvar dwarf_version dwarf_version
+ 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}
+ {ranges $ranges_label DW_FORM_sec_offset}
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+
+ if { $dwarf_version == 5 } {
+ rnglists {} {
+ table {} {
+ ranges_label: list_ {
+ start_end main_1 main_3
+ start_end main_7 main_8
+ }
+ }
+ }
+ } else {
+ ranges { } {
+ ranges_label: sequence {
+ range main_1 main_3
+ range main_7 main_8
+ }
+ }
+ }
+ }
+}
+
+# Wrapper around 'build_dwarf_for_first_block_range', creates DWARF 4 range
+# information.
+
+proc build_dwarf_for_first_block_range_4 { asm_file } {
+ build_dwarf_for_first_block_range $asm_file 4
+}
+
+# Wrapper around 'build_dwarf_for_first_block_range', creates DWARF 5 range
+# information.
+
+proc build_dwarf_for_first_block_range_5 { asm_file } {
+ build_dwarf_for_first_block_range $asm_file 5
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_first_block_range'.
+
+proc check_for_block_ranges_1 {} {
+
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_8" "*UNKNOWN*" \
+ "get address of foo end"]
+
+ set main_4 [get_hexadecimal_valueof "&main_4" "*UNKNOWN*" \
+ "get address of main_4 label"]
+ set main_7 [get_hexadecimal_valueof "&main_7" "*UNKNOWN*" \
+ "get address of main_7 label"]
+
+ gdb_test "maintenance info blocks" \
+ [multi_line \
+ "\\\[\\(block \\*\\) $::hex\\\] $foo_start\\.\\.$foo_end" \
+ " entry pc: $foo_start" \
+ " inline function: foo" \
+ " symbol count: $::decimal" \
+ " address ranges:" \
+ " $foo_start\\.\\.$main_4" \
+ " $main_7\\.\\.$foo_end"] \
+ "block for foo has expected content"
+}
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with two ranges, and it is the second range that needs extending.
+
+proc build_dwarf_for_last_block_range { asm_file dwarf_version } {
+ Dwarf::assemble $asm_file {
+ upvar dwarf_version dwarf_version
+ 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}
+ {ranges $ranges_label DW_FORM_sec_offset}
+ {entry_pc main_1 addr}
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+
+ if { $dwarf_version == 5 } {
+ rnglists {} {
+ table {} {
+ ranges_label: list_ {
+ start_end main_7 main_8
+ start_end main_1 main_3
+ }
+ }
+ }
+ } else {
+ ranges { } {
+ ranges_label: sequence {
+ range main_7 main_8
+ range main_1 main_3
+ }
+ }
+ }
+ }
+}
+
+# Wrapper around 'build_dwarf_for_last_block_range', creates DWARF 4 range
+# information.
+
+proc build_dwarf_for_last_block_range_4 { asm_file } {
+ build_dwarf_for_last_block_range $asm_file 4
+}
+
+# Wrapper around 'build_dwarf_for_last_block_range', creates DWARF 5 range
+# information.
+
+proc build_dwarf_for_last_block_range_5 { asm_file } {
+ build_dwarf_for_last_block_range $asm_file 5
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_last_block_range'.
+
+proc check_for_block_ranges_2 {} {
+
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_8" "*UNKNOWN*" \
+ "get address of foo end"]
+
+ set main_4 [get_hexadecimal_valueof "&main_4" "*UNKNOWN*" \
+ "get address of main_4 label"]
+ set main_7 [get_hexadecimal_valueof "&main_7" "*UNKNOWN*" \
+ "get address of main_7 label"]
+
+ gdb_test "maintenance info blocks" \
+ [multi_line \
+ "\\\[\\(block \\*\\) $::hex\\\] $foo_start\\.\\.$foo_end" \
+ " entry pc: $foo_start" \
+ " inline function: foo" \
+ " symbol count: $::decimal" \
+ " address ranges:" \
+ " $main_7\\.\\.$foo_end" \
+ " $foo_start\\.\\.$main_4"] \
+ "block for foo has expected content"
+}
+
+# Buidl ASM_FILE, along with the global SRCFILE into an executable called
+# TESTFILE. Place a breakpoint in 'foo', run to the breakpoint, and use
+# BLOCK_CHECK_FUNC to ensure the block for 'foo' is correct.
+#
+# Then step through 'foo' and back into 'main'.
+
+proc run_test { asm_file testfile block_check_func } {
+ if {[prepare_for_testing "failed to prepare" $testfile \
+ [list $::srcfile $asm_file] {nodebug}]} {
+ return
+ }
+
+ if {![runto_main]} {
+ return
+ }
+
+ gdb_breakpoint foo
+ gdb_test "continue" \
+ [multi_line \
+ "Breakpoint $::decimal, foo \\(\\) \[^\r\n\]+:$::foo_line_1" \
+ "$::foo_line_1\\s+/\\* foo:1 \\*/"] \
+ "continue to b/p in foo"
+
+ # Check that the block for `foo` has been extended.
+ $block_check_func
+
+ 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"
+
+ gdb_test "step" \
+ "^[expr $::foo_line_1 + 1]\\s+/\\* foo:2 \\*/" \
+ "step to second line of foo"
+
+ gdb_test "step" \
+ "^[expr $::foo_line_1 + 2]\\s+/\\* foo:3 \\*/" \
+ "step to third line of foo"
+
+ gdb_test "step" \
+ [multi_line \
+ "^main \\(\\) at \[^\r\n\]+:$::main_line_4" \
+ "$::main_line_4\\s+/\\* main:4 \\*/"] \
+ "set back to main"
+
+ gdb_test "step" \
+ "^[expr $::main_line_4 + 1]\\s+/\\* main:5 \\*/" \
+ "step again in main"
+}
+
+# Test specifications, items are:
+# 1. Prefix string used to describe the test.
+# 2. Proc to call that builds the DWARF.
+# 3. Proc to call that runs 'maint info blocks' when stopped at the entry
+# $pc for 'foo' (the inline function), and checks that the block details
+# for 'foo' are correct.
+set test_list \
+ [list \
+ [list "block with ranges, extend first range, dwarf 4" \
+ build_dwarf_for_first_block_range_4 \
+ check_for_block_ranges_1] \
+ [list "block with ranges, extend first range, dwarf 5" \
+ build_dwarf_for_first_block_range_5 \
+ check_for_block_ranges_1] \
+ [list "block with ranges, extend last range, dwarf 4" \
+ build_dwarf_for_last_block_range_4 \
+ check_for_block_ranges_2] \
+ [list "block with ranges, extend last range, dwarf 5" \
+ build_dwarf_for_last_block_range_4 \
+ check_for_block_ranges_2] \
+ [list "contiguous block" \
+ build_dwarf_for_contiguous_block \
+ check_contiguous_block] \
+ ]
+
+# Run all the tests.
+set suffix 0
+foreach test_spec $test_list {
+ incr suffix
+
+ set prefix [lindex $test_spec 0]
+ set build_dwarf_func [lindex $test_spec 1]
+ set check_block_func [lindex $test_spec 2]
+
+ with_test_prefix $prefix {
+ set asm_file [standard_output_file ${testfile}-${suffix}.S]
+ $build_dwarf_func $asm_file
+ run_test $asm_file ${testfile}-${suffix} $check_block_func
+ }
+}
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH 0/7] Inline Function Optimised Code Debug Improvements
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (7 preceding siblings ...)
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
@ 2025-08-01 15:41 ` Sam James
8 siblings, 0 replies; 37+ messages in thread
From: Sam James @ 2025-08-01 15:41 UTC (permalink / raw)
To: Andrew Burgess; +Cc: gdb-patches
Andrew Burgess <aburgess@redhat.com> writes:
> This series presents some improvements to debugging inline functions,
> especially in optimised code.
>
> The first two patches of this series have been posted previously here:
>
> https://inbox.sourceware.org/gdb-patches/cover.1736865029.git.aburgess@redhat.com
>
> This series replaces that earlier work, though the patches are
> basically unchanged.
>
> This entire series is an alternative solution to the patches that were
> posted here:
>
> https://inbox.sourceware.org/gdb-patches/AS1PR01MB946510286FBF2497A6F03E83E4922@AS1PR01MB9465.eurprd01.prod.exchangelabs.com/
>
> My reasons for writing an alternative patch set can be found in the
> commit messages of patches (2) and (7), I'm not going to repeat them
> here. However, Bernd, the original patch author, did amazing work
> identifying the problems GDB was having, and these patches likely
> wouldn't exist without the original patch series, so a big thanks
> there.
>
> For reviewing, this series can be considered in 3 parts:
>
> + Patches (1) and (2) are independent from the rest of the series,
> though related in theme. Both these patches could be approved and
> merged ahead of remaining work, and do offer an improvement to GDB
> on their own.
>
> + Patches (3), (4), (5), and (6) are refactoring work to support the
> last patch. These can be reviewed, but shouldn't be merged until
> the last patch is approved.
>
> + Patch (7) covers the final parts of this work, which deal with
> non-empty inline functions.
>
> All thoughts and feedback are welcome.
There hasn't been anything else so I don't feel too guilty about being
noisy: thanks again for these to you and Bernd. I'd stopped using the
patches when they ceased to apply a few months ago and then applied
these and found I could immediately spot the difference with `list`
working correctly inside/out of an inlined function.
All I need now is PR32381 and I'm a very happy debugger indeed!
Anyway, thank you. I'll continue to test them out.
>
> Thanks,
> Andrew
>
> ---
>
> Andrew Burgess (7):
> gdb: improve line number lookup around inline functions
> gdb: handle empty ranges for inline subroutines
> gdb: split dwarf line table parsing in two
> gdb: move block range recording into its own function
> gdb: create address map after parsing all DIE
> gdb: record block end addresses while parsing DIEs
> gdb: fix-up truncated inline function block ranges
>
> gdb/buildsym.c | 42 +-
> gdb/buildsym.h | 13 +-
> gdb/dwarf2/cu.h | 7 +
> gdb/dwarf2/read.c | 288 +++++++--
> gdb/symtab.c | 25 +-
> gdb/testsuite/gdb.cp/step-and-next-inline.exp | 171 ++++--
> .../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 | 260 ++++++++
> .../gdb.dwarf2/dw2-extend-inline-block.c | 78 +++
> .../gdb.dwarf2/dw2-extend-inline-block.exp | 574 ++++++++++++++++++
> gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c | 79 +++
> gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp | 227 +++++++
> .../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 ++++
> gdb/testsuite/gdb.opt/inline-bt.c | 28 +
> gdb/testsuite/gdb.opt/inline-bt.exp | 119 ++--
> 21 files changed, 2342 insertions(+), 195 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.dwarf2/dw2-extend-inline-block.c
> create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
> create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c
> create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.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
>
>
> base-commit: cfbf9925c1c34f9e9d47c8b29d165866557663e3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (6 preceding siblings ...)
2025-08-01 8:58 ` [PATCHv2 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
@ 2025-10-16 17:49 ` Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
` (7 more replies)
7 siblings, 8 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-10-16 17:49 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
In v3:
- Rebased to current upstream HEAD. Resolved merged conflicts, and
retested.
- Reread all the patches, and updated / tweaked some of the comment
and commit messages, but no changes to the code other than as
required to fix merge conflicts.
- Updated the tests to take account of changes to the DWARF assembler.
In v2:
- Fixes to the test added in patch (2) to address failures reported
from Linaro CI. The issues all relate to compiling the DWARF
assembler test with `-pie` which is on by default for the Linaro
CI machines. The issues were all DWARF generation issues.
- Rebased series to more recent upstream/master.
---
This series presents some improvements to debugging inline functions,
especially in optimised code.
The first two patches of this series have been posted previously here:
https://inbox.sourceware.org/gdb-patches/cover.1736865029.git.aburgess@redhat.com
This series replaces that earlier work, though the patches are
basically unchanged.
This entire series is an alternative solution to the patches that were
posted here:
https://inbox.sourceware.org/gdb-patches/AS1PR01MB946510286FBF2497A6F03E83E4922@AS1PR01MB9465.eurprd01.prod.exchangelabs.com/
My reasons for writing an alternative patch set can be found in the
commit messages of patches (2) and (7), I'm not going to repeat them
here. However, Bernd, the original patch author, did amazing work
identifying the problems GDB was having, and these patches likely
wouldn't exist without the original patch series, so a big thanks
there.
For reviewing, this series can be considered in 3 parts:
+ Patches (1) and (2) are independent from the rest of the series,
though related in theme. Both these patches could be approved and
merged ahead of remaining work, and do offer an improvement to GDB
on their own.
+ Patches (3), (4), (5), and (6) are refactoring work to support the
last patch. These can be reviewed, but shouldn't be merged until
the last patch is approved.
+ Patch (7) covers the final parts of this work, which deal with
non-empty inline functions.
All thoughts and feedback are welcome.
Thanks,
Andrew
---
Andrew Burgess (7):
gdb: improve line number lookup around inline functions
gdb: handle empty ranges for inline subroutines
gdb: split dwarf line table parsing in two
gdb: move block range recording into its own function
gdb: create address map after parsing all DIE
gdb: record block end addresses while parsing DIEs
gdb: fix-up truncated inline function block ranges
gdb/buildsym.c | 42 +-
gdb/buildsym.h | 13 +-
gdb/dwarf2/cu.h | 7 +
gdb/dwarf2/line-program.c | 120 +++-
gdb/dwarf2/line-program.h | 25 +-
gdb/dwarf2/read.c | 181 +++++-
gdb/symtab.c | 25 +-
gdb/testsuite/gdb.cp/step-and-next-inline.exp | 171 ++++--
.../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-extend-inline-block.c | 78 +++
.../gdb.dwarf2/dw2-extend-inline-block.exp | 574 ++++++++++++++++++
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c | 79 +++
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp | 227 +++++++
.../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 ++++
gdb/testsuite/gdb.opt/inline-bt.c | 28 +
gdb/testsuite/gdb.opt/inline-bt.exp | 119 ++--
23 files changed, 2352 insertions(+), 225 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.dwarf2/dw2-extend-inline-block.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-inline-bt.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
base-commit: b6753354fbbe7c2c66ae9f452ba7aa049db0fe0c
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv3 1/7] gdb: improve line number lookup around inline functions
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
@ 2025-10-16 17:49 ` Andrew Burgess
2025-10-27 22:22 ` Tom Tromey
2025-10-16 17:49 ` [PATCHv3 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
` (6 subsequent siblings)
7 siblings, 1 reply; 37+ messages in thread
From: Andrew Burgess @ 2025-10-16 17:49 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
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
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv3 2/7] gdb: handle empty ranges for inline subroutines
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
@ 2025-10-16 17:49 ` Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
` (5 subsequent siblings)
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-10-16 17:49 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess, Bernd Edlinger
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 955893c5f0c..49026c2dcf6 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -9131,6 +9131,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:
@@ -9293,7 +9303,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)
@@ -9415,7 +9430,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);
@@ -9633,9 +9653,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.
@@ -9916,8 +9951,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 92caed1c9fa..3449b221798 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..ddd2d2959d1
--- /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 { } {
+ DW_TAG_compile_unit {
+ DW_AT_producer "GNU C 14.1.0"
+ DW_AT_language @DW_LANG_C
+ DW_AT_name $::srcfile
+ DW_AT_comp_dir /tmp
+ DW_AT_low_pc 0 addr
+ DW_AT_stmt_list $lines_table DW_FORM_sec_offset
+ } {
+ inline_func: subprogram {
+ DW_AT_name foo
+ DW_AT_inline @DW_INL_declared_inlined
+ }
+ subprogram {
+ DW_AT_name main
+ DW_AT_decl_file 1 data1
+ DW_AT_decl_line $::main_decl_line data1
+ DW_AT_decl_column 1 data1
+ DW_AT_low_pc $::main_start addr
+ DW_AT_high_pc $::main_len data4
+ DW_AT_external 1 flag
+ } {
+ inlined_subroutine {
+ DW_AT_abstract_origin %$inline_func
+ DW_AT_call_file 1 data1
+ DW_AT_call_line $::foo_call_line data1
+ DW_AT_low_pc main_1 addr
+ DW_AT_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..7c3b3125c2c
--- /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 } {
+ DW_TAG_compile_unit {
+ DW_AT_producer "GNU C 14.1.0"
+ DW_AT_language @DW_LANG_C
+ DW_AT_name $::srcfile
+ DW_AT_comp_dir /tmp
+ DW_AT_low_pc 0 addr
+ DW_AT_stmt_list $lines_table DW_FORM_sec_offset
+ } {
+ inline_func: subprogram {
+ DW_AT_name foo
+ DW_AT_inline @DW_INL_declared_inlined
+ }
+ subprogram {
+ DW_AT_name main
+ DW_AT_decl_file 1 data1
+ DW_AT_decl_line $::main_decl_line data1
+ DW_AT_decl_column 1 data1
+ DW_AT_low_pc $::main_start addr
+ DW_AT_high_pc $::main_len data4
+ DW_AT_external 1 flag
+ } {
+ inlined_subroutine {
+ DW_AT_abstract_origin %$inline_func
+ DW_AT_call_file 1 data1
+ DW_AT_call_line $::foo_call_line data1
+ DW_AT_entry_pc $entry_pc addr
+ DW_AT_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 3c85b998810..f24acf1e946 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 {
- DW_AT_producer "gcc"
+ DW_AT_producer $producer_str
DW_AT_language @DW_LANG_C
DW_AT_name $::srcfile
DW_AT_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
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv3 3/7] gdb: split dwarf line table parsing in two
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
@ 2025-10-16 17:49 ` Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 4/7] gdb: move block range recording into its own function Andrew Burgess
` (4 subsequent siblings)
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-10-16 17:49 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
A later commit in this series, that improves GDB's ability to debug
optimised code, wants to use the line table information in order to
"fix" inline blocks with a truncated address range. For the reasoning
behind wanting to do that, please read ahead in the series.
Assuming that we accept for now the need to use the line table
information to adjust the block ranges, then why is this commit
needed?
GDB splits the line table data info different symtabs, adding end of
sequence markers as we move between symtabs. This seems to work fine
for GDB, but causes a problem for me in this case.
What I will want to do is this: scan the line table and spot line
table entries that corresponds to the end addresses of an inline
block's address range. If the block meets certain requirements, then
the end address of the block is adjusted to be that of the next line
table entry.
The way that GDB currently splits the line table entries between
symtabs makes this harder. I will have the set of blocks end
addresses which I know might be fixable, but to find the line table
entry corresponding to that address requires searching through all the
symtabs. Having found the entry for the end address, I then need to
find the next line table entry. For some blocks this is easy, it's
the next entry in the same symtab. But for other blocks the next
entry might be in a different symtab, which requires yet another full
search.
I did try implementing this approach, but the number of full symtab
searches is significant, and it had a significant impact on GDB's
debug parsing performance. The impact was such that an operation that
currently takes ~7seconds would take ~3minutes or more. Now I could
possibly improve that 3 minutes figure by optimising the code some,
but I think that would add unnecessary complexity.
By deferring building the line table until after we have parsed the
DIEs it becomes simple to spot when a line table entry corresponds to
a block end address, and finding the next entry is always trivial, as,
at this point, the next entry is just the next entry which we will
process. With this approach I see no noticable impact on DWARF
parsing performance.
This patch is just the refactoring. There's no finding block end
addresses and "fixing" being done here. This just sets things up for
the later commits.
The existing code has a single function handle_DW_AT_stmt_list which
loads the line table header and then calls dwarf_decode_lines to
decode the line table itself, splitting the line table entries between
symtabs.
After this commit handle_DW_AT_stmt_list is renamed to
decode_line_header_for_cu, this loads the line table header and
creates the symtabs based off the line table, but doesn't process the
line table. The dwarf_decode_lines function is no longer called at
this point.
In read_file_scope is where dwarf_decode_lines is now called. This
relies on the line table having been loaded earlier, and processes the
line table entries, splitting them between symtabs.
There should be no user visible changes after this commit.
---
gdb/dwarf2/line-program.c | 39 +++---------------
gdb/dwarf2/line-program.h | 25 ++----------
gdb/dwarf2/read.c | 83 +++++++++++++++++++++++++++------------
3 files changed, 67 insertions(+), 80 deletions(-)
diff --git a/gdb/dwarf2/line-program.c b/gdb/dwarf2/line-program.c
index c30f70dd11a..40b570b70c0 100644
--- a/gdb/dwarf2/line-program.c
+++ b/gdb/dwarf2/line-program.c
@@ -480,12 +480,10 @@ lnp_state_machine::check_line_address (struct dwarf2_cu *cu,
}
}
-/* Subroutine of dwarf_decode_lines to simplify it.
- Process the line number information in LH. */
+/* See dwarf2/line-program.h. */
-static void
-dwarf_decode_lines_1 (struct line_header *lh, struct dwarf2_cu *cu,
- unrelocated_addr lowpc)
+void
+dwarf_decode_lines (struct dwarf2_cu *cu, unrelocated_addr lowpc)
{
const gdb_byte *line_ptr, *extended_end;
const gdb_byte *line_end;
@@ -494,6 +492,9 @@ dwarf_decode_lines_1 (struct line_header *lh, struct dwarf2_cu *cu,
struct objfile *objfile = cu->per_objfile->objfile;
bfd *abfd = objfile->obfd.get ();
struct gdbarch *gdbarch = objfile->arch ();
+ struct line_header *lh = cu->line_header;
+
+ gdb_assert (lh != nullptr);
line_ptr = lh->statement_program_start;
line_end = lh->statement_program_end;
@@ -690,31 +691,3 @@ dwarf_decode_lines_1 (struct line_header *lh, struct dwarf2_cu *cu,
state_machine.record_line (true);
}
}
-
-/* See dwarf2/line-program.h. */
-
-void
-dwarf_decode_lines (struct line_header *lh, struct dwarf2_cu *cu,
- unrelocated_addr lowpc, bool decode_mapping)
-{
- if (decode_mapping)
- dwarf_decode_lines_1 (lh, cu, lowpc);
-
- /* Make sure a symtab is created for every file, even files
- which contain only variables (i.e. no code with associated
- line numbers). */
- buildsym_compunit *builder = cu->get_builder ();
- struct compunit_symtab *cust = builder->get_compunit_symtab ();
-
- for (auto &fe : lh->file_names ())
- {
- dwarf2_start_subfile (cu, fe, *lh);
- subfile *sf = builder->get_current_subfile ();
-
- if (sf->symtab == nullptr)
- sf->symtab = allocate_symtab (cust, sf->name.c_str (),
- sf->name_for_id.c_str ());
-
- fe.symtab = sf->symtab;
- }
-}
diff --git a/gdb/dwarf2/line-program.h b/gdb/dwarf2/line-program.h
index 824f18f9613..dc5cf3faa44 100644
--- a/gdb/dwarf2/line-program.h
+++ b/gdb/dwarf2/line-program.h
@@ -20,28 +20,11 @@
#ifndef GDB_DWARF2_LINE_PROGRAM_H
#define GDB_DWARF2_LINE_PROGRAM_H
-/* Decode the Line Number Program (LNP) for the given line_header
- structure and CU. The actual information extracted and the type
- of structures created from the LNP depends on the value of PST.
+/* Decode the Line Number Program (LNP) for the line_header structure in
+ CU.
- FND holds the CU file name and directory, if known.
- It is used for relative paths in the line table.
+ LOWPC is the lowest address in CU (or 0 if not known). */
- NOTE: It is important that psymtabs have the same file name (via
- strcmp) as the corresponding symtab. Since the directory is not
- used in the name of the symtab we don't use it in the name of the
- psymtabs we create. E.g. expand_line_sal requires this when
- finding psymtabs to expand. A good testcase for this is
- mb-inline.exp.
-
- LOWPC is the lowest address in CU (or 0 if not known).
-
- Boolean DECODE_MAPPING specifies we need to fully decode .debug_line
- for its PC<->lines mapping information. Otherwise only the filename
- table is read in. */
-
-extern void dwarf_decode_lines (struct line_header *lh,
- struct dwarf2_cu *cu,
- unrelocated_addr lowpc, bool decode_mapping);
+extern void dwarf_decode_lines (struct dwarf2_cu *cu, unrelocated_addr lowpc);
#endif /* GDB_DWARF2_LINE_PROGRAM_H */
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 49026c2dcf6..6c5157ea8d4 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -5960,29 +5960,55 @@ find_file_and_directory (struct die_info *die, struct dwarf2_cu *cu)
return *cu->per_cu->fnd;
}
-/* Handle DW_AT_stmt_list for a compilation unit.
- DIE is the DW_TAG_compile_unit die for CU.
- COMP_DIR is the compilation directory. LOWPC is passed to
- dwarf_decode_lines. See dwarf_decode_lines comments about it. */
+/* Ensure that every file_entry within the line_table of CU has a symtab
+ allocated for it. */
static void
-handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
- const file_and_directory &fnd, unrelocated_addr lowpc,
- bool have_code) /* ARI: editCase function */
+create_symtabs_from_cu_line_table (struct dwarf2_cu *cu)
{
- dwarf2_per_objfile *per_objfile = cu->per_objfile;
- struct attribute *attr;
- hashval_t line_header_local_hash;
- void **slot;
- int decode_mapping;
+ /* Make sure a symtab is created for every file, even files
+ which contain only variables (i.e. no code with associated
+ line numbers). */
+ buildsym_compunit *builder = cu->get_builder ();
+ struct compunit_symtab *cust = builder->get_compunit_symtab ();
- gdb_assert (! cu->per_cu->is_debug_types);
+ struct line_header *lh = cu->line_header;
+ gdb_assert (lh != nullptr);
- attr = dwarf2_attr (die, DW_AT_stmt_list, cu);
+ for (auto &fe : lh->file_names ())
+ {
+ dwarf2_start_subfile (cu, fe, *lh);
+ subfile *sf = builder->get_current_subfile ();
+
+ if (sf->symtab == nullptr)
+ sf->symtab = allocate_symtab (cust, sf->name.c_str (),
+ sf->name_for_id.c_str ());
+
+ fe.symtab = sf->symtab;
+ }
+}
+
+
+/* Handle DW_AT_stmt_list for a compilation unit. DIE is the
+ DW_TAG_compile_unit die for CU. FND is used to access the compilation
+ directory. This function will decode the line table header and create
+ symtab objects for the files referenced in the line table. The line
+ table itself though is not processed by this function. If there is no
+ line table, or there's a problem decoding the header, then CU will not
+ be updated. */
+
+static void
+decode_line_header_for_cu (struct die_info *die, struct dwarf2_cu *cu,
+ const file_and_directory &fnd)
+{
+ gdb_assert (!cu->per_cu->is_debug_types);
+
+ struct attribute *attr = dwarf2_attr (die, DW_AT_stmt_list, cu);
if (attr == NULL || !attr->form_is_unsigned ())
return;
sect_offset line_offset = (sect_offset) attr->as_unsigned ();
+ dwarf2_per_objfile *per_objfile = cu->per_objfile;
/* The line header hash table is only created if needed (it exists to
prevent redundant reading of the line table for partial_units).
@@ -6000,8 +6026,9 @@ handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
xcalloc, xfree));
}
+ void **slot;
line_header line_header_local (line_offset, cu->per_cu->is_dwz);
- line_header_local_hash = line_header_hash (&line_header_local);
+ hashval_t line_header_local_hash = line_header_hash (&line_header_local);
if (per_objfile->line_header_hash != NULL)
{
slot = htab_find_slot_with_hash (per_objfile->line_header_hash.get (),
@@ -6054,12 +6081,8 @@ handle_DW_AT_stmt_list (struct die_info *die, struct dwarf2_cu *cu,
then this is what we want as well. */
gdb_assert (die->tag != DW_TAG_partial_unit);
}
- decode_mapping = (die->tag != DW_TAG_partial_unit);
- /* The have_code check is here because, if LOWPC and HIGHPC are both 0x0,
- then there won't be any interesting code in the CU, but a check later on
- (in lnp_state_machine::check_line_address) will fail to properly exclude
- an entry that was removed via --gc-sections. */
- dwarf_decode_lines (cu->line_header, cu, lowpc, decode_mapping && have_code);
+
+ create_symtabs_from_cu_line_table (cu);
}
/* Process DW_TAG_compile_unit or DW_TAG_partial_unit. */
@@ -6111,10 +6134,12 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
scoped_restore restore_sym_cu
= make_scoped_restore (&per_objfile->sym_cu, cu);
- /* Decode line number information if present. We do this before
- processing child DIEs, so that the line header table is available
- for DW_AT_decl_file. */
- handle_DW_AT_stmt_list (die, cu, fnd, unrel_low, unrel_low != unrel_high);
+ /* Decode the line header if present. We do this before processing child
+ DIEs, so that information is available for DW_AT_decl_file. We defer
+ parsing the actual line table until after processing the child DIEs,
+ this allows us to fix up some of the inline function blocks as the
+ line table is read. */
+ decode_line_header_for_cu (die, cu, fnd);
/* Process all dies in compilation unit. */
for (die_info *child_die : die->children ())
@@ -6122,6 +6147,12 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
per_objfile->sym_cu = nullptr;
+ /* If we actually have code, then read the line table now. */
+ if (unrel_low != unrel_high
+ && die->tag != DW_TAG_partial_unit
+ && cu->line_header != nullptr)
+ dwarf_decode_lines (cu, unrel_low);
+
/* Decode macro information, if present. Dwarf 2 macro information
refers to information in the line number info statement program
header, so we can only read it if we've read the header
@@ -16144,7 +16175,7 @@ new_symbol (struct die_info *die, struct type *type, struct dwarf2_cu *cu,
if (file_cu->line_header == nullptr)
{
file_and_directory fnd (nullptr, nullptr);
- handle_DW_AT_stmt_list (file_cu->dies, file_cu, fnd, {}, false);
+ decode_line_header_for_cu (file_cu->dies, file_cu, fnd);
}
if (file_cu->line_header != nullptr)
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv3 4/7] gdb: move block range recording into its own function
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (2 preceding siblings ...)
2025-10-16 17:49 ` [PATCHv3 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
@ 2025-10-16 17:49 ` Andrew Burgess
2025-10-27 22:45 ` Tom Tromey
2025-10-16 17:49 ` [PATCHv3 5/7] gdb: create address map after parsing all DIE Andrew Burgess
` (3 subsequent siblings)
7 siblings, 1 reply; 37+ messages in thread
From: Andrew Burgess @ 2025-10-16 17:49 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Like the previous commit, this is a refactor that makes a later commit
in this series easier. The later commit improves GDB's ability to
debug optimised code. To do this I propose to "fix" the address
ranges of some inline blocks.
In order to know which blocks to fix, I need to record the end address
of inline blocks.
And so, I'd like to create a single common function where block ranges
are recorded, in a later commit I can then hook into this function to
record the block's end address(es). This commit sets up this single
common function.
The new function I'm adding dwarf2_record_single_block_range, takes a
currently unused argument unrel_high. This argument will be needed in
the later commit. I've added it now as this will allow the later
commit to be smaller and more focused. I only plan to push this
commit as part of the larger series, so I don't think adding
the (currently) unused argument is too much of a problem.
There should be no user visible change after this commit.
---
gdb/dwarf2/read.c | 22 +++++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 6c5157ea8d4..023cb07edd8 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -9940,6 +9940,21 @@ dwarf2_record_block_entry_pc (struct die_info *die, struct block *block,
}
}
+/* Helper function for dwarf2_record_block_ranges. This function records
+ the address range for a single BLOCK. LOW and HIGH are the block's
+ range, these addresses are inclusive, so LOW is the first address in
+ the range, and HIGH is the last address inside the range. UNREL_HIGH
+ is the unrelocated, exclusive version of HIGH, that is, HIGH is the
+ first address outside the range of BLOCK. */
+
+static void
+dwarf2_record_single_block_range (struct dwarf2_cu *cu, struct block *block,
+ CORE_ADDR low, CORE_ADDR high,
+ unrelocated_addr unrel_high)
+{
+ cu->get_builder ()->record_block_range (block, low, high);
+}
+
/* Record the address ranges for BLOCK, offset by BASEADDR, as given
in DIE. Also set the entry PC for BLOCK. */
@@ -10008,7 +10023,8 @@ dwarf2_record_block_ranges (struct die_info *die, struct block *block,
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);
+ dwarf2_record_single_block_range (cu, block, low, high,
+ unrel_high);
}
attr = dwarf2_attr (die, DW_AT_ranges, cu);
@@ -10038,8 +10054,8 @@ dwarf2_record_block_ranges (struct die_info *die, struct block *block,
{
CORE_ADDR abs_start = per_objfile->relocate (start);
CORE_ADDR abs_end = per_objfile->relocate (end);
- cu->get_builder ()->record_block_range (block, abs_start,
- abs_end - 1);
+ dwarf2_record_single_block_range (cu, block, abs_start,
+ abs_end - 1, end);
blockvec.emplace_back (abs_start, abs_end);
});
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv3 5/7] gdb: create address map after parsing all DIE
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (3 preceding siblings ...)
2025-10-16 17:49 ` [PATCHv3 4/7] gdb: move block range recording into its own function Andrew Burgess
@ 2025-10-16 17:49 ` Andrew Burgess
2025-10-27 22:56 ` Tom Tromey
2025-10-16 17:49 ` [PATCHv3 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
` (2 subsequent siblings)
7 siblings, 1 reply; 37+ messages in thread
From: Andrew Burgess @ 2025-10-16 17:49 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Continuing the work done in the last two commits, this commit defers
building the addrmap for a blockvector until after all the DIE have
been read, and the line table processed.
The benefit of this is that any changes to a block's ranges done
during line table processing (see the next commit) will be reflected
in the blockvector's addrmap.
The alternative to this is to build the addrmap as we initially see
each block, but then adjust the addrmap if we later decide to modify a
block. I think defering the addrmap creation is cleaner, and is less
work overall.
The addrmap requires that we add the most inner blocks first. I
achieve this by walking the blockvector backward, as we always add
parent blocks before their more inner child blocks.
There should be no user visible changes after this commit.
---
gdb/buildsym.c | 42 +++++++++++++++++++++++++++++++++++++-----
gdb/buildsym.h | 13 ++++---------
2 files changed, 41 insertions(+), 14 deletions(-)
diff --git a/gdb/buildsym.c b/gdb/buildsym.c
index 2a8e95e078b..6c46d75621b 100644
--- a/gdb/buildsym.c
+++ b/gdb/buildsym.c
@@ -410,8 +410,6 @@ buildsym_compunit::record_block_range (struct block *block,
if (start != block->start ()
|| end_inclusive + 1 != block->end ())
m_pending_addrmap_interesting = true;
-
- m_pending_addrmap.set_empty (start, end_inclusive, block);
}
struct blockvector *
@@ -447,9 +445,43 @@ buildsym_compunit::make_blockvector ()
/* If we needed an address map for this symtab, record it in the
blockvector. */
if (m_pending_addrmap_interesting)
- blockvector->set_map
- (new (&m_objfile->objfile_obstack) addrmap_fixed
- (&m_objfile->objfile_obstack, &m_pending_addrmap));
+ {
+ struct addrmap_mutable pending_addrmap;
+ int num_blocks = blockvector->num_blocks ();
+
+ /* If M_PENDING_ADDRMAP_INTERESTING is true then we must have seen
+ an interesting block. If we see one block, then we should at a
+ minimum have a global block, and a static block. */
+ gdb_assert (num_blocks > 1);
+
+ /* Assert our understanding of how the blocks are laid out. */
+ gdb_assert (blockvector->block (0)->is_global_block ());
+ gdb_assert (blockvector->block (1)->is_static_block ());
+
+ /* The 'J > 1' here is so that we don't place the global block into
+ the map. For CU with gaps, the static block will reflect the
+ gaps, while the global block will just reflect the full extent of
+ the range. */
+ for (int j = num_blocks; j > 1; )
+ {
+ --j;
+ struct block *b = blockvector->block (j);
+
+ gdb_assert (!b->is_global_block ());
+
+ if (b->is_contiguous ())
+ pending_addrmap.set_empty (b->start (), (b->end () - 1), b);
+ else
+ {
+ for (const auto &br : b->ranges ())
+ pending_addrmap.set_empty (br.start (), (br.end () - 1), b);
+ }
+ }
+
+ blockvector->set_map
+ (new (&m_objfile->objfile_obstack) addrmap_fixed
+ (&m_objfile->objfile_obstack, &pending_addrmap));
+ }
else
blockvector->set_map (nullptr);
diff --git a/gdb/buildsym.h b/gdb/buildsym.h
index 5d4b582349a..0ce460b0448 100644
--- a/gdb/buildsym.h
+++ b/gdb/buildsym.h
@@ -421,15 +421,10 @@ struct buildsym_compunit
struct subfile *m_current_subfile = nullptr;
- /* The mutable address map for the compilation unit whose symbols
- we're currently reading. The symtabs' shared blockvector will
- point to a fixed copy of this. */
- struct addrmap_mutable m_pending_addrmap;
-
- /* True if we recorded any ranges in the addrmap that are different
- from those in the blockvector already. We set this to false when
- we start processing a symfile, and if it's still false at the
- end, then we just toss the addrmap. */
+ /* If there are gaps in the address range of any block associated with
+ this buildsym_compunit, then we need to create an address map, this
+ flag is set true to indicate the addrmap must be created. If this
+ remains false, then no addrmap will be created. */
bool m_pending_addrmap_interesting = false;
/* An obstack used for allocating pending blocks. */
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv3 6/7] gdb: record block end addresses while parsing DIEs
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (4 preceding siblings ...)
2025-10-16 17:49 ` [PATCHv3 5/7] gdb: create address map after parsing all DIE Andrew Burgess
@ 2025-10-16 17:49 ` Andrew Burgess
2025-10-27 23:00 ` Tom Tromey
2025-10-16 17:49 ` [PATCHv3 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
2026-02-04 10:43 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
7 siblings, 1 reply; 37+ messages in thread
From: Andrew Burgess @ 2025-10-16 17:49 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Continuing to work towards the goal of improving GDB's ability to
debug optimised code, this commit stores a map from the end address
of a block (or a block's sub-range) to the block pointer. This
information is collected while parsing the DIEs.
This new map is required as a consequence of the previous commit. The
optimised code fix ups require that we can map from an address back to
a block, something that the address map was perfect for, but the
previous commit deferred building the address map until later on.
The problem is that the optimised code fixes in the next commit
require the address to block map, but also adjust block ranges, which
invalidates the address to block map. We could try to build the full
address to block early on, and then update it as the optimised code
fixes are performed, but this is expensive.
The solution I propose is to build a light weight, partial map, that
only holds the interesting (inline) blocks. This partial map is only
needed between reading the DIE and applying the optimised code fixes,
after which it is done with, and as a consequence we don't need to
update this map as the optimised code fixes adjust block ranges, this
makes the partial map cheaper.
This commit is all about building the new partial map. Currently,
nothing is done with this information; the information is recorded as
the block ranges are parsed, and then discarded after the line table
has been built. But in the next commit, this will be used to help
adjust the ranges of some inline blocks, and this will improve GDB's
ability to debug optimised code.
There should be no user visible changes after this commit.
---
gdb/dwarf2/cu.h | 7 +++++++
gdb/dwarf2/read.c | 8 ++++++++
2 files changed, 15 insertions(+)
diff --git a/gdb/dwarf2/cu.h b/gdb/dwarf2/cu.h
index 68010a060cc..6d16dd1512e 100644
--- a/gdb/dwarf2/cu.h
+++ b/gdb/dwarf2/cu.h
@@ -274,6 +274,13 @@ struct dwarf2_cu
return m_producer;
}
+ /* The end addresses for some inline blocks. For blocks with multiple
+ sub-ranges, this is the end address of every sub-range within the
+ block. These are the inclusive end addresses, that is, these are the
+ last addresses inside the block's ranges. Only the first block that
+ ends at any given address will be recorded. */
+ gdb::unordered_map<unrelocated_addr, struct block *> inline_block_ends;
+
private:
const char *m_producer = nullptr;
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 023cb07edd8..fe8b202c6b2 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -6153,6 +6153,10 @@ read_file_scope (struct die_info *die, struct dwarf2_cu *cu)
&& cu->line_header != nullptr)
dwarf_decode_lines (cu, unrel_low);
+ /* We no longer need to track the inline block end addresses. Release
+ memory associated with this. */
+ cu->inline_block_ends.clear ();
+
/* Decode macro information, if present. Dwarf 2 macro information
refers to information in the line number info statement program
header, so we can only read it if we've read the header
@@ -9952,6 +9956,10 @@ dwarf2_record_single_block_range (struct dwarf2_cu *cu, struct block *block,
CORE_ADDR low, CORE_ADDR high,
unrelocated_addr unrel_high)
{
+ /* If this is the end of an inline block, then record its end address. */
+ if (block->inlined_p () && block->function () != nullptr)
+ cu->inline_block_ends.insert ({unrel_high, block});
+
cu->get_builder ()->record_block_range (block, low, high);
}
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCHv3 7/7] gdb: fix-up truncated inline function block ranges
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (5 preceding siblings ...)
2025-10-16 17:49 ` [PATCHv3 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
@ 2025-10-16 17:49 ` Andrew Burgess
2026-02-04 10:43 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-10-16 17:49 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess, Bernd Edlinger
This commit aims to improve GDB's handling of inline functions. There
are two mechanisms which can tell GDB, or the user of GDB, that the
inferior is within an inline function, these are the block range
associated with an inline instance of a function, and also the line
table, which associates addresses with source lines in the program.
Currently, gcc truncates the address range for, at least some, inline
function blocks, such that a given address is considered outside the
inline function. However, the line table maps that same address to a
line within the inline function.
A consequence of this, is that, when using 'next' to move the inferior
forward, GDB will often stop the inferior believing that the inferior
has left an inline function, and indeed, GDB will claim that the
inferior is in the outer, non-inline function, but GDB will then
display a source line from the inline function as the current location.
An example of this problem can be seen with the test
gdb.cp/step-and-next-inline.exp. Using the
step-and-next-inline-no-header binary that is built as part of the
test:
(gdb) file ./gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Reading symbols from ./gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header...
(gdb) break get_alias_set
Breakpoint 1 at 0x401160: file /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc, line 51.
(gdb) run
Starting program: /tmp/build/gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Breakpoint 1, get_alias_set (t=t@entry=0x404038 <xx>)
at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:51
51 if (t != NULL
(gdb) next
52 && TREE_TYPE (t).z != 1
(gdb) next
43 return x; <------------- Problem line.
(gdb) bt
#0 get_alias_set (t=t@entry=0x404038 <xx>) at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:43
#1 0x000000000040105e in main () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:64
(gdb)
I've labelled the issue as 'Problem line'. After the second 'next'
GDB stopped thinking it was in get_alias_set, but printed a line from
the inline function behind the TREE_TYPE macro. The 'Problem line'
should have been line 53, not line 43.
The $pc at which GDB stopped is 0x40116f. If we then use 'objdump
--dwarf=decodedline' to view the line table for the executable, this
is what we see:
File name Line number Starting address View Stmt
...
step-and-next-inline.cc 38 0x401165 x
step-and-next-inline.cc 40 0x401165 1 x
step-and-next-inline.cc 40 0x401165 2
step-and-next-inline.cc 40 0x401167
step-and-next-inline.cc 42 0x40116f x
step-and-next-inline.cc 43 0x40116f 1 x
step-and-next-inline.cc 43 0x40116f 2
step-and-next-inline.cc 52 0x40116f 3
step-and-next-inline.cc 52 0x401172
step-and-next-inline.cc 38 0x401177 x
step-and-next-inline.cc 40 0x401177 1 x
step-and-next-inline.cc 40 0x401177 2
...
NOTE: I use objdump to view the line table, not 'maintenance info
line-table' as GDB drops some line table entries that it sees as
irrelevant. Using objdump give a complete view of the line table.
We can see that address 0x40116f is associated with three line
numbers, 42, and 43 are both within the inline function, and 52 is the
line from which the inline function was called. Notice too that 52 is
a non-statement line.
If we now look at the block structure for the previous $pc
value 0x40116e (i.e. $pc - 1), then we see this:
(gdb) maintenance info blocks 0x40116e
...
[(block *) 0x3c62900] 0x401040..0x4011a6
entry pc: 0x401160
function: get_alias_set(tree*)
symbol count: 1
address ranges:
0x401160..0x4011a6
0x401040..0x401046
[(block *) 0x3c61260] 0x401041..0x40116f
entry pc: 0x401165
inline function: tree_check(tree*, int)
symbol count: 4
address ranges:
0x401165..0x40116f
0x401041..0x401046
(gdb)
Here we see 'tree_check', the inline function that backs the TREE_TYPE
macro, this is the inline function we have just stepped out of. This
makes sense as the end-address for the tree_check block is 0x40116f,
and as the block's end address is not inclusive, that means that
0x40116f is the first address outside the block, which, is the current
$pc value.
And so, we can see what's going on. When the 'next' starts GDB is in
get_alias_set, GDB steps forward, entering tree_check. GDB then uses
the extent of the block to figure out where the inline function ends,
and steps forward to that address (0x40116f). At this point, GDB
looks up the current line in the line table (43), and reports a stop
at this line.
In this commit, the fix I propose is to look for the line table
pattern seen above, a sequence of line table entries, that end with a
non-statement entry for the calling line of an inline function,
located at the exact end address of an inline function block.
When such a pattern is found, then we can extend the inline function's
address range to the next line table address, so long as doing so does
not extend the inline function beyond the extent of the containing,
non-inline, function.
In the above example, the block for the tree_check function would be
extended to end at 0x401172. With this fix in place, and with the
same test binary, GDB now behaves like this:
(gdb) break get_alias_set
Breakpoint 1 at 0x401160: file /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc, line 51.
(gdb) run
Starting program: /tmp/build/gdb/testsuite/outputs/gdb.cp/step-and-next-inline/step-and-next-inline-no-header
Breakpoint 1, get_alias_set (t=t@entry=0x404038 <xx>)
at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.cp/step-and-next-inline.cc:51
51 if (t != NULL
(gdb) next
52 && TREE_TYPE (t).z != 1
(gdb) next
53 && TREE_TYPE (t).z != 2
(gdb)
The block for the inline function has been updated, like this:
(gdb) maintenance info blocks 0x40116e
...
[(block *) 0x4965530] 0x401040..0x4011a6
entry pc: 0x401160
function: get_alias_set(tree*)
symbol count: 1
address ranges:
0x401160..0x4011a6
0x401040..0x401046
[(block *) 0x49640b0] 0x401040..0x401172
entry pc: 0x401165
inline function: tree_check(tree*, int)
symbol count: 4
address ranges:
0x401165..0x401172
0x401040..0x401041
0x401041..0x401046
This is my alternative to the patch presented here:
https://inbox.sourceware.org/gdb-patches/AS1PR01MB946510286FBF2497A6F03E83E4922@AS1PR01MB9465.eurprd01.prod.exchangelabs.com/
This original patch series from Bernd Edlinger contains a number of
different fixes, some have already been split out and merged into GDB,
but the core idea for how to improve inline function handling by
extending the inline block range is the same, however, the mechanism
Bernd uses is significantly different.
In the above series, the approach taken is to mark the line table at
the end address of an inline function, and a few addresses beyond
that (see the is-weak flag in the above series). Then, when looking
up the block for a given address, if the address is within this marked
region, then we actually return the previous (inline function) block.
I believe that the above is a fair high-level summary of how the above
patch solves the inline function range problem. Any differences are
down to my misunderstanding the above patch, for which I apologise.
My problem with the above patch is that it breaks what I think should
be an invariant of GDB, that when looking up a block for a given
address, the block returned must contain the address within its
ranges. I feel that, if we start to break this invariant, then we
risk introducing bugs within, e.g. the stepping control code.
In contrast, my approach solves this problem during the DWARF parsing,
where problem cases are identified, and the DWARF "fixed" by extending
the block ranges. After this, no additional changes are needed in
GDB, the address to block mapping can work as normal, and the stepping
logic can continue to work just as it always has.
The test changes for gdb.cp/step-and-next-inline.exp have been taken
from Bernd's original patch series, and I've added a Co-Author tag for
Bernd to reflect this, as well as for the inspiration that I took from
his original series when creating this alternative proposal.
If/when this patch is merged, I plan to follow up with some cleanup to
the test case gdb.cp/step-and-next-inline.exp. I think this test
should really be moved to gdb.opt/, it's really testing optimisation
debug, not C++ features, but also the structure of the test file is a
bit of a mess. I think with some restructuring we could make the test
more readable, and also, maybe, test some additional compiler
flags (e.g. more optimisation levels). I've not done the refactoring
in this patch in order to make it clearer what new tests I've added,
and also, I want to leave the test similar to what's in Bernd's
original series, to make comparison easier.
The gdb.cp/step-and-next-inline.exp test was originally added by me
back in 2019, so the problems with it are of my own making.
For testing I've obviously run the entire test suite, but of
particular interest are these tests:
gdb.cp/step-and-next-inline.exp
gdb.dwarf2/dw2-inline-bt.exp
gdb.opt/empty-inline-cxx.exp
gdb.opt/empty-inline.exp
gdb.opt/inline-bt.exp
I've run these tests with a range of different gcc versions: 9.5.0,
10.5.0, 11.5.0, 12.2.0, 13.3.0, 14.2.0, 15.1.0. These tests all
relate to optimised debug of inline functions, and all passed with all
compiler versions listed here.
Co-Authored-By: Bernd Edlinger <bernd.edlinger@hotmail.de>
---
gdb/dwarf2/line-program.c | 81 +++
gdb/testsuite/gdb.cp/step-and-next-inline.exp | 38 +-
.../gdb.dwarf2/dw2-extend-inline-block.c | 78 +++
.../gdb.dwarf2/dw2-extend-inline-block.exp | 574 ++++++++++++++++++
4 files changed, 764 insertions(+), 7 deletions(-)
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
diff --git a/gdb/dwarf2/line-program.c b/gdb/dwarf2/line-program.c
index 40b570b70c0..5a8a27acedc 100644
--- a/gdb/dwarf2/line-program.c
+++ b/gdb/dwarf2/line-program.c
@@ -25,6 +25,7 @@
#include "complaints.h"
#include "filenames.h"
#include "gdbarch.h"
+#include "block.h"
static void
dwarf2_debug_line_missing_file_complaint ()
@@ -355,6 +356,79 @@ lnp_state_machine::finish_line ()
record_line_1 (0, LEF_IS_STMT);
}
+/* Look for an inline block that finishes at ORIGINAL_ADDRESS. If a block
+ is found, then search up the block hierarchy looking for a suitable
+ inline block to extend, a suitable block will be called from LINE. If a
+ block is found then update its end address to EXTENDED_ADDRESS. */
+
+static void
+dwarf_find_and_extend_inline_block_range (dwarf2_cu *cu,
+ unrelocated_addr original_address,
+ unrelocated_addr extended_address,
+ unsigned int line)
+{
+ /* Is there an inline block that ends at ORIGINAL_ADDRESS? */
+ auto it = cu->inline_block_ends.find (original_address);
+ if (it == cu->inline_block_ends.end ())
+ return;
+
+ /* Walk back up the block structure until we find an inline block that is
+ the child of a non-inline block. This is new inline block is our
+ candidate for extending. */
+ struct block *block = nullptr;
+ for (const struct block *b = it->second;
+ b != nullptr;
+ b = b->superblock ())
+ {
+ if (b->function () != nullptr && b->inlined_p ())
+ {
+ if (b->superblock () != nullptr
+ && b->superblock ()->function () != nullptr
+ && !b->superblock ()->inlined_p ())
+ {
+ block = const_cast<struct block *> (b);
+ break;
+ }
+ }
+ }
+
+ /* If we didn't find a block, or the block we found wasn't called from
+ the expected LINE, then we're done. Maybe we should try harder to
+ look for the block that matches LINE, but this would require us to
+ possibly extended more blocks, adding more complexity. Currently,
+ this works well enough for simple cases, we can possibly improve the
+ logic here later on. */
+ if (block == nullptr || block->function ()->line () != line)
+ return;
+
+ /* Sanity check. We should have an inline block, which should have a
+ valid super block. */
+ gdb_assert (block->inlined_p ());
+ gdb_assert (block->superblock () != nullptr);
+
+ CORE_ADDR extended_end = cu->per_objfile->relocate (extended_address);
+
+ /* The proposed new end of BLOCK is outside of the ranges of BLOCK's
+ superblock. If we tried to extend BLOCK then this would create an
+ invalid block structure; BLOCK would no longer be fully nested within
+ its superblock. Don't do that. */
+ if (extended_end > block->superblock ()->end ())
+ return;
+
+ CORE_ADDR original_end = cu->per_objfile->relocate (original_address);
+
+ /* Now find the range of BLOCK that ends at ORIGINAL_END, and extend it
+ out to EXTENDED_END. */
+ for (blockrange &br : block->ranges ())
+ {
+ if (br.end () == original_end)
+ br.set_end (extended_end);
+ }
+
+ if (block->end () == original_end)
+ block->set_end (extended_end);
+}
+
void
lnp_state_machine::record_line (bool end_sequence)
{
@@ -373,6 +447,13 @@ lnp_state_machine::record_line (bool end_sequence)
(end_sequence ? "\t(end sequence)" : ""));
}
+ if (m_address != m_last_address
+ && m_stmt_at_address
+ && m_cu->producer_is_gcc ()
+ && (m_flags & LEF_IS_STMT) == 0)
+ dwarf_find_and_extend_inline_block_range (m_cu, m_last_address,
+ m_address, m_line);
+
file_entry *fe = current_file ();
if (fe == NULL)
diff --git a/gdb/testsuite/gdb.cp/step-and-next-inline.exp b/gdb/testsuite/gdb.cp/step-and-next-inline.exp
index 3449b221798..015b8406609 100644
--- a/gdb/testsuite/gdb.cp/step-and-next-inline.exp
+++ b/gdb/testsuite/gdb.cp/step-and-next-inline.exp
@@ -24,13 +24,6 @@ if {[test_compiler_info gcc*] && ![supports_statement_frontiers] } {
proc do_test { use_header } {
global srcfile testfile
- if { $use_header } {
- # This test will not pass due to poor debug information
- # generated by GCC (at least up to 10.x). See
- # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94474
- return
- }
-
set options {c++ debug nowarnings optimize=-O2}
if { [supports_statement_frontiers] } {
lappend options additional_flags=-gstatement-frontiers
@@ -198,6 +191,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 1 pass 2"
gdb_test "step" ".*TREE_TYPE.*" "step 4"
gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
"not in inline 2 pass 2"
@@ -205,6 +200,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 2 pass 2"
gdb_test "step" ".*TREE_TYPE.*" "step 7"
gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
"not in inline 3 pass 2"
@@ -212,6 +209,8 @@ proc do_test { use_header } {
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 "bt" "\\s*\\#0\\s+\[^\r\]*tree_check\[^\r\]*${hdrfile}:.*" \
+ "return from inline 3 pass 2"
gdb_test "step" "return 0.*" "step 10"
gdb_test "bt" \
"\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
@@ -255,6 +254,31 @@ proc do_test { use_header } {
gdb_test "bt" "#0\\s+\[^\r\n\]*tree_check\[^\r\n\]*${hdrfile}:.*" \
"abort from inline 1 pass 3"
}
+
+ clean_restart ${executable}
+
+ if ![runto_main] {
+ return
+ }
+
+ gdb_test "bt" "\\s*\\#0\\s+main.*" "in main pass 4"
+ gdb_test "skip tree_check" ".*" "skip tree_check pass 4"
+ gdb_test "step" ".*" "step into get_alias_set pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "in get_alias_set pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 1 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 1 pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 2 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 2 pass 4"
+ gdb_test "step" ".*TREE_TYPE.*" "step 3 pass 4"
+ gdb_test "bt" "\\s*\\#0\\s+get_alias_set\[^\r\]*${srcfile}:.*" \
+ "not in inline 3 pass 4"
+ gdb_test "step" "return 0.*" "step 4 pass 4"
+ gdb_test "bt" \
+ "\\s*\\#0\\s+(main|get_alias_set)\[^\r\]*${srcfile}:.*" \
+ "not in inline 4 pass 4"
}
}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
new file mode 100644
index 00000000000..d6becf5d66b
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.c
@@ -0,0 +1,78 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 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;
+
+/* The follow code exists only to be referenced from the generated line
+ table. */
+#if 0
+static inline void
+foo (void)
+{
+ /* foo:1 */
+ /* foo:2 */
+ /* foo:3 */
+}
+
+int
+main (void)
+{ /* main decl line */
+ /* main:1 */
+ /* main:2 */
+ /* main:3 */ foo (); /* foo call line */
+ /* main:4 */
+ /* main:5 */
+ /* main:6 */
+}
+#endif
+
+
+int
+main (void)
+{
+ asm ("main_label: .globl main_label");
+ ++global_var;
+
+ asm ("main_0: .globl main_0");
+ ++global_var;
+
+ asm ("main_1: .globl main_1");
+ ++global_var;
+
+ 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_8");
+ ++global_var;
+
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
new file mode 100644
index 00000000000..47b8eb1be52
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-extend-inline-block.exp
@@ -0,0 +1,574 @@
+# Copyright 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/>.
+
+# When compiling optimised code, GCC will sometimes truncate the address
+# range of an inline function, usually by a single instruction.
+#
+# It is possible to detect when this has happened by looking at the line
+# table, GCC will create two non-statement line table entries associated
+# with the call-line of the inline function, but the end address of the
+# inline function will be set to be the address of the first of these line
+# table entries.
+#
+# The problem here is that block end addresses are not inclusive, which
+# means the block ends before either of these line table entries.
+#
+# What we find is that we get a better debug experience if we extend the
+# inline function to actually end at the second line table entry, that is
+# the first line table entry becomes part of the inline function, while the
+# second entry remains outside the inline function.
+#
+# This test tries to create this situation using the DWARF assembler, and
+# then checks that GDB correctly extends the inline function to include the
+# first line table entry.
+
+load_lib dwarf.exp
+
+require dwarf2_support
+
+standard_testfile .c
+
+# Lines numbers we reference in the generated DWARF.
+set main_decl_line [gdb_get_line_number "main decl line"]
+set main_line_1 [gdb_get_line_number "main:1"]
+set main_line_4 [gdb_get_line_number "main:4"]
+set foo_call_line [gdb_get_line_number "foo call line"]
+set foo_line_1 [gdb_get_line_number "foo:1"]
+
+get_func_info main
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with a contiguous address range that needs extending.
+
+proc build_dwarf_for_contiguous_block { asm_file } {
+ Dwarf::assemble $asm_file {
+ declare_labels lines_table inline_func
+
+ cu { } {
+ DW_TAG_compile_unit {
+ DW_AT_producer "GNU C 14.1.0"
+ DW_AT_language @DW_LANG_C
+ DW_AT_name $::srcfile
+ DW_AT_comp_dir /tmp
+ DW_AT_low_pc 0 addr
+ DW_AT_stmt_list $lines_table DW_FORM_sec_offset
+ } {
+ inline_func: subprogram {
+ DW_AT_name foo
+ DW_AT_inline @DW_INL_declared_inlined
+ }
+ subprogram {
+ DW_AT_name main
+ DW_AT_decl_file 1 data1
+ DW_AT_decl_line $::main_decl_line data1
+ DW_AT_decl_column 1 data1
+ DW_AT_low_pc $::main_start addr
+ DW_AT_high_pc $::main_len data4
+ DW_AT_external 1 flag
+ } {
+ inlined_subroutine {
+ DW_AT_abstract_origin %$inline_func
+ DW_AT_call_file 1 data1
+ DW_AT_call_line $::foo_call_line data1
+ DW_AT_low_pc main_1 addr
+ DW_AT_high_pc main_3 addr
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+ }
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_contiguous_block'.
+
+proc check_contiguous_block {} {
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_4" "*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 expected content"
+}
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with two ranges, and it is the first range that needs extending.
+
+proc build_dwarf_for_first_block_range { asm_file dwarf_version } {
+ Dwarf::assemble $asm_file {
+ upvar dwarf_version dwarf_version
+ declare_labels lines_table inline_func ranges_label
+
+ cu { version $dwarf_version } {
+ DW_TAG_compile_unit {
+ DW_AT_producer "GNU C 14.1.0"
+ DW_AT_language @DW_LANG_C
+ DW_AT_name $::srcfile
+ DW_AT_comp_dir /tmp
+ DW_AT_low_pc 0 addr
+ DW_AT_stmt_list $lines_table DW_FORM_sec_offset
+ } {
+ inline_func: subprogram {
+ DW_AT_name foo
+ DW_AT_inline @DW_INL_declared_inlined
+ }
+ subprogram {
+ DW_AT_name main
+ DW_AT_decl_file 1 data1
+ DW_AT_decl_line $::main_decl_line data1
+ DW_AT_decl_column 1 data1
+ DW_AT_low_pc $::main_start addr
+ DW_AT_high_pc $::main_len data4
+ DW_AT_external 1 flag
+ } {
+ inlined_subroutine {
+ DW_AT_abstract_origin %$inline_func
+ DW_AT_call_file 1 data1
+ DW_AT_call_line $::foo_call_line data1
+ DW_AT_ranges $ranges_label DW_FORM_sec_offset
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+
+ if { $dwarf_version == 5 } {
+ rnglists {} {
+ table {} {
+ ranges_label: list_ {
+ start_end main_1 main_3
+ start_end main_7 main_8
+ }
+ }
+ }
+ } else {
+ ranges { } {
+ ranges_label: sequence {
+ range main_1 main_3
+ range main_7 main_8
+ }
+ }
+ }
+ }
+}
+
+# Wrapper around 'build_dwarf_for_first_block_range', creates DWARF 4 range
+# information.
+
+proc build_dwarf_for_first_block_range_4 { asm_file } {
+ build_dwarf_for_first_block_range $asm_file 4
+}
+
+# Wrapper around 'build_dwarf_for_first_block_range', creates DWARF 5 range
+# information.
+
+proc build_dwarf_for_first_block_range_5 { asm_file } {
+ build_dwarf_for_first_block_range $asm_file 5
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_first_block_range'.
+
+proc check_for_block_ranges_1 {} {
+
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_8" "*UNKNOWN*" \
+ "get address of foo end"]
+
+ set main_4 [get_hexadecimal_valueof "&main_4" "*UNKNOWN*" \
+ "get address of main_4 label"]
+ set main_7 [get_hexadecimal_valueof "&main_7" "*UNKNOWN*" \
+ "get address of main_7 label"]
+
+ gdb_test "maintenance info blocks" \
+ [multi_line \
+ "\\\[\\(block \\*\\) $::hex\\\] $foo_start\\.\\.$foo_end" \
+ " entry pc: $foo_start" \
+ " inline function: foo" \
+ " symbol count: $::decimal" \
+ " address ranges:" \
+ " $foo_start\\.\\.$main_4" \
+ " $main_7\\.\\.$foo_end"] \
+ "block for foo has expected content"
+}
+
+# Create DWARF for the test. In this case, inline function 'foo' is created
+# with two ranges, and it is the second range that needs extending.
+
+proc build_dwarf_for_last_block_range { asm_file dwarf_version } {
+ Dwarf::assemble $asm_file {
+ upvar dwarf_version dwarf_version
+ declare_labels lines_table inline_func ranges_label
+
+ cu { version $dwarf_version } {
+ DW_TAG_compile_unit {
+ DW_AT_producer "GNU C 14.1.0"
+ DW_AT_language @DW_LANG_C
+ DW_AT_name $::srcfile
+ DW_AT_comp_dir /tmp
+ DW_AT_low_pc 0 addr
+ DW_AT_stmt_list $lines_table DW_FORM_sec_offset
+ } {
+ inline_func: subprogram {
+ DW_AT_name foo
+ DW_AT_inline @DW_INL_declared_inlined
+ }
+ subprogram {
+ DW_AT_name main
+ DW_AT_decl_file 1 data1
+ DW_AT_decl_line $::main_decl_line data1
+ DW_AT_decl_column 1 data1
+ DW_AT_low_pc $::main_start addr
+ DW_AT_high_pc $::main_len data4
+ DW_AT_external 1 flag
+ } {
+ inlined_subroutine {
+ DW_AT_abstract_origin %$inline_func
+ DW_AT_call_file 1 data1
+ DW_AT_call_line $::foo_call_line data1
+ DW_AT_ranges $ranges_label DW_FORM_sec_offset
+ DW_AT_entry_pc main_1 addr
+ }
+ }
+ }
+ }
+
+ lines {version 2 default_is_stmt 1} lines_table {
+ include_dir "$::srcdir/$::subdir"
+ file_name "$::srcfile" 1
+
+ program {
+ DW_LNE_set_address main
+ line $::main_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_0
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_1
+ line $::foo_line_1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_2
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_3
+ line $::foo_call_line
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_4
+ DW_LNS_copy
+
+ DW_LNE_set_address main_5
+ DW_LNS_advance_line 1
+ DW_LNS_negate_stmt
+ DW_LNS_copy
+
+ DW_LNE_set_address main_6
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_7
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address main_8
+ DW_LNS_advance_line 1
+ DW_LNS_copy
+
+ DW_LNE_set_address "$::main_start + $::main_len"
+ DW_LNE_end_sequence
+ }
+ }
+
+ if { $dwarf_version == 5 } {
+ rnglists {} {
+ table {} {
+ ranges_label: list_ {
+ start_end main_7 main_8
+ start_end main_1 main_3
+ }
+ }
+ }
+ } else {
+ ranges { } {
+ ranges_label: sequence {
+ range main_7 main_8
+ range main_1 main_3
+ }
+ }
+ }
+ }
+}
+
+# Wrapper around 'build_dwarf_for_last_block_range', creates DWARF 4 range
+# information.
+
+proc build_dwarf_for_last_block_range_4 { asm_file } {
+ build_dwarf_for_last_block_range $asm_file 4
+}
+
+# Wrapper around 'build_dwarf_for_last_block_range', creates DWARF 5 range
+# information.
+
+proc build_dwarf_for_last_block_range_5 { asm_file } {
+ build_dwarf_for_last_block_range $asm_file 5
+}
+
+# Assuming GDB is stopped at the entry $pc for 'foo', use 'maint info
+# blocks' to check the block for 'foo' is correct. This function checks
+# 'foo' created by 'build_dwarf_for_last_block_range'.
+
+proc check_for_block_ranges_2 {} {
+
+ set foo_start [get_hexadecimal_valueof "&main_1" "*UNKNOWN*" \
+ "get address of foo start"]
+ set foo_end [get_hexadecimal_valueof "&main_8" "*UNKNOWN*" \
+ "get address of foo end"]
+
+ set main_4 [get_hexadecimal_valueof "&main_4" "*UNKNOWN*" \
+ "get address of main_4 label"]
+ set main_7 [get_hexadecimal_valueof "&main_7" "*UNKNOWN*" \
+ "get address of main_7 label"]
+
+ gdb_test "maintenance info blocks" \
+ [multi_line \
+ "\\\[\\(block \\*\\) $::hex\\\] $foo_start\\.\\.$foo_end" \
+ " entry pc: $foo_start" \
+ " inline function: foo" \
+ " symbol count: $::decimal" \
+ " address ranges:" \
+ " $main_7\\.\\.$foo_end" \
+ " $foo_start\\.\\.$main_4"] \
+ "block for foo has expected content"
+}
+
+# Buidl ASM_FILE, along with the global SRCFILE into an executable called
+# TESTFILE. Place a breakpoint in 'foo', run to the breakpoint, and use
+# BLOCK_CHECK_FUNC to ensure the block for 'foo' is correct.
+#
+# Then step through 'foo' and back into 'main'.
+
+proc run_test { asm_file testfile block_check_func } {
+ if {[prepare_for_testing "failed to prepare" $testfile \
+ [list $::srcfile $asm_file] {nodebug}]} {
+ return
+ }
+
+ if {![runto_main]} {
+ return
+ }
+
+ gdb_breakpoint foo
+ gdb_test "continue" \
+ [multi_line \
+ "Breakpoint $::decimal, foo \\(\\) \[^\r\n\]+:$::foo_line_1" \
+ "$::foo_line_1\\s+/\\* foo:1 \\*/"] \
+ "continue to b/p in foo"
+
+ # Check that the block for `foo` has been extended.
+ $block_check_func
+
+ 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"
+
+ gdb_test "step" \
+ "^[expr $::foo_line_1 + 1]\\s+/\\* foo:2 \\*/" \
+ "step to second line of foo"
+
+ gdb_test "step" \
+ "^[expr $::foo_line_1 + 2]\\s+/\\* foo:3 \\*/" \
+ "step to third line of foo"
+
+ gdb_test "step" \
+ [multi_line \
+ "^main \\(\\) at \[^\r\n\]+:$::main_line_4" \
+ "$::main_line_4\\s+/\\* main:4 \\*/"] \
+ "set back to main"
+
+ gdb_test "step" \
+ "^[expr $::main_line_4 + 1]\\s+/\\* main:5 \\*/" \
+ "step again in main"
+}
+
+# Test specifications, items are:
+# 1. Prefix string used to describe the test.
+# 2. Proc to call that builds the DWARF.
+# 3. Proc to call that runs 'maint info blocks' when stopped at the entry
+# $pc for 'foo' (the inline function), and checks that the block details
+# for 'foo' are correct.
+set test_list \
+ [list \
+ [list "block with ranges, extend first range, dwarf 4" \
+ build_dwarf_for_first_block_range_4 \
+ check_for_block_ranges_1] \
+ [list "block with ranges, extend first range, dwarf 5" \
+ build_dwarf_for_first_block_range_5 \
+ check_for_block_ranges_1] \
+ [list "block with ranges, extend last range, dwarf 4" \
+ build_dwarf_for_last_block_range_4 \
+ check_for_block_ranges_2] \
+ [list "block with ranges, extend last range, dwarf 5" \
+ build_dwarf_for_last_block_range_4 \
+ check_for_block_ranges_2] \
+ [list "contiguous block" \
+ build_dwarf_for_contiguous_block \
+ check_contiguous_block] \
+ ]
+
+# Run all the tests.
+set suffix 0
+foreach test_spec $test_list {
+ incr suffix
+
+ set prefix [lindex $test_spec 0]
+ set build_dwarf_func [lindex $test_spec 1]
+ set check_block_func [lindex $test_spec 2]
+
+ with_test_prefix $prefix {
+ set asm_file [standard_output_file ${testfile}-${suffix}.S]
+ $build_dwarf_func $asm_file
+ run_test $asm_file ${testfile}-${suffix} $check_block_func
+ }
+}
--
2.47.1
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 1/7] gdb: improve line number lookup around inline functions
2025-10-16 17:49 ` [PATCHv3 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
@ 2025-10-27 22:22 ` Tom Tromey
2025-12-17 14:32 ` Andrew Burgess
0 siblings, 1 reply; 37+ messages in thread
From: Tom Tromey @ 2025-10-27 22:22 UTC (permalink / raw)
To: Andrew Burgess; +Cc: gdb-patches
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
Andrew> So my proposal is that the backward scan of the line table looking for
Andrew> an is-stmt entry should not be performed when NOTCURRENT is true. In
Andrew> the case above this means we will report the entry at index 9, which
Andrew> is for line 10, which is correct.
Thanks for the explanation. It all made sense to me.
Andrew> + if ![runto foo] {
We're bracing if conditions these days.
Otherwise this looks good to me.
Approved-By: Tom Tromey <tom@tromey.com>
Tom
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 4/7] gdb: move block range recording into its own function
2025-10-16 17:49 ` [PATCHv3 4/7] gdb: move block range recording into its own function Andrew Burgess
@ 2025-10-27 22:45 ` Tom Tromey
0 siblings, 0 replies; 37+ messages in thread
From: Tom Tromey @ 2025-10-27 22:45 UTC (permalink / raw)
To: Andrew Burgess; +Cc: gdb-patches
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
Andrew> And so, I'd like to create a single common function where block ranges
Andrew> are recorded, in a later commit I can then hook into this function to
Andrew> record the block's end address(es). This commit sets up this single
Andrew> common function.
Looks good.
Approved-By: Tom Tromey <tom@tromey.com>
Tom
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 5/7] gdb: create address map after parsing all DIE
2025-10-16 17:49 ` [PATCHv3 5/7] gdb: create address map after parsing all DIE Andrew Burgess
@ 2025-10-27 22:56 ` Tom Tromey
2026-01-02 16:36 ` Andrew Burgess
0 siblings, 1 reply; 37+ messages in thread
From: Tom Tromey @ 2025-10-27 22:56 UTC (permalink / raw)
To: Andrew Burgess; +Cc: gdb-patches
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
Andrew> Continuing the work done in the last two commits, this commit defers
Andrew> building the addrmap for a blockvector until after all the DIE have
Andrew> been read, and the line table processed.
Andrew> The benefit of this is that any changes to a block's ranges done
Andrew> during line table processing (see the next commit) will be reflected
Andrew> in the blockvector's addrmap.
Andrew> The alternative to this is to build the addrmap as we initially see
Andrew> each block, but then adjust the addrmap if we later decide to modify a
Andrew> block. I think defering the addrmap creation is cleaner, and is less
Andrew> work overall.
Andrew> The addrmap requires that we add the most inner blocks first. I
Andrew> achieve this by walking the blockvector backward, as we always add
Andrew> parent blocks before their more inner child blocks.
I wonder if this is guaranteed to be correct. Like, does gdb cope
properly if a compiler happens to emit multiple lexical scopes that each
have non-contiguous ranges, and then where the ranges happen to overlap.
However I think your patch probably does not make gdb worse in this
regard.
Andrew> @@ -410,8 +410,6 @@ buildsym_compunit::record_block_range (struct block *block,
Andrew> if (start != block->start ()
Andrew> || end_inclusive + 1 != block->end ())
Andrew> m_pending_addrmap_interesting = true;
Andrew> -
Andrew> - m_pending_addrmap.set_empty (start, end_inclusive, block);
Andrew> }
I think the comment before this method and the comment in the method
both need to be updated, as this doesn't actually record the range any
more.
I wonder why buildsym even tries to see if the pending addrmap might be
interesting. It seems to me that a fixed addrmap is basically the same
data structure as the blockvector: it maps addresses to blocks and uses
a binary search to find the correct entry.
So I'm wondering if, in the longer term -- you definitely don't need to
to this here -- if we should just switch to a single representation.
Furthermore I was wondering if it makes sense for blocks to even track
their ranges. That is, for a block with multiple ranges, could we just
have multiple entries in the blockvector and get rid of block::m_ranges?
FWIW I've been looking into this area a bit while experimenting with
lazy CU expansion. There I think I want to make it so blockvectors are
always expandable, so no fixed addrmap at least -- and mutable addrmaps
have some issues with updating, so probably moving to just having a
vector.
Anyway this seems fine with the comment cleanup.
Approved-By: Tom Tromey <tom@tromey.com>
Tom
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 6/7] gdb: record block end addresses while parsing DIEs
2025-10-16 17:49 ` [PATCHv3 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
@ 2025-10-27 23:00 ` Tom Tromey
0 siblings, 0 replies; 37+ messages in thread
From: Tom Tromey @ 2025-10-27 23:00 UTC (permalink / raw)
To: Andrew Burgess; +Cc: gdb-patches
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
Andrew> The solution I propose is to build a light weight, partial map, that
Andrew> only holds the interesting (inline) blocks. This partial map is only
Andrew> needed between reading the DIE and applying the optimised code fixes,
Andrew> after which it is done with, and as a consequence we don't need to
Andrew> update this map as the optimised code fixes adjust block ranges, this
Andrew> makes the partial map cheaper.
This would also affect lazy expansion, I guess. There I'd like to at
least have function bodies be expanded on demand. So the handling would
have to be done on a per-function basis.
I wonder if it would make sense for me to apply that transform sooner
rather than later. Like:
Andrew> + /* The end addresses for some inline blocks. For blocks with multiple
Andrew> + sub-ranges, this is the end address of every sub-range within the
Andrew> + block. These are the inclusive end addresses, that is, these are the
Andrew> + last addresses inside the block's ranges. Only the first block that
Andrew> + ends at any given address will be recorded. */
Andrew> + gdb::unordered_map<unrelocated_addr, struct block *> inline_block_ends;
... turn this into a pointer and then use a scoped_restore to set/reset
it when reading a function scope.
This idea makes me wonder what happens if a nested function is processed
while reading an outer function. But I suppose it is "nothing" because
an inline function's end address for outer function F shouldn't matter
to inner function F.I.
Approved-By: Tom Tromey <tom@tromey.com>
Tom
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 1/7] gdb: improve line number lookup around inline functions
2025-10-27 22:22 ` Tom Tromey
@ 2025-12-17 14:32 ` Andrew Burgess
2025-12-17 14:48 ` Tom de Vries
0 siblings, 1 reply; 37+ messages in thread
From: Andrew Burgess @ 2025-12-17 14:32 UTC (permalink / raw)
To: Tom Tromey; +Cc: gdb-patches
Tom Tromey <tom@tromey.com> writes:
>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>
> Andrew> So my proposal is that the backward scan of the line table looking for
> Andrew> an is-stmt entry should not be performed when NOTCURRENT is true. In
> Andrew> the case above this means we will report the entry at index 9, which
> Andrew> is for line 10, which is correct.
>
> Thanks for the explanation. It all made sense to me.
>
> Andrew> + if ![runto foo] {
>
> We're bracing if conditions these days.
>
> Otherwise this looks good to me.
>
> Approved-By: Tom Tromey <tom@tromey.com>
I fixed the TCL bracing issue and pushed just this patch for now. I'm
taking a deeper look at some of the questions you raised on the later
patches.
Thanks,
Andrew
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 1/7] gdb: improve line number lookup around inline functions
2025-12-17 14:32 ` Andrew Burgess
@ 2025-12-17 14:48 ` Tom de Vries
2025-12-18 14:46 ` Andrew Burgess
0 siblings, 1 reply; 37+ messages in thread
From: Tom de Vries @ 2025-12-17 14:48 UTC (permalink / raw)
To: Andrew Burgess, Tom Tromey; +Cc: gdb-patches
On 12/17/25 3:32 PM, Andrew Burgess wrote:
> Tom Tromey <tom@tromey.com> writes:
>
>>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>>
>> Andrew> So my proposal is that the backward scan of the line table looking for
>> Andrew> an is-stmt entry should not be performed when NOTCURRENT is true. In
>> Andrew> the case above this means we will report the entry at index 9, which
>> Andrew> is for line 10, which is correct.
>>
>> Thanks for the explanation. It all made sense to me.
>>
>> Andrew> + if ![runto foo] {
>>
>> We're bracing if conditions these days.
>>
>> Otherwise this looks good to me.
>>
>> Approved-By: Tom Tromey <tom@tromey.com>
>
> I fixed the TCL bracing issue and pushed just this patch for now. I'm
> taking a deeper look at some of the questions you raised on the later
> patches.
Hi,
I'm getting:
...
$ pre-commit run --all-files
black....................................................................Passed
flake8...................................................................Passed
isort....................................................................Passed
codespell................................................................Passed
check-include-guards.....................................................Passed
check-gnu-style..........................................................Passed
- hook id: check-gnu-style
- duration: 0.19s
check-whitespace.........................................................Passed
pre-commit-setup.........................................................Passed
tclint...................................................................Failed
- hook id: tclint
- exit code: 1
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp:211:26: expression with
substitutions should be enclosed by braces [unbraced-expr]
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp:212:26: expression with
substitutions should be enclosed by braces [unbraced-expr]
...
Thanks,
- Tom
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 1/7] gdb: improve line number lookup around inline functions
2025-12-17 14:48 ` Tom de Vries
@ 2025-12-18 14:46 ` Andrew Burgess
0 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2025-12-18 14:46 UTC (permalink / raw)
To: Tom de Vries, Tom Tromey; +Cc: gdb-patches
Tom de Vries <tdevries@suse.de> writes:
> On 12/17/25 3:32 PM, Andrew Burgess wrote:
>> Tom Tromey <tom@tromey.com> writes:
>>
>>>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>>>
>>> Andrew> So my proposal is that the backward scan of the line table looking for
>>> Andrew> an is-stmt entry should not be performed when NOTCURRENT is true. In
>>> Andrew> the case above this means we will report the entry at index 9, which
>>> Andrew> is for line 10, which is correct.
>>>
>>> Thanks for the explanation. It all made sense to me.
>>>
>>> Andrew> + if ![runto foo] {
>>>
>>> We're bracing if conditions these days.
>>>
>>> Otherwise this looks good to me.
>>>
>>> Approved-By: Tom Tromey <tom@tromey.com>
>>
>> I fixed the TCL bracing issue and pushed just this patch for now. I'm
>> taking a deeper look at some of the questions you raised on the later
>> patches.
>
> Hi,
>
> I'm getting:
> ...
> $ pre-commit run --all-files
> black....................................................................Passed
> flake8...................................................................Passed
> isort....................................................................Passed
> codespell................................................................Passed
> check-include-guards.....................................................Passed
> check-gnu-style..........................................................Passed
> - hook id: check-gnu-style
> - duration: 0.19s
> check-whitespace.........................................................Passed
> pre-commit-setup.........................................................Passed
> tclint...................................................................Failed
> - hook id: tclint
> - exit code: 1
>
> gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp:211:26: expression with
> substitutions should be enclosed by braces [unbraced-expr]
> gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp:212:26: expression with
> substitutions should be enclosed by braces [unbraced-expr]
Sorry for the breakage. I pushed the patch below to address this.
Thanks,
Andrew
--
commit e3b02794ca17af6c12780ba32cb38c98e152a903
Author: Andrew Burgess <aburgess@redhat.com>
Date: Thu Dec 18 13:30:38 2025 +0000
gdb/testsuite: fix tclint errors
After commit:
commit b5160e2ee6a0192389caf7acee1fc32961ed29f4 (upstream/master, upstream/HEAD, gdb-tmp-g)
Date: Fri Jul 26 16:32:33 2024 +0100
gdb: improve line number lookup around inline functions
the following tclint errors were reported:
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp:211:26: expression with substitutions should be enclosed by braces [unbraced-expr]
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp:212:26: expression with substitutions should be enclosed by braces [unbraced-expr]
Fixed by this commit.
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp b/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp
index cf66897cd70..77a18918eb1 100644
--- a/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp
+++ b/gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp
@@ -208,8 +208,8 @@ proc do_test { } {
# 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]
+ 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)" \
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 5/7] gdb: create address map after parsing all DIE
2025-10-27 22:56 ` Tom Tromey
@ 2026-01-02 16:36 ` Andrew Burgess
2026-01-05 20:03 ` Tom Tromey
0 siblings, 1 reply; 37+ messages in thread
From: Andrew Burgess @ 2026-01-02 16:36 UTC (permalink / raw)
To: Tom Tromey; +Cc: gdb-patches
Hi Tom,
Thanks for taking a look at these patches. I wanted to follow up on
some of your thoughts...
Tom Tromey <tom@tromey.com> writes:
>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>
> Andrew> Continuing the work done in the last two commits, this commit defers
> Andrew> building the addrmap for a blockvector until after all the DIE have
> Andrew> been read, and the line table processed.
>
> Andrew> The benefit of this is that any changes to a block's ranges done
> Andrew> during line table processing (see the next commit) will be reflected
> Andrew> in the blockvector's addrmap.
>
> Andrew> The alternative to this is to build the addrmap as we initially see
> Andrew> each block, but then adjust the addrmap if we later decide to modify a
> Andrew> block. I think defering the addrmap creation is cleaner, and is less
> Andrew> work overall.
>
> Andrew> The addrmap requires that we add the most inner blocks first. I
> Andrew> achieve this by walking the blockvector backward, as we always add
> Andrew> parent blocks before their more inner child blocks.
>
> I wonder if this is guaranteed to be correct. Like, does gdb cope
> properly if a compiler happens to emit multiple lexical scopes that each
> have non-contiguous ranges, and then where the ranges happen to overlap.
I had a play with some examples using DW_TAG_lexical_block to see how
things change.
If things are laid out in what I'd consider a "sane" way, with nested
blocks being nested within the DWARF, then you'll end up with the same
block structure before and after this patch, e.g.:
DW_TAG_lexical_block {
...
} {
DW_TAG_variable {
DW_AT_name "var_a"
...
}
DW_TAG_lexical_block {
...
} {
DW_TAG_variable {
DW_AT_name "var_b"
...
}
}
}
In this example the block containing 'var_a' is added to the blockvector
before the block holding 'var_b', so when we walk the blockvector
backwards, we see the 'var_b' block first, and everything is fine.
If we change the test to be structured like this instead:
DW_TAG_lexical_block {
...
} {
DW_TAG_variable {
DW_AT_name "var_a"
....
}
}
DW_TAG_lexical_block {
...
} {
DW_TAG_variable {
DW_AT_name "var_b"
...
}
}
Now the two lexical blocks are siblings, but if we make the second block
overlap the first then this patch does cause problems.
Current GDB will see the 'var_a' block first and add this to the map,
then when GDB sees the 'var_b' block, only the addresses not claimed by
the 'var_a' block will be allocated to the second lexical block.
With this patch blocks are now processed in reverse order, so we see the
'var_b' block first and assign addresses to this, then any unassigned
addresses are assigned to the 'var_a' block.
This DOES change GDB's behaviour.
> However I think your patch probably does not make gdb worse in this
> regard.
This is what I think. In the sibling block case it's not clear what the
expected behaviour would actually be? In current GDB you'll only see
'var_b' at those addresses not covered by the 'var_a' block. While in
patched GDB the situation is reveresed, it's 'var_a' that's missing in
some cases.
In both cases there are addresses where we can only see one of the
variables. But it isn't 100% clear too me if this is really fixable at
all with our current block hierarchy approach.
Is this sibling case the type of case you were thinking of? Or have I
got the wrong end of the stick here?
>
> Andrew> @@ -410,8 +410,6 @@ buildsym_compunit::record_block_range (struct block *block,
> Andrew> if (start != block->start ()
> Andrew> || end_inclusive + 1 != block->end ())
> Andrew> m_pending_addrmap_interesting = true;
> Andrew> -
> Andrew> - m_pending_addrmap.set_empty (start, end_inclusive, block);
> Andrew> }
>
> I think the comment before this method and the comment in the method
> both need to be updated, as this doesn't actually record the range any
> more.
Yeah. But reading this code again I think I can drop record_block_range
completely now. Blocks are basically interesting if they are
non-contiguous, and buildsym_compunit::make_blockvector, which is the
only place that checks m_pending_addrmap_interesting already has this
code:
/* Count the length of the list of blocks. */
for (next = m_pending_blocks, i = 0; next; next = next->next, i++)
{
}
Where we could check for any non-contiguous blocks. This would mean GDB
starts using the map instead of the vector for the case where a block
uses DW_AT_ranges, but only has a single range. This shouldn't be a
user visible change, but I guess would increase memory use a little.
Still, I expect this would not be a common case as most compilers seem
to optimise to low/high pc attributes for contiguous blocks.
> I wonder why buildsym even tries to see if the pending addrmap might be
> interesting. It seems to me that a fixed addrmap is basically the same
> data structure as the blockvector: it maps addresses to blocks and uses
> a binary search to find the correct entry.
Sure, but I wonder if this is a memory use thing? The addrmap must use
more memory than the vector, so maybe for large programs this would be a
noticable hit.
>
> So I'm wondering if, in the longer term -- you definitely don't need to
> to this here -- if we should just switch to a single representation.
>
> Furthermore I was wondering if it makes sense for blocks to even track
> their ranges. That is, for a block with multiple ranges, could we just
> have multiple entries in the blockvector and get rid of block::m_ranges?
There are a few places where we use the block's ranges for useful work,
these would all need to be rewritten to get the same information from
the blockvector (really the addrmap within the blockvector).
>
> FWIW I've been looking into this area a bit while experimenting with
> lazy CU expansion. There I think I want to make it so blockvectors are
> always expandable, so no fixed addrmap at least -- and mutable addrmaps
> have some issues with updating, so probably moving to just having a
> vector.
Wouldn't that cause problems for non-contiguous blocks? Isn't that the
problem that the addrmap was added to solve?
I'm going to look at updating to remove record_block_range completely,
and test that. But I'd love to hear back about the lexical block
testing I did.
thanks,
Andrew
>
> Anyway this seems fine with the comment cleanup.
> Approved-By: Tom Tromey <tom@tromey.com>
>
> Tom
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 5/7] gdb: create address map after parsing all DIE
2026-01-02 16:36 ` Andrew Burgess
@ 2026-01-05 20:03 ` Tom Tromey
2026-01-05 21:37 ` Andrew Burgess
0 siblings, 1 reply; 37+ messages in thread
From: Tom Tromey @ 2026-01-05 20:03 UTC (permalink / raw)
To: Andrew Burgess; +Cc: Tom Tromey, gdb-patches
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
Andrew> The addrmap requires that we add the most inner blocks first. I
Andrew> achieve this by walking the blockvector backward, as we always add
Andrew> parent blocks before their more inner child blocks.
>> I wonder if this is guaranteed to be correct. Like, does gdb cope
>> properly if a compiler happens to emit multiple lexical scopes that each
>> have non-contiguous ranges, and then where the ranges happen to overlap.
[...]
Andrew> Now the two lexical blocks are siblings, but if we make the second block
Andrew> overlap the first then this patch does cause problems.
[...]
Andrew> This DOES change GDB's behaviour.
Andrew> Is this sibling case the type of case you were thinking of? Or have I
Andrew> got the wrong end of the stick here?
I think your analysis is in line with what I was thinking.
My main concern is whether incorrect input can make gdb crash by
violating some addrmap invariant; and then also if sufficiently weird
but nominally valid input can make gdb do the wrong thing.
>> I wonder why buildsym even tries to see if the pending addrmap might be
>> interesting. It seems to me that a fixed addrmap is basically the same
>> data structure as the blockvector: it maps addresses to blocks and uses
>> a binary search to find the correct entry.
Andrew> Sure, but I wonder if this is a memory use thing? The addrmap must use
Andrew> more memory than the vector, so maybe for large programs this would be a
Andrew> noticable hit.
Yeah, I think what I meant is gdb could perhaps just dispense with the
vector entirely and only store the addrmap.
Though I don't actually want that outcome because I would like the
blockvector to be expandable. This isn't readily possible with addrmap
due to the bottom-up requirement.
Andrew> There are a few places where we use the block's ranges for useful work,
Andrew> these would all need to be rewritten to get the same information from
Andrew> the blockvector (really the addrmap within the blockvector).
Ok, thanks. When this comes back around I will dig for those.
>> FWIW I've been looking into this area a bit while experimenting with
>> lazy CU expansion. There I think I want to make it so blockvectors are
>> always expandable, so no fixed addrmap at least -- and mutable addrmaps
>> have some issues with updating, so probably moving to just having a
>> vector.
Andrew> Wouldn't that cause problems for non-contiguous blocks? Isn't that the
Andrew> problem that the addrmap was added to solve?
For me the desirable end point is that blockvector is an opaque data
structure, where the primary read operation is "give the block for a
given PC", and queries about the internal structure of the blockvector
are forbidden. Right now there is code that looks to see if there a map
exists, which is an abstraction violation; and also buildsym seems to be
pretty chummy with blockvector, though that's less of a problem.
Now, the other thread -- the JIT stuff Jan Vrany is working on -- shows
that there are still some problems to iron out here, because for reasons
I don't really understand, some blockvector users seem to want to get
back the static block in response to a PC lookup. (There was that issue
with some code in libc that is not in a function body. But I don't
understand why that's not just a bug in libc.)
Other than that, though, I don't think that non-contiguous blocks
present a problem. Their existence would be hidden by the blockvector
API.
Though I may not appreciate this fully either. Anyway my current idea
for lazy expansion is to record incomplete symbols for functions, so
that if a blockvector lookup finds a block corresponding to such a
function, it would first be expanded to find the correct enclosed block.
Tom
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 5/7] gdb: create address map after parsing all DIE
2026-01-05 20:03 ` Tom Tromey
@ 2026-01-05 21:37 ` Andrew Burgess
2026-01-06 0:53 ` Tom Tromey
0 siblings, 1 reply; 37+ messages in thread
From: Andrew Burgess @ 2026-01-05 21:37 UTC (permalink / raw)
To: Tom Tromey; +Cc: Tom Tromey, gdb-patches
Tom Tromey <tom@tromey.com> writes:
>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>
> Andrew> The addrmap requires that we add the most inner blocks first. I
> Andrew> achieve this by walking the blockvector backward, as we always add
> Andrew> parent blocks before their more inner child blocks.
>
>>> I wonder if this is guaranteed to be correct. Like, does gdb cope
>>> properly if a compiler happens to emit multiple lexical scopes that each
>>> have non-contiguous ranges, and then where the ranges happen to overlap.
>
> [...]
> Andrew> Now the two lexical blocks are siblings, but if we make the second block
> Andrew> overlap the first then this patch does cause problems.
> [...]
> Andrew> This DOES change GDB's behaviour.
>
> Andrew> Is this sibling case the type of case you were thinking of? Or have I
> Andrew> got the wrong end of the stick here?
>
> I think your analysis is in line with what I was thinking.
>
> My main concern is whether incorrect input can make gdb crash by
> violating some addrmap invariant; and then also if sufficiently weird
> but nominally valid input can make gdb do the wrong thing.
I'm not too worried about addrmap crashing; I'm just adding things in a
slightly different order so if there is some weird overlap, addrmap will
just build a different address to block mapping, but this shouldn't
cause crashes from addrmap. And ideally shouldn't be crashing the rest
of GDB either, we'll just end up with a different block (possibly, and
again, this is only in the case of some weird overlap).
At least, my experience while working on this was less, oops I've built
the wrong addrmap and now GDB crashed, and more, I've built the wrong
addrmap, and now GDB doesn't know where it is stopped, or the local
variables are no longer visible.
Which brings us to your second worry. This patch, for sure changes the
order in which blocks are added into the addrmap. More nested blocks
are added before their parents, so if we don't have overlapping
siblings, then this should result in the same mapping. But I cannot
guarantee that there's not some weird compiler out there that does emit
such cases. But we don't have tests for such cases yet, and I saw no
comments discussing such things, so I'm hoping that's not a thing.
If we do have overlapping siblings then I don't think current GDB will
work; at least, locals from one of the siblings will not be visible in
the overlap region. This patch might change which sibling though.
When I originally wrote this patch I did try to retain the original
order in which blocks are added to the addrmap, and I looked at this
again today. The problem is the particular ordering we use when
building the pending block list (see record_pending_block), as well as
pending block sorting (see end_compunit_symtab_get_static_block). The
conclusion I've come to is that if the original block order is critical,
or if we just don't want to move away from this ordering, then we'd need
to add another data structure to track the desired order. I didn't do
this for two reasons (1) saving space and time, and (2) so long as the
bottom up invariant remained, I figured the other changes should be
harmless.
You did give an approve for the original patch; I still need to update
the comments as you suggested, but is there anything else you'd like me
to investigate to help address your concerns? Otherwise my hope would
be to merge this and address any issues if/when they arise down the
line.
Thanks,
Andrew
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 5/7] gdb: create address map after parsing all DIE
2026-01-05 21:37 ` Andrew Burgess
@ 2026-01-06 0:53 ` Tom Tromey
0 siblings, 0 replies; 37+ messages in thread
From: Tom Tromey @ 2026-01-06 0:53 UTC (permalink / raw)
To: Andrew Burgess; +Cc: Tom Tromey, gdb-patches
Andrew> You did give an approve for the original patch; I still need to update
Andrew> the comments as you suggested, but is there anything else you'd like me
Andrew> to investigate to help address your concerns? Otherwise my hope would
Andrew> be to merge this and address any issues if/when they arise down the
Andrew> line.
I don't think there's anything else. Thank you.
Tom
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
` (6 preceding siblings ...)
2025-10-16 17:49 ` [PATCHv3 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
@ 2026-02-04 10:43 ` Andrew Burgess
7 siblings, 0 replies; 37+ messages in thread
From: Andrew Burgess @ 2026-02-04 10:43 UTC (permalink / raw)
To: gdb-patches
Andrew Burgess <aburgess@redhat.com> writes:
> In v3:
>
> - Rebased to current upstream HEAD. Resolved merged conflicts, and
> retested.
>
> - Reread all the patches, and updated / tweaked some of the comment
> and commit messages, but no changes to the code other than as
> required to fix merge conflicts.
>
> - Updated the tests to take account of changes to the DWARF assembler.
>
> In v2:
>
> - Fixes to the test added in patch (2) to address failures reported
> from Linaro CI. The issues all relate to compiling the DWARF
> assembler test with `-pie` which is on by default for the Linaro
> CI machines. The issues were all DWARF generation issues.
>
> - Rebased series to more recent upstream/master.
>
> ---
>
> This series presents some improvements to debugging inline functions,
> especially in optimised code.
I've now checked this series in.
Thanks,
Andrew
^ permalink raw reply [flat|nested] 37+ messages in thread
end of thread, other threads:[~2026-02-04 10:44 UTC | newest]
Thread overview: 37+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-07-20 10:20 [PATCH 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-07-20 10:20 ` [PATCH 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
2025-07-20 10:20 ` [PATCH 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
2025-07-20 10:20 ` [PATCH 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
2025-07-20 10:20 ` [PATCH 4/7] gdb: move block range recording into its own function Andrew Burgess
2025-07-20 10:20 ` [PATCH 5/7] gdb: create address map after parsing all DIE Andrew Burgess
2025-07-20 10:20 ` [PATCH 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
2025-07-20 10:20 ` [PATCH 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 4/7] gdb: move block range recording into its own function Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 5/7] gdb: create address map after parsing all DIE Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
2025-08-01 8:58 ` [PATCHv2 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 1/7] gdb: improve line number lookup around inline functions Andrew Burgess
2025-10-27 22:22 ` Tom Tromey
2025-12-17 14:32 ` Andrew Burgess
2025-12-17 14:48 ` Tom de Vries
2025-12-18 14:46 ` Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 2/7] gdb: handle empty ranges for inline subroutines Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 3/7] gdb: split dwarf line table parsing in two Andrew Burgess
2025-10-16 17:49 ` [PATCHv3 4/7] gdb: move block range recording into its own function Andrew Burgess
2025-10-27 22:45 ` Tom Tromey
2025-10-16 17:49 ` [PATCHv3 5/7] gdb: create address map after parsing all DIE Andrew Burgess
2025-10-27 22:56 ` Tom Tromey
2026-01-02 16:36 ` Andrew Burgess
2026-01-05 20:03 ` Tom Tromey
2026-01-05 21:37 ` Andrew Burgess
2026-01-06 0:53 ` Tom Tromey
2025-10-16 17:49 ` [PATCHv3 6/7] gdb: record block end addresses while parsing DIEs Andrew Burgess
2025-10-27 23:00 ` Tom Tromey
2025-10-16 17:49 ` [PATCHv3 7/7] gdb: fix-up truncated inline function block ranges Andrew Burgess
2026-02-04 10:43 ` [PATCHv3 0/7] Inline Function Optimised Code Debug Improvements Andrew Burgess
2025-08-01 15:41 ` [PATCH " Sam James
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox