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) >> >
