https://gcc.gnu.org/bugzilla/show_bug.cgi?id=128210
Bug ID: 128210
Summary: [debug] AArch64: stale local variable and parameter
values with -O3
Product: gcc
Version: 17.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: debug
Assignee: unassigned at gcc dot gnu.org
Reporter: MrEven132 at outlook dot com
Target Milestone: ---
Debugger: GNU GDB 17.2
With -O3, GDB displays stale values at two source positions in the same
program: value and copy are displayed as -1 on line 9 in update_result, and
index is displayed as 0 on line 20 in run_loop. With -O2, the values are 0, 0
and 1, respectively. The actual frame file, function and line are checked at
both stops; they are not inferred from the requested breakpoints.
Both symptoms disappear with -O3 -fno-schedule-insns and appear with -O2
-fschedule-insns. They exhibit the same scheduling/debug-location pattern,
although the precise GCC implementation defect has not been established.
Testcase
========
Save the following as case.c:
static int ready;
static int result;
static int update_result(void)
{
int value = -1;
int copy = -1;
ready = 1;
value = 0;
result = (copy = value) | 2;
return value;
}
static int marker;
static int run_loop(int index)
{
for (index = 0; index < 1; ++index) {
marker = 7;
}
return 0;
}
int main(void)
{
update_result();
run_loop(0);
return ready != 1 || result != 2 || marker != 7;
}
Compilation
===========
For an AArch64 target:
gcc -g -gdwarf-5 -gno-statement-frontiers -fno-inline -fno-ipa-sra -O2 case.c
-o O2.elf
gcc -g -gdwarf-5 -gno-statement-frontiers -fno-inline -fno-ipa-sra -O3 case.c
-o O3.elf
The same source and flags are used for both functions. -fno-inline retains the
separate functions, -fno-ipa-sra retains the parameter for inspection, and
-gno-statement-frontiers allows GDB source-line breakpoints to select the
relevant line-table entries. Both executables finish normally with exit status
zero.
Debugger reproduction
=====================
Open each executable in GDB and install both source-line breakpoints:
gdb -q O3.elf
(gdb) break case.c:9
(gdb) break case.c:20
(gdb) run
(gdb) frame
(gdb) info line *$pc
(gdb) print value
(gdb) print copy
(gdb) continue
(gdb) frame
(gdb) info line *$pc
(gdb) print index
(gdb) continue
Repeat the same commands with O2.elf. Neither breakpoint specifies a column or
instruction address. Each function provides a separate reproduction point in
the same executable.
Reproduction point 1: update_result, line 9
==========================================
-O2 captured output:
(gdb) frame
#0 update_result () at case.c:9
9 result = (copy = value) | 2;
(gdb) info line *$pc
Line 9 of "case.c" starts at address 0x4006b0 <update_result+16> and ends at
0x4006b8 <update_result+24>.
(gdb) x/i $pc
=> 0x4006b0 <update_result+16>: mov w0, #0x2 // #2
(gdb) print value
$3 = 0
(gdb) print copy
$4 = 0
-O3 captured output:
(gdb) frame
#0 update_result () at case.c:9
9 result = (copy = value) | 2;
(gdb) info line *$pc
Line 9 of "case.c" starts at address 0x4006ac <update_result+12> and ends at
0x4006b0 <update_result+16>.
(gdb) x/i $pc
=> 0x4006ac <update_result+12>: mov w2, #0x2 // #2
(gdb) print value
$3 = -1
(gdb) print copy
$4 = -1
Line 8 assigns zero to value before the statement on line 9. Thus value should
be displayed as 0 at line 9, rather than its initial value -1. The statement on
line 9 also assigns value to copy; copy is shown here as additional evidence,
while the already completed assignment to value is sufficient to demonstrate
the stale-value symptom.
Reproduction point 2: run_loop, line 20
======================================
-O2 captured output:
(gdb) frame
#0 run_loop (index=1) at case.c:20
20 }
(gdb) info line *$pc
Line 20 of "case.c" starts at address 0x4006cc <run_loop+12> and ends at
0x4006d4 <_fini>.
(gdb) x/i $pc
=> 0x4006cc <run_loop+12>: mov w0, #0x0 // #0
(gdb) print index
$6 = 1
-O3 captured output:
(gdb) frame
#0 run_loop (index=0) at case.c:20
20 }
(gdb) info line *$pc
Line 20 of "case.c" starts at address 0x4006c8 <run_loop+8> and ends at
0x4006cc <run_loop+12>.
(gdb) x/i $pc
=> 0x4006c8 <run_loop+8>: mov w0, #0x0 // #0
(gdb) print index
$6 = 0
At the closing brace on line 20, the loop has completed in source order and
index should be 1. GDB instead displays 0 with -O3. The closing brace is
associated with function-epilogue instructions; the function has not yet
returned to its caller at the breakpoint.
Absolute source paths have been shortened to case.c in these excerpts. The
capture script reads the variables before printing the full diagnostic
transcript, so its GDB value-history numbers differ from a manual session. The
actual frame source lines match the requests on both sides. Both reproduction
points were observed in each of three complete runs for each optimization
level, with normal exit status zero.
Instruction ordering and DWARF
=============================
In update_result, the relevant -O2 sequence is:
0x4006ac: str w2, [x0, #0x2c] // line 7: ready = 1
0x4006b0: mov w0, #2 // line 9; breakpoint
0x4006b4: str w0, [x1, #4] // line 9: store to result
With -O3, the instruction preparing the line-9 value is moved ahead of the
line-7 store:
0x4006ac: mov w2, #2 // line 9; breakpoint
0x4006b0: str w3, [x0, #0x2c] // line 7: ready = 1
0x4006b4: mov w0, #0 // line 11: return-value preparation
0x4006b8: str w2, [x1, #4] // line 9: store to result
The value and copy location lists are identical within each optimization level:
-O2, value and copy:
DW_AT_location:
[0x4006a0, 0x4006b0): DW_OP_const1s -1, DW_OP_stack_value
[0x4006b0, 0x4006c0): DW_OP_lit0, DW_OP_stack_value
-O3, value and copy:
DW_AT_location:
[0x4006a0, 0x4006b4): DW_OP_const1s -1, DW_OP_stack_value
[0x4006b4, 0x4006c0): DW_OP_lit0, DW_OP_stack_value
Thus the O3 breakpoint is attributed to line 9 before the location lists switch
to the updated value.
In run_loop, emitted as run_loop.constprop.0, the relevant -O2 sequence is:
0x4006c8: str w1, [x0, #0x34] // line 17: marker = 7
0x4006cc: mov w0, #0 // line 20; breakpoint
0x4006d0: ret // line 20
With -O3, the return-value preparation is moved ahead of the marker store:
0x4006c8: mov w0, #0 // line 20; breakpoint
0x4006cc: str w2, [x1, #0x34] // line 17: marker = 7
0x4006d0: ret // line 20
The index location lists are:
-O2, index:
DW_AT_location:
[0x4006c0, 0x4006cc): DW_OP_lit0, DW_OP_stack_value
[0x4006cc, 0x4006d4): DW_OP_lit1, DW_OP_stack_value
-O3, index:
DW_AT_location:
[0x4006c0, 0x4006d0): DW_OP_lit0, DW_OP_stack_value
[0x4006d0, 0x4006d4): DW_OP_lit1, DW_OP_stack_value
In both functions, an instruction attributed to a later source position is
scheduled before an earlier source-level store, while the variable-update
location ranges remain after that store. GDB follows the constant expressions
in the generated DWARF and displays the earlier values at the later source
position. This supports an interaction between instruction scheduling,
instruction source locations and variable-update debug records; it does not
establish the exact shared GCC root cause.
Optimization controls
=====================
All rows use -g -gdwarf-5 -gno-statement-frontiers -fno-inline -fno-ipa-sra.
Both breakpoints and actual frame lines remain 9 and 20:
Options value copy index
-O2 0 0 1
-O3 -1 -1 0
-O3 -fno-schedule-insns 0 0 1
-O2 -fschedule-insns -1 -1 0
With x86-64 GCC 17.0.0 20261003 and GDB 17.2, the same source and baseline
options displayed value=0, copy=0 and index=1 at both optimization levels in
three runs; these AArch64 symptoms were not reproduced in that comparison.
O3 line table
=============
The following is the complete decoded line-table row output from the O3
executable used above, compiled with -g -gdwarf-5 -gno-statement-frontiers
-fno-inline -fno-ipa-sra -O3.
Command:
llvm-dwarfdump --debug-line O3.elf
The table uses DWARF version 5. File indices 0 and 1 both refer to case.c.
Original row order, duplicate addresses, flags and both address sequences are
preserved:
Address Line Column File ISA Discriminator OpIndex Flags
------------------ ------ ------ ------ --- ------------- ------- -------------
0x00000000004006a0 4 1 1 0 0 0 is_stmt
0x00000000004006a0 7 11 1 0 0 0 is_stmt
0x00000000004006ac 9 12 1 0 0 0 is_stmt
0x00000000004006b0 7 11 1 0 0 0 is_stmt
0x00000000004006b4 11 1 1 0 0 0 is_stmt
0x00000000004006b8 9 12 1 0 0 0 is_stmt
0x00000000004006bc 11 1 1 0 0 0 is_stmt
0x00000000004006c0 14 12 1 0 0 0 is_stmt
0x00000000004006c0 17 16 1 0 32768 0 is_stmt
0x00000000004006c8 20 1 1 0 0 0 is_stmt
0x00000000004006cc 17 16 1 0 32768 0 is_stmt
0x00000000004006d0 20 1 1 0 0 0 is_stmt
0x00000000004006d4 20 1 1 0 0 0 is_stmt
end_sequence
0x0000000000400500 23 1 1 0 0 0 is_stmt
0x0000000000400508 24 5 1 0 0 0 is_stmt
0x000000000040050c 25 5 1 0 0 0 is_stmt
0x0000000000400510 26 18 1 0 0 0 is_stmt
0x000000000040051c 26 38 1 0 0 0 is_stmt
0x0000000000400524 26 38 1 0 3 0
0x0000000000400528 27 1 1 0 0 0 is_stmt
0x0000000000400530 26 23 1 0 1 0 is_stmt
0x000000000040053c 26 38 1 0 2 0 is_stmt
0x000000000040054c 26 38 1 0 0 0 is_stmt
end_sequence
The first stop is 0x4006ac, line 9, followed by line 7 at 0x4006b0. The second
stop is 0x4006c8, line 20, followed by line 17 at 0x4006cc. Both breakpoint
rows have is_stmt set. end_sequence rows mark address-sequence boundaries
rather than additional instructions.