[PATCH 5/5] gdb: Fix Python unwinders and inline frames

Andrew Burgess
2020-06-17T17:38:23Z

I observed that when making use of a Python frame unwinder, if the
frame sniffing code accessed any registers within an inline frame then
GDB would crash with this error:

gdb/frame.c:579: internal-error: frame_id get_frame_id(frame_info*): Assertion `fi->level == 0' failed.

The problem is that, when in the Python unwinder I write this:

pending_frame.read_register ("register-name")

This is translated internally into a call to `value_of_register',
which in turn becomes a call to `value_of_register_lazy'.

Usually this isn't a problem, `value_of_register_lazy' requires the
next frame (more inner) to have a valid frame_id, which will be the
case (if we're sniffing frame #1, then frame #0 will have had its
frame-id figured out).

Unfortunately if frame #0 is inline within frame #1, then the frame-id
for frame #0 _is_ same as the frame-id for frame #1 - the frame-id for
frame #0 requires us to first compute the frame-id for frame #1. As a
result it is not same to call `value_of_register_lazy' before the
frame-id of the current frame is known.

The solution I propose here is that `value_of_register' stops making a
lazy register value (the step that requires the frame-id, and thus
causes the problems), and instead calls `get_frame_register_value'.
This avoids the need to calculate the frame-id and so avoids the
problem.

This bug has crept in because we allowed calls to the function
`value_of_register_lazy' at a time when the frame-id of the frame
passed to the function didn't have its frame-id calculated. This is
only ever a problem for inline frames. To prevent bugs like this
getting into GDB in the future I've added an assert to the function
`value_of_register_lazy' to require the current frame have its id
calculated.

If we are calling from outside of the frame sniffer then this will
_always_ be true, so is not a problem. If we are calling this
function from within a frame sniffer then it will always trigger.

gdb/ChangeLog:

* findvar.c (value_of_register): Use get_frame_register_value
rather than value_of_register_lazy.
(value_of_register_lazy): Add new assert, and update comments.

gdb/testsuite/ChangeLog:

* gdb.python/py-unwind-inline.c: New file.
* gdb.python/py-unwind-inline.exp: New file.
* gdb.python/py-unwind-inline.py: New file.
---
gdb/ChangeLog | 6 ++
gdb/findvar.c | 28 ++++++--
gdb/testsuite/ChangeLog | 6 ++
gdb/testsuite/gdb.python/py-unwind-inline.c | 37 ++++++++++
gdb/testsuite/gdb.python/py-unwind-inline.exp | 49 +++++++++++++
gdb/testsuite/gdb.python/py-unwind-inline.py | 71 +++++++++++++++++++
6 files changed, 192 insertions(+), 5 deletions(-)
create mode 100644 gdb/testsuite/gdb.python/py-unwind-inline.c
create mode 100644 gdb/testsuite/gdb.python/py-unwind-inline.exp
create mode 100644 gdb/testsuite/gdb.python/py-unwind-inline.py

diff --git a/gdb/findvar.c b/gdb/findvar.c
index c7cd31ce1a6..e5445b34930 100644
--- a/gdb/findvar.c
+++ b/gdb/findvar.c
@@ -263,16 +263,13 @@ struct value *
value_of_register (int regnum, struct frame_info *frame)
{
struct gdbarch *gdbarch = get_frame_arch (frame);
- struct value *reg_val;

/* User registers lie completely outside of the range of normal
registers. Catch them early so that the target never sees them. */
if (regnum >= gdbarch_num_cooked_regs (gdbarch))
return value_of_user_reg (regnum, frame);

- reg_val = value_of_register_lazy (frame, regnum);
- value_fetch_lazy (reg_val);
- return reg_val;
+ return get_frame_register_value (frame, regnum);
}

/* Return a `value' with the contents of (virtual or cooked) register
@@ -292,7 +289,28 @@ value_of_register_lazy (struct frame_info *frame, int regnum)

next_frame = get_next_frame_sentinel_okay (frame);

- /* We should have a valid next frame. */
+ /* If NEXT_FRAME is inline within FRAME, then calculating the frame-id
+ for the next frame will require that FRAME has a valid frame-id.
+ Usually this is not a problem, however, if this function is ever
+ called from a frame sniffer, the small window of time when not all
+ frames have yet had their frame-id calculated, this function will
+ trigger an assert within get_frame_id that a frame at a level > 0
+ doesn't have a frame id.
+
+ Instead of waiting for an inline frame to reveal an invalid use of
+ this function, we instead demand that FRAME must have had its frame-id
+ calculated before we use this function.
+
+ The one time it is safe to call this function when FRAME does not yet
+ have a frame id is when FRAME is at level 0, in this case the
+ assertion in get_frame_id doesn't fire, and instead get_frame_id will
+ correctly compute the frame id for us. */
+ gdb_assert (frame_relative_level (frame) == 0
+ || frame_id_computed_p (frame));
+
+ /* We should have a valid next frame too. Given that FRAME is more
+ outer than NEXT_FRAME, and we know that FRAME has its ID calculated,
+ then this must always be true. */
gdb_assert (frame_id_p (get_frame_id (next_frame)));

reg_val = allocate_value_lazy (register_type (gdbarch, regnum));
diff --git a/gdb/testsuite/gdb.python/py-unwind-inline.c b/gdb/testsuite/gdb.python/py-unwind-inline.c
new file mode 100644
index 00000000000..0cfe06dd273
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-unwind-inline.c
@@ -0,0 +1,37 @@
+/* Copyright 2019-2020 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;
+
+int __attribute__ ((noinline))
+bar ()
+{
+ return global_var;
+}
+
+static inline int __attribute__ ((always_inline))
+foo ()
+{
+ return bar ();
+}
+
+int
+main ()
+{
+ int ans;
+ global_var = 0;
+ ans = foo ();
+ return ans;
+}
diff --git a/gdb/testsuite/gdb.python/py-unwind-inline.exp b/gdb/testsuite/gdb.python/py-unwind-inline.exp
new file mode 100644
index 00000000000..f7c65f6afc8
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-unwind-inline.exp
@@ -0,0 +1,49 @@
+# Copyright (C) 2020 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 script tests GDB's handling of using a Python unwinder in the
+# presence of inlined frames.
+
+load_lib gdb-python.exp
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile} \
+ debug] } {
+ return -1
+}
+
+# Skip all tests if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+# The following tests require execution.
+if ![runto_main] then {
+ fail "can't run to main"
+ return 0
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+gdb_breakpoint "foo"
+
+gdb_test "source ${pyfile}" "Python script imported" \
+ "import python scripts"
+
+gdb_continue_to_breakpoint "foo"
+
+gdb_test_sequence "backtrace" "backtrace with dummy unwinder" {
+ "\\r\\n#0 foo \\(\\)"
+ "\\r\\n#1 main \\(\\)"
+}
diff --git a/gdb/testsuite/gdb.python/py-unwind-inline.py b/gdb/testsuite/gdb.python/py-unwind-inline.py
new file mode 100644
index 00000000000..7206a0b2830
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-unwind-inline.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2020 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/>.
+
+# A dummy stack unwinder used for testing the Python unwinders when we
+# have inline frames. This unwinder will never claim any frames,
+# instead, all it does it try to read all registers possible target
+# registers as part of the frame sniffing process..
+
+import gdb
+from gdb.unwinder import Unwinder
+
+apb_global = None
+
+class dummy_unwinder (Unwinder):
+ """ A dummy unwinder that looks at a bunch of registers as part of
+ the unwinding process."""
+
+ class frame_id (object):
+ """ Basic frame id."""
+
+ def __init__ (self, sp, pc):
+ """ Create the frame id."""
+ self.sp = sp
+ self.pc = pc
+
+ def __init__ (self):
+ """Create the unwinder."""
+ Unwinder.__init__ (self, "dummy stack unwinder")
+ self.void_ptr_t = gdb.lookup_type("void").pointer()
+ self.regs = None
+
+ def get_regs (self, pending_frame):
+ """Return a list of register names that should be read. Only
+ gathers the list once, then caches the result."""
+ if (self.regs != None):
+ return self.regs
+
+ # Collect the names of all registers to read.
+ self.regs = list (pending_frame.architecture ()
+ .register_names ())
+
+ return self.regs
+
+ def __call__ (self, pending_frame):
+ """Actually performs the unwind, or at least sniffs this frame
+ to see if the unwinder should claim it, which is never does."""
+ try:
+ for r in (self.get_regs (pending_frame)):
+ v = pending_frame.read_register (r).cast (self.void_ptr_t)
+ except:
+ print ("Dummy unwinder, exception")
+ raise
+
+ return None
+
+# Register the ComRV stack unwinder.
+gdb.unwinder.register_unwinder (None, dummy_unwinder (), True)
+
+print ("Python script imported")
--
2.25.4