Files
binutils-gdb/gdb/testsuite/gdb.opt/empty-inline-cxx.cc
Andrew Burgess bade3fecaf gdb: handle empty ranges for inline subroutines
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>
2024-12-04 14:03:25 +00:00

66 lines
1.6 KiB
C++

/* This testcase is part of GDB, the GNU debugger.
Copyright 2024 Free Software Foundation, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
#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});
}