https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105036
Bug ID: 105036 Summary: Missing variables when debugging due to overlapping ranges after unrolling, instruction scheduling, and inlining at -O3 Product: gcc Version: 12.0 Status: UNCONFIRMED Severity: normal Priority: P3 Component: debug Assignee: unassigned at gcc dot gnu.org Reporter: assaiante at diag dot uniroma1.it Target Milestone: --- In this minimized C example, variables i and j that index the two nested loops are missing when putting a breakpoint on the statement in the inner loop and executing it for the very first time. Then, they become visible when executing it at iteration (0,1) and still are when about to exit the outer loop, as expected with unrolling. This behavior appears to be caused by i and j’s DWARF location info, for the (0,0) iteration only, overlapping with DWARF location info for the inlined function foo. We observe the issue at -O3 with the root cause likely being schedule-insns2, combined with the effects of inlining foo and loop unrolling. We also noted that if we shorten the loop counts in the example, for instance to have i stopping at 3 and j at 4, the issue is no longer present. Please find below a detailed analysis for -O3 on x64 and a quick assessment on past gcc versions where the issue is sometimes not present. $ cat a.c volatile int a; int b; unsigned c[35]; int d; void foo() { b = 3; for (; b >= -10; b = b - 4) { d++; } } int main() { int i, j; foo(); i = 0; for (; i < 5; i++) { j = 0; for (; j < 7; j++) a = c[i * 5 + j]; } } GCC and GDB version (GCC commit id: 500d3f0a302): $ gcc --version gcc (GCC) 12.0.0 20211227 (experimental) Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ gdb --version GNU gdb (GDB) 11.2 Copyright (C) 2022 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. GDB trace: $ gcc -O3 -g a.c -o opt $ gdb -q opt Reading symbols from opt... (gdb) b 18 Breakpoint 1 at 0x4003b6: /home/stepping/50/reduce/a.c:18. (2 locations) (gdb) r Starting program: /home/stepping/50/reduce/opt Breakpoint 1, foo () at test.c:18 18 a = c[i * 5 + j]; (gdb) info locals No locals. (gdb) n Breakpoint 1, main () at test.c:18 18 a = c[i * 5 + j]; (gdb) info locals i = 0 j = 1 (gdb) n 15 for (; i < 5; i++) { (gdb) info locals i = 5 j = 7 GDB trace with instruction stepping: $ gdb -q opt Reading symbols from opt... (gdb) b 18 Breakpoint 1 at 0x4003b6: /home/stepping/50/reduce/a.c:18. (2 locations) (gdb) r Starting program: /home/stepping/50/reduce/opt Breakpoint 1, foo () at test.c:18 18 a = c[i * 5 + j]; (gdb) info loc No locals. (gdb) si 0x00000000004003bd in main () at test.c:18 18 a = c[i * 5 + j]; (gdb) info loc i = 0 j = 0 (gdb) si Breakpoint 1, main () at test.c:18 18 a = c[i * 5 + j]; (gdb) info loc i = 0 j = 1 We can see how in the source-level stepping GDB trace, when the breakpoint at line 18 is hit for the first time, variables i and j are not available due to the location overlap. Instead, when stepping through instructions, we see the correct value of i and j from the second instruction associated with the first execution of line 18 (since the first one is the only one overlapping). ASM at -O3 (shortened): 00000000004003a0 <main>: 4003a0: 8b 05 da 0c 20 00 mov 0x200cda(%rip),%eax # 601080 <c> 4003a6: 8b 15 e8 0c 20 00 mov 0x200ce8(%rip),%edx # 601094 <c+0x14> 4003ac: c7 05 56 0d 20 00 f3 movl $0xfffffff3,0x200d56(%rip) # 60110c <b> 4003b3: ff ff ff 4003b6: 83 05 a3 0c 20 00 04 addl $0x4,0x200ca3(%rip) # 601060 <d> 4003bd: 89 05 4d 0d 20 00 mov %eax,0x200d4d(%rip) # 601110 <a> 4003c3: 8b 05 bb 0c 20 00 mov 0x200cbb(%rip),%eax # 601084 <c+0x4> [ . . . ] 400519: 8b 05 c9 0b 20 00 mov 0x200bc9(%rip),%eax # 6010e8 <c+0x68> 40051f: 89 05 eb 0b 20 00 mov %eax,0x200beb(%rip) # 601110 <a> 400525: 31 c0 xor %eax,%eax 400527: c3 retq 400528: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40052f: 00 >From the ASM we see that function foo is inlined within the function main and all the loops are fully unrolled. DWARF info at -O3: 0x000000a0: DW_TAG_subprogram DW_AT_external (true) DW_AT_name ("main") DW_AT_decl_file ("/home/stepping/50/reduce/a.c") DW_AT_decl_line (12) DW_AT_decl_column (0x05) DW_AT_type (0x0000003d "int") DW_AT_low_pc (0x00000000004003a0) DW_AT_high_pc (0x0000000000400528) DW_AT_frame_base (DW_OP_call_frame_cfa) DW_AT_call_all_calls (true) DW_AT_sibling (0x00000115) 0x000000c2: DW_TAG_variable DW_AT_name ("i") DW_AT_decl_column (0x07) DW_AT_type (0x0000003d "int") DW_AT_location (0x00000018: [0x00000000004003b6, 0x0000000000400405): DW_OP_lit0, DW_OP_stack_value [ . . . ] [0x0000000000400525, 0x0000000000400528): DW_OP_lit5, DW_OP_stack_value) DW_AT_unknown_2137 (0x0000000c) 0x000000d2: DW_TAG_variable DW_AT_name ("j") DW_AT_decl_column (0x0a) DW_AT_type (0x0000003d "int") DW_AT_location (0x0000009f: [0x00000000004003b6, 0x00000000004003c3): DW_OP_lit0, DW_OP_stack_value [ . . . ] [0x0000000000400525, 0x0000000000400528): DW_OP_lit7, DW_OP_stack_value) DW_AT_unknown_2137 (0x0000004f) 0x000000e0: DW_TAG_inlined_subroutine DW_AT_abstract_origin (0x000000f6 "foo") DW_AT_entry_pc (0x00000000004003a0) DW_AT_unknown_2138 (0x03) DW_AT_ranges (0x0000000c [0x00000000004003a0, 0x00000000004003a0) [0x00000000004003b6, 0x00000000004003bd)) DW_AT_call_file ("/home/stepping/50/reduce/test.c") DW_AT_call_line (13) DW_AT_call_column (0x03) >From the DWARF info at -O3, the first entry in the location definition of variables i and j is defined as: [0x00000000004003b6, 0x0000000000400405): for variable i, with value 0 [0x00000000004003b6, 0x00000000004003c3): for variable j, with value 0 Those values represent the variables upon entering for the first time the body of the nested loop that has been unrolled. The range of addresses that represents the inlined foo function is: [0x00000000004003b6, 0x00000000004003bd). We can see that these ranges overlap, causing the variables i and j to not be available during the debugging of the first iteration of the nested loop in the main function, since according to debug info, at such address, the program is inside function foo where variables i and j do not exist. Through some testing we found out that the optimization that ultimately makes the variables disappear is -fschedule-insns2. By disabling it with -fno-schedule-insns2, the ranges of the variables are correctly defined and do not overlap with the location of the inlined function. About inlining, we can see how some instructions of function main are scheduled before instructions from the inlined function foo and this does not happen when the flag is disabled. ASM with -fno-schedule-insns2 (shortened): 00000000004003a0 <main>: 4003a0: 83 05 b9 0c 20 00 04 addl $0x4,0x200cb9(%rip) # 601060 <d> 4003a7: c7 05 5b 0d 20 00 f3 movl $0xfffffff3,0x200d5b(%rip) # 60110c <b> 4003ae: ff ff ff 4003b1: 8b 05 c9 0c 20 00 mov 0x200cc9(%rip),%eax # 601080 <c> 4003b7: 89 05 53 0d 20 00 mov %eax,0x200d53(%rip) # 601110 <a> [ . . . ] 40051f: 89 05 eb 0b 20 00 mov %eax,0x200beb(%rip) # 601110 <a> 400525: 31 c0 xor %eax,%eax 400527: c3 retq 400528: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40052f: 00 DWARF info with -fno-schedule-insn2 (shortened): 0x000000a0: DW_TAG_subprogram DW_AT_external (true) DW_AT_name ("main") DW_AT_decl_file ("/home/stepping/50/reduce/a.c") DW_AT_decl_line (12) DW_AT_decl_column (0x05) DW_AT_type (0x0000003d "int") DW_AT_low_pc (0x00000000004003a0) DW_AT_high_pc (0x0000000000400528) DW_AT_frame_base (DW_OP_call_frame_cfa) DW_AT_call_all_calls (true) DW_AT_sibling (0x0000011b) 0x000000c2: DW_TAG_variable DW_AT_name ("i") DW_AT_decl_column (0x07) DW_AT_type (0x0000003d "int") DW_AT_location (0x00000018: [0x00000000004003b1, 0x0000000000400405): DW_OP_lit0, DW_OP_stack_value [ . . . ] [0x0000000000400525, 0x0000000000400528): DW_OP_lit5, DW_OP_stack_value) DW_AT_unknown_2137 (0x0000000c) 0x000000d2: DW_TAG_variable DW_AT_name ("j") DW_AT_decl_column (0x0a) DW_AT_type (0x0000003d "int") DW_AT_location (0x0000009f: [0x00000000004003b1, 0x00000000004003bd): DW_OP_lit0, DW_OP_stack_value [ . . . ] [0x0000000000400525, 0x0000000000400528): DW_OP_lit7, DW_OP_stack_value) DW_AT_unknown_2137 (0x0000004f) 0x000000e0: DW_TAG_inlined_subroutine DW_AT_abstract_origin (0x00000102 "foo") DW_AT_entry_pc (0x00000000004003a0) DW_AT_unknown_2138 (0x03) DW_AT_low_pc (0x00000000004003a0) DW_AT_high_pc (0x00000000004003b1) DW_AT_call_file ("/home/stepping/50/reduce/a.c") DW_AT_call_line (13) DW_AT_call_column (0x03) In the DWARF info with -fno-schedule-insns2, the locations of both variables and inlined subroutine do not overlap anymore. We have tested the program with older gcc versions. Here is a recap of the behavior encountered on each one of them: gcc-{6,7}: variables i and j are marked as optimized out and function foo is inlined; gcc-{8,9}: variables i and j are visible and function foo in inlined; gcc-{10,11}: variables i and j not visible at first iteration (just like in the 500d3f0a302 git version that we tested) >From an analysis of DWARF info generated by older versions where the issue is not present, we think that those can be ignored since the DWARF info about the inlined function location was not generated at all (there was only an attribute on the function’s DIE telling that it had been inlined), so no overlap was possible.