public inbox for gdb-patches@sourceware.org
 help / color / mirror / Atom feed
* [PATCH 0/5] Fix for an assertion when unwinding with inline frames
@ 2021-05-29 20:57 Andrew Burgess
  2021-05-29 20:57 ` [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder Andrew Burgess
                   ` (5 more replies)
  0 siblings, 6 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-05-29 20:57 UTC (permalink / raw)
  To: gdb-patches

This series fixes an assertion I ran into when unwinding a stack
containing inline frames (patch #4).

I also fix an assertion from the 'set debug frame 1' code when I ran
into when trying to debug the first assertion (patch #5).

I also fix an assertion in the Python unwinder API that I ran into
while trying to write a test for the first assertion, this ended up
with me extending the functionality of the unwinder API a little,
which is nice (patch #1).

Then I needed to add some additional methods to the Python
Frame/PendingFrame API so I could write the test for the first
assertion (patch #3).

Finally, there was a small drive by clean up which related to the new
Frame/PendingFrame API extension (patch #2).

Thoughts welcome,

thanks,
Andrew

---

Andrew Burgess (5):
  gdb/python: handle saving user registers in a frame  unwinder
  gdb/python: move PyLong_From* calls into py-utils.c
  gdb/python: add PendingFrame.level and Frame.level methods
  gdb: prevent an assertion when computing the frame_id for an inline
    frame
  gdb: remove VALUE_FRAME_ID

 gdb/ChangeLog                                 |  34 +++++
 gdb/doc/ChangeLog                             |   6 +
 gdb/doc/python.texi                           |   9 ++
 gdb/frame.c                                   |  61 +++++---
 gdb/frame.h                                   |   4 -
 gdb/python/py-frame.c                         |  23 +++
 gdb/python/py-inferior.c                      |   2 +-
 gdb/python/py-unwind.c                        |  40 +++++
 gdb/python/py-utils.c                         |  12 ++
 gdb/python/python-internal.h                  |   2 +
 gdb/testsuite/ChangeLog                       |  23 +++
 .../gdb.base/inline-frame-bad-unwind.c        |  58 ++++++++
 .../gdb.base/inline-frame-bad-unwind.exp      | 139 ++++++++++++++++++
 .../gdb.base/inline-frame-bad-unwind.py       |  85 +++++++++++
 gdb/testsuite/gdb.python/py-frame.exp         |  11 ++
 .../gdb.python/py-pending-frame-level.c       |  49 ++++++
 .../gdb.python/py-pending-frame-level.exp     |  65 ++++++++
 .../gdb.python/py-pending-frame-level.py      |  55 +++++++
 .../gdb.python/py-unwind-user-regs.c          |  37 +++++
 .../gdb.python/py-unwind-user-regs.exp        |  98 ++++++++++++
 .../gdb.python/py-unwind-user-regs.py         |  72 +++++++++
 gdb/valops.c                                  |  17 ++-
 gdb/value.c                                   |   5 +-
 gdb/value.h                                   |   6 -
 24 files changed, 874 insertions(+), 39 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.py
 create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.c
 create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.exp
 create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.py
 create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.c
 create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.exp
 create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.py

-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-05-29 20:57 [PATCH 0/5] Fix for an assertion when unwinding with inline frames Andrew Burgess
@ 2021-05-29 20:57 ` Andrew Burgess
  2021-06-07 14:50   ` Tom Tromey
                     ` (2 more replies)
  2021-05-29 20:57 ` [PATCH 2/5] gdb/python: move PyLong_From* calls into py-utils.c Andrew Burgess
                   ` (4 subsequent siblings)
  5 siblings, 3 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-05-29 20:57 UTC (permalink / raw)
  To: gdb-patches

This patch came about because I wanted to write a frame unwinder that
would corrupt the backtrace in a particular way.  In order to achieve
what I wanted I ended up trying to write an unwinder like this:

  class FrameId(object):
      .... snip class definition ....

  class TestUnwinder(Unwinder):
      def __init__(self):
          Unwinder.__init__(self, "some name")

      def __call__(self, pending_frame):
          pc_desc = pending_frame.architecture().registers().find("pc")
          pc = pending_frame.read_register(pc_desc)

          sp_desc = pending_frame.architecture().registers().find("sp")
          sp = pending_frame.read_register(sp_desc)

          # ... snip code to decide if this unwinder applies or not.

          fid = FrameId(pc, sp)
          unwinder = pending_frame.create_unwind_info(fid)
          unwinder.add_saved_register(pc_desc, pc)
          unwinder.add_saved_register(sp_desc, sp)
          return unwinder

The important things here are the two calls:

          unwinder.add_saved_register(pc_desc, pc)
          unwinder.add_saved_register(sp_desc, sp)

On x86-64 these would fail with an assertion error:

  gdb/regcache.c:168: internal-error: int register_size(gdbarch*, int): Assertion `regnum >= 0 && regnum < gdbarch_num_cooked_regs (gdbarch)' failed.

What happens is that in unwind_infopy_add_saved_register (py-unwind.c)
we call register_size, as register_size should only be called on
cooked (real or pseudo) registers, and 'pc' and 'sp' are implemented
as user registers (at least on x86-64), we trigger the assertion.

A simple fix would be to check in unwind_infopy_add_saved_register if
the register number we are handling is a cooked register or not, if
not we can throw a 'Bad register' error back to the Python code.

However, I think we can do better.

Consider that at the CLI we can do this:

  (gdb) set $pc=0x1234

This works because GDB first evaluates '$pc' to get a register value,
then evaluates '0x1234' to create a value encapsulating the
immediate.  The contents of the immediate value are then copied back
to the location of the register value representing '$pc'.

The value location for a user-register will (usually) be the location
of the real register that was accessed, so on x86-64 we'd expect this
to be $rip.

So, in this patch I propose that in the unwinder code, when
add_saved_register is called, if it is passed a
user-register (i.e. non-cooked) then we first fetch the register,
extract the real register number from the value's location, and use
that new register number when handling the add_saved_register call.

If either the value location that we get for the user-register is not
a cooked register then we can throw a 'Bad register' error back to the
Python code, but in most cases this will not happen.

gdb/ChangeLog:

	* python/py-unwind.c (unwind_infopy_add_saved_register): Handle
	saving user registers.

gdb/testsuite/ChangeLog:

	* gdb.python/py-unwind-user-regs.c: New file.
	* gdb.python/py-unwind-user-regs.exp: New file.
	* gdb.python/py-unwind-user-regs.py: New file.
---
 gdb/ChangeLog                                 |  5 +
 gdb/python/py-unwind.c                        | 21 ++++
 gdb/testsuite/ChangeLog                       |  6 ++
 .../gdb.python/py-unwind-user-regs.c          | 37 +++++++
 .../gdb.python/py-unwind-user-regs.exp        | 98 +++++++++++++++++++
 .../gdb.python/py-unwind-user-regs.py         | 72 ++++++++++++++
 6 files changed, 239 insertions(+)
 create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.c
 create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.exp
 create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.py

diff --git a/gdb/python/py-unwind.c b/gdb/python/py-unwind.c
index 7c195eb539d..d6e2f85dbc1 100644
--- a/gdb/python/py-unwind.c
+++ b/gdb/python/py-unwind.c
@@ -27,6 +27,7 @@
 #include "python-internal.h"
 #include "regcache.h"
 #include "valprint.h"
+#include "user-regs.h"
 
 /* Debugging of Python unwinders.  */
 
@@ -265,6 +266,26 @@ unwind_infopy_add_saved_register (PyObject *self, PyObject *args)
       PyErr_SetString (PyExc_ValueError, "Bad register");
       return NULL;
     }
+
+  /* If REGNUM identifies a user register then *maybe* we can convert this
+     to a real (i.e. non-user) register.  The maybe qualifier is because we
+     don't know what user registers each target might add, however, the
+     following logic should work for the usual style of user registers,
+     where the read function just forwards the register read on to some
+     other register with no adjusting the value.  */
+  if (regnum >= gdbarch_num_cooked_regs (pending_frame->gdbarch))
+    {
+      struct value *user_reg_value
+	= value_of_user_reg (regnum, pending_frame->frame_info);
+      if (VALUE_LVAL (user_reg_value) == lval_register)
+	regnum = VALUE_REGNUM (user_reg_value);
+      if (regnum >= gdbarch_num_cooked_regs (pending_frame->gdbarch))
+	{
+	  PyErr_SetString (PyExc_ValueError, "Bad register");
+	  return NULL;
+	}
+    }
+
   {
     struct value *value;
     size_t data_size;
diff --git a/gdb/testsuite/gdb.python/py-unwind-user-regs.c b/gdb/testsuite/gdb.python/py-unwind-user-regs.c
new file mode 100644
index 00000000000..8d1efd1a85d
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-unwind-user-regs.c
@@ -0,0 +1,37 @@
+/* This test program is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+volatile int global_var;
+
+void __attribute__ ((noinline))
+foo (void)
+{
+  ++global_var;		/* Break here.  */
+}
+
+void __attribute__ ((noinline))
+bar (void)
+{
+  foo ();
+}
+
+int
+main (void)
+{
+  bar ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.python/py-unwind-user-regs.exp b/gdb/testsuite/gdb.python/py-unwind-user-regs.exp
new file mode 100644
index 00000000000..7ae3a5bb19f
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-unwind-user-regs.exp
@@ -0,0 +1,98 @@
+# Copyright (C) 2021 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/>.
+
+# Setup an unwinder that uses gdb.UnwindInfo.add_saved_register with
+# the register's 'pc' and 'sp'.  On some (all?) targets, these
+# registers are implemented as user-registers, and so can't normally
+# be written to directly.
+#
+# The Python unwinder now includes code similar to how the expression
+# evaluator would handle something like 'set $pc=0x1234', we fetch the
+# value of '$pc', and then use the value's location to tell us which
+# register to write to.
+#
+# The unwinder defined here deliberately breaks the unwind by setting
+# the unwound $pc and $sp to be equal to the current frame's $pc and
+# $sp.  GDB will spot this as a loop in the backtrace and terminate
+# the unwind.
+#
+# However, by the time the unwind terminates we have already shown
+# that it is possible to call add_saved_register with a user-register,
+# so the test is considered passed.
+#
+# For completeness this test checks two cases, calling
+# add_saved_register with a gdb.RegisterDescriptor and calling
+# add_saved_register with a string containing the register name.
+
+load_lib gdb-python.exp
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
+    return -1
+}
+
+# Skip all tests if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# Load the script containing the unwinders.  There are actually two
+# unwinders defined here that will catch the same function, so we
+# immediately disable one of the unwinders.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+gdb_test "disable unwinder global \"break unwinding using strings\"" \
+    "1 unwinder disabled" "disable the unwinder that uses strings"
+
+# At this point we are using the unwinder that passes a
+# gdb.RegisterDescriptor to add_saved_register.
+gdb_test_sequence "bt"  "Backtrace corrupted by descriptor based unwinder" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "Backtrace stopped: previous frame inner to this frame \\(corrupt stack\\?\\)"
+}
+
+# Disable the unwinder that calls add_saved_register with a
+# gdb.RegisterDescriptor, and enable the unwinder that calls
+# add_saved_register with a string (containing the register name).
+gdb_test "disable unwinder global \"break unwinding using descriptors\"" \
+    "1 unwinder disabled" "disable the unwinder that uses descriptors"
+gdb_test "enable unwinder global \"break unwinding using strings\"" \
+    "1 unwinder enabled" "enable the unwinder that uses strings"
+gdb_test_sequence "bt"  "Backtrace corrupted by string based unwinder" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "Backtrace stopped: previous frame inner to this frame \\(corrupt stack\\?\\)"
+}
+
+# Just for completeness, disable the string unwinder again (neither of
+# our special unwinders are now enabled), and check the backtrace.  We
+# now get the complete stack back to main.
+gdb_test "disable unwinder global \"break unwinding using strings\"" \
+    "1 unwinder disabled" "disable the unwinder that uses strings again"
+gdb_test_sequence "bt"  "Backtrace not corrupted when using no unwinder" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* main \\(\\) at "
+}
diff --git a/gdb/testsuite/gdb.python/py-unwind-user-regs.py b/gdb/testsuite/gdb.python/py-unwind-user-regs.py
new file mode 100644
index 00000000000..e5edd7cbd9c
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-unwind-user-regs.py
@@ -0,0 +1,72 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self, use_descriptors):
+        if use_descriptors:
+            tag = "using descriptors"
+        else:
+            tag = "using strings"
+
+        Unwinder.__init__(self, "break unwinding %s" % tag)
+        self._use_descriptors = use_descriptors
+
+    def __call__(self, pending_frame):
+        pc_desc = pending_frame.architecture().registers().find("pc")
+        pc = pending_frame.read_register(pc_desc)
+
+        sp_desc = pending_frame.architecture().registers().find("sp")
+        sp = pending_frame.read_register(sp_desc)
+
+        block = gdb.block_for_pc(int(pc))
+        if block == None:
+            return None
+        func = block.function
+        if func == None:
+            return None
+        if str(func) != "bar":
+            return None
+
+        fid = FrameId(pc, sp)
+        unwinder = pending_frame.create_unwind_info(fid)
+        if self._use_descriptors:
+            unwinder.add_saved_register(pc_desc, pc)
+            unwinder.add_saved_register(sp_desc, sp)
+        else:
+            unwinder.add_saved_register("pc", pc)
+            unwinder.add_saved_register("sp", sp)
+        return unwinder
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(True), True)
+gdb.unwinder.register_unwinder(None, TestUnwinder(False), True)
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCH 2/5] gdb/python: move PyLong_From* calls into py-utils.c
  2021-05-29 20:57 [PATCH 0/5] Fix for an assertion when unwinding with inline frames Andrew Burgess
  2021-05-29 20:57 ` [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder Andrew Burgess
@ 2021-05-29 20:57 ` Andrew Burgess
  2021-06-07 14:53   ` Tom Tromey
  2021-05-29 20:57 ` [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods Andrew Burgess
                   ` (3 subsequent siblings)
  5 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-05-29 20:57 UTC (permalink / raw)
  To: gdb-patches

We already have two helper functions in py-utils.c:

  gdb_py_object_from_longest (LONGEST l)
  gdb_py_object_from_ulongest (ULONGEST l)

these wrap around calls to either PyLong_FromLongLong,
PyLong_FromLong, or PyInt_From_Long (if Python 2 is being used).

There is one place in gdb/python/* where a call to PyLong_FromLong was
added outside of the above utility functions, this was done in the
recent commit:

  commit 55789354fcbaf879f3ca8475b647b2747dec486e
  Date:   Fri May 14 11:56:31 2021 +0200

      gdb/python: add a 'connection_num' attribute to Inferior objects

In this commit I propose that we move this new call to PyLong_FromLong
into a new utility function gdb_py_object_from_int.  The biggest win I
see here is that we have a consistent interface for converting number
like things into Python objects, however, I have made it such that on
Python 2 we will call PyInt_FromLong, so there is a slight change in
behaviour here, which seems to be inline with how we handle the
LONGEST types.

Beyond the PyLong/PyInt change described above there should be no user
visible changes from this commit.

gdb/ChangeLog:

	* python/py-inferior.c (infpy_get_connection_num): Call new
	function gdb_py_object_from_int.
	* python/py-utils.c (gdb_py_object_from_int): New function.
	* python/python-internal.h (gdb_py_object_from_int): Declare.
---
 gdb/ChangeLog                |  7 +++++++
 gdb/python/py-inferior.c     |  2 +-
 gdb/python/py-utils.c        | 12 ++++++++++++
 gdb/python/python-internal.h |  2 ++
 4 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c
index 336c6426b8c..bdbde0b3e61 100644
--- a/gdb/python/py-inferior.c
+++ b/gdb/python/py-inferior.c
@@ -441,7 +441,7 @@ infpy_get_connection_num (PyObject *self, void *closure)
   if (target == nullptr)
     Py_RETURN_NONE;
 
-  return PyLong_FromLong (target->connection_number);
+  return gdb_py_object_from_int (target->connection_number).release ();
 }
 
 static PyObject *
diff --git a/gdb/python/py-utils.c b/gdb/python/py-utils.c
index 10c4173efcd..c3f712debcb 100644
--- a/gdb/python/py-utils.c
+++ b/gdb/python/py-utils.c
@@ -335,6 +335,18 @@ gdb_py_object_from_ulongest (ULONGEST l)
 #endif
 }
 
+/* Convert an int I to the appropriate Python object.  */
+
+gdbpy_ref<>
+gdb_py_object_from_int (int i)
+{
+#ifdef IS_PY3K
+  return gdbpy_ref<> (PyLong_FromLong (i));
+#else
+  return gdbpy_ref<> (PyInt_FromLong (l));
+#endif
+}
+
 /* Like PyInt_AsLong, but returns 0 on failure, 1 on success, and puts
    the value into an out parameter.  */
 
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index 690d2fb43c0..91b5bea58eb 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -747,6 +747,8 @@ int get_addr_from_python (PyObject *obj, CORE_ADDR *addr)
 
 gdbpy_ref<> gdb_py_object_from_longest (LONGEST l);
 gdbpy_ref<> gdb_py_object_from_ulongest (ULONGEST l);
+gdbpy_ref<> gdb_py_object_from_int (int i);
+
 int gdb_py_int_as_long (PyObject *, long *);
 
 PyObject *gdb_py_generic_dict (PyObject *self, void *closure);
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods
  2021-05-29 20:57 [PATCH 0/5] Fix for an assertion when unwinding with inline frames Andrew Burgess
  2021-05-29 20:57 ` [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder Andrew Burgess
  2021-05-29 20:57 ` [PATCH 2/5] gdb/python: move PyLong_From* calls into py-utils.c Andrew Burgess
@ 2021-05-29 20:57 ` Andrew Burgess
  2021-05-30  5:55   ` Eli Zaretskii
                     ` (3 more replies)
  2021-05-29 20:57 ` [PATCH 4/5] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
                   ` (2 subsequent siblings)
  5 siblings, 4 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-05-29 20:57 UTC (permalink / raw)
  To: gdb-patches

Add new methods to the PendingFrame and Frame classes to obtain the
stack frame level for each object.

The use of 'level' as the method name is consistent with the existing
attribute RecordFunctionSegment.level (though this is an attribute
rather than a method).

For Frame/PendingFrame I went with methods as these classes currently
only use methods, including for simple data like architecture, so I
want to be consistent with this interface.

gdb/ChangeLog:

	* python/py-frame.c (frapy_level): New function.
	(frame_object_methods): Register 'level' method.
	* python/py-unwind.c (pending_framepy_level): New function.
	(pending_frame_object_methods): Register 'level' method.

gdb/doc/ChangeLog:

	* python.texi (Unwinding Frames in Python): Mention
	PendingFrame.level.
	(Frames In Python): Mention Frame.level.

gdb/testsuite/ChangeLog:

	* gdb.python/py-frame.exp: Add Frame.level tests.
	* gdb.python/py-pending-frame-level.c: New file.
	* gdb.python/py-pending-frame-level.exp: New file.
	* gdb.python/py-pending-frame-level.py: New file.
---
 gdb/ChangeLog                                 |  7 ++
 gdb/doc/ChangeLog                             |  6 ++
 gdb/doc/python.texi                           |  9 +++
 gdb/python/py-frame.c                         | 23 +++++++
 gdb/python/py-unwind.c                        | 19 ++++++
 gdb/testsuite/ChangeLog                       |  7 ++
 gdb/testsuite/gdb.python/py-frame.exp         | 11 ++++
 .../gdb.python/py-pending-frame-level.c       | 49 ++++++++++++++
 .../gdb.python/py-pending-frame-level.exp     | 65 +++++++++++++++++++
 .../gdb.python/py-pending-frame-level.py      | 55 ++++++++++++++++
 10 files changed, 251 insertions(+)
 create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.c
 create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.exp
 create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.py

diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 23e6ac666ff..b7e16351a5d 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -2605,6 +2605,11 @@
 the particular frame being unwound.
 @end defun
 
+@defun PendingFrame.level ()
+Return an integer, the stack frame level for this frame.
+@xref{Frames, ,Stack Frames}.
+@end defun
+
 @subheading Unwinder Output: UnwindInfo
 
 Use @code{PendingFrame.create_unwind_info} method described above to
@@ -4813,6 +4818,10 @@
 Stack}.
 @end defun
 
+@defun Frame.level ()
+Return an integer, the stack frame level for this frame.  @xref{Frames, ,Stack Frames}.
+@end defun
+
 @node Blocks In Python
 @subsubsection Accessing blocks from Python
 
diff --git a/gdb/python/py-frame.c b/gdb/python/py-frame.c
index c8eab5291ea..4f218c40367 100644
--- a/gdb/python/py-frame.c
+++ b/gdb/python/py-frame.c
@@ -577,6 +577,27 @@ frapy_select (PyObject *self, PyObject *args)
   Py_RETURN_NONE;
 }
 
+/* The stack frame level for this frame.  */
+
+static PyObject *
+frapy_level (PyObject *self, PyObject *args)
+{
+  struct frame_info *fi;
+
+  try
+    {
+      FRAPY_REQUIRE_VALID (self, fi);
+
+      return gdb_py_object_from_int (frame_relative_level (fi)).release ();
+    }
+  catch (const gdb_exception &except)
+    {
+      GDB_PY_HANDLE_EXCEPTION (except);
+    }
+
+  Py_RETURN_NONE;
+}
+
 /* Implementation of gdb.newest_frame () -> gdb.Frame.
    Returns the newest frame object.  */
 
@@ -748,6 +769,8 @@ Return the frame's symtab and line." },
 Return the value of the variable in this frame." },
   { "select", frapy_select, METH_NOARGS,
     "Select this frame as the user's current frame." },
+  { "level", frapy_level, METH_NOARGS,
+    "The stack level of this frame." },
   {NULL}  /* Sentinel */
 };
 
diff --git a/gdb/python/py-unwind.c b/gdb/python/py-unwind.c
index d6e2f85dbc1..ff1a7e922a7 100644
--- a/gdb/python/py-unwind.c
+++ b/gdb/python/py-unwind.c
@@ -463,6 +463,23 @@ pending_framepy_architecture (PyObject *self, PyObject *args)
   return gdbarch_to_arch_object (pending_frame->gdbarch);
 }
 
+/* Implementation of PendingFrame.level (self) -> Integer.  */
+
+static PyObject *
+pending_framepy_level (PyObject *self, PyObject *args)
+{
+  pending_frame_object *pending_frame = (pending_frame_object *) self;
+
+  if (pending_frame->frame_info == NULL)
+    {
+      PyErr_SetString (PyExc_ValueError,
+		       "Attempting to read stack level from stale PendingFrame");
+      return NULL;
+    }
+  int level = frame_relative_level (pending_frame->frame_info);
+  return gdb_py_object_from_int (level).release ();
+}
+
 /* frame_unwind.this_id method.  */
 
 static void
@@ -704,6 +721,8 @@ static PyMethodDef pending_frame_object_methods[] =
     pending_framepy_architecture, METH_NOARGS,
     "architecture () -> gdb.Architecture\n"
     "The architecture for this PendingFrame." },
+  { "level", pending_framepy_level, METH_NOARGS,
+    "The stack level of this frame." },
   {NULL}  /* Sentinel */
 };
 
diff --git a/gdb/testsuite/gdb.python/py-frame.exp b/gdb/testsuite/gdb.python/py-frame.exp
index a6a5c0de726..05c7fb00dfd 100644
--- a/gdb/testsuite/gdb.python/py-frame.exp
+++ b/gdb/testsuite/gdb.python/py-frame.exp
@@ -70,6 +70,17 @@ gdb_test "up" ".*" ""
 
 gdb_py_test_silent_cmd "python f1 = gdb.selected_frame ()" "get second frame" 0
 gdb_py_test_silent_cmd "python f0 = f1.newer ()" "get first frame" 0
+gdb_py_test_silent_cmd "python f2 = f1.older ()" "get last frame" 0
+
+# Check the Frame.level method.
+gdb_test "python print ('bframe.level = %d' % bframe.level ())" \
+    "bframe\\.level = 0"
+gdb_test "python print ('f0.level = %d' % f0.level ())" \
+    "f0\\.level = 0"
+gdb_test "python print ('f1.level = %d' % f1.level ())" \
+    "f1\\.level = 1"
+gdb_test "python print ('f2.level = %d' % f2.level ())" \
+    "f2\\.level = 2"
 
 gdb_test "python print (f1 == gdb.newest_frame())" False \
     "selected frame -vs- newest frame"
diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.c b/gdb/testsuite/gdb.python/py-pending-frame-level.c
new file mode 100644
index 00000000000..5e5495c1d71
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-pending-frame-level.c
@@ -0,0 +1,49 @@
+/* This test program is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+volatile int global_var;
+
+void __attribute__ ((noinline))
+f0 (void)
+{
+  ++global_var;		/* Break here.  */
+}
+
+void __attribute__ ((noinline))
+f1 (void)
+{
+  f0 ();
+}
+
+void __attribute__ ((noinline))
+f2 (void)
+{
+  f1 ();
+}
+
+void __attribute__ ((noinline))
+f3 (void)
+{
+  f2 ();
+}
+
+int
+main (void)
+{
+  f3 ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.exp b/gdb/testsuite/gdb.python/py-pending-frame-level.exp
new file mode 100644
index 00000000000..1aadcaeacae
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-pending-frame-level.exp
@@ -0,0 +1,65 @@
+# Copyright (C) 2021 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/>.
+
+# Test gdb.PendingFrame.level method.
+
+load_lib gdb-python.exp
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
+    return -1
+}
+
+# Skip all tests if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# An initial look at the stack to ensure it is correct.
+gdb_test_sequence "bt"  "Initial backtrace" {
+    "\\r\\n#0 \[^\r\n\]* f0 \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* f1 \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* f2 \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* f3 \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* main \\(\\) at "
+}
+
+# Load the script containing the unwinder.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Now look at the stack again, we should see output from the Python
+# unwinder mixed in.
+gdb_test_sequence "bt"  "Backtrace with extra Python output" {
+    "Func f0, Level 0"
+    "Func f1, Level 1"
+    "\\r\\n#0 \[^\r\n\]* f0 \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* f1 \\(\\) at "
+    "Func f2, Level 2"
+    "\\r\\n#2 \[^\r\n\]* f2 \\(\\) at "
+    "Func f3, Level 3"
+    "\\r\\n#3 \[^\r\n\]* f3 \\(\\) at "
+    "Func main, Level 4"
+    "\\r\\n#4 \[^\r\n\]* main \\(\\) at "
+}
diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.py b/gdb/testsuite/gdb.python/py-pending-frame-level.py
new file mode 100644
index 00000000000..182edcdc0df
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-pending-frame-level.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "show level")
+
+    def __call__(self, pending_frame):
+        pc_desc = pending_frame.architecture().registers().find("pc")
+        pc = pending_frame.read_register(pc_desc)
+
+        block = gdb.block_for_pc(int(pc))
+        if block == None:
+            return None
+        func = block.function
+        if func == None:
+            return None
+
+        print("Func %s, Level %d" % (str(func), pending_frame.level()))
+
+        # This unwinder never claims any frames.
+        return None
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCH 4/5] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-05-29 20:57 [PATCH 0/5] Fix for an assertion when unwinding with inline frames Andrew Burgess
                   ` (2 preceding siblings ...)
  2021-05-29 20:57 ` [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods Andrew Burgess
@ 2021-05-29 20:57 ` Andrew Burgess
  2021-05-29 20:57 ` [PATCH 5/5] gdb: remove VALUE_FRAME_ID Andrew Burgess
  2021-06-21 19:46 ` [PATCHv2 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
  5 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-05-29 20:57 UTC (permalink / raw)
  To: gdb-patches

I ran into this assertion while GDB was trying to unwind the stack:

  gdb/inline-frame.c:173: internal-error: void inline_frame_this_id(frame_info*, void**, frame_id*): Assertion `frame_id_p (*this_id)' failed.

That is, when building the frame_id for an inline frame, GDB asks for
the frame_id of the previous frame.  Unfortunately, no valid frame_id
was returned for the previous frame, and so the assertion triggers.

What is happening is this, I had a stack that looked something like
this (the arrows '->' point from caller to callee):

  normal_frame -> inline_frame

However, for whatever reason (e.g. broken debug information, or
corrupted stack contents in the inferior), when GDB tries to unwind
"normal_frame", it ends up getting back effectively the same frame,
thus the call stack looks like this to GDB:

  .-> normal_frame -> inline_frame
  |     |
  '-----'

Given such a situation we would expect GDB to terminate the stack with
an error like this:

  Backtrace stopped: previous frame identical to this frame (corrupt stack?)

However, the inline_frame causes a problem, and here's why:

When unwinding we start from the sentinel frame and call
get_prev_frame.  We eventually end up in get_prev_frame_if_no_cycle,
in here we create a raw frame, and as this is frame #0 we immediately
return.

However, eventually we will try to unwind the stack further.  When we
do this we inevitably needing to know the frame_id for frame #0, and
so, eventually, we end up in compute_frame_id.

In compute_frame_id we first find the right unwinder for this frame,
in our case (i.e. for inline_frame) the $pc is within the function
normal_frame, but also within a block associated with the inlined
function inline_frame, as such the inline frame unwinder claims this
frame.

Back in compute_frame_id we next compute the frame_id, for our
inline_frame this means a call to inline_frame_this_id.

The ID of an inline frame is based on the id of the previous frame, so
from inline_frame_this_id we call get_prev_frame_always, this
eventually calls get_prev_frame_if_no_cycle again, which creates
another raw frame and calls compute_frame_id (for frames other than
frame 0 we immediately compute the frame_id).

In compute_frame_id we again identify the correct unwinder for this
frame.  Our $pc is unchanged, however, the fact that the next frame is
of type INLINE_FRAME prevents the inline frame unwinder from claiming
this frame again, and so, the standard DWARF frame unwinder claims
normal_frame.

We return to compute_frame_id and call the standard DWARF function to
build the frame_id for normal_frame.

With the frame_id of normal_frame figured out we return to
compute_frame_id, and then to get_prev_frame_if_no_cycle, where we add
the ID for normal_frame into the frame_id cache, and return the frame
back to inline_frame_this_id.

From inline_frame_this_id we build a frame_id for inline_frame and
return to compute_frame_id, and then to get_prev_frame_if_no_cycle,
which adds the frame_id for inline_frame into the frame_id cache.

So far, so good.

However, as we are trying to unwind the compute stack, we eventually
ask for the previous frame of normal_frame, remember, that at this
point GDB doesn't know the stack is corrupted (with a cycle), GDB
still needs to figure that out.

So, we eventually end up in get_prev_frame_if_no_cycle where we create
a raw frame and call compute_frame_id, remember, this is for the frame
before normal_frame.

The first task for compute_frame_id is to find the unwinder for this
frame, so all of the frame sniffers are tried in order, this includes
the inline frame sniffer.

The inline frame sniffer asks for the $pc, this request is sent up the
stack to normal_frame, which, due to its cyclic behaviour, tells GDB
that the $pc in the previous frame was the same as the $pc in
normal_frame.

GDB spots that this $pc corresponds to both the function normal_frame
and also the inline function inline_frame.  As the next frame is not
an INLINE_FRAME then GDB figures that we have not yet built a frame to
cover inline_frame, and so the inline sniffer claims this new frame.
Our stack is now looking like this:

  inline_frame -> normal_frame -> inline_frame

But, we have not yet computed the frame id for the outer most (on the
left) inline_frame.  After the frame sniffer has claimed the inline
frame GDB returns to compute_frame_id and calls inline_frame_this_id.

In here GDB calls get_prev_frame_always, which eventually ends up
in get_prev_frame_if_no_cycle again, where we create a raw frame and
call compute_frame_id.

Just like before compute_frame_id tries to find an unwinder for this
new frame, it sees that the $pc is within both normal_frame and
inline_frame, but the next frame is, again, an INLINE_FRAME, so, just
like before the standard DWARF unwinder claims this frame.  Back in
compute_frame_id we again call the standard DWARF function to build
the frame_id for this new copy of normal_frame.

At this point the stack looks like this:

  normal_frame -> inline_frame -> normal_frame -> inline_frame

After compute_frame_id we return to get_prev_frame_if_no_cycle, where
we try to add the frame_id for the new normal_frame into the frame_id
cache, however, unlike before, we fail to add this frame_id as this is
a duplicate of the previous normal_frame frame_id.  Having found a
duplicate get_prev_frame_if_no_cycle unlinks the new frame from the
stack, and returns nullptr, the stack now looks like this:

  inline_frame -> normal_frame -> inline_frame

The nullptr result from get_prev_frame_if_no_cycle is fed back to
inline_frame_this_id, which forwards this to get_frame_id, which
immediately returns null_frame_id.  As null_frame_id is not considered
a valid frame_id, this is what triggers the assertion.

In summary then:

 - inline_frame_this_id currently assumes that as the inline frame
   exists, we will always get a valid frame back from
   get_prev_frame_always,

 - get_prev_frame_if_no_cycle currently assumes that it is safe to
   return nullptr when it sees a cycle.

Notice that in frame.c:compute_frame_id, this code:

  fi->this_id.value = outer_frame_id;
  fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
  gdb_assert (frame_id_p (fi->this_id.value));

The assertion makes it clear that the this_id function must always
return a valid frame_id (e.g. null_frame_id is not a valid return
value), and similarly in inline_frame.c:inline_frame_this_id this
code:

  *this_id = get_frame_id (get_prev_frame_always (this_frame));
  /* snip comment */
  gdb_assert (frame_id_p (*this_id));

Makes it clear that every inline frame expects to be able to get a
previous frame, which will have a valid frame_id.

As I have discussed above, these assumptions don't currently hold in
all cases.

One possibility would be to move the call to get_prev_frame_always
forward from inline_frame_this_id to inline_frame_sniffer, however,
this falls foul of (in frame.c:frame_cleanup_after_sniffer) this
assertion:

  /* No sniffer should extend the frame chain; sniff based on what is
     already certain.  */
  gdb_assert (!frame->prev_p);

This assert prohibits any sniffer from trying to get the previous
frame, as getting the previous frame is likely to depend on the next
frame, I can understand why this assertion is a good thing, and I'm in
no rush to alter this rule.

The solution I am proposing here is to add a special case to
get_prev_frame_if_no_cycle, such that, if we find a cycle, and we know
we are fetching the previous frame as a result of computing the
frame_id for the next frame, which is an INLINE_FRAME, then, instead
of returning nullptr, do still return the frame.

This is safe (I claim) because, if the frame_id of the NORMAL_FRAME
was a duplicate then the INLINE_FRAME should also be a duplicate, and
so, the INLINE_FRAME will be rejected as a duplicate just as the
NORMAL_FRAME was.

To catch cases where this special case might go wrong I do two things,
first, even though I do now return the previous frame, I still
disconnect the previous frame from the next/prev links, this allows me
to do the second thing, which is to add an assert, if a frame is added
to the frame_id cache, and it is an INLINE_FRAME, then its prev link
must not be nullptr.

This logic should be sound as, computing the frame_id for an inline
frame requires GDB to fetch the previous frame.  For most (all?) other
frame types this is not the case, and so, it is only inline frames for
which you are guaranteed that, after computing the frame_id, the
previous frame is known.

So, if my new special case triggers, and we return a previous frame
even when that previous frame is a duplicate, and _somehow_ the inline
frame that we return this special case frame too is not then rejected
from the frame_id cache, the inline frame's prev link will be nullptr,
and the new assertion will trigger.

gdb/ChangeLog:

	* frame.c (get_prev_frame_if_no_cycle): Always return prev_frame
	when computing the frame_id for an INLINE_FRAME.  Add an extra
	assertion.

gdb/testsuite/ChangeLog:

	* gdb.base/inline-frame-bad-unwind.c: New file.
	* gdb.base/inline-frame-bad-unwind.exp: New file.
	* gdb.base/inline-frame-bad-unwind.py: New file.
---
 gdb/ChangeLog                                 |   6 +
 gdb/frame.c                                   |  45 ++++++-
 gdb/testsuite/ChangeLog                       |   6 +
 .../gdb.base/inline-frame-bad-unwind.c        |  58 +++++++++
 .../gdb.base/inline-frame-bad-unwind.exp      | 122 ++++++++++++++++++
 .../gdb.base/inline-frame-bad-unwind.py       |  85 ++++++++++++
 6 files changed, 321 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.py

diff --git a/gdb/frame.c b/gdb/frame.c
index d2e14c831a0..b0943c02115 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2125,7 +2125,50 @@ get_prev_frame_if_no_cycle (struct frame_info *this_frame)
 	  /* Unlink.  */
 	  prev_frame->next = NULL;
 	  this_frame->prev = NULL;
-	  prev_frame = NULL;
+
+	  /* Consider the call stack A->B, where A is a normal frame and B
+	     is an inline frame.  When computing the frame-id for B we need
+	     to compute the frame-id for A.
+
+	     If the frame-id for A is a duplicate then it must be the case
+	     that B will also be a duplicate.
+
+	     If we spot A as being a duplicate here and so return NULL then
+	     B will fail to obtain a valid frame-id for A, and thus B will
+	     be unable to return a valid frame-id (in fact an assertion
+	     will trigger).
+
+	     What this means is that, if we are being asked to get the
+	     previous frame for an inline frame and we want to reject the
+	     new (previous) frame then we should really return the frame so
+	     that the inline frame can still compute its frame-id.  This is
+	     safe as we can be confident that the inline frame-id will also
+	     be a duplicate, and so the inline frame (and therefore all
+	     frames previous to it) will then be rejected.  */
+	  if (this_frame->unwind->type != INLINE_FRAME
+	      || this_frame->this_id.p != frame_id_status::COMPUTING)
+	    prev_frame = NULL;
+	}
+      else
+	{
+	  /* This assertion ties into the special handling of inline frames
+	     above.
+
+	     We know that to compute the frame-id of an inline frame we
+	     must first compute the frame-id of the inline frame's previous
+	     frame.
+
+	     If the previous frame is rejected as a duplicate then it
+	     should be the case that the inline frame is also rejected as a
+	     duplicate, and we should not reach this assertion.
+
+	     However, if we do reach this assertion then the inline frame
+	     has not been rejected, thus, it should be the case that the
+	     frame previous to the inline frame has also not be rejected,
+	     this is reflected by the requirement that the inline frame's
+	     previous pointer not be nullptr at this point.  */
+	  gdb_assert (this_frame->unwind->type != INLINE_FRAME
+		      || this_frame->prev != nullptr);
 	}
     }
   catch (const gdb_exception &ex)
diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.c b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.c
new file mode 100644
index 00000000000..704a994c4e6
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+static void foo (void);
+static void bar (void);
+
+volatile int global_var;
+volatile int level_counter;
+
+static void __attribute__((noinline))
+bar (void)
+{
+  /* Do some work.  */
+  ++global_var;
+
+  /* Now the inline function.  */
+  --level_counter;
+  foo ();
+  ++level_counter;
+
+  /* Do some work.  */
+  ++global_var;
+}
+
+static inline void __attribute__((__always_inline__))
+foo (void)
+{
+  if (level_counter > 1)
+    {
+      --level_counter;
+      bar ();
+      ++level_counter;
+    }
+  else
+    ++global_var;	/* Break here.  */
+}
+
+int
+main ()
+{
+  level_counter = 6;
+  bar ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
new file mode 100644
index 00000000000..49c35517801
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
@@ -0,0 +1,122 @@
+# Copyright (C) 2021 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 checks for an edge case when unwinding inline frames which
+# occur towards the older end of the stack when the stack ends with a
+# cycle.  Consider this well formed stack:
+#
+#   main -> normal_frame -> inline_frame
+#
+# Now consider that, for whatever reason, the stack unwinding of
+# "normal_frame" becomes corrupted, such that the stack appears to be
+# this:
+#
+#   .-> normal_frame -> inline_frame
+#   |      |
+#   '------'
+#
+# When confrontend with such a situation we would expect GDB to detect
+# the stack frame cycle and terminate the backtrace at the first
+# instance of "normal_frame" with a message:
+#
+#   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
+#
+# However, at one point there was a bug in GDB's inline frame
+# mechanism such that the fact that "inline_frame" was inlined into
+# "normal_frame" would cause GDB to trigger an assertion.
+#
+# This text makes use of a Python unwinder which can fake the cyclic
+# stack cycle, further the test sets up multiple levels of normal and
+# inline frames.  At the point of testing the stack looks like this:
+#
+#   main -> bar -> foo -> bar -> foo -> bar -> foo
+#
+# Where "bar" is a normal frame, and "foo" is an inline frame.
+#
+# The python unwinder is then used to force a stack cycle at each
+# "bar" frame in turn, we then check that GDB can successfully unwind
+# the stack.
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}]} {
+    return -1
+}
+
+# Skip this test if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to the breakpoint where we will carry out the test.
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# Load the script containing the unwinder, this must be done at the
+# testing point as the script will examine the stack as it is loaded.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Check the unbroken stack.
+gdb_test_sequence "bt"  "Backtrace when the unwind is left unbroken" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
+}
+
+# Arrange to introduce a stack cycle at frame 5.
+gdb_test_no_output "python stop_at_level=5"
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 5" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
+}
+
+# Arrange to introduce a stack cycle at frame 3.
+gdb_test_no_output "python stop_at_level=3"
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 3" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
+}
+
+# Arrange to introduce a stack cycle at frame 1.
+gdb_test_no_output "python stop_at_level=1"
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 1" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py
new file mode 100644
index 00000000000..21743f7864a
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+# Set this to the stack level the backtrace should be corrupted at.
+# This will only work for frame 1, 3, or 5 in the test this unwinder
+# was written for.
+stop_at_level = None
+
+# Set this to the stack frame size of frames 1, 3, and 5.  These
+# frames wil all have the same stack frame size as they are the same
+# function called recursively.
+stack_adjust = None
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "stop at level")
+
+    def __call__(self, pending_frame):
+        global stop_at_level
+        global stack_adjust
+
+        if stop_at_level == None or pending_frame.level() != stop_at_level:
+            return None
+
+        if stack_adjust is None:
+            raise gdb.GdbError("invalid stack_adjust")
+
+        if not stop_at_level in [1, 3, 5]:
+            raise gdb.GdbError("invalid stop_at_level")
+
+        sp_desc = pending_frame.architecture().registers().find("sp")
+        sp = pending_frame.read_register(sp_desc) + stack_adjust
+        pc = (gdb.lookup_symbol("bar"))[0].value().address
+        unwinder = pending_frame.create_unwind_info(FrameId(sp, pc))
+
+        for reg in pending_frame.architecture().registers("general"):
+            val = pending_frame.read_register(reg)
+            unwinder.add_saved_register(reg, val)
+        return unwinder
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
+
+# When loaded, it is expected that the stack looks like:
+#
+#   main -> bar -> foo -> bar -> foo -> bar -> foo
+#
+# Compute the stack frame size of bar, which has foo inlined within
+# it.
+f0 = gdb.newest_frame()
+f1 = f0.older()
+f2 = f1.older()
+f0_sp = f0.read_register("sp")
+f2_sp = f2.read_register("sp")
+stack_adjust = f2_sp - f0_sp
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCH 5/5] gdb: remove VALUE_FRAME_ID
  2021-05-29 20:57 [PATCH 0/5] Fix for an assertion when unwinding with inline frames Andrew Burgess
                   ` (3 preceding siblings ...)
  2021-05-29 20:57 ` [PATCH 4/5] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
@ 2021-05-29 20:57 ` Andrew Burgess
  2021-06-21 19:46 ` [PATCHv2 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
  5 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-05-29 20:57 UTC (permalink / raw)
  To: gdb-patches

While working on the previous commit I happened to switch on 'set
debug frame 1', and ran into a different assertion than the one I was
trying to fix.

The new problem I see is this assertion triggering:

  gdb/frame.c:622: internal-error: frame_id get_frame_id(frame_info*): Assertion `fi->this_id.p != frame_id_status::COMPUTING' failed.

We attempt to get the frame_id for a frame while we are computing the
frame_id for that same frame.

What happens is we have a stack like this:

  normal_frame -> inline_frame -> sentinel_frame

When we initially stop, GDB creates a frame for inline_frame but
doesn't sniff its type, or try to fill in its frame_id (see code near
the top of get_prev_frame_if_no_cycle).

Later on during the stop, this happens:

  process_event_stop_test
    get_stack_frame_id
      skip_artificial_frames
        get_frame_type

The call to get_frame_type causes inline_frame to sniff its type,
establishing that it is an INLINE_FRAME, but does not cause the frame
to build its frame_id.

The same skip_artificial_frames call then calls get_prev_frame_always
to unwind the stack, this will then try to get the frame previous to
inline_frame, i.e. normal_frame.

So, we create a new frame, but unlike frame #0, in
get_prev_frame_if_no_cycle, we immediately try to compute the frame_id
for this new frame #1.

Computing the frame_id for frame #1 invokes the sniffer.  If this
sniffer tries to read a register then we will create a lazy register
value by calling value_of_register_lazy.  As the next frame (frame #0)
is an INLINE_FRAME, we will skip this and instead create the lazy
register value using the frame_id for frame #-1 (the sentinel frame).

Now, when we try to resolve the lazy register value, in the value.c
function value_fetch_lazy_register, we want to print which frame the
lazy register value was for (in debug mode), so we call:

  frame = frame_find_by_id (VALUE_FRAME_ID (val));

where:

  #define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))

In our case we call get_prev_frame_id_by_id with the frame_id of the
sentinel_frame frame, we then call frame_find_by_id, followed by
get_prev_frame (which gives us frame #0), followed by get_frame_id.

Frame #0 has not yet computed its frame_id, so we start that process.
The first thing we need when computing the frame_id of an inline frame
is the frame_id of the previous frame, so that's what we ask for.
Unfortunately, that's where we started this journey, we are already
computing the frame_id for frame #1, and thus the assert fires.

Solving the assertion failure is pretty easy, if we consider the code
in value_fetch_lazy_register and get_prev_frame_id_by_id then what we
do is:

  1. Start with a frame_id taken from a value,
  2. Lookup the corresponding frame,
  3. Find the previous frame,
  4. Get the frame_id for that frame, and
  5. Lookup the corresponding frame
  6. Print the frame's level

Notice that steps 3 and 5 give us the exact same result, step 4 is
just wasted effort.  We could shorten this process such that we drop
steps 4 and 5, thus:

  1. Start with a frame_id taken from a value,
  2. Lookup the corresponding frame,
  3. Find the previous frame,
  6. Print the frame's level

This will give the exact same frame as a result, and this is what I
have done in this patch by removing the use of VALUE_FRAME_ID from
value_fetch_lazy_register.

Out of curiosity I looked to see how widely VALUE_FRAME_ID was used,
and saw it was only used in one other place in valops.c:value_assign,
where, once again, we take the result of VALUE_FRAME_ID and pass it to
frame_find_by_id, thus introducing a redundant frame_id lookup.

I don't think the value_assign case risks triggering the assertion
though, as we are unlikely to call value_assign while computing the
frame_id for a frame, however, we could make value_assign slightly
more efficient, with no real additional complexity, by removing the
use of VALUE_FRAME_ID.

So, in this commit, I completely remove VALUE_FRAME_ID, and replace it
with a use of VALUE_NEXT_FRAME_ID, followed by a direct call to
get_prev_frame_always, this should make no difference in either case,
and resolves the assertion issue from value.c.

One thing that is worth noticing here is that in these two situations
we don't end up getting the frame we expected to, though this is not a
result of my change, we were not getting the expected result before
either.

Consider the debug printing case, the stack is:

  normal_frame -> inline_frame -> sentinel_frame

We read a register from normal_frame (frame #1), the value of which is
fetched from sentinel_frame (frame #-1).  The debug print is trying to
say:

  frame=1,regnum=....

However, as the lazy register value points at frame #-1, we will
actually (incorrectly) print:

  frame=0,regnum=....

Like I said, this bug existed before this commit, and so, if we didn't
assert (i.e. the lazy register read occurred in some context other
than during the frame sniffer), then the debug print would previous
have, and will continue to, print the wrong frame level.  Thankfully,
this is only in debug output, so not something a normal user should
see.

In theory, the same bug exists in the value_assign code, if we are in
normal_frame and try to perform a register assignment, then GDB will
get confused and think we are assigning in the context of
inline_frame.  However, having looked at the code I think we get away
with this as the frame is used for two things that I can see:

  1. Getting the gdbarch for the frame, I can't imagine a situation
  where inline_frame has a different gdbarch to normal_frame, and

  2. Unwinding register values from frame->next.  We should be asking
  to unwind the register values from inline_frame, but really we end
  up asking to unwind from sentinel_frame.  However, if we did ask to
  unwind the values from inline_frame this would just forward the
  request on to the next frame, i.e. sentinel_frame, so we would get
  the exact same result.

In short, though we do use the wrong frame in value_assign, I think
this is harmless.

Fixing this debug printing would require GDB to require extra
information in its value location to indicate how many frames had been
skipped.  For example, with the stack:

  normal_frame -> inline_frame -> sentinel_frame

A lazy register value read in frame normal_frame would have a location
frame_id for sentinel_frame, and a skip value of 2 indicating the
value was read for a frame 2 previous.  In contrast, for a more
standard case, with a stack like this:

  normal_frame -> sentinel_frame

A lazy register value read in frame normal_frame would have a location
frame_id for sentinel_frame and a skip value of 1 indicating the value
was read for a frame 1 previous.

However, adding this seems like a lot of work to fix a single like of
debug print, but might be something we want to consider in the future.

gdb/ChangeLog:

	* frame.c (get_prev_frame_id_by_id): Delete.
	* frame.h (get_prev_frame_id_by_id): Delete declaration.
	* valops.c (value_assign): Use VALUE_NEXT_FRAME_ID and
	get_prev_frame_always, not VALUE_FRAME_ID.
	* value.c (value_fetch_lazy_register): Likewise.
	* value.h (VALUE_FRAME_ID): Delete.

gdb/testsuite/ChangeLog:

	* gdb.base/inline-frame-bad-unwind.exp: Add an extra test.
---
 gdb/ChangeLog                                   |  9 +++++++++
 gdb/frame.c                                     | 16 ----------------
 gdb/frame.h                                     |  4 ----
 gdb/testsuite/ChangeLog                         |  4 ++++
 .../gdb.base/inline-frame-bad-unwind.exp        | 17 +++++++++++++++++
 gdb/valops.c                                    | 17 +++++++++--------
 gdb/value.c                                     |  5 ++---
 gdb/value.h                                     |  6 ------
 8 files changed, 41 insertions(+), 37 deletions(-)

diff --git a/gdb/frame.c b/gdb/frame.c
index b0943c02115..e29a132dc3e 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2631,22 +2631,6 @@ get_prev_frame (struct frame_info *this_frame)
   return get_prev_frame_always (this_frame);
 }
 
-struct frame_id
-get_prev_frame_id_by_id (struct frame_id id)
-{
-  struct frame_id prev_id;
-  struct frame_info *frame;
-
-  frame = frame_find_by_id (id);
-
-  if (frame != NULL)
-    prev_id = get_frame_id (get_prev_frame (frame));
-  else
-    prev_id = null_frame_id;
-
-  return prev_id;
-}
-
 CORE_ADDR
 get_frame_pc (struct frame_info *frame)
 {
diff --git a/gdb/frame.h b/gdb/frame.h
index da52522ad2a..bc46149697e 100644
--- a/gdb/frame.h
+++ b/gdb/frame.h
@@ -383,10 +383,6 @@ extern struct frame_info *get_prev_frame_always (struct frame_info *);
    is not found.  */
 extern struct frame_info *frame_find_by_id (struct frame_id id);
 
-/* Given a frame's ID, find the previous frame's ID.  Returns null_frame_id
-   if the frame is not found.  */
-extern struct frame_id get_prev_frame_id_by_id (struct frame_id id);
-
 /* Base attributes of a frame: */
 
 /* The frame's `resume' address.  Where the program will resume in
diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
index 49c35517801..a0aebf94e41 100644
--- a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
+++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
@@ -120,3 +120,20 @@ gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 1" {
     "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
     "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
 }
+
+# Flush the register cache (which also flushes the frame cache) so we
+# get a full backtrace again, then switch on frame debugging and try
+# to back trace.  At one point this triggered an assertion.
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_no_output "set debug frame 1"
+gdb_test_multiple "bt" "backtrace with debugging on" {
+    -re "^$gdb_prompt $" {
+	pass $gdb_test_name
+    }
+    -re "\[^\r\n\]+\r\n" {
+	exp_continue
+    }
+}
+gdb_test "p 1 + 2 + 3" " = 6" \
+    "ensure GDB is still alive"
diff --git a/gdb/valops.c b/gdb/valops.c
index 8694c124b52..91832cc6b04 100644
--- a/gdb/valops.c
+++ b/gdb/valops.c
@@ -1197,14 +1197,15 @@ value_assign (struct value *toval, struct value *fromval)
 	struct gdbarch *gdbarch;
 	int value_reg;
 
-	/* Figure out which frame this is in currently.
-	
-	   We use VALUE_FRAME_ID for obtaining the value's frame id instead of
-	   VALUE_NEXT_FRAME_ID due to requiring a frame which may be passed to
-	   put_frame_register_bytes() below.  That function will (eventually)
-	   perform the necessary unwind operation by first obtaining the next
-	   frame.  */
-	frame = frame_find_by_id (VALUE_FRAME_ID (toval));
+	/* Figure out which frame this register value is in.  The value
+	   holds the frame_id for the next frame, that is the frame this
+	   register value was unwound from.
+
+	   Below we will call put_frame_register_bytes which requires that
+	   we pass it the actual frame in which the register value is
+	   valid, i.e. not the next frame.  */
+	frame = frame_find_by_id (VALUE_NEXT_FRAME_ID (toval));
+	frame = get_prev_frame_always (frame);
 
 	value_reg = VALUE_REGNUM (toval);
 
diff --git a/gdb/value.c b/gdb/value.c
index 9df035a50b3..034cfa9f11c 100644
--- a/gdb/value.c
+++ b/gdb/value.c
@@ -3950,9 +3950,8 @@ value_fetch_lazy_register (struct value *val)
     {
       struct gdbarch *gdbarch;
       struct frame_info *frame;
-      /* VALUE_FRAME_ID is used here, instead of VALUE_NEXT_FRAME_ID,
-	 so that the frame level will be shown correctly.  */
-      frame = frame_find_by_id (VALUE_FRAME_ID (val));
+      frame = frame_find_by_id (VALUE_NEXT_FRAME_ID (val));
+      frame = get_prev_frame_always (frame);
       regnum = VALUE_REGNUM (val);
       gdbarch = get_frame_arch (frame);
 
diff --git a/gdb/value.h b/gdb/value.h
index a691f3cf3ff..231387784a8 100644
--- a/gdb/value.h
+++ b/gdb/value.h
@@ -458,12 +458,6 @@ extern struct internalvar **deprecated_value_internalvar_hack (struct value *);
 extern struct frame_id *deprecated_value_next_frame_id_hack (struct value *);
 #define VALUE_NEXT_FRAME_ID(val) (*deprecated_value_next_frame_id_hack (val))
 
-/* Frame ID of frame to which a register value is relative.  This is
-   similar to VALUE_NEXT_FRAME_ID, above, but may not be assigned to. 
-   Note that VALUE_FRAME_ID effectively undoes the "next" operation
-   that was performed during the assignment to VALUE_NEXT_FRAME_ID.  */
-#define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))
-
 /* Register number if the value is from a register.  */
 extern int *deprecated_value_regnum_hack (struct value *);
 #define VALUE_REGNUM(val) (*deprecated_value_regnum_hack (val))
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods
  2021-05-29 20:57 ` [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods Andrew Burgess
@ 2021-05-30  5:55   ` Eli Zaretskii
  2021-05-30 18:34   ` Andrew Burgess
                     ` (2 subsequent siblings)
  3 siblings, 0 replies; 48+ messages in thread
From: Eli Zaretskii @ 2021-05-30  5:55 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

> From: Andrew Burgess <andrew.burgess@embecosm.com>
> Date: Sat, 29 May 2021 21:57:12 +0100
> 
> Add new methods to the PendingFrame and Frame classes to obtain the
> stack frame level for each object.
> 
> The use of 'level' as the method name is consistent with the existing
> attribute RecordFunctionSegment.level (though this is an attribute
> rather than a method).
> 
> For Frame/PendingFrame I went with methods as these classes currently
> only use methods, including for simple data like architecture, so I
> want to be consistent with this interface.
> 
> gdb/ChangeLog:
> 
> 	* python/py-frame.c (frapy_level): New function.
> 	(frame_object_methods): Register 'level' method.
> 	* python/py-unwind.c (pending_framepy_level): New function.
> 	(pending_frame_object_methods): Register 'level' method.
> 
> gdb/doc/ChangeLog:
> 
> 	* python.texi (Unwinding Frames in Python): Mention
> 	PendingFrame.level.
> 	(Frames In Python): Mention Frame.level.
> 
> gdb/testsuite/ChangeLog:
> 
> 	* gdb.python/py-frame.exp: Add Frame.level tests.
> 	* gdb.python/py-pending-frame-level.c: New file.
> 	* gdb.python/py-pending-frame-level.exp: New file.
> 	* gdb.python/py-pending-frame-level.py: New file.

OK for the documentation part.

Thanks.

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods
  2021-05-29 20:57 ` [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods Andrew Burgess
  2021-05-30  5:55   ` Eli Zaretskii
@ 2021-05-30 18:34   ` Andrew Burgess
  2021-05-30 18:54     ` Eli Zaretskii
  2021-06-07 14:57   ` Tom Tromey
  2021-06-21 19:42   ` Andrew Burgess
  3 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-05-30 18:34 UTC (permalink / raw)
  To: gdb-patches

I realised that I failed to add a NEW entry.  The patch below is
identical to the original patch, but also includes a NEWS entry.

Thanks,
Andrew

--

commit 6a686fcc389d6b3eb20e200ca9703c9ee2226343
Author: Andrew Burgess <andrew.burgess@embecosm.com>
Date:   Wed May 26 22:01:59 2021 +0100

    gdb/python: add PendingFrame.level and Frame.level methods
    
    Add new methods to the PendingFrame and Frame classes to obtain the
    stack frame level for each object.
    
    The use of 'level' as the method name is consistent with the existing
    attribute RecordFunctionSegment.level (though this is an attribute
    rather than a method).
    
    For Frame/PendingFrame I went with methods as these classes currently
    only use methods, including for simple data like architecture, so I
    want to be consistent with this interface.
    
    gdb/ChangeLog:
    
            * NEWS: Mention the two new methods.
            * python/py-frame.c (frapy_level): New function.
            (frame_object_methods): Register 'level' method.
            * python/py-unwind.c (pending_framepy_level): New function.
            (pending_frame_object_methods): Register 'level' method.
    
    gdb/doc/ChangeLog:
    
            * python.texi (Unwinding Frames in Python): Mention
            PendingFrame.level.
            (Frames In Python): Mention Frame.level.
    
    gdb/testsuite/ChangeLog:
    
            * gdb.python/py-frame.exp: Add Frame.level tests.
            * gdb.python/py-pending-frame-level.c: New file.
            * gdb.python/py-pending-frame-level.exp: New file.
            * gdb.python/py-pending-frame-level.py: New file.

diff --git a/gdb/NEWS b/gdb/NEWS
index ab678acec8b..0b8b363b87b 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -217,6 +217,12 @@ QMemTags
      gives the connection number as seen in 'info connections' and
      'info inferiors'.
 
+  ** New method gdb.Frame.level() which returns the stack level of the
+     frame object.
+
+  ** New method gdb.PendingFrame.level() which returns the stack level
+     of the frame object.
+
 *** Changes in GDB 10
 
 * There are new feature names for ARC targets: "org.gnu.gdb.arc.core"
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 23e6ac666ff..b7e16351a5d 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -2605,6 +2605,11 @@
 the particular frame being unwound.
 @end defun
 
+@defun PendingFrame.level ()
+Return an integer, the stack frame level for this frame.
+@xref{Frames, ,Stack Frames}.
+@end defun
+
 @subheading Unwinder Output: UnwindInfo
 
 Use @code{PendingFrame.create_unwind_info} method described above to
@@ -4813,6 +4818,10 @@
 Stack}.
 @end defun
 
+@defun Frame.level ()
+Return an integer, the stack frame level for this frame.  @xref{Frames, ,Stack Frames}.
+@end defun
+
 @node Blocks In Python
 @subsubsection Accessing blocks from Python
 
diff --git a/gdb/python/py-frame.c b/gdb/python/py-frame.c
index c8eab5291ea..4f218c40367 100644
--- a/gdb/python/py-frame.c
+++ b/gdb/python/py-frame.c
@@ -577,6 +577,27 @@ frapy_select (PyObject *self, PyObject *args)
   Py_RETURN_NONE;
 }
 
+/* The stack frame level for this frame.  */
+
+static PyObject *
+frapy_level (PyObject *self, PyObject *args)
+{
+  struct frame_info *fi;
+
+  try
+    {
+      FRAPY_REQUIRE_VALID (self, fi);
+
+      return gdb_py_object_from_int (frame_relative_level (fi)).release ();
+    }
+  catch (const gdb_exception &except)
+    {
+      GDB_PY_HANDLE_EXCEPTION (except);
+    }
+
+  Py_RETURN_NONE;
+}
+
 /* Implementation of gdb.newest_frame () -> gdb.Frame.
    Returns the newest frame object.  */
 
@@ -748,6 +769,8 @@ Return the frame's symtab and line." },
 Return the value of the variable in this frame." },
   { "select", frapy_select, METH_NOARGS,
     "Select this frame as the user's current frame." },
+  { "level", frapy_level, METH_NOARGS,
+    "The stack level of this frame." },
   {NULL}  /* Sentinel */
 };
 
diff --git a/gdb/python/py-unwind.c b/gdb/python/py-unwind.c
index d6e2f85dbc1..ff1a7e922a7 100644
--- a/gdb/python/py-unwind.c
+++ b/gdb/python/py-unwind.c
@@ -463,6 +463,23 @@ pending_framepy_architecture (PyObject *self, PyObject *args)
   return gdbarch_to_arch_object (pending_frame->gdbarch);
 }
 
+/* Implementation of PendingFrame.level (self) -> Integer.  */
+
+static PyObject *
+pending_framepy_level (PyObject *self, PyObject *args)
+{
+  pending_frame_object *pending_frame = (pending_frame_object *) self;
+
+  if (pending_frame->frame_info == NULL)
+    {
+      PyErr_SetString (PyExc_ValueError,
+		       "Attempting to read stack level from stale PendingFrame");
+      return NULL;
+    }
+  int level = frame_relative_level (pending_frame->frame_info);
+  return gdb_py_object_from_int (level).release ();
+}
+
 /* frame_unwind.this_id method.  */
 
 static void
@@ -704,6 +721,8 @@ static PyMethodDef pending_frame_object_methods[] =
     pending_framepy_architecture, METH_NOARGS,
     "architecture () -> gdb.Architecture\n"
     "The architecture for this PendingFrame." },
+  { "level", pending_framepy_level, METH_NOARGS,
+    "The stack level of this frame." },
   {NULL}  /* Sentinel */
 };
 
diff --git a/gdb/testsuite/gdb.python/py-frame.exp b/gdb/testsuite/gdb.python/py-frame.exp
index a6a5c0de726..05c7fb00dfd 100644
--- a/gdb/testsuite/gdb.python/py-frame.exp
+++ b/gdb/testsuite/gdb.python/py-frame.exp
@@ -70,6 +70,17 @@ gdb_test "up" ".*" ""
 
 gdb_py_test_silent_cmd "python f1 = gdb.selected_frame ()" "get second frame" 0
 gdb_py_test_silent_cmd "python f0 = f1.newer ()" "get first frame" 0
+gdb_py_test_silent_cmd "python f2 = f1.older ()" "get last frame" 0
+
+# Check the Frame.level method.
+gdb_test "python print ('bframe.level = %d' % bframe.level ())" \
+    "bframe\\.level = 0"
+gdb_test "python print ('f0.level = %d' % f0.level ())" \
+    "f0\\.level = 0"
+gdb_test "python print ('f1.level = %d' % f1.level ())" \
+    "f1\\.level = 1"
+gdb_test "python print ('f2.level = %d' % f2.level ())" \
+    "f2\\.level = 2"
 
 gdb_test "python print (f1 == gdb.newest_frame())" False \
     "selected frame -vs- newest frame"
diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.c b/gdb/testsuite/gdb.python/py-pending-frame-level.c
new file mode 100644
index 00000000000..5e5495c1d71
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-pending-frame-level.c
@@ -0,0 +1,49 @@
+/* This test program is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+volatile int global_var;
+
+void __attribute__ ((noinline))
+f0 (void)
+{
+  ++global_var;		/* Break here.  */
+}
+
+void __attribute__ ((noinline))
+f1 (void)
+{
+  f0 ();
+}
+
+void __attribute__ ((noinline))
+f2 (void)
+{
+  f1 ();
+}
+
+void __attribute__ ((noinline))
+f3 (void)
+{
+  f2 ();
+}
+
+int
+main (void)
+{
+  f3 ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.exp b/gdb/testsuite/gdb.python/py-pending-frame-level.exp
new file mode 100644
index 00000000000..1aadcaeacae
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-pending-frame-level.exp
@@ -0,0 +1,65 @@
+# Copyright (C) 2021 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/>.
+
+# Test gdb.PendingFrame.level method.
+
+load_lib gdb-python.exp
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
+    return -1
+}
+
+# Skip all tests if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# An initial look at the stack to ensure it is correct.
+gdb_test_sequence "bt"  "Initial backtrace" {
+    "\\r\\n#0 \[^\r\n\]* f0 \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* f1 \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* f2 \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* f3 \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* main \\(\\) at "
+}
+
+# Load the script containing the unwinder.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Now look at the stack again, we should see output from the Python
+# unwinder mixed in.
+gdb_test_sequence "bt"  "Backtrace with extra Python output" {
+    "Func f0, Level 0"
+    "Func f1, Level 1"
+    "\\r\\n#0 \[^\r\n\]* f0 \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* f1 \\(\\) at "
+    "Func f2, Level 2"
+    "\\r\\n#2 \[^\r\n\]* f2 \\(\\) at "
+    "Func f3, Level 3"
+    "\\r\\n#3 \[^\r\n\]* f3 \\(\\) at "
+    "Func main, Level 4"
+    "\\r\\n#4 \[^\r\n\]* main \\(\\) at "
+}
diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.py b/gdb/testsuite/gdb.python/py-pending-frame-level.py
new file mode 100644
index 00000000000..182edcdc0df
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-pending-frame-level.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "show level")
+
+    def __call__(self, pending_frame):
+        pc_desc = pending_frame.architecture().registers().find("pc")
+        pc = pending_frame.read_register(pc_desc)
+
+        block = gdb.block_for_pc(int(pc))
+        if block == None:
+            return None
+        func = block.function
+        if func == None:
+            return None
+
+        print("Func %s, Level %d" % (str(func), pending_frame.level()))
+
+        # This unwinder never claims any frames.
+        return None
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods
  2021-05-30 18:34   ` Andrew Burgess
@ 2021-05-30 18:54     ` Eli Zaretskii
  0 siblings, 0 replies; 48+ messages in thread
From: Eli Zaretskii @ 2021-05-30 18:54 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

> Date: Sun, 30 May 2021 19:34:42 +0100
> From: Andrew Burgess <andrew.burgess@embecosm.com>
> 
> I realised that I failed to add a NEW entry.  The patch below is
> identical to the original patch, but also includes a NEWS entry.

The NEWS entry is OK, thanks.

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-05-29 20:57 ` [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder Andrew Burgess
@ 2021-06-07 14:50   ` Tom Tromey
  2021-06-07 16:10     ` Andrew Burgess
  2021-06-07 17:07   ` Lancelot SIX
  2021-06-21 19:41   ` Andrew Burgess
  2 siblings, 1 reply; 48+ messages in thread
From: Tom Tromey @ 2021-06-07 14:50 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

>>>>> "Andrew" == Andrew Burgess <andrew.burgess@embecosm.com> writes:

Andrew> So, in this patch I propose that in the unwinder code, when
Andrew> add_saved_register is called, if it is passed a
Andrew> user-register (i.e. non-cooked) then we first fetch the register,
Andrew> extract the real register number from the value's location, and use
Andrew> that new register number when handling the add_saved_register call.

Andrew> If either the value location that we get for the user-register is not
Andrew> a cooked register then we can throw a 'Bad register' error back to the
Andrew> Python code, but in most cases this will not happen.

I was worried that requesting this would require unwinding the register,
which is what is currently being done.  But I suppose the idea is that
the value is normally lazy, so it isn't actually unwound?

Tom

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 2/5] gdb/python: move PyLong_From* calls into py-utils.c
  2021-05-29 20:57 ` [PATCH 2/5] gdb/python: move PyLong_From* calls into py-utils.c Andrew Burgess
@ 2021-06-07 14:53   ` Tom Tromey
  2021-06-21 19:42     ` Andrew Burgess
  0 siblings, 1 reply; 48+ messages in thread
From: Tom Tromey @ 2021-06-07 14:53 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

>>>>> "Andrew" == Andrew Burgess <andrew.burgess@embecosm.com> writes:

Andrew> gdb/ChangeLog:

Andrew> 	* python/py-inferior.c (infpy_get_connection_num): Call new
Andrew> 	function gdb_py_object_from_int.
Andrew> 	* python/py-utils.c (gdb_py_object_from_int): New function.
Andrew> 	* python/python-internal.h (gdb_py_object_from_int): Declare.

This seems fine to me.

It would also probably be ok to just rely on promotion and always use
gdb_py_object_from_longest.

Tom

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods
  2021-05-29 20:57 ` [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods Andrew Burgess
  2021-05-30  5:55   ` Eli Zaretskii
  2021-05-30 18:34   ` Andrew Burgess
@ 2021-06-07 14:57   ` Tom Tromey
  2021-06-21 19:42   ` Andrew Burgess
  3 siblings, 0 replies; 48+ messages in thread
From: Tom Tromey @ 2021-06-07 14:57 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

>>>>> "Andrew" == Andrew Burgess <andrew.burgess@embecosm.com> writes:

Andrew> Add new methods to the PendingFrame and Frame classes to obtain the
Andrew> stack frame level for each object.

Andrew> The use of 'level' as the method name is consistent with the existing
Andrew> attribute RecordFunctionSegment.level (though this is an attribute
Andrew> rather than a method).

Andrew> For Frame/PendingFrame I went with methods as these classes currently
Andrew> only use methods, including for simple data like architecture, so I
Andrew> want to be consistent with this interface.

gdb is already inconsistent here, something we should have been more
careful about.

This looks good to me.  Thank you.

Tom

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-06-07 14:50   ` Tom Tromey
@ 2021-06-07 16:10     ` Andrew Burgess
  2021-06-07 20:38       ` Tom Tromey
  0 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-06-07 16:10 UTC (permalink / raw)
  To: Tom Tromey; +Cc: gdb-patches

* Tom Tromey <tom@tromey.com> [2021-06-07 08:50:56 -0600]:

> >>>>> "Andrew" == Andrew Burgess <andrew.burgess@embecosm.com> writes:
> 
> Andrew> So, in this patch I propose that in the unwinder code, when
> Andrew> add_saved_register is called, if it is passed a
> Andrew> user-register (i.e. non-cooked) then we first fetch the register,
> Andrew> extract the real register number from the value's location, and use
> Andrew> that new register number when handling the add_saved_register call.
> 
> Andrew> If either the value location that we get for the user-register is not
> Andrew> a cooked register then we can throw a 'Bad register' error back to the
> Andrew> Python code, but in most cases this will not happen.
> 
> I was worried that requesting this would require unwinding the register,
> which is what is currently being done.  But I suppose the idea is that
> the value is normally lazy, so it isn't actually unwound?

You are correct, accessing any register from an unwinder is going to
give us a lazy register value, which we'd expect (in this case) to
have the real register number for $pc, and the frame-id of the next
frame.

When you open with "I was worried that requesting this would require
unwinding the register...", was our concern just that this unwinding
might be expensive?  Or are you worried that trying to do that might
break in some way?

Remember, the user can already do:

  PendingFrame.read_register('pc')

which is going to fully fetch the register value (so create the same
lazy register value as above, and then make it unlazy), so I don't
think there should be any technical problems here, even if it turns
out that the register value is not lazy.

I guess what I'm saying is, that yes it will be lazy, but I hadn't
really considered that an important part of the system.

Thanks,
Andrew

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-05-29 20:57 ` [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder Andrew Burgess
  2021-06-07 14:50   ` Tom Tromey
@ 2021-06-07 17:07   ` Lancelot SIX
  2021-06-07 17:20     ` Simon Marchi
  2021-06-21 19:41   ` Andrew Burgess
  2 siblings, 1 reply; 48+ messages in thread
From: Lancelot SIX @ 2021-06-07 17:07 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

Hi,

I just have a minor stylistic remark in the python code in the test:

> […]
> +
> +    def __call__(self, pending_frame):
> +        pc_desc = pending_frame.architecture().registers().find("pc")
> +        pc = pending_frame.read_register(pc_desc)
> +
> +        sp_desc = pending_frame.architecture().registers().find("sp")
> +        sp = pending_frame.read_register(sp_desc)
> +
> +        block = gdb.block_for_pc(int(pc))
> +        if block == None:

When looking for None, it is usually prefered to use 'is None' instead
of '== None'.  The result is the same unless there is a strange overload
of __eq__.

This pattern can also be seen in patch 3 and 4 of your series (patch 4
using both '==' and 'is' to check for None).

Lancelot.

> +            return None
> +        func = block.function
> +        if func == None:
> +            return None

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-06-07 17:07   ` Lancelot SIX
@ 2021-06-07 17:20     ` Simon Marchi
  2021-06-07 18:01       ` Lancelot SIX
  0 siblings, 1 reply; 48+ messages in thread
From: Simon Marchi @ 2021-06-07 17:20 UTC (permalink / raw)
  To: Lancelot SIX, Andrew Burgess; +Cc: gdb-patches

On 2021-06-07 1:07 p.m., Lancelot SIX via Gdb-patches wrote:
> Hi,
> 
> I just have a minor stylistic remark in the python code in the test:
> 
>> […]
>> +
>> +    def __call__(self, pending_frame):
>> +        pc_desc = pending_frame.architecture().registers().find("pc")
>> +        pc = pending_frame.read_register(pc_desc)
>> +
>> +        sp_desc = pending_frame.architecture().registers().find("sp")
>> +        sp = pending_frame.read_register(sp_desc)
>> +
>> +        block = gdb.block_for_pc(int(pc))
>> +        if block == None:
> 
> When looking for None, it is usually prefered to use 'is None' instead
> of '== None'.  The result is the same unless there is a strange overload
> of __eq__.
> 
> This pattern can also be seen in patch 3 and 4 of your series (patch 4
> using both '==' and 'is' to check for None).

I agree, that's the convention in Python.  It is not in our coding
standards, but I suggest using flake8 to check the Python code, it
reports this (and much more):


    $ flake8 testsuite/gdb.python/py-unwind-user-regs.py
    testsuite/gdb.python/py-unwind-user-regs.py:52:18: E711 comparison to None should be 'if cond is None:'
    testsuite/gdb.python/py-unwind-user-regs.py:55:17: E711 comparison to None should be 'if cond is None:'

Simon

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-06-07 17:20     ` Simon Marchi
@ 2021-06-07 18:01       ` Lancelot SIX
  2021-06-07 18:09         ` Simon Marchi
  2021-06-07 20:12         ` Andrew Burgess
  0 siblings, 2 replies; 48+ messages in thread
From: Lancelot SIX @ 2021-06-07 18:01 UTC (permalink / raw)
  To: Simon Marchi; +Cc: Andrew Burgess, gdb-patches

On Mon, Jun 07, 2021 at 01:20:33PM -0400, Simon Marchi wrote:
> On 2021-06-07 1:07 p.m., Lancelot SIX via Gdb-patches wrote:
> > Hi,
> > 
> > I just have a minor stylistic remark in the python code in the test:
> > 
> >> […]
> >> +
> >> +    def __call__(self, pending_frame):
> >> +        pc_desc = pending_frame.architecture().registers().find("pc")
> >> +        pc = pending_frame.read_register(pc_desc)
> >> +
> >> +        sp_desc = pending_frame.architecture().registers().find("sp")
> >> +        sp = pending_frame.read_register(sp_desc)
> >> +
> >> +        block = gdb.block_for_pc(int(pc))
> >> +        if block == None:
> > 
> > When looking for None, it is usually prefered to use 'is None' instead
> > of '== None'.  The result is the same unless there is a strange overload
> > of __eq__.
> > 
> > This pattern can also be seen in patch 3 and 4 of your series (patch 4
> > using both '==' and 'is' to check for None).
> 
> I agree, that's the convention in Python.  It is not in our coding
> standards, but I suggest using flake8 to check the Python code, it
> reports this (and much more):

Hi,

Actually, this is mentioned in the PEP-8[1][2], which states in the
“Programming Recommandations” section:

    Comparisons to singletons like None should always be done with is or
		is not, never the equality operators.

This leads me to an annex question. Given that I still lack a lot of
experience with the overall codebase, I tend to pick this kind of small
stylistic details more easily than design and logic problems.  I do not
always point out those I see when I read the ML, but I can totally
understand those isolated stylistic comments can be considered as noise.
If so, please let me know!

> 
> 
>     $ flake8 testsuite/gdb.python/py-unwind-user-regs.py
>     testsuite/gdb.python/py-unwind-user-regs.py:52:18: E711 comparison to None should be 'if cond is None:'
>     testsuite/gdb.python/py-unwind-user-regs.py:55:17: E711 comparison to None should be 'if cond is None:'

I am currently running the testsuite against a patch that fixes those I
found. I’ll try to post it later tonight.

Lancelot.

> 
> Simon

[1] https://www.python.org/dev/peps/pep-0008/#programming-recommendations
[2] https://sourceware.org/gdb/wiki/Internals%20GDB-Python-Coding-Standards

-- 
Lancelot SIX

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-06-07 18:01       ` Lancelot SIX
@ 2021-06-07 18:09         ` Simon Marchi
  2021-06-07 20:12         ` Andrew Burgess
  1 sibling, 0 replies; 48+ messages in thread
From: Simon Marchi @ 2021-06-07 18:09 UTC (permalink / raw)
  To: Lancelot SIX; +Cc: Andrew Burgess, gdb-patches

> Hi,
> 
> Actually, this is mentioned in the PEP-8[1][2], which states in the
> “Programming Recommandations” section:
> 
>     Comparisons to singletons like None should always be done with is or
> 		is not, never the equality operators.
> 
> This leads me to an annex question. Given that I still lack a lot of
> experience with the overall codebase, I tend to pick this kind of small
> stylistic details more easily than design and logic problems.  I do not
> always point out those I see when I read the ML, but I can totally
> understand those isolated stylistic comments can be considered as noise.
> If so, please let me know!

I think it's perfectly OK.  That's how you begin and then you grow from
there, as your understanding of how things interact in the code base
grows.  I am pretty sure the first patches I reviewed were pointing out
small and easy things.

And even if someone has more experience in GDB, they can still learn
from what you mentioned above.

>>     $ flake8 testsuite/gdb.python/py-unwind-user-regs.py
>>     testsuite/gdb.python/py-unwind-user-regs.py:52:18: E711 comparison to None should be 'if cond is None:'
>>     testsuite/gdb.python/py-unwind-user-regs.py:55:17: E711 comparison to None should be 'if cond is None:'
> 
> I am currently running the testsuite against a patch that fixes those I
> found. I’ll try to post it later tonight.

Great, thanks!

Simon

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-06-07 18:01       ` Lancelot SIX
  2021-06-07 18:09         ` Simon Marchi
@ 2021-06-07 20:12         ` Andrew Burgess
  1 sibling, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-06-07 20:12 UTC (permalink / raw)
  To: Lancelot SIX; +Cc: Simon Marchi, gdb-patches

* Lancelot SIX <lsix@lancelotsix.com> [2021-06-07 19:01:31 +0100]:

> On Mon, Jun 07, 2021 at 01:20:33PM -0400, Simon Marchi wrote:
> > On 2021-06-07 1:07 p.m., Lancelot SIX via Gdb-patches wrote:
> > > Hi,
> > > 
> > > I just have a minor stylistic remark in the python code in the test:
> > > 
> > >> […]
> > >> +
> > >> +    def __call__(self, pending_frame):
> > >> +        pc_desc = pending_frame.architecture().registers().find("pc")
> > >> +        pc = pending_frame.read_register(pc_desc)
> > >> +
> > >> +        sp_desc = pending_frame.architecture().registers().find("sp")
> > >> +        sp = pending_frame.read_register(sp_desc)
> > >> +
> > >> +        block = gdb.block_for_pc(int(pc))
> > >> +        if block == None:
> > > 
> > > When looking for None, it is usually prefered to use 'is None' instead
> > > of '== None'.  The result is the same unless there is a strange overload
> > > of __eq__.
> > > 
> > > This pattern can also be seen in patch 3 and 4 of your series (patch 4
> > > using both '==' and 'is' to check for None).
> > 
> > I agree, that's the convention in Python.  It is not in our coding
> > standards, but I suggest using flake8 to check the Python code, it
> > reports this (and much more):
> 
> Hi,
> 
> Actually, this is mentioned in the PEP-8[1][2], which states in the
> “Programming Recommandations” section:
> 
>     Comparisons to singletons like None should always be done with is or
> 		is not, never the equality operators.
> 
> This leads me to an annex question. Given that I still lack a lot of
> experience with the overall codebase, I tend to pick this kind of small
> stylistic details more easily than design and logic problems.  I do not
> always point out those I see when I read the ML, but I can totally
> understand those isolated stylistic comments can be considered as noise.
> If so, please let me know!

I agree with Simon that any constructive feedback is great.

And specifically, thanks for pointing this issue out to me.

I've updated all of the patches locally to use 'is None' now.

Thanks again,
Andrew

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-06-07 16:10     ` Andrew Burgess
@ 2021-06-07 20:38       ` Tom Tromey
  0 siblings, 0 replies; 48+ messages in thread
From: Tom Tromey @ 2021-06-07 20:38 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: Tom Tromey, gdb-patches

>> I was worried that requesting this would require unwinding the register,
>> which is what is currently being done.  But I suppose the idea is that
>> the value is normally lazy, so it isn't actually unwound?

Andrew> You are correct, accessing any register from an unwinder is going to
Andrew> give us a lazy register value, which we'd expect (in this case) to
Andrew> have the real register number for $pc, and the frame-id of the next
Andrew> frame.

Andrew> When you open with "I was worried that requesting this would require
Andrew> unwinding the register...", was our concern just that this unwinding
Andrew> might be expensive?  Or are you worried that trying to do that might
Andrew> break in some way?

I was concerned that it would break in some way, by trying to unwind a
register while in the process of unwinding.

If it works, and is reasonably robust, then I'm fine.

Andrew> I guess what I'm saying is, that yes it will be lazy, but I hadn't
Andrew> really considered that an important part of the system.

I think I see.  Thanks.

Tom

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder
  2021-05-29 20:57 ` [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder Andrew Burgess
  2021-06-07 14:50   ` Tom Tromey
  2021-06-07 17:07   ` Lancelot SIX
@ 2021-06-21 19:41   ` Andrew Burgess
  2 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-06-21 19:41 UTC (permalink / raw)
  To: gdb-patches

I've pushed this patch.

Thanks,
Andrew


* Andrew Burgess <andrew.burgess@embecosm.com> [2021-05-29 21:57:10 +0100]:

> This patch came about because I wanted to write a frame unwinder that
> would corrupt the backtrace in a particular way.  In order to achieve
> what I wanted I ended up trying to write an unwinder like this:
> 
>   class FrameId(object):
>       .... snip class definition ....
> 
>   class TestUnwinder(Unwinder):
>       def __init__(self):
>           Unwinder.__init__(self, "some name")
> 
>       def __call__(self, pending_frame):
>           pc_desc = pending_frame.architecture().registers().find("pc")
>           pc = pending_frame.read_register(pc_desc)
> 
>           sp_desc = pending_frame.architecture().registers().find("sp")
>           sp = pending_frame.read_register(sp_desc)
> 
>           # ... snip code to decide if this unwinder applies or not.
> 
>           fid = FrameId(pc, sp)
>           unwinder = pending_frame.create_unwind_info(fid)
>           unwinder.add_saved_register(pc_desc, pc)
>           unwinder.add_saved_register(sp_desc, sp)
>           return unwinder
> 
> The important things here are the two calls:
> 
>           unwinder.add_saved_register(pc_desc, pc)
>           unwinder.add_saved_register(sp_desc, sp)
> 
> On x86-64 these would fail with an assertion error:
> 
>   gdb/regcache.c:168: internal-error: int register_size(gdbarch*, int): Assertion `regnum >= 0 && regnum < gdbarch_num_cooked_regs (gdbarch)' failed.
> 
> What happens is that in unwind_infopy_add_saved_register (py-unwind.c)
> we call register_size, as register_size should only be called on
> cooked (real or pseudo) registers, and 'pc' and 'sp' are implemented
> as user registers (at least on x86-64), we trigger the assertion.
> 
> A simple fix would be to check in unwind_infopy_add_saved_register if
> the register number we are handling is a cooked register or not, if
> not we can throw a 'Bad register' error back to the Python code.
> 
> However, I think we can do better.
> 
> Consider that at the CLI we can do this:
> 
>   (gdb) set $pc=0x1234
> 
> This works because GDB first evaluates '$pc' to get a register value,
> then evaluates '0x1234' to create a value encapsulating the
> immediate.  The contents of the immediate value are then copied back
> to the location of the register value representing '$pc'.
> 
> The value location for a user-register will (usually) be the location
> of the real register that was accessed, so on x86-64 we'd expect this
> to be $rip.
> 
> So, in this patch I propose that in the unwinder code, when
> add_saved_register is called, if it is passed a
> user-register (i.e. non-cooked) then we first fetch the register,
> extract the real register number from the value's location, and use
> that new register number when handling the add_saved_register call.
> 
> If either the value location that we get for the user-register is not
> a cooked register then we can throw a 'Bad register' error back to the
> Python code, but in most cases this will not happen.
> 
> gdb/ChangeLog:
> 
> 	* python/py-unwind.c (unwind_infopy_add_saved_register): Handle
> 	saving user registers.
> 
> gdb/testsuite/ChangeLog:
> 
> 	* gdb.python/py-unwind-user-regs.c: New file.
> 	* gdb.python/py-unwind-user-regs.exp: New file.
> 	* gdb.python/py-unwind-user-regs.py: New file.
> ---
>  gdb/ChangeLog                                 |  5 +
>  gdb/python/py-unwind.c                        | 21 ++++
>  gdb/testsuite/ChangeLog                       |  6 ++
>  .../gdb.python/py-unwind-user-regs.c          | 37 +++++++
>  .../gdb.python/py-unwind-user-regs.exp        | 98 +++++++++++++++++++
>  .../gdb.python/py-unwind-user-regs.py         | 72 ++++++++++++++
>  6 files changed, 239 insertions(+)
>  create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.c
>  create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.exp
>  create mode 100644 gdb/testsuite/gdb.python/py-unwind-user-regs.py
> 
> diff --git a/gdb/python/py-unwind.c b/gdb/python/py-unwind.c
> index 7c195eb539d..d6e2f85dbc1 100644
> --- a/gdb/python/py-unwind.c
> +++ b/gdb/python/py-unwind.c
> @@ -27,6 +27,7 @@
>  #include "python-internal.h"
>  #include "regcache.h"
>  #include "valprint.h"
> +#include "user-regs.h"
>  
>  /* Debugging of Python unwinders.  */
>  
> @@ -265,6 +266,26 @@ unwind_infopy_add_saved_register (PyObject *self, PyObject *args)
>        PyErr_SetString (PyExc_ValueError, "Bad register");
>        return NULL;
>      }
> +
> +  /* If REGNUM identifies a user register then *maybe* we can convert this
> +     to a real (i.e. non-user) register.  The maybe qualifier is because we
> +     don't know what user registers each target might add, however, the
> +     following logic should work for the usual style of user registers,
> +     where the read function just forwards the register read on to some
> +     other register with no adjusting the value.  */
> +  if (regnum >= gdbarch_num_cooked_regs (pending_frame->gdbarch))
> +    {
> +      struct value *user_reg_value
> +	= value_of_user_reg (regnum, pending_frame->frame_info);
> +      if (VALUE_LVAL (user_reg_value) == lval_register)
> +	regnum = VALUE_REGNUM (user_reg_value);
> +      if (regnum >= gdbarch_num_cooked_regs (pending_frame->gdbarch))
> +	{
> +	  PyErr_SetString (PyExc_ValueError, "Bad register");
> +	  return NULL;
> +	}
> +    }
> +
>    {
>      struct value *value;
>      size_t data_size;
> diff --git a/gdb/testsuite/gdb.python/py-unwind-user-regs.c b/gdb/testsuite/gdb.python/py-unwind-user-regs.c
> new file mode 100644
> index 00000000000..8d1efd1a85d
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-unwind-user-regs.c
> @@ -0,0 +1,37 @@
> +/* This test program is part of GDB, the GNU debugger.
> +
> +   Copyright 2021 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/>.  */
> +
> +volatile int global_var;
> +
> +void __attribute__ ((noinline))
> +foo (void)
> +{
> +  ++global_var;		/* Break here.  */
> +}
> +
> +void __attribute__ ((noinline))
> +bar (void)
> +{
> +  foo ();
> +}
> +
> +int
> +main (void)
> +{
> +  bar ();
> +  return 0;
> +}
> diff --git a/gdb/testsuite/gdb.python/py-unwind-user-regs.exp b/gdb/testsuite/gdb.python/py-unwind-user-regs.exp
> new file mode 100644
> index 00000000000..7ae3a5bb19f
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-unwind-user-regs.exp
> @@ -0,0 +1,98 @@
> +# Copyright (C) 2021 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/>.
> +
> +# Setup an unwinder that uses gdb.UnwindInfo.add_saved_register with
> +# the register's 'pc' and 'sp'.  On some (all?) targets, these
> +# registers are implemented as user-registers, and so can't normally
> +# be written to directly.
> +#
> +# The Python unwinder now includes code similar to how the expression
> +# evaluator would handle something like 'set $pc=0x1234', we fetch the
> +# value of '$pc', and then use the value's location to tell us which
> +# register to write to.
> +#
> +# The unwinder defined here deliberately breaks the unwind by setting
> +# the unwound $pc and $sp to be equal to the current frame's $pc and
> +# $sp.  GDB will spot this as a loop in the backtrace and terminate
> +# the unwind.
> +#
> +# However, by the time the unwind terminates we have already shown
> +# that it is possible to call add_saved_register with a user-register,
> +# so the test is considered passed.
> +#
> +# For completeness this test checks two cases, calling
> +# add_saved_register with a gdb.RegisterDescriptor and calling
> +# add_saved_register with a string containing the register name.
> +
> +load_lib gdb-python.exp
> +
> +standard_testfile
> +
> +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
> +    return -1
> +}
> +
> +# Skip all tests if Python scripting is not enabled.
> +if { [skip_python_tests] } { continue }
> +
> +if ![runto_main] then {
> +    fail "can't run to main"
> +    return 0
> +}
> +
> +set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
> +
> +gdb_breakpoint [gdb_get_line_number "Break here"]
> +gdb_continue_to_breakpoint "stop at test breakpoint"
> +
> +# Load the script containing the unwinders.  There are actually two
> +# unwinders defined here that will catch the same function, so we
> +# immediately disable one of the unwinders.
> +gdb_test_no_output "source ${pyfile}"\
> +    "import python scripts"
> +gdb_test "disable unwinder global \"break unwinding using strings\"" \
> +    "1 unwinder disabled" "disable the unwinder that uses strings"
> +
> +# At this point we are using the unwinder that passes a
> +# gdb.RegisterDescriptor to add_saved_register.
> +gdb_test_sequence "bt"  "Backtrace corrupted by descriptor based unwinder" {
> +    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
> +    "Backtrace stopped: previous frame inner to this frame \\(corrupt stack\\?\\)"
> +}
> +
> +# Disable the unwinder that calls add_saved_register with a
> +# gdb.RegisterDescriptor, and enable the unwinder that calls
> +# add_saved_register with a string (containing the register name).
> +gdb_test "disable unwinder global \"break unwinding using descriptors\"" \
> +    "1 unwinder disabled" "disable the unwinder that uses descriptors"
> +gdb_test "enable unwinder global \"break unwinding using strings\"" \
> +    "1 unwinder enabled" "enable the unwinder that uses strings"
> +gdb_test_sequence "bt"  "Backtrace corrupted by string based unwinder" {
> +    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
> +    "Backtrace stopped: previous frame inner to this frame \\(corrupt stack\\?\\)"
> +}
> +
> +# Just for completeness, disable the string unwinder again (neither of
> +# our special unwinders are now enabled), and check the backtrace.  We
> +# now get the complete stack back to main.
> +gdb_test "disable unwinder global \"break unwinding using strings\"" \
> +    "1 unwinder disabled" "disable the unwinder that uses strings again"
> +gdb_test_sequence "bt"  "Backtrace not corrupted when using no unwinder" {
> +    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
> +    "\\r\\n#2 \[^\r\n\]* main \\(\\) at "
> +}
> diff --git a/gdb/testsuite/gdb.python/py-unwind-user-regs.py b/gdb/testsuite/gdb.python/py-unwind-user-regs.py
> new file mode 100644
> index 00000000000..e5edd7cbd9c
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-unwind-user-regs.py
> @@ -0,0 +1,72 @@
> +# Copyright (C) 2021 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/>.
> +
> +import gdb
> +from gdb.unwinder import Unwinder
> +
> +
> +class FrameId(object):
> +    def __init__(self, sp, pc):
> +        self._sp = sp
> +        self._pc = pc
> +
> +    @property
> +    def sp(self):
> +        return self._sp
> +
> +    @property
> +    def pc(self):
> +        return self._pc
> +
> +
> +class TestUnwinder(Unwinder):
> +    def __init__(self, use_descriptors):
> +        if use_descriptors:
> +            tag = "using descriptors"
> +        else:
> +            tag = "using strings"
> +
> +        Unwinder.__init__(self, "break unwinding %s" % tag)
> +        self._use_descriptors = use_descriptors
> +
> +    def __call__(self, pending_frame):
> +        pc_desc = pending_frame.architecture().registers().find("pc")
> +        pc = pending_frame.read_register(pc_desc)
> +
> +        sp_desc = pending_frame.architecture().registers().find("sp")
> +        sp = pending_frame.read_register(sp_desc)
> +
> +        block = gdb.block_for_pc(int(pc))
> +        if block == None:
> +            return None
> +        func = block.function
> +        if func == None:
> +            return None
> +        if str(func) != "bar":
> +            return None
> +
> +        fid = FrameId(pc, sp)
> +        unwinder = pending_frame.create_unwind_info(fid)
> +        if self._use_descriptors:
> +            unwinder.add_saved_register(pc_desc, pc)
> +            unwinder.add_saved_register(sp_desc, sp)
> +        else:
> +            unwinder.add_saved_register("pc", pc)
> +            unwinder.add_saved_register("sp", sp)
> +        return unwinder
> +
> +
> +gdb.unwinder.register_unwinder(None, TestUnwinder(True), True)
> +gdb.unwinder.register_unwinder(None, TestUnwinder(False), True)
> -- 
> 2.25.4
> 

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 2/5] gdb/python: move PyLong_From* calls into py-utils.c
  2021-06-07 14:53   ` Tom Tromey
@ 2021-06-21 19:42     ` Andrew Burgess
  0 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-06-21 19:42 UTC (permalink / raw)
  To: gdb-patches

* Tom Tromey <tom@tromey.com> [2021-06-07 08:53:52 -0600]:

> >>>>> "Andrew" == Andrew Burgess <andrew.burgess@embecosm.com> writes:
> 
> Andrew> gdb/ChangeLog:
> 
> Andrew> 	* python/py-inferior.c (infpy_get_connection_num): Call new
> Andrew> 	function gdb_py_object_from_int.
> Andrew> 	* python/py-utils.c (gdb_py_object_from_int): New function.
> Andrew> 	* python/python-internal.h (gdb_py_object_from_int): Declare.
> 
> This seems fine to me.
> 
> It would also probably be ok to just rely on promotion and always use
> gdb_py_object_from_longest.

I took this advice and pushed the patch below.

Thanks,
Andrew

---

commit 8b9c48b287d42d1c816f441e4273dcb8c7af1876
Author: Andrew Burgess <andrew.burgess@embecosm.com>
Date:   Wed May 26 21:28:11 2021 +0100

    gdb/python: move PyLong_From* calls into py-utils.c
    
    We already have two helper functions in py-utils.c:
    
      gdb_py_object_from_longest (LONGEST l)
      gdb_py_object_from_ulongest (ULONGEST l)
    
    these wrap around calls to either PyLong_FromLongLong,
    PyLong_FromLong, or PyInt_From_Long (if Python 2 is being used).
    
    There is one place in gdb/python/* where a call to PyLong_FromLong was
    added outside of the above utility functions, this was done in the
    recent commit:
    
      commit 55789354fcbaf879f3ca8475b647b2747dec486e
      Date:   Fri May 14 11:56:31 2021 +0200
    
          gdb/python: add a 'connection_num' attribute to Inferior objects
    
    In this commit I replace the direct use of PyLong_FromLong with a call
    to gdb_py_object_from_longest.  The only real change with this commit,
    is that, for Python 2, we will now end up calling PyInt_FromLong
    instead of PyLong_FromLong, but this should be invisible to the user.
    For Python 3 there should be absolutely no change.
    
    gdb/ChangeLog:
    
            * python/py-inferior.c (infpy_get_connection_num): Call
            gdb_py_object_from_longest instead of PyLong_FromLong directly.

diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c
index 336c6426b8c..39efa804d80 100644
--- a/gdb/python/py-inferior.c
+++ b/gdb/python/py-inferior.c
@@ -441,7 +441,7 @@ infpy_get_connection_num (PyObject *self, void *closure)
   if (target == nullptr)
     Py_RETURN_NONE;
 
-  return PyLong_FromLong (target->connection_number);
+  return gdb_py_object_from_longest (target->connection_number).release ();
 }
 
 static PyObject *

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods
  2021-05-29 20:57 ` [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods Andrew Burgess
                     ` (2 preceding siblings ...)
  2021-06-07 14:57   ` Tom Tromey
@ 2021-06-21 19:42   ` Andrew Burgess
  3 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-06-21 19:42 UTC (permalink / raw)
  To: gdb-patches

I've pushed this patch now.

Thanks,
Andrew

* Andrew Burgess <andrew.burgess@embecosm.com> [2021-05-29 21:57:12 +0100]:

> Add new methods to the PendingFrame and Frame classes to obtain the
> stack frame level for each object.
> 
> The use of 'level' as the method name is consistent with the existing
> attribute RecordFunctionSegment.level (though this is an attribute
> rather than a method).
> 
> For Frame/PendingFrame I went with methods as these classes currently
> only use methods, including for simple data like architecture, so I
> want to be consistent with this interface.
> 
> gdb/ChangeLog:
> 
> 	* python/py-frame.c (frapy_level): New function.
> 	(frame_object_methods): Register 'level' method.
> 	* python/py-unwind.c (pending_framepy_level): New function.
> 	(pending_frame_object_methods): Register 'level' method.
> 
> gdb/doc/ChangeLog:
> 
> 	* python.texi (Unwinding Frames in Python): Mention
> 	PendingFrame.level.
> 	(Frames In Python): Mention Frame.level.
> 
> gdb/testsuite/ChangeLog:
> 
> 	* gdb.python/py-frame.exp: Add Frame.level tests.
> 	* gdb.python/py-pending-frame-level.c: New file.
> 	* gdb.python/py-pending-frame-level.exp: New file.
> 	* gdb.python/py-pending-frame-level.py: New file.
> ---
>  gdb/ChangeLog                                 |  7 ++
>  gdb/doc/ChangeLog                             |  6 ++
>  gdb/doc/python.texi                           |  9 +++
>  gdb/python/py-frame.c                         | 23 +++++++
>  gdb/python/py-unwind.c                        | 19 ++++++
>  gdb/testsuite/ChangeLog                       |  7 ++
>  gdb/testsuite/gdb.python/py-frame.exp         | 11 ++++
>  .../gdb.python/py-pending-frame-level.c       | 49 ++++++++++++++
>  .../gdb.python/py-pending-frame-level.exp     | 65 +++++++++++++++++++
>  .../gdb.python/py-pending-frame-level.py      | 55 ++++++++++++++++
>  10 files changed, 251 insertions(+)
>  create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.c
>  create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.exp
>  create mode 100644 gdb/testsuite/gdb.python/py-pending-frame-level.py
> 
> diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
> index 23e6ac666ff..b7e16351a5d 100644
> --- a/gdb/doc/python.texi
> +++ b/gdb/doc/python.texi
> @@ -2605,6 +2605,11 @@
>  the particular frame being unwound.
>  @end defun
>  
> +@defun PendingFrame.level ()
> +Return an integer, the stack frame level for this frame.
> +@xref{Frames, ,Stack Frames}.
> +@end defun
> +
>  @subheading Unwinder Output: UnwindInfo
>  
>  Use @code{PendingFrame.create_unwind_info} method described above to
> @@ -4813,6 +4818,10 @@
>  Stack}.
>  @end defun
>  
> +@defun Frame.level ()
> +Return an integer, the stack frame level for this frame.  @xref{Frames, ,Stack Frames}.
> +@end defun
> +
>  @node Blocks In Python
>  @subsubsection Accessing blocks from Python
>  
> diff --git a/gdb/python/py-frame.c b/gdb/python/py-frame.c
> index c8eab5291ea..4f218c40367 100644
> --- a/gdb/python/py-frame.c
> +++ b/gdb/python/py-frame.c
> @@ -577,6 +577,27 @@ frapy_select (PyObject *self, PyObject *args)
>    Py_RETURN_NONE;
>  }
>  
> +/* The stack frame level for this frame.  */
> +
> +static PyObject *
> +frapy_level (PyObject *self, PyObject *args)
> +{
> +  struct frame_info *fi;
> +
> +  try
> +    {
> +      FRAPY_REQUIRE_VALID (self, fi);
> +
> +      return gdb_py_object_from_int (frame_relative_level (fi)).release ();
> +    }
> +  catch (const gdb_exception &except)
> +    {
> +      GDB_PY_HANDLE_EXCEPTION (except);
> +    }
> +
> +  Py_RETURN_NONE;
> +}
> +
>  /* Implementation of gdb.newest_frame () -> gdb.Frame.
>     Returns the newest frame object.  */
>  
> @@ -748,6 +769,8 @@ Return the frame's symtab and line." },
>  Return the value of the variable in this frame." },
>    { "select", frapy_select, METH_NOARGS,
>      "Select this frame as the user's current frame." },
> +  { "level", frapy_level, METH_NOARGS,
> +    "The stack level of this frame." },
>    {NULL}  /* Sentinel */
>  };
>  
> diff --git a/gdb/python/py-unwind.c b/gdb/python/py-unwind.c
> index d6e2f85dbc1..ff1a7e922a7 100644
> --- a/gdb/python/py-unwind.c
> +++ b/gdb/python/py-unwind.c
> @@ -463,6 +463,23 @@ pending_framepy_architecture (PyObject *self, PyObject *args)
>    return gdbarch_to_arch_object (pending_frame->gdbarch);
>  }
>  
> +/* Implementation of PendingFrame.level (self) -> Integer.  */
> +
> +static PyObject *
> +pending_framepy_level (PyObject *self, PyObject *args)
> +{
> +  pending_frame_object *pending_frame = (pending_frame_object *) self;
> +
> +  if (pending_frame->frame_info == NULL)
> +    {
> +      PyErr_SetString (PyExc_ValueError,
> +		       "Attempting to read stack level from stale PendingFrame");
> +      return NULL;
> +    }
> +  int level = frame_relative_level (pending_frame->frame_info);
> +  return gdb_py_object_from_int (level).release ();
> +}
> +
>  /* frame_unwind.this_id method.  */
>  
>  static void
> @@ -704,6 +721,8 @@ static PyMethodDef pending_frame_object_methods[] =
>      pending_framepy_architecture, METH_NOARGS,
>      "architecture () -> gdb.Architecture\n"
>      "The architecture for this PendingFrame." },
> +  { "level", pending_framepy_level, METH_NOARGS,
> +    "The stack level of this frame." },
>    {NULL}  /* Sentinel */
>  };
>  
> diff --git a/gdb/testsuite/gdb.python/py-frame.exp b/gdb/testsuite/gdb.python/py-frame.exp
> index a6a5c0de726..05c7fb00dfd 100644
> --- a/gdb/testsuite/gdb.python/py-frame.exp
> +++ b/gdb/testsuite/gdb.python/py-frame.exp
> @@ -70,6 +70,17 @@ gdb_test "up" ".*" ""
>  
>  gdb_py_test_silent_cmd "python f1 = gdb.selected_frame ()" "get second frame" 0
>  gdb_py_test_silent_cmd "python f0 = f1.newer ()" "get first frame" 0
> +gdb_py_test_silent_cmd "python f2 = f1.older ()" "get last frame" 0
> +
> +# Check the Frame.level method.
> +gdb_test "python print ('bframe.level = %d' % bframe.level ())" \
> +    "bframe\\.level = 0"
> +gdb_test "python print ('f0.level = %d' % f0.level ())" \
> +    "f0\\.level = 0"
> +gdb_test "python print ('f1.level = %d' % f1.level ())" \
> +    "f1\\.level = 1"
> +gdb_test "python print ('f2.level = %d' % f2.level ())" \
> +    "f2\\.level = 2"
>  
>  gdb_test "python print (f1 == gdb.newest_frame())" False \
>      "selected frame -vs- newest frame"
> diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.c b/gdb/testsuite/gdb.python/py-pending-frame-level.c
> new file mode 100644
> index 00000000000..5e5495c1d71
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-pending-frame-level.c
> @@ -0,0 +1,49 @@
> +/* This test program is part of GDB, the GNU debugger.
> +
> +   Copyright 2021 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/>.  */
> +
> +volatile int global_var;
> +
> +void __attribute__ ((noinline))
> +f0 (void)
> +{
> +  ++global_var;		/* Break here.  */
> +}
> +
> +void __attribute__ ((noinline))
> +f1 (void)
> +{
> +  f0 ();
> +}
> +
> +void __attribute__ ((noinline))
> +f2 (void)
> +{
> +  f1 ();
> +}
> +
> +void __attribute__ ((noinline))
> +f3 (void)
> +{
> +  f2 ();
> +}
> +
> +int
> +main (void)
> +{
> +  f3 ();
> +  return 0;
> +}
> diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.exp b/gdb/testsuite/gdb.python/py-pending-frame-level.exp
> new file mode 100644
> index 00000000000..1aadcaeacae
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-pending-frame-level.exp
> @@ -0,0 +1,65 @@
> +# Copyright (C) 2021 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/>.
> +
> +# Test gdb.PendingFrame.level method.
> +
> +load_lib gdb-python.exp
> +
> +standard_testfile
> +
> +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
> +    return -1
> +}
> +
> +# Skip all tests if Python scripting is not enabled.
> +if { [skip_python_tests] } { continue }
> +
> +if ![runto_main] then {
> +    fail "can't run to main"
> +    return 0
> +}
> +
> +set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
> +
> +gdb_breakpoint [gdb_get_line_number "Break here"]
> +gdb_continue_to_breakpoint "stop at test breakpoint"
> +
> +# An initial look at the stack to ensure it is correct.
> +gdb_test_sequence "bt"  "Initial backtrace" {
> +    "\\r\\n#0 \[^\r\n\]* f0 \\(\\) at "
> +    "\\r\\n#1 \[^\r\n\]* f1 \\(\\) at "
> +    "\\r\\n#2 \[^\r\n\]* f2 \\(\\) at "
> +    "\\r\\n#3 \[^\r\n\]* f3 \\(\\) at "
> +    "\\r\\n#4 \[^\r\n\]* main \\(\\) at "
> +}
> +
> +# Load the script containing the unwinder.
> +gdb_test_no_output "source ${pyfile}"\
> +    "import python scripts"
> +
> +# Now look at the stack again, we should see output from the Python
> +# unwinder mixed in.
> +gdb_test_sequence "bt"  "Backtrace with extra Python output" {
> +    "Func f0, Level 0"
> +    "Func f1, Level 1"
> +    "\\r\\n#0 \[^\r\n\]* f0 \\(\\) at "
> +    "\\r\\n#1 \[^\r\n\]* f1 \\(\\) at "
> +    "Func f2, Level 2"
> +    "\\r\\n#2 \[^\r\n\]* f2 \\(\\) at "
> +    "Func f3, Level 3"
> +    "\\r\\n#3 \[^\r\n\]* f3 \\(\\) at "
> +    "Func main, Level 4"
> +    "\\r\\n#4 \[^\r\n\]* main \\(\\) at "
> +}
> diff --git a/gdb/testsuite/gdb.python/py-pending-frame-level.py b/gdb/testsuite/gdb.python/py-pending-frame-level.py
> new file mode 100644
> index 00000000000..182edcdc0df
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-pending-frame-level.py
> @@ -0,0 +1,55 @@
> +# Copyright (C) 2021 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/>.
> +
> +import gdb
> +from gdb.unwinder import Unwinder
> +
> +
> +class FrameId(object):
> +    def __init__(self, sp, pc):
> +        self._sp = sp
> +        self._pc = pc
> +
> +    @property
> +    def sp(self):
> +        return self._sp
> +
> +    @property
> +    def pc(self):
> +        return self._pc
> +
> +
> +class TestUnwinder(Unwinder):
> +    def __init__(self):
> +        Unwinder.__init__(self, "show level")
> +
> +    def __call__(self, pending_frame):
> +        pc_desc = pending_frame.architecture().registers().find("pc")
> +        pc = pending_frame.read_register(pc_desc)
> +
> +        block = gdb.block_for_pc(int(pc))
> +        if block == None:
> +            return None
> +        func = block.function
> +        if func == None:
> +            return None
> +
> +        print("Func %s, Level %d" % (str(func), pending_frame.level()))
> +
> +        # This unwinder never claims any frames.
> +        return None
> +
> +
> +gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
> -- 
> 2.25.4
> 

^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv2 0/2] Fix for an assertion when unwinding with inline frames
  2021-05-29 20:57 [PATCH 0/5] Fix for an assertion when unwinding with inline frames Andrew Burgess
                   ` (4 preceding siblings ...)
  2021-05-29 20:57 ` [PATCH 5/5] gdb: remove VALUE_FRAME_ID Andrew Burgess
@ 2021-06-21 19:46 ` Andrew Burgess
  2021-06-21 19:46   ` [PATCHv2 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
                     ` (2 more replies)
  5 siblings, 3 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-06-21 19:46 UTC (permalink / raw)
  To: gdb-patches

I pushed patches 1-3 of the V1 series as these were useful changes in
their own right.

The V2 series is just the last two patches of the original series
rebased onto current upstream master, there's no other changes.

Thanks,
Andrew

---

Andrew Burgess (2):
  gdb: prevent an assertion when computing the frame_id for an inline
    frame
  gdb: remove VALUE_FRAME_ID

 gdb/ChangeLog                                 |  15 ++
 gdb/frame.c                                   |  61 +++++---
 gdb/frame.h                                   |   4 -
 gdb/testsuite/ChangeLog                       |  10 ++
 .../gdb.base/inline-frame-bad-unwind.c        |  58 ++++++++
 .../gdb.base/inline-frame-bad-unwind.exp      | 139 ++++++++++++++++++
 .../gdb.base/inline-frame-bad-unwind.py       |  85 +++++++++++
 gdb/valops.c                                  |  17 ++-
 gdb/value.c                                   |   5 +-
 gdb/value.h                                   |   6 -
 10 files changed, 362 insertions(+), 38 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.py

-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv2 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-06-21 19:46 ` [PATCHv2 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
@ 2021-06-21 19:46   ` Andrew Burgess
  2021-07-05 11:39     ` Pedro Alves
  2021-06-21 19:46   ` [PATCHv2 2/2] gdb: remove VALUE_FRAME_ID Andrew Burgess
  2021-07-20  9:10   ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
  2 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-06-21 19:46 UTC (permalink / raw)
  To: gdb-patches

I ran into this assertion while GDB was trying to unwind the stack:

  gdb/inline-frame.c:173: internal-error: void inline_frame_this_id(frame_info*, void**, frame_id*): Assertion `frame_id_p (*this_id)' failed.

That is, when building the frame_id for an inline frame, GDB asks for
the frame_id of the previous frame.  Unfortunately, no valid frame_id
was returned for the previous frame, and so the assertion triggers.

What is happening is this, I had a stack that looked something like
this (the arrows '->' point from caller to callee):

  normal_frame -> inline_frame

However, for whatever reason (e.g. broken debug information, or
corrupted stack contents in the inferior), when GDB tries to unwind
"normal_frame", it ends up getting back effectively the same frame,
thus the call stack looks like this to GDB:

  .-> normal_frame -> inline_frame
  |     |
  '-----'

Given such a situation we would expect GDB to terminate the stack with
an error like this:

  Backtrace stopped: previous frame identical to this frame (corrupt stack?)

However, the inline_frame causes a problem, and here's why:

When unwinding we start from the sentinel frame and call
get_prev_frame.  We eventually end up in get_prev_frame_if_no_cycle,
in here we create a raw frame, and as this is frame #0 we immediately
return.

However, eventually we will try to unwind the stack further.  When we
do this we inevitably needing to know the frame_id for frame #0, and
so, eventually, we end up in compute_frame_id.

In compute_frame_id we first find the right unwinder for this frame,
in our case (i.e. for inline_frame) the $pc is within the function
normal_frame, but also within a block associated with the inlined
function inline_frame, as such the inline frame unwinder claims this
frame.

Back in compute_frame_id we next compute the frame_id, for our
inline_frame this means a call to inline_frame_this_id.

The ID of an inline frame is based on the id of the previous frame, so
from inline_frame_this_id we call get_prev_frame_always, this
eventually calls get_prev_frame_if_no_cycle again, which creates
another raw frame and calls compute_frame_id (for frames other than
frame 0 we immediately compute the frame_id).

In compute_frame_id we again identify the correct unwinder for this
frame.  Our $pc is unchanged, however, the fact that the next frame is
of type INLINE_FRAME prevents the inline frame unwinder from claiming
this frame again, and so, the standard DWARF frame unwinder claims
normal_frame.

We return to compute_frame_id and call the standard DWARF function to
build the frame_id for normal_frame.

With the frame_id of normal_frame figured out we return to
compute_frame_id, and then to get_prev_frame_if_no_cycle, where we add
the ID for normal_frame into the frame_id cache, and return the frame
back to inline_frame_this_id.

From inline_frame_this_id we build a frame_id for inline_frame and
return to compute_frame_id, and then to get_prev_frame_if_no_cycle,
which adds the frame_id for inline_frame into the frame_id cache.

So far, so good.

However, as we are trying to unwind the compute stack, we eventually
ask for the previous frame of normal_frame, remember, that at this
point GDB doesn't know the stack is corrupted (with a cycle), GDB
still needs to figure that out.

So, we eventually end up in get_prev_frame_if_no_cycle where we create
a raw frame and call compute_frame_id, remember, this is for the frame
before normal_frame.

The first task for compute_frame_id is to find the unwinder for this
frame, so all of the frame sniffers are tried in order, this includes
the inline frame sniffer.

The inline frame sniffer asks for the $pc, this request is sent up the
stack to normal_frame, which, due to its cyclic behaviour, tells GDB
that the $pc in the previous frame was the same as the $pc in
normal_frame.

GDB spots that this $pc corresponds to both the function normal_frame
and also the inline function inline_frame.  As the next frame is not
an INLINE_FRAME then GDB figures that we have not yet built a frame to
cover inline_frame, and so the inline sniffer claims this new frame.
Our stack is now looking like this:

  inline_frame -> normal_frame -> inline_frame

But, we have not yet computed the frame id for the outer most (on the
left) inline_frame.  After the frame sniffer has claimed the inline
frame GDB returns to compute_frame_id and calls inline_frame_this_id.

In here GDB calls get_prev_frame_always, which eventually ends up
in get_prev_frame_if_no_cycle again, where we create a raw frame and
call compute_frame_id.

Just like before compute_frame_id tries to find an unwinder for this
new frame, it sees that the $pc is within both normal_frame and
inline_frame, but the next frame is, again, an INLINE_FRAME, so, just
like before the standard DWARF unwinder claims this frame.  Back in
compute_frame_id we again call the standard DWARF function to build
the frame_id for this new copy of normal_frame.

At this point the stack looks like this:

  normal_frame -> inline_frame -> normal_frame -> inline_frame

After compute_frame_id we return to get_prev_frame_if_no_cycle, where
we try to add the frame_id for the new normal_frame into the frame_id
cache, however, unlike before, we fail to add this frame_id as this is
a duplicate of the previous normal_frame frame_id.  Having found a
duplicate get_prev_frame_if_no_cycle unlinks the new frame from the
stack, and returns nullptr, the stack now looks like this:

  inline_frame -> normal_frame -> inline_frame

The nullptr result from get_prev_frame_if_no_cycle is fed back to
inline_frame_this_id, which forwards this to get_frame_id, which
immediately returns null_frame_id.  As null_frame_id is not considered
a valid frame_id, this is what triggers the assertion.

In summary then:

 - inline_frame_this_id currently assumes that as the inline frame
   exists, we will always get a valid frame back from
   get_prev_frame_always,

 - get_prev_frame_if_no_cycle currently assumes that it is safe to
   return nullptr when it sees a cycle.

Notice that in frame.c:compute_frame_id, this code:

  fi->this_id.value = outer_frame_id;
  fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
  gdb_assert (frame_id_p (fi->this_id.value));

The assertion makes it clear that the this_id function must always
return a valid frame_id (e.g. null_frame_id is not a valid return
value), and similarly in inline_frame.c:inline_frame_this_id this
code:

  *this_id = get_frame_id (get_prev_frame_always (this_frame));
  /* snip comment */
  gdb_assert (frame_id_p (*this_id));

Makes it clear that every inline frame expects to be able to get a
previous frame, which will have a valid frame_id.

As I have discussed above, these assumptions don't currently hold in
all cases.

One possibility would be to move the call to get_prev_frame_always
forward from inline_frame_this_id to inline_frame_sniffer, however,
this falls foul of (in frame.c:frame_cleanup_after_sniffer) this
assertion:

  /* No sniffer should extend the frame chain; sniff based on what is
     already certain.  */
  gdb_assert (!frame->prev_p);

This assert prohibits any sniffer from trying to get the previous
frame, as getting the previous frame is likely to depend on the next
frame, I can understand why this assertion is a good thing, and I'm in
no rush to alter this rule.

The solution I am proposing here is to add a special case to
get_prev_frame_if_no_cycle, such that, if we find a cycle, and we know
we are fetching the previous frame as a result of computing the
frame_id for the next frame, which is an INLINE_FRAME, then, instead
of returning nullptr, do still return the frame.

This is safe (I claim) because, if the frame_id of the NORMAL_FRAME
was a duplicate then the INLINE_FRAME should also be a duplicate, and
so, the INLINE_FRAME will be rejected as a duplicate just as the
NORMAL_FRAME was.

To catch cases where this special case might go wrong I do two things,
first, even though I do now return the previous frame, I still
disconnect the previous frame from the next/prev links, this allows me
to do the second thing, which is to add an assert, if a frame is added
to the frame_id cache, and it is an INLINE_FRAME, then its prev link
must not be nullptr.

This logic should be sound as, computing the frame_id for an inline
frame requires GDB to fetch the previous frame.  For most (all?) other
frame types this is not the case, and so, it is only inline frames for
which you are guaranteed that, after computing the frame_id, the
previous frame is known.

So, if my new special case triggers, and we return a previous frame
even when that previous frame is a duplicate, and _somehow_ the inline
frame that we return this special case frame too is not then rejected
from the frame_id cache, the inline frame's prev link will be nullptr,
and the new assertion will trigger.

gdb/ChangeLog:

	* frame.c (get_prev_frame_if_no_cycle): Always return prev_frame
	when computing the frame_id for an INLINE_FRAME.  Add an extra
	assertion.

gdb/testsuite/ChangeLog:

	* gdb.base/inline-frame-bad-unwind.c: New file.
	* gdb.base/inline-frame-bad-unwind.exp: New file.
	* gdb.base/inline-frame-bad-unwind.py: New file.
---
 gdb/ChangeLog                                 |   6 +
 gdb/frame.c                                   |  45 ++++++-
 gdb/testsuite/ChangeLog                       |   6 +
 .../gdb.base/inline-frame-bad-unwind.c        |  58 +++++++++
 .../gdb.base/inline-frame-bad-unwind.exp      | 122 ++++++++++++++++++
 .../gdb.base/inline-frame-bad-unwind.py       |  85 ++++++++++++
 6 files changed, 321 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-bad-unwind.py

diff --git a/gdb/frame.c b/gdb/frame.c
index d2e14c831a0..b0943c02115 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2125,7 +2125,50 @@ get_prev_frame_if_no_cycle (struct frame_info *this_frame)
 	  /* Unlink.  */
 	  prev_frame->next = NULL;
 	  this_frame->prev = NULL;
-	  prev_frame = NULL;
+
+	  /* Consider the call stack A->B, where A is a normal frame and B
+	     is an inline frame.  When computing the frame-id for B we need
+	     to compute the frame-id for A.
+
+	     If the frame-id for A is a duplicate then it must be the case
+	     that B will also be a duplicate.
+
+	     If we spot A as being a duplicate here and so return NULL then
+	     B will fail to obtain a valid frame-id for A, and thus B will
+	     be unable to return a valid frame-id (in fact an assertion
+	     will trigger).
+
+	     What this means is that, if we are being asked to get the
+	     previous frame for an inline frame and we want to reject the
+	     new (previous) frame then we should really return the frame so
+	     that the inline frame can still compute its frame-id.  This is
+	     safe as we can be confident that the inline frame-id will also
+	     be a duplicate, and so the inline frame (and therefore all
+	     frames previous to it) will then be rejected.  */
+	  if (this_frame->unwind->type != INLINE_FRAME
+	      || this_frame->this_id.p != frame_id_status::COMPUTING)
+	    prev_frame = NULL;
+	}
+      else
+	{
+	  /* This assertion ties into the special handling of inline frames
+	     above.
+
+	     We know that to compute the frame-id of an inline frame we
+	     must first compute the frame-id of the inline frame's previous
+	     frame.
+
+	     If the previous frame is rejected as a duplicate then it
+	     should be the case that the inline frame is also rejected as a
+	     duplicate, and we should not reach this assertion.
+
+	     However, if we do reach this assertion then the inline frame
+	     has not been rejected, thus, it should be the case that the
+	     frame previous to the inline frame has also not be rejected,
+	     this is reflected by the requirement that the inline frame's
+	     previous pointer not be nullptr at this point.  */
+	  gdb_assert (this_frame->unwind->type != INLINE_FRAME
+		      || this_frame->prev != nullptr);
 	}
     }
   catch (const gdb_exception &ex)
diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.c b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.c
new file mode 100644
index 00000000000..704a994c4e6
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+static void foo (void);
+static void bar (void);
+
+volatile int global_var;
+volatile int level_counter;
+
+static void __attribute__((noinline))
+bar (void)
+{
+  /* Do some work.  */
+  ++global_var;
+
+  /* Now the inline function.  */
+  --level_counter;
+  foo ();
+  ++level_counter;
+
+  /* Do some work.  */
+  ++global_var;
+}
+
+static inline void __attribute__((__always_inline__))
+foo (void)
+{
+  if (level_counter > 1)
+    {
+      --level_counter;
+      bar ();
+      ++level_counter;
+    }
+  else
+    ++global_var;	/* Break here.  */
+}
+
+int
+main ()
+{
+  level_counter = 6;
+  bar ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
new file mode 100644
index 00000000000..49c35517801
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
@@ -0,0 +1,122 @@
+# Copyright (C) 2021 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 checks for an edge case when unwinding inline frames which
+# occur towards the older end of the stack when the stack ends with a
+# cycle.  Consider this well formed stack:
+#
+#   main -> normal_frame -> inline_frame
+#
+# Now consider that, for whatever reason, the stack unwinding of
+# "normal_frame" becomes corrupted, such that the stack appears to be
+# this:
+#
+#   .-> normal_frame -> inline_frame
+#   |      |
+#   '------'
+#
+# When confrontend with such a situation we would expect GDB to detect
+# the stack frame cycle and terminate the backtrace at the first
+# instance of "normal_frame" with a message:
+#
+#   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
+#
+# However, at one point there was a bug in GDB's inline frame
+# mechanism such that the fact that "inline_frame" was inlined into
+# "normal_frame" would cause GDB to trigger an assertion.
+#
+# This text makes use of a Python unwinder which can fake the cyclic
+# stack cycle, further the test sets up multiple levels of normal and
+# inline frames.  At the point of testing the stack looks like this:
+#
+#   main -> bar -> foo -> bar -> foo -> bar -> foo
+#
+# Where "bar" is a normal frame, and "foo" is an inline frame.
+#
+# The python unwinder is then used to force a stack cycle at each
+# "bar" frame in turn, we then check that GDB can successfully unwind
+# the stack.
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}]} {
+    return -1
+}
+
+# Skip this test if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to the breakpoint where we will carry out the test.
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# Load the script containing the unwinder, this must be done at the
+# testing point as the script will examine the stack as it is loaded.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Check the unbroken stack.
+gdb_test_sequence "bt"  "Backtrace when the unwind is left unbroken" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
+}
+
+# Arrange to introduce a stack cycle at frame 5.
+gdb_test_no_output "python stop_at_level=5"
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 5" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
+}
+
+# Arrange to introduce a stack cycle at frame 3.
+gdb_test_no_output "python stop_at_level=3"
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 3" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
+}
+
+# Arrange to introduce a stack cycle at frame 1.
+gdb_test_no_output "python stop_at_level=1"
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 1" {
+    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
+    "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py
new file mode 100644
index 00000000000..370f86cc610
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+# Set this to the stack level the backtrace should be corrupted at.
+# This will only work for frame 1, 3, or 5 in the test this unwinder
+# was written for.
+stop_at_level = None
+
+# Set this to the stack frame size of frames 1, 3, and 5.  These
+# frames wil all have the same stack frame size as they are the same
+# function called recursively.
+stack_adjust = None
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "stop at level")
+
+    def __call__(self, pending_frame):
+        global stop_at_level
+        global stack_adjust
+
+        if stop_at_level is None or pending_frame.level() != stop_at_level:
+            return None
+
+        if stack_adjust is None:
+            raise gdb.GdbError("invalid stack_adjust")
+
+        if not stop_at_level in [1, 3, 5]:
+            raise gdb.GdbError("invalid stop_at_level")
+
+        sp_desc = pending_frame.architecture().registers().find("sp")
+        sp = pending_frame.read_register(sp_desc) + stack_adjust
+        pc = (gdb.lookup_symbol("bar"))[0].value().address
+        unwinder = pending_frame.create_unwind_info(FrameId(sp, pc))
+
+        for reg in pending_frame.architecture().registers("general"):
+            val = pending_frame.read_register(reg)
+            unwinder.add_saved_register(reg, val)
+        return unwinder
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
+
+# When loaded, it is expected that the stack looks like:
+#
+#   main -> bar -> foo -> bar -> foo -> bar -> foo
+#
+# Compute the stack frame size of bar, which has foo inlined within
+# it.
+f0 = gdb.newest_frame()
+f1 = f0.older()
+f2 = f1.older()
+f0_sp = f0.read_register("sp")
+f2_sp = f2.read_register("sp")
+stack_adjust = f2_sp - f0_sp
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv2 2/2] gdb: remove VALUE_FRAME_ID
  2021-06-21 19:46 ` [PATCHv2 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
  2021-06-21 19:46   ` [PATCHv2 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
@ 2021-06-21 19:46   ` Andrew Burgess
  2021-06-29 17:53     ` Simon Marchi
  2021-07-20  9:10   ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
  2 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-06-21 19:46 UTC (permalink / raw)
  To: gdb-patches

While working on the previous commit I happened to switch on 'set
debug frame 1', and ran into a different assertion than the one I was
trying to fix.

The new problem I see is this assertion triggering:

  gdb/frame.c:622: internal-error: frame_id get_frame_id(frame_info*): Assertion `fi->this_id.p != frame_id_status::COMPUTING' failed.

We attempt to get the frame_id for a frame while we are computing the
frame_id for that same frame.

What happens is we have a stack like this:

  normal_frame -> inline_frame -> sentinel_frame

When we initially stop, GDB creates a frame for inline_frame but
doesn't sniff its type, or try to fill in its frame_id (see code near
the top of get_prev_frame_if_no_cycle).

Later on during the stop, this happens:

  process_event_stop_test
    get_stack_frame_id
      skip_artificial_frames
        get_frame_type

The call to get_frame_type causes inline_frame to sniff its type,
establishing that it is an INLINE_FRAME, but does not cause the frame
to build its frame_id.

The same skip_artificial_frames call then calls get_prev_frame_always
to unwind the stack, this will then try to get the frame previous to
inline_frame, i.e. normal_frame.

So, we create a new frame, but unlike frame #0, in
get_prev_frame_if_no_cycle, we immediately try to compute the frame_id
for this new frame #1.

Computing the frame_id for frame #1 invokes the sniffer.  If this
sniffer tries to read a register then we will create a lazy register
value by calling value_of_register_lazy.  As the next frame (frame #0)
is an INLINE_FRAME, we will skip this and instead create the lazy
register value using the frame_id for frame #-1 (the sentinel frame).

Now, when we try to resolve the lazy register value, in the value.c
function value_fetch_lazy_register, we want to print which frame the
lazy register value was for (in debug mode), so we call:

  frame = frame_find_by_id (VALUE_FRAME_ID (val));

where:

  #define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))

In our case we call get_prev_frame_id_by_id with the frame_id of the
sentinel_frame frame, we then call frame_find_by_id, followed by
get_prev_frame (which gives us frame #0), followed by get_frame_id.

Frame #0 has not yet computed its frame_id, so we start that process.
The first thing we need when computing the frame_id of an inline frame
is the frame_id of the previous frame, so that's what we ask for.
Unfortunately, that's where we started this journey, we are already
computing the frame_id for frame #1, and thus the assert fires.

Solving the assertion failure is pretty easy, if we consider the code
in value_fetch_lazy_register and get_prev_frame_id_by_id then what we
do is:

  1. Start with a frame_id taken from a value,
  2. Lookup the corresponding frame,
  3. Find the previous frame,
  4. Get the frame_id for that frame, and
  5. Lookup the corresponding frame
  6. Print the frame's level

Notice that steps 3 and 5 give us the exact same result, step 4 is
just wasted effort.  We could shorten this process such that we drop
steps 4 and 5, thus:

  1. Start with a frame_id taken from a value,
  2. Lookup the corresponding frame,
  3. Find the previous frame,
  6. Print the frame's level

This will give the exact same frame as a result, and this is what I
have done in this patch by removing the use of VALUE_FRAME_ID from
value_fetch_lazy_register.

Out of curiosity I looked to see how widely VALUE_FRAME_ID was used,
and saw it was only used in one other place in valops.c:value_assign,
where, once again, we take the result of VALUE_FRAME_ID and pass it to
frame_find_by_id, thus introducing a redundant frame_id lookup.

I don't think the value_assign case risks triggering the assertion
though, as we are unlikely to call value_assign while computing the
frame_id for a frame, however, we could make value_assign slightly
more efficient, with no real additional complexity, by removing the
use of VALUE_FRAME_ID.

So, in this commit, I completely remove VALUE_FRAME_ID, and replace it
with a use of VALUE_NEXT_FRAME_ID, followed by a direct call to
get_prev_frame_always, this should make no difference in either case,
and resolves the assertion issue from value.c.

One thing that is worth noticing here is that in these two situations
we don't end up getting the frame we expected to, though this is not a
result of my change, we were not getting the expected result before
either.

Consider the debug printing case, the stack is:

  normal_frame -> inline_frame -> sentinel_frame

We read a register from normal_frame (frame #1), the value of which is
fetched from sentinel_frame (frame #-1).  The debug print is trying to
say:

  frame=1,regnum=....

However, as the lazy register value points at frame #-1, we will
actually (incorrectly) print:

  frame=0,regnum=....

Like I said, this bug existed before this commit, and so, if we didn't
assert (i.e. the lazy register read occurred in some context other
than during the frame sniffer), then the debug print would previous
have, and will continue to, print the wrong frame level.  Thankfully,
this is only in debug output, so not something a normal user should
see.

In theory, the same bug exists in the value_assign code, if we are in
normal_frame and try to perform a register assignment, then GDB will
get confused and think we are assigning in the context of
inline_frame.  However, having looked at the code I think we get away
with this as the frame is used for two things that I can see:

  1. Getting the gdbarch for the frame, I can't imagine a situation
  where inline_frame has a different gdbarch to normal_frame, and

  2. Unwinding register values from frame->next.  We should be asking
  to unwind the register values from inline_frame, but really we end
  up asking to unwind from sentinel_frame.  However, if we did ask to
  unwind the values from inline_frame this would just forward the
  request on to the next frame, i.e. sentinel_frame, so we would get
  the exact same result.

In short, though we do use the wrong frame in value_assign, I think
this is harmless.

Fixing this debug printing would require GDB to require extra
information in its value location to indicate how many frames had been
skipped.  For example, with the stack:

  normal_frame -> inline_frame -> sentinel_frame

A lazy register value read in frame normal_frame would have a location
frame_id for sentinel_frame, and a skip value of 2 indicating the
value was read for a frame 2 previous.  In contrast, for a more
standard case, with a stack like this:

  normal_frame -> sentinel_frame

A lazy register value read in frame normal_frame would have a location
frame_id for sentinel_frame and a skip value of 1 indicating the value
was read for a frame 1 previous.

However, adding this seems like a lot of work to fix a single like of
debug print, but might be something we want to consider in the future.

gdb/ChangeLog:

	* frame.c (get_prev_frame_id_by_id): Delete.
	* frame.h (get_prev_frame_id_by_id): Delete declaration.
	* valops.c (value_assign): Use VALUE_NEXT_FRAME_ID and
	get_prev_frame_always, not VALUE_FRAME_ID.
	* value.c (value_fetch_lazy_register): Likewise.
	* value.h (VALUE_FRAME_ID): Delete.

gdb/testsuite/ChangeLog:

	* gdb.base/inline-frame-bad-unwind.exp: Add an extra test.
---
 gdb/ChangeLog                                   |  9 +++++++++
 gdb/frame.c                                     | 16 ----------------
 gdb/frame.h                                     |  4 ----
 gdb/testsuite/ChangeLog                         |  4 ++++
 .../gdb.base/inline-frame-bad-unwind.exp        | 17 +++++++++++++++++
 gdb/valops.c                                    | 17 +++++++++--------
 gdb/value.c                                     |  5 ++---
 gdb/value.h                                     |  6 ------
 8 files changed, 41 insertions(+), 37 deletions(-)

diff --git a/gdb/frame.c b/gdb/frame.c
index b0943c02115..e29a132dc3e 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2631,22 +2631,6 @@ get_prev_frame (struct frame_info *this_frame)
   return get_prev_frame_always (this_frame);
 }
 
-struct frame_id
-get_prev_frame_id_by_id (struct frame_id id)
-{
-  struct frame_id prev_id;
-  struct frame_info *frame;
-
-  frame = frame_find_by_id (id);
-
-  if (frame != NULL)
-    prev_id = get_frame_id (get_prev_frame (frame));
-  else
-    prev_id = null_frame_id;
-
-  return prev_id;
-}
-
 CORE_ADDR
 get_frame_pc (struct frame_info *frame)
 {
diff --git a/gdb/frame.h b/gdb/frame.h
index da52522ad2a..bc46149697e 100644
--- a/gdb/frame.h
+++ b/gdb/frame.h
@@ -383,10 +383,6 @@ extern struct frame_info *get_prev_frame_always (struct frame_info *);
    is not found.  */
 extern struct frame_info *frame_find_by_id (struct frame_id id);
 
-/* Given a frame's ID, find the previous frame's ID.  Returns null_frame_id
-   if the frame is not found.  */
-extern struct frame_id get_prev_frame_id_by_id (struct frame_id id);
-
 /* Base attributes of a frame: */
 
 /* The frame's `resume' address.  Where the program will resume in
diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
index 49c35517801..a0aebf94e41 100644
--- a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
+++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
@@ -120,3 +120,20 @@ gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 1" {
     "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
     "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
 }
+
+# Flush the register cache (which also flushes the frame cache) so we
+# get a full backtrace again, then switch on frame debugging and try
+# to back trace.  At one point this triggered an assertion.
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_no_output "set debug frame 1"
+gdb_test_multiple "bt" "backtrace with debugging on" {
+    -re "^$gdb_prompt $" {
+	pass $gdb_test_name
+    }
+    -re "\[^\r\n\]+\r\n" {
+	exp_continue
+    }
+}
+gdb_test "p 1 + 2 + 3" " = 6" \
+    "ensure GDB is still alive"
diff --git a/gdb/valops.c b/gdb/valops.c
index 8694c124b52..91832cc6b04 100644
--- a/gdb/valops.c
+++ b/gdb/valops.c
@@ -1197,14 +1197,15 @@ value_assign (struct value *toval, struct value *fromval)
 	struct gdbarch *gdbarch;
 	int value_reg;
 
-	/* Figure out which frame this is in currently.
-	
-	   We use VALUE_FRAME_ID for obtaining the value's frame id instead of
-	   VALUE_NEXT_FRAME_ID due to requiring a frame which may be passed to
-	   put_frame_register_bytes() below.  That function will (eventually)
-	   perform the necessary unwind operation by first obtaining the next
-	   frame.  */
-	frame = frame_find_by_id (VALUE_FRAME_ID (toval));
+	/* Figure out which frame this register value is in.  The value
+	   holds the frame_id for the next frame, that is the frame this
+	   register value was unwound from.
+
+	   Below we will call put_frame_register_bytes which requires that
+	   we pass it the actual frame in which the register value is
+	   valid, i.e. not the next frame.  */
+	frame = frame_find_by_id (VALUE_NEXT_FRAME_ID (toval));
+	frame = get_prev_frame_always (frame);
 
 	value_reg = VALUE_REGNUM (toval);
 
diff --git a/gdb/value.c b/gdb/value.c
index 9df035a50b3..034cfa9f11c 100644
--- a/gdb/value.c
+++ b/gdb/value.c
@@ -3950,9 +3950,8 @@ value_fetch_lazy_register (struct value *val)
     {
       struct gdbarch *gdbarch;
       struct frame_info *frame;
-      /* VALUE_FRAME_ID is used here, instead of VALUE_NEXT_FRAME_ID,
-	 so that the frame level will be shown correctly.  */
-      frame = frame_find_by_id (VALUE_FRAME_ID (val));
+      frame = frame_find_by_id (VALUE_NEXT_FRAME_ID (val));
+      frame = get_prev_frame_always (frame);
       regnum = VALUE_REGNUM (val);
       gdbarch = get_frame_arch (frame);
 
diff --git a/gdb/value.h b/gdb/value.h
index a691f3cf3ff..231387784a8 100644
--- a/gdb/value.h
+++ b/gdb/value.h
@@ -458,12 +458,6 @@ extern struct internalvar **deprecated_value_internalvar_hack (struct value *);
 extern struct frame_id *deprecated_value_next_frame_id_hack (struct value *);
 #define VALUE_NEXT_FRAME_ID(val) (*deprecated_value_next_frame_id_hack (val))
 
-/* Frame ID of frame to which a register value is relative.  This is
-   similar to VALUE_NEXT_FRAME_ID, above, but may not be assigned to. 
-   Note that VALUE_FRAME_ID effectively undoes the "next" operation
-   that was performed during the assignment to VALUE_NEXT_FRAME_ID.  */
-#define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))
-
 /* Register number if the value is from a register.  */
 extern int *deprecated_value_regnum_hack (struct value *);
 #define VALUE_REGNUM(val) (*deprecated_value_regnum_hack (val))
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv2 2/2] gdb: remove VALUE_FRAME_ID
  2021-06-21 19:46   ` [PATCHv2 2/2] gdb: remove VALUE_FRAME_ID Andrew Burgess
@ 2021-06-29 17:53     ` Simon Marchi
  2021-06-30 15:18       ` Andrew Burgess
  0 siblings, 1 reply; 48+ messages in thread
From: Simon Marchi @ 2021-06-29 17:53 UTC (permalink / raw)
  To: Andrew Burgess, gdb-patches



On 2021-06-21 3:46 p.m., Andrew Burgess wrote:
> While working on the previous commit I happened to switch on 'set
> debug frame 1', and ran into a different assertion than the one I was
> trying to fix.
> 
> The new problem I see is this assertion triggering:
> 
>   gdb/frame.c:622: internal-error: frame_id get_frame_id(frame_info*): Assertion `fi->this_id.p != frame_id_status::COMPUTING' failed.
> 
> We attempt to get the frame_id for a frame while we are computing the
> frame_id for that same frame.
> 
> What happens is we have a stack like this:
> 
>   normal_frame -> inline_frame -> sentinel_frame
> 
> When we initially stop, GDB creates a frame for inline_frame but
> doesn't sniff its type, or try to fill in its frame_id (see code near
> the top of get_prev_frame_if_no_cycle).
> 
> Later on during the stop, this happens:
> 
>   process_event_stop_test
>     get_stack_frame_id
>       skip_artificial_frames
>         get_frame_type
> 
> The call to get_frame_type causes inline_frame to sniff its type,
> establishing that it is an INLINE_FRAME, but does not cause the frame
> to build its frame_id.
> 
> The same skip_artificial_frames call then calls get_prev_frame_always
> to unwind the stack, this will then try to get the frame previous to
> inline_frame, i.e. normal_frame.
> 
> So, we create a new frame, but unlike frame #0, in
> get_prev_frame_if_no_cycle, we immediately try to compute the frame_id
> for this new frame #1.
> 
> Computing the frame_id for frame #1 invokes the sniffer.  If this
> sniffer tries to read a register then we will create a lazy register
> value by calling value_of_register_lazy.  As the next frame (frame #0)
> is an INLINE_FRAME, we will skip this and instead create the lazy
> register value using the frame_id for frame #-1 (the sentinel frame).
> 
> Now, when we try to resolve the lazy register value, in the value.c
> function value_fetch_lazy_register, we want to print which frame the
> lazy register value was for (in debug mode), so we call:
> 
>   frame = frame_find_by_id (VALUE_FRAME_ID (val));
> 
> where:
> 
>   #define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))
> 
> In our case we call get_prev_frame_id_by_id with the frame_id of the
> sentinel_frame frame, we then call frame_find_by_id, followed by
> get_prev_frame (which gives us frame #0), followed by get_frame_id.
> 
> Frame #0 has not yet computed its frame_id, so we start that process.
> The first thing we need when computing the frame_id of an inline frame
> is the frame_id of the previous frame, so that's what we ask for.
> Unfortunately, that's where we started this journey, we are already
> computing the frame_id for frame #1, and thus the assert fires.
> 
> Solving the assertion failure is pretty easy, if we consider the code
> in value_fetch_lazy_register and get_prev_frame_id_by_id then what we
> do is:
> 
>   1. Start with a frame_id taken from a value,
>   2. Lookup the corresponding frame,
>   3. Find the previous frame,
>   4. Get the frame_id for that frame, and
>   5. Lookup the corresponding frame
>   6. Print the frame's level
> 
> Notice that steps 3 and 5 give us the exact same result, step 4 is
> just wasted effort.  We could shorten this process such that we drop
> steps 4 and 5, thus:
> 
>   1. Start with a frame_id taken from a value,
>   2. Lookup the corresponding frame,
>   3. Find the previous frame,
>   6. Print the frame's level
> 
> This will give the exact same frame as a result, and this is what I
> have done in this patch by removing the use of VALUE_FRAME_ID from
> value_fetch_lazy_register.
> 
> Out of curiosity I looked to see how widely VALUE_FRAME_ID was used,
> and saw it was only used in one other place in valops.c:value_assign,
> where, once again, we take the result of VALUE_FRAME_ID and pass it to
> frame_find_by_id, thus introducing a redundant frame_id lookup.
> 
> I don't think the value_assign case risks triggering the assertion
> though, as we are unlikely to call value_assign while computing the
> frame_id for a frame, however, we could make value_assign slightly
> more efficient, with no real additional complexity, by removing the
> use of VALUE_FRAME_ID.
> 
> So, in this commit, I completely remove VALUE_FRAME_ID, and replace it
> with a use of VALUE_NEXT_FRAME_ID, followed by a direct call to
> get_prev_frame_always, this should make no difference in either case,
> and resolves the assertion issue from value.c.
> 
> One thing that is worth noticing here is that in these two situations
> we don't end up getting the frame we expected to, though this is not a
> result of my change, we were not getting the expected result before
> either.
> 
> Consider the debug printing case, the stack is:
> 
>   normal_frame -> inline_frame -> sentinel_frame
> 
> We read a register from normal_frame (frame #1), the value of which is
> fetched from sentinel_frame (frame #-1).  The debug print is trying to
> say:
> 
>   frame=1,regnum=....
> 
> However, as the lazy register value points at frame #-1, we will
> actually (incorrectly) print:
> 
>   frame=0,regnum=....
> 
> Like I said, this bug existed before this commit, and so, if we didn't
> assert (i.e. the lazy register read occurred in some context other
> than during the frame sniffer), then the debug print would previous
> have, and will continue to, print the wrong frame level.  Thankfully,
> this is only in debug output, so not something a normal user should
> see.

Are you sure this is not expected?  An inline frame does not have
registers of its own, so it could be normal/expected, that to unwind
registers, we just skip over inline frames, so that a register value's
next frame id skips over inline frame.

See this commit (that you wrote :)):

  https://gitlab.com/gnutools/gdb/-/commit/9fc501fdfe5dc82b5e5388cde4ac2ab70ed69d75

I might not understand correctly what you mean though.

The patch itself LGTM.

Simon

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv2 2/2] gdb: remove VALUE_FRAME_ID
  2021-06-29 17:53     ` Simon Marchi
@ 2021-06-30 15:18       ` Andrew Burgess
  2021-07-05 14:22         ` Simon Marchi
  0 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-06-30 15:18 UTC (permalink / raw)
  To: Simon Marchi; +Cc: gdb-patches

* Simon Marchi <simon.marchi@polymtl.ca> [2021-06-29 13:53:20 -0400]:

> 
> 
> On 2021-06-21 3:46 p.m., Andrew Burgess wrote:
> > While working on the previous commit I happened to switch on 'set
> > debug frame 1', and ran into a different assertion than the one I was
> > trying to fix.
> > 
> > The new problem I see is this assertion triggering:
> > 
> >   gdb/frame.c:622: internal-error: frame_id get_frame_id(frame_info*): Assertion `fi->this_id.p != frame_id_status::COMPUTING' failed.
> > 
> > We attempt to get the frame_id for a frame while we are computing the
> > frame_id for that same frame.
> > 
> > What happens is we have a stack like this:
> > 
> >   normal_frame -> inline_frame -> sentinel_frame
> > 
> > When we initially stop, GDB creates a frame for inline_frame but
> > doesn't sniff its type, or try to fill in its frame_id (see code near
> > the top of get_prev_frame_if_no_cycle).
> > 
> > Later on during the stop, this happens:
> > 
> >   process_event_stop_test
> >     get_stack_frame_id
> >       skip_artificial_frames
> >         get_frame_type
> > 
> > The call to get_frame_type causes inline_frame to sniff its type,
> > establishing that it is an INLINE_FRAME, but does not cause the frame
> > to build its frame_id.
> > 
> > The same skip_artificial_frames call then calls get_prev_frame_always
> > to unwind the stack, this will then try to get the frame previous to
> > inline_frame, i.e. normal_frame.
> > 
> > So, we create a new frame, but unlike frame #0, in
> > get_prev_frame_if_no_cycle, we immediately try to compute the frame_id
> > for this new frame #1.
> > 
> > Computing the frame_id for frame #1 invokes the sniffer.  If this
> > sniffer tries to read a register then we will create a lazy register
> > value by calling value_of_register_lazy.  As the next frame (frame #0)
> > is an INLINE_FRAME, we will skip this and instead create the lazy
> > register value using the frame_id for frame #-1 (the sentinel frame).
> > 
> > Now, when we try to resolve the lazy register value, in the value.c
> > function value_fetch_lazy_register, we want to print which frame the
> > lazy register value was for (in debug mode), so we call:
> > 
> >   frame = frame_find_by_id (VALUE_FRAME_ID (val));
> > 
> > where:
> > 
> >   #define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))
> > 
> > In our case we call get_prev_frame_id_by_id with the frame_id of the
> > sentinel_frame frame, we then call frame_find_by_id, followed by
> > get_prev_frame (which gives us frame #0), followed by get_frame_id.
> > 
> > Frame #0 has not yet computed its frame_id, so we start that process.
> > The first thing we need when computing the frame_id of an inline frame
> > is the frame_id of the previous frame, so that's what we ask for.
> > Unfortunately, that's where we started this journey, we are already
> > computing the frame_id for frame #1, and thus the assert fires.
> > 
> > Solving the assertion failure is pretty easy, if we consider the code
> > in value_fetch_lazy_register and get_prev_frame_id_by_id then what we
> > do is:
> > 
> >   1. Start with a frame_id taken from a value,
> >   2. Lookup the corresponding frame,
> >   3. Find the previous frame,
> >   4. Get the frame_id for that frame, and
> >   5. Lookup the corresponding frame
> >   6. Print the frame's level
> > 
> > Notice that steps 3 and 5 give us the exact same result, step 4 is
> > just wasted effort.  We could shorten this process such that we drop
> > steps 4 and 5, thus:
> > 
> >   1. Start with a frame_id taken from a value,
> >   2. Lookup the corresponding frame,
> >   3. Find the previous frame,
> >   6. Print the frame's level
> > 
> > This will give the exact same frame as a result, and this is what I
> > have done in this patch by removing the use of VALUE_FRAME_ID from
> > value_fetch_lazy_register.
> > 
> > Out of curiosity I looked to see how widely VALUE_FRAME_ID was used,
> > and saw it was only used in one other place in valops.c:value_assign,
> > where, once again, we take the result of VALUE_FRAME_ID and pass it to
> > frame_find_by_id, thus introducing a redundant frame_id lookup.
> > 
> > I don't think the value_assign case risks triggering the assertion
> > though, as we are unlikely to call value_assign while computing the
> > frame_id for a frame, however, we could make value_assign slightly
> > more efficient, with no real additional complexity, by removing the
> > use of VALUE_FRAME_ID.
> > 
> > So, in this commit, I completely remove VALUE_FRAME_ID, and replace it
> > with a use of VALUE_NEXT_FRAME_ID, followed by a direct call to
> > get_prev_frame_always, this should make no difference in either case,
> > and resolves the assertion issue from value.c.
> > 
> > One thing that is worth noticing here is that in these two situations
> > we don't end up getting the frame we expected to, though this is not a
> > result of my change, we were not getting the expected result before
> > either.
> > 
> > Consider the debug printing case, the stack is:
> > 
> >   normal_frame -> inline_frame -> sentinel_frame
> > 
> > We read a register from normal_frame (frame #1), the value of which is
> > fetched from sentinel_frame (frame #-1).  The debug print is trying to
> > say:
> > 
> >   frame=1,regnum=....
> > 
> > However, as the lazy register value points at frame #-1, we will
> > actually (incorrectly) print:
> > 
> >   frame=0,regnum=....
> > 
> > Like I said, this bug existed before this commit, and so, if we didn't
> > assert (i.e. the lazy register read occurred in some context other
> > than during the frame sniffer), then the debug print would previous
> > have, and will continue to, print the wrong frame level.  Thankfully,
> > this is only in debug output, so not something a normal user should
> > see.
> 
> Are you sure this is not expected?  An inline frame does not have
> registers of its own, so it could be normal/expected, that to unwind
> registers, we just skip over inline frames, so that a register value's
> next frame id skips over inline frame.
> 
> See this commit (that you wrote :)):
> 
>   https://gitlab.com/gnutools/gdb/-/commit/9fc501fdfe5dc82b5e5388cde4ac2ab70ed69d75
> 
> I might not understand correctly what you mean though.

It was unexpected to me :)

The frame number being reported here is the frame that wants the
register, not the frame that provides the register, so given a stack
like this (now with frame numbers):

  #1:normal_frame -> #0:inline_frame -> #-1:sentinel_frame

And GDB says:

  { value_fetch_lazy (frame=1,regnum=.......

GDB is trying to say that frame #1 asked to read a register, and the
value returned as .... whatever ....

BUT, what we _actually_ end up saying is:

  { value_fetch_lazy (frame=0,regnum=.......

Which just isn't correct, frame 0 didn't cause the lazy register value
to be created, frame 1 did.  A developer reading these logs needs to
understand that frame 0 is inline, and that the value in frame 1 is
the same as the value in frame 0.

You are correct that the patch you linked is indeed what introduced
this mess in the first place, but I think the reasoning behind that
original patch is still good.

If this makes more sense then I will update the commit message to
hopefully make things clearer - what do you think?

Thanks,
Andrew

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv2 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-06-21 19:46   ` [PATCHv2 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
@ 2021-07-05 11:39     ` Pedro Alves
  2021-07-05 14:14       ` Simon Marchi
  0 siblings, 1 reply; 48+ messages in thread
From: Pedro Alves @ 2021-07-05 11:39 UTC (permalink / raw)
  To: Andrew Burgess, gdb-patches

Hi Andrew,

I've read this, and I think you came up with a reasonable solution.

Some minor-ish comments below.

On 2021-06-21 8:46 p.m., Andrew Burgess wrote:

> +static void foo (void);
> +static void bar (void);
> +
> +volatile int global_var;
> +volatile int level_counter;
> +
> +static void __attribute__((noinline))
> +bar (void)
> +{
> +  /* Do some work.  */
> +  ++global_var;
> +
> +  /* Now the inline function.  */
> +  --level_counter;
> +  foo ();
> +  ++level_counter;
> +
> +  /* Do some work.  */
> +  ++global_var;
> +}
> +
> +static inline void __attribute__((__always_inline__))
> +foo (void)
> +{
> +  if (level_counter > 1)
> +    {
> +      --level_counter;
> +      bar ();
> +      ++level_counter;
> +    }
> +  else
> +    ++global_var;	/* Break here.  */
> +}

I'd suggest renaming these "foo" "bar" functions to "normal_func" "inline_func"
or "norm" "inln" or something like that.  I think it'll make the backtraces in
the .exp code more obvious.

> +
> +int
> +main ()
> +{
> +  level_counter = 6;
> +  bar ();
> +  return 0;
> +}
> diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp
> new file mode 100644
> index 00000000000..49c35517801
> --- /dev/null
> +++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.exp

I'd suggest naming this something with "cycle" instead of "bad" as being more
to the point.  There can be many forms of badness.

> +
> +# This test checks for an edge case when unwinding inline frames which
> +# occur towards the older end of the stack when the stack ends with a
> +# cycle.  Consider this well formed stack:
> +#
> +#   main -> normal_frame -> inline_frame
> +#
> +# Now consider that, for whatever reason, the stack unwinding of
> +# "normal_frame" becomes corrupted, such that the stack appears to be
> +# this:
> +#
> +#   .-> normal_frame -> inline_frame
> +#   |      |
> +#   '------'
> +#
> +# When confrontend with such a situation we would expect GDB to detect

Typo: confrontend -> confronted

> +
> +# Check the unbroken stack.
> +gdb_test_sequence "bt"  "Backtrace when the unwind is left unbroken" {
> +    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
> +    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
> +    "\\r\\n#4 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#5 \[^\r\n\]* bar \\(\\) at "
> +    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
> +}
> +
> +# Arrange to introduce a stack cycle at frame 5.
> +gdb_test_no_output "python stop_at_level=5"
> +gdb_test "maint flush register-cache" \
> +    "Register cache flushed\\." ""

How about using with_test_prefix instead of suppressing the test message?
Like:

with_test_prefix "broken at frame 5 {
  # Arrange to introduce a stack cycle at frame 5.
  gdb_test_no_output "python stop_at_level=5"
  gdb_test "maint flush register-cache" "Register cache flushed\\."
  gdb_test_sequence "bt" "" {
    ...
  }
}

> +gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 5" {

Spurious double space, and lowercase "Backtrace".

> +    "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
> +    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
> +    "\\r\\n#4 \[^\r\n\]* foo \\(\\) at "
> +    "\\r\\n#5 \[^\r\n\]* bar \\(\\) at "
> +    "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"

Actually, I don't think gdb_test_sequence is the right proc for these tests, because it
consumes lines between each pattern.  I mean, above, the test will pass if GDB prints
frame #6 too, before printing the "Backtrace stopped" line.  E.g., with this change,
the test will still pass:

@@ -92,10 +90,6 @@ gdb_test "maint flush register-cache" \
 gdb_test_sequence "bt"  "Backtrace when the unwind is broken at frame 5" {
     "\\r\\n#0 \[^\r\n\]* foo \\(\\) at "
     "\\r\\n#1 \[^\r\n\]* bar \\(\\) at "
-    "\\r\\n#2 \[^\r\n\]* foo \\(\\) at "
-    "\\r\\n#3 \[^\r\n\]* bar \\(\\) at "
-    "\\r\\n#4 \[^\r\n\]* foo \\(\\) at "
-    "\\r\\n#5 \[^\r\n\]* bar \\(\\) at "
     "\\r\\nBacktrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"
 }

> diff --git a/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py
> new file mode 100644
> index 00000000000..370f86cc610
> --- /dev/null
> +++ b/gdb/testsuite/gdb.base/inline-frame-bad-unwind.py
> @@ -0,0 +1,85 @@
> +# Copyright (C) 2021 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/>.
> +
> +import gdb
> +from gdb.unwinder import Unwinder
> +
> +# Set this to the stack level the backtrace should be corrupted at.
> +# This will only work for frame 1, 3, or 5 in the test this unwinder
> +# was written for.
> +stop_at_level = None
> +
> +# Set this to the stack frame size of frames 1, 3, and 5.  These
> +# frames wil all have the same stack frame size as they are the same

Typo "wil" -> "will".


^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv2 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-07-05 11:39     ` Pedro Alves
@ 2021-07-05 14:14       ` Simon Marchi
  0 siblings, 0 replies; 48+ messages in thread
From: Simon Marchi @ 2021-07-05 14:14 UTC (permalink / raw)
  To: Pedro Alves, Andrew Burgess, gdb-patches

On 2021-07-05 7:39 a.m., Pedro Alves wrote:
> Hi Andrew,
> 
> I've read this, and I think you came up with a reasonable solution.

I discussed this with Andrew on IRC, and my bad, I didn't post the
summary of our discussions here.

I suggested that this was confusing because get_prev_frame_if_no_cycle
now becomes get_prev_frame_if_no_cycle_except_in_some_cases, and it
makes get_prev_frame_if_no_cycle alter its behavior based on what it
thinks its caller is doing.  And it causes the inline frame's this_id
method to "lie" if it can't successfully compute the frame id, instead
of properly reporting failure.  The problem, in my opinion, is that:

 - inline_frame_this_id is currently not allowed to fail (return
   null_frame_id)
 - inline_frame_this_id uses get_prev_frame_if_no_cycle, which is
   allowed to fail (return nullptr)

So the question, in my opinion, is: how to propagate the
get_prev_frame_if_no_cycle failure up the stack.  I suggested that the
this_id method could throw a "FRAME_CYCLE_ERROR" in that case, caught
by the get_prev_frame_if_no_cycle call up the stack.  It would then
set the last physical frame's stop reason to UNWIND_SAME_ID.

I made a prototype here, which passes Andrew's test:

  https://review.lttng.org/c/binutils-gdb/+/6122

I prefer this option because it avoids adding special cases.  If the id
can't be computed, this_id throws and that's it, the caller decides what
to do with it.  But if people prefer the original solution, I don't
mind :).

Simon

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv2 2/2] gdb: remove VALUE_FRAME_ID
  2021-06-30 15:18       ` Andrew Burgess
@ 2021-07-05 14:22         ` Simon Marchi
  0 siblings, 0 replies; 48+ messages in thread
From: Simon Marchi @ 2021-07-05 14:22 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

> It was unexpected to me :)
> 
> The frame number being reported here is the frame that wants the
> register, not the frame that provides the register, so given a stack
> like this (now with frame numbers):
> 
>   #1:normal_frame -> #0:inline_frame -> #-1:sentinel_frame
> 
> And GDB says:
> 
>   { value_fetch_lazy (frame=1,regnum=.......
> 
> GDB is trying to say that frame #1 asked to read a register, and the
> value returned as .... whatever ....
> 
> BUT, what we _actually_ end up saying is:
> 
>   { value_fetch_lazy (frame=0,regnum=.......
> 
> Which just isn't correct, frame 0 didn't cause the lazy register value
> to be created, frame 1 did.  A developer reading these logs needs to
> understand that frame 0 is inline, and that the value in frame 1 is
> the same as the value in frame 0.
> 
> You are correct that the patch you linked is indeed what introduced
> this mess in the first place, but I think the reasoning behind that
> original patch is still good.
> 
> If this makes more sense then I will update the commit message to
> hopefully make things clearer - what do you think?

Ok, I (think) I see what you mean.  I think we could adjust the debug
print to print the next frame's (which supplies the values) level
instead (and make it clear that it's the next frame's level that we
print).

Simon

^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames
  2021-06-21 19:46 ` [PATCHv2 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
  2021-06-21 19:46   ` [PATCHv2 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
  2021-06-21 19:46   ` [PATCHv2 2/2] gdb: remove VALUE_FRAME_ID Andrew Burgess
@ 2021-07-20  9:10   ` Andrew Burgess
  2021-07-20  9:10     ` [PATCHv3 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
                       ` (3 more replies)
  2 siblings, 4 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-07-20  9:10 UTC (permalink / raw)
  To: gdb-patches

Thanks for the feedback on v2.

In v3 I have:

 - Addressed all of Pedro's feedback on the test in patch #1.
 
 - Rewritten how the problem in patch #1 is fixed based on Simon's
   exception based approach.

Thanks,
Andrew

---

Andrew Burgess (2):
  gdb: prevent an assertion when computing the frame_id for an inline
    frame
  gdb: remove VALUE_FRAME_ID

 gdb/frame.c                                   |  57 ++++---
 gdb/frame.h                                   |   4 -
 gdb/inline-frame.c                            |  31 +++-
 .../gdb.base/inline-frame-cycle-unwind.c      |  58 +++++++
 .../gdb.base/inline-frame-cycle-unwind.exp    | 145 ++++++++++++++++++
 .../gdb.base/inline-frame-cycle-unwind.py     |  85 ++++++++++
 gdb/valops.c                                  |  17 +-
 gdb/value.c                                   |   5 +-
 gdb/value.h                                   |   6 -
 gdbsupport/common-exceptions.h                |   5 +
 10 files changed, 373 insertions(+), 40 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py

-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv3 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-07-20  9:10   ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
@ 2021-07-20  9:10     ` Andrew Burgess
  2021-07-20  9:10     ` [PATCHv3 2/2] gdb: remove VALUE_FRAME_ID Andrew Burgess
                       ` (2 subsequent siblings)
  3 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-07-20  9:10 UTC (permalink / raw)
  To: gdb-patches

I ran into this assertion while GDB was trying to unwind the stack:

  gdb/inline-frame.c:173: internal-error: void inline_frame_this_id(frame_info*, void**, frame_id*): Assertion `frame_id_p (*this_id)' failed.

That is, when building the frame_id for an inline frame, GDB asks for
the frame_id of the previous frame.  Unfortunately, no valid frame_id
was returned for the previous frame, and so the assertion triggers.

What is happening is this, I had a stack that looked something like
this (the arrows '->' point from caller to callee):

  normal_frame -> inline_frame

However, for whatever reason (e.g. broken debug information, or
corrupted stack contents in the inferior), when GDB tries to unwind
"normal_frame", it ends up getting back effectively the same frame,
thus the call stack looks like this to GDB:

  .-> normal_frame -> inline_frame
  |     |
  '-----'

Given such a situation we would expect GDB to terminate the stack with
an error like this:

  Backtrace stopped: previous frame identical to this frame (corrupt stack?)

However, the inline_frame causes a problem, and here's why:

When unwinding we start from the sentinel frame and call
get_prev_frame.  We eventually end up in get_prev_frame_if_no_cycle,
in here we create a raw frame, and as this is frame #0 we immediately
return.

However, eventually we will try to unwind the stack further.  When we
do this we inevitably needing to know the frame_id for frame #0, and
so, eventually, we end up in compute_frame_id.

In compute_frame_id we first find the right unwinder for this frame,
in our case (i.e. for inline_frame) the $pc is within the function
normal_frame, but also within a block associated with the inlined
function inline_frame, as such the inline frame unwinder claims this
frame.

Back in compute_frame_id we next compute the frame_id, for our
inline_frame this means a call to inline_frame_this_id.

The ID of an inline frame is based on the id of the previous frame, so
from inline_frame_this_id we call get_prev_frame_always, this
eventually calls get_prev_frame_if_no_cycle again, which creates
another raw frame and calls compute_frame_id (for frames other than
frame 0 we immediately compute the frame_id).

In compute_frame_id we again identify the correct unwinder for this
frame.  Our $pc is unchanged, however, the fact that the next frame is
of type INLINE_FRAME prevents the inline frame unwinder from claiming
this frame again, and so, the standard DWARF frame unwinder claims
normal_frame.

We return to compute_frame_id and call the standard DWARF function to
build the frame_id for normal_frame.

With the frame_id of normal_frame figured out we return to
compute_frame_id, and then to get_prev_frame_if_no_cycle, where we add
the ID for normal_frame into the frame_id cache, and return the frame
back to inline_frame_this_id.

From inline_frame_this_id we build a frame_id for inline_frame and
return to compute_frame_id, and then to get_prev_frame_if_no_cycle,
which adds the frame_id for inline_frame into the frame_id cache.

So far, so good.

However, as we are trying to unwind the complete stack, we eventually
ask for the previous frame of normal_frame, remember, at this point
GDB doesn't know the stack is corrupted (with a cycle), GDB still
needs to figure that out.

So, we eventually end up in get_prev_frame_if_no_cycle where we create
a raw frame and call compute_frame_id, remember, this is for the frame
before normal_frame.

The first task for compute_frame_id is to find the unwinder for this
frame, so all of the frame sniffers are tried in order, this includes
the inline frame sniffer.

The inline frame sniffer asks for the $pc, this request is sent up the
stack to normal_frame, which, due to its cyclic behaviour, tells GDB
that the $pc in the previous frame was the same as the $pc in
normal_frame.

GDB spots that this $pc corresponds to both the function normal_frame
and also the inline function inline_frame.  As the next frame is not
an INLINE_FRAME then GDB figures that we have not yet built a frame to
cover inline_frame, and so the inline sniffer claims this new frame.
Our stack is now looking like this:

  inline_frame -> normal_frame -> inline_frame

But, we have not yet computed the frame id for the outer most (on the
left) inline_frame.  After the frame sniffer has claimed the inline
frame GDB returns to compute_frame_id and calls inline_frame_this_id.

In here GDB calls get_prev_frame_always, which eventually ends up
in get_prev_frame_if_no_cycle again, where we create a raw frame and
call compute_frame_id.

Just like before, compute_frame_id tries to find an unwinder for this
new frame, it sees that the $pc is within both normal_frame and
inline_frame, but the next frame is, again, an INLINE_FRAME, so, just
like before the standard DWARF unwinder claims this frame.  Back in
compute_frame_id we again call the standard DWARF function to build
the frame_id for this new copy of normal_frame.

At this point the stack looks like this:

  normal_frame -> inline_frame -> normal_frame -> inline_frame

After compute_frame_id we return to get_prev_frame_if_no_cycle, where
we try to add the frame_id for the new normal_frame into the frame_id
cache, however, unlike before, we fail to add this frame_id as it is
a duplicate of the previous normal_frame frame_id.  Having found a
duplicate get_prev_frame_if_no_cycle unlinks the new frame from the
stack, and returns nullptr, the stack now looks like this:

  inline_frame -> normal_frame -> inline_frame

The nullptr result from get_prev_frame_if_no_cycle is fed back to
inline_frame_this_id, which forwards this to get_frame_id, which
immediately returns null_frame_id.  As null_frame_id is not considered
a valid frame_id, this is what triggers the assertion.

In summary then:

 - inline_frame_this_id currently assumes that as the inline frame
   exists, we will always get a valid frame back from
   get_prev_frame_always,

 - get_prev_frame_if_no_cycle currently assumes that it is safe to
   return nullptr when it sees a cycle.

Notice that in frame.c:compute_frame_id, this code:

  fi->this_id.value = outer_frame_id;
  fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
  gdb_assert (frame_id_p (fi->this_id.value));

The assertion makes it clear that the this_id function must always
return a valid frame_id (e.g. null_frame_id is not a valid return
value), and similarly in inline_frame.c:inline_frame_this_id this
code:

  *this_id = get_frame_id (get_prev_frame_always (this_frame));
  /* snip comment */
  gdb_assert (frame_id_p (*this_id));

Makes it clear that every inline frame expects to be able to get a
previous frame, which will have a valid frame_id.

As I have discussed above, these assumptions don't currently hold in
all cases.

One possibility would be to move the call to get_prev_frame_always
forward from inline_frame_this_id to inline_frame_sniffer, however,
this falls foul of (in frame.c:frame_cleanup_after_sniffer) this
assertion:

  /* No sniffer should extend the frame chain; sniff based on what is
     already certain.  */
  gdb_assert (!frame->prev_p);

This assert prohibits any sniffer from trying to get the previous
frame, as getting the previous frame is likely to depend on the next
frame, I can understand why this assertion is a good thing, and I'm in
no rush to alter this rule.

In a previous version of this patch:

  https://sourceware.org/pipermail/gdb-patches/2021-June/180208.html

I proposed adding a special case to get_prev_frame_if_no_cycle, such
that, if we find a cycle, and we know we are fetching the previous
frame as a result of computing the frame_id for the next frame, which
is an INLINE_FRAME, then, instead of returning nullptr, do still
return the frame.

The idea here was to make adding the "normal_frame -> inline_frame ->"
to the frame list more of an atomic(-ish) operation, we would defer
removing normal_frame if we know is needed to support inline_frame,
and inline_frame will disconnect both if appropriate.

I discussed this approach on IRC, and there was some push back.  Simon
proposed an alternative approach:

  https://sourceware.org/pipermail/gdb-patches/2021-July/180663.html

This alternative approach is what I have implemented here.

Under this approach, in inline_frame_this_id, GDB spots when the call
to get_prev_frame_always returns nullptr.  This nullptr indicates that
we failed to get the previous frame for some reason.

We can then call get_frame_unwind_stop_reason on the current frame.
The stop reason will have been updated to indicate why we couldn't
find a previous frame.

In the specific case that the unwind stop reason is UNWIND_SAME_ID
then we know that the normal_frame had a duplicate frame-id.  For this
case we throw a new exception type (INLINE_FRAME_ID_SAME_ID_ERROR).
For any other stop reason we throw an existing more generic
error (NOT_FOUND_ERROR) to indicate the frame-id (of the inline frame)
was not found.

The other part is to catch INLINE_FRAME_ID_SAME_ID_ERROR in
get_prev_frame_if_no_cycle.  We can then push the UNWIND_SAME_ID stop
reason up the frame stack.

One final issue I ran into was that when I turned did 'set debug frame
on', I ran into a crash from get_prev_frame_always_1.  This is because
of this code:

  /* Only try to do the unwind once.  */
  if (this_frame->prev_p)
    {
      frame_debug_printf ("  -> %s // cached",
			  this_frame->prev->to_string ().c_str ());
      return this_frame->prev;
    }

The frame_debug_printf call assumes that every frame will have a valid
previous frame, this just is not true, the last frame in the call
stack will always have its prev field set to nullptr.  I've handled
this with a specific check for nullptr.  There's no test for this
check in this commit, however, the next commit fixes yet another bug
related to 'set debug frame on', the above bug will trigger for the
test added in the next commit.
---
 gdb/frame.c                                   |  41 +++++-
 gdb/inline-frame.c                            |  31 ++++-
 .../gdb.base/inline-frame-cycle-unwind.c      |  58 ++++++++
 .../gdb.base/inline-frame-cycle-unwind.exp    | 128 ++++++++++++++++++
 .../gdb.base/inline-frame-cycle-unwind.py     |  85 ++++++++++++
 gdbsupport/common-exceptions.h                |   5 +
 6 files changed, 345 insertions(+), 3 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py

diff --git a/gdb/frame.c b/gdb/frame.c
index 3f2d2700541..4f612d3546b 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2091,6 +2091,38 @@ get_prev_frame_if_no_cycle (struct frame_info *this_frame)
 	  this_frame->prev = NULL;
 	}
 
+      if (ex.error == INLINE_FRAME_ID_SAME_ID_ERROR)
+	{
+	  /* This exception is a special case.  Imagine this situation:
+
+	     normal_frame -> inline_frame -> normal_frame -> inline_frame
+
+	     where both normal_frames have the same frame-id, and both
+	     inline_frame's therefore also have the same frame-id.
+
+	     When trying to compute the frame-id of the outer most (on the
+	     left) inline_frame we first ask for its previous frame, this
+	     is the outer most normal_frame.  As this normal_frame is a
+	     duplicate then the inline_frame is returned nullptr instead of
+	     an actual frame_info pointer.
+
+	     When we spot this situation (while calculating the
+	     inline_frame's frame-id) we throw
+	     INLINE_FRAME_ID_SAME_ID_ERROR, which we catch here.
+
+	     We can then set the stop reason to UNWIND_SAME_ID for
+	     THIS_FRAME and return nullptr.  */
+
+	  frame_debug_printf ("  -> nullptr // this frame has same ID");
+
+	  this_frame->stop_reason = UNWIND_SAME_ID;
+
+	  /* THIS_FRAME should already have been unlinked above.  */
+	  gdb_assert (get_frame_cache_generation () == entry_generation);
+
+	  return nullptr;
+	}
+
       throw;
     }
 
@@ -2121,8 +2153,13 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
   /* Only try to do the unwind once.  */
   if (this_frame->prev_p)
     {
-      frame_debug_printf ("  -> %s // cached",
-			  this_frame->prev->to_string ().c_str ());
+      if (this_frame->prev != nullptr)
+	frame_debug_printf ("  -> %s // cached",
+			    this_frame->prev->to_string ().c_str ());
+      else
+	frame_debug_printf
+	  ("  -> nullptr // %s // cached",
+	   frame_stop_reason_symbol_string (this_frame->stop_reason));
       return this_frame->prev;
     }
 
diff --git a/gdb/inline-frame.c b/gdb/inline-frame.c
index c98af1842a6..33ba7bf6f92 100644
--- a/gdb/inline-frame.c
+++ b/gdb/inline-frame.c
@@ -163,7 +163,36 @@ inline_frame_this_id (struct frame_info *this_frame,
      function, there must be previous frames, so this is safe - as
      long as we're careful not to create any cycles.  See related
      comments in get_prev_frame_always_1.  */
-  *this_id = get_frame_id (get_prev_frame_always (this_frame));
+  frame_info *prev_frame = get_prev_frame_always (this_frame);
+  if (prev_frame == nullptr)
+    {
+      /* Failure to find a previous frame could happen for any number of
+	 reasons, however, the stop reason on THIS_FRAME will be valid at
+	 this point.
+
+	 If trying to find the previous frame set the stop reason to
+	 UNWIND_SAME_ID then this indicates that the "normal" frame in
+	 which this frame is inline is a duplicate, and therefore this
+	 inline frame is also a duplicate.
+
+	 To communicate this back to the frame.c code we throw a specific
+	 exception here (INLINE_FRAME_ID_SAME_ID_ERROR) which is caught and
+	 handled in frame.c.
+
+	 If the stop reason is anything other than UNWIND_SAME_ID then this
+	 is some other, unexpected, error, and we throw a generic error.  */
+      if (get_frame_unwind_stop_reason (this_frame) == UNWIND_SAME_ID)
+	throw_error (INLINE_FRAME_ID_SAME_ID_ERROR,
+		     "Failed to find the frame previous to an inline frame, "
+		     "the previous frame had stop reason UNWIND_SAME_ID");
+      else
+	throw_error (NOT_FOUND_ERROR,
+		     "Failed to find the frame previous to an inline frame, "
+		     "the previous frame had stop reason %s",
+		     frame_stop_reason_string (this_frame));
+    }
+
+  *this_id = get_frame_id (prev_frame);
 
   /* We need a valid frame ID, so we need to be based on a valid
      frame.  FSF submission NOTE: this would be a good assertion to
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
new file mode 100644
index 00000000000..183c40928b6
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+static void inline_func (void);
+static void normal_func (void);
+
+volatile int global_var;
+volatile int level_counter;
+
+static void __attribute__((noinline))
+normal_func (void)
+{
+  /* Do some work.  */
+  ++global_var;
+
+  /* Now the inline function.  */
+  --level_counter;
+  inline_func ();
+  ++level_counter;
+
+  /* Do some work.  */
+  ++global_var;
+}
+
+static inline void __attribute__((__always_inline__))
+inline_func (void)
+{
+  if (level_counter > 1)
+    {
+      --level_counter;
+      normal_func ();
+      ++level_counter;
+    }
+  else
+    ++global_var;	/* Break here.  */
+}
+
+int
+main ()
+{
+  level_counter = 6;
+  normal_func ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
new file mode 100644
index 00000000000..27709fe2232
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
@@ -0,0 +1,128 @@
+# Copyright (C) 2021 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 checks for an edge case when unwinding inline frames which
+# occur towards the older end of the stack when the stack ends with a
+# cycle.  Consider this well formed stack:
+#
+#   main -> normal_frame -> inline_frame
+#
+# Now consider that, for whatever reason, the stack unwinding of
+# "normal_frame" becomes corrupted, such that the stack appears to be
+# this:
+#
+#   .-> normal_frame -> inline_frame
+#   |      |
+#   '------'
+#
+# When confronted with such a situation we would expect GDB to detect
+# the stack frame cycle and terminate the backtrace at the first
+# instance of "normal_frame" with a message:
+#
+#   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
+#
+# However, at one point there was a bug in GDB's inline frame
+# mechanism such that the fact that "inline_frame" was inlined into
+# "normal_frame" would cause GDB to trigger an assertion.
+#
+# This text makes use of a Python unwinder which can fake the cyclic
+# stack cycle, further the test sets up multiple levels of normal and
+# inline frames.  At the point of testing the stack looks like this:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Where "normal_func" is a normal frame, and "inline_func" is an inline frame.
+#
+# The python unwinder is then used to force a stack cycle at each
+# "normal_func" frame in turn, we then check that GDB can successfully unwind
+# the stack.
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}]} {
+    return -1
+}
+
+# Skip this test if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to the breakpoint where we will carry out the test.
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# Load the script containing the unwinder, this must be done at the
+# testing point as the script will examine the stack as it is loaded.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Check the unbroken stack.
+gdb_test_sequence "bt" "backtrace when the unwind is left unbroken" {
+    "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
+}
+
+with_test_prefix "cycle at level 5" {
+    # Arrange to introduce a stack cycle at frame 5.
+    gdb_test_no_output "python stop_at_level=5"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 5" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#4 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#5 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 3" {
+    # Arrange to introduce a stack cycle at frame 3.
+    gdb_test_no_output "python stop_at_level=3"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 3" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 1" {
+    # Arrange to introduce a stack cycle at frame 1.
+    gdb_test_no_output "python stop_at_level=1"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 1" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
new file mode 100644
index 00000000000..99c571f973c
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+# Set this to the stack level the backtrace should be corrupted at.
+# This will only work for frame 1, 3, or 5 in the test this unwinder
+# was written for.
+stop_at_level = None
+
+# Set this to the stack frame size of frames 1, 3, and 5.  These
+# frames will all have the same stack frame size as they are the same
+# function called recursively.
+stack_adjust = None
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "stop at level")
+
+    def __call__(self, pending_frame):
+        global stop_at_level
+        global stack_adjust
+
+        if stop_at_level is None or pending_frame.level() != stop_at_level:
+            return None
+
+        if stack_adjust is None:
+            raise gdb.GdbError("invalid stack_adjust")
+
+        if not stop_at_level in [1, 3, 5]:
+            raise gdb.GdbError("invalid stop_at_level")
+
+        sp_desc = pending_frame.architecture().registers().find("sp")
+        sp = pending_frame.read_register(sp_desc) + stack_adjust
+        pc = (gdb.lookup_symbol("normal_func"))[0].value().address
+        unwinder = pending_frame.create_unwind_info(FrameId(sp, pc))
+
+        for reg in pending_frame.architecture().registers("general"):
+            val = pending_frame.read_register(reg)
+            unwinder.add_saved_register(reg, val)
+        return unwinder
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
+
+# When loaded, it is expected that the stack looks like:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Compute the stack frame size of normal_func, which has inline_func
+# inlined within it.
+f0 = gdb.newest_frame()
+f1 = f0.older()
+f2 = f1.older()
+f0_sp = f0.read_register("sp")
+f2_sp = f2.read_register("sp")
+stack_adjust = f2_sp - f0_sp
diff --git a/gdbsupport/common-exceptions.h b/gdbsupport/common-exceptions.h
index 92f43d267ad..cff2ad9bd85 100644
--- a/gdbsupport/common-exceptions.h
+++ b/gdbsupport/common-exceptions.h
@@ -106,6 +106,11 @@ enum errors {
      "_ERROR" is appended to the name.  */
   MAX_COMPLETIONS_REACHED_ERROR,
 
+  /* An error to throw when computing the frame-id of an inline frame, in
+     the situation where we can't get the frame-id of the previous frame
+     due to it being a duplicate.  */
+  INLINE_FRAME_ID_SAME_ID_ERROR,
+
   /* Add more errors here.  */
   NR_ERRORS
 };
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv3 2/2] gdb: remove VALUE_FRAME_ID
  2021-07-20  9:10   ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
  2021-07-20  9:10     ` [PATCHv3 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
@ 2021-07-20  9:10     ` Andrew Burgess
  2021-07-20 21:59     ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Simon Marchi
  2021-07-27 10:10     ` [PATCHv4] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
  3 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-07-20  9:10 UTC (permalink / raw)
  To: gdb-patches

While working on the previous commit I happened to switch on 'set
debug frame 1', and ran into a different assertion than the one I was
trying to fix.

The new problem I see is this assertion triggering:

  gdb/frame.c:622: internal-error: frame_id get_frame_id(frame_info*): Assertion `fi->this_id.p != frame_id_status::COMPUTING' failed.

We attempt to get the frame_id for a frame while we are computing the
frame_id for that same frame.

What happens is we have a stack like this:

  normal_frame -> inline_frame -> sentinel_frame

When we initially stop, GDB creates a frame for inline_frame but
doesn't sniff its type, or try to fill in its frame_id (see code near
the top of get_prev_frame_if_no_cycle).

Later on during the stop, this happens:

  process_event_stop_test
    get_stack_frame_id
      skip_artificial_frames
        get_frame_type

The call to get_frame_type causes inline_frame to sniff its type,
establishing that it is an INLINE_FRAME, but does not cause the frame
to build its frame_id.

The same skip_artificial_frames call then calls get_prev_frame_always
to unwind the stack, this will then try to get the frame previous to
inline_frame, i.e. normal_frame.

So, we create a new frame, but unlike frame #0, in
get_prev_frame_if_no_cycle, we immediately try to compute the frame_id
for this new frame #1.

Computing the frame_id for frame #1 invokes the sniffer.  If this
sniffer tries to read a register then we will create a lazy register
value by calling value_of_register_lazy.  As the next frame (frame #0)
is an INLINE_FRAME, we will skip this and instead create the lazy
register value using the frame_id for frame #-1 (the sentinel frame).

Now, when we try to resolve the lazy register value, in the value.c
function value_fetch_lazy_register, we want to print which frame the
lazy register value was for (in debug mode), so we call:

  frame = frame_find_by_id (VALUE_FRAME_ID (val));

where:

  #define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))

In our case we call get_prev_frame_id_by_id with the frame_id of the
sentinel_frame frame, we then call frame_find_by_id, followed by
get_prev_frame (which gives us frame #0), followed by get_frame_id.

Frame #0 has not yet computed its frame_id, so we start that process.
The first thing we need when computing the frame_id of an inline frame
is the frame_id of the previous frame, so that's what we ask for.
Unfortunately, that's where we started this journey, we are already
computing the frame_id for frame #1, and thus the assert fires.

Solving the assertion failure is pretty easy, if we consider the code
in value_fetch_lazy_register and get_prev_frame_id_by_id then what we
do is:

  1. Start with a frame_id taken from a value,
  2. Lookup the corresponding frame,
  3. Find the previous frame,
  4. Get the frame_id for that frame, and
  5. Lookup the corresponding frame
  6. Print the frame's level

Notice that steps 3 and 5 give us the exact same result, step 4 is
just wasted effort.  We could shorten this process such that we drop
steps 4 and 5, thus:

  1. Start with a frame_id taken from a value,
  2. Lookup the corresponding frame,
  3. Find the previous frame,
  6. Print the frame's level

This will give the exact same frame as a result, and this is what I
have done in this patch by removing the use of VALUE_FRAME_ID from
value_fetch_lazy_register.

Out of curiosity I looked to see how widely VALUE_FRAME_ID was used,
and saw it was only used in one other place in valops.c:value_assign,
where, once again, we take the result of VALUE_FRAME_ID and pass it to
frame_find_by_id, thus introducing a redundant frame_id lookup.

I don't think the value_assign case risks triggering the assertion
though, as we are unlikely to call value_assign while computing the
frame_id for a frame, however, we could make value_assign slightly
more efficient, with no real additional complexity, by removing the
use of VALUE_FRAME_ID.

So, in this commit, I completely remove VALUE_FRAME_ID, and replace it
with a use of VALUE_NEXT_FRAME_ID, followed by a direct call to
get_prev_frame_always, this should make no difference in either case,
and resolves the assertion issue from value.c.

One thing that is worth noticing here is that in these two situations
we don't end up getting the frame we expected to, though this is not a
result of my change, we were not getting the expected result before
either.

Consider the debug printing case, the stack is:

  normal_frame -> inline_frame -> sentinel_frame

We read a register from normal_frame (frame #1), the value of which is
fetched from sentinel_frame (frame #-1).  The debug print is trying to
say:

  frame=1,regnum=....

However, as the lazy register value points at frame #-1, we will
actually (incorrectly) print:

  frame=0,regnum=....

Like I said, this bug existed before this commit, and so, if we didn't
assert (i.e. the lazy register read occurred in some context other
than during the frame sniffer), then the debug print would previous
have, and will continue to, print the wrong frame level.  Thankfully,
this is only in debug output, so not something a normal user should
see.

In theory, the same bug exists in the value_assign code, if we are in
normal_frame and try to perform a register assignment, then GDB will
get confused and think we are assigning in the context of
inline_frame.  However, having looked at the code I think we get away
with this as the frame is used for two things that I can see:

  1. Getting the gdbarch for the frame, I can't imagine a situation
  where inline_frame has a different gdbarch to normal_frame, and

  2. Unwinding register values from frame->next.  We should be asking
  to unwind the register values from inline_frame, but really we end
  up asking to unwind from sentinel_frame.  However, if we did ask to
  unwind the values from inline_frame this would just forward the
  request on to the next frame, i.e. sentinel_frame, so we would get
  the exact same result.

In short, though we do use the wrong frame in value_assign, I think
this is harmless.

Fixing this debug printing would require GDB to require extra
information in its value location to indicate how many frames had been
skipped.  For example, with the stack:

  normal_frame -> inline_frame -> sentinel_frame

A lazy register value read in frame normal_frame would have a location
frame_id for sentinel_frame, and a skip value of 2 indicating the
value was read for a frame 2 previous.  In contrast, for a more
standard case, with a stack like this:

  normal_frame -> sentinel_frame

A lazy register value read in frame normal_frame would have a location
frame_id for sentinel_frame and a skip value of 1 indicating the value
was read for a frame 1 previous.

However, adding this seems like a lot of work to fix a single like of
debug print, but might be something we want to consider in the future.
---
 gdb/frame.c                                     | 16 ----------------
 gdb/frame.h                                     |  4 ----
 .../gdb.base/inline-frame-cycle-unwind.exp      | 17 +++++++++++++++++
 gdb/valops.c                                    | 17 +++++++++--------
 gdb/value.c                                     |  5 ++---
 gdb/value.h                                     |  6 ------
 6 files changed, 28 insertions(+), 37 deletions(-)

diff --git a/gdb/frame.c b/gdb/frame.c
index 4f612d3546b..45a34badec6 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2552,22 +2552,6 @@ get_prev_frame (struct frame_info *this_frame)
   return get_prev_frame_always (this_frame);
 }
 
-struct frame_id
-get_prev_frame_id_by_id (struct frame_id id)
-{
-  struct frame_id prev_id;
-  struct frame_info *frame;
-
-  frame = frame_find_by_id (id);
-
-  if (frame != NULL)
-    prev_id = get_frame_id (get_prev_frame (frame));
-  else
-    prev_id = null_frame_id;
-
-  return prev_id;
-}
-
 CORE_ADDR
 get_frame_pc (struct frame_info *frame)
 {
diff --git a/gdb/frame.h b/gdb/frame.h
index 0d2bc08a47b..2548846c1ed 100644
--- a/gdb/frame.h
+++ b/gdb/frame.h
@@ -394,10 +394,6 @@ extern struct frame_info *get_prev_frame_always (struct frame_info *);
    is not found.  */
 extern struct frame_info *frame_find_by_id (struct frame_id id);
 
-/* Given a frame's ID, find the previous frame's ID.  Returns null_frame_id
-   if the frame is not found.  */
-extern struct frame_id get_prev_frame_id_by_id (struct frame_id id);
-
 /* Base attributes of a frame: */
 
 /* The frame's `resume' address.  Where the program will resume in
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
index 27709fe2232..2801b683a03 100644
--- a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
@@ -126,3 +126,20 @@ with_test_prefix "cycle at level 1" {
 	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
 	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
 }
+
+# Flush the register cache (which also flushes the frame cache) so we
+# get a full backtrace again, then switch on frame debugging and try
+# to back trace.  At one point this triggered an assertion.
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_no_output "set debug frame 1"
+gdb_test_multiple "bt" "backtrace with debugging on" {
+    -re "^$gdb_prompt $" {
+	pass $gdb_test_name
+    }
+    -re "\[^\r\n\]+\r\n" {
+	exp_continue
+    }
+}
+gdb_test "p 1 + 2 + 3" " = 6" \
+    "ensure GDB is still alive"
diff --git a/gdb/valops.c b/gdb/valops.c
index bd547923496..50874a5f55d 100644
--- a/gdb/valops.c
+++ b/gdb/valops.c
@@ -1197,14 +1197,15 @@ value_assign (struct value *toval, struct value *fromval)
 	struct gdbarch *gdbarch;
 	int value_reg;
 
-	/* Figure out which frame this is in currently.
-	
-	   We use VALUE_FRAME_ID for obtaining the value's frame id instead of
-	   VALUE_NEXT_FRAME_ID due to requiring a frame which may be passed to
-	   put_frame_register_bytes() below.  That function will (eventually)
-	   perform the necessary unwind operation by first obtaining the next
-	   frame.  */
-	frame = frame_find_by_id (VALUE_FRAME_ID (toval));
+	/* Figure out which frame this register value is in.  The value
+	   holds the frame_id for the next frame, that is the frame this
+	   register value was unwound from.
+
+	   Below we will call put_frame_register_bytes which requires that
+	   we pass it the actual frame in which the register value is
+	   valid, i.e. not the next frame.  */
+	frame = frame_find_by_id (VALUE_NEXT_FRAME_ID (toval));
+	frame = get_prev_frame_always (frame);
 
 	value_reg = VALUE_REGNUM (toval);
 
diff --git a/gdb/value.c b/gdb/value.c
index 6a07495d32b..91db66fa3be 100644
--- a/gdb/value.c
+++ b/gdb/value.c
@@ -3950,9 +3950,8 @@ value_fetch_lazy_register (struct value *val)
     {
       struct gdbarch *gdbarch;
       struct frame_info *frame;
-      /* VALUE_FRAME_ID is used here, instead of VALUE_NEXT_FRAME_ID,
-	 so that the frame level will be shown correctly.  */
-      frame = frame_find_by_id (VALUE_FRAME_ID (val));
+      frame = frame_find_by_id (VALUE_NEXT_FRAME_ID (val));
+      frame = get_prev_frame_always (frame);
       regnum = VALUE_REGNUM (val);
       gdbarch = get_frame_arch (frame);
 
diff --git a/gdb/value.h b/gdb/value.h
index 379cddafbe7..e1c6aabfa29 100644
--- a/gdb/value.h
+++ b/gdb/value.h
@@ -458,12 +458,6 @@ extern struct internalvar **deprecated_value_internalvar_hack (struct value *);
 extern struct frame_id *deprecated_value_next_frame_id_hack (struct value *);
 #define VALUE_NEXT_FRAME_ID(val) (*deprecated_value_next_frame_id_hack (val))
 
-/* Frame ID of frame to which a register value is relative.  This is
-   similar to VALUE_NEXT_FRAME_ID, above, but may not be assigned to. 
-   Note that VALUE_FRAME_ID effectively undoes the "next" operation
-   that was performed during the assignment to VALUE_NEXT_FRAME_ID.  */
-#define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))
-
 /* Register number if the value is from a register.  */
 extern int *deprecated_value_regnum_hack (struct value *);
 #define VALUE_REGNUM(val) (*deprecated_value_regnum_hack (val))
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames
  2021-07-20  9:10   ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
  2021-07-20  9:10     ` [PATCHv3 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
  2021-07-20  9:10     ` [PATCHv3 2/2] gdb: remove VALUE_FRAME_ID Andrew Burgess
@ 2021-07-20 21:59     ` Simon Marchi
  2021-07-26 11:11       ` Andrew Burgess
  2021-07-27 10:10     ` [PATCHv4] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
  3 siblings, 1 reply; 48+ messages in thread
From: Simon Marchi @ 2021-07-20 21:59 UTC (permalink / raw)
  To: Andrew Burgess, gdb-patches

On 2021-07-20 5:10 a.m., Andrew Burgess wrote:
> Thanks for the feedback on v2.
> 
> In v3 I have:
> 
>  - Addressed all of Pedro's feedback on the test in patch #1.
>  
>  - Rewritten how the problem in patch #1 is fixed based on Simon's
>    exception based approach.
> 
> Thanks,
> Andrew

When speaking to Pedro off-line, it did sound like he had concerns with
what I proposed, so let's wait to hear what he has to say.

Simon

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames
  2021-07-20 21:59     ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Simon Marchi
@ 2021-07-26 11:11       ` Andrew Burgess
  2021-07-26 13:57         ` Simon Marchi
  0 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-07-26 11:11 UTC (permalink / raw)
  To: Simon Marchi; +Cc: gdb-patches, Joel Brobecker

* Simon Marchi <simon.marchi@polymtl.ca> [2021-07-20 17:59:45 -0400]:

> On 2021-07-20 5:10 a.m., Andrew Burgess wrote:
> > Thanks for the feedback on v2.
> > 
> > In v3 I have:
> > 
> >  - Addressed all of Pedro's feedback on the test in patch #1.
> >  
> >  - Rewritten how the problem in patch #1 is fixed based on Simon's
> >    exception based approach.
> > 
> > Thanks,
> > Andrew
> 
> When speaking to Pedro off-line, it did sound like he had concerns with
> what I proposed, so let's wait to hear what he has to say.

That's fine.  Hopefully Pedro will be able to offer some feedback
soon.

Before then, I've spun off patch #2 and an additional fix related to
'set debug frame on' into a separate patch (below).  I think all of
this code is unrelated to the whole should we use an exception, or
change the API of get_prev_frame.

What are your thoughts on this patch?

Additionally, this one might be a possible candidate for merging into
gdb-11-branch.  Here's a ChangeLog if we decide that's a good idea.

gdb/ChangeLog:
	* frame.c (get_prev_frame_always_1): Handle case where
	this_frame->prev is nullptr.
	(get_prev_frame_id_by_id): Delete.
	* frame.h (get_prev_frame_id_by_id): Delete declaration.
	* valops.c (value_assign): Remove use of VALUE_FRAME_ID.
	* vaule.c (value_fetch_lazy_register): Likewise.
	* value.h (VALUE_FRAME_ID): Delete.

gdb/testsuite/ChangeLog:
	* gdb.base/premature-dummy-frame-removal.exp: Repeat test with
	'set debug frame on' in effect.

Thanks,
Andrew

---

commit 4365cdedccda9eaacd84af84a89c981d28ac1ef7
Author: Andrew Burgess <andrew.burgess@embecosm.com>
Date:   Wed May 26 15:50:05 2021 +0100

    gdb: remove VALUE_FRAME_ID and fix another frame debug issue
    
    This commit was originally part of this patch series:
    
      (v1): https://sourceware.org/pipermail/gdb-patches/2021-May/179357.html
      (v2): https://sourceware.org/pipermail/gdb-patches/2021-June/180208.html
      (v3): https://sourceware.org/pipermail/gdb-patches/2021-July/181028.html
    
    However, that series is being held up in review, so I wanted to break
    out some of the non-related fixes in order to get these merged.
    
    This commit addresses two semi-related issues, both of which are
    problems exposed by using 'set debug frame on'.
    
    The first issue is in frame.c in get_prev_frame_always_1, and was
    introduced by this commit:
    
      commit a05a883fbaba69d0f80806e46a9457727fcbe74c
      Date:   Tue Jun 29 12:03:50 2021 -0400
    
          gdb: introduce frame_debug_printf
    
    This commit replaced fprint_frame with frame_info::to_string.
    However, the former could handle taking a nullptr while the later, a
    member function, obviously requires a non-nullptr in order to make the
    function call.  In one place we are not-guaranteed to have a
    non-nullptr, and so, there is the possibility of triggering undefined
    behaviour.
    
    The second issue addressed in this commit has existed for a while in
    GDB, and would cause this assertion:
    
      gdb/frame.c:622: internal-error: frame_id get_frame_id(frame_info*): Assertion `fi->this_id.p != frame_id_status::COMPUTING' failed.
    
    We attempt to get the frame_id for a frame while we are computing the
    frame_id for that same frame.
    
    What happens is that when GDB stops we create a frame_info object for
    the sentinel frame (frame #-1) and then we attempt to unwind this
    frame to create a frame_info object for frame #0.
    
    In the test case used here to expose the issue we have created a
    Python frame unwinder.  In the Python unwinder we attemt to read the
    program counter register.
    
    Reading this register will initially create a lazy register value.
    The frame-id stored in the lazy register value will be for the
    sentinel frame (lazy register values hold the frame-id for the frame
    from which the register will be unwound).
    
    However, the Python unwinder does actually want to examine the value
    of the program counter, and so the lazy register value is resolved
    into a non-lazy value.  This sends GDB into value_fetch_lazy_register
    in value.c.
    
    Now, inside this function, if 'set debug frame on' is in effect, then
    we want to print something like:
    
      frame=%d, regnum=%d(%s), ....
    
    Where 'frame=%d' will be the relative frame level of the frame for
    which the register is being fetched, so, in this case we would expect
    to see 'frame=0', i.e. we are reading a register as it would be in
    frame #0.  But, remember, the lazy register value actually holds the
    frame-id for frame #-1 (the sentinel frame).
    
    So, to get the frame_info for frame #0 we used to call:
    
      frame = frame_find_by_id (VALUE_FRAME_ID (val));
    
    Where VALUE_FRAME_ID is:
    
      #define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))
    
    That is, we start with the frame-id for the next frame as obtained by
    VALUE_NEXT_FRAME_ID, then call get_prev_frame_id_by_id to get the
    frame-id of the previous frame.
    
    The get_prev_frame_id_by_id function finds the frame_info for the
    given frame-id (in this case frame #-1), calls get_prev_frame to get
    the previous frame, and then calls get_frame_id.
    
    The problem here is that calling get_frame_id requires that we know
    the frame unwinder, so then have to try each frame unwinder in turn,
    which would include the Python unwinder.... which is where we started,
    and thus we have a loop!
    
    To prevent this loop GDB has an assertion in place, which is what
    actually triggers.
    
    Solving the assertion failure is pretty easy, if we consider the code
    in value_fetch_lazy_register and get_prev_frame_id_by_id then what we
    do is:
    
      1. Start with a frame_id taken from a value,
      2. Lookup the corresponding frame,
      3. Find the previous frame,
      4. Get the frame_id for that frame, and
      5. Lookup the corresponding frame
      6. Print the frame's level
    
    Notice that steps 3 and 5 give us the exact same result, step 4 is
    just wasted effort.  We could shorten this process such that we drop
    steps 4 and 5, thus:
    
      1. Start with a frame_id taken from a value,
      2. Lookup the corresponding frame,
      3. Find the previous frame,
      6. Print the frame's level
    
    This will give the exact same frame as a result, and this is what I
    have done in this patch by removing the use of VALUE_FRAME_ID from
    value_fetch_lazy_register.
    
    Out of curiosity I looked to see how widely VALUE_FRAME_ID was used,
    and saw it was only used in one other place in valops.c:value_assign,
    where, once again, we take the result of VALUE_FRAME_ID and pass it to
    frame_find_by_id, thus introducing a redundant frame_id lookup.
    
    I don't think the value_assign case risks triggering the assertion
    though, as we are unlikely to call value_assign while computing the
    frame_id for a frame, however, we could make value_assign slightly
    more efficient, with no real additional complexity, by removing the
    use of VALUE_FRAME_ID.
    
    So, in this commit, I completely remove VALUE_FRAME_ID, and replace it
    with a use of VALUE_NEXT_FRAME_ID, followed by a direct call to
    get_prev_frame_always, this should make no difference in either case,
    and resolves the assertion issue from value.c.
    
    As I said, this patch was originally part of another series, the
    original test relied on the fixes in that original series.  However, I
    was able to create an alternative test for this issue by enabling
    frame debug within an existing test script.

diff --git a/gdb/frame.c b/gdb/frame.c
index 3f2d2700541..2332418f347 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2121,8 +2121,13 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
   /* Only try to do the unwind once.  */
   if (this_frame->prev_p)
     {
-      frame_debug_printf ("  -> %s // cached",
-			  this_frame->prev->to_string ().c_str ());
+      if (this_frame->prev != nullptr)
+	frame_debug_printf ("  -> %s // cached",
+			    this_frame->prev->to_string ().c_str ());
+      else
+	frame_debug_printf
+	  ("  -> nullptr // %s // cached",
+	   frame_stop_reason_symbol_string (this_frame->stop_reason));
       return this_frame->prev;
     }
 
@@ -2515,22 +2520,6 @@ get_prev_frame (struct frame_info *this_frame)
   return get_prev_frame_always (this_frame);
 }
 
-struct frame_id
-get_prev_frame_id_by_id (struct frame_id id)
-{
-  struct frame_id prev_id;
-  struct frame_info *frame;
-
-  frame = frame_find_by_id (id);
-
-  if (frame != NULL)
-    prev_id = get_frame_id (get_prev_frame (frame));
-  else
-    prev_id = null_frame_id;
-
-  return prev_id;
-}
-
 CORE_ADDR
 get_frame_pc (struct frame_info *frame)
 {
diff --git a/gdb/frame.h b/gdb/frame.h
index 0d2bc08a47b..2548846c1ed 100644
--- a/gdb/frame.h
+++ b/gdb/frame.h
@@ -394,10 +394,6 @@ extern struct frame_info *get_prev_frame_always (struct frame_info *);
    is not found.  */
 extern struct frame_info *frame_find_by_id (struct frame_id id);
 
-/* Given a frame's ID, find the previous frame's ID.  Returns null_frame_id
-   if the frame is not found.  */
-extern struct frame_id get_prev_frame_id_by_id (struct frame_id id);
-
 /* Base attributes of a frame: */
 
 /* The frame's `resume' address.  Where the program will resume in
diff --git a/gdb/testsuite/gdb.base/premature-dummy-frame-removal.exp b/gdb/testsuite/gdb.base/premature-dummy-frame-removal.exp
index bf2a2a79756..1c0826201bb 100644
--- a/gdb/testsuite/gdb.base/premature-dummy-frame-removal.exp
+++ b/gdb/testsuite/gdb.base/premature-dummy-frame-removal.exp
@@ -51,3 +51,22 @@ set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
 gdb_test_no_output "source ${pyfile}" "load python file"
 
 gdb_test "p some_func ()" " = 0"
+
+# When frame debugging is turned on, this test has (previously)
+# revealed some crashes due to the Python frame unwinder trying to
+# read registers.
+#
+# Enable frame debug and rerun the test.  We don't bother checking the
+# output of calling 'p some_func ()' as the output will be full of
+# debug, to format of which isn't fixed.  All we care about is that
+# GDB is still running afterwards.
+#
+# All of the debug output makes this really slow when testing with the
+# special read1 version of expect, hence the timeout factor.
+with_read1_timeout_factor 10 {
+    gdb_test_no_output "set debug frame on"
+    gdb_test "p some_func ()" ".*" \
+	"repeat p some_func () with frame debug on"
+    gdb_test_no_output "set debug frame off"
+}
+gdb_test "p 1 + 2 + 3" " = 6"
diff --git a/gdb/valops.c b/gdb/valops.c
index bd547923496..50874a5f55d 100644
--- a/gdb/valops.c
+++ b/gdb/valops.c
@@ -1197,14 +1197,15 @@ value_assign (struct value *toval, struct value *fromval)
 	struct gdbarch *gdbarch;
 	int value_reg;
 
-	/* Figure out which frame this is in currently.
-	
-	   We use VALUE_FRAME_ID for obtaining the value's frame id instead of
-	   VALUE_NEXT_FRAME_ID due to requiring a frame which may be passed to
-	   put_frame_register_bytes() below.  That function will (eventually)
-	   perform the necessary unwind operation by first obtaining the next
-	   frame.  */
-	frame = frame_find_by_id (VALUE_FRAME_ID (toval));
+	/* Figure out which frame this register value is in.  The value
+	   holds the frame_id for the next frame, that is the frame this
+	   register value was unwound from.
+
+	   Below we will call put_frame_register_bytes which requires that
+	   we pass it the actual frame in which the register value is
+	   valid, i.e. not the next frame.  */
+	frame = frame_find_by_id (VALUE_NEXT_FRAME_ID (toval));
+	frame = get_prev_frame_always (frame);
 
 	value_reg = VALUE_REGNUM (toval);
 
diff --git a/gdb/value.c b/gdb/value.c
index 6a07495d32b..91db66fa3be 100644
--- a/gdb/value.c
+++ b/gdb/value.c
@@ -3950,9 +3950,8 @@ value_fetch_lazy_register (struct value *val)
     {
       struct gdbarch *gdbarch;
       struct frame_info *frame;
-      /* VALUE_FRAME_ID is used here, instead of VALUE_NEXT_FRAME_ID,
-	 so that the frame level will be shown correctly.  */
-      frame = frame_find_by_id (VALUE_FRAME_ID (val));
+      frame = frame_find_by_id (VALUE_NEXT_FRAME_ID (val));
+      frame = get_prev_frame_always (frame);
       regnum = VALUE_REGNUM (val);
       gdbarch = get_frame_arch (frame);
 
diff --git a/gdb/value.h b/gdb/value.h
index 379cddafbe7..e1c6aabfa29 100644
--- a/gdb/value.h
+++ b/gdb/value.h
@@ -458,12 +458,6 @@ extern struct internalvar **deprecated_value_internalvar_hack (struct value *);
 extern struct frame_id *deprecated_value_next_frame_id_hack (struct value *);
 #define VALUE_NEXT_FRAME_ID(val) (*deprecated_value_next_frame_id_hack (val))
 
-/* Frame ID of frame to which a register value is relative.  This is
-   similar to VALUE_NEXT_FRAME_ID, above, but may not be assigned to. 
-   Note that VALUE_FRAME_ID effectively undoes the "next" operation
-   that was performed during the assignment to VALUE_NEXT_FRAME_ID.  */
-#define VALUE_FRAME_ID(val) (get_prev_frame_id_by_id (VALUE_NEXT_FRAME_ID (val)))
-
 /* Register number if the value is from a register.  */
 extern int *deprecated_value_regnum_hack (struct value *);
 #define VALUE_REGNUM(val) (*deprecated_value_regnum_hack (val))


^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames
  2021-07-26 11:11       ` Andrew Burgess
@ 2021-07-26 13:57         ` Simon Marchi
  2021-07-27 10:06           ` Andrew Burgess
  0 siblings, 1 reply; 48+ messages in thread
From: Simon Marchi @ 2021-07-26 13:57 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches, Joel Brobecker

On 2021-07-26 7:11 a.m., Andrew Burgess wrote:
> * Simon Marchi <simon.marchi@polymtl.ca> [2021-07-20 17:59:45 -0400]:
> 
>> On 2021-07-20 5:10 a.m., Andrew Burgess wrote:
>>> Thanks for the feedback on v2.
>>>
>>> In v3 I have:
>>>
>>>  - Addressed all of Pedro's feedback on the test in patch #1.
>>>  
>>>  - Rewritten how the problem in patch #1 is fixed based on Simon's
>>>    exception based approach.
>>>
>>> Thanks,
>>> Andrew
>>
>> When speaking to Pedro off-line, it did sound like he had concerns with
>> what I proposed, so let's wait to hear what he has to say.
> 
> That's fine.  Hopefully Pedro will be able to offer some feedback
> soon.
> 
> Before then, I've spun off patch #2 and an additional fix related to
> 'set debug frame on' into a separate patch (below).  I think all of
> this code is unrelated to the whole should we use an exception, or
> change the API of get_prev_frame.
> 
> What are your thoughts on this patch?
> 
> Additionally, this one might be a possible candidate for merging into
> gdb-11-branch.  Here's a ChangeLog if we decide that's a good idea.
> 
> gdb/ChangeLog:
> 	* frame.c (get_prev_frame_always_1): Handle case where
> 	this_frame->prev is nullptr.
> 	(get_prev_frame_id_by_id): Delete.
> 	* frame.h (get_prev_frame_id_by_id): Delete declaration.
> 	* valops.c (value_assign): Remove use of VALUE_FRAME_ID.
> 	* vaule.c (value_fetch_lazy_register): Likewise.
> 	* value.h (VALUE_FRAME_ID): Delete.
> 
> gdb/testsuite/ChangeLog:
> 	* gdb.base/premature-dummy-frame-removal.exp: Repeat test with
> 	'set debug frame on' in effect.
> 
> Thanks,
> Andrew

The patch below does LGTM, thanks.

Simon

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames
  2021-07-26 13:57         ` Simon Marchi
@ 2021-07-27 10:06           ` Andrew Burgess
  0 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-07-27 10:06 UTC (permalink / raw)
  To: Simon Marchi; +Cc: gdb-patches, Joel Brobecker

* Simon Marchi <simon.marchi@polymtl.ca> [2021-07-26 09:57:02 -0400]:

> On 2021-07-26 7:11 a.m., Andrew Burgess wrote:
> > * Simon Marchi <simon.marchi@polymtl.ca> [2021-07-20 17:59:45 -0400]:
> > 
> >> On 2021-07-20 5:10 a.m., Andrew Burgess wrote:
> >>> Thanks for the feedback on v2.
> >>>
> >>> In v3 I have:
> >>>
> >>>  - Addressed all of Pedro's feedback on the test in patch #1.
> >>>  
> >>>  - Rewritten how the problem in patch #1 is fixed based on Simon's
> >>>    exception based approach.
> >>>
> >>> Thanks,
> >>> Andrew
> >>
> >> When speaking to Pedro off-line, it did sound like he had concerns with
> >> what I proposed, so let's wait to hear what he has to say.
> > 
> > That's fine.  Hopefully Pedro will be able to offer some feedback
> > soon.
> > 
> > Before then, I've spun off patch #2 and an additional fix related to
> > 'set debug frame on' into a separate patch (below).  I think all of
> > this code is unrelated to the whole should we use an exception, or
> > change the API of get_prev_frame.
> > 
> > What are your thoughts on this patch?
> > 
> > Additionally, this one might be a possible candidate for merging into
> > gdb-11-branch.  Here's a ChangeLog if we decide that's a good idea.
> > 
> > gdb/ChangeLog:
> > 	* frame.c (get_prev_frame_always_1): Handle case where
> > 	this_frame->prev is nullptr.
> > 	(get_prev_frame_id_by_id): Delete.
> > 	* frame.h (get_prev_frame_id_by_id): Delete declaration.
> > 	* valops.c (value_assign): Remove use of VALUE_FRAME_ID.
> > 	* vaule.c (value_fetch_lazy_register): Likewise.
> > 	* value.h (VALUE_FRAME_ID): Delete.
> > 
> > gdb/testsuite/ChangeLog:
> > 	* gdb.base/premature-dummy-frame-removal.exp: Repeat test with
> > 	'set debug frame on' in effect.
> > 
> > Thanks,
> > Andrew
> 
> The patch below does LGTM, thanks.

Thanks,

I pushed this just to master for now.

Andrew

^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv4] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-07-20  9:10   ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
                       ` (2 preceding siblings ...)
  2021-07-20 21:59     ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Simon Marchi
@ 2021-07-27 10:10     ` Andrew Burgess
  2021-08-09 15:41       ` [PATCHv5] " Andrew Burgess
  3 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-07-27 10:10 UTC (permalink / raw)
  To: gdb-patches

As I have now pushed patch #2 from the v3 series I am now left with
just this one patch.

The implementation here is still the same approach as was taken in v3,
my understanding is that Pedro would like to comment on this patch.

All feedback welcome.

Thanks,
Andrew

---

I ran into this assertion while GDB was trying to unwind the stack:

  gdb/inline-frame.c:173: internal-error: void inline_frame_this_id(frame_info*, void**, frame_id*): Assertion `frame_id_p (*this_id)' failed.

That is, when building the frame_id for an inline frame, GDB asks for
the frame_id of the previous frame.  Unfortunately, no valid frame_id
was returned for the previous frame, and so the assertion triggers.

What is happening is this, I had a stack that looked something like
this (the arrows '->' point from caller to callee):

  normal_frame -> inline_frame

However, for whatever reason (e.g. broken debug information, or
corrupted stack contents in the inferior), when GDB tries to unwind
"normal_frame", it ends up getting back effectively the same frame,
thus the call stack looks like this to GDB:

  .-> normal_frame -> inline_frame
  |     |
  '-----'

Given such a situation we would expect GDB to terminate the stack with
an error like this:

  Backtrace stopped: previous frame identical to this frame (corrupt stack?)

However, the inline_frame causes a problem, and here's why:

When unwinding we start from the sentinel frame and call
get_prev_frame.  We eventually end up in get_prev_frame_if_no_cycle,
in here we create a raw frame, and as this is frame #0 we immediately
return.

However, eventually we will try to unwind the stack further.  When we
do this we inevitably needing to know the frame_id for frame #0, and
so, eventually, we end up in compute_frame_id.

In compute_frame_id we first find the right unwinder for this frame,
in our case (i.e. for inline_frame) the $pc is within the function
normal_frame, but also within a block associated with the inlined
function inline_frame, as such the inline frame unwinder claims this
frame.

Back in compute_frame_id we next compute the frame_id, for our
inline_frame this means a call to inline_frame_this_id.

The ID of an inline frame is based on the id of the previous frame, so
from inline_frame_this_id we call get_prev_frame_always, this
eventually calls get_prev_frame_if_no_cycle again, which creates
another raw frame and calls compute_frame_id (for frames other than
frame 0 we immediately compute the frame_id).

In compute_frame_id we again identify the correct unwinder for this
frame.  Our $pc is unchanged, however, the fact that the next frame is
of type INLINE_FRAME prevents the inline frame unwinder from claiming
this frame again, and so, the standard DWARF frame unwinder claims
normal_frame.

We return to compute_frame_id and call the standard DWARF function to
build the frame_id for normal_frame.

With the frame_id of normal_frame figured out we return to
compute_frame_id, and then to get_prev_frame_if_no_cycle, where we add
the ID for normal_frame into the frame_id cache, and return the frame
back to inline_frame_this_id.

From inline_frame_this_id we build a frame_id for inline_frame and
return to compute_frame_id, and then to get_prev_frame_if_no_cycle,
which adds the frame_id for inline_frame into the frame_id cache.

So far, so good.

However, as we are trying to unwind the complete stack, we eventually
ask for the previous frame of normal_frame, remember, at this point
GDB doesn't know the stack is corrupted (with a cycle), GDB still
needs to figure that out.

So, we eventually end up in get_prev_frame_if_no_cycle where we create
a raw frame and call compute_frame_id, remember, this is for the frame
before normal_frame.

The first task for compute_frame_id is to find the unwinder for this
frame, so all of the frame sniffers are tried in order, this includes
the inline frame sniffer.

The inline frame sniffer asks for the $pc, this request is sent up the
stack to normal_frame, which, due to its cyclic behaviour, tells GDB
that the $pc in the previous frame was the same as the $pc in
normal_frame.

GDB spots that this $pc corresponds to both the function normal_frame
and also the inline function inline_frame.  As the next frame is not
an INLINE_FRAME then GDB figures that we have not yet built a frame to
cover inline_frame, and so the inline sniffer claims this new frame.
Our stack is now looking like this:

  inline_frame -> normal_frame -> inline_frame

But, we have not yet computed the frame id for the outer most (on the
left) inline_frame.  After the frame sniffer has claimed the inline
frame GDB returns to compute_frame_id and calls inline_frame_this_id.

In here GDB calls get_prev_frame_always, which eventually ends up
in get_prev_frame_if_no_cycle again, where we create a raw frame and
call compute_frame_id.

Just like before, compute_frame_id tries to find an unwinder for this
new frame, it sees that the $pc is within both normal_frame and
inline_frame, but the next frame is, again, an INLINE_FRAME, so, just
like before the standard DWARF unwinder claims this frame.  Back in
compute_frame_id we again call the standard DWARF function to build
the frame_id for this new copy of normal_frame.

At this point the stack looks like this:

  normal_frame -> inline_frame -> normal_frame -> inline_frame

After compute_frame_id we return to get_prev_frame_if_no_cycle, where
we try to add the frame_id for the new normal_frame into the frame_id
cache, however, unlike before, we fail to add this frame_id as it is
a duplicate of the previous normal_frame frame_id.  Having found a
duplicate get_prev_frame_if_no_cycle unlinks the new frame from the
stack, and returns nullptr, the stack now looks like this:

  inline_frame -> normal_frame -> inline_frame

The nullptr result from get_prev_frame_if_no_cycle is fed back to
inline_frame_this_id, which forwards this to get_frame_id, which
immediately returns null_frame_id.  As null_frame_id is not considered
a valid frame_id, this is what triggers the assertion.

In summary then:

 - inline_frame_this_id currently assumes that as the inline frame
   exists, we will always get a valid frame back from
   get_prev_frame_always,

 - get_prev_frame_if_no_cycle currently assumes that it is safe to
   return nullptr when it sees a cycle.

Notice that in frame.c:compute_frame_id, this code:

  fi->this_id.value = outer_frame_id;
  fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
  gdb_assert (frame_id_p (fi->this_id.value));

The assertion makes it clear that the this_id function must always
return a valid frame_id (e.g. null_frame_id is not a valid return
value), and similarly in inline_frame.c:inline_frame_this_id this
code:

  *this_id = get_frame_id (get_prev_frame_always (this_frame));
  /* snip comment */
  gdb_assert (frame_id_p (*this_id));

Makes it clear that every inline frame expects to be able to get a
previous frame, which will have a valid frame_id.

As I have discussed above, these assumptions don't currently hold in
all cases.

One possibility would be to move the call to get_prev_frame_always
forward from inline_frame_this_id to inline_frame_sniffer, however,
this falls foul of (in frame.c:frame_cleanup_after_sniffer) this
assertion:

  /* No sniffer should extend the frame chain; sniff based on what is
     already certain.  */
  gdb_assert (!frame->prev_p);

This assert prohibits any sniffer from trying to get the previous
frame, as getting the previous frame is likely to depend on the next
frame, I can understand why this assertion is a good thing, and I'm in
no rush to alter this rule.

In a previous version of this patch:

  https://sourceware.org/pipermail/gdb-patches/2021-June/180208.html

I proposed adding a special case to get_prev_frame_if_no_cycle, such
that, if we find a cycle, and we know we are fetching the previous
frame as a result of computing the frame_id for the next frame, which
is an INLINE_FRAME, then, instead of returning nullptr, do still
return the frame.

The idea here was to make adding the "normal_frame -> inline_frame ->"
to the frame list more of an atomic(-ish) operation, we would defer
removing normal_frame if we know is needed to support inline_frame,
and inline_frame will disconnect both if appropriate.

I discussed this approach on IRC, and there was some push back.  Simon
proposed an alternative approach:

  https://sourceware.org/pipermail/gdb-patches/2021-July/180663.html

This alternative approach is what I have implemented here.

Under this approach, in inline_frame_this_id, GDB spots when the call
to get_prev_frame_always returns nullptr.  This nullptr indicates that
we failed to get the previous frame for some reason.

We can then call get_frame_unwind_stop_reason on the current frame.
The stop reason will have been updated to indicate why we couldn't
find a previous frame.

In the specific case that the unwind stop reason is UNWIND_SAME_ID
then we know that the normal_frame had a duplicate frame-id.  For this
case we throw a new exception type (INLINE_FRAME_ID_SAME_ID_ERROR).
For any other stop reason we throw an existing more generic
error (NOT_FOUND_ERROR) to indicate the frame-id (of the inline frame)
was not found.

The other part is to catch INLINE_FRAME_ID_SAME_ID_ERROR in
get_prev_frame_if_no_cycle.  We can then push the UNWIND_SAME_ID stop
reason up the frame stack.
---
 gdb/frame.c                                   |  32 ++++
 gdb/inline-frame.c                            |  31 +++-
 .../gdb.base/inline-frame-cycle-unwind.c      |  58 +++++++
 .../gdb.base/inline-frame-cycle-unwind.exp    | 145 ++++++++++++++++++
 .../gdb.base/inline-frame-cycle-unwind.py     |  85 ++++++++++
 gdbsupport/common-exceptions.h                |   5 +
 6 files changed, 355 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py

diff --git a/gdb/frame.c b/gdb/frame.c
index 2332418f347..45a34badec6 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2091,6 +2091,38 @@ get_prev_frame_if_no_cycle (struct frame_info *this_frame)
 	  this_frame->prev = NULL;
 	}
 
+      if (ex.error == INLINE_FRAME_ID_SAME_ID_ERROR)
+	{
+	  /* This exception is a special case.  Imagine this situation:
+
+	     normal_frame -> inline_frame -> normal_frame -> inline_frame
+
+	     where both normal_frames have the same frame-id, and both
+	     inline_frame's therefore also have the same frame-id.
+
+	     When trying to compute the frame-id of the outer most (on the
+	     left) inline_frame we first ask for its previous frame, this
+	     is the outer most normal_frame.  As this normal_frame is a
+	     duplicate then the inline_frame is returned nullptr instead of
+	     an actual frame_info pointer.
+
+	     When we spot this situation (while calculating the
+	     inline_frame's frame-id) we throw
+	     INLINE_FRAME_ID_SAME_ID_ERROR, which we catch here.
+
+	     We can then set the stop reason to UNWIND_SAME_ID for
+	     THIS_FRAME and return nullptr.  */
+
+	  frame_debug_printf ("  -> nullptr // this frame has same ID");
+
+	  this_frame->stop_reason = UNWIND_SAME_ID;
+
+	  /* THIS_FRAME should already have been unlinked above.  */
+	  gdb_assert (get_frame_cache_generation () == entry_generation);
+
+	  return nullptr;
+	}
+
       throw;
     }
 
diff --git a/gdb/inline-frame.c b/gdb/inline-frame.c
index c98af1842a6..33ba7bf6f92 100644
--- a/gdb/inline-frame.c
+++ b/gdb/inline-frame.c
@@ -163,7 +163,36 @@ inline_frame_this_id (struct frame_info *this_frame,
      function, there must be previous frames, so this is safe - as
      long as we're careful not to create any cycles.  See related
      comments in get_prev_frame_always_1.  */
-  *this_id = get_frame_id (get_prev_frame_always (this_frame));
+  frame_info *prev_frame = get_prev_frame_always (this_frame);
+  if (prev_frame == nullptr)
+    {
+      /* Failure to find a previous frame could happen for any number of
+	 reasons, however, the stop reason on THIS_FRAME will be valid at
+	 this point.
+
+	 If trying to find the previous frame set the stop reason to
+	 UNWIND_SAME_ID then this indicates that the "normal" frame in
+	 which this frame is inline is a duplicate, and therefore this
+	 inline frame is also a duplicate.
+
+	 To communicate this back to the frame.c code we throw a specific
+	 exception here (INLINE_FRAME_ID_SAME_ID_ERROR) which is caught and
+	 handled in frame.c.
+
+	 If the stop reason is anything other than UNWIND_SAME_ID then this
+	 is some other, unexpected, error, and we throw a generic error.  */
+      if (get_frame_unwind_stop_reason (this_frame) == UNWIND_SAME_ID)
+	throw_error (INLINE_FRAME_ID_SAME_ID_ERROR,
+		     "Failed to find the frame previous to an inline frame, "
+		     "the previous frame had stop reason UNWIND_SAME_ID");
+      else
+	throw_error (NOT_FOUND_ERROR,
+		     "Failed to find the frame previous to an inline frame, "
+		     "the previous frame had stop reason %s",
+		     frame_stop_reason_string (this_frame));
+    }
+
+  *this_id = get_frame_id (prev_frame);
 
   /* We need a valid frame ID, so we need to be based on a valid
      frame.  FSF submission NOTE: this would be a good assertion to
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
new file mode 100644
index 00000000000..183c40928b6
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+static void inline_func (void);
+static void normal_func (void);
+
+volatile int global_var;
+volatile int level_counter;
+
+static void __attribute__((noinline))
+normal_func (void)
+{
+  /* Do some work.  */
+  ++global_var;
+
+  /* Now the inline function.  */
+  --level_counter;
+  inline_func ();
+  ++level_counter;
+
+  /* Do some work.  */
+  ++global_var;
+}
+
+static inline void __attribute__((__always_inline__))
+inline_func (void)
+{
+  if (level_counter > 1)
+    {
+      --level_counter;
+      normal_func ();
+      ++level_counter;
+    }
+  else
+    ++global_var;	/* Break here.  */
+}
+
+int
+main ()
+{
+  level_counter = 6;
+  normal_func ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
new file mode 100644
index 00000000000..2801b683a03
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
@@ -0,0 +1,145 @@
+# Copyright (C) 2021 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 checks for an edge case when unwinding inline frames which
+# occur towards the older end of the stack when the stack ends with a
+# cycle.  Consider this well formed stack:
+#
+#   main -> normal_frame -> inline_frame
+#
+# Now consider that, for whatever reason, the stack unwinding of
+# "normal_frame" becomes corrupted, such that the stack appears to be
+# this:
+#
+#   .-> normal_frame -> inline_frame
+#   |      |
+#   '------'
+#
+# When confronted with such a situation we would expect GDB to detect
+# the stack frame cycle and terminate the backtrace at the first
+# instance of "normal_frame" with a message:
+#
+#   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
+#
+# However, at one point there was a bug in GDB's inline frame
+# mechanism such that the fact that "inline_frame" was inlined into
+# "normal_frame" would cause GDB to trigger an assertion.
+#
+# This text makes use of a Python unwinder which can fake the cyclic
+# stack cycle, further the test sets up multiple levels of normal and
+# inline frames.  At the point of testing the stack looks like this:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Where "normal_func" is a normal frame, and "inline_func" is an inline frame.
+#
+# The python unwinder is then used to force a stack cycle at each
+# "normal_func" frame in turn, we then check that GDB can successfully unwind
+# the stack.
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}]} {
+    return -1
+}
+
+# Skip this test if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to the breakpoint where we will carry out the test.
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# Load the script containing the unwinder, this must be done at the
+# testing point as the script will examine the stack as it is loaded.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Check the unbroken stack.
+gdb_test_sequence "bt" "backtrace when the unwind is left unbroken" {
+    "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
+}
+
+with_test_prefix "cycle at level 5" {
+    # Arrange to introduce a stack cycle at frame 5.
+    gdb_test_no_output "python stop_at_level=5"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 5" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#4 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#5 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 3" {
+    # Arrange to introduce a stack cycle at frame 3.
+    gdb_test_no_output "python stop_at_level=3"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 3" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 1" {
+    # Arrange to introduce a stack cycle at frame 1.
+    gdb_test_no_output "python stop_at_level=1"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 1" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+# Flush the register cache (which also flushes the frame cache) so we
+# get a full backtrace again, then switch on frame debugging and try
+# to back trace.  At one point this triggered an assertion.
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_no_output "set debug frame 1"
+gdb_test_multiple "bt" "backtrace with debugging on" {
+    -re "^$gdb_prompt $" {
+	pass $gdb_test_name
+    }
+    -re "\[^\r\n\]+\r\n" {
+	exp_continue
+    }
+}
+gdb_test "p 1 + 2 + 3" " = 6" \
+    "ensure GDB is still alive"
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
new file mode 100644
index 00000000000..99c571f973c
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+# Set this to the stack level the backtrace should be corrupted at.
+# This will only work for frame 1, 3, or 5 in the test this unwinder
+# was written for.
+stop_at_level = None
+
+# Set this to the stack frame size of frames 1, 3, and 5.  These
+# frames will all have the same stack frame size as they are the same
+# function called recursively.
+stack_adjust = None
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "stop at level")
+
+    def __call__(self, pending_frame):
+        global stop_at_level
+        global stack_adjust
+
+        if stop_at_level is None or pending_frame.level() != stop_at_level:
+            return None
+
+        if stack_adjust is None:
+            raise gdb.GdbError("invalid stack_adjust")
+
+        if not stop_at_level in [1, 3, 5]:
+            raise gdb.GdbError("invalid stop_at_level")
+
+        sp_desc = pending_frame.architecture().registers().find("sp")
+        sp = pending_frame.read_register(sp_desc) + stack_adjust
+        pc = (gdb.lookup_symbol("normal_func"))[0].value().address
+        unwinder = pending_frame.create_unwind_info(FrameId(sp, pc))
+
+        for reg in pending_frame.architecture().registers("general"):
+            val = pending_frame.read_register(reg)
+            unwinder.add_saved_register(reg, val)
+        return unwinder
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
+
+# When loaded, it is expected that the stack looks like:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Compute the stack frame size of normal_func, which has inline_func
+# inlined within it.
+f0 = gdb.newest_frame()
+f1 = f0.older()
+f2 = f1.older()
+f0_sp = f0.read_register("sp")
+f2_sp = f2.read_register("sp")
+stack_adjust = f2_sp - f0_sp
diff --git a/gdbsupport/common-exceptions.h b/gdbsupport/common-exceptions.h
index 92f43d267ad..cff2ad9bd85 100644
--- a/gdbsupport/common-exceptions.h
+++ b/gdbsupport/common-exceptions.h
@@ -106,6 +106,11 @@ enum errors {
      "_ERROR" is appended to the name.  */
   MAX_COMPLETIONS_REACHED_ERROR,
 
+  /* An error to throw when computing the frame-id of an inline frame, in
+     the situation where we can't get the frame-id of the previous frame
+     due to it being a duplicate.  */
+  INLINE_FRAME_ID_SAME_ID_ERROR,
+
   /* Add more errors here.  */
   NR_ERRORS
 };
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv5] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-07-27 10:10     ` [PATCHv4] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
@ 2021-08-09 15:41       ` Andrew Burgess
  2021-08-23  9:41         ` Andrew Burgess
                           ` (2 more replies)
  0 siblings, 3 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-08-09 15:41 UTC (permalink / raw)
  To: gdb-patches

I've not heard anything from Pedro, but I'd like to move this patch
forward.

My assumption is that Pedro doesn't like using exceptions to pass
around information for a case that maybe isn't that exceptional.  As
Pedro was happy with v2, this new patch removes the use of exceptions.

Simon's concerns with v1/v2 were, I think, summarised as:

  1. What get_prev_frame_if_no_cycle does no longer matches the
     function name, and

  2. get_prev_frame_if_no_cycle was trying to figure out the callers
     intention in order to change its behaviour.

To try and address these two things I have:

  1. Renamed get_prev_frame_if_no_cycle to hopefully make it clearer
     that its result is not so clear cut, and

  2. Changed the behaviour of get_prev_frame_if_no_cycle based on a
     passed in parameter instead of "peeking" at various bits of state.

My hope is that this might be more acceptable to everyone, but I'd
love to hear any thoughts,

Thanks,
Andrew

---

I ran into this assertion while GDB was trying to unwind the stack:

  gdb/inline-frame.c:173: internal-error: void inline_frame_this_id(frame_info*, void**, frame_id*): Assertion `frame_id_p (*this_id)' failed.

That is, when building the frame_id for an inline frame, GDB asks for
the frame_id of the previous frame.  Unfortunately, no valid frame_id
was returned for the previous frame, and so the assertion triggers.

What is happening is this, I had a stack that looked something like
this (the arrows '->' point from caller to callee):

  normal_frame -> inline_frame

However, for whatever reason (e.g. broken debug information, or
corrupted stack contents in the inferior), when GDB tries to unwind
"normal_frame", it ends up getting back effectively the same frame,
thus the call stack looks like this to GDB:

  .-> normal_frame -> inline_frame
  |     |
  '-----'

Given such a situation we would expect GDB to terminate the stack with
an error like this:

  Backtrace stopped: previous frame identical to this frame (corrupt stack?)

However, the inline_frame causes a problem, and here's why:

When unwinding we start from the sentinel frame and call
get_prev_frame.  We eventually end up in get_prev_frame_if_no_cycle,
in here we create a raw frame, and as this is frame #0 we immediately
return.

However, eventually we will try to unwind the stack further.  When we
do this we inevitably needing to know the frame_id for frame #0, and
so, eventually, we end up in compute_frame_id.

In compute_frame_id we first find the right unwinder for this frame,
in our case (i.e. for inline_frame) the $pc is within the function
normal_frame, but also within a block associated with the inlined
function inline_frame, as such the inline frame unwinder claims this
frame.

Back in compute_frame_id we next compute the frame_id, for our
inline_frame this means a call to inline_frame_this_id.

The ID of an inline frame is based on the id of the previous frame, so
from inline_frame_this_id we call get_prev_frame_always, this
eventually calls get_prev_frame_if_no_cycle again, which creates
another raw frame and calls compute_frame_id (for frames other than
frame 0 we immediately compute the frame_id).

In compute_frame_id we again identify the correct unwinder for this
frame.  Our $pc is unchanged, however, the fact that the next frame is
of type INLINE_FRAME prevents the inline frame unwinder from claiming
this frame again, and so, the standard DWARF frame unwinder claims
normal_frame.

We return to compute_frame_id and call the standard DWARF function to
build the frame_id for normal_frame.

With the frame_id of normal_frame figured out we return to
compute_frame_id, and then to get_prev_frame_if_no_cycle, where we add
the ID for normal_frame into the frame_id cache, and return the frame
back to inline_frame_this_id.

From inline_frame_this_id we build a frame_id for inline_frame and
return to compute_frame_id, and then to get_prev_frame_if_no_cycle,
which adds the frame_id for inline_frame into the frame_id cache.

So far, so good.

However, as we are trying to unwind the complete stack, we eventually
ask for the previous frame of normal_frame, remember, at this point
GDB doesn't know the stack is corrupted (with a cycle), GDB still
needs to figure that out.

So, we eventually end up in get_prev_frame_if_no_cycle where we create
a raw frame and call compute_frame_id, remember, this is for the frame
before normal_frame.

The first task for compute_frame_id is to find the unwinder for this
frame, so all of the frame sniffers are tried in order, this includes
the inline frame sniffer.

The inline frame sniffer asks for the $pc, this request is sent up the
stack to normal_frame, which, due to its cyclic behaviour, tells GDB
that the $pc in the previous frame was the same as the $pc in
normal_frame.

GDB spots that this $pc corresponds to both the function normal_frame
and also the inline function inline_frame.  As the next frame is not
an INLINE_FRAME then GDB figures that we have not yet built a frame to
cover inline_frame, and so the inline sniffer claims this new frame.
Our stack is now looking like this:

  inline_frame -> normal_frame -> inline_frame

But, we have not yet computed the frame id for the outer most (on the
left) inline_frame.  After the frame sniffer has claimed the inline
frame GDB returns to compute_frame_id and calls inline_frame_this_id.

In here GDB calls get_prev_frame_always, which eventually ends up
in get_prev_frame_if_no_cycle again, where we create a raw frame and
call compute_frame_id.

Just like before, compute_frame_id tries to find an unwinder for this
new frame, it sees that the $pc is within both normal_frame and
inline_frame, but the next frame is, again, an INLINE_FRAME, so, just
like before the standard DWARF unwinder claims this frame.  Back in
compute_frame_id we again call the standard DWARF function to build
the frame_id for this new copy of normal_frame.

At this point the stack looks like this:

  normal_frame -> inline_frame -> normal_frame -> inline_frame

After compute_frame_id we return to get_prev_frame_if_no_cycle, where
we try to add the frame_id for the new normal_frame into the frame_id
cache, however, unlike before, we fail to add this frame_id as it is
a duplicate of the previous normal_frame frame_id.  Having found a
duplicate get_prev_frame_if_no_cycle unlinks the new frame from the
stack, and returns nullptr, the stack now looks like this:

  inline_frame -> normal_frame -> inline_frame

The nullptr result from get_prev_frame_if_no_cycle is fed back to
inline_frame_this_id, which forwards this to get_frame_id, which
immediately returns null_frame_id.  As null_frame_id is not considered
a valid frame_id, this is what triggers the assertion.

In summary then:

 - inline_frame_this_id currently assumes that as the inline frame
   exists, we will always get a valid frame back from
   get_prev_frame_always,

 - get_prev_frame_if_no_cycle currently assumes that it is safe to
   return nullptr when it sees a cycle.

Notice that in frame.c:compute_frame_id, this code:

  fi->this_id.value = outer_frame_id;
  fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
  gdb_assert (frame_id_p (fi->this_id.value));

The assertion makes it clear that the this_id function must always
return a valid frame_id (e.g. null_frame_id is not a valid return
value), and similarly in inline_frame.c:inline_frame_this_id this
code:

  *this_id = get_frame_id (get_prev_frame_always (this_frame));
  /* snip comment */
  gdb_assert (frame_id_p (*this_id));

Makes it clear that every inline frame expects to be able to get a
previous frame, which will have a valid frame_id.

As I have discussed above, these assumptions don't currently hold in
all cases.

One possibility would be to move the call to get_prev_frame_always
forward from inline_frame_this_id to inline_frame_sniffer, however,
this falls foul of (in frame.c:frame_cleanup_after_sniffer) this
assertion:

  /* No sniffer should extend the frame chain; sniff based on what is
     already certain.  */
  gdb_assert (!frame->prev_p);

This assert prohibits any sniffer from trying to get the previous
frame, as getting the previous frame is likely to depend on the next
frame, I can understand why this assertion is a good thing, and I'm in
no rush to alter this rule.

In a previous version of this patch:

  https://sourceware.org/pipermail/gdb-patches/2021-June/180208.html

I proposed adding a special case to get_prev_frame_if_no_cycle, such
that, if we find a cycle, and we know we are fetching the previous
frame as a result of computing the frame_id for the next frame, which
is an INLINE_FRAME, then, instead of returning nullptr, do still
return the frame.

The idea here was to make adding the "normal_frame -> inline_frame ->"
to the frame list more of an atomic(-ish) operation, we would defer
removing normal_frame if we know is needed to support inline_frame,
and inline_frame will disconnect both if appropriate.

This approach was liked by some:

  https://sourceware.org/pipermail/gdb-patches/2021-July/180651.html

But not by everyone:

  https://sourceware.org/pipermail/gdb-patches/2021-July/180663.html

Based on Simon's feedback, I then proposed an alternative patch:

  https://sourceware.org/pipermail/gdb-patches/2021-July/181029.html

With this approach, in inline_frame_this_id, GDB spots when the call
to get_prev_frame_always returns nullptr.  This nullptr indicates that
we failed to get the previous frame for some reason.

We can then call get_frame_unwind_stop_reason on the current frame.
The stop reason will have been updated to indicate why we couldn't
find a previous frame.

In the specific case that the unwind stop reason is UNWIND_SAME_ID
then we know that the normal_frame had a duplicate frame-id.  For this
case we throw a new exception type (INLINE_FRAME_ID_SAME_ID_ERROR).
For any other stop reason we throw an existing more generic
error (NOT_FOUND_ERROR) to indicate the frame-id (of the inline frame)
was not found.

The other part is to catch INLINE_FRAME_ID_SAME_ID_ERROR in
get_prev_frame_if_no_cycle.  We can then push the UNWIND_SAME_ID stop
reason up the frame stack.

Unfortunately, this approach wasn't liked either:

  https://sourceware.org/pipermail/gdb-patches/2021-July/181035.html

So now I'm proposing a new patch, which is closer to the first patch
again.

The concern with the first patch was that that changes I made to
get_prev_frame_if_no_cycle, broke the defined API, it was more, return
the previous frame if there's no cycle, except in some cases where we
do still want the previous frame.

And, this criticism about the function no longer doing what the name
suggests is fair, however, while is patch 2 I tried to solve this by
not changing how the function behaves, in this patch I just lean into
this, and rename the function to
get_prev_frame_with_optional_cycle_detection, now, I suggest, the
function does exactly what its name implies.

In this version I have taken a slightly different approach than in the
first patch, get_prev_frame_if_no_cycle (as it was called) is only
called from get_prev_frame_always_1 (twice), one of these calls
handles the inline frame case, while the other call handles all other
frame types.  I now pass a parameter through from each call site to
control the behaviour of the new
get_prev_frame_with_optional_cycle_detection, but the general idea is
the same as with patch 1.

When we ask for the previous frame of an inline frame we will not
reject a candidate frame just because it has a duplicate frame_id.  We
assume that if the previous frame has a duplicate frame_id then the
inline frame will also have a duplicate frame_id, and thus will be
rejected.
---
 gdb/frame.c                                   |  56 ++++++-
 gdb/inline-frame.c                            |   5 +-
 .../gdb.base/inline-frame-cycle-unwind.c      |  58 +++++++
 .../gdb.base/inline-frame-cycle-unwind.exp    | 145 ++++++++++++++++++
 .../gdb.base/inline-frame-cycle-unwind.py     |  85 ++++++++++
 5 files changed, 343 insertions(+), 6 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py

diff --git a/gdb/frame.c b/gdb/frame.c
index 4d7505f7ae3..397e3c664d6 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2044,13 +2044,54 @@ frame_register_unwind_location (struct frame_info *this_frame, int regnum,
    outermost, with UNWIND_SAME_ID stop reason.  Unlike the other
    validity tests, that compare THIS_FRAME and the next frame, we do
    this right after creating the previous frame, to avoid ever ending
-   up with two frames with the same id in the frame chain.  */
+   up with two frames with the same id in the frame chain.
+
+   There is however, one case where this cycle detection is not desirable,
+   when asking for the previous frame of an inline frame, in this case, if
+   the previous frame is a duplicate and we return nullptr then we will be
+   unable to calculate the frame_id of the inline frame, this in turn
+   causes inline_frame_this_id() to fail.  So for inline frames (and only
+   for inline frames) it is acceptable to pass CYCLE_DETECTION_P as false,
+   in that case the previous frame will always be returned, even when it
+   has a duplicate frame_id.  We're not worried about cycles in the frame
+   chain as, if the previous frame returned here has a duplicate frame_id,
+   then the frame_id of the inline frame, calculated based off the frame_id
+   of the previous frame, should also be a duplicate.  */
 
 static struct frame_info *
-get_prev_frame_if_no_cycle (struct frame_info *this_frame)
+get_prev_frame_with_optional_cycle_detection (struct frame_info *this_frame,
+					      bool cycle_detection_p)
 {
   struct frame_info *prev_frame;
 
+  /* This assert primarily checks that CYCLE_DETECTION_P is only false for
+     inline frames.  However, this assertion also makes some claims about
+     what the state of GDB should be when we enter this function and
+     THIS_FRAME is an inline frame.
+
+     If frame #0 is an inline frame then we put off calculating the
+     frame_id until we specifically make a call to get_frame_id().  As a
+     result we can enter this function in two possible states.  If GDB
+     asked for the previous frame of frame #0 then THIS_FRAME will be frame
+     #0 (an inline frame), and the frame_id will be in the NOT_COMPUTED
+     state.  However, if GDB asked for the frame_id of frame #0, then, as
+     getting the frame_id of an inline frame requires us to get the
+     frame_id of the previous frame, we will still end up in here, and the
+     frame_id status will be COMPUTING.
+
+     If we consider an inline frame at a level greater than #0 then things
+     are simpler.  For these frames we immediately compute the frame_id,
+     and so, for those frames, we will always reenter this function with
+     the frame_id status of COMPUTING.  */
+  gdb_assert (cycle_detection_p
+	      || (get_frame_type (this_frame) == INLINE_FRAME
+		  && ((this_frame->level > 0
+		       && (this_frame->this_id.p
+			   == frame_id_status::COMPUTING))
+		      || (this_frame->level == 0
+			  && (this_frame->this_id.p
+			      != frame_id_status::COMPUTED)))));
+
   prev_frame = get_prev_frame_raw (this_frame);
 
   /* Don't compute the frame id of the current frame yet.  Unwinding
@@ -2070,7 +2111,12 @@ get_prev_frame_if_no_cycle (struct frame_info *this_frame)
   try
     {
       compute_frame_id (prev_frame);
-      if (!frame_stash_add (prev_frame))
+
+      /* We must do the CYCLE_DETECTION_P check after attempting to add
+	 PREV_FRAME into the cache; if PREV_FRAME is unique then we do want
+	 it in the cache, but if it is a duplicate and CYCLE_DETECTION_P is
+	 false, then we don't want to unlink it.  */
+      if (!frame_stash_add (prev_frame) && cycle_detection_p)
 	{
 	  /* Another frame with the same id was already in the stash.  We just
 	     detected a cycle.  */
@@ -2147,7 +2193,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
      until we have unwound all the way down to the previous non-inline
      frame.  */
   if (get_frame_type (this_frame) == INLINE_FRAME)
-    return get_prev_frame_if_no_cycle (this_frame);
+    return get_prev_frame_with_optional_cycle_detection (this_frame, false);
 
   /* If this_frame is the current frame, then compute and stash its
      frame id prior to fetching and computing the frame id of the
@@ -2248,7 +2294,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
 	}
     }
 
-  return get_prev_frame_if_no_cycle (this_frame);
+  return get_prev_frame_with_optional_cycle_detection (this_frame, true);
 }
 
 /* Return a "struct frame_info" corresponding to the frame that called
diff --git a/gdb/inline-frame.c b/gdb/inline-frame.c
index c98af1842a6..8fb5eb05609 100644
--- a/gdb/inline-frame.c
+++ b/gdb/inline-frame.c
@@ -163,7 +163,10 @@ inline_frame_this_id (struct frame_info *this_frame,
      function, there must be previous frames, so this is safe - as
      long as we're careful not to create any cycles.  See related
      comments in get_prev_frame_always_1.  */
-  *this_id = get_frame_id (get_prev_frame_always (this_frame));
+  frame_info *prev_frame = get_prev_frame_always (this_frame);
+  if (prev_frame == nullptr)
+    error ("failed to find previous frame when computing inline frame id");
+  *this_id = get_frame_id (prev_frame);
 
   /* We need a valid frame ID, so we need to be based on a valid
      frame.  FSF submission NOTE: this would be a good assertion to
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
new file mode 100644
index 00000000000..183c40928b6
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+static void inline_func (void);
+static void normal_func (void);
+
+volatile int global_var;
+volatile int level_counter;
+
+static void __attribute__((noinline))
+normal_func (void)
+{
+  /* Do some work.  */
+  ++global_var;
+
+  /* Now the inline function.  */
+  --level_counter;
+  inline_func ();
+  ++level_counter;
+
+  /* Do some work.  */
+  ++global_var;
+}
+
+static inline void __attribute__((__always_inline__))
+inline_func (void)
+{
+  if (level_counter > 1)
+    {
+      --level_counter;
+      normal_func ();
+      ++level_counter;
+    }
+  else
+    ++global_var;	/* Break here.  */
+}
+
+int
+main ()
+{
+  level_counter = 6;
+  normal_func ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
new file mode 100644
index 00000000000..2801b683a03
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
@@ -0,0 +1,145 @@
+# Copyright (C) 2021 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 checks for an edge case when unwinding inline frames which
+# occur towards the older end of the stack when the stack ends with a
+# cycle.  Consider this well formed stack:
+#
+#   main -> normal_frame -> inline_frame
+#
+# Now consider that, for whatever reason, the stack unwinding of
+# "normal_frame" becomes corrupted, such that the stack appears to be
+# this:
+#
+#   .-> normal_frame -> inline_frame
+#   |      |
+#   '------'
+#
+# When confronted with such a situation we would expect GDB to detect
+# the stack frame cycle and terminate the backtrace at the first
+# instance of "normal_frame" with a message:
+#
+#   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
+#
+# However, at one point there was a bug in GDB's inline frame
+# mechanism such that the fact that "inline_frame" was inlined into
+# "normal_frame" would cause GDB to trigger an assertion.
+#
+# This text makes use of a Python unwinder which can fake the cyclic
+# stack cycle, further the test sets up multiple levels of normal and
+# inline frames.  At the point of testing the stack looks like this:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Where "normal_func" is a normal frame, and "inline_func" is an inline frame.
+#
+# The python unwinder is then used to force a stack cycle at each
+# "normal_func" frame in turn, we then check that GDB can successfully unwind
+# the stack.
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}]} {
+    return -1
+}
+
+# Skip this test if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to the breakpoint where we will carry out the test.
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# Load the script containing the unwinder, this must be done at the
+# testing point as the script will examine the stack as it is loaded.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Check the unbroken stack.
+gdb_test_sequence "bt" "backtrace when the unwind is left unbroken" {
+    "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
+}
+
+with_test_prefix "cycle at level 5" {
+    # Arrange to introduce a stack cycle at frame 5.
+    gdb_test_no_output "python stop_at_level=5"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 5" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#4 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#5 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 3" {
+    # Arrange to introduce a stack cycle at frame 3.
+    gdb_test_no_output "python stop_at_level=3"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 3" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 1" {
+    # Arrange to introduce a stack cycle at frame 1.
+    gdb_test_no_output "python stop_at_level=1"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 1" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+# Flush the register cache (which also flushes the frame cache) so we
+# get a full backtrace again, then switch on frame debugging and try
+# to back trace.  At one point this triggered an assertion.
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_no_output "set debug frame 1"
+gdb_test_multiple "bt" "backtrace with debugging on" {
+    -re "^$gdb_prompt $" {
+	pass $gdb_test_name
+    }
+    -re "\[^\r\n\]+\r\n" {
+	exp_continue
+    }
+}
+gdb_test "p 1 + 2 + 3" " = 6" \
+    "ensure GDB is still alive"
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
new file mode 100644
index 00000000000..99c571f973c
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+# Set this to the stack level the backtrace should be corrupted at.
+# This will only work for frame 1, 3, or 5 in the test this unwinder
+# was written for.
+stop_at_level = None
+
+# Set this to the stack frame size of frames 1, 3, and 5.  These
+# frames will all have the same stack frame size as they are the same
+# function called recursively.
+stack_adjust = None
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "stop at level")
+
+    def __call__(self, pending_frame):
+        global stop_at_level
+        global stack_adjust
+
+        if stop_at_level is None or pending_frame.level() != stop_at_level:
+            return None
+
+        if stack_adjust is None:
+            raise gdb.GdbError("invalid stack_adjust")
+
+        if not stop_at_level in [1, 3, 5]:
+            raise gdb.GdbError("invalid stop_at_level")
+
+        sp_desc = pending_frame.architecture().registers().find("sp")
+        sp = pending_frame.read_register(sp_desc) + stack_adjust
+        pc = (gdb.lookup_symbol("normal_func"))[0].value().address
+        unwinder = pending_frame.create_unwind_info(FrameId(sp, pc))
+
+        for reg in pending_frame.architecture().registers("general"):
+            val = pending_frame.read_register(reg)
+            unwinder.add_saved_register(reg, val)
+        return unwinder
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
+
+# When loaded, it is expected that the stack looks like:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Compute the stack frame size of normal_func, which has inline_func
+# inlined within it.
+f0 = gdb.newest_frame()
+f1 = f0.older()
+f2 = f1.older()
+f0_sp = f0.read_register("sp")
+f2_sp = f2.read_register("sp")
+stack_adjust = f2_sp - f0_sp
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv5] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-08-09 15:41       ` [PATCHv5] " Andrew Burgess
@ 2021-08-23  9:41         ` Andrew Burgess
  2021-08-23 10:26           ` Pedro Alves
  2021-09-20 12:24         ` Pedro Alves
  2021-09-21 13:54         ` [PATCHv6] " Andrew Burgess
  2 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-08-23  9:41 UTC (permalink / raw)
  To: gdb-patches

Ping!

Simon, what are your thoughts on this approach?  From what I recall of
our discussion on IRC your concerns with my original patch were that
after my change the function implementation no longer matched with the
function name, this made GDB's internal APIs confusing and
inconsistent.

With this patch I've tried to avoid this by renaming the functions to
hopefully make the API clearer.

I'm just guessing, but I suspect it was the use of exceptions that
Pedro wasn't happy with, so maybe if you're happy with this latest
patch we could merge this change, given Pedro already approved the
pre-exception patch.

Thanks,
Andrew


* Andrew Burgess <andrew.burgess@embecosm.com> [2021-08-09 16:41:22 +0100]:

> I've not heard anything from Pedro, but I'd like to move this patch
> forward.
> 
> My assumption is that Pedro doesn't like using exceptions to pass
> around information for a case that maybe isn't that exceptional.  As
> Pedro was happy with v2, this new patch removes the use of exceptions.
> 
> Simon's concerns with v1/v2 were, I think, summarised as:
> 
>   1. What get_prev_frame_if_no_cycle does no longer matches the
>      function name, and
> 
>   2. get_prev_frame_if_no_cycle was trying to figure out the callers
>      intention in order to change its behaviour.
> 
> To try and address these two things I have:
> 
>   1. Renamed get_prev_frame_if_no_cycle to hopefully make it clearer
>      that its result is not so clear cut, and
> 
>   2. Changed the behaviour of get_prev_frame_if_no_cycle based on a
>      passed in parameter instead of "peeking" at various bits of state.
> 
> My hope is that this might be more acceptable to everyone, but I'd
> love to hear any thoughts,
> 
> Thanks,
> Andrew
> 
> ---
> 
> I ran into this assertion while GDB was trying to unwind the stack:
> 
>   gdb/inline-frame.c:173: internal-error: void inline_frame_this_id(frame_info*, void**, frame_id*): Assertion `frame_id_p (*this_id)' failed.
> 
> That is, when building the frame_id for an inline frame, GDB asks for
> the frame_id of the previous frame.  Unfortunately, no valid frame_id
> was returned for the previous frame, and so the assertion triggers.
> 
> What is happening is this, I had a stack that looked something like
> this (the arrows '->' point from caller to callee):
> 
>   normal_frame -> inline_frame
> 
> However, for whatever reason (e.g. broken debug information, or
> corrupted stack contents in the inferior), when GDB tries to unwind
> "normal_frame", it ends up getting back effectively the same frame,
> thus the call stack looks like this to GDB:
> 
>   .-> normal_frame -> inline_frame
>   |     |
>   '-----'
> 
> Given such a situation we would expect GDB to terminate the stack with
> an error like this:
> 
>   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
> 
> However, the inline_frame causes a problem, and here's why:
> 
> When unwinding we start from the sentinel frame and call
> get_prev_frame.  We eventually end up in get_prev_frame_if_no_cycle,
> in here we create a raw frame, and as this is frame #0 we immediately
> return.
> 
> However, eventually we will try to unwind the stack further.  When we
> do this we inevitably needing to know the frame_id for frame #0, and
> so, eventually, we end up in compute_frame_id.
> 
> In compute_frame_id we first find the right unwinder for this frame,
> in our case (i.e. for inline_frame) the $pc is within the function
> normal_frame, but also within a block associated with the inlined
> function inline_frame, as such the inline frame unwinder claims this
> frame.
> 
> Back in compute_frame_id we next compute the frame_id, for our
> inline_frame this means a call to inline_frame_this_id.
> 
> The ID of an inline frame is based on the id of the previous frame, so
> from inline_frame_this_id we call get_prev_frame_always, this
> eventually calls get_prev_frame_if_no_cycle again, which creates
> another raw frame and calls compute_frame_id (for frames other than
> frame 0 we immediately compute the frame_id).
> 
> In compute_frame_id we again identify the correct unwinder for this
> frame.  Our $pc is unchanged, however, the fact that the next frame is
> of type INLINE_FRAME prevents the inline frame unwinder from claiming
> this frame again, and so, the standard DWARF frame unwinder claims
> normal_frame.
> 
> We return to compute_frame_id and call the standard DWARF function to
> build the frame_id for normal_frame.
> 
> With the frame_id of normal_frame figured out we return to
> compute_frame_id, and then to get_prev_frame_if_no_cycle, where we add
> the ID for normal_frame into the frame_id cache, and return the frame
> back to inline_frame_this_id.
> 
> From inline_frame_this_id we build a frame_id for inline_frame and
> return to compute_frame_id, and then to get_prev_frame_if_no_cycle,
> which adds the frame_id for inline_frame into the frame_id cache.
> 
> So far, so good.
> 
> However, as we are trying to unwind the complete stack, we eventually
> ask for the previous frame of normal_frame, remember, at this point
> GDB doesn't know the stack is corrupted (with a cycle), GDB still
> needs to figure that out.
> 
> So, we eventually end up in get_prev_frame_if_no_cycle where we create
> a raw frame and call compute_frame_id, remember, this is for the frame
> before normal_frame.
> 
> The first task for compute_frame_id is to find the unwinder for this
> frame, so all of the frame sniffers are tried in order, this includes
> the inline frame sniffer.
> 
> The inline frame sniffer asks for the $pc, this request is sent up the
> stack to normal_frame, which, due to its cyclic behaviour, tells GDB
> that the $pc in the previous frame was the same as the $pc in
> normal_frame.
> 
> GDB spots that this $pc corresponds to both the function normal_frame
> and also the inline function inline_frame.  As the next frame is not
> an INLINE_FRAME then GDB figures that we have not yet built a frame to
> cover inline_frame, and so the inline sniffer claims this new frame.
> Our stack is now looking like this:
> 
>   inline_frame -> normal_frame -> inline_frame
> 
> But, we have not yet computed the frame id for the outer most (on the
> left) inline_frame.  After the frame sniffer has claimed the inline
> frame GDB returns to compute_frame_id and calls inline_frame_this_id.
> 
> In here GDB calls get_prev_frame_always, which eventually ends up
> in get_prev_frame_if_no_cycle again, where we create a raw frame and
> call compute_frame_id.
> 
> Just like before, compute_frame_id tries to find an unwinder for this
> new frame, it sees that the $pc is within both normal_frame and
> inline_frame, but the next frame is, again, an INLINE_FRAME, so, just
> like before the standard DWARF unwinder claims this frame.  Back in
> compute_frame_id we again call the standard DWARF function to build
> the frame_id for this new copy of normal_frame.
> 
> At this point the stack looks like this:
> 
>   normal_frame -> inline_frame -> normal_frame -> inline_frame
> 
> After compute_frame_id we return to get_prev_frame_if_no_cycle, where
> we try to add the frame_id for the new normal_frame into the frame_id
> cache, however, unlike before, we fail to add this frame_id as it is
> a duplicate of the previous normal_frame frame_id.  Having found a
> duplicate get_prev_frame_if_no_cycle unlinks the new frame from the
> stack, and returns nullptr, the stack now looks like this:
> 
>   inline_frame -> normal_frame -> inline_frame
> 
> The nullptr result from get_prev_frame_if_no_cycle is fed back to
> inline_frame_this_id, which forwards this to get_frame_id, which
> immediately returns null_frame_id.  As null_frame_id is not considered
> a valid frame_id, this is what triggers the assertion.
> 
> In summary then:
> 
>  - inline_frame_this_id currently assumes that as the inline frame
>    exists, we will always get a valid frame back from
>    get_prev_frame_always,
> 
>  - get_prev_frame_if_no_cycle currently assumes that it is safe to
>    return nullptr when it sees a cycle.
> 
> Notice that in frame.c:compute_frame_id, this code:
> 
>   fi->this_id.value = outer_frame_id;
>   fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
>   gdb_assert (frame_id_p (fi->this_id.value));
> 
> The assertion makes it clear that the this_id function must always
> return a valid frame_id (e.g. null_frame_id is not a valid return
> value), and similarly in inline_frame.c:inline_frame_this_id this
> code:
> 
>   *this_id = get_frame_id (get_prev_frame_always (this_frame));
>   /* snip comment */
>   gdb_assert (frame_id_p (*this_id));
> 
> Makes it clear that every inline frame expects to be able to get a
> previous frame, which will have a valid frame_id.
> 
> As I have discussed above, these assumptions don't currently hold in
> all cases.
> 
> One possibility would be to move the call to get_prev_frame_always
> forward from inline_frame_this_id to inline_frame_sniffer, however,
> this falls foul of (in frame.c:frame_cleanup_after_sniffer) this
> assertion:
> 
>   /* No sniffer should extend the frame chain; sniff based on what is
>      already certain.  */
>   gdb_assert (!frame->prev_p);
> 
> This assert prohibits any sniffer from trying to get the previous
> frame, as getting the previous frame is likely to depend on the next
> frame, I can understand why this assertion is a good thing, and I'm in
> no rush to alter this rule.
> 
> In a previous version of this patch:
> 
>   https://sourceware.org/pipermail/gdb-patches/2021-June/180208.html
> 
> I proposed adding a special case to get_prev_frame_if_no_cycle, such
> that, if we find a cycle, and we know we are fetching the previous
> frame as a result of computing the frame_id for the next frame, which
> is an INLINE_FRAME, then, instead of returning nullptr, do still
> return the frame.
> 
> The idea here was to make adding the "normal_frame -> inline_frame ->"
> to the frame list more of an atomic(-ish) operation, we would defer
> removing normal_frame if we know is needed to support inline_frame,
> and inline_frame will disconnect both if appropriate.
> 
> This approach was liked by some:
> 
>   https://sourceware.org/pipermail/gdb-patches/2021-July/180651.html
> 
> But not by everyone:
> 
>   https://sourceware.org/pipermail/gdb-patches/2021-July/180663.html
> 
> Based on Simon's feedback, I then proposed an alternative patch:
> 
>   https://sourceware.org/pipermail/gdb-patches/2021-July/181029.html
> 
> With this approach, in inline_frame_this_id, GDB spots when the call
> to get_prev_frame_always returns nullptr.  This nullptr indicates that
> we failed to get the previous frame for some reason.
> 
> We can then call get_frame_unwind_stop_reason on the current frame.
> The stop reason will have been updated to indicate why we couldn't
> find a previous frame.
> 
> In the specific case that the unwind stop reason is UNWIND_SAME_ID
> then we know that the normal_frame had a duplicate frame-id.  For this
> case we throw a new exception type (INLINE_FRAME_ID_SAME_ID_ERROR).
> For any other stop reason we throw an existing more generic
> error (NOT_FOUND_ERROR) to indicate the frame-id (of the inline frame)
> was not found.
> 
> The other part is to catch INLINE_FRAME_ID_SAME_ID_ERROR in
> get_prev_frame_if_no_cycle.  We can then push the UNWIND_SAME_ID stop
> reason up the frame stack.
> 
> Unfortunately, this approach wasn't liked either:
> 
>   https://sourceware.org/pipermail/gdb-patches/2021-July/181035.html
> 
> So now I'm proposing a new patch, which is closer to the first patch
> again.
> 
> The concern with the first patch was that that changes I made to
> get_prev_frame_if_no_cycle, broke the defined API, it was more, return
> the previous frame if there's no cycle, except in some cases where we
> do still want the previous frame.
> 
> And, this criticism about the function no longer doing what the name
> suggests is fair, however, while is patch 2 I tried to solve this by
> not changing how the function behaves, in this patch I just lean into
> this, and rename the function to
> get_prev_frame_with_optional_cycle_detection, now, I suggest, the
> function does exactly what its name implies.
> 
> In this version I have taken a slightly different approach than in the
> first patch, get_prev_frame_if_no_cycle (as it was called) is only
> called from get_prev_frame_always_1 (twice), one of these calls
> handles the inline frame case, while the other call handles all other
> frame types.  I now pass a parameter through from each call site to
> control the behaviour of the new
> get_prev_frame_with_optional_cycle_detection, but the general idea is
> the same as with patch 1.
> 
> When we ask for the previous frame of an inline frame we will not
> reject a candidate frame just because it has a duplicate frame_id.  We
> assume that if the previous frame has a duplicate frame_id then the
> inline frame will also have a duplicate frame_id, and thus will be
> rejected.
> ---
>  gdb/frame.c                                   |  56 ++++++-
>  gdb/inline-frame.c                            |   5 +-
>  .../gdb.base/inline-frame-cycle-unwind.c      |  58 +++++++
>  .../gdb.base/inline-frame-cycle-unwind.exp    | 145 ++++++++++++++++++
>  .../gdb.base/inline-frame-cycle-unwind.py     |  85 ++++++++++
>  5 files changed, 343 insertions(+), 6 deletions(-)
>  create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
>  create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
>  create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
> 
> diff --git a/gdb/frame.c b/gdb/frame.c
> index 4d7505f7ae3..397e3c664d6 100644
> --- a/gdb/frame.c
> +++ b/gdb/frame.c
> @@ -2044,13 +2044,54 @@ frame_register_unwind_location (struct frame_info *this_frame, int regnum,
>     outermost, with UNWIND_SAME_ID stop reason.  Unlike the other
>     validity tests, that compare THIS_FRAME and the next frame, we do
>     this right after creating the previous frame, to avoid ever ending
> -   up with two frames with the same id in the frame chain.  */
> +   up with two frames with the same id in the frame chain.
> +
> +   There is however, one case where this cycle detection is not desirable,
> +   when asking for the previous frame of an inline frame, in this case, if
> +   the previous frame is a duplicate and we return nullptr then we will be
> +   unable to calculate the frame_id of the inline frame, this in turn
> +   causes inline_frame_this_id() to fail.  So for inline frames (and only
> +   for inline frames) it is acceptable to pass CYCLE_DETECTION_P as false,
> +   in that case the previous frame will always be returned, even when it
> +   has a duplicate frame_id.  We're not worried about cycles in the frame
> +   chain as, if the previous frame returned here has a duplicate frame_id,
> +   then the frame_id of the inline frame, calculated based off the frame_id
> +   of the previous frame, should also be a duplicate.  */
>  
>  static struct frame_info *
> -get_prev_frame_if_no_cycle (struct frame_info *this_frame)
> +get_prev_frame_with_optional_cycle_detection (struct frame_info *this_frame,
> +					      bool cycle_detection_p)
>  {
>    struct frame_info *prev_frame;
>  
> +  /* This assert primarily checks that CYCLE_DETECTION_P is only false for
> +     inline frames.  However, this assertion also makes some claims about
> +     what the state of GDB should be when we enter this function and
> +     THIS_FRAME is an inline frame.
> +
> +     If frame #0 is an inline frame then we put off calculating the
> +     frame_id until we specifically make a call to get_frame_id().  As a
> +     result we can enter this function in two possible states.  If GDB
> +     asked for the previous frame of frame #0 then THIS_FRAME will be frame
> +     #0 (an inline frame), and the frame_id will be in the NOT_COMPUTED
> +     state.  However, if GDB asked for the frame_id of frame #0, then, as
> +     getting the frame_id of an inline frame requires us to get the
> +     frame_id of the previous frame, we will still end up in here, and the
> +     frame_id status will be COMPUTING.
> +
> +     If we consider an inline frame at a level greater than #0 then things
> +     are simpler.  For these frames we immediately compute the frame_id,
> +     and so, for those frames, we will always reenter this function with
> +     the frame_id status of COMPUTING.  */
> +  gdb_assert (cycle_detection_p
> +	      || (get_frame_type (this_frame) == INLINE_FRAME
> +		  && ((this_frame->level > 0
> +		       && (this_frame->this_id.p
> +			   == frame_id_status::COMPUTING))
> +		      || (this_frame->level == 0
> +			  && (this_frame->this_id.p
> +			      != frame_id_status::COMPUTED)))));
> +
>    prev_frame = get_prev_frame_raw (this_frame);
>  
>    /* Don't compute the frame id of the current frame yet.  Unwinding
> @@ -2070,7 +2111,12 @@ get_prev_frame_if_no_cycle (struct frame_info *this_frame)
>    try
>      {
>        compute_frame_id (prev_frame);
> -      if (!frame_stash_add (prev_frame))
> +
> +      /* We must do the CYCLE_DETECTION_P check after attempting to add
> +	 PREV_FRAME into the cache; if PREV_FRAME is unique then we do want
> +	 it in the cache, but if it is a duplicate and CYCLE_DETECTION_P is
> +	 false, then we don't want to unlink it.  */
> +      if (!frame_stash_add (prev_frame) && cycle_detection_p)
>  	{
>  	  /* Another frame with the same id was already in the stash.  We just
>  	     detected a cycle.  */
> @@ -2147,7 +2193,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
>       until we have unwound all the way down to the previous non-inline
>       frame.  */
>    if (get_frame_type (this_frame) == INLINE_FRAME)
> -    return get_prev_frame_if_no_cycle (this_frame);
> +    return get_prev_frame_with_optional_cycle_detection (this_frame, false);
>  
>    /* If this_frame is the current frame, then compute and stash its
>       frame id prior to fetching and computing the frame id of the
> @@ -2248,7 +2294,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
>  	}
>      }
>  
> -  return get_prev_frame_if_no_cycle (this_frame);
> +  return get_prev_frame_with_optional_cycle_detection (this_frame, true);
>  }
>  
>  /* Return a "struct frame_info" corresponding to the frame that called
> diff --git a/gdb/inline-frame.c b/gdb/inline-frame.c
> index c98af1842a6..8fb5eb05609 100644
> --- a/gdb/inline-frame.c
> +++ b/gdb/inline-frame.c
> @@ -163,7 +163,10 @@ inline_frame_this_id (struct frame_info *this_frame,
>       function, there must be previous frames, so this is safe - as
>       long as we're careful not to create any cycles.  See related
>       comments in get_prev_frame_always_1.  */
> -  *this_id = get_frame_id (get_prev_frame_always (this_frame));
> +  frame_info *prev_frame = get_prev_frame_always (this_frame);
> +  if (prev_frame == nullptr)
> +    error ("failed to find previous frame when computing inline frame id");
> +  *this_id = get_frame_id (prev_frame);
>  
>    /* We need a valid frame ID, so we need to be based on a valid
>       frame.  FSF submission NOTE: this would be a good assertion to
> diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
> new file mode 100644
> index 00000000000..183c40928b6
> --- /dev/null
> +++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
> @@ -0,0 +1,58 @@
> +/* This testcase is part of GDB, the GNU debugger.
> +
> +   Copyright 2021 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/>.  */
> +
> +static void inline_func (void);
> +static void normal_func (void);
> +
> +volatile int global_var;
> +volatile int level_counter;
> +
> +static void __attribute__((noinline))
> +normal_func (void)
> +{
> +  /* Do some work.  */
> +  ++global_var;
> +
> +  /* Now the inline function.  */
> +  --level_counter;
> +  inline_func ();
> +  ++level_counter;
> +
> +  /* Do some work.  */
> +  ++global_var;
> +}
> +
> +static inline void __attribute__((__always_inline__))
> +inline_func (void)
> +{
> +  if (level_counter > 1)
> +    {
> +      --level_counter;
> +      normal_func ();
> +      ++level_counter;
> +    }
> +  else
> +    ++global_var;	/* Break here.  */
> +}
> +
> +int
> +main ()
> +{
> +  level_counter = 6;
> +  normal_func ();
> +  return 0;
> +}
> diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
> new file mode 100644
> index 00000000000..2801b683a03
> --- /dev/null
> +++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
> @@ -0,0 +1,145 @@
> +# Copyright (C) 2021 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 checks for an edge case when unwinding inline frames which
> +# occur towards the older end of the stack when the stack ends with a
> +# cycle.  Consider this well formed stack:
> +#
> +#   main -> normal_frame -> inline_frame
> +#
> +# Now consider that, for whatever reason, the stack unwinding of
> +# "normal_frame" becomes corrupted, such that the stack appears to be
> +# this:
> +#
> +#   .-> normal_frame -> inline_frame
> +#   |      |
> +#   '------'
> +#
> +# When confronted with such a situation we would expect GDB to detect
> +# the stack frame cycle and terminate the backtrace at the first
> +# instance of "normal_frame" with a message:
> +#
> +#   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
> +#
> +# However, at one point there was a bug in GDB's inline frame
> +# mechanism such that the fact that "inline_frame" was inlined into
> +# "normal_frame" would cause GDB to trigger an assertion.
> +#
> +# This text makes use of a Python unwinder which can fake the cyclic
> +# stack cycle, further the test sets up multiple levels of normal and
> +# inline frames.  At the point of testing the stack looks like this:
> +#
> +#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
> +#
> +# Where "normal_func" is a normal frame, and "inline_func" is an inline frame.
> +#
> +# The python unwinder is then used to force a stack cycle at each
> +# "normal_func" frame in turn, we then check that GDB can successfully unwind
> +# the stack.
> +
> +standard_testfile
> +
> +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}]} {
> +    return -1
> +}
> +
> +# Skip this test if Python scripting is not enabled.
> +if { [skip_python_tests] } { continue }
> +
> +if ![runto_main] then {
> +    fail "can't run to main"
> +    return 0
> +}
> +
> +set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
> +
> +# Run to the breakpoint where we will carry out the test.
> +gdb_breakpoint [gdb_get_line_number "Break here"]
> +gdb_continue_to_breakpoint "stop at test breakpoint"
> +
> +# Load the script containing the unwinder, this must be done at the
> +# testing point as the script will examine the stack as it is loaded.
> +gdb_test_no_output "source ${pyfile}"\
> +    "import python scripts"
> +
> +# Check the unbroken stack.
> +gdb_test_sequence "bt" "backtrace when the unwind is left unbroken" {
> +    "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
> +    "\\r\\n#1 \[^\r\n\]* normal_func \\(\\) at "
> +    "\\r\\n#2 \[^\r\n\]* inline_func \\(\\) at "
> +    "\\r\\n#3 \[^\r\n\]* normal_func \\(\\) at "
> +    "\\r\\n#4 \[^\r\n\]* inline_func \\(\\) at "
> +    "\\r\\n#5 \[^\r\n\]* normal_func \\(\\) at "
> +    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
> +}
> +
> +with_test_prefix "cycle at level 5" {
> +    # Arrange to introduce a stack cycle at frame 5.
> +    gdb_test_no_output "python stop_at_level=5"
> +    gdb_test "maint flush register-cache" \
> +	"Register cache flushed\\."
> +    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 5" \
> +	[multi_line \
> +	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
> +	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
> +	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
> +	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
> +	     "#4 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
> +	     "#5 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
> +	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
> +}
> +
> +with_test_prefix "cycle at level 3" {
> +    # Arrange to introduce a stack cycle at frame 3.
> +    gdb_test_no_output "python stop_at_level=3"
> +    gdb_test "maint flush register-cache" \
> +	"Register cache flushed\\."
> +    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 3" \
> +	[multi_line \
> +	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
> +	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
> +	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
> +	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
> +	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
> +}
> +
> +with_test_prefix "cycle at level 1" {
> +    # Arrange to introduce a stack cycle at frame 1.
> +    gdb_test_no_output "python stop_at_level=1"
> +    gdb_test "maint flush register-cache" \
> +	"Register cache flushed\\."
> +    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 1" \
> +	[multi_line \
> +	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
> +	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
> +	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
> +}
> +
> +# Flush the register cache (which also flushes the frame cache) so we
> +# get a full backtrace again, then switch on frame debugging and try
> +# to back trace.  At one point this triggered an assertion.
> +gdb_test "maint flush register-cache" \
> +    "Register cache flushed\\." ""
> +gdb_test_no_output "set debug frame 1"
> +gdb_test_multiple "bt" "backtrace with debugging on" {
> +    -re "^$gdb_prompt $" {
> +	pass $gdb_test_name
> +    }
> +    -re "\[^\r\n\]+\r\n" {
> +	exp_continue
> +    }
> +}
> +gdb_test "p 1 + 2 + 3" " = 6" \
> +    "ensure GDB is still alive"
> diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
> new file mode 100644
> index 00000000000..99c571f973c
> --- /dev/null
> +++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
> @@ -0,0 +1,85 @@
> +# Copyright (C) 2021 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/>.
> +
> +import gdb
> +from gdb.unwinder import Unwinder
> +
> +# Set this to the stack level the backtrace should be corrupted at.
> +# This will only work for frame 1, 3, or 5 in the test this unwinder
> +# was written for.
> +stop_at_level = None
> +
> +# Set this to the stack frame size of frames 1, 3, and 5.  These
> +# frames will all have the same stack frame size as they are the same
> +# function called recursively.
> +stack_adjust = None
> +
> +
> +class FrameId(object):
> +    def __init__(self, sp, pc):
> +        self._sp = sp
> +        self._pc = pc
> +
> +    @property
> +    def sp(self):
> +        return self._sp
> +
> +    @property
> +    def pc(self):
> +        return self._pc
> +
> +
> +class TestUnwinder(Unwinder):
> +    def __init__(self):
> +        Unwinder.__init__(self, "stop at level")
> +
> +    def __call__(self, pending_frame):
> +        global stop_at_level
> +        global stack_adjust
> +
> +        if stop_at_level is None or pending_frame.level() != stop_at_level:
> +            return None
> +
> +        if stack_adjust is None:
> +            raise gdb.GdbError("invalid stack_adjust")
> +
> +        if not stop_at_level in [1, 3, 5]:
> +            raise gdb.GdbError("invalid stop_at_level")
> +
> +        sp_desc = pending_frame.architecture().registers().find("sp")
> +        sp = pending_frame.read_register(sp_desc) + stack_adjust
> +        pc = (gdb.lookup_symbol("normal_func"))[0].value().address
> +        unwinder = pending_frame.create_unwind_info(FrameId(sp, pc))
> +
> +        for reg in pending_frame.architecture().registers("general"):
> +            val = pending_frame.read_register(reg)
> +            unwinder.add_saved_register(reg, val)
> +        return unwinder
> +
> +
> +gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
> +
> +# When loaded, it is expected that the stack looks like:
> +#
> +#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
> +#
> +# Compute the stack frame size of normal_func, which has inline_func
> +# inlined within it.
> +f0 = gdb.newest_frame()
> +f1 = f0.older()
> +f2 = f1.older()
> +f0_sp = f0.read_register("sp")
> +f2_sp = f2.read_register("sp")
> +stack_adjust = f2_sp - f0_sp
> -- 
> 2.25.4
> 

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv5] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-08-23  9:41         ` Andrew Burgess
@ 2021-08-23 10:26           ` Pedro Alves
  2021-08-23 12:31             ` Andrew Burgess
  0 siblings, 1 reply; 48+ messages in thread
From: Pedro Alves @ 2021-08-23 10:26 UTC (permalink / raw)
  To: Andrew Burgess, gdb-patches

Hi Andrew,

Sorry I hadn't responded yet.  I was out on vacation a few weeks ago, and thought I would be able to
get to this once I got back, but I got immediately pulled to a tight internal deadline instead.  FWIW, I've
been very frustrated about failing to reply to you.  I should be able to take a look this week, though.

On 2021-08-23 10:41 a.m., Andrew Burgess wrote:
> Ping!
> 
> Simon, what are your thoughts on this approach?  From what I recall of
> our discussion on IRC your concerns with my original patch were that
> after my change the function implementation no longer matched with the
> function name, this made GDB's internal APIs confusing and
> inconsistent.
> 
> With this patch I've tried to avoid this by renaming the functions to
> hopefully make the API clearer.
> 
> I'm just guessing, but I suspect it was the use of exceptions that
> Pedro wasn't happy with, so maybe if you're happy with this latest
> patch we could merge this change, given Pedro already approved the
> pre-exception patch.

I don't recall the discussion with Simon, but yes, use custom exceptions
for this doesn't sound great to me.  Let me take a better look.

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv5] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-08-23 10:26           ` Pedro Alves
@ 2021-08-23 12:31             ` Andrew Burgess
  2021-09-20 10:04               ` Andrew Burgess
  0 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-08-23 12:31 UTC (permalink / raw)
  To: Pedro Alves; +Cc: gdb-patches, Simon Marchi

* Pedro Alves <pedro@palves.net> [2021-08-23 11:26:49 +0100]:

> Hi Andrew,
> 
> Sorry I hadn't responded yet.  I was out on vacation a few weeks ago, and thought I would be able to
> get to this once I got back, but I got immediately pulled to a tight internal deadline instead.  FWIW, I've
> been very frustrated about failing to reply to you.  I should be able to take a look this week, though.
> 
> On 2021-08-23 10:41 a.m., Andrew Burgess wrote:
> > Ping!
> > 
> > Simon, what are your thoughts on this approach?  From what I recall of
> > our discussion on IRC your concerns with my original patch were that
> > after my change the function implementation no longer matched with the
> > function name, this made GDB's internal APIs confusing and
> > inconsistent.
> > 
> > With this patch I've tried to avoid this by renaming the functions to
> > hopefully make the API clearer.
> > 
> > I'm just guessing, but I suspect it was the use of exceptions that
> > Pedro wasn't happy with, so maybe if you're happy with this latest
> > patch we could merge this change, given Pedro already approved the
> > pre-exception patch.
> 
> I don't recall the discussion with Simon, but yes, use custom exceptions
> for this doesn't sound great to me.  Let me take a better look.

Thanks for taking the time to reply.  I completely understand other
pressures getting in the way of being as active on the m/l.  Even just
knowing for sure which part of the patch you didn't like is great
information as I can start coming up with alternatives.  So I'm always
grateful for any feedback, no matter how terse.

Thanks again,
Andrew

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv5] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-08-23 12:31             ` Andrew Burgess
@ 2021-09-20 10:04               ` Andrew Burgess
  0 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-09-20 10:04 UTC (permalink / raw)
  To: Simon Marchi; +Cc: gdb-patches, Pedro Alves

Simon,

I wonder if you had any thoughts on v5? As this patch is closer to v2
which Pedro was happy with, I'd really like to hear your opinions on
this version.

Thanks,
Andrew


* Andrew Burgess <andrew.burgess@embecosm.com> [2021-08-23 13:31:09 +0100]:

> * Pedro Alves <pedro@palves.net> [2021-08-23 11:26:49 +0100]:
> 
> > Hi Andrew,
> > 
> > Sorry I hadn't responded yet.  I was out on vacation a few weeks ago, and thought I would be able to
> > get to this once I got back, but I got immediately pulled to a tight internal deadline instead.  FWIW, I've
> > been very frustrated about failing to reply to you.  I should be able to take a look this week, though.
> > 
> > On 2021-08-23 10:41 a.m., Andrew Burgess wrote:
> > > Ping!
> > > 
> > > Simon, what are your thoughts on this approach?  From what I recall of
> > > our discussion on IRC your concerns with my original patch were that
> > > after my change the function implementation no longer matched with the
> > > function name, this made GDB's internal APIs confusing and
> > > inconsistent.
> > > 
> > > With this patch I've tried to avoid this by renaming the functions to
> > > hopefully make the API clearer.
> > > 
> > > I'm just guessing, but I suspect it was the use of exceptions that
> > > Pedro wasn't happy with, so maybe if you're happy with this latest
> > > patch we could merge this change, given Pedro already approved the
> > > pre-exception patch.
> > 
> > I don't recall the discussion with Simon, but yes, use custom exceptions
> > for this doesn't sound great to me.  Let me take a better look.
> 
> Thanks for taking the time to reply.  I completely understand other
> pressures getting in the way of being as active on the m/l.  Even just
> knowing for sure which part of the patch you didn't like is great
> information as I can start coming up with alternatives.  So I'm always
> grateful for any feedback, no matter how terse.
> 
> Thanks again,
> Andrew

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv5] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-08-09 15:41       ` [PATCHv5] " Andrew Burgess
  2021-08-23  9:41         ` Andrew Burgess
@ 2021-09-20 12:24         ` Pedro Alves
  2021-09-21 13:52           ` Andrew Burgess
  2021-09-21 13:54         ` [PATCHv6] " Andrew Burgess
  2 siblings, 1 reply; 48+ messages in thread
From: Pedro Alves @ 2021-09-20 12:24 UTC (permalink / raw)
  To: Andrew Burgess, gdb-patches

Hi Andrew,

On 2021-08-09 4:41 p.m., Andrew Burgess wrote:
> I've not heard anything from Pedro, but I'd like to move this patch
> forward.
> 
> My assumption is that Pedro doesn't like using exceptions to pass
> around information for a case that maybe isn't that exceptional.  As
> Pedro was happy with v2, this new patch removes the use of exceptions.
> 
> Simon's concerns with v1/v2 were, I think, summarised as:
> 
>   1. What get_prev_frame_if_no_cycle does no longer matches the
>      function name, and
> 
>   2. get_prev_frame_if_no_cycle was trying to figure out the callers
>      intention in order to change its behaviour.
> 
> To try and address these two things I have:
> 
>   1. Renamed get_prev_frame_if_no_cycle to hopefully make it clearer
>      that its result is not so clear cut, and
> 
>   2. Changed the behaviour of get_prev_frame_if_no_cycle based on a
>      passed in parameter instead of "peeking" at various bits of state.
> 
> My hope is that this might be more acceptable to everyone, but I'd
> love to hear any thoughts,
> 

To be honest, to me, the new "cycle_detection_p" argument seems pointless/redundant,
since inside the function, you have the INLINE_FRAME assertion checks anyhow.  I.e.,
putting:

  bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;

inside get_prev_frame_with_optional_cycle_detection yields the exact same, IIUC.

So adding the argument is just adding scope for someone passing the wrong 
cycle_detection_p argument at some point, it seems to me.

frame.c already has to know special things about inline frames, I mean,
even get_prev_frame_always_1 already must know to bypass a bunch of tests
for inline frames, like:

  /* If we are unwinding from an inline frame, all of the below tests
     were already performed when we unwound from the next non-inline
     frame.  We must skip them, since we can not get THIS_FRAME's ID
     until we have unwound all the way down to the previous non-inline
     frame.  */
  if (get_frame_type (this_frame) == INLINE_FRAME)
    return get_prev_frame_if_no_cycle (this_frame);


I'd remove the cycle_detection_p argument, replacing it said local:

  bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;

and rename get_prev_frame_if_no_cycle to, say, get_prev_frame_maybe_check_cycle.

Like, e.g.:

~~~~
diff --git a/gdb/frame.c b/gdb/frame.c
index 6433e9db788..2c9a324c93f 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2051,16 +2051,14 @@ frame_register_unwind_location (struct frame_info *this_frame, int regnum,
    the previous frame is a duplicate and we return nullptr then we will be
    unable to calculate the frame_id of the inline frame, this in turn
    causes inline_frame_this_id() to fail.  So for inline frames (and only
-   for inline frames) it is acceptable to pass CYCLE_DETECTION_P as false,
-   in that case the previous frame will always be returned, even when it
+   for inline frames), the previous frame will always be returned, even when it
    has a duplicate frame_id.  We're not worried about cycles in the frame
    chain as, if the previous frame returned here has a duplicate frame_id,
    then the frame_id of the inline frame, calculated based off the frame_id
    of the previous frame, should also be a duplicate.  */
 
 static struct frame_info *
-get_prev_frame_with_optional_cycle_detection (struct frame_info *this_frame,
-					      bool cycle_detection_p)
+get_prev_frame_maybe_check_cycle (struct frame_info *this_frame)
 {
   struct frame_info *prev_frame;
 
@@ -2083,6 +2081,9 @@ get_prev_frame_with_optional_cycle_detection (struct frame_info *this_frame,
      are simpler.  For these frames we immediately compute the frame_id,
      and so, for those frames, we will always reenter this function with
      the frame_id status of COMPUTING.  */
+
+  bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;
+
   gdb_assert (cycle_detection_p
 	      || (get_frame_type (this_frame) == INLINE_FRAME
 		  && ((this_frame->level > 0
@@ -2193,7 +2194,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
      until we have unwound all the way down to the previous non-inline
      frame.  */
   if (get_frame_type (this_frame) == INLINE_FRAME)
-    return get_prev_frame_with_optional_cycle_detection (this_frame, false);
+    return get_prev_frame_maybe_check_cycle (this_frame);
 
   /* If this_frame is the current frame, then compute and stash its
      frame id prior to fetching and computing the frame id of the
@@ -2294,7 +2295,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
 	}
     }
 
-  return get_prev_frame_with_optional_cycle_detection (this_frame, true);
+  return get_prev_frame_maybe_check_cycle (this_frame);
 }
 
 /* Return a "struct frame_info" corresponding to the frame that called
~~~~



> --- a/gdb/inline-frame.c
> +++ b/gdb/inline-frame.c
> @@ -163,7 +163,10 @@ inline_frame_this_id (struct frame_info *this_frame,
>       function, there must be previous frames, so this is safe - as
>       long as we're careful not to create any cycles.  See related
>       comments in get_prev_frame_always_1.  */
> -  *this_id = get_frame_id (get_prev_frame_always (this_frame));
> +  frame_info *prev_frame = get_prev_frame_always (this_frame);
> +  if (prev_frame == nullptr)
> +    error ("failed to find previous frame when computing inline frame id");

Missing _().  But, is this a "just in case" check/error?  As in, a softer
gdb_assert?  With the bug fixed, is this error call ever expected to
trigger (modulo other gdb bugs)?

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv5] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-09-20 12:24         ` Pedro Alves
@ 2021-09-21 13:52           ` Andrew Burgess
  0 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-09-21 13:52 UTC (permalink / raw)
  To: Pedro Alves; +Cc: gdb-patches, Simon Marchi

* Pedro Alves <pedro@palves.net> [2021-09-20 13:24:50 +0100]:

> Hi Andrew,
> 
> On 2021-08-09 4:41 p.m., Andrew Burgess wrote:
> > I've not heard anything from Pedro, but I'd like to move this patch
> > forward.
> > 
> > My assumption is that Pedro doesn't like using exceptions to pass
> > around information for a case that maybe isn't that exceptional.  As
> > Pedro was happy with v2, this new patch removes the use of exceptions.
> > 
> > Simon's concerns with v1/v2 were, I think, summarised as:
> > 
> >   1. What get_prev_frame_if_no_cycle does no longer matches the
> >      function name, and
> > 
> >   2. get_prev_frame_if_no_cycle was trying to figure out the callers
> >      intention in order to change its behaviour.
> > 
> > To try and address these two things I have:
> > 
> >   1. Renamed get_prev_frame_if_no_cycle to hopefully make it clearer
> >      that its result is not so clear cut, and
> > 
> >   2. Changed the behaviour of get_prev_frame_if_no_cycle based on a
> >      passed in parameter instead of "peeking" at various bits of state.
> > 
> > My hope is that this might be more acceptable to everyone, but I'd
> > love to hear any thoughts,
> > 
> 
> To be honest, to me, the new "cycle_detection_p" argument seems pointless/redundant,
> since inside the function, you have the INLINE_FRAME assertion checks anyhow.  I.e.,
> putting:
> 
>   bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;
> 
> inside get_prev_frame_with_optional_cycle_detection yields the exact same, IIUC.
> 
> So adding the argument is just adding scope for someone passing the wrong 
> cycle_detection_p argument at some point, it seems to me.
> 
> frame.c already has to know special things about inline frames, I mean,
> even get_prev_frame_always_1 already must know to bypass a bunch of tests
> for inline frames, like:
> 
>   /* If we are unwinding from an inline frame, all of the below tests
>      were already performed when we unwound from the next non-inline
>      frame.  We must skip them, since we can not get THIS_FRAME's ID
>      until we have unwound all the way down to the previous non-inline
>      frame.  */
>   if (get_frame_type (this_frame) == INLINE_FRAME)
>     return get_prev_frame_if_no_cycle (this_frame);
> 
> 
> I'd remove the cycle_detection_p argument, replacing it said local:
> 
>   bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;
> 
> and rename get_prev_frame_if_no_cycle to, say, get_prev_frame_maybe_check_cycle.
> 
> Like, e.g.:
> 
> ~~~~
> diff --git a/gdb/frame.c b/gdb/frame.c
> index 6433e9db788..2c9a324c93f 100644
> --- a/gdb/frame.c
> +++ b/gdb/frame.c
> @@ -2051,16 +2051,14 @@ frame_register_unwind_location (struct frame_info *this_frame, int regnum,
>     the previous frame is a duplicate and we return nullptr then we will be
>     unable to calculate the frame_id of the inline frame, this in turn
>     causes inline_frame_this_id() to fail.  So for inline frames (and only
> -   for inline frames) it is acceptable to pass CYCLE_DETECTION_P as false,
> -   in that case the previous frame will always be returned, even when it
> +   for inline frames), the previous frame will always be returned, even when it
>     has a duplicate frame_id.  We're not worried about cycles in the frame
>     chain as, if the previous frame returned here has a duplicate frame_id,
>     then the frame_id of the inline frame, calculated based off the frame_id
>     of the previous frame, should also be a duplicate.  */
>  
>  static struct frame_info *
> -get_prev_frame_with_optional_cycle_detection (struct frame_info *this_frame,
> -					      bool cycle_detection_p)
> +get_prev_frame_maybe_check_cycle (struct frame_info *this_frame)
>  {
>    struct frame_info *prev_frame;
>  
> @@ -2083,6 +2081,9 @@ get_prev_frame_with_optional_cycle_detection (struct frame_info *this_frame,
>       are simpler.  For these frames we immediately compute the frame_id,
>       and so, for those frames, we will always reenter this function with
>       the frame_id status of COMPUTING.  */
> +
> +  bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;
> +
>    gdb_assert (cycle_detection_p
>  	      || (get_frame_type (this_frame) == INLINE_FRAME
>  		  && ((this_frame->level > 0
> @@ -2193,7 +2194,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
>       until we have unwound all the way down to the previous non-inline
>       frame.  */
>    if (get_frame_type (this_frame) == INLINE_FRAME)
> -    return get_prev_frame_with_optional_cycle_detection (this_frame, false);
> +    return get_prev_frame_maybe_check_cycle (this_frame);
>  
>    /* If this_frame is the current frame, then compute and stash its
>       frame id prior to fetching and computing the frame id of the
> @@ -2294,7 +2295,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
>  	}
>      }
>  
> -  return get_prev_frame_with_optional_cycle_detection (this_frame, true);
> +  return get_prev_frame_maybe_check_cycle (this_frame);
>  }
>  
>  /* Return a "struct frame_info" corresponding to the frame that called
> ~~~~

Thanks, I think that's a great suggestion.  I'll use this in the next
version.

> 
> 
> 
> > --- a/gdb/inline-frame.c
> > +++ b/gdb/inline-frame.c
> > @@ -163,7 +163,10 @@ inline_frame_this_id (struct frame_info *this_frame,
> >       function, there must be previous frames, so this is safe - as
> >       long as we're careful not to create any cycles.  See related
> >       comments in get_prev_frame_always_1.  */
> > -  *this_id = get_frame_id (get_prev_frame_always (this_frame));
> > +  frame_info *prev_frame = get_prev_frame_always (this_frame);
> > +  if (prev_frame == nullptr)
> > +    error ("failed to find previous frame when computing inline frame id");
> 
> Missing _().  But, is this a "just in case" check/error?  As in, a softer
> gdb_assert?  With the bug fixed, is this error call ever expected to
> trigger (modulo other gdb bugs)?

No, I don't think this is a softer assert.  get_prev_frame_always can
return nullptr for other reasons, e.g if a MEMORY_ERROR is thrown
while figuring out the previous frame.

If that should happen then we'll be back in the original situation,
*this_id will be null_frame_id, and the assert that I originally saw
will trigger again.

I don't see any reason why get_prev_frame_always _shouldn't_ return
nullptr here, but if it does I we can't get the inline frame's id.

I'll post a new patch with the changes made.

Thanks,
Andrew

^ permalink raw reply	[flat|nested] 48+ messages in thread

* [PATCHv6] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-08-09 15:41       ` [PATCHv5] " Andrew Burgess
  2021-08-23  9:41         ` Andrew Burgess
  2021-09-20 12:24         ` Pedro Alves
@ 2021-09-21 13:54         ` Andrew Burgess
  2021-09-22 14:14           ` Simon Marchi
  2 siblings, 1 reply; 48+ messages in thread
From: Andrew Burgess @ 2021-09-21 13:54 UTC (permalink / raw)
  To: gdb-patches

I ran into this assertion while GDB was trying to unwind the stack:

  gdb/inline-frame.c:173: internal-error: void inline_frame_this_id(frame_info*, void**, frame_id*): Assertion `frame_id_p (*this_id)' failed.

That is, when building the frame_id for an inline frame, GDB asks for
the frame_id of the previous frame.  Unfortunately, no valid frame_id
was returned for the previous frame, and so the assertion triggers.

What is happening is this, I had a stack that looked something like
this (the arrows '->' point from caller to callee):

  normal_frame -> inline_frame

However, for whatever reason (e.g. broken debug information, or
corrupted stack contents in the inferior), when GDB tries to unwind
"normal_frame", it ends up getting back effectively the same frame,
thus the call stack looks like this to GDB:

  .-> normal_frame -> inline_frame
  |     |
  '-----'

Given such a situation we would expect GDB to terminate the stack with
an error like this:

  Backtrace stopped: previous frame identical to this frame (corrupt stack?)

However, the inline_frame causes a problem, and here's why:

When unwinding we start from the sentinel frame and call
get_prev_frame.  We eventually end up in get_prev_frame_if_no_cycle,
in here we create a raw frame, and as this is frame #0 we immediately
return.

However, eventually we will try to unwind the stack further.  When we
do this we inevitably needing to know the frame_id for frame #0, and
so, eventually, we end up in compute_frame_id.

In compute_frame_id we first find the right unwinder for this frame,
in our case (i.e. for inline_frame) the $pc is within the function
normal_frame, but also within a block associated with the inlined
function inline_frame, as such the inline frame unwinder claims this
frame.

Back in compute_frame_id we next compute the frame_id, for our
inline_frame this means a call to inline_frame_this_id.

The ID of an inline frame is based on the id of the previous frame, so
from inline_frame_this_id we call get_prev_frame_always, this
eventually calls get_prev_frame_if_no_cycle again, which creates
another raw frame and calls compute_frame_id (for frames other than
frame 0 we immediately compute the frame_id).

In compute_frame_id we again identify the correct unwinder for this
frame.  Our $pc is unchanged, however, the fact that the next frame is
of type INLINE_FRAME prevents the inline frame unwinder from claiming
this frame again, and so, the standard DWARF frame unwinder claims
normal_frame.

We return to compute_frame_id and call the standard DWARF function to
build the frame_id for normal_frame.

With the frame_id of normal_frame figured out we return to
compute_frame_id, and then to get_prev_frame_if_no_cycle, where we add
the ID for normal_frame into the frame_id cache, and return the frame
back to inline_frame_this_id.

From inline_frame_this_id we build a frame_id for inline_frame and
return to compute_frame_id, and then to get_prev_frame_if_no_cycle,
which adds the frame_id for inline_frame into the frame_id cache.

So far, so good.

However, as we are trying to unwind the complete stack, we eventually
ask for the previous frame of normal_frame, remember, at this point
GDB doesn't know the stack is corrupted (with a cycle), GDB still
needs to figure that out.

So, we eventually end up in get_prev_frame_if_no_cycle where we create
a raw frame and call compute_frame_id, remember, this is for the frame
before normal_frame.

The first task for compute_frame_id is to find the unwinder for this
frame, so all of the frame sniffers are tried in order, this includes
the inline frame sniffer.

The inline frame sniffer asks for the $pc, this request is sent up the
stack to normal_frame, which, due to its cyclic behaviour, tells GDB
that the $pc in the previous frame was the same as the $pc in
normal_frame.

GDB spots that this $pc corresponds to both the function normal_frame
and also the inline function inline_frame.  As the next frame is not
an INLINE_FRAME then GDB figures that we have not yet built a frame to
cover inline_frame, and so the inline sniffer claims this new frame.
Our stack is now looking like this:

  inline_frame -> normal_frame -> inline_frame

But, we have not yet computed the frame id for the outer most (on the
left) inline_frame.  After the frame sniffer has claimed the inline
frame GDB returns to compute_frame_id and calls inline_frame_this_id.

In here GDB calls get_prev_frame_always, which eventually ends up
in get_prev_frame_if_no_cycle again, where we create a raw frame and
call compute_frame_id.

Just like before, compute_frame_id tries to find an unwinder for this
new frame, it sees that the $pc is within both normal_frame and
inline_frame, but the next frame is, again, an INLINE_FRAME, so, just
like before the standard DWARF unwinder claims this frame.  Back in
compute_frame_id we again call the standard DWARF function to build
the frame_id for this new copy of normal_frame.

At this point the stack looks like this:

  normal_frame -> inline_frame -> normal_frame -> inline_frame

After compute_frame_id we return to get_prev_frame_if_no_cycle, where
we try to add the frame_id for the new normal_frame into the frame_id
cache, however, unlike before, we fail to add this frame_id as it is
a duplicate of the previous normal_frame frame_id.  Having found a
duplicate get_prev_frame_if_no_cycle unlinks the new frame from the
stack, and returns nullptr, the stack now looks like this:

  inline_frame -> normal_frame -> inline_frame

The nullptr result from get_prev_frame_if_no_cycle is fed back to
inline_frame_this_id, which forwards this to get_frame_id, which
immediately returns null_frame_id.  As null_frame_id is not considered
a valid frame_id, this is what triggers the assertion.

In summary then:

 - inline_frame_this_id currently assumes that as the inline frame
   exists, we will always get a valid frame back from
   get_prev_frame_always,

 - get_prev_frame_if_no_cycle currently assumes that it is safe to
   return nullptr when it sees a cycle.

Notice that in frame.c:compute_frame_id, this code:

  fi->this_id.value = outer_frame_id;
  fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
  gdb_assert (frame_id_p (fi->this_id.value));

The assertion makes it clear that the this_id function must always
return a valid frame_id (e.g. null_frame_id is not a valid return
value), and similarly in inline_frame.c:inline_frame_this_id this
code:

  *this_id = get_frame_id (get_prev_frame_always (this_frame));
  /* snip comment */
  gdb_assert (frame_id_p (*this_id));

Makes it clear that every inline frame expects to be able to get a
previous frame, which will have a valid frame_id.

As I have discussed above, these assumptions don't currently hold in
all cases.

One possibility would be to move the call to get_prev_frame_always
forward from inline_frame_this_id to inline_frame_sniffer, however,
this falls foul of (in frame.c:frame_cleanup_after_sniffer) this
assertion:

  /* No sniffer should extend the frame chain; sniff based on what is
     already certain.  */
  gdb_assert (!frame->prev_p);

This assert prohibits any sniffer from trying to get the previous
frame, as getting the previous frame is likely to depend on the next
frame, I can understand why this assertion is a good thing, and I'm in
no rush to alter this rule.

The solution proposed here takes onboard feedback from both Pedro, and
Simon (see the links below).  The get_prev_frame_if_no_cycle function
is renamed to get_prev_frame_maybe_check_cycle, and will now not do
cycle detection for inline frames, even when we spot a duplicate frame
it is still returned.  This is fine, as, if the normal frame has a
duplicate frame-id then the inline frame will also have a duplicate
frame-id.  And so, when we reject the inline frame, the duplicate
normal frame, which is previous to the inline frame, will also be
rejected.

In inline-frame.c the call to get_prev_frame_always is no longer
nested inside the call to get_frame_id.  There are reasons why
get_prev_frame_always can return nullptr, for example, if there is a
memory error while trying to get the previous frame, if this should
happen then we now give a more informative error message.

Historical Links:

 Patch v2: https://sourceware.org/pipermail/gdb-patches/2021-June/180208.html
 Feedback: https://sourceware.org/pipermail/gdb-patches/2021-July/180651.html
           https://sourceware.org/pipermail/gdb-patches/2021-July/180663.html

 Patch v3: https://sourceware.org/pipermail/gdb-patches/2021-July/181029.html
 Feedback: https://sourceware.org/pipermail/gdb-patches/2021-July/181035.html

 Additional input: https://sourceware.org/pipermail/gdb-patches/2021-September/182040.html
---
 gdb/frame.c                                   |  57 ++++++-
 gdb/inline-frame.c                            |   5 +-
 .../gdb.base/inline-frame-cycle-unwind.c      |  58 +++++++
 .../gdb.base/inline-frame-cycle-unwind.exp    | 145 ++++++++++++++++++
 .../gdb.base/inline-frame-cycle-unwind.py     |  85 ++++++++++
 5 files changed, 344 insertions(+), 6 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
 create mode 100644 gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py

diff --git a/gdb/frame.c b/gdb/frame.c
index d28944075ed..2c9a324c93f 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2044,13 +2044,55 @@ frame_register_unwind_location (struct frame_info *this_frame, int regnum,
    outermost, with UNWIND_SAME_ID stop reason.  Unlike the other
    validity tests, that compare THIS_FRAME and the next frame, we do
    this right after creating the previous frame, to avoid ever ending
-   up with two frames with the same id in the frame chain.  */
+   up with two frames with the same id in the frame chain.
+
+   There is however, one case where this cycle detection is not desirable,
+   when asking for the previous frame of an inline frame, in this case, if
+   the previous frame is a duplicate and we return nullptr then we will be
+   unable to calculate the frame_id of the inline frame, this in turn
+   causes inline_frame_this_id() to fail.  So for inline frames (and only
+   for inline frames), the previous frame will always be returned, even when it
+   has a duplicate frame_id.  We're not worried about cycles in the frame
+   chain as, if the previous frame returned here has a duplicate frame_id,
+   then the frame_id of the inline frame, calculated based off the frame_id
+   of the previous frame, should also be a duplicate.  */
 
 static struct frame_info *
-get_prev_frame_if_no_cycle (struct frame_info *this_frame)
+get_prev_frame_maybe_check_cycle (struct frame_info *this_frame)
 {
   struct frame_info *prev_frame;
 
+  /* This assert primarily checks that CYCLE_DETECTION_P is only false for
+     inline frames.  However, this assertion also makes some claims about
+     what the state of GDB should be when we enter this function and
+     THIS_FRAME is an inline frame.
+
+     If frame #0 is an inline frame then we put off calculating the
+     frame_id until we specifically make a call to get_frame_id().  As a
+     result we can enter this function in two possible states.  If GDB
+     asked for the previous frame of frame #0 then THIS_FRAME will be frame
+     #0 (an inline frame), and the frame_id will be in the NOT_COMPUTED
+     state.  However, if GDB asked for the frame_id of frame #0, then, as
+     getting the frame_id of an inline frame requires us to get the
+     frame_id of the previous frame, we will still end up in here, and the
+     frame_id status will be COMPUTING.
+
+     If we consider an inline frame at a level greater than #0 then things
+     are simpler.  For these frames we immediately compute the frame_id,
+     and so, for those frames, we will always reenter this function with
+     the frame_id status of COMPUTING.  */
+
+  bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;
+
+  gdb_assert (cycle_detection_p
+	      || (get_frame_type (this_frame) == INLINE_FRAME
+		  && ((this_frame->level > 0
+		       && (this_frame->this_id.p
+			   == frame_id_status::COMPUTING))
+		      || (this_frame->level == 0
+			  && (this_frame->this_id.p
+			      != frame_id_status::COMPUTED)))));
+
   prev_frame = get_prev_frame_raw (this_frame);
 
   /* Don't compute the frame id of the current frame yet.  Unwinding
@@ -2070,7 +2112,12 @@ get_prev_frame_if_no_cycle (struct frame_info *this_frame)
   try
     {
       compute_frame_id (prev_frame);
-      if (!frame_stash_add (prev_frame))
+
+      /* We must do the CYCLE_DETECTION_P check after attempting to add
+	 PREV_FRAME into the cache; if PREV_FRAME is unique then we do want
+	 it in the cache, but if it is a duplicate and CYCLE_DETECTION_P is
+	 false, then we don't want to unlink it.  */
+      if (!frame_stash_add (prev_frame) && cycle_detection_p)
 	{
 	  /* Another frame with the same id was already in the stash.  We just
 	     detected a cycle.  */
@@ -2147,7 +2194,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
      until we have unwound all the way down to the previous non-inline
      frame.  */
   if (get_frame_type (this_frame) == INLINE_FRAME)
-    return get_prev_frame_if_no_cycle (this_frame);
+    return get_prev_frame_maybe_check_cycle (this_frame);
 
   /* If this_frame is the current frame, then compute and stash its
      frame id prior to fetching and computing the frame id of the
@@ -2248,7 +2295,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
 	}
     }
 
-  return get_prev_frame_if_no_cycle (this_frame);
+  return get_prev_frame_maybe_check_cycle (this_frame);
 }
 
 /* Return a "struct frame_info" corresponding to the frame that called
diff --git a/gdb/inline-frame.c b/gdb/inline-frame.c
index c98af1842a6..df7bd826ff5 100644
--- a/gdb/inline-frame.c
+++ b/gdb/inline-frame.c
@@ -163,7 +163,10 @@ inline_frame_this_id (struct frame_info *this_frame,
      function, there must be previous frames, so this is safe - as
      long as we're careful not to create any cycles.  See related
      comments in get_prev_frame_always_1.  */
-  *this_id = get_frame_id (get_prev_frame_always (this_frame));
+  frame_info *prev_frame = get_prev_frame_always (this_frame);
+  if (prev_frame == nullptr)
+    error (_("failed to find previous frame when computing inline frame id"));
+  *this_id = get_frame_id (prev_frame);
 
   /* We need a valid frame ID, so we need to be based on a valid
      frame.  FSF submission NOTE: this would be a good assertion to
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
new file mode 100644
index 00000000000..183c40928b6
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+static void inline_func (void);
+static void normal_func (void);
+
+volatile int global_var;
+volatile int level_counter;
+
+static void __attribute__((noinline))
+normal_func (void)
+{
+  /* Do some work.  */
+  ++global_var;
+
+  /* Now the inline function.  */
+  --level_counter;
+  inline_func ();
+  ++level_counter;
+
+  /* Do some work.  */
+  ++global_var;
+}
+
+static inline void __attribute__((__always_inline__))
+inline_func (void)
+{
+  if (level_counter > 1)
+    {
+      --level_counter;
+      normal_func ();
+      ++level_counter;
+    }
+  else
+    ++global_var;	/* Break here.  */
+}
+
+int
+main ()
+{
+  level_counter = 6;
+  normal_func ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
new file mode 100644
index 00000000000..2801b683a03
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
@@ -0,0 +1,145 @@
+# Copyright (C) 2021 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 checks for an edge case when unwinding inline frames which
+# occur towards the older end of the stack when the stack ends with a
+# cycle.  Consider this well formed stack:
+#
+#   main -> normal_frame -> inline_frame
+#
+# Now consider that, for whatever reason, the stack unwinding of
+# "normal_frame" becomes corrupted, such that the stack appears to be
+# this:
+#
+#   .-> normal_frame -> inline_frame
+#   |      |
+#   '------'
+#
+# When confronted with such a situation we would expect GDB to detect
+# the stack frame cycle and terminate the backtrace at the first
+# instance of "normal_frame" with a message:
+#
+#   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
+#
+# However, at one point there was a bug in GDB's inline frame
+# mechanism such that the fact that "inline_frame" was inlined into
+# "normal_frame" would cause GDB to trigger an assertion.
+#
+# This text makes use of a Python unwinder which can fake the cyclic
+# stack cycle, further the test sets up multiple levels of normal and
+# inline frames.  At the point of testing the stack looks like this:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Where "normal_func" is a normal frame, and "inline_func" is an inline frame.
+#
+# The python unwinder is then used to force a stack cycle at each
+# "normal_func" frame in turn, we then check that GDB can successfully unwind
+# the stack.
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}]} {
+    return -1
+}
+
+# Skip this test if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to the breakpoint where we will carry out the test.
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# Load the script containing the unwinder, this must be done at the
+# testing point as the script will examine the stack as it is loaded.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Check the unbroken stack.
+gdb_test_sequence "bt" "backtrace when the unwind is left unbroken" {
+    "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
+}
+
+with_test_prefix "cycle at level 5" {
+    # Arrange to introduce a stack cycle at frame 5.
+    gdb_test_no_output "python stop_at_level=5"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 5" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#4 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#5 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 3" {
+    # Arrange to introduce a stack cycle at frame 3.
+    gdb_test_no_output "python stop_at_level=3"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 3" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 1" {
+    # Arrange to introduce a stack cycle at frame 1.
+    gdb_test_no_output "python stop_at_level=1"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 1" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+# Flush the register cache (which also flushes the frame cache) so we
+# get a full backtrace again, then switch on frame debugging and try
+# to back trace.  At one point this triggered an assertion.
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_no_output "set debug frame 1"
+gdb_test_multiple "bt" "backtrace with debugging on" {
+    -re "^$gdb_prompt $" {
+	pass $gdb_test_name
+    }
+    -re "\[^\r\n\]+\r\n" {
+	exp_continue
+    }
+}
+gdb_test "p 1 + 2 + 3" " = 6" \
+    "ensure GDB is still alive"
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
new file mode 100644
index 00000000000..99c571f973c
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+# Set this to the stack level the backtrace should be corrupted at.
+# This will only work for frame 1, 3, or 5 in the test this unwinder
+# was written for.
+stop_at_level = None
+
+# Set this to the stack frame size of frames 1, 3, and 5.  These
+# frames will all have the same stack frame size as they are the same
+# function called recursively.
+stack_adjust = None
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "stop at level")
+
+    def __call__(self, pending_frame):
+        global stop_at_level
+        global stack_adjust
+
+        if stop_at_level is None or pending_frame.level() != stop_at_level:
+            return None
+
+        if stack_adjust is None:
+            raise gdb.GdbError("invalid stack_adjust")
+
+        if not stop_at_level in [1, 3, 5]:
+            raise gdb.GdbError("invalid stop_at_level")
+
+        sp_desc = pending_frame.architecture().registers().find("sp")
+        sp = pending_frame.read_register(sp_desc) + stack_adjust
+        pc = (gdb.lookup_symbol("normal_func"))[0].value().address
+        unwinder = pending_frame.create_unwind_info(FrameId(sp, pc))
+
+        for reg in pending_frame.architecture().registers("general"):
+            val = pending_frame.read_register(reg)
+            unwinder.add_saved_register(reg, val)
+        return unwinder
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
+
+# When loaded, it is expected that the stack looks like:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Compute the stack frame size of normal_func, which has inline_func
+# inlined within it.
+f0 = gdb.newest_frame()
+f1 = f0.older()
+f2 = f1.older()
+f0_sp = f0.read_register("sp")
+f2_sp = f2.read_register("sp")
+stack_adjust = f2_sp - f0_sp
-- 
2.25.4


^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv6] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-09-21 13:54         ` [PATCHv6] " Andrew Burgess
@ 2021-09-22 14:14           ` Simon Marchi
  2021-09-22 16:46             ` Andrew Burgess
  0 siblings, 1 reply; 48+ messages in thread
From: Simon Marchi @ 2021-09-22 14:14 UTC (permalink / raw)
  To: Andrew Burgess, gdb-patches

>  static struct frame_info *
> -get_prev_frame_if_no_cycle (struct frame_info *this_frame)
> +get_prev_frame_maybe_check_cycle (struct frame_info *this_frame)
>  {
>    struct frame_info *prev_frame;
>  
> +  /* This assert primarily checks that CYCLE_DETECTION_P is only false for
> +     inline frames.  However, this assertion also makes some claims about
> +     what the state of GDB should be when we enter this function and
> +     THIS_FRAME is an inline frame.
> +
> +     If frame #0 is an inline frame then we put off calculating the
> +     frame_id until we specifically make a call to get_frame_id().  As a
> +     result we can enter this function in two possible states.  If GDB
> +     asked for the previous frame of frame #0 then THIS_FRAME will be frame
> +     #0 (an inline frame), and the frame_id will be in the NOT_COMPUTED
> +     state.  However, if GDB asked for the frame_id of frame #0, then, as
> +     getting the frame_id of an inline frame requires us to get the
> +     frame_id of the previous frame, we will still end up in here, and the
> +     frame_id status will be COMPUTING.
> +
> +     If we consider an inline frame at a level greater than #0 then things
> +     are simpler.  For these frames we immediately compute the frame_id,
> +     and so, for those frames, we will always reenter this function with
> +     the frame_id status of COMPUTING.  */
> +
> +  bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;

I'm just wondering whether the "This assert primarily ..." comment above
needs a bit of re-wording following the latest code changes.  As it
looks right now, the comment says that we make sure that cycle detection
is only false for inline frame, then cycle_detection_p is defined
exactly as false for inline frames.  So it's kind of stating the
obvious.

Other than that, this is fine with me.

Simon

^ permalink raw reply	[flat|nested] 48+ messages in thread

* Re: [PATCHv6] gdb: prevent an assertion when computing the frame_id for an inline frame
  2021-09-22 14:14           ` Simon Marchi
@ 2021-09-22 16:46             ` Andrew Burgess
  0 siblings, 0 replies; 48+ messages in thread
From: Andrew Burgess @ 2021-09-22 16:46 UTC (permalink / raw)
  To: gdb-patches

* Simon Marchi <simon.marchi@polymtl.ca> [2021-09-22 10:14:07 -0400]:

> >  static struct frame_info *
> > -get_prev_frame_if_no_cycle (struct frame_info *this_frame)
> > +get_prev_frame_maybe_check_cycle (struct frame_info *this_frame)
> >  {
> >    struct frame_info *prev_frame;
> >  
> > +  /* This assert primarily checks that CYCLE_DETECTION_P is only false for
> > +     inline frames.  However, this assertion also makes some claims about
> > +     what the state of GDB should be when we enter this function and
> > +     THIS_FRAME is an inline frame.
> > +
> > +     If frame #0 is an inline frame then we put off calculating the
> > +     frame_id until we specifically make a call to get_frame_id().  As a
> > +     result we can enter this function in two possible states.  If GDB
> > +     asked for the previous frame of frame #0 then THIS_FRAME will be frame
> > +     #0 (an inline frame), and the frame_id will be in the NOT_COMPUTED
> > +     state.  However, if GDB asked for the frame_id of frame #0, then, as
> > +     getting the frame_id of an inline frame requires us to get the
> > +     frame_id of the previous frame, we will still end up in here, and the
> > +     frame_id status will be COMPUTING.
> > +
> > +     If we consider an inline frame at a level greater than #0 then things
> > +     are simpler.  For these frames we immediately compute the frame_id,
> > +     and so, for those frames, we will always reenter this function with
> > +     the frame_id status of COMPUTING.  */
> > +
> > +  bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;
> 
> I'm just wondering whether the "This assert primarily ..." comment above
> needs a bit of re-wording following the latest code changes.  As it
> looks right now, the comment says that we make sure that cycle detection
> is only false for inline frame, then cycle_detection_p is defined
> exactly as false for inline frames.  So it's kind of stating the
> obvious.
> 
> Other than that, this is fine with me.

Thanks Simon.

I rewrote the comment, and moved the assertion, and the setup of
cycle_detection_p to later in the function, closer to where
cycle_detection_p is actually used.

The latest version of this patch is included below.  I think with
these changes everyone is now happy, so I plan to push this in a
couple of days, unless someone objects.

Thanks,
Andrew

---

commit 7709dff844aa2cd201dcc9e02c5fb8e8cb406509
Author: Andrew Burgess <andrew.burgess@embecosm.com>
Date:   Wed May 26 22:03:23 2021 +0100

    gdb: prevent an assertion when computing the frame_id for an inline frame
    
    I ran into this assertion while GDB was trying to unwind the stack:
    
      gdb/inline-frame.c:173: internal-error: void inline_frame_this_id(frame_info*, void**, frame_id*): Assertion `frame_id_p (*this_id)' failed.
    
    That is, when building the frame_id for an inline frame, GDB asks for
    the frame_id of the previous frame.  Unfortunately, no valid frame_id
    was returned for the previous frame, and so the assertion triggers.
    
    What is happening is this, I had a stack that looked something like
    this (the arrows '->' point from caller to callee):
    
      normal_frame -> inline_frame
    
    However, for whatever reason (e.g. broken debug information, or
    corrupted stack contents in the inferior), when GDB tries to unwind
    "normal_frame", it ends up getting back effectively the same frame,
    thus the call stack looks like this to GDB:
    
      .-> normal_frame -> inline_frame
      |     |
      '-----'
    
    Given such a situation we would expect GDB to terminate the stack with
    an error like this:
    
      Backtrace stopped: previous frame identical to this frame (corrupt stack?)
    
    However, the inline_frame causes a problem, and here's why:
    
    When unwinding we start from the sentinel frame and call
    get_prev_frame.  We eventually end up in get_prev_frame_if_no_cycle,
    in here we create a raw frame, and as this is frame #0 we immediately
    return.
    
    However, eventually we will try to unwind the stack further.  When we
    do this we inevitably needing to know the frame_id for frame #0, and
    so, eventually, we end up in compute_frame_id.
    
    In compute_frame_id we first find the right unwinder for this frame,
    in our case (i.e. for inline_frame) the $pc is within the function
    normal_frame, but also within a block associated with the inlined
    function inline_frame, as such the inline frame unwinder claims this
    frame.
    
    Back in compute_frame_id we next compute the frame_id, for our
    inline_frame this means a call to inline_frame_this_id.
    
    The ID of an inline frame is based on the id of the previous frame, so
    from inline_frame_this_id we call get_prev_frame_always, this
    eventually calls get_prev_frame_if_no_cycle again, which creates
    another raw frame and calls compute_frame_id (for frames other than
    frame 0 we immediately compute the frame_id).
    
    In compute_frame_id we again identify the correct unwinder for this
    frame.  Our $pc is unchanged, however, the fact that the next frame is
    of type INLINE_FRAME prevents the inline frame unwinder from claiming
    this frame again, and so, the standard DWARF frame unwinder claims
    normal_frame.
    
    We return to compute_frame_id and call the standard DWARF function to
    build the frame_id for normal_frame.
    
    With the frame_id of normal_frame figured out we return to
    compute_frame_id, and then to get_prev_frame_if_no_cycle, where we add
    the ID for normal_frame into the frame_id cache, and return the frame
    back to inline_frame_this_id.
    
    From inline_frame_this_id we build a frame_id for inline_frame and
    return to compute_frame_id, and then to get_prev_frame_if_no_cycle,
    which adds the frame_id for inline_frame into the frame_id cache.
    
    So far, so good.
    
    However, as we are trying to unwind the complete stack, we eventually
    ask for the previous frame of normal_frame, remember, at this point
    GDB doesn't know the stack is corrupted (with a cycle), GDB still
    needs to figure that out.
    
    So, we eventually end up in get_prev_frame_if_no_cycle where we create
    a raw frame and call compute_frame_id, remember, this is for the frame
    before normal_frame.
    
    The first task for compute_frame_id is to find the unwinder for this
    frame, so all of the frame sniffers are tried in order, this includes
    the inline frame sniffer.
    
    The inline frame sniffer asks for the $pc, this request is sent up the
    stack to normal_frame, which, due to its cyclic behaviour, tells GDB
    that the $pc in the previous frame was the same as the $pc in
    normal_frame.
    
    GDB spots that this $pc corresponds to both the function normal_frame
    and also the inline function inline_frame.  As the next frame is not
    an INLINE_FRAME then GDB figures that we have not yet built a frame to
    cover inline_frame, and so the inline sniffer claims this new frame.
    Our stack is now looking like this:
    
      inline_frame -> normal_frame -> inline_frame
    
    But, we have not yet computed the frame id for the outer most (on the
    left) inline_frame.  After the frame sniffer has claimed the inline
    frame GDB returns to compute_frame_id and calls inline_frame_this_id.
    
    In here GDB calls get_prev_frame_always, which eventually ends up
    in get_prev_frame_if_no_cycle again, where we create a raw frame and
    call compute_frame_id.
    
    Just like before, compute_frame_id tries to find an unwinder for this
    new frame, it sees that the $pc is within both normal_frame and
    inline_frame, but the next frame is, again, an INLINE_FRAME, so, just
    like before the standard DWARF unwinder claims this frame.  Back in
    compute_frame_id we again call the standard DWARF function to build
    the frame_id for this new copy of normal_frame.
    
    At this point the stack looks like this:
    
      normal_frame -> inline_frame -> normal_frame -> inline_frame
    
    After compute_frame_id we return to get_prev_frame_if_no_cycle, where
    we try to add the frame_id for the new normal_frame into the frame_id
    cache, however, unlike before, we fail to add this frame_id as it is
    a duplicate of the previous normal_frame frame_id.  Having found a
    duplicate get_prev_frame_if_no_cycle unlinks the new frame from the
    stack, and returns nullptr, the stack now looks like this:
    
      inline_frame -> normal_frame -> inline_frame
    
    The nullptr result from get_prev_frame_if_no_cycle is fed back to
    inline_frame_this_id, which forwards this to get_frame_id, which
    immediately returns null_frame_id.  As null_frame_id is not considered
    a valid frame_id, this is what triggers the assertion.
    
    In summary then:
    
     - inline_frame_this_id currently assumes that as the inline frame
       exists, we will always get a valid frame back from
       get_prev_frame_always,
    
     - get_prev_frame_if_no_cycle currently assumes that it is safe to
       return nullptr when it sees a cycle.
    
    Notice that in frame.c:compute_frame_id, this code:
    
      fi->this_id.value = outer_frame_id;
      fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
      gdb_assert (frame_id_p (fi->this_id.value));
    
    The assertion makes it clear that the this_id function must always
    return a valid frame_id (e.g. null_frame_id is not a valid return
    value), and similarly in inline_frame.c:inline_frame_this_id this
    code:
    
      *this_id = get_frame_id (get_prev_frame_always (this_frame));
      /* snip comment */
      gdb_assert (frame_id_p (*this_id));
    
    Makes it clear that every inline frame expects to be able to get a
    previous frame, which will have a valid frame_id.
    
    As I have discussed above, these assumptions don't currently hold in
    all cases.
    
    One possibility would be to move the call to get_prev_frame_always
    forward from inline_frame_this_id to inline_frame_sniffer, however,
    this falls foul of (in frame.c:frame_cleanup_after_sniffer) this
    assertion:
    
      /* No sniffer should extend the frame chain; sniff based on what is
         already certain.  */
      gdb_assert (!frame->prev_p);
    
    This assert prohibits any sniffer from trying to get the previous
    frame, as getting the previous frame is likely to depend on the next
    frame, I can understand why this assertion is a good thing, and I'm in
    no rush to alter this rule.
    
    The solution proposed here takes onboard feedback from both Pedro, and
    Simon (see the links below).  The get_prev_frame_if_no_cycle function
    is renamed to get_prev_frame_maybe_check_cycle, and will now not do
    cycle detection for inline frames, even when we spot a duplicate frame
    it is still returned.  This is fine, as, if the normal frame has a
    duplicate frame-id then the inline frame will also have a duplicate
    frame-id.  And so, when we reject the inline frame, the duplicate
    normal frame, which is previous to the inline frame, will also be
    rejected.
    
    In inline-frame.c the call to get_prev_frame_always is no longer
    nested inside the call to get_frame_id.  There are reasons why
    get_prev_frame_always can return nullptr, for example, if there is a
    memory error while trying to get the previous frame, if this should
    happen then we now give a more informative error message.
    
    Historical Links:
    
     Patch v2: https://sourceware.org/pipermail/gdb-patches/2021-June/180208.html
     Feedback: https://sourceware.org/pipermail/gdb-patches/2021-July/180651.html
               https://sourceware.org/pipermail/gdb-patches/2021-July/180663.html
    
     Patch v3: https://sourceware.org/pipermail/gdb-patches/2021-July/181029.html
     Feedback: https://sourceware.org/pipermail/gdb-patches/2021-July/181035.html
    
     Additional input: https://sourceware.org/pipermail/gdb-patches/2021-September/182040.html

diff --git a/gdb/frame.c b/gdb/frame.c
index d28944075ed..16673258373 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2044,14 +2044,23 @@ frame_register_unwind_location (struct frame_info *this_frame, int regnum,
    outermost, with UNWIND_SAME_ID stop reason.  Unlike the other
    validity tests, that compare THIS_FRAME and the next frame, we do
    this right after creating the previous frame, to avoid ever ending
-   up with two frames with the same id in the frame chain.  */
+   up with two frames with the same id in the frame chain.
+
+   There is however, one case where this cycle detection is not desirable,
+   when asking for the previous frame of an inline frame, in this case, if
+   the previous frame is a duplicate and we return nullptr then we will be
+   unable to calculate the frame_id of the inline frame, this in turn
+   causes inline_frame_this_id() to fail.  So for inline frames (and only
+   for inline frames), the previous frame will always be returned, even when it
+   has a duplicate frame_id.  We're not worried about cycles in the frame
+   chain as, if the previous frame returned here has a duplicate frame_id,
+   then the frame_id of the inline frame, calculated based off the frame_id
+   of the previous frame, should also be a duplicate.  */
 
 static struct frame_info *
-get_prev_frame_if_no_cycle (struct frame_info *this_frame)
+get_prev_frame_maybe_check_cycle (struct frame_info *this_frame)
 {
-  struct frame_info *prev_frame;
-
-  prev_frame = get_prev_frame_raw (this_frame);
+  struct frame_info *prev_frame = get_prev_frame_raw (this_frame);
 
   /* Don't compute the frame id of the current frame yet.  Unwinding
      the sentinel frame can fail (e.g., if the thread is gone and we
@@ -2070,7 +2079,42 @@ get_prev_frame_if_no_cycle (struct frame_info *this_frame)
   try
     {
       compute_frame_id (prev_frame);
-      if (!frame_stash_add (prev_frame))
+
+      bool cycle_detection_p = get_frame_type (this_frame) != INLINE_FRAME;
+
+      /* This assert checks GDB's state with respect to calculating the
+	 frame-id of THIS_FRAME, in the case where THIS_FRAME is an inline
+	 frame.
+
+	 If THIS_FRAME is frame #0, and is an inline frame, then we put off
+	 calculating the frame_id until we specifically make a call to
+	 get_frame_id().  As a result we can enter this function in two
+	 possible states.  If GDB asked for the previous frame of frame #0
+	 then THIS_FRAME will be frame #0 (an inline frame), and the
+	 frame_id will be in the NOT_COMPUTED state.  However, if GDB asked
+	 for the frame_id of frame #0, then, as getting the frame_id of an
+	 inline frame requires us to get the frame_id of the previous
+	 frame, we will still end up in here, and the frame_id status will
+	 be COMPUTING.
+
+	 If, instead, THIS_FRAME is at a level greater than #0 then things
+	 are simpler.  For these frames we immediately compute the frame_id
+	 when the frame is initially created, and so, for those frames, we
+	 will always enter this function with the frame_id status of
+	 COMPUTING.  */
+      gdb_assert (cycle_detection_p
+		  || (this_frame->level > 0
+		      && (this_frame->this_id.p
+			  == frame_id_status::COMPUTING))
+		  || (this_frame->level == 0
+		      && (this_frame->this_id.p
+			  != frame_id_status::COMPUTED)));
+
+      /* We must do the CYCLE_DETECTION_P check after attempting to add
+	 PREV_FRAME into the cache; if PREV_FRAME is unique then we do want
+	 it in the cache, but if it is a duplicate and CYCLE_DETECTION_P is
+	 false, then we don't want to unlink it.  */
+      if (!frame_stash_add (prev_frame) && cycle_detection_p)
 	{
 	  /* Another frame with the same id was already in the stash.  We just
 	     detected a cycle.  */
@@ -2147,7 +2191,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
      until we have unwound all the way down to the previous non-inline
      frame.  */
   if (get_frame_type (this_frame) == INLINE_FRAME)
-    return get_prev_frame_if_no_cycle (this_frame);
+    return get_prev_frame_maybe_check_cycle (this_frame);
 
   /* If this_frame is the current frame, then compute and stash its
      frame id prior to fetching and computing the frame id of the
@@ -2248,7 +2292,7 @@ get_prev_frame_always_1 (struct frame_info *this_frame)
 	}
     }
 
-  return get_prev_frame_if_no_cycle (this_frame);
+  return get_prev_frame_maybe_check_cycle (this_frame);
 }
 
 /* Return a "struct frame_info" corresponding to the frame that called
diff --git a/gdb/inline-frame.c b/gdb/inline-frame.c
index c98af1842a6..df7bd826ff5 100644
--- a/gdb/inline-frame.c
+++ b/gdb/inline-frame.c
@@ -163,7 +163,10 @@ inline_frame_this_id (struct frame_info *this_frame,
      function, there must be previous frames, so this is safe - as
      long as we're careful not to create any cycles.  See related
      comments in get_prev_frame_always_1.  */
-  *this_id = get_frame_id (get_prev_frame_always (this_frame));
+  frame_info *prev_frame = get_prev_frame_always (this_frame);
+  if (prev_frame == nullptr)
+    error (_("failed to find previous frame when computing inline frame id"));
+  *this_id = get_frame_id (prev_frame);
 
   /* We need a valid frame ID, so we need to be based on a valid
      frame.  FSF submission NOTE: this would be a good assertion to
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
new file mode 100644
index 00000000000..183c40928b6
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021 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/>.  */
+
+static void inline_func (void);
+static void normal_func (void);
+
+volatile int global_var;
+volatile int level_counter;
+
+static void __attribute__((noinline))
+normal_func (void)
+{
+  /* Do some work.  */
+  ++global_var;
+
+  /* Now the inline function.  */
+  --level_counter;
+  inline_func ();
+  ++level_counter;
+
+  /* Do some work.  */
+  ++global_var;
+}
+
+static inline void __attribute__((__always_inline__))
+inline_func (void)
+{
+  if (level_counter > 1)
+    {
+      --level_counter;
+      normal_func ();
+      ++level_counter;
+    }
+  else
+    ++global_var;	/* Break here.  */
+}
+
+int
+main ()
+{
+  level_counter = 6;
+  normal_func ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
new file mode 100644
index 00000000000..2801b683a03
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
@@ -0,0 +1,145 @@
+# Copyright (C) 2021 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 checks for an edge case when unwinding inline frames which
+# occur towards the older end of the stack when the stack ends with a
+# cycle.  Consider this well formed stack:
+#
+#   main -> normal_frame -> inline_frame
+#
+# Now consider that, for whatever reason, the stack unwinding of
+# "normal_frame" becomes corrupted, such that the stack appears to be
+# this:
+#
+#   .-> normal_frame -> inline_frame
+#   |      |
+#   '------'
+#
+# When confronted with such a situation we would expect GDB to detect
+# the stack frame cycle and terminate the backtrace at the first
+# instance of "normal_frame" with a message:
+#
+#   Backtrace stopped: previous frame identical to this frame (corrupt stack?)
+#
+# However, at one point there was a bug in GDB's inline frame
+# mechanism such that the fact that "inline_frame" was inlined into
+# "normal_frame" would cause GDB to trigger an assertion.
+#
+# This text makes use of a Python unwinder which can fake the cyclic
+# stack cycle, further the test sets up multiple levels of normal and
+# inline frames.  At the point of testing the stack looks like this:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Where "normal_func" is a normal frame, and "inline_func" is an inline frame.
+#
+# The python unwinder is then used to force a stack cycle at each
+# "normal_func" frame in turn, we then check that GDB can successfully unwind
+# the stack.
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}]} {
+    return -1
+}
+
+# Skip this test if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to the breakpoint where we will carry out the test.
+gdb_breakpoint [gdb_get_line_number "Break here"]
+gdb_continue_to_breakpoint "stop at test breakpoint"
+
+# Load the script containing the unwinder, this must be done at the
+# testing point as the script will examine the stack as it is loaded.
+gdb_test_no_output "source ${pyfile}"\
+    "import python scripts"
+
+# Check the unbroken stack.
+gdb_test_sequence "bt" "backtrace when the unwind is left unbroken" {
+    "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#1 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#2 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#3 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#4 \[^\r\n\]* inline_func \\(\\) at "
+    "\\r\\n#5 \[^\r\n\]* normal_func \\(\\) at "
+    "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
+}
+
+with_test_prefix "cycle at level 5" {
+    # Arrange to introduce a stack cycle at frame 5.
+    gdb_test_no_output "python stop_at_level=5"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 5" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#4 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#5 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 3" {
+    # Arrange to introduce a stack cycle at frame 3.
+    gdb_test_no_output "python stop_at_level=3"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 3" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+with_test_prefix "cycle at level 1" {
+    # Arrange to introduce a stack cycle at frame 1.
+    gdb_test_no_output "python stop_at_level=1"
+    gdb_test "maint flush register-cache" \
+	"Register cache flushed\\."
+    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 1" \
+	[multi_line \
+	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
+	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+}
+
+# Flush the register cache (which also flushes the frame cache) so we
+# get a full backtrace again, then switch on frame debugging and try
+# to back trace.  At one point this triggered an assertion.
+gdb_test "maint flush register-cache" \
+    "Register cache flushed\\." ""
+gdb_test_no_output "set debug frame 1"
+gdb_test_multiple "bt" "backtrace with debugging on" {
+    -re "^$gdb_prompt $" {
+	pass $gdb_test_name
+    }
+    -re "\[^\r\n\]+\r\n" {
+	exp_continue
+    }
+}
+gdb_test "p 1 + 2 + 3" " = 6" \
+    "ensure GDB is still alive"
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
new file mode 100644
index 00000000000..99c571f973c
--- /dev/null
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2021 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/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+# Set this to the stack level the backtrace should be corrupted at.
+# This will only work for frame 1, 3, or 5 in the test this unwinder
+# was written for.
+stop_at_level = None
+
+# Set this to the stack frame size of frames 1, 3, and 5.  These
+# frames will all have the same stack frame size as they are the same
+# function called recursively.
+stack_adjust = None
+
+
+class FrameId(object):
+    def __init__(self, sp, pc):
+        self._sp = sp
+        self._pc = pc
+
+    @property
+    def sp(self):
+        return self._sp
+
+    @property
+    def pc(self):
+        return self._pc
+
+
+class TestUnwinder(Unwinder):
+    def __init__(self):
+        Unwinder.__init__(self, "stop at level")
+
+    def __call__(self, pending_frame):
+        global stop_at_level
+        global stack_adjust
+
+        if stop_at_level is None or pending_frame.level() != stop_at_level:
+            return None
+
+        if stack_adjust is None:
+            raise gdb.GdbError("invalid stack_adjust")
+
+        if not stop_at_level in [1, 3, 5]:
+            raise gdb.GdbError("invalid stop_at_level")
+
+        sp_desc = pending_frame.architecture().registers().find("sp")
+        sp = pending_frame.read_register(sp_desc) + stack_adjust
+        pc = (gdb.lookup_symbol("normal_func"))[0].value().address
+        unwinder = pending_frame.create_unwind_info(FrameId(sp, pc))
+
+        for reg in pending_frame.architecture().registers("general"):
+            val = pending_frame.read_register(reg)
+            unwinder.add_saved_register(reg, val)
+        return unwinder
+
+
+gdb.unwinder.register_unwinder(None, TestUnwinder(), True)
+
+# When loaded, it is expected that the stack looks like:
+#
+#   main -> normal_func -> inline_func -> normal_func -> inline_func -> normal_func -> inline_func
+#
+# Compute the stack frame size of normal_func, which has inline_func
+# inlined within it.
+f0 = gdb.newest_frame()
+f1 = f0.older()
+f2 = f1.older()
+f0_sp = f0.read_register("sp")
+f2_sp = f2.read_register("sp")
+stack_adjust = f2_sp - f0_sp

^ permalink raw reply	[flat|nested] 48+ messages in thread

end of thread, other threads:[~2021-09-22 16:46 UTC | newest]

Thread overview: 48+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-05-29 20:57 [PATCH 0/5] Fix for an assertion when unwinding with inline frames Andrew Burgess
2021-05-29 20:57 ` [PATCH 1/5] gdb/python: handle saving user registers in a frame unwinder Andrew Burgess
2021-06-07 14:50   ` Tom Tromey
2021-06-07 16:10     ` Andrew Burgess
2021-06-07 20:38       ` Tom Tromey
2021-06-07 17:07   ` Lancelot SIX
2021-06-07 17:20     ` Simon Marchi
2021-06-07 18:01       ` Lancelot SIX
2021-06-07 18:09         ` Simon Marchi
2021-06-07 20:12         ` Andrew Burgess
2021-06-21 19:41   ` Andrew Burgess
2021-05-29 20:57 ` [PATCH 2/5] gdb/python: move PyLong_From* calls into py-utils.c Andrew Burgess
2021-06-07 14:53   ` Tom Tromey
2021-06-21 19:42     ` Andrew Burgess
2021-05-29 20:57 ` [PATCH 3/5] gdb/python: add PendingFrame.level and Frame.level methods Andrew Burgess
2021-05-30  5:55   ` Eli Zaretskii
2021-05-30 18:34   ` Andrew Burgess
2021-05-30 18:54     ` Eli Zaretskii
2021-06-07 14:57   ` Tom Tromey
2021-06-21 19:42   ` Andrew Burgess
2021-05-29 20:57 ` [PATCH 4/5] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
2021-05-29 20:57 ` [PATCH 5/5] gdb: remove VALUE_FRAME_ID Andrew Burgess
2021-06-21 19:46 ` [PATCHv2 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
2021-06-21 19:46   ` [PATCHv2 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
2021-07-05 11:39     ` Pedro Alves
2021-07-05 14:14       ` Simon Marchi
2021-06-21 19:46   ` [PATCHv2 2/2] gdb: remove VALUE_FRAME_ID Andrew Burgess
2021-06-29 17:53     ` Simon Marchi
2021-06-30 15:18       ` Andrew Burgess
2021-07-05 14:22         ` Simon Marchi
2021-07-20  9:10   ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Andrew Burgess
2021-07-20  9:10     ` [PATCHv3 1/2] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
2021-07-20  9:10     ` [PATCHv3 2/2] gdb: remove VALUE_FRAME_ID Andrew Burgess
2021-07-20 21:59     ` [PATCHv3 0/2] Fix for an assertion when unwinding with inline frames Simon Marchi
2021-07-26 11:11       ` Andrew Burgess
2021-07-26 13:57         ` Simon Marchi
2021-07-27 10:06           ` Andrew Burgess
2021-07-27 10:10     ` [PATCHv4] gdb: prevent an assertion when computing the frame_id for an inline frame Andrew Burgess
2021-08-09 15:41       ` [PATCHv5] " Andrew Burgess
2021-08-23  9:41         ` Andrew Burgess
2021-08-23 10:26           ` Pedro Alves
2021-08-23 12:31             ` Andrew Burgess
2021-09-20 10:04               ` Andrew Burgess
2021-09-20 12:24         ` Pedro Alves
2021-09-21 13:52           ` Andrew Burgess
2021-09-21 13:54         ` [PATCHv6] " Andrew Burgess
2021-09-22 14:14           ` Simon Marchi
2021-09-22 16:46             ` Andrew Burgess

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).