https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124864
Longjun Luo <luolongjuna at gmail dot com> changed:
What |Removed |Added
----------------------------------------------------------------------------
CC| |luolongjuna at gmail dot com
--- Comment #7 from Longjun Luo <luolongjuna at gmail dot com> ---
Following up on comments 5 and 6, here are two multi-instance testcases for the
reparenting question in comment 6, plus a plain C lexical-block case showing
that the issue is not specific to C++ or constructor clones.
Tested on x86_64 GNU/Linux with GCC 17.0.0 (g:bd921def14a4), GDB 17.2 and
binutils 2.46.1 across unpatched LTO, LTO with attachment 64213, and non-LTO
builds.
GCC 17 build configuration:
--enable-languages=c,c++ --disable-bootstrap --disable-multilib
--disable-nls --disable-werror --enable-checking=yes
--without-isl --with-system-zlib
A. Constructor with separate C1 and C2 instances (-O0)
Because A has a virtual base, A::A emits separate C1 (complete-object, executed
for 'A a') and C2 (base-object, executed for 'B b') bodies sharing static x.
Stopping at repro.C:11, 'print x' gives:
Instance Unpatched LTO With att. 64213 Non-LTO
----------- --------------- --------------- -------
C1 (in A a) <optimized out> <optimized out> 7
C2 (in B b) <optimized out> 7 7
As expected in comment 6, reparenting the single limbo DIE into C2 (emitted
first) fixes C2 while leaving C1 without a location-bearing x DIE.
B. One inline function inlined into two callers (-O2)
f is inlined into both first and second during LGEN. Stopping at repro.C:5 in
each caller:
Build Instance info locals (and MI) print x
--------------- --------- ------------------------- ------------------
Unpatched LTO in first result=8 (x omitted) 7
in second result=9 (x omitted) 7
With att. 64213 in first result=8, f(int)::x=7 No symbol "x" ...
in second result=9 (x omitted) 7
Non-LTO in first result=8, x=7 7
in second result=9, x=7 7
(GDB/MI '-stack-list-locals 1' and '-stack-list-variables 1' likewise omit x
wherever 'info locals' omits it.)
Reparenting moves the single limbo DIE into the DW_TAG_inlined_subroutine in
first, leaving second without one. Placing that DIE directly under
DW_TAG_inlined_subroutine also hits a separate GDB naming quirk: for a variable
inheriting DW_AT_external from its abstract origin, GDB's die_needs_namespace
treats DW_TAG_subprogram and DW_TAG_lexical_block as local scopes, but not
DW_TAG_inlined_subroutine, qualifying the symbol as 'f(int)::x' so unqualified
'print x' fails.
C. Two disjoint lexical blocks in an ordinary C function (-O2, C)
Each lexical block declares its own 'static volatile int x' (initialized to 11
and 22). Stopping at repro.c:7 and repro.c:12:
Stop Build info locals print x
------- --------------- ---------------------------- ---------------
Block 1 Unpatched LTO x=<optimized out>, result=11 <optimized out>
x=11, x=22
Non-LTO / 64213 x=11, result=11 11
Block 2 Unpatched LTO x=<optimized out>, result=33 <optimized out>
x=11, x=22
Non-LTO / 64213 x=22, result=33 22
In unpatched LTO DWARF, both address-bearing x DIEs are attached directly under
the function DIE (via DECL_CONTEXT in flush_limbo_die_list) rather than under
their respective lexical blocks, while the lexical blocks only inherit the
early x DIEs, which have no location. Here each block has a single concrete
instance, so preserving BLOCK_VARS and reparenting in attachment 64213 restores
the expected scoping.
Historical boundary and design notes
For a C++ testcase equivalent to Case C, linked DWARF places each
address-bearing x in its lexical block with GCC 8.1 and 8.5, but at function
scope with GCC 9.1. Non-LTO controls with GCC 8.5 and 9.1 retain lexical
placement. This release-level boundary is consistent with the BLOCK_VARS change
in r9-1322-g694dc72ebe8701 noted in comment 6. The original constructor
testcase already has the scope-placement problem in GCC 8.1's DWARF, so the
8-to-9 boundary applies specifically to the nested-block testcase.
Related cases filed while investigating this issue:
- PR128622: cross-TU case where the prevailing function body (from b.o) and
local static (from a.o) come from different inputs. Reparenting the limbo DIE
into the executing function makes x visible there, but places a variable whose
origin is in a.C under a function whose origin is in b.C, triggering a GDB
"different parents" complaint.
- PR128619: duplicated constructor/destructor parameters and locals under LTO
(including the duplicated 'this' on this PR's original testcase and on both
instances in Case A above).
- PR128621: the same find_decls_types_r loop also strips TYPE_DECLs, allowing
later scope cleanup to prune a typedef-only outer block, which causes duplicate
locals in GDB.
Cases A and B and PR128622 show the limits of reparenting a single
location-bearing DIE. Following the direction suggested at the end of comment
5, I am preparing an RFC exploring per-instance DIEs for local statics: each
refers to the corresponding variable in the instance's early scope, while its
DW_AT_location describes the selected storage definition (for inline instances,
a direct child of DW_TAG_inlined_subroutine also hits the GDB naming quirk in
Case B).
Reproducers
Save each case in a separate directory.
=== Case A ===
--- repro.C ---
volatile int sink;
struct V { int value; };
struct A : virtual V
{
A ();
};
A::A ()
{
static volatile int x = 7;
sink = x;
asm volatile ("nop" : : : "memory");
}
struct B : A {};
int main ()
{
A a;
B b;
return sink != 7;
}
--- run.sh ---
#!/bin/sh
set -eu
export LC_ALL=C
mode=${1:--flto}
"${CXX:-g++}" -O0 -g -gdwarf-5 "$mode" -flto-partition=one \
-no-pie repro.C -o repro
"${GDB:-gdb}" -q -nx -batch -iex 'set debuginfod enabled off' \
-ex 'break repro.C:11' -ex run \
-ex 'info locals' -ex 'print x' \
-ex continue -ex 'info locals' -ex 'print x' repro
Run with:
CXX=g++ GDB=gdb sh run.sh -flto
readelf --debug-dump=info --wide repro
CXX=g++ GDB=gdb sh run.sh -fno-lto
=== Case B ===
--- repro.C ---
inline int f (int n)
{
static volatile int x = 7;
int result = x + n;
asm volatile ("nop" : "+r" (result) : : "memory");
return result;
}
__attribute__((noipa)) int first (int n) { return f (n); }
__attribute__((noipa)) int second (int n) { return f (n); }
int main ()
{
int a = first (1);
int b = second (2);
return a + b != 17;
}
--- run.sh ---
#!/bin/sh
set -eu
export LC_ALL=C
mode=${1:--flto}
"${CXX:-g++}" -O2 -g -gdwarf-5 "$mode" -flto-partition=one \
-no-pie repro.C -o repro
"${GDB:-gdb}" -q -nx -batch -iex 'set debuginfod enabled off' \
-ex 'break repro.C:5' -ex run \
-ex 'info locals' -ex 'print x' \
-ex 'interpreter-exec mi "-stack-list-locals 1"' \
-ex 'interpreter-exec mi "-stack-list-variables 1"' \
-ex continue -ex 'info locals' -ex 'print x' \
-ex 'interpreter-exec mi "-stack-list-locals 1"' \
-ex 'interpreter-exec mi "-stack-list-variables 1"' repro
Run with:
CXX=g++ GDB=gdb sh run.sh -flto
readelf --debug-dump=info --wide repro
CXX=g++ GDB=gdb sh run.sh -fno-lto
=== Case C ===
--- repro.c ---
__attribute__((noinline, noclone)) int f (void)
{
int result;
{
static volatile int x = 11;
result = x;
asm volatile ("nop" : "+r" (result) : : "memory");
}
{
static volatile int x = 22;
result += x;
asm volatile ("nop" : "+r" (result) : : "memory");
}
return result;
}
int main (void) { return f () != 33; }
--- run.sh ---
#!/bin/sh
set -eu
export LC_ALL=C
mode=${1:--flto}
"${CC:-gcc}" -O2 -g -gdwarf-5 "$mode" -flto-partition=one \
-no-pie repro.c -o repro
"${GDB:-gdb}" -q -nx -batch -iex 'set debuginfod enabled off' \
-ex 'break repro.c:7' -ex 'break repro.c:12' -ex run \
-ex 'info locals' -ex 'print x' \
-ex continue -ex 'info locals' -ex 'print x' repro
Run with:
CC=gcc GDB=gdb sh run.sh -flto
readelf --debug-dump=info --wide repro
CC=gcc GDB=gdb sh run.sh -fno-lto