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)