From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from simark.ca by simark.ca with LMTP id orwzOJTb/2D1dAAAWB0awg (envelope-from ) for ; Tue, 27 Jul 2021 06:10:28 -0400 Received: by simark.ca (Postfix, from userid 112) id DA8701EDFB; Tue, 27 Jul 2021 06:10:28 -0400 (EDT) X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on simark.ca X-Spam-Level: X-Spam-Status: No, score=-0.5 required=5.0 tests=DKIM_SIGNED, MAILING_LIST_MULTI,RDNS_DYNAMIC,T_DKIM_INVALID,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.2 Received: from sourceware.org (ip-8-43-85-97.sourceware.org [8.43.85.97]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by simark.ca (Postfix) with ESMTPS id 60D231E79C for ; Tue, 27 Jul 2021 06:10:26 -0400 (EDT) Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 17E30397EC20 for ; Tue, 27 Jul 2021 10:10:26 +0000 (GMT) Received: from mail-wm1-x332.google.com (mail-wm1-x332.google.com [IPv6:2a00:1450:4864:20::332]) by sourceware.org (Postfix) with ESMTPS id 2D4B53850427 for ; Tue, 27 Jul 2021 10:10:12 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.1 sourceware.org 2D4B53850427 Authentication-Results: sourceware.org; dmarc=none (p=none dis=none) header.from=embecosm.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=embecosm.com Received: by mail-wm1-x332.google.com with SMTP id o5-20020a1c4d050000b02901fc3a62af78so1446595wmh.3 for ; Tue, 27 Jul 2021 03:10:12 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=embecosm.com; s=google; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=87QsNK2RK3Lfl9DH7/J2kDDIYu2DghMhtOU7IEW/4lg=; b=LHnJifIzOGpsqFESgsul/0hJlBMZFimL3735QO2TXIfc7ZecOt8GcmfPEeM44ivW8S sVhqN2v/LdWoE/9uGo1Zx7/2sbHrXvCAB+6Lg4ByHqeKDB0HO9vqz+3su60eyTcvU/56 wp9dNjrOiWMUpeA4Gcyrc8qgcFvPsgRbHw0EJTkEPw0dOy7PDjlg4HuOyeIyzpeumux6 Kb59Upe4m9dgWdDncrVPDLl7pz6IUCzZKwK1eie1QafVMoALGxYJGBqboaWOu2EHr0V8 aQFV99tnsv41EcD8sW3S4DC4cQtJ8aanyuVL1eUHOno7jR99tbPOGb/+l3eYJ1dUeKmh /vVg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=87QsNK2RK3Lfl9DH7/J2kDDIYu2DghMhtOU7IEW/4lg=; b=UZIZ4Ye2zku2hfhesmbewlDAUpEbC0a998pwiVxCgOGUgHVZtMTu3TBN1o8UajAjae yJN/PDBkMyJl7r9ydc8JVQIZw6SJFeaB2iZzZOxIvugxlnJwMp+59nSfIGFkgdsY1W5u X2Kq+yaMmnf1fIuugQAXjDIfa2FsyH8WRyOdhPd7GPOpSvGegfMR+7noOZczJ8WywTFT OEzJnxt5TONg6j4ASZ/dwzQEuwTETnFGwXdjNc8RU55hC9PWe+n5xivg7JU7WTKK1fTo bEm0ASM7QiSJZqqAb9AEY4fJfDERiIB347d61BahR5UQzn6zZkVkhOoo6AcyKndPG59s otBA== X-Gm-Message-State: AOAM531fL9Rzm5qscrFLBjnCrda4bNEeewc2w8+fUHlUUmOePtFyl/gY WcGRzZ/5jU2S6MFY81U8ekdjMSjIyAf40w== X-Google-Smtp-Source: ABdhPJzxWEY0YcMEoID58qq9JL2qgXF0LGAL12os5PF+kQN33ippWGzWWeqdNKh7+WOS7sAp5fqmGw== X-Received: by 2002:a1c:116:: with SMTP id 22mr10855322wmb.118.1627380610804; Tue, 27 Jul 2021 03:10:10 -0700 (PDT) Received: from localhost (host86-161-16-194.range86-161.btcentralplus.com. [86.161.16.194]) by smtp.gmail.com with ESMTPSA id f17sm3128930wrr.81.2021.07.27.03.10.09 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 27 Jul 2021 03:10:10 -0700 (PDT) From: Andrew Burgess To: gdb-patches@sourceware.org Subject: [PATCHv4] gdb: prevent an assertion when computing the frame_id for an inline frame Date: Tue, 27 Jul 2021 11:10:03 +0100 Message-Id: <20210727101003.2910993-1-andrew.burgess@embecosm.com> X-Mailer: git-send-email 2.25.4 In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-BeenThere: gdb-patches@sourceware.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Gdb-patches mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: gdb-patches-bounces+public-inbox=simark.ca@sourceware.org Sender: "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 . */ + +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 . + +# 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 . + +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