[PATCHv2] gdb: fix parsing of DIEs with both low/high pc AND ranges attributes

Message ID 818ec3d3c07bbcdd21ef0bc5643379ea57722031.1733153476.git.aburgess@redhat.com
State New
Headers
Series [PATCHv2] gdb: fix parsing of DIEs with both low/high pc AND ranges attributes |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_check--master-arm success Test passed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 success Test passed

Commit Message

Andrew Burgess Dec. 2, 2024, 3:33 p.m. UTC
  In v2:

  - Added a DWARF assembler test.

Given this patch is fixing a regression (on older distributions) I do
plan to merge this fix later this week unless someone jumps in with
any concerns.  As always, I'm happy to address issues after I commit,
if nobody wants to review ahead of time.

Thanks,
Andrew

--

After the commit:

  commit b9de07a5ff74663ff39bf03632d1b2ea417bf8d5
  Date:   Thu Oct 10 11:37:34 2024 +0100

      gdb: fix handling of DW_AT_entry_pc of inlined subroutines

GDB's buildbot CI testing highlighted this assertion failure:

  (gdb) c
  Continuing.
  ../../binutils-gdb/gdb/block.h:203: internal-error: set_entry_pc: Assertion `start >= this->start () && start < this->end ()' failed.
  A problem internal to GDB has been detected,
  further debugging may prove unreliable.
  ----- Backtrace -----
  FAIL: gdb.base/break-probes.exp: run til our library loads (GDB internal error)

This assertion was in the new function set_entry_pc and is asserting
that the default_entry_pc() value is within the blocks start/end
range.

The default_entry_pc() is the value GDB will use as the entry-pc if
the DWARF doesn't specifically override the entry-pc.  This value is
calculated as:

  1. The start address of the first sub-range within the block, if the
  block has more than 1 range, or

  2. The low address (from DW_AT_low_pc) for the block.

If the block only has a single range then this means the block was
defined with low/high pc attributes (case #2 above).  These low/high
pc values are what block::start() and block::end() return.  This means
that by definition, if the block is continuous, the above assert
cannot trigger as 'start', the default_entry_pc() would be equivalent
to block::start().

This means that, for the assert to trigger, the block must have
multiple ranges, and the first address of the first range is not
within the blocks low/high address range.  This seems wrong.

I inspected the state at the time the assert triggered and discovered
the block's start() address.  Then I removed the assert and restarted
GDB.  I was now able to inspect the blocks at the offending address:

  (gdb) maintenance info blocks 0x7ffff7dddaa4
  Blocks at 0x7ffff7dddaa4:
    from objfile: [(objfile *) 0x44a37f0] /lib64/ld-linux-x86-64.so.2
 [(block *) 0x46b30c0] 0x7ffff7ddd5a0..0x7ffff7dde8a6
    entry pc: 0x7ffff7ddd5a0
    is global block
    symbol count: 4
    is contiguous
  [(block *) 0x46b3020] 0x7ffff7ddd5a0..0x7ffff7dde8a6
    entry pc: 0x7ffff7ddd5a0
    is static block
    symbol count: 9
    is contiguous
  [(block *) 0x46b2f70] 0x7ffff7ddda00..0x7ffff7dddac3
    entry pc: 0x7ffff7ddda00
    function: __GI__dl_find_dso_for_object
    symbol count: 4
    is contiguous
  [(block *) 0x46b2e10] 0x7ffff7dddaa4..0x7ffff7dddac3
    entry pc: 0x7ffff7dddaa4
    inline function: __GI__dl_find_dso_for_object
    symbol count: 5
    is contiguous
  [(block *) 0x46b2a40] 0x7ffff7dddaa4..0x7ffff7dddac3
    entry pc: 0x7ffff7dddaa4
    symbol count: 1
    is contiguous
  [(block *) 0x46b2970] 0x7ffff7dddaa4..0x7ffff7dddac3
    entry pc: 0x7ffff7dddaa4
    symbol count: 2
    address ranges:
      0x7ffff7ddda0e..0x7ffff7ddda77
      0x7ffff7ddda90..0x7ffff7ddda96

I've left everything in for context, but the only really interesting
bit is the very last block, it's low/high range is:

  0x7ffff7dddaa4..0x7ffff7dddac3

but it has separate ranges:

  0x7ffff7ddda0e..0x7ffff7ddda77
  0x7ffff7ddda90..0x7ffff7ddda96

which are all outside the low/high range.  This is what triggers the
assert.  But why does that block exist at all?

What I believe is happening is that we're running into a bug in older
versions of GCC.  The buildbot failure was with an 8.5 gcc, and Tom de
Vries also reported seeing failures when using version 7 and 8 gcc,
but not with gcc 9 and onward.

Looking at the DWARF I can see that the problematic block is created
from this DIE:

  <4><15efb>: Abbrev Number: 83 (DW_TAG_lexical_block)
     <15efc>   DW_AT_abstract_origin: <0x15e9f>
     <15efe>   DW_AT_low_pc      : 0x7ffff7dddaa4
     <15f06>   DW_AT_high_pc     : 31

which links via DW_AT_abstract_origin to:

  <2><15e9f>: Abbrev Number: 80 (DW_TAG_lexical_block)
     <15ea0>   DW_AT_ranges      : 0x38e0
     <15ea4>   DW_AT_sibling     : <0x15eca>

And so we can see that <15efb> has got both low/high pc attributes and
a ranges attribute.

If I widen my checking to parents of DIE <15efb> then I see that they
also have DW_AT_abstract_origin, however, there is something
interesting going on, the parent DIEs are linking to a different DIE
tree than <15efb>.

What I believe is happening is this, we have an abstract instance
tree, this is rooted at a DW_AT_subprogram, and contains all the
blocks, variables, parameters, etc, that you would expect.  As this is
an abstract instance, then there are no low/high pc attributes, and no
ranges attributes in this tree.  This makes sense.

Now elsewhere we have a DW_TAG_subprogram (not
DW_TAG_inlined_subroutine) which links via
DW_AT_abstract_origin to the abstract DW_AT_subprogram.  This case is
documented in the DWARF 5 spec in section 3.3.8.3, and describes an
Out-of-Line Instance of an Inlined Subroutine.  Within this out of
line instance many of the DIE correctly link back, using
DW_AT_abstract_origin to the abstract instance tree.  This tree also
includes the DIE <15e9f>, which is where our problem DIE references.

Now, to really confuse things, within this out-of-line instance we
have a DW_TAG_inlined_subroutine, which is another instance of the
same abstract instance tree!  This would seem to indicate a recursive
call to the inline function, and the compiler, for some reason, needed
to instantiate an out of line instance of this function.

And it is within this nested, inlined subroutine, that the problem DIE
exists.  The problem DIE is referencing the corresponding DIE within
the out of line instance tree, but I am convinced this must be a (long
fixed) GCC bug, and that the problem DIE should be referencing the DIE
within the abstract instance tree.

I'm aware that the above is pretty confusing.  The actual DWARF would
be a around 200 lines long, so I'd like to avoid dumping it in here.
But here's my attempt at representing what's going on in a minimal
example.  The numbers down the side represent the section offset, not
the nesting level, and I've removed any attributes that are not
relevant:

  <1> DW_TAG_subprogram
  <2>   DW_TAG_lexical_block
  <3> DW_TAG_subprogram
        DW_AT_abstract_origin <1>
  <4>   DW_TAG_lexical_block
          DW_AT_ranges ...
  <5>   DW_TAG_inlined_subroutine
          DW_AT_abstract_origin <1>
  <6>     DW_TAG_lexical_block
            DW_AT_abstract_origin <4>
            DW_AT_low_pc ...
            DW_AT_high_pc ...

The lexical block at <6> is linking to <4> when it should be linking
to <2>.

There is one additional thing that we might wonder about, which is,
when calculating the low/high pc range for a block, why does GDB not
make use of the range information and expand the range beyond the
defined low/high values?

The answer to this is in dwarf_get_pc_bounds_ranges_or_highlow_pc in
dwarf/read.c.  This is where the low/high bounds are calculated.  What
we see is that GDB first checks for a low/high attribute pair, and if
that is present, this defines the address range for the block.  Only
if there is no DW_AT_low_pc do we check for the DW_AT_ranges, and use
that to define the extent of the block.  And this makes sense, section
3.5 of the DWARF-5 spec says:

  The lexical block entry may have either a DW_AT_low_pc and DW_AT_high_pc
  pair of attributes or a DW_AT_ranges attribute whose values encode the
  contiguous or non-contiguous address ranges, respectively, of the machine
  instructions generated for the lexical block...

Section 3.5 is specifically about lexical blocks, but the same
wording, about it being either low/high OR ranges is repeated for
other DW_TAG_ types.

So this explains why GDB doesn't use the ranges to expand the problem
blocks ranges; as the first DIE has low/high addresses, these are
used, and the ranges is not consulted.

It is only later in dwarf2_record_block_ranges that we create a range
based off the low/high pc, and then also process the ranges data, this
allows the problem block to exist with ranges that are outside the
low/high range.

To solve this I considered a number of options:

1. Prevent loading certain attributes from an abstract instance.

Section 3.3.8.1 of the DWARF-5 spec talks about which attributes are
appropriate to place in an abstract instance.  Any attribute that
might vary between instances should not appear in an abstract
instance.  DW_AT_ranges is included as an example in the
non-exhaustive list of attributes that should not appear in an
abstract instance.

Currently in dwarf2_attr (dwarf2/read.c), when we see a
DW_AT_abstract_origin attribute, we always follow this to try and find
the attribute we are looking for.  But we could change this function
so that we prevent this following for attributes that we know should
not be looked up in an abstract instance.  This would solve the
problem in this case by preventing us finding the DW_AT_ranges in the
incorrect abstract instance.

2. Filter the ranges.

Having established a blocks low/high address range in
dwarf_get_pc_bounds_ranges_or_highlow_pc, we could allow
dwarf2_record_block_ranges to parse the ranges, but we could reject
any range that extends outside the blocks defined start and end
addresses.

For well behaved DWARF where we have either low/high or ranges, then
the blocks start/end are defined from the range data, and so, by
definition, every range would be acceptable.

But in our problem case we would reject all of the invalid ranges.

This is my least favourite solution as it feels like rejecting the
ranges is tackling the problem too late on.

3. Don't try to parse ranges when we have low/high attributes.

This option involves updating dwarf2_record_block_ranges to match the
behaviour of dwarf_get_pc_bounds_ranges_or_highlow_pc, and, I believe,
to match the DWARF spec: don't try to read range data from
DW_AT_ranges if we have low/high pc attributes.

In our case this solves the issue because the problematic DIE has the
low/high attributes, and it then links to the wrong DIE which happens
to have DW_AT_ranges.  With this change in place we don't even look
for the DW_AT_ranges.

If the problem were reversed, and the initial DIE had DW_AT_ranges,
but the incorrectly referenced DIE had the low/high pc attributes,
we would pick up the wrong addresses, but this wouldn't trigger any
asserts.  The reason is that dwarf_get_pc_bounds_ranges_or_highlow_pc
would also find the low/high addresses from the incorrectly referenced
DIE, and so we would just end up with a block which had the wrong
address ranges, but the block would be self consistent, which is
different to the problem we hit here.

In the end, in this commit I went with solution #3, having
dwarf_get_pc_bounds_ranges_or_highlow_pc and
dwarf2_record_block_ranges be consistent seems sensible.  However, I
do wonder if in the future we might want to explore solution #1 as an
additional safety feature.

With this patch in place I'm able to run the gdb.base/break-probes.exp
without seeing the assert that CI testing highlighted.  I see no
regressions when testing on x86-64 GNU/Linux with gcc  9.3.1.

Note: the diff in this commit looks big, but it's really just me
indenting the code.
---
 gdb/dwarf2/read.c                             |  68 +++--
 .../gdb.dwarf2/dw2-bad-abstract-origin.c      |  83 ++++++
 .../gdb.dwarf2/dw2-bad-abstract-origin.exp    | 274 ++++++++++++++++++
 3 files changed, 403 insertions(+), 22 deletions(-)
 create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-bad-abstract-origin.c
 create mode 100644 gdb/testsuite/gdb.dwarf2/dw2-bad-abstract-origin.exp


base-commit: 5930bcb592a599ec7182cdc9c2f47511e6082a5e
  

Comments

Tom Tromey Dec. 4, 2024, 6:07 p.m. UTC | #1
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:

Andrew> In v2:
Andrew>   - Added a DWARF assembler test.

Andrew> Given this patch is fixing a regression (on older distributions) I do
Andrew> plan to merge this fix later this week unless someone jumps in with
Andrew> any concerns.  As always, I'm happy to address issues after I commit,
Andrew> if nobody wants to review ahead of time.

We encountered this assertion failure on various builds after importing
gdb-head this week.  E.g., many internal tests fail on Ubuntu 18.

I tested this patch and can confirm it fixes the problems we are seeing.
Thank you.

Approved-By: Tom Tromey <tom@tromey.com>

Andrew> 1. Prevent loading certain attributes from an abstract instance.

Andrew> Section 3.3.8.1 of the DWARF-5 spec talks about which attributes are
Andrew> appropriate to place in an abstract instance.  Any attribute that
Andrew> might vary between instances should not appear in an abstract
Andrew> instance.  DW_AT_ranges is included as an example in the
Andrew> non-exhaustive list of attributes that should not appear in an
Andrew> abstract instance.

Andrew> Currently in dwarf2_attr (dwarf2/read.c), when we see a
Andrew> DW_AT_abstract_origin attribute, we always follow this to try and find
Andrew> the attribute we are looking for.  But we could change this function
Andrew> so that we prevent this following for attributes that we know should
Andrew> not be looked up in an abstract instance.  This would solve the
Andrew> problem in this case by preventing us finding the DW_AT_ranges in the
Andrew> incorrect abstract instance.

Andrew> However, I do wonder if in the future we might want to explore
Andrew> solution #1 as an additional safety feature.

Yeah, I wonder this as well.

Or perhaps this particular spot should be changed to explicitly not
follow links to an abstract origin, rather than making it conditional on
the particular attribute?  I am not sure if that's better or worse.

Tom
  
Andrew Burgess Dec. 4, 2024, 11:44 p.m. UTC | #2
Tom Tromey <tromey@adacore.com> writes:

>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>
> Andrew> In v2:
> Andrew>   - Added a DWARF assembler test.
>
> Andrew> Given this patch is fixing a regression (on older distributions) I do
> Andrew> plan to merge this fix later this week unless someone jumps in with
> Andrew> any concerns.  As always, I'm happy to address issues after I commit,
> Andrew> if nobody wants to review ahead of time.
>
> We encountered this assertion failure on various builds after importing
> gdb-head this week.  E.g., many internal tests fail on Ubuntu 18.
>
> I tested this patch and can confirm it fixes the problems we are seeing.
> Thank you.
>
> Approved-By: Tom Tromey <tom@tromey.com>

Sorry for the breakage.  I've now pushed this patch.

>
> Andrew> 1. Prevent loading certain attributes from an abstract instance.
>
> Andrew> Section 3.3.8.1 of the DWARF-5 spec talks about which attributes are
> Andrew> appropriate to place in an abstract instance.  Any attribute that
> Andrew> might vary between instances should not appear in an abstract
> Andrew> instance.  DW_AT_ranges is included as an example in the
> Andrew> non-exhaustive list of attributes that should not appear in an
> Andrew> abstract instance.
>
> Andrew> Currently in dwarf2_attr (dwarf2/read.c), when we see a
> Andrew> DW_AT_abstract_origin attribute, we always follow this to try and find
> Andrew> the attribute we are looking for.  But we could change this function
> Andrew> so that we prevent this following for attributes that we know should
> Andrew> not be looked up in an abstract instance.  This would solve the
> Andrew> problem in this case by preventing us finding the DW_AT_ranges in the
> Andrew> incorrect abstract instance.
>
> Andrew> However, I do wonder if in the future we might want to explore
> Andrew> solution #1 as an additional safety feature.
>
> Yeah, I wonder this as well.
>
> Or perhaps this particular spot should be changed to explicitly not
> follow links to an abstract origin, rather than making it conditional on
> the particular attribute?  I am not sure if that's better or worse.

I've added an item to my todo list to revisit this in the new year.

Thanks,
Andrew
  

Patch

diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 995abf1405d..5a5e27f30a6 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -11450,6 +11450,21 @@  dwarf2_record_block_ranges (struct die_info *die, struct block *block,
   struct attribute *attr;
   struct attribute *attr_high;
 
+  /* Like dwarf_get_pc_bounds_ranges_or_highlow_pc, we read either the
+     low/high pc attributes, OR the ranges attribute, but not both.  If we
+     parse both here then we open up the possibility that, due to invalid
+     DWARF, a block's start() and end() might not contain all of the ranges.
+
+     We have seen this in the wild with older (pre v9) versions of GCC.  In
+     this case a GCC bug meant that a DIE was linked via DW_AT_abstract_origin
+     to the wrong DIE.  Instead of pointing at the abstract DIE, GCC was
+     linking one instance DIE to an earlier instance DIE.  The first instance
+     DIE had low/high pc attributes, while the second instance DIE had a
+     ranges attribute.  When processing the incorrectly linked instance GDB
+     would see a DIE with both a low/high pc and some ranges data.  However,
+     the ranges data was all outside the low/high range, which would trigger
+     asserts when setting the entry-pc.  */
+
   attr_high = dwarf2_attr (die, DW_AT_high_pc, cu);
   if (attr_high)
     {
@@ -11467,32 +11482,41 @@  dwarf2_record_block_ranges (struct die_info *die, struct block *block,
 	  CORE_ADDR high = per_objfile->relocate (unrel_high);
 	  cu->get_builder ()->record_block_range (block, low, high - 1);
 	}
-    }
 
-  attr = dwarf2_attr (die, DW_AT_ranges, cu);
-  if (attr != nullptr && attr->form_is_unsigned ())
+      attr = dwarf2_attr (die, DW_AT_ranges, cu);
+      if (attr != nullptr && attr->form_is_unsigned ())
+	complaint (_("in %s, DIE %s, DW_AT_ranges ignored due to DW_AT_low_pc"),
+		   objfile_name (per_objfile->objfile),
+		   sect_offset_str (die->sect_off));
+    }
+  else
     {
-      /* Offset in the .debug_ranges or .debug_rnglist section (depending
-	 on DWARF version).  */
-      ULONGEST ranges_offset = attr->as_unsigned ();
-
-      /* See dwarf2_cu::gnu_ranges_base's doc for why we might want to add
-	 this value.  */
-      if (die->tag != DW_TAG_compile_unit)
-	ranges_offset += cu->gnu_ranges_base;
-
-      std::vector<blockrange> blockvec;
-      dwarf2_ranges_process (ranges_offset, cu, die->tag,
-	[&] (unrelocated_addr start, unrelocated_addr end)
+      attr = dwarf2_attr (die, DW_AT_ranges, cu);
+      if (attr != nullptr && attr->form_is_unsigned ())
 	{
-	  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);
-	  blockvec.emplace_back (abs_start, abs_end);
-	});
+	  /* Offset in the .debug_ranges or .debug_rnglist section (depending
+	     on DWARF version).  */
+	  ULONGEST ranges_offset = attr->as_unsigned ();
 
-      block->set_ranges (make_blockranges (objfile, blockvec));
+	  /* See dwarf2_cu::gnu_ranges_base's doc for why we might want to add
+	     this value.  */
+	  if (die->tag != DW_TAG_compile_unit)
+	    ranges_offset += cu->gnu_ranges_base;
+
+	  std::vector<blockrange> blockvec;
+	  dwarf2_ranges_process (ranges_offset, cu, die->tag,
+				 [&] (unrelocated_addr start,
+				      unrelocated_addr end)
+	  {
+	    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);
+	    blockvec.emplace_back (abs_start, abs_end);
+	  });
+
+	  block->set_ranges (make_blockranges (objfile, blockvec));
+	}
     }
 
   dwarf2_record_block_entry_pc (die, block, cu);
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-bad-abstract-origin.c b/gdb/testsuite/gdb.dwarf2/dw2-bad-abstract-origin.c
new file mode 100644
index 00000000000..96014af7ecd
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-bad-abstract-origin.c
@@ -0,0 +1,83 @@ 
+/* 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/>.  */
+
+volatile int global_var = 0;
+
+void
+func_a (void)	/* func_a decl line */
+{
+  /* This label is used to find the start of 'func_a' when generating the
+     debug information.  Place nothing before it.  */
+  asm ("func_a_label: .globl func_a_label");
+  ++global_var;
+
+  asm ("func_a_0: .globl func_a_0");
+  ++global_var;
+
+  asm ("func_a_1: .globl func_a_1");
+  ++global_var;
+
+  asm ("func_a_2: .globl func_a_2");
+  ++global_var;
+
+  asm ("func_a_3: .globl func_a_3");
+  ++global_var;
+
+  asm ("func_a_4: .globl func_a_4");
+  ++global_var;
+
+  asm ("func_a_5: .globl func_a_5");
+  ++global_var;
+
+  asm ("func_a_6: .globl func_a_6");
+  ++global_var;
+}
+
+void
+func_b (void)	/* func_b decl line */
+{
+  /* This label is used to find the start of 'func_b' when generating the
+     debug information.  Place nothing before it.  */
+  asm ("func_b_label: .globl func_b_label");
+  ++global_var;
+
+  asm ("func_b_0: .globl func_b_0");
+  ++global_var;		/* inline func_a call line */
+
+  asm ("func_b_1: .globl func_b_1");
+  ++global_var;
+
+  asm ("func_b_2: .globl func_b_2");
+  ++global_var;
+
+  asm ("func_b_3: .globl func_b_3");
+  ++global_var;
+
+  asm ("func_b_4: .globl func_b_4");
+  ++global_var;
+
+  asm ("func_b_5: .globl func_b_5");
+  ++global_var;
+}
+
+int
+main (void)
+{
+  asm ("main_label: .globl main_label");
+  func_b ();
+  func_a ();
+}
diff --git a/gdb/testsuite/gdb.dwarf2/dw2-bad-abstract-origin.exp b/gdb/testsuite/gdb.dwarf2/dw2-bad-abstract-origin.exp
new file mode 100644
index 00000000000..5e7d8cc1725
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/dw2-bad-abstract-origin.exp
@@ -0,0 +1,274 @@ 
+# 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/>.
+
+# There was a bug in GCC, which appears to be fixed in version 9 and
+# later, where GCC would, in some case, create an invalid
+# DW_AT_abstract_origin value.
+#
+# The bug was that there existed a function which could be inlined,
+# and so the DWARF contained a DW_TAG_subprogram describing the
+# abstract instance of the function.
+#
+# For whatever reason, the compiler generated a non-inline instance of
+# the function, and so we had a DW_TAG_subprogram with a
+# DW_AT_abstract_origin that referenced the abstract instance.
+#
+# Additionally there was an inlined instance of the function, and so
+# we had a DW_TAG_inlined_subroutine with a DW_AT_abstract_origin that
+# referenced the abstract instance.
+#
+# Within the function there was a DW_TAG_lexical_block, which also
+# appeared in the abstract instance, and both concrete instances.  The
+# lexical block also has DW_AT_abstract_origin that should link back
+# to the lexical block within the abstract instance.
+#
+# The bug was that the DW_AT_abstract_origin for the lexical block
+# within the inlined instance instead referenced the lexical block
+# within the non-inline instance, not within the abstract instance.
+#
+# The problem this caused is that the non-inline instance defined the
+# extents of the lexical block using DW_AT_ranges, while the inline
+# instance defined the extend using DW_AT_low_pc and DW_AT_high_pc.
+#
+# When GDB tried to parse the block ranges for the lexical block for
+# the inline function GDB would then find both the DW_AT_ranges and
+# the DW_AT_low_pc/DW_AT_high_pc values.  This alone is unexpected.
+#
+# What is worse though, is that the DW_AT_ranges were not within the
+# low-pc/high-pc bounds, and this really confused GDB.
+#
+# The solution is that, when GDB finds blocks with both ranges AND
+# low-pc/high-pc information, GDB should only accept the
+# low-pc/high-pc information.
+#
+# Of course, there's no guarantee which of the information is correct,
+# but if GDB tries to hold both piece of information, then we end up
+# in a non-consistent state, and this triggers assertions.
+
+load_lib dwarf.exp
+
+require dwarf2_support
+
+standard_testfile
+
+# This compiles the source file and starts and stops GDB, so run it
+# before calling prepare_for_testing otherwise GDB will have exited.
+get_func_info func_a
+get_func_info func_b
+
+# Some line numbers needed in the generated DWARF.
+set func_a_decl_line [gdb_get_line_number "func_a decl line"]
+set func_b_decl_line [gdb_get_line_number "func_b decl line"]
+set call_line [gdb_get_line_number "inline func_a call line"]
+
+# See the problem description at the head of this file.
+#
+# Create the test program, use DWARF_VERSION to decide which format of
+# ranges table to generate.
+#
+# Then run the test program and check that GDB doesn't crash, and
+# check that the block structure is as we expect.
+proc run_test { dwarf_version } {
+    set dw_testname "${::testfile}-${dwarf_version}"
+
+    set asm_file [standard_output_file "${dw_testname}.S"]
+    Dwarf::assemble $asm_file {
+	upvar dwarf_version dwarf_version
+	upvar entry_label entry_label
+
+	declare_labels lines_table foo_func foo_block block_ranges bad_block \
+	    value_label int_label
+
+	cu { version $dwarf_version } {
+	    compile_unit {
+		{producer "GNU C 14.1.0"}
+		{language @DW_LANG_C}
+		{name $::srcfile}
+		{comp_dir /tmp}
+		{stmt_list $lines_table DW_FORM_sec_offset}
+		{low_pc 0 addr}
+	    } {
+		int_label: base_type {
+		    {name "int"}
+		    {byte_size 4 sdata}
+		    {encoding @DW_ATE_signed}
+		}
+		foo_func: subprogram {
+		    {name foo}
+		    {inline @DW_INL_declared_inlined}
+		    {decl_file 1 data1}
+		    {decl_line $::func_a_decl_line data1}
+		} {
+		    foo_block: lexical_block {
+		    } {
+			value_label: DW_TAG_variable {
+			    {name value}
+			    {type :$int_label}
+			}
+		    }
+		}
+		subprogram {
+		    {abstract_origin %$foo_func}
+		    {low_pc func_a_0 addr}
+		    {high_pc func_a_6 addr}
+		    {external 1 flag}
+		} {
+		    bad_block: lexical_block {
+			{abstract_origin %$foo_block}
+			{ranges $block_ranges DW_FORM_sec_offset}
+		    } {
+			DW_TAG_variable {
+			    {abstract_origin %$value_label}
+			    {DW_AT_location {
+				DW_OP_const1u 23
+				DW_OP_stack_value
+			    } SPECIAL_expr}
+			}
+		    }
+		}
+		subprogram {
+		    {name baz}
+		    {low_pc func_b_0 addr}
+		    {high_pc func_b_5 addr}
+		    {external 1 flag}
+		} {
+		    inlined_subroutine {
+			{abstract_origin %$foo_func}
+			{call_file 1 data1}
+			{call_line $::call_line data1}
+			{low_pc func_b_1 addr}
+			{high_pc func_b_4 addr}
+		    } {
+			lexical_block {
+			    {abstract_origin %$bad_block}
+			    {low_pc func_b_2 addr}
+			    {high_pc func_b_3 addr}
+			} {
+			    DW_TAG_variable {
+				{abstract_origin %$value_label}
+				{DW_AT_location {
+				    DW_OP_const1u 99
+				    DW_OP_stack_value
+				} SPECIAL_expr}
+			    }
+			}
+		    }
+		}
+	    }
+	}
+
+	lines {version 2} lines_table {
+	    include_dir "$::srcdir/$::subdir"
+	    file_name "$::srcfile" 1
+	}
+
+	if { $dwarf_version == 5 } {
+	    rnglists {} {
+		table {} {
+		    block_ranges: list_ {
+			start_end func_a_1 func_a_2
+			start_end func_a_4 func_a_5
+		    }
+		}
+	    }
+	} else {
+	    ranges { } {
+		block_ranges: sequence {
+		    range func_a_1 func_a_2
+		    range func_a_4 func_a_5
+		}
+	    }
+	}
+    }
+
+    if {[prepare_for_testing "failed to prepare" "${dw_testname}" \
+	     [list $::srcfile $asm_file] {nodebug}]} {
+	return false
+    }
+
+    if {![runto_main]} {
+	return false
+    }
+
+    # Breakpoint on the inline function `foo'.
+    gdb_breakpoint foo
+
+    # Breakpoint within the lexical block inside of `foo'.
+    gdb_breakpoint func_a_1
+    gdb_breakpoint func_b_2
+
+    gdb_continue_to_breakpoint "continue to first foo breakpoint"
+    gdb_continue_to_breakpoint "continue to func_b_2 breakpoint"
+
+    gdb_test "print value" " = 99" "print value at func_b_2"
+
+    # Some addresses we need to look for in the 'maint info blocks'
+    # output.
+    set func_b_0 [get_hexadecimal_valueof "&func_b_0" "*UNKNOWN*"]
+    set func_b_1 [get_hexadecimal_valueof "&func_b_1" "*UNKNOWN*"]
+    set func_b_2 [get_hexadecimal_valueof "&func_b_2" "*UNKNOWN*"]
+    set func_b_3 [get_hexadecimal_valueof "&func_b_3" "*UNKNOWN*"]
+    set func_b_4 [get_hexadecimal_valueof "&func_b_4" "*UNKNOWN*"]
+    set func_b_5 [get_hexadecimal_valueof "&func_b_5" "*UNKNOWN*"]
+
+    gdb_test "maint info blocks" \
+	[multi_line \
+	     "\\\[\\(block \\*\\) $::hex\\\] $func_b_0\\.\\.$func_b_5" \
+	     "  entry pc: $func_b_0" \
+	     "  function: baz" \
+	     "  is contiguous" \
+	     "\\\[\\(block \\*\\) $::hex\\\] $func_b_1\\.\\.$func_b_4" \
+	     "  entry pc: $func_b_1" \
+	     "  inline function: foo" \
+	     "  symbol count: $::decimal" \
+	     "  is contiguous" \
+	     "\\\[\\(block \\*\\) $::hex\\\] $func_b_2\\.\\.$func_b_3" \
+	     "  entry pc: $func_b_2" \
+	     "  symbol count: $::decimal" \
+	     "  is contiguous"] \
+	"check block structure at func_b_2"
+
+    gdb_continue_to_breakpoint "continue to second foo breakpoint"
+    gdb_continue_to_breakpoint "continue to func_a_1 breakpoint"
+
+    gdb_test "print value" " = 23" "print value at func_a_1"
+
+    # Some addresses we need to look for in the 'maint info blocks'
+    # output.
+    set func_a_0 [get_hexadecimal_valueof "&func_a_0" "*UNKNOWN*"]
+    set func_a_1 [get_hexadecimal_valueof "&func_a_1" "*UNKNOWN*"]
+    set func_a_2 [get_hexadecimal_valueof "&func_a_2" "*UNKNOWN*"]
+    set func_a_4 [get_hexadecimal_valueof "&func_a_4" "*UNKNOWN*"]
+    set func_a_5 [get_hexadecimal_valueof "&func_a_5" "*UNKNOWN*"]
+    set func_a_6 [get_hexadecimal_valueof "&func_a_6" "*UNKNOWN*"]
+
+    gdb_test "maint info blocks" \
+	[multi_line \
+	     "\\\[\\(block \\*\\) $::hex\\\] $func_a_0\\.\\.$func_a_6" \
+	     "  entry pc: $func_a_0" \
+	     "  function: foo" \
+	     "  is contiguous" \
+	     "\\\[\\(block \\*\\) $::hex\\\] $func_a_1\\.\\.$func_a_5" \
+	     "  entry pc: $func_a_1" \
+	     "  symbol count: $::decimal" \
+	     "  address ranges:" \
+	     "    $func_a_1\\.\\.$func_a_2" \
+	     "    $func_a_4\\.\\.$func_a_5"] \
+	"check block structure at func_a_1"
+}
+
+foreach_with_prefix dwarf_version { 4 5 } {
+    run_test $dwarf_version
+}