From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from simark.ca by simark.ca with LMTP id 4cd4Bxwvf2purSUAWB0awg (envelope-from ) for ; Fri, 14 Aug 2026 11:07:08 -0400 Authentication-Results: simark.ca; dkim=pass (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=X7biDnGY; dkim-atps=neutral Received: by simark.ca (Postfix, from userid 112) id EACF81E167; Fri, 14 Aug 2026 11:07:07 -0400 (EDT) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on simark.ca X-Spam-Level: X-Spam-Status: No, score=-6.4 required=5.0 tests=ARC_SIGNED,ARC_VALID,BAYES_00, DKIMWL_WL_HIGH,DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,MAILING_LIST_MULTI, RCVD_IN_DNSWL_MED autolearn=ham autolearn_force=no version=4.0.1 Received: from vm01.sourceware.org (vm01.sourceware.org [38.145.34.32]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by simark.ca (Postfix) with ESMTPS id E6EC01E09B for ; Fri, 14 Aug 2026 11:07:04 -0400 (EDT) Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 707734BA9011 for ; Fri, 14 Aug 2026 15:07:04 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 707734BA9011 Authentication-Results: sourceware.org; dkim=pass (1024-bit key, unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=X7biDnGY Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by sourceware.org (Postfix) with ESMTP id 34D8A4BA798D for ; Fri, 14 Aug 2026 15:06:30 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 34D8A4BA798D Authentication-Results: sourceware.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=redhat.com ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 34D8A4BA798D Authentication-Results: sourceware.org; arc=none smtp.remote-ip=170.10.133.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1786719990; cv=none; b=RNp498eR2FVVbIp6vMK/K4UmDNw6ItvdWmIIw9hQB908yKgUhKJLAP6zK52nYr4+z5PWUIDe5PtbONbMnyAFIGyO73mNIoWZDdACu2hcZ3d7z20JwG2ycP8mpVw5p/SqnpHVBELGLck15WcZSP+YBRr95homvBEDZsbI690GLiI= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1786719990; c=relaxed/simple; bh=GxSHc6FtMs7OCyaNLMBDjdoEhuoqkZ/oU9SeJoRk7Hs=; h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version; b=v8Vvro7PwxPc3gMNcilIQFc1s2t3PY4DawVfHeqgWnlKSWgqKNFAqGvagjl52mtSDrIg5rMNEQr6LiWtk+/aqJgtRPthT7QIRwijMV3zRklAs8gAmx53yZHGTxVWMjWLabdN+6B1Jtlacb7KCegfZZtzdOtbnFHkk6DXctgCbyc= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (1024-bit key, unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=X7biDnGY DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 34D8A4BA798D DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1786719989; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=UsQQJXvs/Vce/qE0G1goQwA/BtpycAjdUrDCYFwWpq0=; b=X7biDnGYyn9n0mYYvv07xBslR1oH2wBXhl3Hx2+asYX6jBvrbNtWIlRGZpGPbsJh+IxgA6 W3cuKw6wEzLuEzIJL4BuESC0tSjn6OwkyNGcGPcUpZVrUoo7tPWfoDgGGdky2k8tVWkTIw r/aHtPMBw0ABssoyuOVtUON8t1YBiLs= Received: from mail-wm1-f69.google.com (mail-wm1-f69.google.com [209.85.128.69]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-424-MtKlF15OOCWs42BjiYIc5g-1; Fri, 14 Aug 2026 11:06:23 -0400 X-MC-Unique: MtKlF15OOCWs42BjiYIc5g-1 X-Mimecast-MFC-AGG-ID: MtKlF15OOCWs42BjiYIc5g_1786719982 Received: by mail-wm1-f69.google.com with SMTP id 5b1f17b1804b1-4957287363bso6770745e9.0 for ; Fri, 14 Aug 2026 08:06:23 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786719982; x=1787324782; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=UsQQJXvs/Vce/qE0G1goQwA/BtpycAjdUrDCYFwWpq0=; b=p4zqzCgoordsEHO72e/+SlQJhtXoEl+MhPRiP/ELymCPElg0+LCTSHFpC3sOThvtFZ Colw1vGhG33YJYNPPLEpnm06yMinKGQbh0E2h2kScPP2HkQXPuD0zpmvizSNqbK4QTz8 acCstJO15d70pKdI8FTNPG/04JYGRUvmqlLQ7ksq/iOhOUwuiohdqOzpZciiPv86v2wP 9Pl+MfnPn3hE40EC3tT2IW+4E3StXMHsA2BzW0y8s9DDKH3WkFELM4AqsUwEQqjZsiHR M1t2/njkZsjyvr6hzWAKv7o/f2WJ04Zc8j7RlLF2OquFDZ66isamWrdCFqrYRR1SuHhH Dh3Q== X-Gm-Message-State: AOJu0YwkFWgnSrvrYe9XOSqlXCo/gW1DLmGH2GVRy4WZEhcsr20ss9vW mPTEFcBm32npaDICAOVIZ+XupN3SsH8FtRH9P/FS5O2heJJaOToxWDJYdfvRrjrhFRiYie2VgCT 7zmzcJhyCsi7s7LO0y4cAurJJbfP3ZZhJAfor7w1WcFlCSGOPmpWqdG2iEfDNprlyPh/HT/sNlC 0puiC1HawmsrxN22GiMsX+bRXADSPPYVHYnrH2/KrQC3HCovQ= X-Gm-Gg: AR+sD13rVFyD+ndI5E5bXjsWJ/tssiGYbPQvCxwFxVQ5UAc4PM47RwJlmDmBVJHgPES qrpOpZK2YFnZgUPWsEgitMbUjQmFFQdwUCXLUHAHtvT0dUKFIKW/ZR7bKAZN6h6+FdYS2LjnwfG aeZeoSDxozvs1m4cZ/5sEeJkTM6LPc5c91GGuDVMxFUtn8xatwZDGx3N9R/1ktBnfieXVU+H2Zv vf82vhXEkc8IaDLbXE5gnG3YOmGEnUpD/n5PAG2gHNXBHATBKK3FtSlx51ByRS2H0fgX+VgVtpn TtM45mNoJfdaA5u/X6Br12bER7XEITT1epJWTqmmYtmDI1xe5LfBC8spjc89heXCxkI6MqkGEtN dlX3z/kpO0B7A75JoXWg= X-Received: by 2002:a05:600c:19ca:b0:499:7ce1:d8a2 with SMTP id 5b1f17b1804b1-499879426e9mr75724635e9.8.1786719981812; Fri, 14 Aug 2026 08:06:21 -0700 (PDT) X-Received: by 2002:a05:600c:19ca:b0:499:7ce1:d8a2 with SMTP id 5b1f17b1804b1-499879426e9mr75723645e9.8.1786719981094; Fri, 14 Aug 2026 08:06:21 -0700 (PDT) Received: from localhost (67.72.115.87.dyn.plus.net. [87.115.72.67]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4998ba01c62sm12719015e9.14.2026.08.14.08.06.19 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 14 Aug 2026 08:06:20 -0700 (PDT) From: Andrew Burgess To: gdb-patches@sourceware.org Cc: Andrew Burgess Subject: [RFC 1/3] gdb: limited support for frame cache flushing while building stack Date: Fri, 14 Aug 2026 16:06:06 +0100 Message-Id: <72b90c7013e2e8616321f82f69975fe7a13a2256.1786715843.git.aburgess@redhat.com> X-Mailer: git-send-email 2.25.4 In-Reply-To: References: MIME-Version: 1.0 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: BGtnrZbERBI4RUNyBRo5QY5xfujjpI0aBML2_oIPCrs_1786719982 X-Mimecast-Originator: redhat.com Content-Transfer-Encoding: 8bit content-type: text/plain; charset="US-ASCII"; x-default=true X-BeenThere: gdb-patches@sourceware.org X-Mailman-Version: 2.1.30 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 This commit adds a mechanism to GDB to allow for a limit number of frame cache flushes while GDB is building the inferior's stack. Normally GDB takes care to ensure that the frame cache is never flushed while building the inferior's stack, i.e. while creating new frames. If we did allow flushing the frame cache at will during frame creation then we would (potentially) never finish building the stack, as each frame cache flush requires that we restart building the stack from the start. The problem we have, is that as we add more Python API hooks into GDB, especially around stack unwinding, the more chance we have that a user action is going to trigger a frame cache flush. We have already seen at least one bug in this area PR gdb/32120 where an apparently innocent RemoteTargetConnection.send_packet call can trigger a frame cache flush. In the next commit I am adding support for deferred debug information downloading via debuginfod. With that commit we will initially download the .gdb_index for a debug info file, and only download the full debug info file later when GDB realises that it needs it. The problem that this cases is that GDB might realise that it needs a debug info file while it is building the stack. For example if frame 5 is in a shared library for which we are using deferred debug info downloading, then GDB will now download the required debug info file until it tries to build frame 5. Loading the deferred debug info file will trigger the new_objfile observer, which will then load any associated Python scripts. These Python scripts could trigger a frame cache flush, e.g. by installing a custom stack unwinder. The change presented here is designed specifically to handle the deferred debug info download case. In that case we only expect to see a frame cache flush the first time we attempt to create a frame; the debug info is downloaded, the support scripts are loaded, and the frame cache is flushed. If we try to create the frame again, then the debug information is already downloaded and added to GDB so we no longer expect to see a second debug info download (for the same frame), and so there should be no new objfile event, and no possible frame cache flush. This is achieved by adding a mechanism to defer the frame cache flushing, scoped_defer_reinit_frame_cache. While a scoped_defer_reinit_frame_cache is active on the stack, any frame cache flushes are postponed, this means that the frame_info objects will not be invalidated, and frame creation can continue as if the flush had not happened. This does mean that the generated frame could be invalid though. When the scoped_defer_reinit_frame_cache goes out of scope then any pending frame cache flushes are performed, invalidating any (potentially) invalid frames that have been created. In addition to the above a new helper function, with_protected_frame_cache has been added. This helper wraps around some other function call, performing the wrapped function call with a scoped_defer_reinit_frame_cache in place. After calling the wrapped function, if there is no frame cache flush pending then we are done. If there is a pending frame cache flush then the wrapped function is called a second time. Again, after this call, if there are no pending frame cache flushes, then we are done. But, if after the second call there is still a pending flush, then an error is thrown. This indicates the case where (apparently) the frame cache is being flushed every time GDB tries to build a frame. We could imagine situations where exactly 3 attempts is needed to reach a stable situation. Or 4 attempts. Or 5. Or any number we choose. The problem is we cannot just spin forever hoping that GDB eventually breaks out of the loop. At some point we need to just give up. I believe debuginfod offers a compelling case for making 2 attempts, so that's what this patch proposes. In order to support the new with_protected_frame_cache template function there's a new type trait which can be used to check that every argument in an argument pack is const. This is needed because, if the function with_protected_frame_cache is wrapping can update its arguments, then calling the wrapped function twice might not produce the expected results. --- gdb/frame.c | 124 +++++++++++++++++- .../gdb.python/py-frame-cache-flushing-lib1.c | 26 ++++ .../gdb.python/py-frame-cache-flushing-lib2.c | 26 ++++ .../gdb.python/py-frame-cache-flushing.c | 55 ++++++++ .../gdb.python/py-frame-cache-flushing.exp | 115 ++++++++++++++++ .../gdb.python/py-frame-cache-flushing.py | 52 ++++++++ gdbsupport/traits.h | 7 + 7 files changed, 402 insertions(+), 3 deletions(-) create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.c create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.exp create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.py diff --git a/gdb/frame.c b/gdb/frame.c index b91e18fad99..912404cd26a 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -97,6 +97,14 @@ static frame_info_ptr selected_frame; static frame_info_ptr sentinel_frame; +/* When nonzero, reinit_frame_cache is deferred: frame_info objects + are kept alive so that mid-operation code (e.g. the DWARF unwinder + computing a frame ID) does not encounter freed memory. */ +static unsigned int defer_reinit_frame_cache_depth = 0; + +/* Set when reinit_frame_cache was requested while deferred. */ +static bool defer_reinit_frame_cache_pending = false; + /* See frame.h. */ unsigned int @@ -424,6 +432,100 @@ scoped_restore_selected_frame::~scoped_restore_selected_frame () set_language (m_lang); } +/* RAII class to defer reinit_frame_cache calls. While an instance of + this class is alive, calls to reinit_frame_cache are deferred: the + frame cache is not cleared and frame_info objects remain valid. + When the last instance goes out of scope, a single + reinit_frame_cache call is made if any were deferred. + + This is used during deferred debuginfo downloads to prevent the + frame cache from being destroyed mid-operation (e.g. while the + DWARF unwinder is computing a frame ID). */ + +class scoped_defer_reinit_frame_cache +{ +public: + scoped_defer_reinit_frame_cache () + { + defer_reinit_frame_cache_depth++; + } + + ~scoped_defer_reinit_frame_cache () + { + gdb_assert (defer_reinit_frame_cache_depth > 0); + defer_reinit_frame_cache_depth--; + + if (defer_reinit_frame_cache_depth == 0 + && defer_reinit_frame_cache_pending) + { + frame_debug_printf ("performing deferred frame cache reinit"); + defer_reinit_frame_cache_pending = false; + reinit_frame_cache (); + } + } + + bool will_trigger_reinit () const + { + gdb_assert (defer_reinit_frame_cache_depth > 0); + return this->has_pending_reinit () && defer_reinit_frame_cache_depth == 1; + } + + bool has_pending_reinit () const + { + gdb_assert (defer_reinit_frame_cache_depth > 0); + return defer_reinit_frame_cache_pending; + } + + DISABLE_COPY_AND_ASSIGN (scoped_defer_reinit_frame_cache); +}; + +/* Call Func passing in Args while a scoped_defer_reinit_frame_cache is in + effect. Once Func completes, if the frame cache has been reinitialised + then try calling Func again. If after the second call the frame cache + has again been initialised then raise an error. Because Func can be + called multiple times, it is required that every argument in ARGS be + 'const'. + + This can be used to wrap frame unwinding related calls where an + extension language hook might trigger a frame cache flush. The hope is + that whatever action triggers the flush will only happen the first + time, and that the second time through will not result in a frame cache + flush. */ + +template >> +static decltype(auto) +with_protected_frame_cache (Func&& func, Args&&... args) +{ + FRAME_SCOPED_DEBUG_ENTER_EXIT; + + using ReturnType = std::invoke_result_t; + + for (int i = 0; i < 2; ++i) + { + scoped_defer_reinit_frame_cache defer_reinit_frame_cache; + + if constexpr (std::is_void_v) + { + std::invoke (std::forward (func), + std::forward (args)...); + + if (!defer_reinit_frame_cache.will_trigger_reinit ()) + return; + } + else + { + decltype(auto) result = std::invoke (std::forward (func), + std::forward (args)...); + + if (!defer_reinit_frame_cache.will_trigger_reinit ()) + return result; + } + } + + error ("frame cache repeatedly reinitialized"); +} + /* Flag to control debugging. */ bool frame_debug; @@ -1985,8 +2087,8 @@ invalidate_selected_frame () /* See frame.h. */ -void -select_frame (const frame_info_ptr &fi) +static void +select_frame_1 (const frame_info_ptr &fi) { gdb_assert (fi != nullptr); @@ -2062,6 +2164,12 @@ select_frame (const frame_info_ptr &fi) } } +void +select_frame (const frame_info_ptr &fi) +{ + with_protected_frame_cache (select_frame_1, fi); +} + /* Create an arbitrary (i.e. address specified by user) or innermost frame. Always returns a non-NULL value. */ @@ -2169,6 +2277,15 @@ frame_observer_target_changed (struct target_ops *target) void reinit_frame_cache (void) { + if (defer_reinit_frame_cache_depth > 0) + { + frame_debug_printf ("mark frame cache flush as pending"); + defer_reinit_frame_cache_pending = true; + return; + } + + frame_debug_printf ("flushing the frame cache"); + ++frame_cache_generation; if (!frame_stash.empty ()) @@ -2551,7 +2668,8 @@ get_prev_frame_always (const frame_info_ptr &this_frame) try { - prev_frame = get_prev_frame_always_1 (this_frame); + prev_frame = with_protected_frame_cache (get_prev_frame_always_1, + this_frame); } catch (const gdb_exception_error &ex) { diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c b/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c new file mode 100644 index 00000000000..b954375fe50 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c @@ -0,0 +1,26 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2026 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 . */ + +/* ... */ + +typedef void (*callback_t) (void); + +void +library1_function (callback_t cb) +{ + cb (); +} diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c b/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c new file mode 100644 index 00000000000..b742f2c4347 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c @@ -0,0 +1,26 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2026 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 . */ + +/* ... */ + +typedef void (*callback_t) (void); + +void +library2_function (callback_t cb) +{ + cb (); +} diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing.c b/gdb/testsuite/gdb.python/py-frame-cache-flushing.c new file mode 100644 index 00000000000..5f169d64751 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing.c @@ -0,0 +1,55 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2026 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 . */ + +/* ... */ + +typedef void (*callback_t) (void); +extern void library1_function (callback_t cb); +extern void library2_function (callback_t cb); + +volatile int global_var; + +void +f0 (void) +{ + global_var = 42; /* Break here. */ +} + +void +f1 (void) +{ + f0 (); +} + +void +f3 (void) +{ + library2_function (f1); +} + +void +f5 (void) +{ + library1_function (f3); +} + +int +main (void) +{ + f5 (); + return global_var - 42; +} diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing.exp b/gdb/testsuite/gdb.python/py-frame-cache-flushing.exp new file mode 100644 index 00000000000..988a34e66e4 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing.exp @@ -0,0 +1,115 @@ +# Copyright 2026 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 . + +# .... + +load_lib gdb-python.exp + +standard_testfile .c -lib1.c -lib2.c + +# Build the first shared library. +set lib1_testfile "lib1-${testfile}.so" +set lib1_srcfile $srcfile2 +set lib1_binfile [standard_output_file $lib1_testfile] +if { [build_executable "build $lib1_testfile" $lib1_testfile $lib1_srcfile \ + {debug build-id shlib}] } { + return +} + +# Build the second shared library. +set lib2_testfile "lib2-${testfile}.so" +set lib2_srcfile $srcfile3 +set lib2_binfile [standard_output_file $lib2_testfile] +if { [build_executable "build $lib2_testfile" $lib2_testfile $lib2_srcfile \ + {debug build-id shlib}] } { + return +} + +if { [build_executable "build executable" $testfile $srcfile \ + [list debug build-id shlib=$lib1_binfile \ + shlib=$lib2_binfile]] != 0 } { + return +} + +set remote_python_file [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] + +clean_restart + +proc test { flush_levels } { + clean_restart $::testfile + + if {![runto_main]} { + return + } + + gdb_test_no_output "source $::remote_python_file" "load python file" + + set expected_failure_level -1 + set counter 0 + foreach lvl $flush_levels { + incr counter + with_test_prefix "setup step $counter" { + gdb_test_no_output "python flush_cache_at_levels\[$lvl\] \ + = flush_cache_at_levels.get($lvl, 0) + 1" \ + "setup step $counter, prepare for cache flush at level $lvl" + set times [get_python_valueof "flush_cache_at_levels\[$lvl\]" \ + UNKNOWN "get flush count for level $lvl"] + if { $times > 1 && ($lvl < $expected_failure_level \ + || $expected_failure_level == -1) } { + set expected_failure_level $lvl + } + } + } + + gdb_breakpoint [gdb_get_line_number "Break here." $::srcfile] + + gdb_continue_to_breakpoint "breakpoint in f0" + + set full_bt_regexp \ + [list "#0 f0 \\(\\) at \[^\r\n\]+" \ + "#1 $::hex in f1 \\(\\) at \[^\r\n\]+" \ + "#2 $::hex in library2_function \\(cb=$::hex \\) at \[^\r\n\]+" \ + "#3 $::hex in f3 \\(\\) at \[^\r\n\]+" \ + "#4 $::hex in library1_function \\(cb=$::hex \\) at \[^\r\n\]+" \ + "#5 $::hex in f5 \\(\\) at \[^\r\n\]+" \ + "#6 $::hex in main \\(\\) at \[^\r\n\]+"] + + set lvl 0 + set re {} + foreach line $full_bt_regexp { + if { $lvl == $expected_failure_level } { + lappend re "frame cache repeatedly reinitialized" + break + } + + lappend re $line + incr lvl + } + + if { [llength $re] > 1 } { + set re [multi_line {*}$re] + } else { + set re [lindex $re 0] + } + + gdb_test "bt" $re "check backtrace" +} + +# Each LEVEL_LIST is a list of the frame levels at which the frame +# cache should be flushed. The frame cache will be flush once for +# each occurance of a frame level within a list. +foreach_with_prefix level_list { {} {0} {1} {2} {3} {4} {5} {6} {2 4} {0 1 2 3 4 5 6} {4 4} } { + test $level_list +} diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing.py b/gdb/testsuite/gdb.python/py-frame-cache-flushing.py new file mode 100644 index 00000000000..73644d4110b --- /dev/null +++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing.py @@ -0,0 +1,52 @@ +# Copyright (C) 2026 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 + +test_debugging = False + + +def debug_log(msg): + global test_debugging + if not test_debugging: + return + print(msg) + + +flush_cache_at_levels = {} + + +class cache_flush_unwinder(Unwinder): + def __init__(self): + super().__init__("cache_flush_unwinder") + + def __call__(self, pending_frame): + global flush_cache_at_levels + + level = pending_frame.level() + debug_log("Cache flushing unwinder at level %d" % (level)) + + if level in flush_cache_at_levels: + if flush_cache_at_levels[level] > 0: + debug_log(" '-> Flushing the frame cache") + gdb.invalidate_cached_frames() + flush_cache_at_levels[level] -= 1 + + # This unwinder never claims any frames. + return None + + +gdb.unwinder.register_unwinder(None, cache_flush_unwinder(), True) diff --git a/gdbsupport/traits.h b/gdbsupport/traits.h index 4fe05ee0e0e..be738186da2 100644 --- a/gdbsupport/traits.h +++ b/gdbsupport/traits.h @@ -104,6 +104,13 @@ using And = std::conjunction; /* Concepts-light-like helper to make SFINAE logic easier to read. */ template using Requires = typename std::enable_if::type; + +/* Type trait that can be used to ensure an argument pack contains only + constant arguments. */ +template +struct all_args_are_const + : std::bool_constant<(std::is_const_v> && ...)> +{ /* Nothing. */ }; } template -- 2.25.4