Re: [PATCH v2] debug: Preserve TU provenance for IPA split clones in LTO [PR debug/126348]

Richard Biener <[email protected]>
Newsgroups gmane.comp.gcc.patches
Message-ID <[email protected]>
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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.