From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from simark.ca by simark.ca with LMTP id 6tzMLa4GJ2qVdTwAWB0awg (envelope-from ) for ; Mon, 08 Jun 2026 14:15:10 -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=gJBJZGhw; dkim-atps=neutral Received: by simark.ca (Postfix, from userid 112) id AB87E1E0A3; Mon, 08 Jun 2026 14:15:10 -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 7713F1E062 for ; Mon, 08 Jun 2026 14:15:09 -0400 (EDT) Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 23B7C51A4330 for ; Mon, 8 Jun 2026 18:15:08 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 23B7C51A4330 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=gJBJZGhw Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.129.124]) by sourceware.org (Postfix) with ESMTP id 5FE3B51A4327 for ; Mon, 8 Jun 2026 18:14:39 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 5FE3B51A4327 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 5FE3B51A4327 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=170.10.129.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1780942479; cv=none; b=TgxXUVsuc8D5gAOPQPkowV3YxDrYLGkYFBAZcDIfbqejRARgYmlFZiotzHhea7CK++6BlmN51uPrQk0gmuwv5kaTth6fKoyDCgKqDKxmQO/SDvrKPbxo9RgTiNRjHWDlVM4GoO5wgDJ31jHw94q1A/lNqSmmUERkfU8OaH4DJsA= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1780942479; c=relaxed/simple; bh=8unvNvAQWYK8OBChU9PsJ80TZnU5uP9dFqRGIs3bsHs=; h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version; b=KzIT65gvVoeIC0ExfIOyBlG4nuzZJxVjrb/WJoEn6fd5a5qWQUmrZy29eHYa2NoeUeg5v+CHgNn9X5Kskj9v5v0b9kg91OrTYHSLwZ3tUDiWDvwZlx7Iofua0mJrIx2Jk6h0rT3TQBJluG/E56ruxhr5LCsFQI4A9h6z58WQG80= 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=gJBJZGhw DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 5FE3B51A4327 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1780942479; 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; bh=xoIzXop8fBuqp/6K2grNhzvXPMqX2dvIhSXFJjYi8i4=; b=gJBJZGhwCaNIXPn9q76UY3KbK1hD3uZEl3QsXSJCAkBOHMcEF7uou4dd2qCwE0zIH8AgOV 365R+zmjWqq2ZeejALzSf5Ql89wnp/bq0kO7biz5zLEv/GK77NQg2zK4UFG0FTuKmxzoOM 9qlqoDw5EgoACEbAh5R7+YYrfKLw6UY= Received: from mail-wr1-f70.google.com (mail-wr1-f70.google.com [209.85.221.70]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-681-Fa7Jr9TfMR24T51d6v4YNg-1; Mon, 08 Jun 2026 14:14:37 -0400 X-MC-Unique: Fa7Jr9TfMR24T51d6v4YNg-1 X-Mimecast-MFC-AGG-ID: Fa7Jr9TfMR24T51d6v4YNg_1780942474 Received: by mail-wr1-f70.google.com with SMTP id ffacd0b85a97d-45eee3f9f03so4531749f8f.1 for ; Mon, 08 Jun 2026 11:14:35 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1780942474; x=1781547274; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=xoIzXop8fBuqp/6K2grNhzvXPMqX2dvIhSXFJjYi8i4=; b=j99iAQUXeaIFTM+MwtBc237C4m7XOsAxryjhONGxMQRLg+G0P3Io7fYr06T/9IpPco ORs2sEV6DV6ddPZmvDGWvioY3JzCklHuy4/jZdJMYLzc6/BvPBxmZYYbC6g8tb7jSeqj H58K56NpM8y8149p/bTJQRgNmg0ocZd7okoQhXLCtiqUpZDqZLmYItzzUrG2efJYWL1P 5z5ZPNOdnVOdI3urns5Rdc5Hq/VL8FkL2dYLP2Nm5jiy1fgamLrN1OLSXrLSMo8/y13T k8oK2yVLjFJPEk42ckSdd2SGLLzm5NlxzDSv4bLoweGzZDgGJboaat4i4y5jO1x76Ni6 ygxg== X-Gm-Message-State: AOJu0Yz16pIN1UwXAeUV+JNcP2WIRpNT6E5Ff+0/IzUzjs7VN4iS9JrC e02HivvLRpPANpbIIEsoAArj7bC1w9RCjsqkkNjtDQhklb5c9A81zdXf2/jR6wHoCBleZwNx8jf pKcJkTOdowywPIGLW7BrGne4tCsXSLt6D+RalNglUNlopZIU+AibDVGZ+wOydob6F6cgaILPxBr nBa3VqNL5gGj+RBeQnQyuLn0xN0kWKIifZsXwmcWdRiBdZX5E= X-Gm-Gg: Acq92OHk6kQDzBLfwVvRR6SUu5VnlV/OALnScWcl4+KQTe8yXio3WnksNsbdvIlNtsl 7fe97R3VvjLum7W3YxzLVxJKjL2ogdSXBRsCdvingi+k4rR05D2OSqdQCkGY/2sptazD54UgHBd Z4gFvAeCWuJLVfoQJs94BTqIV1onDV6i4QSY4AQA5CYNtdQEzzFo9oNK2KixOmy+DMXh0JlMNxx 8DpQxpjTF2uyHLbv8UqdgyUE6rZVz2dvy7gnzqxoMtLyue6Q0xxUdZRTKSLi41xNwZYoQwfskwU RWspi3zA50khduNL+yQXOync1nFRFAejtXsSuyDNHCMfgfnqEounsHBNpD7VnaFoHnnx5ep2BTs wnkS/WKrVHKUgFWQovZbLHBVSfw== X-Received: by 2002:a5d:5301:0:b0:45e:dabf:a00e with SMTP id ffacd0b85a97d-4603061ed2cmr19381871f8f.31.1780942474089; Mon, 08 Jun 2026 11:14:34 -0700 (PDT) X-Received: by 2002:a5d:5301:0:b0:45e:dabf:a00e with SMTP id ffacd0b85a97d-4603061ed2cmr19381824f8f.31.1780942473340; Mon, 08 Jun 2026 11:14:33 -0700 (PDT) Received: from localhost ([213.31.44.97]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-4601f3529e0sm56404165f8f.28.2026.06.08.11.14.32 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 08 Jun 2026 11:14:32 -0700 (PDT) From: Andrew Burgess To: gdb-patches@sourceware.org Cc: Andrew Burgess Subject: [PATCH] gdb: allow 'until' to work in outermost frame Date: Mon, 8 Jun 2026 19:14:30 +0100 Message-Id: <41fe591d58ba010fa771e80ca674b61e30feef2f.1780942441.git.aburgess@redhat.com> X-Mailer: git-send-email 2.25.4 MIME-Version: 1.0 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: yg4TNP4_AfFHMw3AHXQoHSo__wQzsFE0LYUmagtdje8_1780942474 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 'until' command with an argument, e.g. 'until *ADDRESS', is implemented by the until_break_command in breakpoint.c. The most important thing this function does is convert the location argument '*ADDRESS' in my example, into a vector of symtab_and_line objects. Each of these symtab_and_line objects is then used to create a temporary bp_until breakpoint. The other thing that until_break_command does is create a breakpoint in the caller frame. This breakpoint is there to ensure the inferior stops upon exiting the frame in which the 'until' command was issued. When compiling an assembler file into a static test program, if I use 'starti' to stop the inferior within the outermost frame, and then try to use 'until *ADDRESS' I see the following error: (gdb) starti Starting program: /tmp/hello Program stopped. 0x0000000000401000 in _start () (gdb) bt #0 0x0000000000401000 in _start () (gdb) until *0x0000000000401015 Warning: Cannot insert breakpoint 0. Cannot access memory at address 0x1 Command aborted. (gdb) The problem here is the breakpoint that 'until' tries to create in the caller frame. Though the 'bt' in the above example indicates that there is only a single frame, the outermost frame, this is only because GDB has specific code in get_prev_frame to stop the backtrace at the outermost frame. If we turn this off and try 'bt' again: (gdb) set backtrace past-entry on (gdb) bt #0 0x0000000000401000 in _start () #1 0x0000000000000001 in ?? () #2 0x00007fffffffac73 in ?? () #3 0x0000000000000000 in ?? () (gdb) What we are seeing here is the garbage values that happen to be in the registers tricking GDB into thinking there are frames before _start. GDB's code to handle this is in get_prev_frame, however, when 'until' creates the caller frame breakpoint it calls frame_unwind_caller_pc which calls frame_unwind_caller_frame, which skips get_prev_frame and calls get_prev_frame_always. As a result, the 'until' command will try to place a breakpoint in the bogus frame #1 shown above. As the frame is at address 0x1, which is non-writable, we see an error when trying to insert the breakpoint. In this commit I propose that we share the outer frame detection logic between get_prev_frame and frame_unwind_caller_frame. When 'backtrace past-entry' is off, which is the default, frame_unwind_caller_frame will return NULL for the outermost frame. This means that a command like 'until *ADDRESS' will no longer try to create a breakpoint in the caller frame when used in the outermost frame. If the user turns 'backtrace past-entry' on then we assume that the user knows best, and will try to place the breakpoint in the caller frame. The test for this fix ran into an issue where the frame-id for the outermost frame would change between the first instruction and later instructions in the frame. I created bug PR gdb/34245 for this issue. Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=34245 --- gdb/frame.c | 37 ++++- gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 153 ++++++++++++++++++ 3 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.c create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.exp diff --git a/gdb/frame.c b/gdb/frame.c index f64f5554f8c..5561ba0f2bd 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -74,6 +74,7 @@ set_backtrace_options user_set_backtrace_options; static frame_info_ptr get_prev_frame_raw (const frame_info_ptr &this_frame); static const char *frame_stop_reason_symbol_string (enum unwind_stop_reason reason); static frame_info_ptr create_new_frame (frame_id id); +static bool inside_entry_func (const frame_info_ptr &this_frame); /* Status of some values cached in the frame_info object. */ @@ -697,6 +698,21 @@ get_stack_frame_id (const frame_info_ptr &next_frame) return get_frame_id (skip_artificial_frames (next_frame)); } +/* If FRAME is the outermost frame, and the user has not turned on + 'backtrace past-entry', then return true, otherwise, return false. */ + +static bool +stop_unwinding_due_to_outermost_frame (const frame_info_ptr &frame) +{ + std::optional frame_pc = get_frame_pc_if_available (frame); + + return (frame->level >= 0 + && get_frame_type (frame) == NORMAL_FRAME + && !user_set_backtrace_options.backtrace_past_entry + && frame_pc.has_value () + && inside_entry_func (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. @@ -707,6 +723,21 @@ get_stack_frame_id (const frame_info_ptr &next_frame) static frame_info_ptr frame_unwind_caller_frame (const frame_info_ptr &initial_next_frame) { + /* The frames prior to the entry frame (not main, but the actual entry + frame) are usually invalid. If INITIAL_NEXT_FRAME is the entry frame + then just claim that there is no caller frame. This prevents things + like 'until' from trying to place breakpoints in the invalid frame + which GDB thinks called the entry frame. + + Of course, if the user has turned on 'backtrace past-entry' then we + assume that the user knows best, and that those frames are valid, in + which case we do unwind past the entry frame. */ + if (stop_unwinding_due_to_outermost_frame (initial_next_frame)) + { + frame_debug_printf ("inside entry func"); + return nullptr; + } + frame_info_ptr this_frame = get_prev_frame_always (initial_next_frame); if (this_frame == nullptr) return nullptr; @@ -2785,11 +2816,7 @@ get_prev_frame (const frame_info_ptr &this_frame) from main returns directly to the caller of main. Since we don't stop at main, we should at least stop at the entry point of the application. */ - if (this_frame->level >= 0 - && get_frame_type (this_frame) == NORMAL_FRAME - && !user_set_backtrace_options.backtrace_past_entry - && frame_pc.has_value () - && inside_entry_func (this_frame)) + if (stop_unwinding_due_to_outermost_frame (this_frame)) { frame_debug_got_null_frame (this_frame, "inside entry func"); return NULL; diff --git a/gdb/testsuite/gdb.base/until-in-entry-frame.c b/gdb/testsuite/gdb.base/until-in-entry-frame.c new file mode 100644 index 00000000000..9b08ea9e1d9 --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.c @@ -0,0 +1,22 @@ +/* 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 . */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.base/until-in-entry-frame.exp b/gdb/testsuite/gdb.base/until-in-entry-frame.exp new file mode 100644 index 00000000000..aaf117ac3aa --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp @@ -0,0 +1,153 @@ +# 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 . + +# Check that 'until' in the outermost frame works as expected. + +standard_testfile + +if { [build_executable "failed to build" ${testfile} $srcfile \ + { debug ldflags=-static } ] } { + return +} + +# Use 'bt 1' to get the name of the current function. Returns the +# name of the current function, or the empty string if the current +# function name cannot be established. +proc current_function_name { testname } { + set func_name "" + gdb_test_multiple "bt 1" $testname { + -re -wrap "#0\\s+\[^\r\n\]*\\s+in (\[^( \]+) \\(.*" { + set func_name $expect_out(1,string) + } + } + + return $func_name +} + +# Start the inferior with 'starti', then use 'until' within the +# outermost frame. This is the real outermost frame, not 'main'. +# +# PAST_ENTRY should be 'on' or 'off' and is used in a 'set backtrace +# past-entry ...' command. +proc run_test { use_stepi past_entry } { + clean_restart $::testfile + + if { [gdb_starti_cmd] < 0 } { + untested starti + return + } + + gdb_test "" "Program stopped\\..*" "starti" + + set func_name [current_function_name \ + "function name for first function"] + + gdb_test_no_output "set backtrace past-entry $past_entry" + + # On x86-64 GDB does get a valid frame-id for the very first + # instruction, but from the second instruction onwards it gets + # outer_frame_id. As a result an 'until' setup at the first + # instruction doesn't match later on within the same function as + # the frame-ids no longer match. + # + # Try to work around this by issuing a 'stepi' to move to the + # second instruction. + # + # This is only a heuristic though. On different architectures GDB + # might have a valid frame-id for multiple instructions within the + # outer frame, or might not have outer_frame_id for all + # instructions. + # + # Also, we need to consider that the very first instruction might + # be a control flow instruction, so using stepi could send the + # inferior to another function. We try to spot this case and + # perform an early return if that happens. + if { $use_stepi } { + gdb_test "stepi" + set func_name_after [current_function_name \ + "function name after stepi"] + if { $func_name ne $func_name_after } { + unsupported "stepi moved to another function" + return + } + } + + # If the 'until' doesn't trigger then stop in 'main' as a backup. + gdb_breakpoint main + + # Find an address to use in the 'until' command. + set until_address "" + set capture_address false + gdb_test_multiple "disassemble" "find address for until" { + -re "^=> $::hex \[^\r\n\]+\r\n" { + set capture_address true + exp_continue + } + + -re "^\\s+($::hex) \[^\r\n\]+\r\n" { + if { $capture_address } { + set until_address $expect_out(1,string) + set capture_address false + } + + exp_continue + } + + -re "^$::gdb_prompt $" { + set found_address [expr { $until_address ne "" }] + gdb_assert { $found_address } $gdb_test_name + if { !$found_address } { + return + } + } + + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } + + # Send the 'until' command and then wait for GDB to stop. See the + # notes above about the 'stepi' for why we sometimes expect to see + # GDB reach 'main' here. + send_gdb "until *${until_address}\n" + gdb_test_multiple "" "after until" { + -re -wrap "Breakpoint $::decimal, (?:\[^\r\n\]+ in )?main \\(\\).*" { + kfail "gdb/34245" "$gdb_test_name (skipped until breakpoint)" + } + + -re -wrap "$::hex in $func_name \\(\\).*" { + pass $gdb_test_name + } + + -re -wrap "Warning:\r\nCannot insert breakpoint 0\\.\r\nCannot access memory at address $::hex.*Command aborted\\." { + # With PAST_ENTRY 'on', GDB discovers some invalid frames + # past the entry frame based on whatever happens to be in + # the registers when the entry function is called. These + # invalid frames are often non-writable, so GDB will fail + # to insert the breakpoint when the inferior resumes. + # + # We still count this as a pass though; the user should + # only turn on 'backtrace past-entry' when they know that + # is needed, so failure here is totally expected. + gdb_assert { $past_entry eq "on" } "$gdb_test_name (caller breakpoint failed)" + } + } +} + +foreach_with_prefix past_entry { off on } { + foreach_with_prefix use_stepi { true false } { + run_test $use_stepi $past_entry + } +} base-commit: 4562eab73d375e57f3c5a67f62d2678d7730d7ab -- 2.25.4