From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from simark.ca by simark.ca with LMTP id ySR3Mj6tdGnbwRIAWB0awg (envelope-from ) for ; Sat, 24 Jan 2026 06:30:06 -0500 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=Y+PNlcAJ; dkim-atps=neutral Received: by simark.ca (Postfix, from userid 112) id C828A1E0E1; Sat, 24 Jan 2026 06:30:06 -0500 (EST) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on simark.ca X-Spam-Level: X-Spam-Status: No, score=-3.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,RCVD_IN_VALIDITY_CERTIFIED_BLOCKED, RCVD_IN_VALIDITY_RPBL_BLOCKED,RCVD_IN_VALIDITY_SAFE_BLOCKED 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 2187E1E08D for ; Sat, 24 Jan 2026 06:30:05 -0500 (EST) Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id 8BB044BC894A for ; Sat, 24 Jan 2026 11:30:04 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 8BB044BC894A 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=Y+PNlcAJ 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 3C70B4BA9002 for ; Sat, 24 Jan 2026 11:29:28 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 3C70B4BA9002 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 3C70B4BA9002 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=170.10.133.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1769254168; cv=none; b=tSaueIgYo+2Y/0e8aTPuxtkvK6JbALqOoR9bl5hWm3Zqn0wAjbQZrxyM/Q7bXgi0Drjjw85NUNit8gyzgGMjc/1W+lJVktkJASko6p8GRhzxFP1M6xXFXu4Uj09SgyKFDkTSuTkU4OlHef5cTVi9AwH+VBdYAzrMGQH+ggfCCMI= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1769254168; c=relaxed/simple; bh=q+iCznUpq8LM4UzOeHJ2CuvQzXSTqhlFjN2O4YToz7Q=; h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version; b=mLwVv2kmCXLF+0S9/0QHGrTdqs1EICZZEvOhnSi2+73fkqPFpmcMQS1p1PMDbF4qcUIbJON9mTyv1SG2XjwePDSVE22wxdhMOVlu9sMC+d3GZLhVHsTliecv8eDjIg4kVBFt9gtZowTbV7nJcydYIeZPTfvYcGRt0t1xOjs8eYs= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 3C70B4BA9002 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1769254167; 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=rfGtZVJEcXtbj7e1U2wkHLwlvBs5kL70Nb8e7fE3S+4=; b=Y+PNlcAJl9M2XxcPBIc+dkDD71IYa/uzBZ9wyK9c+TAJQGhaf32FG24qGZLiQnS4g7q7ws kMJfQSM6WctH57G4olFQCyY+GeDvK7Smggms+3pk7svP7tPwDXI8kTOJMYnVFF07y1MN7h rcEpCJB57clSv8IHaBQ/GbeJSyECq5I= Received: from mail-wm1-f70.google.com (mail-wm1-f70.google.com [209.85.128.70]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-118-ICjUNcd0NoiM2UlQklF9QA-1; Sat, 24 Jan 2026 06:29:26 -0500 X-MC-Unique: ICjUNcd0NoiM2UlQklF9QA-1 X-Mimecast-MFC-AGG-ID: ICjUNcd0NoiM2UlQklF9QA_1769254165 Received: by mail-wm1-f70.google.com with SMTP id 5b1f17b1804b1-47ee33324e8so23160325e9.1 for ; Sat, 24 Jan 2026 03:29:25 -0800 (PST) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1769254164; x=1769858964; 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; bh=rfGtZVJEcXtbj7e1U2wkHLwlvBs5kL70Nb8e7fE3S+4=; b=RotKs4dri9/o5GuO7wbxzGq8NL+Y2R027VsAW4VkT4xm5v0l5fE4a/CrrwVjnKpbmB 1BUaGWElUOZeudhrjMWcpPjJ1T9uHqRV23g6huO8O/R+uSpB3dwJQefybx3xK94hst6R vVLaYYBdWTSXj+7Du6ok6KshDHIomRw8M5/wwFmF6wXNvMgcTjn/tLe6Jovbyfet6Tuv euIb7ey4QcOaVw/CKLuEsGkHkLxlXtW4eq4jV82Wo6YE3sF/0zF8ZHno7Emxw2Ooc8uP faKD3lR4Xks18xWz8FOCAo63zn+B/9WqtyDhXKZpE25SvvKMxQ0v1qucZ+b/32ofPryT qxLg== X-Gm-Message-State: AOJu0YyrJNKnv/gJ4VFA1gq9ErHVjdn9FFHI0q8YJhrRKckw9AdKLvjl WPYKWAvbQZaOtLPE/+QynamUxg0ic16KrPlLI/Nu8RB+o+rbk8OpYrE89QOLoHjiA+NUHsUWCa+ B5jZRrEnsmeiYFiImpip4ceEsudFjOjcjD8HNOEpxT2WE8JCaoeB9I4PSPRU6TGJymBRdGNDG08 3jZxb2K76oyfZjv2D/7Hrj1EPPiRYM1C+o8Vj/UhlATcWFhYI= X-Gm-Gg: AZuq6aIK+/ouorI2pzCB/qEpXvLvvEfJ4EcL2id+usThlmL0t2fTEeyypso35/5PB6N 5UZIkwC1a0UMzo4rRDx0ASPZhcYi+6LtSxqx4CoHPMe+K1oNx8hytxdT8QZzxFAFC88SVV7yWqN dfgJAYLLsl2zyIQUIlpfAFVUMU1QFLY2SVVdgpUSuGbHzpdTctNRkGZY2EPk0wownb/EnEnNabA D8jLjeqDdPa11GdnPBSs2yQUi6G079PU++I18LSRpmC3Y6+i+6FvWpodRlD2jQnNHrIVioZ6F5b YXIPHtVVH7lwprcQigWZcYIHOvs6kZhhCiRq7Ee0AgJxf2yLT7bFAJZ+YqzflJnCoedZQpo0k7o mPlFCGdRjxGTWUIcAneam0HWa5jsSBBQ= X-Received: by 2002:a05:600c:190e:b0:477:58:7cf4 with SMTP id 5b1f17b1804b1-4804c947bf7mr99614185e9.4.1769254163985; Sat, 24 Jan 2026 03:29:23 -0800 (PST) X-Received: by 2002:a05:600c:190e:b0:477:58:7cf4 with SMTP id 5b1f17b1804b1-4804c947bf7mr99613915e9.4.1769254163362; Sat, 24 Jan 2026 03:29:23 -0800 (PST) Received: from localhost (92.40.184.43.threembb.co.uk. [92.40.184.43]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4804dbbb3c4sm45086605e9.13.2026.01.24.03.29.22 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 24 Jan 2026 03:29:22 -0800 (PST) From: Andrew Burgess To: gdb-patches@sourceware.org Cc: Andrew Burgess Subject: [PATCH 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Date: Sat, 24 Jan 2026 11:29:11 +0000 Message-Id: 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: lQ2nG8nVgzX2P0u-D4JQ7N5pkgHpiu3f3lYoWzTlD7Y_1769254165 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 The 3 frame_unwind_caller_WHAT functions: + frame_unwind_caller_id + frame_unwind_caller_pc + frame_unwind_caller_arch Are, I believe, currently all broken with respect to inline and tail call functions. The Python FinishBreakpoint type creates a breakpoint in the caller function which, when triggered, indicates that the FinishBreakpoint has gone out of scope. I was writing a test for the FinishBreakpoint type which included a tail call function, and the FinishBreakpoint was being created for the tail call function frame. What I observed is that the out of scope breakpoint was never being hit. The call stack in my new test looked like this: main -> tailcall_function -> normal_function I would stop in normal_function, and then create a FinishBreakpoint for the parent (tailcall_function) frame. The FinishBreakpoint's out of scope breakpoint was being correctly placed in the 'main' function, but would never trigger. The problem is that the breakpoint placed in 'main' holds a frame-id. This frame-id is the frame in which the breakpoint should trigger. This frame-id exists to prevent premature stops due to recursion. But in this case, when the breakpoint in 'main' was hit, despite no recursion having occurred, the frame-id didn't match, and so the breakpoint was ignored. The problem is that in bpfinishpy_init we call frame_unwind_caller_id to compute the frame-id of the frame in which we should stop, and frame_unwind_caller_id was returning the wrong frame-id. As far as I can tell frame_unwind_caller_id has been broken since it was updated for inline functions in commit edb3359dff90ef8a. The frame_unwind_caller_id function, and all the frame_unwind_caller_WHAT functions, are intended to return the previous frame, but should skip over any inline, or tail call frames. Let's look at an example call stack: #0 A // A normal function. #1 B // An inline function. #2 C // An inline function. #3 D // A normal function. #4 E // A normal function. Starting from #0, a normal function, frame_unwind_caller_id, should return the frame-id for #3, and this is what happens. But if we start in #1 and call frame_unwind_caller_id, then we should still return the frame-id for #3, but this is not what happens. Instead we return the frame-id for #4, skipping a frame. The problem is that frame_unwind_caller_id starts by calling skip_artificial_frames, which calls get_prev_frame_always until we reach a non-inline (or non-tail call) frame, this moves us from #1 to Then, back in frame_unwind_caller_id we call get_prev_frame_always, which moves us to #4. Then frame_unwind_caller_id finishes with a call to skip_artificial_frames, this could potentially result in additional frames being skipped, but in my example above this isn't the case. The problem here is that if skip_artificial_frames skips anything, then we have already unwound to the caller frame, and the get_prev_frame_always call in frame_unwind_caller_id is unnecessary. I propose to add a new helper function frame_unwind_caller_frame, which should do the correct thing; it unwinds one frame and then calls skip_artificial_frames. This should do exactly what is needed. Then all the frame_unwind_caller_WHAT functions will be updated to use this helper function, and just extract the required property from the resulting frame. With this fix in place I could then write the FinishBreakpoint test, which now works. I took a look for other places where frame_unwind_caller_id is used and spotted that the 'until' command does much the same thing, placing a breakpoint in the caller frame. As predicted, the 'until' command is also broken when used within a tail call frame. This patch fixes that issue too. There's also a test for the until command. --- gdb/frame.c | 35 ++++-- gdb/testsuite/gdb.base/until-in-tailcall.c | 53 +++++++++ gdb/testsuite/gdb.base/until-in-tailcall.exp | 108 ++++++++++++++++++ .../py-finish-breakpoint-tailcall.c | 45 ++++++++ .../py-finish-breakpoint-tailcall.exp | 93 +++++++++++++++ .../py-finish-breakpoint-tailcall.py | 26 +++++ 6 files changed, 349 insertions(+), 11 deletions(-) create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.c create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.exp create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py diff --git a/gdb/frame.c b/gdb/frame.c index 8cb1d0a5c42..f80b3e281b6 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -699,6 +699,23 @@ get_stack_frame_id (const frame_info_ptr &next_frame) return get_frame_id (skip_artificial_frames (next_frame)); } +/* Helper for the various frame_unwind_caller_* functions. Unwind + INITIAL_NEXT_FRAME at least one frame, but skip any artificial frames, + that is inline or tailcall frames. + + Return the Nth previous frame (as required to find a non-artificial + frame), or NULL if no previous frame could be found. */ + +static frame_info_ptr +frame_unwind_caller_frame (const frame_info_ptr &initial_next_frame) +{ + frame_info_ptr this_frame = get_prev_frame_always (initial_next_frame); + if (this_frame == nullptr) + return nullptr; + + return skip_artificial_frames (this_frame); +} + struct frame_id frame_unwind_caller_id (const frame_info_ptr &initial_next_frame) { @@ -707,15 +724,11 @@ frame_unwind_caller_id (const frame_info_ptr &initial_next_frame) unintentionally returning a null_frame_id (e.g., when a caller requests the frame ID of "main()"s caller. */ - frame_info_ptr next_frame = skip_artificial_frames (initial_next_frame); - if (next_frame == NULL) + frame_info_ptr this_frame = frame_unwind_caller_frame (initial_next_frame); + if (this_frame == nullptr) return null_frame_id; - frame_info_ptr this_frame = get_prev_frame_always (next_frame); - if (this_frame) - return get_frame_id (skip_artificial_frames (this_frame)); - else - return null_frame_id; + return get_frame_id (this_frame); } const struct frame_id null_frame_id = { 0 }; /* All zeros. */ @@ -1074,14 +1087,14 @@ frame_unwind_pc (const frame_info_ptr &this_frame) CORE_ADDR frame_unwind_caller_pc (const frame_info_ptr &initial_this_frame) { - frame_info_ptr this_frame = skip_artificial_frames (initial_this_frame); + frame_info_ptr this_frame = frame_unwind_caller_frame (initial_this_frame); /* We must have a non-artificial frame. The caller is supposed to check the result of frame_unwind_caller_id (), which returns NULL_FRAME_ID in this case. */ gdb_assert (this_frame != nullptr); - return frame_unwind_pc (this_frame); + return get_frame_pc (this_frame); } bool @@ -3135,14 +3148,14 @@ frame_unwind_arch (const frame_info_ptr &next_frame) struct gdbarch * frame_unwind_caller_arch (const frame_info_ptr &initial_next_frame) { - frame_info_ptr next_frame = skip_artificial_frames (initial_next_frame); + frame_info_ptr next_frame = frame_unwind_caller_frame (initial_next_frame); /* We must have a non-artificial frame. The caller is supposed to check the result of frame_unwind_caller_id (), which returns NULL_FRAME_ID in this case. */ gdb_assert (next_frame != nullptr); - return frame_unwind_arch (next_frame); + return get_frame_arch (next_frame); } /* Gets the language of FRAME. */ diff --git a/gdb/testsuite/gdb.base/until-in-tailcall.c b/gdb/testsuite/gdb.base/until-in-tailcall.c new file mode 100644 index 00000000000..3e3853ba6df --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-tailcall.c @@ -0,0 +1,53 @@ +/* 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 . */ + +/* This test relies on tailcall_function actually compiling to a tail call + function. we try to force this by preventing tailcall_function and + normal_function from being inlined, then compiling this file at -O2. + Still, that's no guarantee. If tailcall_function isn't a tail call, + then the test becomes pointless. */ + +volatile int global_var = 42; + +int __attribute__ ((noinline, noclone)) +normal_function (int x) +{ + return x + 1; +} + +int __attribute__ ((noinline, noclone)) +tailcall_function (int x) +{ + return normal_function (x); +} + +void __attribute__ ((noinline, noclone)) +worker_func (void) +{ + ++global_var; + ++global_var; /* Run until here. */ + ++global_var; +} + +int +main (void) +{ + int result = tailcall_function (42); + result -= global_var; /* Should stop here. */ + worker_func (); + return result; +} diff --git a/gdb/testsuite/gdb.base/until-in-tailcall.exp b/gdb/testsuite/gdb.base/until-in-tailcall.exp new file mode 100644 index 00000000000..cd66d0ceaf8 --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-tailcall.exp @@ -0,0 +1,108 @@ +# 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 . + +# Run until we have a tail call frame in the stack. The stack looks like this: +# +# main -> tailcall_function -> normal_function +# +# We stop on normal_function, and then use 'up' to go to the +# tailcall_function frame. We then use 'until NN' where 'NN' is a +# line that should not be reached until after the inferior has +# returned to main. +# +# However, when the inferior returns to main, GDB should stop the +# inferior as the 'until' has gone out of scope. +# +# At one point this was not happening as GDB would place the out of +# scope 'until' guard breakpoint in the wrong place. +# +# This test relies on the source file compiling with a tail call in +# place. + +standard_testfile + +if {[prepare_for_testing "failed to prepare" $testfile $srcfile \ + {debug optimize=-O2}]} { + return +} + +# Run until there is a tail call frame in the backtrace. Then use the +# 'until' command with a line that will not be reached until after the +# current stack has unwound back to main. We expect the until's out +# of scope breakpoint to be hit first. +# +# When USE_PARENT_FRAME_P is true we move up to the tail call frame +# and issue the 'until' command while that frame is selected. +# +# When USE_PARENT_FRAME_P is false we issue the 'until' command while +# normal_function is selected, this function is called from the tail +# call function. +# +# In both cases, we expect the out of scope breakpoint to be placed +# back in main, which is where the inferior should be stopped. +proc run_test { use_parent_frame_p } { + clean_restart $::testfile + + if {![runto normal_function]} { + return + } + + # Check the stack looks correct. + gdb_test "bt" \ + [multi_line \ + "#0\\s+normal_function\[^\r\n\]+" \ + "#1\\s+(?:$::hex in )?tailcall_function\[^\r\n\]+" \ + "#2\\s+(?:$::hex in )?main\[^\r\n\]+"] \ + "check call stack" + + # Move up to tailcall_function if needed. + if { $use_parent_frame_p } { + gdb_test "up" ".* in tailcall_function .*" \ + "move up to tailcall_function" + } + + # Where we ask the 'until' to run to. + set until_lineno [gdb_get_line_number "Run until here."] + + # Where we actually expect the inferior to stop. + set stop_lineno [gdb_get_line_number "Should stop here."] + + # Issue the 'until' command and check that the inferior stops in main + # when the until guard breakpoint notices we have left the + # tailcall_function. + set saw_stop_location false + set saw_stop_source false + gdb_test_multiple "until $until_lineno" "run until worker_func" { + -re "^until $until_lineno\r\n" { + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert { $saw_stop_location && $saw_stop_source } \ + $gdb_test_name + } + -re "^main \\(\\) at \[^\r\n\]+/$::srcfile:$stop_lineno\r\n" { + set saw_stop_location true + exp_continue + } + -re "^$stop_lineno\\s+\[^\r\n\]+\r\n" { + set saw_stop_source true + exp_continue + } + } +} + +foreach_with_prefix use_parent_frame { true false } { + run_test $use_parent_frame +} diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c new file mode 100644 index 00000000000..d129d7e41b4 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c @@ -0,0 +1,45 @@ +/* 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 . */ + +/* This test relies on tailcall_function actually compiling to a tail call + function. we try to force this by preventing tailcall_function and + normal_function from being inlined, then compiling this file at -O2. + Still, that's no guarantee. If tailcall_function isn't a tail call, + then the test becomes pointless. */ + +volatile int global_var = 42; + +int __attribute__ ((noinline, noclone)) +normal_function (int x) +{ + return x + 1; +} + +int __attribute__ ((noinline, noclone)) +tailcall_function (int x) +{ + ++global_var; + return normal_function (x); +} + +int +main (void) +{ + int result = tailcall_function (42); + result -= global_var; /* Temporary breakpoint here. */ + return result; +} diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp new file mode 100644 index 00000000000..e5b5ebc5183 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp @@ -0,0 +1,93 @@ +# 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 . + +# Check that FinishBreakpoints are reached when set for a tailcall +# frame. +# +# Frame #0 is never a tailcall frame. As such, we need to do more +# than just stop in a tailcall function and create a finish +# breakpoint. Instead, we need to stop in a function called from a +# tail call function, then walk back up the stack and create the +# finish breakpoint on the tail call frame. +# +# At one point, GDB would create the breakpoint in the correct +# location, but set the frame-id on the breakpoint for the wrong +# frame, with the result that the breakpoint would never trigger. + +load_lib gdb-python.exp + +require allow_python_tests + +standard_testfile + +if {[build_executable "failed to build" $testfile $srcfile \ + {debug optimize=-O2}]} { + return +} + +# For remote host testing. +set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] + +# Run to 'normal_function' and then try to create a FinishBreakpoint +# in the parent frame. +proc run_test {} { + clean_restart $::testfile + + if {![runto normal_function]} { + return + } + + gdb_test "bt" \ + [multi_line \ + "#0\\s+normal_function\[^\r\n\]+" \ + "#1\\s+(?:$::hex in )?tailcall_function\[^\r\n\]+" \ + "#2\\s+(?:$::hex in )?main\[^\r\n\]+"] \ + "check call stack" + + gdb_test "source $::pyfile" "Python script imported" "import python scripts" + + set lineno [gdb_get_line_number "Temporary breakpoint here."] + gdb_test "python MyFinishBreakpoint(gdb.selected_frame().older())" \ + "Temporary breakpoint $::decimal at $::hex: file \[^\r\n\]+/$::srcfile, line $lineno\\." \ + "create finish breakpoint" + + set saw_stopped_message false + set saw_breakpoint_line false + set saw_source_line false + gdb_test_multiple "continue" "" { + -re "^Stopped at MyFinishBreakpoint\r\n" { + set saw_stopped_message true + exp_continue + } + -re "^Breakpoint $::decimal, main \\(\\) at \[^\r\n\]+/$::srcfile:$lineno\r\n" { + set saw_breakpoint_line true + exp_continue + } + -re "^$lineno\\s+\[^\r\n\]+\r\n" { + set saw_source_line true + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert { $saw_stopped_message \ + && $saw_breakpoint_line \ + && $saw_source_line } $gdb_test_name + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } +} + +run_test diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py new file mode 100644 index 00000000000..bd10109a3dc --- /dev/null +++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py @@ -0,0 +1,26 @@ +# 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 + + +class MyFinishBreakpoint(gdb.FinishBreakpoint): + def stop(self): + print("Stopped at MyFinishBreakpoint") + return True + + +print("Python script imported") -- 2.25.4