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