* [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
* 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 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-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-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-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
* [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
* 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 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
* [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
* 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 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 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
* [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
* [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
* 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
* [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 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).