I have now tried leaving DECL_ABSTRACT_ORIGIN out of the prevailing
fixup, but that is too late: the reference has already resolved to the
prevailing FUNCTION_DECL as the tree SCCs are read and unified.  Both
split clones therefore reach lto_fixup_prevailing_decls with the same
origin, and the test still fails.

I then tried the other approach you suggested.  During initial LGEN, the
prototype records an additional early DIE reference, when available, for
a public or external FUNCTION_DECL used as DECL_ABSTRACT_ORIGIN or
BLOCK_ABSTRACT_ORIGIN.  On input, it copies the streamed origin into a
private, abstract FUNCTION_DECL, associates the saved DIE reference with
that copy, and uses the copy as the debug origin.  The copy has no symtab
node and is not registered as a symbol.

This fixes both the original split-clone mismatch and the same problem in
an inlined block from the second TU.  I added a scan for the latter as
well.

With -r -nostdlib -flinker-output=nolto-rel, the unpatched test links
successfully but still fails the provenance checks, while the prototype
passes all 7 checks.  The gcc and g++ LTO suites also complete without
unexpected results:

  gcc:  1941 passes, 3 unsupported
  g++:  1758 passes, 1 expected failure

The targeted test also passes with aggressive GGC settings under the
none, one, 1to1, and max LTO partitioning modes.  This is still a
non-bootstrap prototype; full bootstrap and regression testing will come
with v4.

Does the private abstract fake-decl approach match what you had in mind,
or would you prefer a TREE_DIE_REF or streaming DECL_ABSTRACT_ORIGIN
through the local section?

Longjun Luo <[email protected]> 于2026年8月17日周一 22:34写道:

> Thanks for digging into the DWARF -- this matches what I see.
>
>
> The v2/v3 fallback fixes the emitted DIE reference, but not the underlying
>
> tree inconsistency: lto_fixup_prevailing_decls replaces the FUNCTION_DECL's
>
> DECL_ABSTRACT_ORIGIN, while the function-local PARM_DECLs and declarations
>
> in BLOCK_VARS retain their physical input-TU origins.
>
>
> I will first try leaving DECL_ABSTRACT_ORIGIN untouched during prevailing.
>
> That may leave it referring to a non-prevailing FUNCTION_DECL. Does the
>
> ordinary tree reference keep that declaration available for debug purposes,
>
> or must it be preserved explicitly?
>
>
> If that is insufficient, I will investigate fixing the local origins during
>
> stream-in or streaming DECL_ABSTRACT_ORIGIN through the function-body local
>
> section. I expect the lto_write_tree_1 fallback to become unnecessary and
>
> will verify and remove it.
>
>
> The v3 posted later only changes the testcase options; the implementation
> is
>
> unchanged, so this feedback applies to it as well.
>
>
> For the testcase link failure, I will use
>
> -r -nostdlib -flinker-output=nolto-rel to avoid the unrelated libstdc++
>
> dependency, then verify again that the test fails without the compiler fix
>
> and passes with it.
>
> Richard Biener <[email protected]> 于2026年8月17日周一 22:09写道:
>
>> On Fri, 14 Aug 2026, Longjun Luo wrote:
>>
>> > IPA split clones are created after early debug generation, so they have
>> no
>> > early DIE of their own and the exact die_ref_for_decl lookup for such a
>> > clone fails during initial LTO streaming.
>> >
>> > Without a reference of its own, the clone's concrete DIE has to follow
>> its
>> > origin declaration.  WPA tree merging selects a prevailing origin, and
>> the
>> > external DIE reference retained for it can name a different input TU,
>> while
>> > the clone's parameters and local variables keep references to their
>> physical
>> > input TU.  The concrete subprogram DIE and its children then carry
>> abstract
>> > origins from different TUs.
>>
>> I was looking for this in the DWARF for the testcase when not patched.
>> I can see
>>
>>  <1><1c1>: Abbrev Number: 5 (DW_TAG_subprogram)
>>     <1c2>   DW_AT_abstract_origin: <0x508>
>>     <1c6>   DW_AT_low_pc      : 0x6e
>>     <1ce>   DW_AT_high_pc     : 0x5d
>>     <1d6>   DW_AT_frame_base  : 1 byte block: 9c
>> (DW_OP_call_frame_cfa)
>>     <1d8>   DW_AT_call_all_calls: 1
>>     <1d8>   DW_AT_sibling     : <0x256>
>>  <2><1dc>: Abbrev Number: 2 (DW_TAG_formal_parameter)
>>     <1dd>   DW_AT_abstract_origin: <0x6b3>
>>     <1e1>   DW_AT_location    : 0xcc (location list)
>>     <1e5>   DW_AT_GNU_locviews: 0xc8
>>
>> where 508 refers to a DIE with specification at 459 (for printable_length)
>> where the formal parameter above is part of the 6a6 subprogram DIE
>> with specification at 5f7 (also for printable_length).
>>
>> So the inconsistency arises because the FUNCTION_DECLs
>> DECL_ABSTRACT_ORIGIN is subject to LTO symbol merging while the
>> function-local streamed PARM_DECLs DECL_ABSTRACT_ORIGIN is not.
>>
>> As you show DECL_ABSTRACT_ORIGIN, at least in absence of a
>> die_ref_for_decl, is a reference to a debug info instance
>> (my very original plan was to make a TREE_DIE_REF and put that
>> into DECL_ABSTRACT_ORIGIN, keeping only the early DWARF and not
>> the trees here).  So my prefered solution would be to not
>> replace DECL_ABSTRACT_ORIGIN during unification (short of, again,
>> not actually streaming DECL_ABSTRACT_ORIGIN but instead to
>> stream a DIE ref only, possibly materializing a fake decl
>> for this on read-in).
>>
>> I think the issue would show in "bogus" tree structures as well,
>> meaning the PARM_DECLs refer to different abstract origins
>> than the FUNCTION_DECL.  I wonder if we can possibly fixup
>> the local PARM_DECLs abstract origin during stream-in.
>> BLOCK_VARs should be similarly affected.  Or maybe we should
>> stream DECL_ABSTRACT_ORIGIN in the local section in the first
>> place?
>>
>> > Stream the early DIE reference of the clone's DECL_ORIGIN when an exact
>> > reference is unavailable.  This has to happen during initial LTO
>> streaming,
>> > before merging; afterwards the origin's reference no longer identifies
>> the
>> > clone's physical TU.  Do this at the streaming caller so that
>> > die_ref_for_decl keeps its exact-DECL lookup semantics, and restrict the
>> > fallback to cgraph_node::split_part clones.
>> >
>> > Add an LTO regression test that verifies that an address-bearing
>> concrete
>> > split-function DIE and its direct children refer to the same physical
>> input
>> > TU.
>> >
>> > Bootstrapped and regression-tested on x86_64-pc-linux-gnu (C and C++
>> only),
>> > with no new failures.  The new test fails without the patch and passes
>> with
>> > it.
>>
>> I'll note the testcase fails to link:
>>
>> FAIL: g++.dg/lto/pr126348 cp_lto_pr126348_0.o-cp_lto_pr126348_2.o link,
>> -O2 -g -gdwarf-5 -dA -save-temps -flto -flto-partition=one -fno-ipa-icf
>>
>> with
>>
>> ./libstdc++-v3/src/.libs/libstdc++.so: undefined reference to
>> `std::__cxx11::basic_string<char, std::char_traits<char>,
>> std::allocator<char> >::_M_create_plus(unsigned long, unsigned long)'
>>
>> maybe you want to add -r -flinker-output=nolto-rel to the set of linker
>> options?
>>
>> >       PR debug/126348
>> >
>> > gcc/ChangeLog:
>> >
>> >       * lto-streamer-out.cc (lto_write_tree_1): Use the abstract
>> origin's
>> >       DIE reference as a fallback when initially streaming an IPA split
>> >       clone.
>> >
>> > gcc/testsuite/ChangeLog:
>> >
>> >       * g++.dg/lto/pr126348.h: New test.
>> >       * g++.dg/lto/pr126348_0.C: New test.
>> >       * g++.dg/lto/pr126348_1.C: New test.
>> >       * g++.dg/lto/pr126348_2.C: New test.
>> >
>> > Signed-off-by: Longjun Luo <[email protected]>
>> > ---
>> > Apologies for the delayed follow-up; I only saw your reply in the
>> archive
>> > today.
>> >
>> > Changes in v2, addressing your review of v1:
>> >
>> > - Moved the fallback out of dwarf2out_die_ref_for_decl into the initial
>> LTO
>> >   streaming caller, so that primitive keeps its exact-DECL lookup
>> semantics,
>> >   and restricted it to cgraph_node::split_part clones.  This is the more
>> >   specific place you asked for.
>> >
>> > - On your first point: the concrete DIE does refer to an early DIE via
>> >   DW_AT_abstract_origin, but after WPA merging that early DIE can live
>> in a
>> >   different input TU than the one the clone's own parameters and local
>> >   variables refer to, and that is the inconsistency being fixed.  The
>> >   address-bearing late DIE is preserved: the test now also requires
>> >   DW_AT_low_pc or DW_AT_ranges on the concrete split-function DIE, so
>> the
>> >   fix cannot be satisfied by an abstract-only DIE.
>> >
>> > - The test uses three input TUs and checks the concrete subprogram DIE
>> and
>> >   its direct children against the same TU.
>> >
>> > The fallback is deliberately limited to split parts, the case covered by
>> > the reproducer and regression test.  Other compiler-generated clones
>> > without an early DIE have not been audited.
>> >
>> >  gcc/lto-streamer-out.cc               | 31 +++++++++++++++++++++++++--
>> >  gcc/testsuite/g++.dg/lto/pr126348.h   | 28 ++++++++++++++++++++++++
>> >  gcc/testsuite/g++.dg/lto/pr126348_0.C | 24 +++++++++++++++++++++
>> >  gcc/testsuite/g++.dg/lto/pr126348_1.C | 28 ++++++++++++++++++++++++
>> >  gcc/testsuite/g++.dg/lto/pr126348_2.C | 14 ++++++++++++
>> >  5 files changed, 123 insertions(+), 2 deletions(-)
>> >  create mode 100644 gcc/testsuite/g++.dg/lto/pr126348.h
>> >  create mode 100644 gcc/testsuite/g++.dg/lto/pr126348_0.C
>> >  create mode 100644 gcc/testsuite/g++.dg/lto/pr126348_1.C
>> >  create mode 100644 gcc/testsuite/g++.dg/lto/pr126348_2.C
>> >
>> > diff --git a/gcc/lto-streamer-out.cc b/gcc/lto-streamer-out.cc
>> > index 7afc2673ea2..108945bb93d 100644
>> > --- a/gcc/lto-streamer-out.cc
>> > +++ b/gcc/lto-streamer-out.cc
>> > @@ -726,8 +726,35 @@ lto_write_tree_1 (struct output_block *ob, tree
>> expr, bool ref_p)
>> >      {
>> >        const char *sym;
>> >        unsigned HOST_WIDE_INT off;
>> > -      if (debug_info_level > DINFO_LEVEL_NONE
>> > -       && debug_hooks->die_ref_for_decl (expr, &sym, &off))
>> > +      bool have_ref = false;
>> > +
>> > +      if (debug_info_level > DINFO_LEVEL_NONE)
>> > +     {
>> > +       have_ref = debug_hooks->die_ref_for_decl (expr, &sym, &off);
>> > +
>> > +       /* IPA split clones are created after early debug and have no
>> > +          early DIE of their own.  During initial LTO streaming,
>> preserve
>> > +          the physical input TU by using the clone's abstract origin.
>> > +          This must happen before WPA tree merging can make the origin
>> > +          refer to a prevailing declaration from another input TU.
>> Keep
>> > +          die_ref_for_decl's exact-DECL lookup contract intact and
>> > +          restrict the fallback to the split clone that needs it.  */
>> > +       if (!have_ref
>> > +           && !in_lto_p
>> > +           && TREE_CODE (expr) == FUNCTION_DECL)
>> > +         {
>> > +           cgraph_node *node = cgraph_node::get (expr);
>> > +           if (node && node->split_part)
>> > +             {
>> > +               tree origin = DECL_ORIGIN (expr);
>> > +               if (origin != expr)
>> > +                 have_ref
>> > +                   = debug_hooks->die_ref_for_decl (origin, &sym,
>> &off);
>> > +             }
>> > +         }
>> > +     }
>> > +
>> > +      if (have_ref)
>> >       {
>> >         streamer_write_string (ob, ob->main_stream, sym, true);
>> >         streamer_write_uhwi (ob, off);
>> > diff --git a/gcc/testsuite/g++.dg/lto/pr126348.h
>> b/gcc/testsuite/g++.dg/lto/pr126348.h
>> > new file mode 100644
>> > index 00000000000..eac6698064a
>> > --- /dev/null
>> > +++ b/gcc/testsuite/g++.dg/lto/pr126348.h
>> > @@ -0,0 +1,28 @@
>> > +struct Location
>> > +{
>> > +  const char *file;
>> > +  const char *function;
>> > +  int line;
>> > +};
>> > +
>> > +extern void fail (int, const char *, const Location &)
>> > +  __attribute__ ((noreturn, cold, noipa));
>> > +extern void note (const char *, int) __attribute__ ((cold, noipa));
>> > +
>> > +static const char source_file[] = __BASE_FILE__;
>> > +
>> > +struct Buffer
>> > +{
>> > +  int length;
>> > +
>> > +  int printable_length () const
>> > +  {
>> > +    if (__builtin_expect (length < 1024, 1))
>> > +      return length;
>> > +    const Location location = { source_file, __func__, __LINE__ };
>> > +    note (location.file, location.line);
>> > +    note (location.function, length);
>> > +    note (location.file, length + 1);
>> > +    fail (3, "length < 1024", location);
>> > +  }
>> > +};
>> > diff --git a/gcc/testsuite/g++.dg/lto/pr126348_0.C
>> b/gcc/testsuite/g++.dg/lto/pr126348_0.C
>> > new file mode 100644
>> > index 00000000000..bca7cada808
>> > --- /dev/null
>> > +++ b/gcc/testsuite/g++.dg/lto/pr126348_0.C
>> > @@ -0,0 +1,24 @@
>> > +/* PR debug/126348 */
>> > +/* Verify that an out-of-line split function and its direct children
>> retain
>> > +   the same input TU provenance, and that the function has an
>> address-bearing
>> > +   late DIE.  */
>> > +/* { dg-lto-do link } */
>> > +/* { dg-skip-if "No DWARF debug support" { hppa*-*-hpux* } } */
>> > +/* { dg-skip-if "AIX DWARF5" { powerpc-ibm-aix* } } */
>> > +/* { dg-lto-options { { -O2 -g -gdwarf-5 -dA -save-temps -flto
>> -flto-partition=one -fno-ipa-icf } } } */
>> > +/* { dg-final { scan-lto-assembler
>> "DW_TAG_subprogram\\)(?:\[^\n\]*\n){1,6}\[^\n\]*pr126348_0\[^\n\]*DW_AT_abstract_origin(?:\[^\n\]*\n){1,4}\[^\n\]*DW_AT_(?:low_pc|ranges)(?:\[^\n\]*\n){1,8}\[^\n\]*DW_TAG_formal_parameter\\)(?:\[^\n\]*\n){1,6}\[^\n\]*pr126348_0\[^\n\]*DW_AT_abstract_origin(?:\[^\n\]*\n){1,4}\[^\n\]*DW_TAG_variable\\)(?:\[^\n\]*\n){1,6}\[^\n\]*pr126348_0\[^\n\]*DW_AT_abstract_origin"
>> } } */
>> > +/* { dg-final { scan-lto-assembler
>> "DW_TAG_subprogram\\)(?:\[^\n\]*\n){1,6}\[^\n\]*pr126348_1\[^\n\]*DW_AT_abstract_origin(?:\[^\n\]*\n){1,4}\[^\n\]*DW_AT_(?:low_pc|ranges)(?:\[^\n\]*\n){1,8}\[^\n\]*DW_TAG_formal_parameter\\)(?:\[^\n\]*\n){1,6}\[^\n\]*pr126348_1\[^\n\]*DW_AT_abstract_origin(?:\[^\n\]*\n){1,4}\[^\n\]*DW_TAG_variable\\)(?:\[^\n\]*\n){1,6}\[^\n\]*pr126348_1\[^\n\]*DW_AT_abstract_origin"
>> } } */
>> > +
>> > +#include "pr126348.h"
>> > +
>> > +__attribute__ ((noinline)) int
>> > +one (const Buffer &buffer)
>> > +{
>> > +  return buffer.printable_length ();
>> > +}
>> > +
>> > +__attribute__ ((noinline)) int
>> > +one_extra (const Buffer &buffer)
>> > +{
>> > +  return buffer.printable_length ();
>> > +}
>> > diff --git a/gcc/testsuite/g++.dg/lto/pr126348_1.C
>> b/gcc/testsuite/g++.dg/lto/pr126348_1.C
>> > new file mode 100644
>> > index 00000000000..bad1a25aa63
>> > --- /dev/null
>> > +++ b/gcc/testsuite/g++.dg/lto/pr126348_1.C
>> > @@ -0,0 +1,28 @@
>> > +#include "pr126348.h"
>> > +
>> > +__attribute__ ((noinline)) int one (const Buffer &);
>> > +__attribute__ ((noinline)) int one_extra (const Buffer &);
>> > +
>> > +void
>> > +fail (int, const char *, const Location &)
>> > +{
>> > +  __builtin_trap ();
>> > +}
>> > +
>> > +void
>> > +note (const char *, int)
>> > +{
>> > +  asm volatile ("" ::: "memory");
>> > +}
>> > +
>> > +__attribute__ ((noinline)) int
>> > +two (const Buffer &buffer)
>> > +{
>> > +  return buffer.printable_length ();
>> > +}
>> > +
>> > +__attribute__ ((noinline)) int
>> > +two_extra (const Buffer &buffer)
>> > +{
>> > +  return buffer.printable_length ();
>> > +}
>> > diff --git a/gcc/testsuite/g++.dg/lto/pr126348_2.C
>> b/gcc/testsuite/g++.dg/lto/pr126348_2.C
>> > new file mode 100644
>> > index 00000000000..7a4015f3b49
>> > --- /dev/null
>> > +++ b/gcc/testsuite/g++.dg/lto/pr126348_2.C
>> > @@ -0,0 +1,14 @@
>> > +#include "pr126348.h"
>> > +
>> > +__attribute__ ((noinline)) int one (const Buffer &);
>> > +__attribute__ ((noinline)) int one_extra (const Buffer &);
>> > +__attribute__ ((noinline)) int two (const Buffer &);
>> > +__attribute__ ((noinline)) int two_extra (const Buffer &);
>> > +
>> > +int
>> > +main (int argc, char **)
>> > +{
>> > +  Buffer buffer = { argc };
>> > +  return one (buffer) + one_extra (buffer)
>> > +      + two (buffer) + two_extra (buffer);
>> > +}
>> >
>>
>> --
>> Richard Biener <[email protected]>
>> SUSE Software Solutions Germany GmbH,
>> Frankenstrasse 146, 90461 Nuernberg, Germany;
>> GF: Jochen Jaser, Andrew McDonald, Abhinav Puri; (HRB 36809, AG Nuernberg)
>>
>

Reply via email to