gdb: fix until behavior with trailing !is_stmt lines
authorBruno Larsen <blarsen@redhat.com>
Wed, 26 Jan 2022 13:08:13 +0000 (10:08 -0300)
committerAndrew Burgess <aburgess@redhat.com>
Fri, 11 Feb 2022 15:15:48 +0000 (15:15 +0000)
When using the command "until", it is expected that GDB will exit a
loop if the current instruction is the last one related to that loop.
However, if there were trailing non-statement instructions, "until"
would just behave as "next".  This was noticeable in clang-compiled
code, but might happen with gcc-compiled as well.  PR gdb/17315 relates
to this problem, as running gdb.base/watchpoint.exp with clang
would fail for this reason.

To better understand this issue, consider the following source code,
with line numbers marked on the left:

  10: for (i = 0; i < 10; ++i)
  11:     loop_body ();
  12:   other_stuff ();

If we transform this to pseudo-assembler, and generate a line table,
we could end up with something like this:

  Address | Pseudo-Assembler | Line | Is-Statement?

  0x100   | i = 0            | 10   | Yes
  0x104   | loop_body ()     | 11   | Yes
  0x108   | i = i + 1        | 10   | Yes
  0x10c   | if (i < 10):     | 10   | No
  0x110   |     goto 0x104   | 10   | No
  0x114   | other_stuff ()   | 12   | Yes

Notice the two non-statement instructions at the end of the loop.

The problem is that when we reach address 0x108 and use 'until',
hoping to leave the loop, GDB sets up a stepping range that runs from
the start of the function (0x100 in our example) to the end of the
current line table entry, that is 0x10c in our example.  GDB then
starts stepping forward.

When 0x10c is reached GDB spots that we have left the stepping range,
that the new location is not a statement, and that the new location is
associated with the same source line number as the previous stepping
range.  GDB then sets up a new stepping range that runs from 0x10c to
0x114, and continues stepping forward.

Within that stepping range the inferior hits the goto (at 0x110) and
loops back to address 0x104.

At 0x104 GDB spots that we have left the previous stepping range, that
the new address is marked as a statement, and that the new address is
for a different source line.  As a result, GDB stops and returns
control to the user.  This is not what the user was expecting, they
expected GDB to exit the loop.

The fix proposed in this patch, is that, when the user issues the
'until' command, and GDB sets up the initial stepping range, GDB will
check subsequent SALs (symtab_and_lines) to see if they are
non-statements associated with the same line number.  If they are then
the end of the initial stepping range is extended to the end of the
non-statement SALs.

In our example above, the user is at 0x108 and uses 'until', GDB now
sets up a stepping range from the start of the function 0x100 to
0x114, the first address associated with a different line.

Now as GDB steps around the loop it never leaves the initial stepping
range.  It is only when GDB exits the loop that we leave the stepping
range, and the stepping finishes at address 0x114.

This patch also adds a test case that can be run with gcc to test that
this functionality is not broken in the future.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=17315

gdb/infcmd.c
gdb/testsuite/gdb.base/until-trailing-insns.c [new file with mode: 0644]
gdb/testsuite/gdb.base/until-trailing-insns.exp [new file with mode: 0644]

index 48dda9b23ee2a04b72b63d6f6c3eb9f6c5e74d96..0e1cfcbadcd6e57ed4244f2247a0fe43258fb266 100644 (file)
@@ -1346,6 +1346,45 @@ until_next_command (int from_tty)
 
       tp->control.step_range_start = BLOCK_ENTRY_PC (SYMBOL_BLOCK_VALUE (func));
       tp->control.step_range_end = sal.end;
+
+      /* By setting the step_range_end based on the current pc, we are
+        assuming that the last line table entry for any given source line
+        will have is_stmt set to true.  This is not necessarily the case,
+        there may be additional entries for the same source line with
+        is_stmt set false.  Consider the following code:
+
+        for (int i = 0; i < 10; i++)
+          loop_body ();
+
+        Clang-13, will generate multiple line table entries at the end of
+        the loop all associated with the 'for' line.  The first of these
+        entries is marked is_stmt true, but the other entries are is_stmt
+        false.
+
+        If we only use the values in SAL, then our stepping range may not
+        extend to the end of the loop. The until command will reach the
+        end of the range, find a non is_stmt instruction, and step to the
+        next is_stmt instruction. This stopping point, however, will be
+        inside the loop, which is not what we wanted.
+
+        Instead, we now check any subsequent line table entries to see if
+        they are for the same line.  If they are, and they are marked
+        is_stmt false, then we extend the end of our stepping range.
+
+        When we finish this process the end of the stepping range will
+        point either to a line with a different line number, or, will
+        point at an address for the same line number that is marked as a
+        statement.  */
+
+      struct symtab_and_line final_sal
+       = find_pc_line (tp->control.step_range_end, 0);
+
+      while (final_sal.line == sal.line && final_sal.symtab == sal.symtab
+            && !final_sal.is_stmt)
+       {
+         tp->control.step_range_end = final_sal.end;
+         final_sal = find_pc_line (final_sal.end, 0);
+       }
     }
   tp->control.may_range_step = 1;
 
diff --git a/gdb/testsuite/gdb.base/until-trailing-insns.c b/gdb/testsuite/gdb.base/until-trailing-insns.c
new file mode 100644 (file)
index 0000000..749695b
--- /dev/null
@@ -0,0 +1,35 @@
+/* Copyright 2022 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+int
+main ()
+{                                                      /* TAG: main prologue */
+    asm ("main_label: .globl main_label");
+    asm ("loop_start: .globl loop_start");
+    int a, i;
+    i = 0;                                             /* TAG: loop assignment */
+    while (1)                                          /* TAG: loop line */
+      {
+       asm ("loop_condition: .globl loop_condition");
+       if (i >= 10) break;                             /* TAG: loop condition */
+       asm ("loop_code: .globl loop_code");
+       a = i;                                          /* TAG: loop code */
+       asm ("loop_increment: .globl loop_increment");
+       i++;                                            /* TAG: loop increment */
+       asm ("loop_jump: .globl loop_jump");
+      }
+    asm ("main_return: .globl main_return");
+    return 0;                                          /* TAG: main return */
+}
diff --git a/gdb/testsuite/gdb.base/until-trailing-insns.exp b/gdb/testsuite/gdb.base/until-trailing-insns.exp
new file mode 100644 (file)
index 0000000..e87f4a3
--- /dev/null
@@ -0,0 +1,183 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# This test sets up debug information for a loop as we see in some cases
+# from clang-13.  In this situation, instructions at both the start and end
+# of the loop are associated (in the line table), with the header line of
+# the loop (line 10 in the example below).
+#
+# At the end of the loop we see some instructions marked as not a statement,
+# but still associated with the same loop header line.  For example,
+# consider the following C code:
+#
+# 10:  for (i = 0; i < 10; ++i)
+# 11:     loop_body ();
+# 12:   other_stuff ();
+#
+# Transformed into the following pseudo-assembler, with associated line table:
+#
+# Address | Pseudo-Assembler | Line | Is-Statement?
+#
+# 0x100   | i = 0            | 10   | Yes
+# 0x104   | loop_body ()     | 11   | Yes
+# 0x108   | i = i + 1        | 10   | Yes
+# 0x10c   | if (i < 10):     | 10   | No
+# 0x110   |     goto 0x104   | 10   | No
+# 0x114   | other_stuff ()   | 12   | Yes
+#
+# Notice the two non-statement instructions at the end of the loop.
+#
+# The problem here is that when we reach address 0x108 and use 'until',
+# hoping to leave the loop, GDB sets up a stepping range that runs from the
+# start of the function (0x100 in our example) to the end of the current
+# line table entry, that is 0x10c in our example.  GDB then starts stepping
+# forward.
+#
+# When 0x10c is reached GDB spots that we have left the stepping range, that
+# the new location is not a statement, and that the new location is
+# associated with the same source line number as the previous stepping
+# range.  GDB then sets up a new stepping range that runs from 0x10c to
+# 0x114, and continues stepping forward.
+#
+# Within that stepping range the inferior hits the goto and loops back to
+# address 0x104.
+#
+# At 0x104 GDB spots that we have left the previous stepping range, that the
+# new address is marked as a statement, and that the new address is for a
+# different source line.  As a result, GDB stops and returns control to the
+# user.  This is not what the user was expecting, they expected GDB not to
+# stop until they were outside of the loop.
+#
+# The fix is that, when the user issues the 'until' command, and GDB sets up
+# the initial stepping range, GDB will check subsequent SALs to see if they
+# are non-statements associated with the same line number.  If they are then
+# the end of the initial stepping range is pushed out to the end of the
+# non-statement SALs.
+#
+# In our example above, the user is at 0x108 and uses 'until'.  GDB now sets
+# up a stepping range from the start of the function 0x100 to 0x114, the
+# first address associated with a different line.
+#
+# Now as GDB steps around the loop it never leaves the initial stepping
+# range.  It is only when GDB exits the loop that we leave the stepping
+# range, and the stepping finishes at address 0x114.
+#
+# This test checks this behaviour using the DWARF assembler.
+
+load_lib dwarf.exp
+
+# This test can only be run on targets which support DWARF-2 and use gas.
+if {![dwarf2_support]} {
+    unsupported "dwarf2 support required for this test"
+    return 0
+}
+
+if [get_compiler_info] {
+    return -1
+}
+
+# The DWARF assembler requires the gcc compiler.
+if {!$gcc_compiled} {
+    unsupported "gcc is required for this test"
+    return 0
+}
+
+standard_testfile .c .S
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
+    return -1
+}
+
+set asm_file [standard_output_file $srcfile2]
+Dwarf::assemble $asm_file {
+    global srcdir subdir srcfile
+    declare_labels integer_label L
+    set int_size [get_sizeof "int" 4]
+
+    # Find start address and length for our functions.
+    lassign [function_range main [list ${srcdir}/${subdir}/$srcfile]] \
+       main_start main_len
+    set main_end "$main_start + $main_len"
+
+    cu {} {
+       compile_unit {
+           {language @DW_LANG_C}
+           {name until-trailing-isns.c}
+           {stmt_list $L DW_FORM_sec_offset}
+           {low_pc 0 addr}
+       } {
+           subprogram {
+               {external 1 flag}
+               {name main}
+               {low_pc $main_start addr}
+               {high_pc $main_len DW_FORM_data4}
+           }
+       }
+    }
+
+    lines {version 2 default_is_stmt 1} L {
+       include_dir "${srcdir}/${subdir}"
+       file_name "$srcfile" 1
+
+       # Generate a line table program.  This mimicks clang-13's behavior
+       # of adding some !is_stmt at the end of a loop line, making until
+       # not work properly.
+       program {
+           {DW_LNE_set_address $main_start}
+           {line [gdb_get_line_number "TAG: main prologue"]}
+           {DW_LNS_copy}
+           {DW_LNE_set_address loop_start}
+           {line [gdb_get_line_number "TAG: loop line"]}
+           {DW_LNS_copy}
+           {DW_LNE_set_address loop_condition}
+           {line [gdb_get_line_number "TAG: loop line"]}
+           {DW_LNS_negate_stmt}
+           {DW_LNS_copy}
+           {DW_LNE_set_address loop_code}
+           {line [gdb_get_line_number "TAG: loop code"]}
+           {DW_LNS_negate_stmt}
+           {DW_LNS_copy}
+           {DW_LNE_set_address loop_increment}
+           {line [gdb_get_line_number "TAG: loop line"]}
+           {DW_LNS_copy}
+           {DW_LNE_set_address loop_jump}
+           {line [gdb_get_line_number "TAG: loop line"]}
+           {DW_LNS_negate_stmt}
+           {DW_LNS_copy}
+           {DW_LNE_set_address main_return}
+           {line [gdb_get_line_number "TAG: main return"]}
+           {DW_LNS_negate_stmt}
+           {DW_LNS_copy}
+           {DW_LNE_set_address $main_end}
+           {line [expr [gdb_get_line_number "TAG: main return"] + 1]}
+           {DW_LNS_copy}
+           {DW_LNE_end_sequence}
+       }
+    }
+
+}
+
+if { [prepare_for_testing "failed to prepare" ${testfile} \
+       [list $srcfile $asm_file] {nodebug} ] } {
+    return -1
+}
+
+if ![runto_main] {
+    return -1
+}
+
+gdb_test "next" ".* TAG: loop code .*" "inside the loop"
+gdb_test "next" ".* TAG: loop line .*" "ending of loop"
+gdb_test "until" ".* TAG: main return .*" "left loop"