* [PATCH] gdb: allow 'until' to work in outermost frame @ 2026-06-08 18:14 Andrew Burgess 2026-06-11 19:24 ` Guinevere Larsen 2026-06-11 21:59 ` [PATCHv2 0/3] " Andrew Burgess 0 siblings, 2 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-08 18:14 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess 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<CORE_ADDR> 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 <http://www.gnu.org/licenses/>. */ + +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 <http://www.gnu.org/licenses/>. + +# 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 ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH] gdb: allow 'until' to work in outermost frame 2026-06-08 18:14 [PATCH] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-06-11 19:24 ` Guinevere Larsen 2026-06-11 20:40 ` Andrew Burgess 2026-06-11 21:59 ` [PATCHv2 0/3] " Andrew Burgess 1 sibling, 1 reply; 54+ messages in thread From: Guinevere Larsen @ 2026-06-11 19:24 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 6/8/26 3:14 PM, Andrew Burgess wrote: > 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. Why not simply change frame_unwind_caller_frame to call get_prev_frame instead of get_prev_frame_always? feels like it would be a better idea to not assume that the caller is always available anyway, rather than porting the bits of work-arounds back to it > > 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<CORE_ADDR> 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 <http://www.gnu.org/licenses/>. */ > + > +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 <http://www.gnu.org/licenses/>. > + > +# 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 -- Cheers, Guinevere Larsen it/its she/her (deprecated) ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH] gdb: allow 'until' to work in outermost frame 2026-06-11 19:24 ` Guinevere Larsen @ 2026-06-11 20:40 ` Andrew Burgess 2026-06-12 12:49 ` Guinevere Larsen 0 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-06-11 20:40 UTC (permalink / raw) To: Guinevere Larsen, gdb-patches Guinevere Larsen <guinevere@redhat.com> writes: > On 6/8/26 3:14 PM, Andrew Burgess wrote: >> 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. > > Why not simply change frame_unwind_caller_frame to call get_prev_frame > instead of get_prev_frame_always? feels like it would be a better idea > to not assume that the caller is always available anyway, rather than > porting the bits of work-arounds back to it That's a great question. get_prev_frame presents a "user" configured view of the backtrace, for example it will stop at 'main', or the user can adjust the number of visible frame with 'set backtrace limit N'. In these cases it is possible that get_prev_frame will return NULL indicating that there is no previous frame, when in reality, there is a previous frame. In the context of the "until" command, where we want to place a breakpoint in the caller frame, we want that breakpoint created even if the user wouldn't normally see the caller frame in the 'bt' output. As a concrete example, if a user makes use of 'until' in the 'main' function, but the 'until' line is not hit, then GDB should stop upon return from 'main'. If we used get_prev_frame that wouldn't happen as the previous frame is usually hidden from the user. The entry frame is different. The entry frame is the very first frame, and there are no earlier frames. But, that doesn't mean that GDB doesn't see earlier frames! If the inferior starts with garbage in its registers then GDB might think there are frames before the entry frame, but the chances are these frames are completely bogus, and trying to place a breakpoint in them will only cause problems (e.g. cannot write to memory errors). If we look at all the checks in get_prev_frame there are: 1. Should the unwind stop at main? This check should be ignored for things like 'until'. 2. Has the user backtrace limit been reached? This check should be ignored for things like 'until'. 3. Has the entry frame been reached? I think this check should apply in the 'until' case. 4. Have we reached a frame with a zero $pc? I'm unsure about this one, so chose to ignore it for now. It's possible that this should also apply in the 'until' case, but I've chosen to ignore this case for now without a motivating example. A final note, I'm about to post a V2 for this patch, as what I posted here still has some problems. Thanks, Andrew ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH] gdb: allow 'until' to work in outermost frame 2026-06-11 20:40 ` Andrew Burgess @ 2026-06-12 12:49 ` Guinevere Larsen 0 siblings, 0 replies; 54+ messages in thread From: Guinevere Larsen @ 2026-06-12 12:49 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 6/11/26 5:40 PM, Andrew Burgess wrote: > Guinevere Larsen <guinevere@redhat.com> writes: > >> On 6/8/26 3:14 PM, Andrew Burgess wrote: >>> 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. >> Why not simply change frame_unwind_caller_frame to call get_prev_frame >> instead of get_prev_frame_always? feels like it would be a better idea >> to not assume that the caller is always available anyway, rather than >> porting the bits of work-arounds back to it > That's a great question. get_prev_frame presents a "user" configured > view of the backtrace, for example it will stop at 'main', or the user > can adjust the number of visible frame with 'set backtrace limit N'. In > these cases it is possible that get_prev_frame will return NULL > indicating that there is no previous frame, when in reality, there is a > previous frame. > > In the context of the "until" command, where we want to place a > breakpoint in the caller frame, we want that breakpoint created even if > the user wouldn't normally see the caller frame in the 'bt' output. > > As a concrete example, if a user makes use of 'until' in the 'main' > function, but the 'until' line is not hit, then GDB should stop upon > return from 'main'. If we used get_prev_frame that wouldn't happen as > the previous frame is usually hidden from the user. I see. The explanation makes perfect sense, but then I have further questions about either the naming convention or the functionality of get_prev_frame_always. Based on the names, without exploring functionality, it seemed to me that "get_prev_frame" should be the version called in the rest of the code, and it would itself call "get_prev_frame_always", as it has guaranteed that there must be a previous frame and would avoid or handle the errors. In this understading, the _always version is essentially just a helper that separates the inferior reading from the error handling. If some other functions can call "get_prev_frame_always" directly, I think that function should identify when no previous frame exists and not return anything (therefore using "always" as the opposition for the user option). Another option would be to rename "get_prev_frame" to make it apparent that it is the user-configured view (maybe get_user_prev_frame), and have an actual get_prev_frame that works in the (in my opinion, more intuitive) way, so that get_prev_frame_always can continue to work as it does now. Bottom line is that I think there should be a function that handles points 3 and 4 that is called by those that call "get_prev_frame_always", so that we don't need to export this logic to all callers. -- Cheers, Guinevere Larsen it/its she/her (deprecated) > > The entry frame is different. The entry frame is the very first frame, > and there are no earlier frames. But, that doesn't mean that GDB > doesn't see earlier frames! If the inferior starts with garbage in its > registers then GDB might think there are frames before the entry frame, > but the chances are these frames are completely bogus, and trying to > place a breakpoint in them will only cause problems (e.g. cannot write > to memory errors). > > If we look at all the checks in get_prev_frame there are: > > 1. Should the unwind stop at main? This check should be ignored for > things like 'until'. > > 2. Has the user backtrace limit been reached? This check should be > ignored for things like 'until'. > > 3. Has the entry frame been reached? I think this check should apply > in the 'until' case. > > 4. Have we reached a frame with a zero $pc? I'm unsure about this > one, so chose to ignore it for now. It's possible that this should > also apply in the 'until' case, but I've chosen to ignore this case > for now without a motivating example. > > A final note, I'm about to post a V2 for this patch, as what I posted > here still has some problems. > > Thanks, > Andrew > ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv2 0/3] gdb: allow 'until' to work in outermost frame 2026-06-08 18:14 [PATCH] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-11 19:24 ` Guinevere Larsen @ 2026-06-11 21:59 ` Andrew Burgess 2026-06-11 21:59 ` [PATCHv2 1/3] gdb: rename program_space::entry_point_address* functions Andrew Burgess ` (3 more replies) 1 sibling, 4 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-11 21:59 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess Series has expanded since V1! The previous iteration used program_space::entry_point_address_query to figure out which frame was the enty frame. But this frame is the entry frame within the main executable, not the actual entry frame for the whole inferior, i.e the entry frame for the run-time linker in a dynamically linked executable. That's why the test in the previous version needed to be compiled with the '-static' flag. I figured that this was just another bug in GDB, and started looking at how to fix this so that program_space::entry_point_address_query would return the actual entry address for the whole inferior. But I figure that for things like displaced stepping using the entry address of the executable probably makes the more sense, so then I started looking at ways we could make use of both possible entry address values. And that's what this series does. Patch #1 renames some functions ready for the later patches. Patch #2 adds a mechanism to access both entry address values. Patch #3 fixes 'until' using infrastructure from the previous patch. Thanks, Andrew --- Andrew Burgess (3): gdb: rename program_space::entry_point_address* functions gdb: introduce program_space::get_entry_point_info function gdb: allow 'until' to work in outermost frame gdb/NEWS | 7 + gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/doc/gdb.texinfo | 14 ++ gdb/frame.c | 36 +++- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 66 ++++++- gdb/progspace.h | 60 ++++++- gdb/solib-frv.c | 2 +- gdb/solib-svr4.c | 102 +++++++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 153 ++++++++++++++++ gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 163 ++++++++++++++++++ 16 files changed, 628 insertions(+), 20 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.c create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.exp base-commit: be655796fa92c0059224f51e4599c854ffd7440f -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv2 1/3] gdb: rename program_space::entry_point_address* functions 2026-06-11 21:59 ` [PATCHv2 0/3] " Andrew Burgess @ 2026-06-11 21:59 ` Andrew Burgess 2026-06-12 15:41 ` Pedro Alves 2026-06-11 21:59 ` [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function Andrew Burgess ` (2 subsequent siblings) 3 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-06-11 21:59 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess Rename program_space::entry_point_address to program_space::exec_entry_point_address and program_space::entry_point_address_query to program_space::exec_entry_point_address_if_available. There are two aspects to this renaming. First I replace 'query' with 'if_available' in one of the functions. I feel this better describes the function, and also is inline with how other, similar, functions are named in GDB. The second part of the renaming is to add the 'exec_' prefix to the front of both function names. When a dynamically linked inferior is started the first instruction executed is actually within the run-time linker, not within the main executable, so it could be argued that the actual entry address for the inferior is not the entry address of the main executable. However, there is an equally valid argument that the entry address of the executable is also something worth finding. The existing entry address within the inferior is used for a number of tasks in GDB, for example displaced stepping (arch-utils.c), inferior function calls (arc-tdep.c and infcall.c), and for detecting the "entry" frame. This last one, the entry frame detection is interesting. In a dynamically linked executable it could be argued that there are two entry frames. The very first frame that is executed in the inferior, this is where 'starti' stops the inferior. And then the very first frame within the main executable. I think that both of these are valid. Currently, as we can only find the entry address within the main executable, we can only identify the first frame of the main executable. And this can cause some problems, consider: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) Here frames #1 to #3 are all bogus, created by GDB based on whatever values happen to be in the registers when the inferior starts. In a later commit I'd like to fix this problem, however, I would prefer that the other users of program_space::entry_point_address continue to use the address within the main executable. So, in order to keep the distinction between the two different types of entry point, I'm renaming the existing functions with the 'exec_' prefix. There should be no user visible changes after this commit. --- gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/frame.c | 4 ++-- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 6 +++--- gdb/progspace.h | 12 ++++++------ gdb/solib-frv.c | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gdb/arc-tdep.c b/gdb/arc-tdep.c index 4936a5c8fbb..7ed83c4cae6 100644 --- a/gdb/arc-tdep.c +++ b/gdb/arc-tdep.c @@ -860,7 +860,7 @@ arc_push_dummy_code (struct gdbarch *gdbarch, CORE_ADDR sp, CORE_ADDR funaddr, struct regcache *regcache) { *real_pc = funaddr; - *bp_addr = current_program_space->entry_point_address (); + *bp_addr = current_program_space->exec_entry_point_address (); return sp; } diff --git a/gdb/arch-utils.c b/gdb/arch-utils.c index e959788bd3b..88047731ce6 100644 --- a/gdb/arch-utils.c +++ b/gdb/arch-utils.c @@ -57,7 +57,7 @@ displaced_step_at_entry_point (struct gdbarch *gdbarch) CORE_ADDR addr; int bp_len; - addr = current_program_space->entry_point_address (); + addr = current_program_space->exec_entry_point_address (); /* Inferior calls also use the entry point as a breakpoint location. We don't want displaced stepping to interfere with those diff --git a/gdb/frame.c b/gdb/frame.c index f64f5554f8c..f5a6a8919df 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2700,7 +2700,7 @@ static bool inside_entry_func (const frame_info_ptr &this_frame) { std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) return false; @@ -2776,7 +2776,7 @@ get_prev_frame (const frame_info_ptr &this_frame) added to work around that (now fixed) case. */ /* NOTE: cagney/2003-07-15: danielj (if I'm reading it right) suggested having the inside_entry_func test use the - inside_main_func() msymbol trick (along with entry_point_address() + inside_main_func() msymbol trick (along with exec_entry_point_address() I guess) to determine the address range of the start function. That should provide a far better stopper than the current heuristics. */ diff --git a/gdb/infcall.c b/gdb/infcall.c index e6b24ff5310..6d26841ede3 100644 --- a/gdb/infcall.c +++ b/gdb/infcall.c @@ -1300,7 +1300,7 @@ call_function_by_hand_dummy (struct value *function, CORE_ADDR dummy_addr; real_pc = funaddr; - dummy_addr = current_program_space->entry_point_address (); + dummy_addr = current_program_space->exec_entry_point_address (); /* A call dummy always consists of just a single breakpoint, so its address is the same as the address of the dummy. diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c index a7381677498..d03e4768792 100644 --- a/gdb/linux-tdep.c +++ b/gdb/linux-tdep.c @@ -2952,7 +2952,7 @@ linux_displaced_step_location (struct gdbarch *gdbarch) /* Determine entry point from target auxiliary vector. This avoids the need for symbols. Also, when debugging a stand-alone SPU - executable, entry_point_address () will point to an SPU + executable, exec_entry_point_address () will point to an SPU local-store address and is thus not usable as displaced stepping location. The auxiliary vector gets us the PowerPC-side entry point address instead. */ diff --git a/gdb/progspace.c b/gdb/progspace.c index 1407b058dfd..0e8516f4640 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -263,7 +263,7 @@ program_space::empty () /* See progspace.h. */ std::optional<CORE_ADDR> -program_space::entry_point_address_query () const +program_space::exec_entry_point_address_if_available () const { objfile *objf = symfile_object_file; if (objf == NULL || !objf->per_bfd->ei.entry_point_p) @@ -276,9 +276,9 @@ program_space::entry_point_address_query () const /* See progspace.h. */ CORE_ADDR -program_space::entry_point_address () const +program_space::exec_entry_point_address () const { - std::optional<CORE_ADDR> retval = entry_point_address_query (); + std::optional<CORE_ADDR> retval = exec_entry_point_address_if_available (); if (!retval.has_value ()) error (_("Entry point address is not known.")); diff --git a/gdb/progspace.h b/gdb/progspace.h index e9261ff8590..1ae1e42f3bb 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,13 +323,13 @@ struct program_space return m_target_sections; } - /* If there is a valid and known entry point in this program space, - return it. Otherwise return an empty optional. */ - std::optional<CORE_ADDR> entry_point_address_query () const; + /* If there is a valid and known entry point in the main executable of + this program space, return it. Otherwise return an empty optional. */ + std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; - /* Get the entry point address in this program space. Call error if - it is not known. */ - CORE_ADDR entry_point_address () const; + /* Get the entry point address for the main executable in this program + space. Call error if it is not known. */ + CORE_ADDR exec_entry_point_address () const; /* Return true if any objfile of this program space has partial symbols. */ diff --git a/gdb/solib-frv.c b/gdb/solib-frv.c index 4f0aac31e73..69b882e534a 100644 --- a/gdb/solib-frv.c +++ b/gdb/solib-frv.c @@ -689,7 +689,7 @@ enable_break (void) } std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) { solib_debug_printf ("Symbol file has no entry point."); -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 1/3] gdb: rename program_space::entry_point_address* functions 2026-06-11 21:59 ` [PATCHv2 1/3] gdb: rename program_space::entry_point_address* functions Andrew Burgess @ 2026-06-12 15:41 ` Pedro Alves 0 siblings, 0 replies; 54+ messages in thread From: Pedro Alves @ 2026-06-12 15:41 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2026-06-11 22:59, Andrew Burgess wrote: > Rename program_space::entry_point_address to > program_space::exec_entry_point_address and > program_space::entry_point_address_query to > program_space::exec_entry_point_address_if_available. I like this. Approved-By: Pedro Alves <pedro@palves.net> ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-11 21:59 ` [PATCHv2 0/3] " Andrew Burgess 2026-06-11 21:59 ` [PATCHv2 1/3] gdb: rename program_space::entry_point_address* functions Andrew Burgess @ 2026-06-11 21:59 ` Andrew Burgess 2026-06-12 6:07 ` Eli Zaretskii 2026-06-12 15:41 ` Pedro Alves 2026-06-11 21:59 ` [PATCHv2 3/3] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 0/4] " Andrew Burgess 3 siblings, 2 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-11 21:59 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess I noticed that when debugging a dynamically linked executable, if I started the inferior with 'starti' then used 'bt' I would see some bogus frames: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) This surprised me as 'backtrace past-entry' was off: (gdb) show backtrace past-entry Whether backtraces should continue past the entry point of a program is off. I was expecting GDB to stop the backtrace at the inferior's entry address. Frame unwinding starts in get_prev_frame, and in here we find this block: 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)) { frame_debug_got_null_frame (this_frame, "inside entry func"); return NULL; } Which uses inside_entry_func to terminate the backtrace when we reach the entry frame. The inside_entry_func function calls current_program_space->exec_entry_point_address_if_available and uses the result to figure out if we are in the entry frame. And here the problem becomes obvious, we are only checking if we are in the "entry frame" for the main executable, not for the inferior as a whole. And indeed, if I compile the same test program as a static binary, where there will be no run-time linker, and the entry point of the executable is the entry point for the inferior, then the 'bt' problem I saw above goes away. This suggests, I think, that we need to track two different entry addresses, the entry address for the executable file, and the entry address for the entire inferior. Then, for dynamically linked executables, these two addresses can be different, the former will still be the same address within the main executable file, while the latter will be the address of the entry point within the run-time linker. If we had this information then we could extend inside_entry_func to check both addresses, and the 'bt' problem seen above will be resolved. To make this information available I added a new solib_ops method, solib_ops::inferior_entry_point_address, for svr4 targets this figures out if the main executable is dynamically linked, and if it is, finds the run-time linker and uses that to compute the entry address. I then added program_space::get_entry_point_info, which returns a struct containing the two entry point addresses, one comes from the new solib_ops method, and one comes from the existing method program_space::exec_entry_point_address_if_available. With the infrastructure in place I can then update inside_entry_func to check against both entry addresses. To aid in debugging GDB, I added a new maintenance command: maintenance info entry-address which just calls program_space::get_entry_point_info and then prints the two addresses. I left this as a maintenance command as I don't see much user utility in this right now, but it made it easier for me to see what GDB was doing, so I left the command in this commit. --- gdb/NEWS | 7 + gdb/doc/gdb.texinfo | 14 ++ gdb/frame.c | 10 +- gdb/progspace.c | 60 ++++++++ gdb/progspace.h | 48 +++++++ gdb/solib-svr4.c | 102 ++++++++++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 153 +++++++++++++++++++++ 9 files changed, 404 insertions(+), 5 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp diff --git a/gdb/NEWS b/gdb/NEWS index d5214a98a57..99c9fb6999c 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -146,6 +146,13 @@ disable skip These are new aliases for 'skip delete', 'skip enable', and 'skip disable' respectively. +maint info entry-address + Display the inferior and main executable entry addresses for the + current inferior. The inferior entry address is the address of the + first instruction in the inferior that was executed. The main + executable entry address is the address of the first instruction in + the main executable that was executed. + * MI changes ** The "-trace-save" command no longer supports the "-ctf" flag. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index a698b2b8451..dd6cfd04011 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -43212,6 +43212,20 @@ Maintenance Commands Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M @end smallexample + +@kindex maint info entry-address +@item maint info entry-address +Display the entry addresses for the currently selected inferior. Two +addresses are displayed, the first is the address of the first +instruction in the inferior, and the second is for the first +instruction in the main executable, see @kbd{file} in @ref{Files, +,Commands to Specify Files}. + +For statically linked inferiors, these addresses will usually be the +same, but for dynamically linked executables these addresses can be +different, with the inferior's entry address being an address within +the run-time linker, while the main executable's entry address will +always be an address within the executable file. @end table The following command is useful for non-interactive invocations of diff --git a/gdb/frame.c b/gdb/frame.c index f5a6a8919df..3920d8d03f7 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2699,12 +2699,12 @@ inside_main_func (const frame_info_ptr &this_frame) static bool inside_entry_func (const frame_info_ptr &this_frame) { - std::optional<CORE_ADDR> entry_point - = current_program_space->exec_entry_point_address_if_available (); - if (!entry_point.has_value ()) - return false; + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); - return get_frame_func (this_frame) == *entry_point; + CORE_ADDR frame_func_addr = get_frame_func (this_frame); + return (ep_info.exec_entry_address () == frame_func_addr + || ep_info.inferior_entry_address () == frame_func_addr); } /* Return a structure containing various interesting information about diff --git a/gdb/progspace.c b/gdb/progspace.c index 0e8516f4640..8a20e8d770a 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -26,6 +26,7 @@ #include <algorithm> #include "cli/cli-style.h" #include "observable.h" +#include "arch-utils.h" /* The last program space number assigned. */ static int last_program_space_num = 0; @@ -288,6 +289,55 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ +program_space::entry_point_info +program_space::get_entry_point_info () const +{ + std::optional<CORE_ADDR> exec_entry_address + = this->exec_entry_point_address_if_available (); + + std::optional<CORE_ADDR> inferior_entry_address; + if (m_solib_ops != nullptr) + inferior_entry_address = m_solib_ops->inferior_entry_point_address (); + + return entry_point_info (std::move (inferior_entry_address), + std::move (exec_entry_address)); +} + +/* Implement the 'maint info entry-address' command. */ + +static void +maintenance_info_entry_address (const char *args, int from_tty) +{ + if (args != nullptr && *args != '\0') + error (_("unknown argument: %s"), args); + + struct gdbarch *gdbarch = get_current_arch (); + + program_space::entry_point_info ep_info + = current_program_space->get_entry_point_info (); + + /* Display a single entry address ADDR with TITLE as a description. */ + auto display_entry_address + = [&gdbarch] (const char *title, + const std::optional<CORE_ADDR> &addr) -> void + { + gdb_puts (title); + if (addr.has_value ()) + fputs_styled (paddress (gdbarch, addr.value ()), + address_style.style (), gdb_stdout); + else + fputs_styled ("<unknown>", metadata_style.style (), gdb_stdout); + gdb_puts ("\n"); + }; + + display_entry_address (_("Inferior entry address: "), + ep_info.inferior_entry_address ()); + display_entry_address (_("Executable entry address: "), + ep_info.exec_entry_address ()); +} + +/* See progspace.h. */ + bool program_space::has_partial_symbols () { @@ -498,6 +548,16 @@ initialize_progspace () _("Info about currently known program spaces."), &maintenanceinfolist); + add_cmd ("entry-address", class_maintenance, + maintenance_info_entry_address, + _("Information about the current inferior's entry addresses.\n\ +Display the address of the first instruction executed within the\n\ +inferior, and the first instruction executed within the main executable.\n\ +The entry address of the whole inferior will be <unknown> prior to the\n\ +inferior starting, and either or both addresses can be <unknown> if\n\ +GDB is unable to find the required information."), + &maintenanceinfolist); + /* There's always one program space. Note that this function isn't an automatic _initialize_foo function, since other _initialize_foo routines may need to install their per-pspace diff --git a/gdb/progspace.h b/gdb/progspace.h index 1ae1e42f3bb..d75379fe621 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,6 +323,54 @@ struct program_space return m_target_sections; } + /* Class for tracking two possible entry points that an inferior might + have, the entry point for the entire inferior, and the entry point + within the main executable. */ + struct entry_point_info + { + explicit entry_point_info (std::optional<CORE_ADDR> inferior_entry_address, + std::optional<CORE_ADDR> exec_entry_address) + : m_inferior_entry_address (std::move (inferior_entry_address)), + m_exec_entry_address (std::move (exec_entry_address)) + { /* Nothing. */ } + + DISABLE_COPY_AND_ASSIGN (entry_point_info); + + /* The entry address within the inferior as a whole. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &inferior_entry_address () const + { + return m_inferior_entry_address; + } + + /* The entry address within the main executable. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &exec_entry_address () const + { + return m_exec_entry_address; + } + + private: + /* The entry address within the inferior. This can be outside of the + main executable, e.g. for dynamically linked executables this could + be the entry address for the run-time linker. For statically linked + executables this will be the entry address of the main executable. + This can be empty if GDB doesn't know how to figure out the correct + entry address for any reason. */ + std::optional<CORE_ADDR> m_inferior_entry_address; + + /* The entry address within the main executable. This can be empty if + the main executable is not set yet, or GDB doesn't have an objfile + associated with the main executable, e.g. in some attach, or remote + debug cases. */ + std::optional<CORE_ADDR> m_exec_entry_address; + }; + + /* Return information about the entry point in the main executable, and + the entry point for the inferior, which might be different from the + main executable. */ + entry_point_info get_entry_point_info () const; + /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c index 8e3de4d3ea1..fa49670ece9 100644 --- a/gdb/solib-svr4.c +++ b/gdb/solib-svr4.c @@ -3777,6 +3777,108 @@ svr4_solib_ops::get_solibs_in_ns (int nsid) const return ns_solibs; } +/* See solib.h. */ + +std::optional<CORE_ADDR> +svr4_solib_ops::inferior_entry_point_address () const +{ + std::optional<gdb::byte_vector> interp_name_holder + = svr4_find_program_interpreter (); + + /* No interpreter means this is a static executable. Ask the + program_space for the entry address within the main executable. */ + if (!interp_name_holder.has_value ()) + return m_pspace->exec_entry_point_address_if_available (); + + /* If we can find a solib that matches the interpreter name then we can + use that to find the actual entry address of the inferior. */ + const solib *interp_solib = nullptr; + const char *interp_name = (const char *) interp_name_holder->data (); + for (const solib &so : m_pspace->solibs ()) + { + if (svr4_same_name (interp_name, so.original_name.c_str ())) + { + interp_solib = &so; + + const objfile *objfile = so.objfile; + if (objfile != nullptr && objfile->per_bfd->ei.entry_point_p) + { + const entry_info &ei = objfile->per_bfd->ei; + return (ei.entry_point + + objfile->section_offsets[ei.the_bfd_section_index]); + } + } + } + + gdb_bfd_ref_ptr tmp_bfd; + target_ops_up tmp_bfd_target; + try + { + tmp_bfd = solib_bfd_open (interp_name); + + /* Failed to open the interpreter BFD. */ + if (tmp_bfd == nullptr) + return {}; + + tmp_bfd_target = target_bfd_reopen (tmp_bfd); + } + catch (const gdb_exception &ex) + { + return {}; + } + + CORE_ADDR entry_addr + = exec_entry_point (tmp_bfd.get (), tmp_bfd_target.get ()); + + /* We found the solib for the interpreter, but we don't have a + corresponding objfile, or the objfile doesn't have entry point + information. Maybe symbols haven't been loaded yet? */ + if (interp_solib != nullptr) + { + gdb_assert (interp_solib->objfile == nullptr + || !interp_solib->objfile->per_bfd->ei.entry_point_p); + + CORE_ADDR load_addr + = this->lm_addr_check (*interp_solib, tmp_bfd.get ()); + + return load_addr + entry_addr; + } + + /* We could not find a solib corresponding to the interpreter, this might + mean that the name didn't match for some reason, or maybe GDB has + failed to create a solib due to some other issue reading the solib + list from the inferior. + + If we got here we have managed to open the expected interpreter on + disk though, so we can try using the AT_BASE value. */ + CORE_ADDR load_addr; + if (target_auxv_search (AT_BASE, &load_addr) > 0) + { + int addr_bit = gdbarch_addr_bit (current_inferior ()->arch ()); + + /* Ensure LOAD_ADDR has proper sign in its possible upper bits so + that `+ load_addr' will overflow CORE_ADDR width not creating + invalid addresses like 0x101234567 for 32bit inferiors on 64bit + GDB. */ + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) + { + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; + + gdb_assert (load_addr < space_size); + + /* ENTRY_ADDR exceeding SPACE_SIZE would be for prelinked + 64bit ld.so with 32bit executable, it should not happen. */ + if (entry_addr < space_size + && entry_addr + load_addr >= space_size) + load_addr -= space_size; + } + + return load_addr + entry_addr; + } + + return {}; +} + INIT_GDB_FILE (svr4_solib) { gdb::observers::free_objfile.attach (svr4_free_objfile_observer, diff --git a/gdb/solib-svr4.h b/gdb/solib-svr4.h index 3078a092778..f76f350d951 100644 --- a/gdb/solib-svr4.h +++ b/gdb/solib-svr4.h @@ -115,6 +115,7 @@ struct svr4_solib_ops : public solib_ops void iterate_over_objfiles_in_search_order (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const override; + std::optional<CORE_ADDR> inferior_entry_point_address () const override; /* Return the appropriate link map offsets table for the architecture. */ virtual link_map_offsets *fetch_link_map_offsets () const = 0; diff --git a/gdb/solib.h b/gdb/solib.h index 9e6c3f7346e..7ed00c63471 100644 --- a/gdb/solib.h +++ b/gdb/solib.h @@ -288,6 +288,20 @@ struct solib_ops (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const; + /* Return the inferior entry point address. This isn't always the entry + point of the main executable, but will be the actual entry point where + the inferior starts (or started) executing. + + For example on SVR4 targets, for dynamically linked executables, this + will be the entry address of the dynamic linker. But for statically + linked executables, this will be the entry point of the main + executable. + + If the entry address cannot be found then an empty optional is + returned. */ + virtual std::optional<CORE_ADDR> inferior_entry_point_address () const + { return {}; } + protected: /* The program space for which this solib_ops was created. */ program_space *m_pspace; diff --git a/gdb/testsuite/gdb.base/bt-after-starti.exp b/gdb/testsuite/gdb.base/bt-after-starti.exp new file mode 100644 index 00000000000..15a97f4c256 --- /dev/null +++ b/gdb/testsuite/gdb.base/bt-after-starti.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 <http://www.gnu.org/licenses/>. + +# Check that 'bt' from the very first instruction (where 'starti' +# stops GDB) doesn't display any unexpected frames. + +require !use_gdb_stub + +standard_testfile main.c + +# Build default executable, hopefully dynamically linked, but we check +# this below. +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +# Build a statically linked executable. +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + {debug additional_flags=-static}] } { + return +} + +# Run the 'maint info entry-address' command. Expect INF_RE to match +# the inferior entry address and EXE_RE to match the executable entry +# address. TESTNAME is used as the test name. +proc check_maint_info_entry_addr { inf_re exe_re testname } { + gdb_test "maint info entry-address" \ + [multi_line \ + "Inferior entry address: ${inf_re}" \ + "Executable entry address: ${exe_re}"] \ + $testname +} + +# Use 'readelf' to extract the entry point address from TESTFILE. +proc get_exec_entry_address { testfile } { + set readelf_program [gdb_find_readelf] + set command "exec $readelf_program -Wh [standard_output_file $testfile]" + verbose -log "command is $command" + set result [catch {{*}$command} output] + verbose -log "result is $result" + verbose -log "output is $output" + + set testname "get_exec_entry_address" + + if {$result != 0} { + fail $testname + return "UNKNOWN" + } + + if {![regexp "\n +Entry point address: +($::hex)\n" $output trash addr]} { + fail "$testname (no entry point address header)" + return "UNKNOWN" + } + + return $addr +} + +# Start TESTFILE using 'starti'. Check if the 'backtrace' command +# prints any additional frames; we don't expect any. +# +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked +# executable, otherwise TESTFILE is statically linked. +proc run_test { testfile is_dynamic } { + clean_restart $testfile + + set exe_entry_addr [get_exec_entry_address $testfile] + + # The inferior entry address is always unknown before starting the + # inferior as at the point GDB has no solib_ops, and it is only + # through that that we figure out the inferior entry address. + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ + "check 'maint info entry-address' before inferior starts" + + # Start inferior with 'starti' and then wait for a prompt. + gdb_starti_cmd + gdb_test "" ".*" "prompt after starti" + + # Only svr4 targets currently support querying the inferior entry + # address. + if {[istarget *-linux*]} { + set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN" \ + "get current program counter"] + + if { !$is_dynamic } { + # In static binaries, the first address should be the first + # address in the executable. + gdb_assert { $pc == $exe_entry_addr } \ + "stopped at executable entry address" + } + + set inf_entry_addr $pc + } else { + set inf_entry_addr "<unknown>" + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after starti" + + # Allow backtrace past the entry frame, count how many frames GDB + # finds. If on this target there are no additional frames then + # the following test isn't going to tell us much, so skip it. + gdb_test_no_output "set backtrace past-entry on" + set frame_count 0 + gdb_test_multiple "bt" "count possible frames" { + -re "^#($::decimal)\\s+\[^\r\n\]+\r\n" { + incr frame_count + exp_continue + } + -re "^$::gdb_prompt $" { + if { $frame_count > 1 } { + pass $gdb_test_name + } else { + unsupported "$gdb_test_name (insufficient frames)" + return + } + } + -re "^\[^\r\n\]*\r\n" { + exp_continue + } + } + + # Turn off backtrace past the entry frame. Use the 'bt' command, + # and check we see only a single frame. + gdb_test_no_output "set backtrace past-entry off" + gdb_test "bt" "^#0 \[^\r\n\]+" \ + "single frame when backtrace past-entry is off" +} + +with_test_prefix "dynamic" { + set dyln_name [section_get $binfile .interp] + if { $dyln_name eq "" } { + unsupported "couldn't find dynamic linker name" + } else { + run_test $testfile true + } +} + +with_test_prefix "static" { + run_test $testfile_static false +} -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-11 21:59 ` [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-06-12 6:07 ` Eli Zaretskii 2026-06-15 10:14 ` Andrew Burgess 2026-06-12 15:41 ` Pedro Alves 1 sibling, 1 reply; 54+ messages in thread From: Eli Zaretskii @ 2026-06-12 6:07 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches > From: Andrew Burgess <aburgess@redhat.com> > Cc: Andrew Burgess <aburgess@redhat.com> > Date: Thu, 11 Jun 2026 22:59:05 +0100 > > I noticed that when debugging a dynamically linked executable, if I > started the inferior with 'starti' then used 'bt' I would see some > bogus frames: > > (gdb) starti > Starting program: /tmp/hello > > Program stopped. > 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 > (gdb) bt > #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 > #1 0x0000000000000001 in ?? () > #2 0x00007fffffffac13 in ?? () > #3 0x0000000000000000 in ?? () > (gdb) > > This surprised me as 'backtrace past-entry' was off: > > (gdb) show backtrace past-entry > Whether backtraces should continue past the entry point of a program is off. > > I was expecting GDB to stop the backtrace at the inferior's entry > address. > > Frame unwinding starts in get_prev_frame, and in here we find this > block: > > 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)) > { > frame_debug_got_null_frame (this_frame, "inside entry func"); > return NULL; > } > > Which uses inside_entry_func to terminate the backtrace when we reach > the entry frame. The inside_entry_func function calls > current_program_space->exec_entry_point_address_if_available and uses > the result to figure out if we are in the entry frame. > > And here the problem becomes obvious, we are only checking if we are > in the "entry frame" for the main executable, not for the inferior as > a whole. And indeed, if I compile the same test program as a static > binary, where there will be no run-time linker, and the entry point of > the executable is the entry point for the inferior, then the 'bt' > problem I saw above goes away. > > This suggests, I think, that we need to track two different entry > addresses, the entry address for the executable file, and the entry > address for the entire inferior. Then, for dynamically linked > executables, these two addresses can be different, the former will > still be the same address within the main executable file, while the > latter will be the address of the entry point within the run-time > linker. > > If we had this information then we could extend inside_entry_func to > check both addresses, and the 'bt' problem seen above will be > resolved. > > To make this information available I added a new solib_ops method, > solib_ops::inferior_entry_point_address, for svr4 targets this figures > out if the main executable is dynamically linked, and if it is, finds > the run-time linker and uses that to compute the entry address. > > I then added program_space::get_entry_point_info, which returns a > struct containing the two entry point addresses, one comes from the > new solib_ops method, and one comes from the existing method > program_space::exec_entry_point_address_if_available. > > With the infrastructure in place I can then update inside_entry_func > to check against both entry addresses. > > To aid in debugging GDB, I added a new maintenance command: > > maintenance info entry-address > > which just calls program_space::get_entry_point_info and then prints > the two addresses. I left this as a maintenance command as I don't > see much user utility in this right now, but it made it easier for me > to see what GDB was doing, so I left the command in this commit. > --- > gdb/NEWS | 7 + > gdb/doc/gdb.texinfo | 14 ++ > gdb/frame.c | 10 +- > gdb/progspace.c | 60 ++++++++ > gdb/progspace.h | 48 +++++++ > gdb/solib-svr4.c | 102 ++++++++++++++ > gdb/solib-svr4.h | 1 + > gdb/solib.h | 14 ++ > gdb/testsuite/gdb.base/bt-after-starti.exp | 153 +++++++++++++++++++++ > 9 files changed, 404 insertions(+), 5 deletions(-) > create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp > > diff --git a/gdb/NEWS b/gdb/NEWS > index d5214a98a57..99c9fb6999c 100644 > --- a/gdb/NEWS > +++ b/gdb/NEWS > @@ -146,6 +146,13 @@ disable skip > These are new aliases for 'skip delete', 'skip enable', and 'skip > disable' respectively. > > +maint info entry-address > + Display the inferior and main executable entry addresses for the > + current inferior. The inferior entry address is the address of the > + first instruction in the inferior that was executed. The main > + executable entry address is the address of the first instruction in > + the main executable that was executed. > + > * MI changes > > ** The "-trace-save" command no longer supports the "-ctf" flag. > diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo > index a698b2b8451..dd6cfd04011 100644 > --- a/gdb/doc/gdb.texinfo > +++ b/gdb/doc/gdb.texinfo > @@ -43212,6 +43212,20 @@ Maintenance Commands > Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M > Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M > @end smallexample > + > +@kindex maint info entry-address > +@item maint info entry-address > +Display the entry addresses for the currently selected inferior. Two > +addresses are displayed, the first is the address of the first > +instruction in the inferior, and the second is for the first > +instruction in the main executable, see @kbd{file} in @ref{Files, > +,Commands to Specify Files}. > + > +For statically linked inferiors, these addresses will usually be the > +same, but for dynamically linked executables these addresses can be > +different, with the inferior's entry address being an address within > +the run-time linker, while the main executable's entry address will > +always be an address within the executable file. > @end table Thanks, the documentation parts are okay. Although I would suggest to perhaps clarify the description in the manual by explaining how these addresses are related to the first instruction in the 'main' function of the program. As written, the description is highly-technical, and I think it will only be clear enough for people who are intimately familiar with the program's startup code on different systems. Reviewed-By: Eli Zaretskii <eliz@gnu.org> ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-12 6:07 ` Eli Zaretskii @ 2026-06-15 10:14 ` Andrew Burgess 2026-06-15 12:01 ` Eli Zaretskii 0 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-06-15 10:14 UTC (permalink / raw) To: Eli Zaretskii; +Cc: gdb-patches Eli Zaretskii <eliz@gnu.org> writes: >> From: Andrew Burgess <aburgess@redhat.com> >> Cc: Andrew Burgess <aburgess@redhat.com> >> Date: Thu, 11 Jun 2026 22:59:05 +0100 >> >> I noticed that when debugging a dynamically linked executable, if I >> started the inferior with 'starti' then used 'bt' I would see some >> bogus frames: >> >> (gdb) starti >> Starting program: /tmp/hello >> >> Program stopped. >> 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 >> (gdb) bt >> #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 >> #1 0x0000000000000001 in ?? () >> #2 0x00007fffffffac13 in ?? () >> #3 0x0000000000000000 in ?? () >> (gdb) >> >> This surprised me as 'backtrace past-entry' was off: >> >> (gdb) show backtrace past-entry >> Whether backtraces should continue past the entry point of a program is off. >> >> I was expecting GDB to stop the backtrace at the inferior's entry >> address. >> >> Frame unwinding starts in get_prev_frame, and in here we find this >> block: >> >> 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)) >> { >> frame_debug_got_null_frame (this_frame, "inside entry func"); >> return NULL; >> } >> >> Which uses inside_entry_func to terminate the backtrace when we reach >> the entry frame. The inside_entry_func function calls >> current_program_space->exec_entry_point_address_if_available and uses >> the result to figure out if we are in the entry frame. >> >> And here the problem becomes obvious, we are only checking if we are >> in the "entry frame" for the main executable, not for the inferior as >> a whole. And indeed, if I compile the same test program as a static >> binary, where there will be no run-time linker, and the entry point of >> the executable is the entry point for the inferior, then the 'bt' >> problem I saw above goes away. >> >> This suggests, I think, that we need to track two different entry >> addresses, the entry address for the executable file, and the entry >> address for the entire inferior. Then, for dynamically linked >> executables, these two addresses can be different, the former will >> still be the same address within the main executable file, while the >> latter will be the address of the entry point within the run-time >> linker. >> >> If we had this information then we could extend inside_entry_func to >> check both addresses, and the 'bt' problem seen above will be >> resolved. >> >> To make this information available I added a new solib_ops method, >> solib_ops::inferior_entry_point_address, for svr4 targets this figures >> out if the main executable is dynamically linked, and if it is, finds >> the run-time linker and uses that to compute the entry address. >> >> I then added program_space::get_entry_point_info, which returns a >> struct containing the two entry point addresses, one comes from the >> new solib_ops method, and one comes from the existing method >> program_space::exec_entry_point_address_if_available. >> >> With the infrastructure in place I can then update inside_entry_func >> to check against both entry addresses. >> >> To aid in debugging GDB, I added a new maintenance command: >> >> maintenance info entry-address >> >> which just calls program_space::get_entry_point_info and then prints >> the two addresses. I left this as a maintenance command as I don't >> see much user utility in this right now, but it made it easier for me >> to see what GDB was doing, so I left the command in this commit. >> --- >> gdb/NEWS | 7 + >> gdb/doc/gdb.texinfo | 14 ++ >> gdb/frame.c | 10 +- >> gdb/progspace.c | 60 ++++++++ >> gdb/progspace.h | 48 +++++++ >> gdb/solib-svr4.c | 102 ++++++++++++++ >> gdb/solib-svr4.h | 1 + >> gdb/solib.h | 14 ++ >> gdb/testsuite/gdb.base/bt-after-starti.exp | 153 +++++++++++++++++++++ >> 9 files changed, 404 insertions(+), 5 deletions(-) >> create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp >> >> diff --git a/gdb/NEWS b/gdb/NEWS >> index d5214a98a57..99c9fb6999c 100644 >> --- a/gdb/NEWS >> +++ b/gdb/NEWS >> @@ -146,6 +146,13 @@ disable skip >> These are new aliases for 'skip delete', 'skip enable', and 'skip >> disable' respectively. >> >> +maint info entry-address >> + Display the inferior and main executable entry addresses for the >> + current inferior. The inferior entry address is the address of the >> + first instruction in the inferior that was executed. The main >> + executable entry address is the address of the first instruction in >> + the main executable that was executed. >> + >> * MI changes >> >> ** The "-trace-save" command no longer supports the "-ctf" flag. >> diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo >> index a698b2b8451..dd6cfd04011 100644 >> --- a/gdb/doc/gdb.texinfo >> +++ b/gdb/doc/gdb.texinfo >> @@ -43212,6 +43212,20 @@ Maintenance Commands >> Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M >> Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M >> @end smallexample >> + >> +@kindex maint info entry-address >> +@item maint info entry-address >> +Display the entry addresses for the currently selected inferior. Two >> +addresses are displayed, the first is the address of the first >> +instruction in the inferior, and the second is for the first >> +instruction in the main executable, see @kbd{file} in @ref{Files, >> +,Commands to Specify Files}. >> + >> +For statically linked inferiors, these addresses will usually be the >> +same, but for dynamically linked executables these addresses can be >> +different, with the inferior's entry address being an address within >> +the run-time linker, while the main executable's entry address will >> +always be an address within the executable file. >> @end table > > Thanks, the documentation parts are okay. Although I would suggest to > perhaps clarify the description in the manual by explaining how these > addresses are related to the first instruction in the 'main' function > of the program. As written, the description is highly-technical, and > I think it will only be clear enough for people who are intimately > familiar with the program's startup code on different systems. > > Reviewed-By: Eli Zaretskii <eliz@gnu.org> I rewrote the doc/ part to try and make it more approachable. I included some text describing how the reported addresses relate to the address of 'main', and why the inferior entry address exists, and is different for dynamically linked executables. Obviously the text relating to dynamically linked executables is a massive simplification, but I don't see it as our job to be teaching about dynamic linking, I figure what I've written is correct enough. If I don't hear any feedback I'll assume this is OK. Thanks, Andrew --- diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index a698b2b8451..4c0c4709a23 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -43212,6 +43212,28 @@ Maintenance Commands Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M @end smallexample + +@kindex maint info entry-address +@item maint info entry-address +Display the entry addresses for the currently selected inferior. Two +addresses are displayed, the inferior entry address and the executable +entry address. + +Neither address is the address of @code{main}. When a program is +started, low-level startup code (typically a function called +@code{_start}) runs before @code{main} is called. + +The executable entry address is the address of this startup code +within the main executable (@pxref{Files, ,Commands to Specify +Files}). + +The inferior entry address is the address of the very first +instruction executed when the inferior is started. For statically +linked programs this is the same as the executable entry address. For +dynamically linked programs the run-time linker must execute first in +order to load shared libraries, so the inferior entry address will be +an address within the run-time linker rather than within the main +executable. @end table The following command is useful for non-interactive invocations of ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-15 10:14 ` Andrew Burgess @ 2026-06-15 12:01 ` Eli Zaretskii 0 siblings, 0 replies; 54+ messages in thread From: Eli Zaretskii @ 2026-06-15 12:01 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches > From: Andrew Burgess <aburgess@redhat.com> > Cc: gdb-patches@sourceware.org > Date: Mon, 15 Jun 2026 11:14:38 +0100 > > Eli Zaretskii <eliz@gnu.org> writes: > > > Thanks, the documentation parts are okay. Although I would suggest to > > perhaps clarify the description in the manual by explaining how these > > addresses are related to the first instruction in the 'main' function > > of the program. As written, the description is highly-technical, and > > I think it will only be clear enough for people who are intimately > > familiar with the program's startup code on different systems. > > > > Reviewed-By: Eli Zaretskii <eliz@gnu.org> > > I rewrote the doc/ part to try and make it more approachable. I > included some text describing how the reported addresses relate to the > address of 'main', and why the inferior entry address exists, and is > different for dynamically linked executables. Obviously the text > relating to dynamically linked executables is a massive simplification, > but I don't see it as our job to be teaching about dynamic linking, I > figure what I've written is correct enough. > > If I don't hear any feedback I'll assume this is OK. The new text is fine by me, thanks. ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-11 21:59 ` [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function Andrew Burgess 2026-06-12 6:07 ` Eli Zaretskii @ 2026-06-12 15:41 ` Pedro Alves 2026-06-15 10:29 ` Andrew Burgess 1 sibling, 1 reply; 54+ messages in thread From: Pedro Alves @ 2026-06-12 15:41 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2026-06-11 22:59, Andrew Burgess wrote: > > +/* See solib.h. */ > + > +std::optional<CORE_ADDR> > +svr4_solib_ops::inferior_entry_point_address () const > +{ > + std::optional<gdb::byte_vector> interp_name_holder > + = svr4_find_program_interpreter (); > + > + /* No interpreter means this is a static executable. Ask the > + program_space for the entry address within the main executable. */ > + if (!interp_name_holder.has_value ()) > + return m_pspace->exec_entry_point_address_if_available (); > + > + /* If we can find a solib that matches the interpreter name then we can > + use that to find the actual entry address of the inferior. */ > + const solib *interp_solib = nullptr; > + const char *interp_name = (const char *) interp_name_holder->data (); > + for (const solib &so : m_pspace->solibs ()) > + { > + if (svr4_same_name (interp_name, so.original_name.c_str ())) > + { > + interp_solib = &so; > + > + const objfile *objfile = so.objfile; > + if (objfile != nullptr && objfile->per_bfd->ei.entry_point_p) > + { > + const entry_info &ei = objfile->per_bfd->ei; > + return (ei.entry_point > + + objfile->section_offsets[ei.the_bfd_section_index]); > + } > + } > + } > + > + gdb_bfd_ref_ptr tmp_bfd; > + target_ops_up tmp_bfd_target; > + try > + { > + tmp_bfd = solib_bfd_open (interp_name); > + > + /* Failed to open the interpreter BFD. */ > + if (tmp_bfd == nullptr) > + return {}; > + > + tmp_bfd_target = target_bfd_reopen (tmp_bfd); > + } > + catch (const gdb_exception &ex) > + { Swallows Ctrl-C. (I see you're just copying this from elsewhere.) > + return {}; > + } > + > + CORE_ADDR entry_addr > + = exec_entry_point (tmp_bfd.get (), tmp_bfd_target.get ()); > + > + /* We found the solib for the interpreter, but we don't have a > + corresponding objfile, or the objfile doesn't have entry point > + information. Maybe symbols haven't been loaded yet? */ > + if (interp_solib != nullptr) > + { > + gdb_assert (interp_solib->objfile == nullptr > + || !interp_solib->objfile->per_bfd->ei.entry_point_p); > + > + CORE_ADDR load_addr > + = this->lm_addr_check (*interp_solib, tmp_bfd.get ()); > + > + return load_addr + entry_addr; > + } > + > + /* We could not find a solib corresponding to the interpreter, this might > + mean that the name didn't match for some reason, or maybe GDB has > + failed to create a solib due to some other issue reading the solib > + list from the inferior. > + > + If we got here we have managed to open the expected interpreter on > + disk though, so we can try using the AT_BASE value. */ > + CORE_ADDR load_addr; > + if (target_auxv_search (AT_BASE, &load_addr) > 0) > + { > + int addr_bit = gdbarch_addr_bit (current_inferior ()->arch ()); > + > + /* Ensure LOAD_ADDR has proper sign in its possible upper bits so > + that `+ load_addr' will overflow CORE_ADDR width not creating > + invalid addresses like 0x101234567 for 32bit inferiors on 64bit > + GDB. */ > + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) > + { > + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; > + > + gdb_assert (load_addr < space_size); > + > + /* ENTRY_ADDR exceeding SPACE_SIZE would be for prelinked > + 64bit ld.so with 32bit executable, it should not happen. */ > + if (entry_addr < space_size > + && entry_addr + load_addr >= space_size) > + load_addr -= space_size; > + } > + > + return load_addr + entry_addr; > + } I wonder whether just looking at AT_BASE, which points at an ELF, and then adding elf_header->e_entr, wouldn't be simpler than all of this. We already use Elf32_External/Elf64_External structs in the file, so maybe the manual ELF peeking is OK. Alternatively, of lot of that code looks similar to enable_break. Maybe it could be refactored to be shared. Or enable_break could cache the entry address. > +# Use 'readelf' to extract the entry point address from TESTFILE. > +proc get_exec_entry_address { testfile } { > + set readelf_program [gdb_find_readelf] > + set command "exec $readelf_program -Wh [standard_output_file $testfile]" If you use "objdump -f" instead, this has a better chance of working on Windows PE/COFF too (eventually): $ objdump.exe -f ./foo.exe ./foo.exe: file format pei-x86-64 architecture: i386:x86-64, flags 0x0000013b: HAS_RELOC, EXEC_P, HAS_DEBUG, HAS_SYMS, HAS_LOCALS, D_PAGED start address 0x00000001400013f0 > +# Start TESTFILE using 'starti'. Check if the 'backtrace' command > +# prints any additional frames; we don't expect any. > +# > +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked > +# executable, otherwise TESTFILE is statically linked. > +proc run_test { testfile is_dynamic } { > + clean_restart $testfile > + > + set exe_entry_addr [get_exec_entry_address $testfile] > + > + # The inferior entry address is always unknown before starting the > + # inferior as at the point GDB has no solib_ops, and it is only > + # through that that we figure out the inferior entry address. > + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ > + "check 'maint info entry-address' before inferior starts" > + > + # Start inferior with 'starti' and then wait for a prompt. > + gdb_starti_cmd > + gdb_test "" ".*" "prompt after starti" > + > + # Only svr4 targets currently support querying the inferior entry > + # address. > + if {[istarget *-linux*]} { There are more svr4 targets than linux. I think this style of allow-list has a good chance of never getting updated. Deny-lists are better, IMHO. If the test fails on some port, that might trigger someone to add the feature there. > + set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN" \ > + "get current program counter"] > + > + if { !$is_dynamic } { > + # In static binaries, the first address should be the first > + # address in the executable. > + gdb_assert { $pc == $exe_entry_addr } \ > + "stopped at executable entry address" > + } > + > + set inf_entry_addr $pc > + } else { > + set inf_entry_addr "<unknown>" > + } > + > + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ > + "check 'maint info entry-address' after starti" > + > + # Allow backtrace past the entry frame, count how many frames GDB > + # finds. If on this target there are no additional frames then > + # the following test isn't going to tell us much, so skip it. > + gdb_test_no_output "set backtrace past-entry on" > + set frame_count 0 > + gdb_test_multiple "bt" "count possible frames" { > + -re "^#($::decimal)\\s+\[^\r\n\]+\r\n" { > + incr frame_count > + exp_continue > + } > + -re "^$::gdb_prompt $" { > + if { $frame_count > 1 } { > + pass $gdb_test_name > + } else { > + unsupported "$gdb_test_name (insufficient frames)" > + return > + } > + } > + -re "^\[^\r\n\]*\r\n" { > + exp_continue > + } This looks like could use -lbl. ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-12 15:41 ` Pedro Alves @ 2026-06-15 10:29 ` Andrew Burgess 2026-06-16 18:36 ` Pedro Alves 0 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-06-15 10:29 UTC (permalink / raw) To: Pedro Alves, gdb-patches Pedro Alves <pedro@palves.net> writes: > On 2026-06-11 22:59, Andrew Burgess wrote: > >> >> +/* See solib.h. */ >> + >> +std::optional<CORE_ADDR> >> +svr4_solib_ops::inferior_entry_point_address () const >> +{ >> + std::optional<gdb::byte_vector> interp_name_holder >> + = svr4_find_program_interpreter (); >> + >> + /* No interpreter means this is a static executable. Ask the >> + program_space for the entry address within the main executable. */ >> + if (!interp_name_holder.has_value ()) >> + return m_pspace->exec_entry_point_address_if_available (); >> + >> + /* If we can find a solib that matches the interpreter name then we can >> + use that to find the actual entry address of the inferior. */ >> + const solib *interp_solib = nullptr; >> + const char *interp_name = (const char *) interp_name_holder->data (); >> + for (const solib &so : m_pspace->solibs ()) >> + { >> + if (svr4_same_name (interp_name, so.original_name.c_str ())) >> + { >> + interp_solib = &so; >> + >> + const objfile *objfile = so.objfile; >> + if (objfile != nullptr && objfile->per_bfd->ei.entry_point_p) >> + { >> + const entry_info &ei = objfile->per_bfd->ei; >> + return (ei.entry_point >> + + objfile->section_offsets[ei.the_bfd_section_index]); >> + } >> + } >> + } >> + >> + gdb_bfd_ref_ptr tmp_bfd; >> + target_ops_up tmp_bfd_target; >> + try >> + { >> + tmp_bfd = solib_bfd_open (interp_name); >> + >> + /* Failed to open the interpreter BFD. */ >> + if (tmp_bfd == nullptr) >> + return {}; >> + >> + tmp_bfd_target = target_bfd_reopen (tmp_bfd); >> + } >> + catch (const gdb_exception &ex) >> + { > > Swallows Ctrl-C. (I see you're just copying this from elsewhere.) > >> + return {}; >> + } >> + >> + CORE_ADDR entry_addr >> + = exec_entry_point (tmp_bfd.get (), tmp_bfd_target.get ()); >> + >> + /* We found the solib for the interpreter, but we don't have a >> + corresponding objfile, or the objfile doesn't have entry point >> + information. Maybe symbols haven't been loaded yet? */ >> + if (interp_solib != nullptr) >> + { >> + gdb_assert (interp_solib->objfile == nullptr >> + || !interp_solib->objfile->per_bfd->ei.entry_point_p); >> + >> + CORE_ADDR load_addr >> + = this->lm_addr_check (*interp_solib, tmp_bfd.get ()); >> + >> + return load_addr + entry_addr; >> + } >> + >> + /* We could not find a solib corresponding to the interpreter, this might >> + mean that the name didn't match for some reason, or maybe GDB has >> + failed to create a solib due to some other issue reading the solib >> + list from the inferior. >> + >> + If we got here we have managed to open the expected interpreter on >> + disk though, so we can try using the AT_BASE value. */ >> + CORE_ADDR load_addr; >> + if (target_auxv_search (AT_BASE, &load_addr) > 0) >> + { >> + int addr_bit = gdbarch_addr_bit (current_inferior ()->arch ()); >> + >> + /* Ensure LOAD_ADDR has proper sign in its possible upper bits so >> + that `+ load_addr' will overflow CORE_ADDR width not creating >> + invalid addresses like 0x101234567 for 32bit inferiors on 64bit >> + GDB. */ >> + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) >> + { >> + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; >> + >> + gdb_assert (load_addr < space_size); >> + >> + /* ENTRY_ADDR exceeding SPACE_SIZE would be for prelinked >> + 64bit ld.so with 32bit executable, it should not happen. */ >> + if (entry_addr < space_size >> + && entry_addr + load_addr >= space_size) >> + load_addr -= space_size; >> + } >> + >> + return load_addr + entry_addr; >> + } > > I wonder whether just looking at AT_BASE, which points at an ELF, and then > adding elf_header->e_entr, wouldn't be simpler than all of this. > We already use Elf32_External/Elf64_External structs in the file, so maybe > the manual ELF peeking is OK. > > Alternatively, of lot of that code looks similar to enable_break. Maybe > it could be refactored to be shared. Or enable_break could cache the > entry address. > > >> +# Use 'readelf' to extract the entry point address from TESTFILE. >> +proc get_exec_entry_address { testfile } { >> + set readelf_program [gdb_find_readelf] >> + set command "exec $readelf_program -Wh [standard_output_file $testfile]" > > If you use "objdump -f" instead, this has a better chance of working on Windows PE/COFF too (eventually): > > $ objdump.exe -f ./foo.exe > > ./foo.exe: file format pei-x86-64 > architecture: i386:x86-64, flags 0x0000013b: > HAS_RELOC, EXEC_P, HAS_DEBUG, HAS_SYMS, HAS_LOCALS, D_PAGED > start address 0x00000001400013f0 >> +# Start TESTFILE using 'starti'. Check if the 'backtrace' command >> +# prints any additional frames; we don't expect any. >> +# >> +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked >> +# executable, otherwise TESTFILE is statically linked. >> +proc run_test { testfile is_dynamic } { >> + clean_restart $testfile >> + >> + set exe_entry_addr [get_exec_entry_address $testfile] >> + >> + # The inferior entry address is always unknown before starting the >> + # inferior as at the point GDB has no solib_ops, and it is only >> + # through that that we figure out the inferior entry address. >> + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ >> + "check 'maint info entry-address' before inferior starts" >> + >> + # Start inferior with 'starti' and then wait for a prompt. >> + gdb_starti_cmd >> + gdb_test "" ".*" "prompt after starti" >> + >> + # Only svr4 targets currently support querying the inferior entry >> + # address. >> + if {[istarget *-linux*]} { > > There are more svr4 targets than linux. I think this style of allow-list has > a good chance of never getting updated. Deny-lists are better, IMHO. If the > test fails on some port, that might trigger someone to add the feature > there. Hey Pedro, I'm still looking at your other feedback, but before I start making this specific change, I just wanted to clarify how you see this as being different from what I have right now. If I write this as a deny list, e.g. if { ![istarget ....] && ![istarget ....] } { # Run tests. } Is there not the same problem? We rely on someone realising that the reason the test isn't run on their target is some missing GDB functionality, and them adding the functionality and removing the `istarget` block for their target. Another option would be for me to just not add any test skipping right now. I know this will lead to the test failing for some targets as the new feature is only implemented for svr4 targets. Then, if someone on a failing target cares, they can add the missing feature. This approach feels a bit mean, I don't like adding failing tests for others, but it does draw attention to the problem. I wonder if a better approach would to stick with my allow list, but add an 'unsupported' call in the else block. Maybe someone on e.g. Windows, will periodically look at tests that report 'unsupported' and see if there is anything that could be done? If 'unsupported' is the wrong type then I can use whatever you prefer. I agree with your feedback on svr4 being wider than just Linux, I'll add a helper function for `is_svr4_target` to lib/gdb.exp and then make use of that, the svr4 list can then be improved over time. Thanks, Andrew ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-15 10:29 ` Andrew Burgess @ 2026-06-16 18:36 ` Pedro Alves 2026-06-23 9:46 ` Andrew Burgess 0 siblings, 1 reply; 54+ messages in thread From: Pedro Alves @ 2026-06-16 18:36 UTC (permalink / raw) To: Andrew Burgess, gdb-patches Hi! On 2026-06-15 11:29, Andrew Burgess wrote: > Pedro Alves <pedro@palves.net> writes: > >> On 2026-06-11 22:59, Andrew Burgess wrote: >>> + # Only svr4 targets currently support querying the inferior entry >>> + # address. >>> + if {[istarget *-linux*]} { >> >> There are more svr4 targets than linux. I think this style of allow-list has >> a good chance of never getting updated. Deny-lists are better, IMHO. If the >> test fails on some port, that might trigger someone to add the feature >> there. > > Hey Pedro, > > I'm still looking at your other feedback, but before I start making this > specific change, I just wanted to clarify how you see this as being > different from what I have right now. > > If I write this as a deny list, e.g. > > if { ![istarget ....] && ![istarget ....] } { > # Run tests. > } > > Is there not the same problem? We rely on someone realising that the > reason the test isn't run on their target is some missing GDB > functionality, and them adding the functionality and removing the > `istarget` block for their target. Speaking from principles, and not this particular case: The difference is that there was a failure that made someone see that some functionality is missing. With the allow-list, nobody ever notices it. With a target-based allow-list, even if someone adds some functionality to a port, it's typical to not comb through the testsuite and relax the relevant allow-lists to also allow their targets. And even if they want to, there's no easy marker to grep for. E.g, say I implement feature X on Windows, then how do I know that I need to grep for "istarget Y" to find all the tests that I need to adjust? And which Y? And then which ones that hit my grep should I look at? I'll give you one example, in gdb.threads/watchpoint-fork.exp: # Only GNU/Linux is known to support `set follow-fork-mode child'. if {[istarget "*-*-linux*"]} { test child FOLLOW_CHILD } else { untested "${testfile}: child" } I think at least FreeBSD has supported that for over a decade. > > Another option would be for me to just not add any test skipping right > now. I know this will lead to the test failing for some targets as the > new feature is only implemented for svr4 targets. Then, if someone on a > failing target cares, they can add the missing feature. This approach > feels a bit mean, I don't like adding failing tests for others, but it > does draw attention to the problem. I think the best would be to just add a reasonable deny-list from the get go. I suspect this is going to be a good enough starting point: proc supports_process_entry_point {} { if windows || darwin return 0 if is_svr4_target return 1 # works even if remote, svr4 is handled on the host. if remote return 0 # no RSP packet that gets us the info # Assume yes. return 1 } > > I wonder if a better approach would to stick with my allow list, but add > an 'unsupported' call in the else block. Maybe someone on e.g. Windows, > will periodically look at tests that report 'unsupported' and see if > there is anything that could be done? If 'unsupported' is the wrong > type then I can use whatever you prefer. I don't think so. In my experience, people ignore/miss PASS -> UNSUPPORTED regressions, let alone looking for UNSUPPORTED results indicating tests they should/could enable. It is still good to issue an UNSUPPORTED, but I wouldn't make that the discovery mechanism. Pedro Alves > > I agree with your feedback on svr4 being wider than just Linux, I'll add > a helper function for `is_svr4_target` to lib/gdb.exp and then make use > of that, the svr4 list can then be improved over time. > > Thanks, > Andrew > ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-16 18:36 ` Pedro Alves @ 2026-06-23 9:46 ` Andrew Burgess 2026-06-23 10:20 ` Pedro Alves 0 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-06-23 9:46 UTC (permalink / raw) To: Pedro Alves, gdb-patches Pedro Alves <pedro@palves.net> writes: > Hi! > > On 2026-06-15 11:29, Andrew Burgess wrote: >> Pedro Alves <pedro@palves.net> writes: >> >>> On 2026-06-11 22:59, Andrew Burgess wrote: >>>> + # Only svr4 targets currently support querying the inferior entry >>>> + # address. >>>> + if {[istarget *-linux*]} { >>> >>> There are more svr4 targets than linux. I think this style of allow-list has >>> a good chance of never getting updated. Deny-lists are better, IMHO. If the >>> test fails on some port, that might trigger someone to add the feature >>> there. >> >> Hey Pedro, >> >> I'm still looking at your other feedback, but before I start making this >> specific change, I just wanted to clarify how you see this as being >> different from what I have right now. >> >> If I write this as a deny list, e.g. >> >> if { ![istarget ....] && ![istarget ....] } { >> # Run tests. >> } >> >> Is there not the same problem? We rely on someone realising that the >> reason the test isn't run on their target is some missing GDB >> functionality, and them adding the functionality and removing the >> `istarget` block for their target. > > > Speaking from principles, and not this particular case: > > The difference is that there was a failure that made someone see that > some functionality is missing. With the allow-list, nobody ever notices it. > > With a target-based allow-list, even if someone adds some functionality > to a port, it's typical to not comb through the testsuite and > relax the relevant allow-lists to also allow their targets. > > And even if they want to, there's no easy marker to grep for. E.g, say I > implement feature X on Windows, then how do I know that I need to > grep for "istarget Y" to find all the tests that I need to adjust? > And which Y? And then which ones that hit my grep should I look at? > > I'll give you one example, in gdb.threads/watchpoint-fork.exp: > > # Only GNU/Linux is known to support `set follow-fork-mode child'. > if {[istarget "*-*-linux*"]} { > test child FOLLOW_CHILD > } else { > untested "${testfile}: child" > } > > I think at least FreeBSD has supported that for over a decade. Sure, but how would rewriting this as you're suggesting have helped in any way? From what you suggest below I'm imagining your rewrite of this would have looked like this: if { [supports_fork_follow_child] } { test child FOLLOW_CHILD } else { untested "${testfile}: child" } with: proc supports_fork_follow_child {} { if { [istarget *-freebsd*] } { # Not supported here. I assume at the point the test was first # added this feature wasn't supported on FreeBSD. return 0 } # Assume true by default. return 1 } Now, I agree that this is better as fixing `supports_fork_follow_child` means we only need to update one proc and all tests that used the proc would then start testing fork follow child behaviour. But, as you say, it's unlikely that when this feature was fixed on FreeBSD the support proc would actually be updated, so I fail to see how this is significantly different. And the example you give is fundamentally different than my code. What I wrote is this: if { [is_svr4_target] } { set expected_result "PATTERN WHEN SUPPORTED" } else { set expected_result "PATTERN WHEN NOT SUPPORTED" } gdb_test "some command" $expected_result The difference here is that if a non-svr4 target is fixed then it will stop emitting "PATTERN WHEN NOT SUPPORTED" and will start emitting "PATTERN WHEN SUPPORTED". If the developer actually runs the complete testsuite then they will see a PASS -> FAIL for this test, which will force them to update the `if` condition (but see below). I tried to investigate if the test you quoted above could be made to work in the same way; i.e. on the unsupported path, actually run a test that confirms the feature is unsupported, which would have immediately identified when the feature started being supported, but I couldn't see how GDB would fail, most of the fork handling code appears to be generic, and targets that don't have specific overrides fallback to process_stratum_target, which doesn't care if we're following the parent or child. > >> >> Another option would be for me to just not add any test skipping right >> now. I know this will lead to the test failing for some targets as the >> new feature is only implemented for svr4 targets. Then, if someone on a >> failing target cares, they can add the missing feature. This approach >> feels a bit mean, I don't like adding failing tests for others, but it >> does draw attention to the problem. > > I think the best would be to just add a reasonable deny-list from > the get go. I suspect this is going to be a good enough starting point: > > proc supports_process_entry_point {} { > if windows || darwin > return 0 > > if is_svr4_target > return 1 # works even if remote, svr4 is handled on the host. > > if remote > return 0 # no RSP packet that gets us the info > > # Assume yes. > return 1 > } I do agree with you that having a 'supports_....' proc will be better than having to update the `if` condition within the test itself. If the supports proc ends up being used multiple times then we only need to update the one place and all the tests will be fixed. I'll follow the structure you propose here as I'd like to progress this patch. I'll post a v4 with the update soon. Thanks, Andrew ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function 2026-06-23 9:46 ` Andrew Burgess @ 2026-06-23 10:20 ` Pedro Alves 0 siblings, 0 replies; 54+ messages in thread From: Pedro Alves @ 2026-06-23 10:20 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2026-06-23 10:46, Andrew Burgess wrote: > Pedro Alves <pedro@palves.net> writes: > >> Hi! >> >> On 2026-06-15 11:29, Andrew Burgess wrote: >>> Pedro Alves <pedro@palves.net> writes: >>> >>>> On 2026-06-11 22:59, Andrew Burgess wrote: >>>>> + # Only svr4 targets currently support querying the inferior entry >>>>> + # address. >>>>> + if {[istarget *-linux*]} { >>>> >>>> There are more svr4 targets than linux. I think this style of allow-list has >>>> a good chance of never getting updated. Deny-lists are better, IMHO. If the >>>> test fails on some port, that might trigger someone to add the feature >>>> there. >>> >>> Hey Pedro, >>> >>> I'm still looking at your other feedback, but before I start making this >>> specific change, I just wanted to clarify how you see this as being >>> different from what I have right now. >>> >>> If I write this as a deny list, e.g. >>> >>> if { ![istarget ....] && ![istarget ....] } { >>> # Run tests. >>> } >>> >>> Is there not the same problem? We rely on someone realising that the >>> reason the test isn't run on their target is some missing GDB >>> functionality, and them adding the functionality and removing the >>> `istarget` block for their target. >> >> >> Speaking from principles, and not this particular case: >> >> The difference is that there was a failure that made someone see that >> some functionality is missing. With the allow-list, nobody ever notices it. >> >> With a target-based allow-list, even if someone adds some functionality >> to a port, it's typical to not comb through the testsuite and >> relax the relevant allow-lists to also allow their targets. >> >> And even if they want to, there's no easy marker to grep for. E.g, say I >> implement feature X on Windows, then how do I know that I need to >> grep for "istarget Y" to find all the tests that I need to adjust? >> And which Y? And then which ones that hit my grep should I look at? >> >> I'll give you one example, in gdb.threads/watchpoint-fork.exp: >> >> # Only GNU/Linux is known to support `set follow-fork-mode child'. >> if {[istarget "*-*-linux*"]} { >> test child FOLLOW_CHILD >> } else { >> untested "${testfile}: child" >> } >> >> I think at least FreeBSD has supported that for over a decade. > > Sure, but how would rewriting this as you're suggesting have helped in > any way? From what you suggest below I'm imagining your rewrite of this > would have looked like this: > > if { [supports_fork_follow_child] } { > test child FOLLOW_CHILD > } else { > untested "${testfile}: child" > } > > with: > > proc supports_fork_follow_child {} { > if { [istarget *-freebsd*] } { > # Not supported here. I assume at the point the test was first > # added this feature wasn't supported on FreeBSD. > return 0 > } > > # Assume true by default. > return 1 > } > > Now, I agree that this is better as fixing `supports_fork_follow_child` > means we only need to update one proc and all tests that used the proc > would then start testing fork follow child behaviour. But, as you say, > it's unlikely that when this feature was fixed on FreeBSD the support > proc would actually be updated, so I fail to see how this is > significantly different. Well, with the proc, you effectively turned: if {[istarget "*-*-linux*"]} { test child FOLLOW_CHILD } else { untested "${testfile}: child" } into: if {![istarget "*-*-freebsd*"]} { test child FOLLOW_CHILD } else { untested "${testfile}: child" } ... meaning, any new target that comes along, after the testcase is written, automatically gets that test exercised. While the "before" is stuck in testing only on linux and nobody ever remembers to update it. That's the main point about allow vs deny lists. Also, the wrapper proc is a lot more discoverable, especially if its in gdb.exp. You only have to discover it once, and from there you can easily find all the testcases that use it. With explicit "istarget", you have the issue I mentioned earlier: no clue what to look for. Even assuming you ran into that one in gdb.threads/watchpoint-fork.exp one, which other testcases have a similar issue? Maybe there are others using "istarget linux"? Or some other slightly different if condition? > > And the example you give is fundamentally different than my code. What > I wrote is this: > > if { [is_svr4_target] } { > set expected_result "PATTERN WHEN SUPPORTED" > } else { > set expected_result "PATTERN WHEN NOT SUPPORTED" > } > > gdb_test "some command" $expected_result OK, I missed that. That's even better, and takes care of my main concern -- discoverability. > > The difference here is that if a non-svr4 target is fixed then it will > stop emitting "PATTERN WHEN NOT SUPPORTED" and will start emitting > "PATTERN WHEN SUPPORTED". If the developer actually runs the complete > testsuite then they will see a PASS -> FAIL for this test, which will > force them to update the `if` condition (but see below). Agreed, and that's the ideal. Thanks. Pedro Alves > I do agree with you that having a 'supports_....' proc will be better > than having to update the `if` condition within the test itself. If the > supports proc ends up being used multiple times then we only need to > update the one place and all the tests will be fixed. > > I'll follow the structure you propose here as I'd like to progress this > patch. I'll post a v4 with the update soon. ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv2 3/3] gdb: allow 'until' to work in outermost frame 2026-06-11 21:59 ` [PATCHv2 0/3] " Andrew Burgess 2026-06-11 21:59 ` [PATCHv2 1/3] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-06-11 21:59 ` [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-06-11 21:59 ` Andrew Burgess 2026-06-12 15:57 ` Pedro Alves 2026-06-16 19:47 ` [PATCHv3 0/4] " Andrew Burgess 3 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-06-11 21:59 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess 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, if the until breakpoint was not hit. 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 using program_space::get_entry_point_info within frame_unwind_caller_frame to spot when we are attempting to unwind the inferior's entry frame. If this is detected then we will return nullptr indicating that there is no previous frame. In this case the 'until' command will skip creating the frame exit breakpoint. I specifically only check the inferior_entry_address value and not the exec_entry_address value within frame_unwind_caller_frame. It is possible that there are valid frames between the exec_entry_address frame and the inferior_entry_address frame, for example, in a dynamically linked executable the exec_entry_address frame will be the first frame within the main executable while the inferior_entry_address frame will be the first frame within the dynamic linker. It is only in this second case that there truly are no previous frames in which to place a breakpoint. I have made the new detection logic conditional on 'backtrace past-entry' being off. If the user turns this on then we assume that they know what they are doing, and that (for reasons we cannot know) there really are frames prior to the entry-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 | 24 +++ gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 163 ++++++++++++++++++ 3 files changed, 209 insertions(+) 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 3920d8d03f7..a1970096ab3 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -707,6 +707,30 @@ 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) { + /* Unless 'backtrace past-entry' is on then there is no caller for the + frame at the inferior entry address. Sometimes the register values + present when an inferior starts can make GDB believe that there are + frames before the entry frame, but trying to use that frame to + e.g. place a breakpoint, will fail. */ + if (initial_next_frame->level >= 0 + && get_frame_type (initial_next_frame) == NORMAL_FRAME + && !user_set_backtrace_options.backtrace_past_entry + && get_frame_pc_if_available (initial_next_frame).has_value ()) + { + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + CORE_ADDR frame_func_addr = get_frame_func (initial_next_frame); + + /* We only check the inferior entry address here, not the main + executable entry address. If there are frames between the + executable entry address frame and the inferior entry address + frame then these are likely valid, and can be used for placing + e.g. frame exit breakpoints. */ + if (ep_info.inferior_entry_address () == frame_func_addr) + return nullptr; + } + frame_info_ptr this_frame = get_prev_frame_always (initial_next_frame); if (this_frame == nullptr) return nullptr; 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 <http://www.gnu.org/licenses/>. */ + +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..9130f7a19cd --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp @@ -0,0 +1,163 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'until' in the outermost frame works as expected. + +standard_testfile + +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + { debug additional_flags=-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 testfile } { + 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 "" }] + if { !$found_address } { + unsupported "$gdb_test_name (no instruction found)" + return + } + pass $gdb_test_name + } + + -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 $testfile + + with_test_prefix "static" { + run_test $use_stepi $past_entry $testfile_static + } + } +} -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv2 3/3] gdb: allow 'until' to work in outermost frame 2026-06-11 21:59 ` [PATCHv2 3/3] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-06-12 15:57 ` Pedro Alves 0 siblings, 0 replies; 54+ messages in thread From: Pedro Alves @ 2026-06-12 15:57 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2026-06-11 22:59, Andrew Burgess wrote: > +# 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. Nit: I'd think "frame" would be more natural than "bt 1". > +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 testfile } { > + clean_restart $testfile > + > + if { [gdb_starti_cmd] < 0 } { > + untested starti > + return > + } > + > + gdb_test "" "Program stopped\\..*" "starti" On Windows when you get to the entry address there's already more than one thread, so the above will fail. E.g.: (gdb) starti Starting program: C:/msys2/home/alves/gdb/build-testsuite/outputs/gdb.base/patch/patch.exe [New Thread 12888.0x3260] Thread 1 stopped. 0x00007ff9abd3d78e in ntdll!RtlGetReturnAddressHijackTarget () from C:/WINDOWS/SYSTEM32/ntdll.dll (gdb) info thr Id Target Id Frame * 1 Thread 12888.0x404c 0x00007ff9abd3d78e in ntdll!RtlGetReturnAddressHijackTarget () from C:/WINDOWS/SYSTEM32/ntdll.dll 2 Thread 12888.0x3260 0x00007ff9abd83d24 in ntdll!ZwWaitForWorkViaWorkerFactory () from C:/WINDOWS/SYSTEM32/ntdll.dll (gdb) > + # 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 "" }] > + if { !$found_address } { > + unsupported "$gdb_test_name (no instruction found)" > + return > + } > + pass $gdb_test_name > + } > + > + -re "^\[^\r\n\]*\r\n" { > + exp_continue > + } Could use -lbl ? ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv3 0/4] gdb: allow 'until' to work in outermost frame 2026-06-11 21:59 ` [PATCHv2 0/3] " Andrew Burgess ` (2 preceding siblings ...) 2026-06-11 21:59 ` [PATCHv2 3/3] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-06-16 19:47 ` Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess ` (4 more replies) 3 siblings, 5 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-16 19:47 UTC (permalink / raw) To: gdb-patches; +Cc: Guinevere Larsen, Pedro Alves, Andrew Burgess Updates to address review comments from v2 series: - Doc updates based on Eli's feedback. - Patch #2: + Avoid swallowing Ctrl-C in try/catch. That whole try/catch block has been removed in v3, so problem sorted! + Rewrote entry address calculation as suggested by Pedro, now pulls all required information from the inferior. + Use 'objdump -f' rather than readelf to get the entry address in the test. + Added a helper proc for is_svr4_target, which includes a larger list of targets beyond just Linux. This is then used in the test. + Test updated to use -lbl flag in gdb_test_multiple. - Patch #3: + Use 'frame' rather than 'bt 1' + Loosen the test regexp after the 'starti' command, this should now work on Windows targets, or any target with multiple threads at startup. + Test updated to use -lbl in gdb_test_multiple. - In patch #3 the entry frame detection is moved from `frame_unwind_caller_frame` into `get_prev_frame_always`, I hope this will address Guinevere's concerns about having to add the additional entry frame detection in multiple locations. Guinevere also had some concerns about the naming of existing functions (i.e. not things added by me in this commit), I don't plan to make any naming changes to the get_prev_frame* functions in this series. - Patch #4 is new, this adds some caching to avoid repeatedly looking up the entry address. The new approach requires reading target memory, which can be expensive, especially on remote targets. --- Andrew Burgess (4): gdb: rename program_space::entry_point_address* functions gdb: introduce program_space::get_entry_point_info function gdb: allow 'until' to work in outermost frame gdb: cache program space entry point information gdb/NEWS | 7 + gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/doc/gdb.texinfo | 22 +++ gdb/frame.c | 105 +++++++---- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 94 +++++++++- gdb/progspace.h | 67 ++++++- gdb/solib-frv.c | 2 +- gdb/solib-svr4.c | 85 +++++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 167 ++++++++++++++++++ gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 162 +++++++++++++++++ gdb/testsuite/lib/gdb.exp | 8 + 17 files changed, 717 insertions(+), 47 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.c create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.exp base-commit: c311f4d37f31ff3fbb5db6923abcdf93bb75a37b -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv3 1/4] gdb: rename program_space::entry_point_address* functions 2026-06-16 19:47 ` [PATCHv3 0/4] " Andrew Burgess @ 2026-06-16 19:47 ` Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess ` (3 subsequent siblings) 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-16 19:47 UTC (permalink / raw) To: gdb-patches; +Cc: Guinevere Larsen, Pedro Alves, Andrew Burgess Rename program_space::entry_point_address to program_space::exec_entry_point_address and program_space::entry_point_address_query to program_space::exec_entry_point_address_if_available. There are two aspects to this renaming. First I replace 'query' with 'if_available' in one of the functions. I feel this better describes the function, and also is inline with how other, similar, functions are named in GDB. The second part of the renaming is to add the 'exec_' prefix to the front of both function names. When a dynamically linked inferior is started the first instruction executed is actually within the run-time linker, not within the main executable, so it could be argued that the actual entry address for the inferior is not the entry address of the main executable. However, there is an equally valid argument that the entry address of the executable is also something worth finding. The existing entry address within the inferior is used for a number of tasks in GDB, for example displaced stepping (arch-utils.c), inferior function calls (arc-tdep.c and infcall.c), and for detecting the "entry" frame. This last one, the entry frame detection is interesting. In a dynamically linked executable it could be argued that there are two entry frames. The very first frame that is executed in the inferior, this is where 'starti' stops the inferior. And then the very first frame within the main executable. I think that both of these are valid. Currently, as we can only find the entry address within the main executable, we can only identify the first frame of the main executable. And this can cause some problems, consider: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) Here frames #1 to #3 are all bogus, created by GDB based on whatever values happen to be in the registers when the inferior starts. In a later commit I'd like to fix this problem, however, I would prefer that the other users of program_space::entry_point_address continue to use the address within the main executable. So, in order to keep the distinction between the two different types of entry point, I'm renaming the existing functions with the 'exec_' prefix. There should be no user visible changes after this commit. Approved-By: Pedro Alves <pedro@palves.net> --- gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/frame.c | 4 ++-- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 6 +++--- gdb/progspace.h | 12 ++++++------ gdb/solib-frv.c | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gdb/arc-tdep.c b/gdb/arc-tdep.c index 4936a5c8fbb..7ed83c4cae6 100644 --- a/gdb/arc-tdep.c +++ b/gdb/arc-tdep.c @@ -860,7 +860,7 @@ arc_push_dummy_code (struct gdbarch *gdbarch, CORE_ADDR sp, CORE_ADDR funaddr, struct regcache *regcache) { *real_pc = funaddr; - *bp_addr = current_program_space->entry_point_address (); + *bp_addr = current_program_space->exec_entry_point_address (); return sp; } diff --git a/gdb/arch-utils.c b/gdb/arch-utils.c index e959788bd3b..88047731ce6 100644 --- a/gdb/arch-utils.c +++ b/gdb/arch-utils.c @@ -57,7 +57,7 @@ displaced_step_at_entry_point (struct gdbarch *gdbarch) CORE_ADDR addr; int bp_len; - addr = current_program_space->entry_point_address (); + addr = current_program_space->exec_entry_point_address (); /* Inferior calls also use the entry point as a breakpoint location. We don't want displaced stepping to interfere with those diff --git a/gdb/frame.c b/gdb/frame.c index f64f5554f8c..f5a6a8919df 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2700,7 +2700,7 @@ static bool inside_entry_func (const frame_info_ptr &this_frame) { std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) return false; @@ -2776,7 +2776,7 @@ get_prev_frame (const frame_info_ptr &this_frame) added to work around that (now fixed) case. */ /* NOTE: cagney/2003-07-15: danielj (if I'm reading it right) suggested having the inside_entry_func test use the - inside_main_func() msymbol trick (along with entry_point_address() + inside_main_func() msymbol trick (along with exec_entry_point_address() I guess) to determine the address range of the start function. That should provide a far better stopper than the current heuristics. */ diff --git a/gdb/infcall.c b/gdb/infcall.c index e6b24ff5310..6d26841ede3 100644 --- a/gdb/infcall.c +++ b/gdb/infcall.c @@ -1300,7 +1300,7 @@ call_function_by_hand_dummy (struct value *function, CORE_ADDR dummy_addr; real_pc = funaddr; - dummy_addr = current_program_space->entry_point_address (); + dummy_addr = current_program_space->exec_entry_point_address (); /* A call dummy always consists of just a single breakpoint, so its address is the same as the address of the dummy. diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c index a7381677498..d03e4768792 100644 --- a/gdb/linux-tdep.c +++ b/gdb/linux-tdep.c @@ -2952,7 +2952,7 @@ linux_displaced_step_location (struct gdbarch *gdbarch) /* Determine entry point from target auxiliary vector. This avoids the need for symbols. Also, when debugging a stand-alone SPU - executable, entry_point_address () will point to an SPU + executable, exec_entry_point_address () will point to an SPU local-store address and is thus not usable as displaced stepping location. The auxiliary vector gets us the PowerPC-side entry point address instead. */ diff --git a/gdb/progspace.c b/gdb/progspace.c index 1407b058dfd..0e8516f4640 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -263,7 +263,7 @@ program_space::empty () /* See progspace.h. */ std::optional<CORE_ADDR> -program_space::entry_point_address_query () const +program_space::exec_entry_point_address_if_available () const { objfile *objf = symfile_object_file; if (objf == NULL || !objf->per_bfd->ei.entry_point_p) @@ -276,9 +276,9 @@ program_space::entry_point_address_query () const /* See progspace.h. */ CORE_ADDR -program_space::entry_point_address () const +program_space::exec_entry_point_address () const { - std::optional<CORE_ADDR> retval = entry_point_address_query (); + std::optional<CORE_ADDR> retval = exec_entry_point_address_if_available (); if (!retval.has_value ()) error (_("Entry point address is not known.")); diff --git a/gdb/progspace.h b/gdb/progspace.h index e9261ff8590..1ae1e42f3bb 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,13 +323,13 @@ struct program_space return m_target_sections; } - /* If there is a valid and known entry point in this program space, - return it. Otherwise return an empty optional. */ - std::optional<CORE_ADDR> entry_point_address_query () const; + /* If there is a valid and known entry point in the main executable of + this program space, return it. Otherwise return an empty optional. */ + std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; - /* Get the entry point address in this program space. Call error if - it is not known. */ - CORE_ADDR entry_point_address () const; + /* Get the entry point address for the main executable in this program + space. Call error if it is not known. */ + CORE_ADDR exec_entry_point_address () const; /* Return true if any objfile of this program space has partial symbols. */ diff --git a/gdb/solib-frv.c b/gdb/solib-frv.c index 4f0aac31e73..69b882e534a 100644 --- a/gdb/solib-frv.c +++ b/gdb/solib-frv.c @@ -689,7 +689,7 @@ enable_break (void) } std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) { solib_debug_printf ("Symbol file has no entry point."); -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv3 2/4] gdb: introduce program_space::get_entry_point_info function 2026-06-16 19:47 ` [PATCHv3 0/4] " Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess @ 2026-06-16 19:47 ` Andrew Burgess 2026-06-17 11:52 ` Eli Zaretskii 2026-06-16 19:47 ` [PATCHv3 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 subsequent siblings) 4 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-06-16 19:47 UTC (permalink / raw) To: gdb-patches; +Cc: Guinevere Larsen, Pedro Alves, Andrew Burgess, Eli Zaretskii I noticed that when debugging a dynamically linked executable, if I started the inferior with 'starti' then used 'bt' I would see some bogus frames: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) This surprised me as 'backtrace past-entry' was off: (gdb) show backtrace past-entry Whether backtraces should continue past the entry point of a program is off. I was expecting GDB to stop the backtrace at the inferior's entry address. Frame unwinding starts in get_prev_frame, and in here we find this block: 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)) { frame_debug_got_null_frame (this_frame, "inside entry func"); return NULL; } Which uses inside_entry_func to terminate the backtrace when we reach the entry frame. The inside_entry_func function calls current_program_space->exec_entry_point_address_if_available and uses the result to figure out if we are in the entry frame. And here the problem becomes obvious, we are only checking if we are in the "entry frame" for the main executable, not for the inferior as a whole. And indeed, if I compile the same test program as a static binary, where there will be no run-time linker, and the entry point of the executable is the entry point for the inferior, then the 'bt' problem I saw above goes away. This suggests, I think, that we need to track two different entry addresses, the entry address for the executable file, and the entry address for the entire inferior. Then, for dynamically linked executables, these two addresses can be different, the former will still be the same address within the main executable file, while the latter will be the address of the entry point within the run-time linker. If we had this information then we could extend inside_entry_func to check both addresses, and the 'bt' problem seen above will be resolved. To make this information available I added a new solib_ops method, solib_ops::inferior_entry_point_address, for svr4 targets this figures out if the main executable is dynamically linked, and if it is, uses the AT_BASE auxv entry and the entry address pulled from the ELF header to compute the inferior entry address. I then added program_space::get_entry_point_info, which returns a struct containing the two entry point addresses, one comes from the new solib_ops method, and one comes from the existing method program_space::exec_entry_point_address_if_available. With the infrastructure in place I can then update inside_entry_func to check against both entry addresses. To aid in debugging GDB, I added a new maintenance command: maintenance info entry-address which just calls program_space::get_entry_point_info and then prints the two addresses. I left this as a maintenance command as I don't see much user utility in this right now, but it made it easier for me to see what GDB was doing, so I left the command in this commit. Reviewed-By: Eli Zaretskii <eliz@gnu.org> --- gdb/NEWS | 7 + gdb/doc/gdb.texinfo | 22 +++ gdb/frame.c | 10 +- gdb/progspace.c | 60 ++++++++ gdb/progspace.h | 48 ++++++ gdb/solib-svr4.c | 85 +++++++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 167 +++++++++++++++++++++ gdb/testsuite/lib/gdb.exp | 8 + 10 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp diff --git a/gdb/NEWS b/gdb/NEWS index d5214a98a57..99c9fb6999c 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -146,6 +146,13 @@ disable skip These are new aliases for 'skip delete', 'skip enable', and 'skip disable' respectively. +maint info entry-address + Display the inferior and main executable entry addresses for the + current inferior. The inferior entry address is the address of the + first instruction in the inferior that was executed. The main + executable entry address is the address of the first instruction in + the main executable that was executed. + * MI changes ** The "-trace-save" command no longer supports the "-ctf" flag. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index a698b2b8451..4c0c4709a23 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -43212,6 +43212,28 @@ Maintenance Commands Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M @end smallexample + +@kindex maint info entry-address +@item maint info entry-address +Display the entry addresses for the currently selected inferior. Two +addresses are displayed, the inferior entry address and the executable +entry address. + +Neither address is the address of @code{main}. When a program is +started, low-level startup code (typically a function called +@code{_start}) runs before @code{main} is called. + +The executable entry address is the address of this startup code +within the main executable (@pxref{Files, ,Commands to Specify +Files}). + +The inferior entry address is the address of the very first +instruction executed when the inferior is started. For statically +linked programs this is the same as the executable entry address. For +dynamically linked programs the run-time linker must execute first in +order to load shared libraries, so the inferior entry address will be +an address within the run-time linker rather than within the main +executable. @end table The following command is useful for non-interactive invocations of diff --git a/gdb/frame.c b/gdb/frame.c index f5a6a8919df..3920d8d03f7 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2699,12 +2699,12 @@ inside_main_func (const frame_info_ptr &this_frame) static bool inside_entry_func (const frame_info_ptr &this_frame) { - std::optional<CORE_ADDR> entry_point - = current_program_space->exec_entry_point_address_if_available (); - if (!entry_point.has_value ()) - return false; + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); - return get_frame_func (this_frame) == *entry_point; + CORE_ADDR frame_func_addr = get_frame_func (this_frame); + return (ep_info.exec_entry_address () == frame_func_addr + || ep_info.inferior_entry_address () == frame_func_addr); } /* Return a structure containing various interesting information about diff --git a/gdb/progspace.c b/gdb/progspace.c index 0e8516f4640..26e16cae8d0 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -26,6 +26,7 @@ #include <algorithm> #include "cli/cli-style.h" #include "observable.h" +#include "arch-utils.h" /* The last program space number assigned. */ static int last_program_space_num = 0; @@ -288,6 +289,55 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ +program_space::entry_point_info +program_space::get_entry_point_info () const +{ + std::optional<CORE_ADDR> exec_entry_address + = this->exec_entry_point_address_if_available (); + + std::optional<CORE_ADDR> inferior_entry_address; + if (m_solib_ops != nullptr) + inferior_entry_address = m_solib_ops->inferior_entry_point_address (); + + return entry_point_info (std::move (inferior_entry_address), + std::move (exec_entry_address)); +} + +/* Implement the 'maint info entry-address' command. */ + +static void +maintenance_info_entry_address (const char *args, int from_tty) +{ + if (args != nullptr && *args != '\0') + error (_("unknown argument: %s"), args); + + struct gdbarch *gdbarch = get_current_arch (); + + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + /* Display a single entry address ADDR with TITLE as a description. */ + auto display_entry_address + = [&gdbarch] (const char *title, + const std::optional<CORE_ADDR> &addr) -> void + { + gdb_puts (title); + if (addr.has_value ()) + fputs_styled (paddress (gdbarch, addr.value ()), + address_style.style (), gdb_stdout); + else + fputs_styled ("<unknown>", metadata_style.style (), gdb_stdout); + gdb_puts ("\n"); + }; + + display_entry_address (_("Inferior entry address: "), + ep_info.inferior_entry_address ()); + display_entry_address (_("Executable entry address: "), + ep_info.exec_entry_address ()); +} + +/* See progspace.h. */ + bool program_space::has_partial_symbols () { @@ -498,6 +548,16 @@ initialize_progspace () _("Info about currently known program spaces."), &maintenanceinfolist); + add_cmd ("entry-address", class_maintenance, + maintenance_info_entry_address, + _("Information about the current inferior's entry addresses.\n\ +Display the address of the first instruction executed within the\n\ +inferior, and the first instruction executed within the main executable.\n\ +The entry address of the whole inferior will be <unknown> prior to the\n\ +inferior starting, and either or both addresses can be <unknown> if\n\ +GDB is unable to find the required information."), + &maintenanceinfolist); + /* There's always one program space. Note that this function isn't an automatic _initialize_foo function, since other _initialize_foo routines may need to install their per-pspace diff --git a/gdb/progspace.h b/gdb/progspace.h index 1ae1e42f3bb..d75379fe621 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,6 +323,54 @@ struct program_space return m_target_sections; } + /* Class for tracking two possible entry points that an inferior might + have, the entry point for the entire inferior, and the entry point + within the main executable. */ + struct entry_point_info + { + explicit entry_point_info (std::optional<CORE_ADDR> inferior_entry_address, + std::optional<CORE_ADDR> exec_entry_address) + : m_inferior_entry_address (std::move (inferior_entry_address)), + m_exec_entry_address (std::move (exec_entry_address)) + { /* Nothing. */ } + + DISABLE_COPY_AND_ASSIGN (entry_point_info); + + /* The entry address within the inferior as a whole. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &inferior_entry_address () const + { + return m_inferior_entry_address; + } + + /* The entry address within the main executable. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &exec_entry_address () const + { + return m_exec_entry_address; + } + + private: + /* The entry address within the inferior. This can be outside of the + main executable, e.g. for dynamically linked executables this could + be the entry address for the run-time linker. For statically linked + executables this will be the entry address of the main executable. + This can be empty if GDB doesn't know how to figure out the correct + entry address for any reason. */ + std::optional<CORE_ADDR> m_inferior_entry_address; + + /* The entry address within the main executable. This can be empty if + the main executable is not set yet, or GDB doesn't have an objfile + associated with the main executable, e.g. in some attach, or remote + debug cases. */ + std::optional<CORE_ADDR> m_exec_entry_address; + }; + + /* Return information about the entry point in the main executable, and + the entry point for the inferior, which might be different from the + main executable. */ + entry_point_info get_entry_point_info () const; + /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c index 8e3de4d3ea1..12f3e783744 100644 --- a/gdb/solib-svr4.c +++ b/gdb/solib-svr4.c @@ -3777,6 +3777,91 @@ svr4_solib_ops::get_solibs_in_ns (int nsid) const return ns_solibs; } +/* See solib.h. */ + +std::optional<CORE_ADDR> +svr4_solib_ops::inferior_entry_point_address () const +{ + std::optional<gdb::byte_vector> interp_name_holder + = svr4_find_program_interpreter (); + + /* No interpreter means this is a static executable. Ask the + program_space for the entry address within the main executable. */ + if (!interp_name_holder.has_value ()) + return m_pspace->exec_entry_point_address_if_available (); + + /* For a dynamically linked executable the inferior's true entry point + is the entry point of the dynamic linker. We find this using the + AT_BASE auxiliary vector entry, which gives the dynamic linker's + load address, combined with e_entry address pulled from the + inferior. We assume that the ELF header can be read from AT_BASE. */ + CORE_ADDR at_base_addr; + if (target_auxv_search (AT_BASE, &at_base_addr) <= 0) + return {}; + + /* Determine ELF architecture type. Use the size of a program header + entry to determine which ELF header we can expect to find. */ + size_t e_entry_offset = 0; + size_t e_entry_size = 0; + CORE_ADDR at_phent; + if (target_auxv_search (AT_PHENT, &at_phent) <= 0) + return {}; + if (at_phent == sizeof (Elf32_External_Phdr)) + { + e_entry_offset = offsetof (Elf32_External_Ehdr, e_entry); + e_entry_size = 4; + } + else if (at_phent == sizeof (Elf64_External_Phdr)) + { + e_entry_offset = offsetof (Elf64_External_Ehdr, e_entry); + e_entry_size = 8; + } + else + return {}; + + /* Architecture of the current inferior. */ + gdbarch *gdbarch = current_inferior ()->arch (); + + /* Read the entry address from the ELF header at AT_BASE_ADDR. */ + CORE_ADDR e_entry; + gdb_byte buffer[sizeof (CORE_ADDR)]; + if (target_read_memory (at_base_addr + e_entry_offset, buffer, e_entry_size)) + return {}; + bfd_endian byte_order = gdbarch_byte_order (gdbarch); + e_entry = extract_unsigned_integer (buffer, e_entry_size, byte_order); + + /* Ensure AT_BASE_ADDR has proper sign in its possible upper bits so + that `+ at_base_addr' will overflow CORE_ADDR width not creating + invalid addresses like 0x101234567 for 32bit inferiors on 64bit + GDB. */ + int addr_bit = gdbarch_addr_bit (gdbarch); + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) + { + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; + + gdb_assert (at_base_addr < space_size); + + /* E_ENTRY exceeding SPACE_SIZE would be for prelinked + 64bit ld.so with 32bit executable, it should not happen. */ + if (e_entry < space_size + && e_entry + at_base_addr >= space_size) + at_base_addr -= space_size; + } + + /* Compute the entry address. */ + e_entry = at_base_addr + e_entry; + + /* Handle the case where E_ENTRY is a function descriptor. Also remove + any non-address (e.g. tag) bits from E_ENTRY. */ + e_entry + = gdbarch_convert_from_func_ptr_addr (gdbarch, e_entry, + current_inferior ()->top_target ()); + e_entry + = gdbarch_addr_bits_remove (gdbarch, e_entry); + + return e_entry; +} + INIT_GDB_FILE (svr4_solib) { gdb::observers::free_objfile.attach (svr4_free_objfile_observer, diff --git a/gdb/solib-svr4.h b/gdb/solib-svr4.h index 3078a092778..f76f350d951 100644 --- a/gdb/solib-svr4.h +++ b/gdb/solib-svr4.h @@ -115,6 +115,7 @@ struct svr4_solib_ops : public solib_ops void iterate_over_objfiles_in_search_order (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const override; + std::optional<CORE_ADDR> inferior_entry_point_address () const override; /* Return the appropriate link map offsets table for the architecture. */ virtual link_map_offsets *fetch_link_map_offsets () const = 0; diff --git a/gdb/solib.h b/gdb/solib.h index 9e6c3f7346e..7ed00c63471 100644 --- a/gdb/solib.h +++ b/gdb/solib.h @@ -288,6 +288,20 @@ struct solib_ops (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const; + /* Return the inferior entry point address. This isn't always the entry + point of the main executable, but will be the actual entry point where + the inferior starts (or started) executing. + + For example on SVR4 targets, for dynamically linked executables, this + will be the entry address of the dynamic linker. But for statically + linked executables, this will be the entry point of the main + executable. + + If the entry address cannot be found then an empty optional is + returned. */ + virtual std::optional<CORE_ADDR> inferior_entry_point_address () const + { return {}; } + protected: /* The program space for which this solib_ops was created. */ program_space *m_pspace; diff --git a/gdb/testsuite/gdb.base/bt-after-starti.exp b/gdb/testsuite/gdb.base/bt-after-starti.exp new file mode 100644 index 00000000000..6fd8a065878 --- /dev/null +++ b/gdb/testsuite/gdb.base/bt-after-starti.exp @@ -0,0 +1,167 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'bt' from the very first instruction (where 'starti' +# stops GDB) doesn't display any unexpected frames. + +require !use_gdb_stub + +standard_testfile main.c + +# Build default executable, hopefully dynamically linked, but we check +# this below. +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +# Build a statically linked executable. +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + {debug additional_flags=-static}] } { + return +} + +# Run the 'maint info entry-address' command. Expect INF_RE to match +# the inferior entry address and EXE_RE to match the executable entry +# address. TESTNAME is used as the test name. +proc check_maint_info_entry_addr { inf_re exe_re testname } { + gdb_test "maint info entry-address" \ + [multi_line \ + "Inferior entry address: ${inf_re}" \ + "Executable entry address: ${exe_re}"] \ + $testname +} + +# Use 'objdump' to extract the entry point address from TESTFILE. +proc get_exec_entry_address { testfile } { + set objdump_program [gdb_find_objdump] + set command "exec $objdump_program -f [standard_output_file $testfile]" + verbose -log "command is $command" + set result [catch {{*}$command} output] + verbose -log "result is $result" + verbose -log "output is $output" + + set testname "get_exec_entry_address" + + if {$result != 0} { + fail $testname + return "UNKNOWN" + } + + if {![regexp "\nstart address ($::hex)\n" $output trash addr]} { + fail "$testname (no entry point address header)" + return "UNKNOWN" + } + + return 0x[format %x $addr] +} + +# Start TESTFILE using 'starti'. Check if the 'backtrace' command +# prints any additional frames; we don't expect any. Create a core +# file from the running inferior. Restart GDB and load the core file, +# check that GDB can still figure out the entry addresses. +# +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked +# executable, otherwise TESTFILE is statically linked. +proc run_test { testfile is_dynamic } { + clean_restart $testfile + + set exe_entry_addr [get_exec_entry_address $testfile] + + # The inferior entry address is always unknown before starting the + # inferior because GDB has no solib_ops yet, and it is only + # through that that we figure out the inferior entry address. + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ + "check 'maint info entry-address' before inferior starts" + + # Start inferior with 'starti' and then wait for a prompt. + gdb_starti_cmd + gdb_test "" ".*" "prompt after starti" + + # Only svr4 targets currently support querying the inferior entry + # address. + if {[is_svr4_target]} { + set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN" \ + "get current program counter"] + + if { !$is_dynamic } { + # In static binaries, the first address should be the first + # address in the executable. + gdb_assert { $pc == $exe_entry_addr } \ + "stopped at executable entry address" + } + + set inf_entry_addr $pc + } else { + set inf_entry_addr "<unknown>" + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after starti" + + # Allow backtrace past the entry frame, count how many frames GDB + # finds. If on this target there are no additional frames then + # the following test isn't going to tell us much, so skip it. + gdb_test_no_output "set backtrace past-entry on" + set frame_count 0 + gdb_test_multiple "bt" "count possible frames" -lbl { + -re "\r\n#($::decimal)\\s+\[^\r\n\]+(?=\r\n)" { + incr frame_count + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $frame_count > 0 } $gdb_test_name + } + } + + if { $frame_count > 1 } { + # Turn off backtrace past the entry frame. Use the 'bt' command, + # and check we see only a single frame. + gdb_test_no_output "set backtrace past-entry off" + gdb_test "bt" "^#0 \[^\r\n\]+" \ + "single frame when backtrace past-entry is off" + } else { + unsupported "no additional frames that GDB can hide" + } + + # Create a core file. Restart GDB. Load the core file. Check + # that 'maint info entry-address' gives the correct output. + set corefile ${::binfile}.core + if {![gdb_gcore_cmd $corefile "generate corefile"]} { + return + } + + clean_restart $testfile + + if {[gdb_core_cmd $corefile "load corefile"] != 1} { + return + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after loading core file" +} + +with_test_prefix "dynamic" { + set dyln_name [section_get $binfile .interp] + if { $dyln_name eq "" } { + unsupported "couldn't find dynamic linker name" + } else { + run_test $testfile true + } +} + +with_test_prefix "static" { + run_test $testfile_static false +} diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp index 7b1feeaac84..7a924a270f1 100644 --- a/gdb/testsuite/lib/gdb.exp +++ b/gdb/testsuite/lib/gdb.exp @@ -4188,6 +4188,14 @@ proc is_aarch64_target {} { return [expr {![is_aarch32_target]}] } +# Return true for svr4 targets, otherwise, return false. + +proc is_svr4_target {} { + return [expr {[istarget *-linux*] || [istarget *-freebsd*] + || [istarget *-netbsd*] || [istarget *-openbsd*] + || [istarget *-solaris*] || [istarget *-gnu]}] +} + # Return 1 if displaced stepping is supported on target, otherwise, return 0. proc support_displaced_stepping {} { -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv3 2/4] gdb: introduce program_space::get_entry_point_info function 2026-06-16 19:47 ` [PATCHv3 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-06-17 11:52 ` Eli Zaretskii 0 siblings, 0 replies; 54+ messages in thread From: Eli Zaretskii @ 2026-06-17 11:52 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches, guinevere, pedro > From: Andrew Burgess <aburgess@redhat.com> > Cc: Guinevere Larsen <guinevere@redhat.com>, > Pedro Alves <pedro@palves.net>, > Andrew Burgess <aburgess@redhat.com>, > Eli Zaretskii <eliz@gnu.org> > Date: Tue, 16 Jun 2026 20:47:15 +0100 > > I noticed that when debugging a dynamically linked executable, if I > started the inferior with 'starti' then used 'bt' I would see some > bogus frames: > > (gdb) starti > Starting program: /tmp/hello > > Program stopped. > 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 > (gdb) bt > #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 > #1 0x0000000000000001 in ?? () > #2 0x00007fffffffac13 in ?? () > #3 0x0000000000000000 in ?? () > (gdb) > > This surprised me as 'backtrace past-entry' was off: > > (gdb) show backtrace past-entry > Whether backtraces should continue past the entry point of a program is off. > > I was expecting GDB to stop the backtrace at the inferior's entry > address. > > Frame unwinding starts in get_prev_frame, and in here we find this > block: > > 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)) > { > frame_debug_got_null_frame (this_frame, "inside entry func"); > return NULL; > } > > Which uses inside_entry_func to terminate the backtrace when we reach > the entry frame. The inside_entry_func function calls > current_program_space->exec_entry_point_address_if_available and uses > the result to figure out if we are in the entry frame. > > And here the problem becomes obvious, we are only checking if we are > in the "entry frame" for the main executable, not for the inferior as > a whole. And indeed, if I compile the same test program as a static > binary, where there will be no run-time linker, and the entry point of > the executable is the entry point for the inferior, then the 'bt' > problem I saw above goes away. > > This suggests, I think, that we need to track two different entry > addresses, the entry address for the executable file, and the entry > address for the entire inferior. Then, for dynamically linked > executables, these two addresses can be different, the former will > still be the same address within the main executable file, while the > latter will be the address of the entry point within the run-time > linker. > > If we had this information then we could extend inside_entry_func to > check both addresses, and the 'bt' problem seen above will be > resolved. > > To make this information available I added a new solib_ops method, > solib_ops::inferior_entry_point_address, for svr4 targets this figures > out if the main executable is dynamically linked, and if it is, uses > the AT_BASE auxv entry and the entry address pulled from the ELF > header to compute the inferior entry address. > > I then added program_space::get_entry_point_info, which returns a > struct containing the two entry point addresses, one comes from the > new solib_ops method, and one comes from the existing method > program_space::exec_entry_point_address_if_available. > > With the infrastructure in place I can then update inside_entry_func > to check against both entry addresses. > > To aid in debugging GDB, I added a new maintenance command: > > maintenance info entry-address > > which just calls program_space::get_entry_point_info and then prints > the two addresses. I left this as a maintenance command as I don't > see much user utility in this right now, but it made it easier for me > to see what GDB was doing, so I left the command in this commit. > > Reviewed-By: Eli Zaretskii <eliz@gnu.org> > --- > gdb/NEWS | 7 + > gdb/doc/gdb.texinfo | 22 +++ > gdb/frame.c | 10 +- > gdb/progspace.c | 60 ++++++++ > gdb/progspace.h | 48 ++++++ > gdb/solib-svr4.c | 85 +++++++++++ > gdb/solib-svr4.h | 1 + > gdb/solib.h | 14 ++ > gdb/testsuite/gdb.base/bt-after-starti.exp | 167 +++++++++++++++++++++ > gdb/testsuite/lib/gdb.exp | 8 + > 10 files changed, 417 insertions(+), 5 deletions(-) > create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp Thanks, the documentation parts are okay. Reviewed-By: Eli Zaretskii <eliz@gnu.org> ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv3 3/4] gdb: allow 'until' to work in outermost frame 2026-06-16 19:47 ` [PATCHv3 0/4] " Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-06-16 19:47 ` Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 4/4] gdb: cache program space entry point information Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-16 19:47 UTC (permalink / raw) To: gdb-patches; +Cc: Guinevere Larsen, Pedro Alves, Andrew Burgess 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, if the until breakpoint was not hit. 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, where we call inside_entry_func. This checks if a frame is one of the two possible entry frames, the inferior entry frame or the executable entry frame. See the previous commit for more details. The important thing is that the inferior entry frame is the absolute outer frame, the very first frame that the inferior executed when starting, while the executable entry frame is just the first frame within the main executable. There are a number of user configurable filters in get_prev_frame, the backtrace past-main filter, the backtrace frame limit filter, and the backtrace past-entry filter that we are discussing here. When creating the caller frame breakpoint, the 'until' command doesn't use get_prev_frame, it uses get_prev_frame_always. This is so that the user configurable filters don't prevent the caller frame breakpoint from being created. But this means that when creating the breakpoint, we skip the entry frame check. 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 split the inside_entry_func handling in to two parts. In get_prev_frame we will retain the check for the executable entry frame. In theory it is possible that there are frames between the executable entry frame and the inferior entry frame. These are the frames the 'backtrace past-entry' can hide or reveal (if the frames can be discovered). The check for the inferior entry frame I propose moving into get_prev_frame_always. I think this is a better place for it because any frames before the inferior entry frame are almost certainly bogus. As such we want to elide those frames even for "inner" use cases, like the caller frame breakpoint of the 'until' command. 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 | 103 +++++++---- gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 162 ++++++++++++++++++ 3 files changed, 255 insertions(+), 32 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 3920d8d03f7..e86024f7663 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2237,6 +2237,63 @@ frame_register_unwind_location (const frame_info_ptr &initial_this_frame, } } +/* When checking if a frame is an entry frame, there are two different + entry frames to consider. This enum is used to choose between them. */ + +enum class entry_address_type +{ + /* The executable's entry frame. This is the first frame within the main + executable. */ + executable, + + /* The inferior's entry frame. This is the first frame within the + inferior, this can be outside the main executable, e.g. for a + dynamically linked executable, this will be the first frame in the + dynamic linker. */ + inferior, +}; + +/* Test whether THIS_FRAME is inside the TYPE process entry point + function. */ + +static bool +inside_entry_func (const frame_info_ptr &this_frame, + entry_address_type type) +{ + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + CORE_ADDR frame_func_addr; + if (!get_frame_func_if_available (this_frame, &frame_func_addr)) + return false; + + switch (type) + { + case entry_address_type::executable: + return ep_info.exec_entry_address () == frame_func_addr; + + case entry_address_type::inferior: + return ep_info.inferior_entry_address () == frame_func_addr; + } + + gdb_assert_not_reached ("unknown entry address type: %d", ((int) type)); +} + +/* Debug routine to print a NULL frame being returned. */ + +static void +frame_debug_got_null_frame (const frame_info_ptr &this_frame, + const char *reason) +{ + if (frame_debug) + { + if (this_frame != NULL) + frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); + else + frame_debug_printf ("this_frame=nullptr -> %s", reason); + } +} + /* Get the previous raw frame, and check that it is not identical to same other frame frame already in the chain. If it is, there is most likely a stack cycle, so we discard it, and mark THIS_FRAME as @@ -2543,6 +2600,17 @@ get_prev_frame_always_1 (const frame_info_ptr &this_frame) frame_info_ptr get_prev_frame_always (const frame_info_ptr &this_frame) { + if (this_frame->level >= 0 + && get_frame_type (this_frame) == NORMAL_FRAME + && !user_set_backtrace_options.backtrace_past_entry + && inside_entry_func (this_frame, entry_address_type::inferior)) + { + this_frame->prev_p = true; + this_frame->stop_reason = UNWIND_OUTERMOST; + frame_debug_got_null_frame (this_frame, "inside inferior entry func"); + return nullptr; + } + frame_info_ptr prev_frame = NULL; try @@ -2631,21 +2699,6 @@ get_prev_frame_raw (const frame_info_ptr &this_frame) return frame_info_ptr (prev_frame); } -/* Debug routine to print a NULL frame being returned. */ - -static void -frame_debug_got_null_frame (const frame_info_ptr &this_frame, - const char *reason) -{ - if (frame_debug) - { - if (this_frame != NULL) - frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); - else - frame_debug_printf ("this_frame=nullptr -> %s", reason); - } -} - /* Is this (non-sentinel) frame in the "main"() function? */ static bool @@ -2694,19 +2747,6 @@ inside_main_func (const frame_info_ptr &this_frame) return sym_addr == get_frame_func (this_frame); } -/* Test whether THIS_FRAME is inside the process entry point function. */ - -static bool -inside_entry_func (const frame_info_ptr &this_frame) -{ - const program_space::entry_point_info &ep_info - = current_program_space->get_entry_point_info (); - - CORE_ADDR frame_func_addr = get_frame_func (this_frame); - return (ep_info.exec_entry_address () == frame_func_addr - || ep_info.inferior_entry_address () == frame_func_addr); -} - /* Return a structure containing various interesting information about the frame that called THIS_FRAME. Returns NULL if there is either no such frame or the frame fails any of a set of target-independent @@ -2788,11 +2828,10 @@ get_prev_frame (const frame_info_ptr &this_frame) 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)) + && inside_entry_func (this_frame, entry_address_type::executable)) { - frame_debug_got_null_frame (this_frame, "inside entry func"); - return NULL; + frame_debug_got_null_frame (this_frame, "inside executable entry func"); + return nullptr; } /* Assume that the only way to get a zero PC is through something 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 <http://www.gnu.org/licenses/>. */ + +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..2338f027160 --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp @@ -0,0 +1,162 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'until' in the outermost frame works as expected. + +require !use_gdb_stub + +standard_testfile + +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + { debug additional_flags=-static } ] } { + return +} + +# Use 'frame' 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 "frame" $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 testfile } { + clean_restart $testfile + + if { [gdb_starti_cmd] < 0 } { + untested starti + return + } + + # Consume up to the first prompt after 'starti' command. + gdb_test "" "" "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" -lbl { + -re "\r\n=> $::hex \[^\r\n\]+(?=\r\n)" { + set capture_address true + exp_continue + } + + -re "\r\n\\s+($::hex) \[^\r\n\]+(?=\r\n)" { + if { $capture_address } { + set until_address $expect_out(1,string) + set capture_address false + } + + exp_continue + } + + -re "\r\n$::gdb_prompt $" { + set found_address [expr { $until_address ne "" }] + if { !$found_address } { + unsupported "$gdb_test_name (no instruction found)" + return + } + pass $gdb_test_name + } + } + + # 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 $testfile + + with_test_prefix "static" { + run_test $use_stepi $past_entry $testfile_static + } + } +} -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv3 4/4] gdb: cache program space entry point information 2026-06-16 19:47 ` [PATCHv3 0/4] " Andrew Burgess ` (2 preceding siblings ...) 2026-06-16 19:47 ` [PATCHv3 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-06-16 19:47 ` Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-16 19:47 UTC (permalink / raw) To: gdb-patches; +Cc: Guinevere Larsen, Pedro Alves, Andrew Burgess After the previous two patches, every time that GDB unwinds a frame there are now two places where we check if the frame is an entry point frame, these are in get_prev_frame and get_prev_frame_always, look for the calls to `inside_entry_func`, which then calls program_space::get_entry_point_info. The calls to program_space::get_entry_point_info are not crazy expensive, but they are not free either, there are reads from target memory to read the auxv vector and the entry address offset, so on remote targets this could introduce a small delay, especially as this occurs twice per frame now. However, the information returned by program_space::get_entry_point_info is not expected to change from one call to the next. This information should be a property of the executable and libraries, so we really only need to figure it out once. This commit caches the entry point information within program_space, clearing it whenever an inferior starts or exits, or whenever the executable is updated. After clearing GDB will compute, and cache the updated information the next time it is needed, which will be whenever GDB needs to unwind a frame. It will be possible to observe this change by, for example, monitoring the remote target packets, but as far as the normal GDB output is concerned, there should be no user visible changes after this commit. --- gdb/progspace.c | 34 +++++++++++++++++++++++++++++++--- gdb/progspace.h | 9 ++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/gdb/progspace.c b/gdb/progspace.c index 26e16cae8d0..c1f3044f4c5 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -289,9 +289,12 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ -program_space::entry_point_info +const program_space::entry_point_info & program_space::get_entry_point_info () const { + if (m_entry_point_info.has_value ()) + return m_entry_point_info.value (); + std::optional<CORE_ADDR> exec_entry_address = this->exec_entry_point_address_if_available (); @@ -299,8 +302,9 @@ program_space::get_entry_point_info () const if (m_solib_ops != nullptr) inferior_entry_address = m_solib_ops->inferior_entry_point_address (); - return entry_point_info (std::move (inferior_entry_address), - std::move (exec_entry_address)); + m_entry_point_info.emplace (std::move (inferior_entry_address), + std::move (exec_entry_address)); + return m_entry_point_info.value (); } /* Implement the 'maint info entry-address' command. */ @@ -538,11 +542,35 @@ program_space::clear_solib_cache () deleted_solibs.clear (); } +/* Clear cached entry point information in the program space of INF. */ + +static void +clear_cached_entry_point_info_for_inferior (inferior *inf) +{ + inf->pspace->clear_cached_entry_point_info (); +} + +/* Clear cached entry point information in the program space PSPACE. */ + +static void +clear_cached_entry_point_info_for_pspace (program_space *pspace, + bool /* reload */) +{ + pspace->clear_cached_entry_point_info (); +} + /* See progspace.h. */ void initialize_progspace () { + gdb::observers::inferior_created.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::inferior_exit.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::executable_changed.attach + (clear_cached_entry_point_info_for_pspace, "program-space"); + add_cmd ("program-spaces", class_maintenance, maintenance_info_program_spaces_command, _("Info about currently known program spaces."), diff --git a/gdb/progspace.h b/gdb/progspace.h index d75379fe621..407d2595136 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -369,7 +369,11 @@ struct program_space /* Return information about the entry point in the main executable, and the entry point for the inferior, which might be different from the main executable. */ - entry_point_info get_entry_point_info () const; + const entry_point_info &get_entry_point_info () const; + + /* Clear any cached entry point information. */ + void clear_cached_entry_point_info () + { m_entry_point_info.reset (); } /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ @@ -462,6 +466,9 @@ struct program_space /* See `exec_filename`. */ gdb::unique_xmalloc_ptr<char> m_exec_filename; + + /* Cached entry point information. See get_entry_point_info. */ + mutable std::optional<entry_point_info> m_entry_point_info; }; /* The list of all program spaces. There's always at least one. */ -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame 2026-06-16 19:47 ` [PATCHv3 0/4] " Andrew Burgess ` (3 preceding siblings ...) 2026-06-16 19:47 ` [PATCHv3 4/4] gdb: cache program space entry point information Andrew Burgess @ 2026-06-23 10:47 ` Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess ` (4 more replies) 4 siblings, 5 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-23 10:47 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves In v4: - Rebase to current HEAD of master. - In patch #2, added a supports_process_entry_point proc as suggested by Pedro, and make use of this in the new test. Updates to address review comments from v2 series: - Doc updates based on Eli's feedback. - Patch #2: + Avoid swallowing Ctrl-C in try/catch. That whole try/catch block has been removed in v3, so problem sorted! + Rewrote entry address calculation as suggested by Pedro, now pulls all required information from the inferior. + Use 'objdump -f' rather than readelf to get the entry address in the test. + Added a helper proc for is_svr4_target, which includes a larger list of targets beyond just Linux. This is then used in the test. + Test updated to use -lbl flag in gdb_test_multiple. - Patch #3: + Use 'frame' rather than 'bt 1' + Loosen the test regexp after the 'starti' command, this should now work on Windows targets, or any target with multiple threads at startup. + Test updated to use -lbl in gdb_test_multiple. - In patch #3 the entry frame detection is moved from `frame_unwind_caller_frame` into `get_prev_frame_always`, I hope this will address Guinevere's concerns about having to add the additional entry frame detection in multiple locations. Guinevere also had some concerns about the naming of existing functions (i.e. not things added by me in this commit), I don't plan to make any naming changes to the get_prev_frame* functions in this series. - Patch #4 is new, this adds some caching to avoid repeatedly looking up the entry address. The new approach requires reading target memory, which can be expensive, especially on remote targets. --- Andrew Burgess (4): gdb: rename program_space::entry_point_address* functions gdb: introduce program_space::get_entry_point_info function gdb: allow 'until' to work in outermost frame gdb: cache program space entry point information gdb/NEWS | 7 + gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/doc/gdb.texinfo | 22 +++ gdb/frame.c | 105 +++++++---- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 94 +++++++++- gdb/progspace.h | 67 ++++++- gdb/solib-frv.c | 2 +- gdb/solib-svr4.c | 85 +++++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 168 ++++++++++++++++++ gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 162 +++++++++++++++++ gdb/testsuite/lib/gdb.exp | 39 ++++ 17 files changed, 749 insertions(+), 47 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.c create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.exp base-commit: 717ef05c9c6d5f7a07540df2e79853d04b56f750 -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv4 1/4] gdb: rename program_space::entry_point_address* functions 2026-06-23 10:47 ` [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-06-23 10:47 ` Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess ` (3 subsequent siblings) 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-23 10:47 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves Rename program_space::entry_point_address to program_space::exec_entry_point_address and program_space::entry_point_address_query to program_space::exec_entry_point_address_if_available. There are two aspects to this renaming. First I replace 'query' with 'if_available' in one of the functions. I feel this better describes the function, and also is inline with how other, similar, functions are named in GDB. The second part of the renaming is to add the 'exec_' prefix to the front of both function names. When a dynamically linked inferior is started the first instruction executed is actually within the run-time linker, not within the main executable, so it could be argued that the actual entry address for the inferior is not the entry address of the main executable. However, there is an equally valid argument that the entry address of the executable is also something worth finding. The existing entry address within the inferior is used for a number of tasks in GDB, for example displaced stepping (arch-utils.c), inferior function calls (arc-tdep.c and infcall.c), and for detecting the "entry" frame. This last one, the entry frame detection is interesting. In a dynamically linked executable it could be argued that there are two entry frames. The very first frame that is executed in the inferior, this is where 'starti' stops the inferior. And then the very first frame within the main executable. I think that both of these are valid. Currently, as we can only find the entry address within the main executable, we can only identify the first frame of the main executable. And this can cause some problems, consider: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) Here frames #1 to #3 are all bogus, created by GDB based on whatever values happen to be in the registers when the inferior starts. In a later commit I'd like to fix this problem, however, I would prefer that the other users of program_space::entry_point_address continue to use the address within the main executable. So, in order to keep the distinction between the two different types of entry point, I'm renaming the existing functions with the 'exec_' prefix. There should be no user visible changes after this commit. Approved-By: Pedro Alves <pedro@palves.net> --- gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/frame.c | 4 ++-- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 6 +++--- gdb/progspace.h | 12 ++++++------ gdb/solib-frv.c | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gdb/arc-tdep.c b/gdb/arc-tdep.c index 4936a5c8fbb..7ed83c4cae6 100644 --- a/gdb/arc-tdep.c +++ b/gdb/arc-tdep.c @@ -860,7 +860,7 @@ arc_push_dummy_code (struct gdbarch *gdbarch, CORE_ADDR sp, CORE_ADDR funaddr, struct regcache *regcache) { *real_pc = funaddr; - *bp_addr = current_program_space->entry_point_address (); + *bp_addr = current_program_space->exec_entry_point_address (); return sp; } diff --git a/gdb/arch-utils.c b/gdb/arch-utils.c index e959788bd3b..88047731ce6 100644 --- a/gdb/arch-utils.c +++ b/gdb/arch-utils.c @@ -57,7 +57,7 @@ displaced_step_at_entry_point (struct gdbarch *gdbarch) CORE_ADDR addr; int bp_len; - addr = current_program_space->entry_point_address (); + addr = current_program_space->exec_entry_point_address (); /* Inferior calls also use the entry point as a breakpoint location. We don't want displaced stepping to interfere with those diff --git a/gdb/frame.c b/gdb/frame.c index f64f5554f8c..f5a6a8919df 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2700,7 +2700,7 @@ static bool inside_entry_func (const frame_info_ptr &this_frame) { std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) return false; @@ -2776,7 +2776,7 @@ get_prev_frame (const frame_info_ptr &this_frame) added to work around that (now fixed) case. */ /* NOTE: cagney/2003-07-15: danielj (if I'm reading it right) suggested having the inside_entry_func test use the - inside_main_func() msymbol trick (along with entry_point_address() + inside_main_func() msymbol trick (along with exec_entry_point_address() I guess) to determine the address range of the start function. That should provide a far better stopper than the current heuristics. */ diff --git a/gdb/infcall.c b/gdb/infcall.c index e6b24ff5310..6d26841ede3 100644 --- a/gdb/infcall.c +++ b/gdb/infcall.c @@ -1300,7 +1300,7 @@ call_function_by_hand_dummy (struct value *function, CORE_ADDR dummy_addr; real_pc = funaddr; - dummy_addr = current_program_space->entry_point_address (); + dummy_addr = current_program_space->exec_entry_point_address (); /* A call dummy always consists of just a single breakpoint, so its address is the same as the address of the dummy. diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c index a7381677498..d03e4768792 100644 --- a/gdb/linux-tdep.c +++ b/gdb/linux-tdep.c @@ -2952,7 +2952,7 @@ linux_displaced_step_location (struct gdbarch *gdbarch) /* Determine entry point from target auxiliary vector. This avoids the need for symbols. Also, when debugging a stand-alone SPU - executable, entry_point_address () will point to an SPU + executable, exec_entry_point_address () will point to an SPU local-store address and is thus not usable as displaced stepping location. The auxiliary vector gets us the PowerPC-side entry point address instead. */ diff --git a/gdb/progspace.c b/gdb/progspace.c index 1407b058dfd..0e8516f4640 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -263,7 +263,7 @@ program_space::empty () /* See progspace.h. */ std::optional<CORE_ADDR> -program_space::entry_point_address_query () const +program_space::exec_entry_point_address_if_available () const { objfile *objf = symfile_object_file; if (objf == NULL || !objf->per_bfd->ei.entry_point_p) @@ -276,9 +276,9 @@ program_space::entry_point_address_query () const /* See progspace.h. */ CORE_ADDR -program_space::entry_point_address () const +program_space::exec_entry_point_address () const { - std::optional<CORE_ADDR> retval = entry_point_address_query (); + std::optional<CORE_ADDR> retval = exec_entry_point_address_if_available (); if (!retval.has_value ()) error (_("Entry point address is not known.")); diff --git a/gdb/progspace.h b/gdb/progspace.h index e9261ff8590..1ae1e42f3bb 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,13 +323,13 @@ struct program_space return m_target_sections; } - /* If there is a valid and known entry point in this program space, - return it. Otherwise return an empty optional. */ - std::optional<CORE_ADDR> entry_point_address_query () const; + /* If there is a valid and known entry point in the main executable of + this program space, return it. Otherwise return an empty optional. */ + std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; - /* Get the entry point address in this program space. Call error if - it is not known. */ - CORE_ADDR entry_point_address () const; + /* Get the entry point address for the main executable in this program + space. Call error if it is not known. */ + CORE_ADDR exec_entry_point_address () const; /* Return true if any objfile of this program space has partial symbols. */ diff --git a/gdb/solib-frv.c b/gdb/solib-frv.c index 4f0aac31e73..69b882e534a 100644 --- a/gdb/solib-frv.c +++ b/gdb/solib-frv.c @@ -689,7 +689,7 @@ enable_break (void) } std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) { solib_debug_printf ("Symbol file has no entry point."); -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv4 2/4] gdb: introduce program_space::get_entry_point_info function 2026-06-23 10:47 ` [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess @ 2026-06-23 10:47 ` Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 subsequent siblings) 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-23 10:47 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves, Eli Zaretskii I noticed that when debugging a dynamically linked executable, if I started the inferior with 'starti' then used 'bt' I would see some bogus frames: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) This surprised me as 'backtrace past-entry' was off: (gdb) show backtrace past-entry Whether backtraces should continue past the entry point of a program is off. I was expecting GDB to stop the backtrace at the inferior's entry address. Frame unwinding starts in get_prev_frame, and in here we find this block: 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)) { frame_debug_got_null_frame (this_frame, "inside entry func"); return NULL; } Which uses inside_entry_func to terminate the backtrace when we reach the entry frame. The inside_entry_func function calls current_program_space->exec_entry_point_address_if_available and uses the result to figure out if we are in the entry frame. And here the problem becomes obvious, we are only checking if we are in the "entry frame" for the main executable, not for the inferior as a whole. And indeed, if I compile the same test program as a static binary, where there will be no run-time linker, and the entry point of the executable is the entry point for the inferior, then the 'bt' problem I saw above goes away. This suggests, I think, that we need to track two different entry addresses, the entry address for the executable file, and the entry address for the entire inferior. Then, for dynamically linked executables, these two addresses can be different, the former will still be the same address within the main executable file, while the latter will be the address of the entry point within the run-time linker. If we had this information then we could extend inside_entry_func to check both addresses, and the 'bt' problem seen above will be resolved. To make this information available I added a new solib_ops method, solib_ops::inferior_entry_point_address, for svr4 targets this figures out if the main executable is dynamically linked, and if it is, uses the AT_BASE auxv entry and the entry address pulled from the ELF header to compute the inferior entry address. I then added program_space::get_entry_point_info, which returns a struct containing the two entry point addresses, one comes from the new solib_ops method, and one comes from the existing method program_space::exec_entry_point_address_if_available. With the infrastructure in place I can then update inside_entry_func to check against both entry addresses. To aid in debugging GDB, I added a new maintenance command: maintenance info entry-address which just calls program_space::get_entry_point_info and then prints the two addresses. I left this as a maintenance command as I don't see much user utility in this right now, but it made it easier for me to see what GDB was doing, so I left the command in this commit. Reviewed-By: Eli Zaretskii <eliz@gnu.org> --- gdb/NEWS | 7 + gdb/doc/gdb.texinfo | 22 +++ gdb/frame.c | 10 +- gdb/progspace.c | 60 ++++++++ gdb/progspace.h | 48 ++++++ gdb/solib-svr4.c | 85 +++++++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 168 +++++++++++++++++++++ gdb/testsuite/lib/gdb.exp | 39 +++++ 10 files changed, 449 insertions(+), 5 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp diff --git a/gdb/NEWS b/gdb/NEWS index d5214a98a57..4290f835845 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -146,6 +146,13 @@ disable skip These are new aliases for 'skip delete', 'skip enable', and 'skip disable' respectively. +maint info entry-address + Display the inferior and main executable entry addresses for the + current inferior. The inferior entry address is the address of the + first instruction in the inferior that was executed. The main + executable entry address is the address of the first instruction in + the main executable that will be executed. + * MI changes ** The "-trace-save" command no longer supports the "-ctf" flag. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index a698b2b8451..4c0c4709a23 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -43212,6 +43212,28 @@ Maintenance Commands Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M @end smallexample + +@kindex maint info entry-address +@item maint info entry-address +Display the entry addresses for the currently selected inferior. Two +addresses are displayed, the inferior entry address and the executable +entry address. + +Neither address is the address of @code{main}. When a program is +started, low-level startup code (typically a function called +@code{_start}) runs before @code{main} is called. + +The executable entry address is the address of this startup code +within the main executable (@pxref{Files, ,Commands to Specify +Files}). + +The inferior entry address is the address of the very first +instruction executed when the inferior is started. For statically +linked programs this is the same as the executable entry address. For +dynamically linked programs the run-time linker must execute first in +order to load shared libraries, so the inferior entry address will be +an address within the run-time linker rather than within the main +executable. @end table The following command is useful for non-interactive invocations of diff --git a/gdb/frame.c b/gdb/frame.c index f5a6a8919df..3920d8d03f7 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2699,12 +2699,12 @@ inside_main_func (const frame_info_ptr &this_frame) static bool inside_entry_func (const frame_info_ptr &this_frame) { - std::optional<CORE_ADDR> entry_point - = current_program_space->exec_entry_point_address_if_available (); - if (!entry_point.has_value ()) - return false; + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); - return get_frame_func (this_frame) == *entry_point; + CORE_ADDR frame_func_addr = get_frame_func (this_frame); + return (ep_info.exec_entry_address () == frame_func_addr + || ep_info.inferior_entry_address () == frame_func_addr); } /* Return a structure containing various interesting information about diff --git a/gdb/progspace.c b/gdb/progspace.c index 0e8516f4640..26e16cae8d0 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -26,6 +26,7 @@ #include <algorithm> #include "cli/cli-style.h" #include "observable.h" +#include "arch-utils.h" /* The last program space number assigned. */ static int last_program_space_num = 0; @@ -288,6 +289,55 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ +program_space::entry_point_info +program_space::get_entry_point_info () const +{ + std::optional<CORE_ADDR> exec_entry_address + = this->exec_entry_point_address_if_available (); + + std::optional<CORE_ADDR> inferior_entry_address; + if (m_solib_ops != nullptr) + inferior_entry_address = m_solib_ops->inferior_entry_point_address (); + + return entry_point_info (std::move (inferior_entry_address), + std::move (exec_entry_address)); +} + +/* Implement the 'maint info entry-address' command. */ + +static void +maintenance_info_entry_address (const char *args, int from_tty) +{ + if (args != nullptr && *args != '\0') + error (_("unknown argument: %s"), args); + + struct gdbarch *gdbarch = get_current_arch (); + + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + /* Display a single entry address ADDR with TITLE as a description. */ + auto display_entry_address + = [&gdbarch] (const char *title, + const std::optional<CORE_ADDR> &addr) -> void + { + gdb_puts (title); + if (addr.has_value ()) + fputs_styled (paddress (gdbarch, addr.value ()), + address_style.style (), gdb_stdout); + else + fputs_styled ("<unknown>", metadata_style.style (), gdb_stdout); + gdb_puts ("\n"); + }; + + display_entry_address (_("Inferior entry address: "), + ep_info.inferior_entry_address ()); + display_entry_address (_("Executable entry address: "), + ep_info.exec_entry_address ()); +} + +/* See progspace.h. */ + bool program_space::has_partial_symbols () { @@ -498,6 +548,16 @@ initialize_progspace () _("Info about currently known program spaces."), &maintenanceinfolist); + add_cmd ("entry-address", class_maintenance, + maintenance_info_entry_address, + _("Information about the current inferior's entry addresses.\n\ +Display the address of the first instruction executed within the\n\ +inferior, and the first instruction executed within the main executable.\n\ +The entry address of the whole inferior will be <unknown> prior to the\n\ +inferior starting, and either or both addresses can be <unknown> if\n\ +GDB is unable to find the required information."), + &maintenanceinfolist); + /* There's always one program space. Note that this function isn't an automatic _initialize_foo function, since other _initialize_foo routines may need to install their per-pspace diff --git a/gdb/progspace.h b/gdb/progspace.h index 1ae1e42f3bb..d75379fe621 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,6 +323,54 @@ struct program_space return m_target_sections; } + /* Class for tracking two possible entry points that an inferior might + have, the entry point for the entire inferior, and the entry point + within the main executable. */ + struct entry_point_info + { + explicit entry_point_info (std::optional<CORE_ADDR> inferior_entry_address, + std::optional<CORE_ADDR> exec_entry_address) + : m_inferior_entry_address (std::move (inferior_entry_address)), + m_exec_entry_address (std::move (exec_entry_address)) + { /* Nothing. */ } + + DISABLE_COPY_AND_ASSIGN (entry_point_info); + + /* The entry address within the inferior as a whole. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &inferior_entry_address () const + { + return m_inferior_entry_address; + } + + /* The entry address within the main executable. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &exec_entry_address () const + { + return m_exec_entry_address; + } + + private: + /* The entry address within the inferior. This can be outside of the + main executable, e.g. for dynamically linked executables this could + be the entry address for the run-time linker. For statically linked + executables this will be the entry address of the main executable. + This can be empty if GDB doesn't know how to figure out the correct + entry address for any reason. */ + std::optional<CORE_ADDR> m_inferior_entry_address; + + /* The entry address within the main executable. This can be empty if + the main executable is not set yet, or GDB doesn't have an objfile + associated with the main executable, e.g. in some attach, or remote + debug cases. */ + std::optional<CORE_ADDR> m_exec_entry_address; + }; + + /* Return information about the entry point in the main executable, and + the entry point for the inferior, which might be different from the + main executable. */ + entry_point_info get_entry_point_info () const; + /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c index 8e3de4d3ea1..12f3e783744 100644 --- a/gdb/solib-svr4.c +++ b/gdb/solib-svr4.c @@ -3777,6 +3777,91 @@ svr4_solib_ops::get_solibs_in_ns (int nsid) const return ns_solibs; } +/* See solib.h. */ + +std::optional<CORE_ADDR> +svr4_solib_ops::inferior_entry_point_address () const +{ + std::optional<gdb::byte_vector> interp_name_holder + = svr4_find_program_interpreter (); + + /* No interpreter means this is a static executable. Ask the + program_space for the entry address within the main executable. */ + if (!interp_name_holder.has_value ()) + return m_pspace->exec_entry_point_address_if_available (); + + /* For a dynamically linked executable the inferior's true entry point + is the entry point of the dynamic linker. We find this using the + AT_BASE auxiliary vector entry, which gives the dynamic linker's + load address, combined with e_entry address pulled from the + inferior. We assume that the ELF header can be read from AT_BASE. */ + CORE_ADDR at_base_addr; + if (target_auxv_search (AT_BASE, &at_base_addr) <= 0) + return {}; + + /* Determine ELF architecture type. Use the size of a program header + entry to determine which ELF header we can expect to find. */ + size_t e_entry_offset = 0; + size_t e_entry_size = 0; + CORE_ADDR at_phent; + if (target_auxv_search (AT_PHENT, &at_phent) <= 0) + return {}; + if (at_phent == sizeof (Elf32_External_Phdr)) + { + e_entry_offset = offsetof (Elf32_External_Ehdr, e_entry); + e_entry_size = 4; + } + else if (at_phent == sizeof (Elf64_External_Phdr)) + { + e_entry_offset = offsetof (Elf64_External_Ehdr, e_entry); + e_entry_size = 8; + } + else + return {}; + + /* Architecture of the current inferior. */ + gdbarch *gdbarch = current_inferior ()->arch (); + + /* Read the entry address from the ELF header at AT_BASE_ADDR. */ + CORE_ADDR e_entry; + gdb_byte buffer[sizeof (CORE_ADDR)]; + if (target_read_memory (at_base_addr + e_entry_offset, buffer, e_entry_size)) + return {}; + bfd_endian byte_order = gdbarch_byte_order (gdbarch); + e_entry = extract_unsigned_integer (buffer, e_entry_size, byte_order); + + /* Ensure AT_BASE_ADDR has proper sign in its possible upper bits so + that `+ at_base_addr' will overflow CORE_ADDR width not creating + invalid addresses like 0x101234567 for 32bit inferiors on 64bit + GDB. */ + int addr_bit = gdbarch_addr_bit (gdbarch); + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) + { + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; + + gdb_assert (at_base_addr < space_size); + + /* E_ENTRY exceeding SPACE_SIZE would be for prelinked + 64bit ld.so with 32bit executable, it should not happen. */ + if (e_entry < space_size + && e_entry + at_base_addr >= space_size) + at_base_addr -= space_size; + } + + /* Compute the entry address. */ + e_entry = at_base_addr + e_entry; + + /* Handle the case where E_ENTRY is a function descriptor. Also remove + any non-address (e.g. tag) bits from E_ENTRY. */ + e_entry + = gdbarch_convert_from_func_ptr_addr (gdbarch, e_entry, + current_inferior ()->top_target ()); + e_entry + = gdbarch_addr_bits_remove (gdbarch, e_entry); + + return e_entry; +} + INIT_GDB_FILE (svr4_solib) { gdb::observers::free_objfile.attach (svr4_free_objfile_observer, diff --git a/gdb/solib-svr4.h b/gdb/solib-svr4.h index 3078a092778..f76f350d951 100644 --- a/gdb/solib-svr4.h +++ b/gdb/solib-svr4.h @@ -115,6 +115,7 @@ struct svr4_solib_ops : public solib_ops void iterate_over_objfiles_in_search_order (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const override; + std::optional<CORE_ADDR> inferior_entry_point_address () const override; /* Return the appropriate link map offsets table for the architecture. */ virtual link_map_offsets *fetch_link_map_offsets () const = 0; diff --git a/gdb/solib.h b/gdb/solib.h index 9e6c3f7346e..7ed00c63471 100644 --- a/gdb/solib.h +++ b/gdb/solib.h @@ -288,6 +288,20 @@ struct solib_ops (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const; + /* Return the inferior entry point address. This isn't always the entry + point of the main executable, but will be the actual entry point where + the inferior starts (or started) executing. + + For example on SVR4 targets, for dynamically linked executables, this + will be the entry address of the dynamic linker. But for statically + linked executables, this will be the entry point of the main + executable. + + If the entry address cannot be found then an empty optional is + returned. */ + virtual std::optional<CORE_ADDR> inferior_entry_point_address () const + { return {}; } + protected: /* The program space for which this solib_ops was created. */ program_space *m_pspace; diff --git a/gdb/testsuite/gdb.base/bt-after-starti.exp b/gdb/testsuite/gdb.base/bt-after-starti.exp new file mode 100644 index 00000000000..48030e06a4b --- /dev/null +++ b/gdb/testsuite/gdb.base/bt-after-starti.exp @@ -0,0 +1,168 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'bt' from the very first instruction (where 'starti' +# stops GDB) doesn't display any unexpected frames. + +require !use_gdb_stub + +standard_testfile main.c + +# Build default executable, hopefully dynamically linked, but we check +# this below. +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +# Build a statically linked executable. +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + {debug additional_flags=-static}] } { + return +} + +# Run the 'maint info entry-address' command. Expect INF_RE to match +# the inferior entry address and EXE_RE to match the executable entry +# address. TESTNAME is used as the test name. +proc check_maint_info_entry_addr { inf_re exe_re testname } { + gdb_test "maint info entry-address" \ + [multi_line \ + "Inferior entry address: ${inf_re}" \ + "Executable entry address: ${exe_re}"] \ + $testname +} + +# Use 'objdump' to extract the entry point address from TESTFILE. +proc get_exec_entry_address { testfile } { + set objdump_program [gdb_find_objdump] + set command "exec $objdump_program -f [standard_output_file $testfile]" + verbose -log "command is $command" + set result [catch {{*}$command} output] + verbose -log "result is $result" + verbose -log "output is $output" + + set testname "get_exec_entry_address" + + if {$result != 0} { + fail $testname + return "UNKNOWN" + } + + if {![regexp "\nstart address ($::hex)\n" $output trash addr]} { + fail "$testname (no entry point address header)" + return "UNKNOWN" + } + + return 0x[format %x $addr] +} + +# Start TESTFILE using 'starti'. Check if the 'backtrace' command +# prints any additional frames; we don't expect any. Create a core +# file from the running inferior. Restart GDB and load the core file, +# check that GDB can still figure out the entry addresses. +# +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked +# executable, otherwise TESTFILE is statically linked. +proc run_test { testfile is_dynamic } { + clean_restart $testfile + + set exe_entry_addr [get_exec_entry_address $testfile] + + # The inferior entry address is always unknown before starting the + # inferior because GDB has no solib_ops yet, and it is only + # through that that we figure out the inferior entry address. + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ + "check 'maint info entry-address' before inferior starts" + + # Start inferior with 'starti' and then wait for a prompt. + gdb_starti_cmd + gdb_test "" ".*" "prompt after starti" + + # Not every target supports finding the process entry point. + # Targets that don't support this report '<unknown>', we check for + # this too on those targets. + if {[supports_process_entry_point]} { + set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN" \ + "get current program counter"] + + if { !$is_dynamic } { + # In static binaries, the first address should be the first + # address in the executable. + gdb_assert { $pc == $exe_entry_addr } \ + "stopped at executable entry address" + } + + set inf_entry_addr $pc + } else { + set inf_entry_addr "<unknown>" + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after starti" + + # Allow backtrace past the entry frame, count how many frames GDB + # finds. If on this target there are no additional frames then + # the following test isn't going to tell us much, so skip it. + gdb_test_no_output "set backtrace past-entry on" + set frame_count 0 + gdb_test_multiple "bt" "count possible frames" -lbl { + -re "\r\n#($::decimal)\\s+\[^\r\n\]+(?=\r\n)" { + incr frame_count + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $frame_count > 0 } $gdb_test_name + } + } + + if { $frame_count > 1 } { + # Turn off backtrace past the entry frame. Use the 'bt' command, + # and check we see only a single frame. + gdb_test_no_output "set backtrace past-entry off" + gdb_test "bt" "^#0 \[^\r\n\]+" \ + "single frame when backtrace past-entry is off" + } else { + unsupported "no additional frames that GDB can hide" + } + + # Create a core file. Restart GDB. Load the core file. Check + # that 'maint info entry-address' gives the correct output. + set corefile ${::binfile}.core + if {![gdb_gcore_cmd $corefile "generate corefile"]} { + return + } + + clean_restart $testfile + + if {[gdb_core_cmd $corefile "load corefile"] != 1} { + return + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after loading core file" +} + +with_test_prefix "dynamic" { + set dyln_name [section_get $binfile .interp] + if { $dyln_name eq "" } { + unsupported "couldn't find dynamic linker name" + } else { + run_test $testfile true + } +} + +with_test_prefix "static" { + run_test $testfile_static false +} diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp index d8619ded236..8be2e12ab73 100644 --- a/gdb/testsuite/lib/gdb.exp +++ b/gdb/testsuite/lib/gdb.exp @@ -4226,6 +4226,45 @@ proc is_aarch64_target {} { return [expr {![is_aarch32_target]}] } +# Return true for svr4 targets, otherwise, return false. + +proc is_svr4_target {} { + return [expr {[istarget *-linux*] || [istarget *-freebsd*] + || [istarget *-netbsd*] || [istarget *-openbsd*] + || [istarget *-solaris*] || [istarget *-gnu]}] +} + +# Return false for targets that don't support finding the whole +# process entry address, otherwise, return true. + +proc supports_process_entry_point {} { + # SVR4 targets support finding the entry point. This is done + # within GDB so will work even for remote targets. + if {[is_svr4_target]} { + return true + } + + # Windows and Darwin don't currently support this. + if {[istarget *-*-mingw*] + || [istarget *-*-cygwin*] + || [istarget *-*-pe*] + || [istarget *-*-darwin*]} { + return false + } + + # For remote targets there is no RSP packet to retrieve the entry + # point, so this won't work unless the solib code can handle this + # within GDB, see the svr4 check above. + if {[gdb_protocol_is_remote]} { + return false + } + + # Assume everything else supports this by default. If a test + # fails because we get here then either fix GDB to support this + # feature, or add a new deny list entry above. + return true +} + # Return 1 if displaced stepping is supported on target, otherwise, return 0. proc support_displaced_stepping {} { -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv4 3/4] gdb: allow 'until' to work in outermost frame 2026-06-23 10:47 ` [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-06-23 10:47 ` Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 4/4] gdb: cache program space entry point information Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-23 10:47 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves 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, if the until breakpoint was not hit. 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, where we call inside_entry_func. This checks if a frame is one of the two possible entry frames, the inferior entry frame or the executable entry frame. See the previous commit for more details. The important thing is that the inferior entry frame is the absolute outer frame, the very first frame that the inferior executed when starting, while the executable entry frame is just the first frame within the main executable. There are a number of user configurable filters in get_prev_frame, the backtrace past-main filter, the backtrace frame limit filter, and the backtrace past-entry filter that we are discussing here. When creating the caller frame breakpoint, the 'until' command doesn't use get_prev_frame, it uses get_prev_frame_always. This is so that the user configurable filters don't prevent the caller frame breakpoint from being created. But this means that when creating the breakpoint, we skip the entry frame check. 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 split the inside_entry_func handling in to two parts. In get_prev_frame we will retain the check for the executable entry frame. In theory it is possible that there are frames between the executable entry frame and the inferior entry frame. These are the frames the 'backtrace past-entry' can hide or reveal (if the frames can be discovered). The check for the inferior entry frame I propose moving into get_prev_frame_always. I think this is a better place for it because any frames before the inferior entry frame are almost certainly bogus. As such we want to elide those frames even for "inner" use cases, like the caller frame breakpoint of the 'until' command. 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 | 103 +++++++---- gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 162 ++++++++++++++++++ 3 files changed, 255 insertions(+), 32 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 3920d8d03f7..e86024f7663 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2237,6 +2237,63 @@ frame_register_unwind_location (const frame_info_ptr &initial_this_frame, } } +/* When checking if a frame is an entry frame, there are two different + entry frames to consider. This enum is used to choose between them. */ + +enum class entry_address_type +{ + /* The executable's entry frame. This is the first frame within the main + executable. */ + executable, + + /* The inferior's entry frame. This is the first frame within the + inferior, this can be outside the main executable, e.g. for a + dynamically linked executable, this will be the first frame in the + dynamic linker. */ + inferior, +}; + +/* Test whether THIS_FRAME is inside the TYPE process entry point + function. */ + +static bool +inside_entry_func (const frame_info_ptr &this_frame, + entry_address_type type) +{ + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + CORE_ADDR frame_func_addr; + if (!get_frame_func_if_available (this_frame, &frame_func_addr)) + return false; + + switch (type) + { + case entry_address_type::executable: + return ep_info.exec_entry_address () == frame_func_addr; + + case entry_address_type::inferior: + return ep_info.inferior_entry_address () == frame_func_addr; + } + + gdb_assert_not_reached ("unknown entry address type: %d", ((int) type)); +} + +/* Debug routine to print a NULL frame being returned. */ + +static void +frame_debug_got_null_frame (const frame_info_ptr &this_frame, + const char *reason) +{ + if (frame_debug) + { + if (this_frame != NULL) + frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); + else + frame_debug_printf ("this_frame=nullptr -> %s", reason); + } +} + /* Get the previous raw frame, and check that it is not identical to same other frame frame already in the chain. If it is, there is most likely a stack cycle, so we discard it, and mark THIS_FRAME as @@ -2543,6 +2600,17 @@ get_prev_frame_always_1 (const frame_info_ptr &this_frame) frame_info_ptr get_prev_frame_always (const frame_info_ptr &this_frame) { + if (this_frame->level >= 0 + && get_frame_type (this_frame) == NORMAL_FRAME + && !user_set_backtrace_options.backtrace_past_entry + && inside_entry_func (this_frame, entry_address_type::inferior)) + { + this_frame->prev_p = true; + this_frame->stop_reason = UNWIND_OUTERMOST; + frame_debug_got_null_frame (this_frame, "inside inferior entry func"); + return nullptr; + } + frame_info_ptr prev_frame = NULL; try @@ -2631,21 +2699,6 @@ get_prev_frame_raw (const frame_info_ptr &this_frame) return frame_info_ptr (prev_frame); } -/* Debug routine to print a NULL frame being returned. */ - -static void -frame_debug_got_null_frame (const frame_info_ptr &this_frame, - const char *reason) -{ - if (frame_debug) - { - if (this_frame != NULL) - frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); - else - frame_debug_printf ("this_frame=nullptr -> %s", reason); - } -} - /* Is this (non-sentinel) frame in the "main"() function? */ static bool @@ -2694,19 +2747,6 @@ inside_main_func (const frame_info_ptr &this_frame) return sym_addr == get_frame_func (this_frame); } -/* Test whether THIS_FRAME is inside the process entry point function. */ - -static bool -inside_entry_func (const frame_info_ptr &this_frame) -{ - const program_space::entry_point_info &ep_info - = current_program_space->get_entry_point_info (); - - CORE_ADDR frame_func_addr = get_frame_func (this_frame); - return (ep_info.exec_entry_address () == frame_func_addr - || ep_info.inferior_entry_address () == frame_func_addr); -} - /* Return a structure containing various interesting information about the frame that called THIS_FRAME. Returns NULL if there is either no such frame or the frame fails any of a set of target-independent @@ -2788,11 +2828,10 @@ get_prev_frame (const frame_info_ptr &this_frame) 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)) + && inside_entry_func (this_frame, entry_address_type::executable)) { - frame_debug_got_null_frame (this_frame, "inside entry func"); - return NULL; + frame_debug_got_null_frame (this_frame, "inside executable entry func"); + return nullptr; } /* Assume that the only way to get a zero PC is through something 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 <http://www.gnu.org/licenses/>. */ + +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..2338f027160 --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp @@ -0,0 +1,162 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'until' in the outermost frame works as expected. + +require !use_gdb_stub + +standard_testfile + +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + { debug additional_flags=-static } ] } { + return +} + +# Use 'frame' 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 "frame" $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 testfile } { + clean_restart $testfile + + if { [gdb_starti_cmd] < 0 } { + untested starti + return + } + + # Consume up to the first prompt after 'starti' command. + gdb_test "" "" "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" -lbl { + -re "\r\n=> $::hex \[^\r\n\]+(?=\r\n)" { + set capture_address true + exp_continue + } + + -re "\r\n\\s+($::hex) \[^\r\n\]+(?=\r\n)" { + if { $capture_address } { + set until_address $expect_out(1,string) + set capture_address false + } + + exp_continue + } + + -re "\r\n$::gdb_prompt $" { + set found_address [expr { $until_address ne "" }] + if { !$found_address } { + unsupported "$gdb_test_name (no instruction found)" + return + } + pass $gdb_test_name + } + } + + # 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 $testfile + + with_test_prefix "static" { + run_test $use_stepi $past_entry $testfile_static + } + } +} -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv4 4/4] gdb: cache program space entry point information 2026-06-23 10:47 ` [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 preceding siblings ...) 2026-06-23 10:47 ` [PATCHv4 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-06-23 10:47 ` Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-23 10:47 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves After the previous two patches, every time that GDB unwinds a frame there are now two places where we check if the frame is an entry point frame, these are in get_prev_frame and get_prev_frame_always, look for the calls to `inside_entry_func`, which then calls program_space::get_entry_point_info. The calls to program_space::get_entry_point_info are not crazy expensive, but they are not free either, there are reads from target memory to read the auxv vector and the entry address offset, so on remote targets this could introduce a small delay, especially as this occurs twice per frame now. However, the information returned by program_space::get_entry_point_info is not expected to change from one call to the next. This information should be a property of the executable and libraries, so we really only need to figure it out once. This commit caches the entry point information within program_space, clearing it whenever an inferior starts or exits, or whenever the executable is updated. After clearing GDB will compute, and cache the updated information the next time it is needed, which will be whenever GDB needs to unwind a frame. It will be possible to observe this change by, for example, monitoring the remote target packets, but as far as the normal GDB output is concerned, there should be no user visible changes after this commit. --- gdb/progspace.c | 34 +++++++++++++++++++++++++++++++--- gdb/progspace.h | 9 ++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/gdb/progspace.c b/gdb/progspace.c index 26e16cae8d0..c1f3044f4c5 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -289,9 +289,12 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ -program_space::entry_point_info +const program_space::entry_point_info & program_space::get_entry_point_info () const { + if (m_entry_point_info.has_value ()) + return m_entry_point_info.value (); + std::optional<CORE_ADDR> exec_entry_address = this->exec_entry_point_address_if_available (); @@ -299,8 +302,9 @@ program_space::get_entry_point_info () const if (m_solib_ops != nullptr) inferior_entry_address = m_solib_ops->inferior_entry_point_address (); - return entry_point_info (std::move (inferior_entry_address), - std::move (exec_entry_address)); + m_entry_point_info.emplace (std::move (inferior_entry_address), + std::move (exec_entry_address)); + return m_entry_point_info.value (); } /* Implement the 'maint info entry-address' command. */ @@ -538,11 +542,35 @@ program_space::clear_solib_cache () deleted_solibs.clear (); } +/* Clear cached entry point information in the program space of INF. */ + +static void +clear_cached_entry_point_info_for_inferior (inferior *inf) +{ + inf->pspace->clear_cached_entry_point_info (); +} + +/* Clear cached entry point information in the program space PSPACE. */ + +static void +clear_cached_entry_point_info_for_pspace (program_space *pspace, + bool /* reload */) +{ + pspace->clear_cached_entry_point_info (); +} + /* See progspace.h. */ void initialize_progspace () { + gdb::observers::inferior_created.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::inferior_exit.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::executable_changed.attach + (clear_cached_entry_point_info_for_pspace, "program-space"); + add_cmd ("program-spaces", class_maintenance, maintenance_info_program_spaces_command, _("Info about currently known program spaces."), diff --git a/gdb/progspace.h b/gdb/progspace.h index d75379fe621..407d2595136 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -369,7 +369,11 @@ struct program_space /* Return information about the entry point in the main executable, and the entry point for the inferior, which might be different from the main executable. */ - entry_point_info get_entry_point_info () const; + const entry_point_info &get_entry_point_info () const; + + /* Clear any cached entry point information. */ + void clear_cached_entry_point_info () + { m_entry_point_info.reset (); } /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ @@ -462,6 +466,9 @@ struct program_space /* See `exec_filename`. */ gdb::unique_xmalloc_ptr<char> m_exec_filename; + + /* Cached entry point information. See get_entry_point_info. */ + mutable std::optional<entry_point_info> m_entry_point_info; }; /* The list of all program spaces. There's always at least one. */ -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame 2026-06-23 10:47 ` [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (3 preceding siblings ...) 2026-06-23 10:47 ` [PATCHv4 4/4] gdb: cache program space entry point information Andrew Burgess @ 2026-06-25 15:26 ` Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess ` (4 more replies) 4 siblings, 5 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-25 15:26 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves In v5: - Update to test in patch #2 to allow the test to work on machines that build PIE by default. I actually extended the test to build both PIE and non-PIE executables, and test with both. In v4: - Rebase to current HEAD of master. - In patch #2, added a supports_process_entry_point proc as suggested by Pedro, and make use of this in the new test. Updates to address review comments from v2 series: - Doc updates based on Eli's feedback. - Patch #2: + Avoid swallowing Ctrl-C in try/catch. That whole try/catch block has been removed in v3, so problem sorted! + Rewrote entry address calculation as suggested by Pedro, now pulls all required information from the inferior. + Use 'objdump -f' rather than readelf to get the entry address in the test. + Added a helper proc for is_svr4_target, which includes a larger list of targets beyond just Linux. This is then used in the test. + Test updated to use -lbl flag in gdb_test_multiple. - Patch #3: + Use 'frame' rather than 'bt 1' + Loosen the test regexp after the 'starti' command, this should now work on Windows targets, or any target with multiple threads at startup. + Test updated to use -lbl in gdb_test_multiple. - In patch #3 the entry frame detection is moved from `frame_unwind_caller_frame` into `get_prev_frame_always`, I hope this will address Guinevere's concerns about having to add the additional entry frame detection in multiple locations. Guinevere also had some concerns about the naming of existing functions (i.e. not things added by me in this commit), I don't plan to make any naming changes to the get_prev_frame* functions in this series. - Patch #4 is new, this adds some caching to avoid repeatedly looking up the entry address. The new approach requires reading target memory, which can be expensive, especially on remote targets. --- Andrew Burgess (4): gdb: rename program_space::entry_point_address* functions gdb: introduce program_space::get_entry_point_info function gdb: allow 'until' to work in outermost frame gdb: cache program space entry point information gdb/NEWS | 7 + gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/doc/gdb.texinfo | 22 ++ gdb/frame.c | 105 ++++++--- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 94 +++++++- gdb/progspace.h | 67 +++++- gdb/solib-frv.c | 2 +- gdb/solib-svr4.c | 85 +++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 215 ++++++++++++++++++ gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 ++ .../gdb.base/until-in-entry-frame.exp | 162 +++++++++++++ gdb/testsuite/lib/gdb.exp | 39 ++++ 17 files changed, 796 insertions(+), 47 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.c create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.exp base-commit: 5e59c8f6f695ea9acc6c07e5a555a95dd8d1a1f8 -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv5 1/4] gdb: rename program_space::entry_point_address* functions 2026-06-25 15:26 ` [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-06-25 15:26 ` Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess ` (3 subsequent siblings) 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-25 15:26 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves Rename program_space::entry_point_address to program_space::exec_entry_point_address and program_space::entry_point_address_query to program_space::exec_entry_point_address_if_available. There are two aspects to this renaming. First I replace 'query' with 'if_available' in one of the functions. I feel this better describes the function, and also is inline with how other, similar, functions are named in GDB. The second part of the renaming is to add the 'exec_' prefix to the front of both function names. When a dynamically linked inferior is started the first instruction executed is actually within the run-time linker, not within the main executable, so it could be argued that the actual entry address for the inferior is not the entry address of the main executable. However, there is an equally valid argument that the entry address of the executable is also something worth finding. The existing entry address within the inferior is used for a number of tasks in GDB, for example displaced stepping (arch-utils.c), inferior function calls (arc-tdep.c and infcall.c), and for detecting the "entry" frame. This last one, the entry frame detection is interesting. In a dynamically linked executable it could be argued that there are two entry frames. The very first frame that is executed in the inferior, this is where 'starti' stops the inferior. And then the very first frame within the main executable. I think that both of these are valid. Currently, as we can only find the entry address within the main executable, we can only identify the first frame of the main executable. And this can cause some problems, consider: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) Here frames #1 to #3 are all bogus, created by GDB based on whatever values happen to be in the registers when the inferior starts. In a later commit I'd like to fix this problem, however, I would prefer that the other users of program_space::entry_point_address continue to use the address within the main executable. So, in order to keep the distinction between the two different types of entry point, I'm renaming the existing functions with the 'exec_' prefix. There should be no user visible changes after this commit. Approved-By: Pedro Alves <pedro@palves.net> --- gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/frame.c | 4 ++-- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 6 +++--- gdb/progspace.h | 12 ++++++------ gdb/solib-frv.c | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gdb/arc-tdep.c b/gdb/arc-tdep.c index 4936a5c8fbb..7ed83c4cae6 100644 --- a/gdb/arc-tdep.c +++ b/gdb/arc-tdep.c @@ -860,7 +860,7 @@ arc_push_dummy_code (struct gdbarch *gdbarch, CORE_ADDR sp, CORE_ADDR funaddr, struct regcache *regcache) { *real_pc = funaddr; - *bp_addr = current_program_space->entry_point_address (); + *bp_addr = current_program_space->exec_entry_point_address (); return sp; } diff --git a/gdb/arch-utils.c b/gdb/arch-utils.c index e959788bd3b..88047731ce6 100644 --- a/gdb/arch-utils.c +++ b/gdb/arch-utils.c @@ -57,7 +57,7 @@ displaced_step_at_entry_point (struct gdbarch *gdbarch) CORE_ADDR addr; int bp_len; - addr = current_program_space->entry_point_address (); + addr = current_program_space->exec_entry_point_address (); /* Inferior calls also use the entry point as a breakpoint location. We don't want displaced stepping to interfere with those diff --git a/gdb/frame.c b/gdb/frame.c index 4137e1d5edd..8532664cae2 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2695,7 +2695,7 @@ static bool inside_entry_func (const frame_info_ptr &this_frame) { std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) return false; @@ -2771,7 +2771,7 @@ get_prev_frame (const frame_info_ptr &this_frame) added to work around that (now fixed) case. */ /* NOTE: cagney/2003-07-15: danielj (if I'm reading it right) suggested having the inside_entry_func test use the - inside_main_func() msymbol trick (along with entry_point_address() + inside_main_func() msymbol trick (along with exec_entry_point_address() I guess) to determine the address range of the start function. That should provide a far better stopper than the current heuristics. */ diff --git a/gdb/infcall.c b/gdb/infcall.c index e6b24ff5310..6d26841ede3 100644 --- a/gdb/infcall.c +++ b/gdb/infcall.c @@ -1300,7 +1300,7 @@ call_function_by_hand_dummy (struct value *function, CORE_ADDR dummy_addr; real_pc = funaddr; - dummy_addr = current_program_space->entry_point_address (); + dummy_addr = current_program_space->exec_entry_point_address (); /* A call dummy always consists of just a single breakpoint, so its address is the same as the address of the dummy. diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c index a7381677498..d03e4768792 100644 --- a/gdb/linux-tdep.c +++ b/gdb/linux-tdep.c @@ -2952,7 +2952,7 @@ linux_displaced_step_location (struct gdbarch *gdbarch) /* Determine entry point from target auxiliary vector. This avoids the need for symbols. Also, when debugging a stand-alone SPU - executable, entry_point_address () will point to an SPU + executable, exec_entry_point_address () will point to an SPU local-store address and is thus not usable as displaced stepping location. The auxiliary vector gets us the PowerPC-side entry point address instead. */ diff --git a/gdb/progspace.c b/gdb/progspace.c index 1407b058dfd..0e8516f4640 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -263,7 +263,7 @@ program_space::empty () /* See progspace.h. */ std::optional<CORE_ADDR> -program_space::entry_point_address_query () const +program_space::exec_entry_point_address_if_available () const { objfile *objf = symfile_object_file; if (objf == NULL || !objf->per_bfd->ei.entry_point_p) @@ -276,9 +276,9 @@ program_space::entry_point_address_query () const /* See progspace.h. */ CORE_ADDR -program_space::entry_point_address () const +program_space::exec_entry_point_address () const { - std::optional<CORE_ADDR> retval = entry_point_address_query (); + std::optional<CORE_ADDR> retval = exec_entry_point_address_if_available (); if (!retval.has_value ()) error (_("Entry point address is not known.")); diff --git a/gdb/progspace.h b/gdb/progspace.h index e9261ff8590..1ae1e42f3bb 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,13 +323,13 @@ struct program_space return m_target_sections; } - /* If there is a valid and known entry point in this program space, - return it. Otherwise return an empty optional. */ - std::optional<CORE_ADDR> entry_point_address_query () const; + /* If there is a valid and known entry point in the main executable of + this program space, return it. Otherwise return an empty optional. */ + std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; - /* Get the entry point address in this program space. Call error if - it is not known. */ - CORE_ADDR entry_point_address () const; + /* Get the entry point address for the main executable in this program + space. Call error if it is not known. */ + CORE_ADDR exec_entry_point_address () const; /* Return true if any objfile of this program space has partial symbols. */ diff --git a/gdb/solib-frv.c b/gdb/solib-frv.c index 4f0aac31e73..69b882e534a 100644 --- a/gdb/solib-frv.c +++ b/gdb/solib-frv.c @@ -689,7 +689,7 @@ enable_break (void) } std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) { solib_debug_printf ("Symbol file has no entry point."); -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv5 2/4] gdb: introduce program_space::get_entry_point_info function 2026-06-25 15:26 ` [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess @ 2026-06-25 15:26 ` Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 subsequent siblings) 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-25 15:26 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves, Eli Zaretskii I noticed that when debugging a dynamically linked executable, if I started the inferior with 'starti' then used 'bt' I would see some bogus frames: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) This surprised me as 'backtrace past-entry' was off: (gdb) show backtrace past-entry Whether backtraces should continue past the entry point of a program is off. I was expecting GDB to stop the backtrace at the inferior's entry address. Frame unwinding starts in get_prev_frame, and in here we find this block: 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)) { frame_debug_got_null_frame (this_frame, "inside entry func"); return NULL; } Which uses inside_entry_func to terminate the backtrace when we reach the entry frame. The inside_entry_func function calls current_program_space->exec_entry_point_address_if_available and uses the result to figure out if we are in the entry frame. And here the problem becomes obvious, we are only checking if we are in the "entry frame" for the main executable, not for the inferior as a whole. And indeed, if I compile the same test program as a static binary, where there will be no run-time linker, and the entry point of the executable is the entry point for the inferior, then the 'bt' problem I saw above goes away. This suggests, I think, that we need to track two different entry addresses, the entry address for the executable file, and the entry address for the entire inferior. Then, for dynamically linked executables, these two addresses can be different, the former will still be the same address within the main executable file, while the latter will be the address of the entry point within the run-time linker. If we had this information then we could extend inside_entry_func to check both addresses, and the 'bt' problem seen above will be resolved. To make this information available I added a new solib_ops method, solib_ops::inferior_entry_point_address, for svr4 targets this figures out if the main executable is dynamically linked, and if it is, uses the AT_BASE auxv entry and the entry address pulled from the ELF header to compute the inferior entry address. I then added program_space::get_entry_point_info, which returns a struct containing the two entry point addresses, one comes from the new solib_ops method, and one comes from the existing method program_space::exec_entry_point_address_if_available. With the infrastructure in place I can then update inside_entry_func to check against both entry addresses. To aid in debugging GDB, I added a new maintenance command: maintenance info entry-address which just calls program_space::get_entry_point_info and then prints the two addresses. I left this as a maintenance command as I don't see much user utility in this right now, but it made it easier for me to see what GDB was doing, so I left the command in this commit. Reviewed-By: Eli Zaretskii <eliz@gnu.org> --- gdb/NEWS | 7 + gdb/doc/gdb.texinfo | 22 +++ gdb/frame.c | 10 +- gdb/progspace.c | 60 ++++++ gdb/progspace.h | 48 +++++ gdb/solib-svr4.c | 85 ++++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 215 +++++++++++++++++++++ gdb/testsuite/lib/gdb.exp | 39 ++++ 10 files changed, 496 insertions(+), 5 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp diff --git a/gdb/NEWS b/gdb/NEWS index d5214a98a57..4290f835845 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -146,6 +146,13 @@ disable skip These are new aliases for 'skip delete', 'skip enable', and 'skip disable' respectively. +maint info entry-address + Display the inferior and main executable entry addresses for the + current inferior. The inferior entry address is the address of the + first instruction in the inferior that was executed. The main + executable entry address is the address of the first instruction in + the main executable that will be executed. + * MI changes ** The "-trace-save" command no longer supports the "-ctf" flag. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index a698b2b8451..4c0c4709a23 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -43212,6 +43212,28 @@ Maintenance Commands Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M @end smallexample + +@kindex maint info entry-address +@item maint info entry-address +Display the entry addresses for the currently selected inferior. Two +addresses are displayed, the inferior entry address and the executable +entry address. + +Neither address is the address of @code{main}. When a program is +started, low-level startup code (typically a function called +@code{_start}) runs before @code{main} is called. + +The executable entry address is the address of this startup code +within the main executable (@pxref{Files, ,Commands to Specify +Files}). + +The inferior entry address is the address of the very first +instruction executed when the inferior is started. For statically +linked programs this is the same as the executable entry address. For +dynamically linked programs the run-time linker must execute first in +order to load shared libraries, so the inferior entry address will be +an address within the run-time linker rather than within the main +executable. @end table The following command is useful for non-interactive invocations of diff --git a/gdb/frame.c b/gdb/frame.c index 8532664cae2..be9a3aa58c0 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2694,12 +2694,12 @@ inside_main_func (const frame_info_ptr &this_frame) static bool inside_entry_func (const frame_info_ptr &this_frame) { - std::optional<CORE_ADDR> entry_point - = current_program_space->exec_entry_point_address_if_available (); - if (!entry_point.has_value ()) - return false; + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); - return get_frame_func (this_frame) == *entry_point; + CORE_ADDR frame_func_addr = get_frame_func (this_frame); + return (ep_info.exec_entry_address () == frame_func_addr + || ep_info.inferior_entry_address () == frame_func_addr); } /* Return a structure containing various interesting information about diff --git a/gdb/progspace.c b/gdb/progspace.c index 0e8516f4640..26e16cae8d0 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -26,6 +26,7 @@ #include <algorithm> #include "cli/cli-style.h" #include "observable.h" +#include "arch-utils.h" /* The last program space number assigned. */ static int last_program_space_num = 0; @@ -288,6 +289,55 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ +program_space::entry_point_info +program_space::get_entry_point_info () const +{ + std::optional<CORE_ADDR> exec_entry_address + = this->exec_entry_point_address_if_available (); + + std::optional<CORE_ADDR> inferior_entry_address; + if (m_solib_ops != nullptr) + inferior_entry_address = m_solib_ops->inferior_entry_point_address (); + + return entry_point_info (std::move (inferior_entry_address), + std::move (exec_entry_address)); +} + +/* Implement the 'maint info entry-address' command. */ + +static void +maintenance_info_entry_address (const char *args, int from_tty) +{ + if (args != nullptr && *args != '\0') + error (_("unknown argument: %s"), args); + + struct gdbarch *gdbarch = get_current_arch (); + + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + /* Display a single entry address ADDR with TITLE as a description. */ + auto display_entry_address + = [&gdbarch] (const char *title, + const std::optional<CORE_ADDR> &addr) -> void + { + gdb_puts (title); + if (addr.has_value ()) + fputs_styled (paddress (gdbarch, addr.value ()), + address_style.style (), gdb_stdout); + else + fputs_styled ("<unknown>", metadata_style.style (), gdb_stdout); + gdb_puts ("\n"); + }; + + display_entry_address (_("Inferior entry address: "), + ep_info.inferior_entry_address ()); + display_entry_address (_("Executable entry address: "), + ep_info.exec_entry_address ()); +} + +/* See progspace.h. */ + bool program_space::has_partial_symbols () { @@ -498,6 +548,16 @@ initialize_progspace () _("Info about currently known program spaces."), &maintenanceinfolist); + add_cmd ("entry-address", class_maintenance, + maintenance_info_entry_address, + _("Information about the current inferior's entry addresses.\n\ +Display the address of the first instruction executed within the\n\ +inferior, and the first instruction executed within the main executable.\n\ +The entry address of the whole inferior will be <unknown> prior to the\n\ +inferior starting, and either or both addresses can be <unknown> if\n\ +GDB is unable to find the required information."), + &maintenanceinfolist); + /* There's always one program space. Note that this function isn't an automatic _initialize_foo function, since other _initialize_foo routines may need to install their per-pspace diff --git a/gdb/progspace.h b/gdb/progspace.h index 1ae1e42f3bb..d75379fe621 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,6 +323,54 @@ struct program_space return m_target_sections; } + /* Class for tracking two possible entry points that an inferior might + have, the entry point for the entire inferior, and the entry point + within the main executable. */ + struct entry_point_info + { + explicit entry_point_info (std::optional<CORE_ADDR> inferior_entry_address, + std::optional<CORE_ADDR> exec_entry_address) + : m_inferior_entry_address (std::move (inferior_entry_address)), + m_exec_entry_address (std::move (exec_entry_address)) + { /* Nothing. */ } + + DISABLE_COPY_AND_ASSIGN (entry_point_info); + + /* The entry address within the inferior as a whole. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &inferior_entry_address () const + { + return m_inferior_entry_address; + } + + /* The entry address within the main executable. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &exec_entry_address () const + { + return m_exec_entry_address; + } + + private: + /* The entry address within the inferior. This can be outside of the + main executable, e.g. for dynamically linked executables this could + be the entry address for the run-time linker. For statically linked + executables this will be the entry address of the main executable. + This can be empty if GDB doesn't know how to figure out the correct + entry address for any reason. */ + std::optional<CORE_ADDR> m_inferior_entry_address; + + /* The entry address within the main executable. This can be empty if + the main executable is not set yet, or GDB doesn't have an objfile + associated with the main executable, e.g. in some attach, or remote + debug cases. */ + std::optional<CORE_ADDR> m_exec_entry_address; + }; + + /* Return information about the entry point in the main executable, and + the entry point for the inferior, which might be different from the + main executable. */ + entry_point_info get_entry_point_info () const; + /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c index 8e3de4d3ea1..12f3e783744 100644 --- a/gdb/solib-svr4.c +++ b/gdb/solib-svr4.c @@ -3777,6 +3777,91 @@ svr4_solib_ops::get_solibs_in_ns (int nsid) const return ns_solibs; } +/* See solib.h. */ + +std::optional<CORE_ADDR> +svr4_solib_ops::inferior_entry_point_address () const +{ + std::optional<gdb::byte_vector> interp_name_holder + = svr4_find_program_interpreter (); + + /* No interpreter means this is a static executable. Ask the + program_space for the entry address within the main executable. */ + if (!interp_name_holder.has_value ()) + return m_pspace->exec_entry_point_address_if_available (); + + /* For a dynamically linked executable the inferior's true entry point + is the entry point of the dynamic linker. We find this using the + AT_BASE auxiliary vector entry, which gives the dynamic linker's + load address, combined with e_entry address pulled from the + inferior. We assume that the ELF header can be read from AT_BASE. */ + CORE_ADDR at_base_addr; + if (target_auxv_search (AT_BASE, &at_base_addr) <= 0) + return {}; + + /* Determine ELF architecture type. Use the size of a program header + entry to determine which ELF header we can expect to find. */ + size_t e_entry_offset = 0; + size_t e_entry_size = 0; + CORE_ADDR at_phent; + if (target_auxv_search (AT_PHENT, &at_phent) <= 0) + return {}; + if (at_phent == sizeof (Elf32_External_Phdr)) + { + e_entry_offset = offsetof (Elf32_External_Ehdr, e_entry); + e_entry_size = 4; + } + else if (at_phent == sizeof (Elf64_External_Phdr)) + { + e_entry_offset = offsetof (Elf64_External_Ehdr, e_entry); + e_entry_size = 8; + } + else + return {}; + + /* Architecture of the current inferior. */ + gdbarch *gdbarch = current_inferior ()->arch (); + + /* Read the entry address from the ELF header at AT_BASE_ADDR. */ + CORE_ADDR e_entry; + gdb_byte buffer[sizeof (CORE_ADDR)]; + if (target_read_memory (at_base_addr + e_entry_offset, buffer, e_entry_size)) + return {}; + bfd_endian byte_order = gdbarch_byte_order (gdbarch); + e_entry = extract_unsigned_integer (buffer, e_entry_size, byte_order); + + /* Ensure AT_BASE_ADDR has proper sign in its possible upper bits so + that `+ at_base_addr' will overflow CORE_ADDR width not creating + invalid addresses like 0x101234567 for 32bit inferiors on 64bit + GDB. */ + int addr_bit = gdbarch_addr_bit (gdbarch); + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) + { + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; + + gdb_assert (at_base_addr < space_size); + + /* E_ENTRY exceeding SPACE_SIZE would be for prelinked + 64bit ld.so with 32bit executable, it should not happen. */ + if (e_entry < space_size + && e_entry + at_base_addr >= space_size) + at_base_addr -= space_size; + } + + /* Compute the entry address. */ + e_entry = at_base_addr + e_entry; + + /* Handle the case where E_ENTRY is a function descriptor. Also remove + any non-address (e.g. tag) bits from E_ENTRY. */ + e_entry + = gdbarch_convert_from_func_ptr_addr (gdbarch, e_entry, + current_inferior ()->top_target ()); + e_entry + = gdbarch_addr_bits_remove (gdbarch, e_entry); + + return e_entry; +} + INIT_GDB_FILE (svr4_solib) { gdb::observers::free_objfile.attach (svr4_free_objfile_observer, diff --git a/gdb/solib-svr4.h b/gdb/solib-svr4.h index 3078a092778..f76f350d951 100644 --- a/gdb/solib-svr4.h +++ b/gdb/solib-svr4.h @@ -115,6 +115,7 @@ struct svr4_solib_ops : public solib_ops void iterate_over_objfiles_in_search_order (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const override; + std::optional<CORE_ADDR> inferior_entry_point_address () const override; /* Return the appropriate link map offsets table for the architecture. */ virtual link_map_offsets *fetch_link_map_offsets () const = 0; diff --git a/gdb/solib.h b/gdb/solib.h index 9e6c3f7346e..7ed00c63471 100644 --- a/gdb/solib.h +++ b/gdb/solib.h @@ -288,6 +288,20 @@ struct solib_ops (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const; + /* Return the inferior entry point address. This isn't always the entry + point of the main executable, but will be the actual entry point where + the inferior starts (or started) executing. + + For example on SVR4 targets, for dynamically linked executables, this + will be the entry address of the dynamic linker. But for statically + linked executables, this will be the entry point of the main + executable. + + If the entry address cannot be found then an empty optional is + returned. */ + virtual std::optional<CORE_ADDR> inferior_entry_point_address () const + { return {}; } + protected: /* The program space for which this solib_ops was created. */ program_space *m_pspace; diff --git a/gdb/testsuite/gdb.base/bt-after-starti.exp b/gdb/testsuite/gdb.base/bt-after-starti.exp new file mode 100644 index 00000000000..c2a78a05cf4 --- /dev/null +++ b/gdb/testsuite/gdb.base/bt-after-starti.exp @@ -0,0 +1,215 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'bt' from the very first instruction (where 'starti' +# stops GDB) doesn't display any unexpected frames. + +require !use_gdb_stub + +standard_testfile main.c + +# Build default executable, hopefully dynamically linked, but we check +# this below. +foreach pie_mode { pie nopie } { + if { [build_executable "failed to build" $testfile-$pie_mode $srcfile \ + [list debug $pie_mode]] } { + return + } +} + +# Build a statically linked executable. +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + {debug additional_flags=-static}] } { + return +} + +# Run the 'maint info entry-address' command. Expect INF_RE to match +# the inferior entry address and EXE_RE to match the executable entry +# address. TESTNAME is used as the test name. +proc check_maint_info_entry_addr { inf_re exe_re testname } { + gdb_test "maint info entry-address" \ + [multi_line \ + "Inferior entry address: ${inf_re}" \ + "Executable entry address: ${exe_re}"] \ + $testname +} + +# Use 'info proc mappings' to find the address of the first mapping of +# TESTFILE, return this address. If the address cannot be found then +# the empty string is returned. +# +# This emits a PASS or FAIL depending on whether the address is found +# or not. +proc get_mapped_file_base_addr { testfile } { + set base_addr "" + gdb_test_multiple "info proc mappings" "" -lbl { + -re "\r\n($::hex) \[^\r\n\]+/${testfile}\\s*(?=\r\n)" { + if { $base_addr eq "" } { + set base_addr $expect_out(1,string) + } + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $base_addr ne "" } \ + "find base address of $testfile" + } + } + return $base_addr +} + +# Use 'objdump' to extract the entry point address from TESTFILE. +proc get_exec_entry_address { testfile } { + set objdump_program [gdb_find_objdump] + set command "exec $objdump_program -f [standard_output_file $testfile]" + verbose -log "command is $command" + set result [catch {{*}$command} output] + verbose -log "result is $result" + verbose -log "output is $output" + + set testname "get_exec_entry_address" + + if {$result != 0} { + fail $testname + return "UNKNOWN" + } + + if {![regexp "\nstart address ($::hex)\n" $output trash addr]} { + fail "$testname (no entry point address header)" + return "UNKNOWN" + } + + return 0x[format %x $addr] +} + +# Start TESTFILE using 'starti'. Check if the 'backtrace' command +# prints any additional frames; we don't expect any. Create a core +# file from the running inferior. Restart GDB and load the core file, +# check that GDB can still figure out the entry addresses. +# +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked +# executable, otherwise TESTFILE is statically linked. +proc run_test { testfile is_dynamic } { + clean_restart $testfile + + set exe_entry_addr [get_exec_entry_address $testfile] + + # The inferior entry address is always unknown before starting the + # inferior because GDB has no solib_ops yet, and it is only + # through that that we figure out the inferior entry address. + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ + "check 'maint info entry-address' before inferior starts" + + # Start inferior with 'starti' and then wait for a prompt. + gdb_starti_cmd + gdb_test "" ".*" "prompt after starti" + + # Not every target supports finding the process entry point. + # Targets that don't support this report '<unknown>', we check for + # this too on those targets. + if {[supports_process_entry_point]} { + set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN" \ + "get current program counter"] + + if { !$is_dynamic } { + # In static binaries, the first address should be the first + # address in the executable. + gdb_assert { $pc == $exe_entry_addr } \ + "stopped at executable entry address" + } + + set inf_entry_addr $pc + } else { + set inf_entry_addr "<unknown>" + } + + # If using PIE then the executable entry address we looked up from + # the objfile will have been relocated. Attempt to compute the + # relocated address and updated EXE_ENTRY_ADDR, otherwise set + # EXE_ENTRY_ADDR to a generic pattern that matches any address. + if {[exec_is_pie [standard_output_file $testfile]]} { + set base_addr [get_mapped_file_base_addr $testfile] + if { $base_addr ne "" } { + set exe_entry_addr \ + 0x[format %x [expr {$base_addr + $exe_entry_addr}]] + } else { + # We failed to find the base address. The + # get_mapped_file_base_addr will have already emitted a + # FAIL. Set the expected pattern to something generic and + # push on, we can still test a bunch of things, we just + # don't know the address is correct. + set exe_entry_addr $::hex + } + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after starti" + + # Allow backtrace past the entry frame, count how many frames GDB + # finds. If on this target there are no additional frames then + # the following test isn't going to tell us much, so skip it. + gdb_test_no_output "set backtrace past-entry on" + set frame_count 0 + gdb_test_multiple "bt" "count possible frames" -lbl { + -re "\r\n#($::decimal)\\s+\[^\r\n\]+(?=\r\n)" { + incr frame_count + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $frame_count > 0 } $gdb_test_name + } + } + + if { $frame_count > 1 } { + # Turn off backtrace past the entry frame. Use the 'bt' command, + # and check we see only a single frame. + gdb_test_no_output "set backtrace past-entry off" + gdb_test "bt" "^#0 \[^\r\n\]+" \ + "single frame when backtrace past-entry is off" + } else { + unsupported "no additional frames that GDB can hide" + } + + # Create a core file. Restart GDB. Load the core file. Check + # that 'maint info entry-address' gives the correct output. + set corefile [standard_output_file $testfile].core + if {![gdb_gcore_cmd $corefile "generate corefile"]} { + return + } + + clean_restart $testfile + + if {[gdb_core_cmd $corefile "load corefile"] != 1} { + return + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after loading core file" +} + +with_test_prefix "dynamic" { + foreach_with_prefix pie_mode { pie nopie } { + set dyln_name [section_get $binfile-$pie_mode .interp] + if { $dyln_name eq "" } { + unsupported "couldn't find dynamic linker name" + } else { + run_test $testfile-$pie_mode true + } + } +} + +with_test_prefix "static" { + run_test $testfile_static false +} diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp index d8619ded236..8be2e12ab73 100644 --- a/gdb/testsuite/lib/gdb.exp +++ b/gdb/testsuite/lib/gdb.exp @@ -4226,6 +4226,45 @@ proc is_aarch64_target {} { return [expr {![is_aarch32_target]}] } +# Return true for svr4 targets, otherwise, return false. + +proc is_svr4_target {} { + return [expr {[istarget *-linux*] || [istarget *-freebsd*] + || [istarget *-netbsd*] || [istarget *-openbsd*] + || [istarget *-solaris*] || [istarget *-gnu]}] +} + +# Return false for targets that don't support finding the whole +# process entry address, otherwise, return true. + +proc supports_process_entry_point {} { + # SVR4 targets support finding the entry point. This is done + # within GDB so will work even for remote targets. + if {[is_svr4_target]} { + return true + } + + # Windows and Darwin don't currently support this. + if {[istarget *-*-mingw*] + || [istarget *-*-cygwin*] + || [istarget *-*-pe*] + || [istarget *-*-darwin*]} { + return false + } + + # For remote targets there is no RSP packet to retrieve the entry + # point, so this won't work unless the solib code can handle this + # within GDB, see the svr4 check above. + if {[gdb_protocol_is_remote]} { + return false + } + + # Assume everything else supports this by default. If a test + # fails because we get here then either fix GDB to support this + # feature, or add a new deny list entry above. + return true +} + # Return 1 if displaced stepping is supported on target, otherwise, return 0. proc support_displaced_stepping {} { -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv5 3/4] gdb: allow 'until' to work in outermost frame 2026-06-25 15:26 ` [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-06-25 15:26 ` Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 4/4] gdb: cache program space entry point information Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-25 15:26 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves 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, if the until breakpoint was not hit. 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, where we call inside_entry_func. This checks if a frame is one of the two possible entry frames, the inferior entry frame or the executable entry frame. See the previous commit for more details. The important thing is that the inferior entry frame is the absolute outer frame, the very first frame that the inferior executed when starting, while the executable entry frame is just the first frame within the main executable. There are a number of user configurable filters in get_prev_frame, the backtrace past-main filter, the backtrace frame limit filter, and the backtrace past-entry filter that we are discussing here. When creating the caller frame breakpoint, the 'until' command doesn't use get_prev_frame, it uses get_prev_frame_always. This is so that the user configurable filters don't prevent the caller frame breakpoint from being created. But this means that when creating the breakpoint, we skip the entry frame check. 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 split the inside_entry_func handling in to two parts. In get_prev_frame we will retain the check for the executable entry frame. In theory it is possible that there are frames between the executable entry frame and the inferior entry frame. These are the frames the 'backtrace past-entry' can hide or reveal (if the frames can be discovered). The check for the inferior entry frame I propose moving into get_prev_frame_always. I think this is a better place for it because any frames before the inferior entry frame are almost certainly bogus. As such we want to elide those frames even for "inner" use cases, like the caller frame breakpoint of the 'until' command. 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 | 103 +++++++---- gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 162 ++++++++++++++++++ 3 files changed, 255 insertions(+), 32 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 be9a3aa58c0..a87d3c91831 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2232,6 +2232,63 @@ frame_register_unwind_location (const frame_info_ptr &initial_this_frame, } } +/* When checking if a frame is an entry frame, there are two different + entry frames to consider. This enum is used to choose between them. */ + +enum class entry_address_type +{ + /* The executable's entry frame. This is the first frame within the main + executable. */ + executable, + + /* The inferior's entry frame. This is the first frame within the + inferior, this can be outside the main executable, e.g. for a + dynamically linked executable, this will be the first frame in the + dynamic linker. */ + inferior, +}; + +/* Test whether THIS_FRAME is inside the TYPE process entry point + function. */ + +static bool +inside_entry_func (const frame_info_ptr &this_frame, + entry_address_type type) +{ + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + CORE_ADDR frame_func_addr; + if (!get_frame_func_if_available (this_frame, &frame_func_addr)) + return false; + + switch (type) + { + case entry_address_type::executable: + return ep_info.exec_entry_address () == frame_func_addr; + + case entry_address_type::inferior: + return ep_info.inferior_entry_address () == frame_func_addr; + } + + gdb_assert_not_reached ("unknown entry address type: %d", ((int) type)); +} + +/* Debug routine to print a NULL frame being returned. */ + +static void +frame_debug_got_null_frame (const frame_info_ptr &this_frame, + const char *reason) +{ + if (frame_debug) + { + if (this_frame != NULL) + frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); + else + frame_debug_printf ("this_frame=nullptr -> %s", reason); + } +} + /* Get the previous raw frame, and check that it is not identical to same other frame frame already in the chain. If it is, there is most likely a stack cycle, so we discard it, and mark THIS_FRAME as @@ -2538,6 +2595,17 @@ get_prev_frame_always_1 (const frame_info_ptr &this_frame) frame_info_ptr get_prev_frame_always (const frame_info_ptr &this_frame) { + if (this_frame->level >= 0 + && get_frame_type (this_frame) == NORMAL_FRAME + && !user_set_backtrace_options.backtrace_past_entry + && inside_entry_func (this_frame, entry_address_type::inferior)) + { + this_frame->prev_p = true; + this_frame->stop_reason = UNWIND_OUTERMOST; + frame_debug_got_null_frame (this_frame, "inside inferior entry func"); + return nullptr; + } + frame_info_ptr prev_frame = NULL; try @@ -2626,21 +2694,6 @@ get_prev_frame_raw (const frame_info_ptr &this_frame) return frame_info_ptr (prev_frame); } -/* Debug routine to print a NULL frame being returned. */ - -static void -frame_debug_got_null_frame (const frame_info_ptr &this_frame, - const char *reason) -{ - if (frame_debug) - { - if (this_frame != NULL) - frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); - else - frame_debug_printf ("this_frame=nullptr -> %s", reason); - } -} - /* Is this (non-sentinel) frame in the "main"() function? */ static bool @@ -2689,19 +2742,6 @@ inside_main_func (const frame_info_ptr &this_frame) return sym_addr == get_frame_func (this_frame); } -/* Test whether THIS_FRAME is inside the process entry point function. */ - -static bool -inside_entry_func (const frame_info_ptr &this_frame) -{ - const program_space::entry_point_info &ep_info - = current_program_space->get_entry_point_info (); - - CORE_ADDR frame_func_addr = get_frame_func (this_frame); - return (ep_info.exec_entry_address () == frame_func_addr - || ep_info.inferior_entry_address () == frame_func_addr); -} - /* Return a structure containing various interesting information about the frame that called THIS_FRAME. Returns NULL if there is either no such frame or the frame fails any of a set of target-independent @@ -2783,11 +2823,10 @@ get_prev_frame (const frame_info_ptr &this_frame) 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)) + && inside_entry_func (this_frame, entry_address_type::executable)) { - frame_debug_got_null_frame (this_frame, "inside entry func"); - return NULL; + frame_debug_got_null_frame (this_frame, "inside executable entry func"); + return nullptr; } /* Assume that the only way to get a zero PC is through something 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 <http://www.gnu.org/licenses/>. */ + +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..2338f027160 --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp @@ -0,0 +1,162 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'until' in the outermost frame works as expected. + +require !use_gdb_stub + +standard_testfile + +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + { debug additional_flags=-static } ] } { + return +} + +# Use 'frame' 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 "frame" $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 testfile } { + clean_restart $testfile + + if { [gdb_starti_cmd] < 0 } { + untested starti + return + } + + # Consume up to the first prompt after 'starti' command. + gdb_test "" "" "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" -lbl { + -re "\r\n=> $::hex \[^\r\n\]+(?=\r\n)" { + set capture_address true + exp_continue + } + + -re "\r\n\\s+($::hex) \[^\r\n\]+(?=\r\n)" { + if { $capture_address } { + set until_address $expect_out(1,string) + set capture_address false + } + + exp_continue + } + + -re "\r\n$::gdb_prompt $" { + set found_address [expr { $until_address ne "" }] + if { !$found_address } { + unsupported "$gdb_test_name (no instruction found)" + return + } + pass $gdb_test_name + } + } + + # 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 $testfile + + with_test_prefix "static" { + run_test $use_stepi $past_entry $testfile_static + } + } +} -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv5 4/4] gdb: cache program space entry point information 2026-06-25 15:26 ` [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 preceding siblings ...) 2026-06-25 15:26 ` [PATCHv5 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-06-25 15:26 ` Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-06-25 15:26 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves After the previous two patches, every time that GDB unwinds a frame there are now two places where we check if the frame is an entry point frame, these are in get_prev_frame and get_prev_frame_always, look for the calls to `inside_entry_func`, which then calls program_space::get_entry_point_info. The calls to program_space::get_entry_point_info are not crazy expensive, but they are not free either, there are reads from target memory to read the auxv vector and the entry address offset, so on remote targets this could introduce a small delay, especially as this occurs twice per frame now. However, the information returned by program_space::get_entry_point_info is not expected to change from one call to the next. This information should be a property of the executable and libraries, so we really only need to figure it out once. This commit caches the entry point information within program_space, clearing it whenever an inferior starts or exits, or whenever the executable is updated. After clearing GDB will compute, and cache the updated information the next time it is needed, which will be whenever GDB needs to unwind a frame. It will be possible to observe this change by, for example, monitoring the remote target packets, but as far as the normal GDB output is concerned, there should be no user visible changes after this commit. --- gdb/progspace.c | 34 +++++++++++++++++++++++++++++++--- gdb/progspace.h | 9 ++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/gdb/progspace.c b/gdb/progspace.c index 26e16cae8d0..c1f3044f4c5 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -289,9 +289,12 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ -program_space::entry_point_info +const program_space::entry_point_info & program_space::get_entry_point_info () const { + if (m_entry_point_info.has_value ()) + return m_entry_point_info.value (); + std::optional<CORE_ADDR> exec_entry_address = this->exec_entry_point_address_if_available (); @@ -299,8 +302,9 @@ program_space::get_entry_point_info () const if (m_solib_ops != nullptr) inferior_entry_address = m_solib_ops->inferior_entry_point_address (); - return entry_point_info (std::move (inferior_entry_address), - std::move (exec_entry_address)); + m_entry_point_info.emplace (std::move (inferior_entry_address), + std::move (exec_entry_address)); + return m_entry_point_info.value (); } /* Implement the 'maint info entry-address' command. */ @@ -538,11 +542,35 @@ program_space::clear_solib_cache () deleted_solibs.clear (); } +/* Clear cached entry point information in the program space of INF. */ + +static void +clear_cached_entry_point_info_for_inferior (inferior *inf) +{ + inf->pspace->clear_cached_entry_point_info (); +} + +/* Clear cached entry point information in the program space PSPACE. */ + +static void +clear_cached_entry_point_info_for_pspace (program_space *pspace, + bool /* reload */) +{ + pspace->clear_cached_entry_point_info (); +} + /* See progspace.h. */ void initialize_progspace () { + gdb::observers::inferior_created.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::inferior_exit.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::executable_changed.attach + (clear_cached_entry_point_info_for_pspace, "program-space"); + add_cmd ("program-spaces", class_maintenance, maintenance_info_program_spaces_command, _("Info about currently known program spaces."), diff --git a/gdb/progspace.h b/gdb/progspace.h index d75379fe621..407d2595136 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -369,7 +369,11 @@ struct program_space /* Return information about the entry point in the main executable, and the entry point for the inferior, which might be different from the main executable. */ - entry_point_info get_entry_point_info () const; + const entry_point_info &get_entry_point_info () const; + + /* Clear any cached entry point information. */ + void clear_cached_entry_point_info () + { m_entry_point_info.reset (); } /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ @@ -462,6 +466,9 @@ struct program_space /* See `exec_filename`. */ gdb::unique_xmalloc_ptr<char> m_exec_filename; + + /* Cached entry point information. See get_entry_point_info. */ + mutable std::optional<entry_point_info> m_entry_point_info; }; /* The list of all program spaces. There's always at least one. */ -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame 2026-06-25 15:26 ` [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (3 preceding siblings ...) 2026-06-25 15:26 ` [PATCHv5 4/4] gdb: cache program space entry point information Andrew Burgess @ 2026-07-10 14:24 ` Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess ` (4 more replies) 4 siblings, 5 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-10 14:24 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess In v6: - More adjustments to the test in patch #2 to address issues the Linaro CI testing highlighted. Nothing serious, just minor tweaks to some patterns to handle different output patterns. - Rebased to HEAD. In v5: - Update to test in patch #2 to allow the test to work on machines that build PIE by default. I actually extended the test to build both PIE and non-PIE executables, and test with both. In v4: - Rebase to current HEAD of master. - In patch #2, added a supports_process_entry_point proc as suggested by Pedro, and make use of this in the new test. Updates to address review comments from v2 series: - Doc updates based on Eli's feedback. - Patch #2: + Avoid swallowing Ctrl-C in try/catch. That whole try/catch block has been removed in v3, so problem sorted! + Rewrote entry address calculation as suggested by Pedro, now pulls all required information from the inferior. + Use 'objdump -f' rather than readelf to get the entry address in the test. + Added a helper proc for is_svr4_target, which includes a larger list of targets beyond just Linux. This is then used in the test. + Test updated to use -lbl flag in gdb_test_multiple. - Patch #3: + Use 'frame' rather than 'bt 1' + Loosen the test regexp after the 'starti' command, this should now work on Windows targets, or any target with multiple threads at startup. + Test updated to use -lbl in gdb_test_multiple. - In patch #3 the entry frame detection is moved from `frame_unwind_caller_frame` into `get_prev_frame_always`, I hope this will address Guinevere's concerns about having to add the additional entry frame detection in multiple locations. Guinevere also had some concerns about the naming of existing functions (i.e. not things added by me in this commit), I don't plan to make any naming changes to the get_prev_frame* functions in this series. - Patch #4 is new, this adds some caching to avoid repeatedly looking up the entry address. The new approach requires reading target memory, which can be expensive, especially on remote targets. --- Andrew Burgess (4): gdb: rename program_space::entry_point_address* functions gdb: introduce program_space::get_entry_point_info function gdb: allow 'until' to work in outermost frame gdb: cache program space entry point information gdb/NEWS | 7 + gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/doc/gdb.texinfo | 22 ++ gdb/frame.c | 105 ++++++--- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 94 +++++++- gdb/progspace.h | 67 +++++- gdb/solib-frv.c | 2 +- gdb/solib-svr4.c | 85 +++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 215 ++++++++++++++++++ gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 ++ .../gdb.base/until-in-entry-frame.exp | 166 ++++++++++++++ gdb/testsuite/lib/gdb.exp | 39 ++++ 17 files changed, 800 insertions(+), 47 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.c create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.exp base-commit: 32a7e94f84a3df0bcd08a2835e2c316f5ed07f77 -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv6 1/4] gdb: rename program_space::entry_point_address* functions 2026-07-10 14:24 ` [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-07-10 14:24 ` Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess ` (3 subsequent siblings) 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-10 14:24 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves Rename program_space::entry_point_address to program_space::exec_entry_point_address and program_space::entry_point_address_query to program_space::exec_entry_point_address_if_available. There are two aspects to this renaming. First I replace 'query' with 'if_available' in one of the functions. I feel this better describes the function, and also is inline with how other, similar, functions are named in GDB. The second part of the renaming is to add the 'exec_' prefix to the front of both function names. When a dynamically linked inferior is started the first instruction executed is actually within the run-time linker, not within the main executable, so it could be argued that the actual entry address for the inferior is not the entry address of the main executable. However, there is an equally valid argument that the entry address of the executable is also something worth finding. The existing entry address within the inferior is used for a number of tasks in GDB, for example displaced stepping (arch-utils.c), inferior function calls (arc-tdep.c and infcall.c), and for detecting the "entry" frame. This last one, the entry frame detection is interesting. In a dynamically linked executable it could be argued that there are two entry frames. The very first frame that is executed in the inferior, this is where 'starti' stops the inferior. And then the very first frame within the main executable. I think that both of these are valid. Currently, as we can only find the entry address within the main executable, we can only identify the first frame of the main executable. And this can cause some problems, consider: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) Here frames #1 to #3 are all bogus, created by GDB based on whatever values happen to be in the registers when the inferior starts. In a later commit I'd like to fix this problem, however, I would prefer that the other users of program_space::entry_point_address continue to use the address within the main executable. So, in order to keep the distinction between the two different types of entry point, I'm renaming the existing functions with the 'exec_' prefix. There should be no user visible changes after this commit. Approved-By: Pedro Alves <pedro@palves.net> --- gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/frame.c | 4 ++-- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 6 +++--- gdb/progspace.h | 12 ++++++------ gdb/solib-frv.c | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gdb/arc-tdep.c b/gdb/arc-tdep.c index 4936a5c8fbb..7ed83c4cae6 100644 --- a/gdb/arc-tdep.c +++ b/gdb/arc-tdep.c @@ -860,7 +860,7 @@ arc_push_dummy_code (struct gdbarch *gdbarch, CORE_ADDR sp, CORE_ADDR funaddr, struct regcache *regcache) { *real_pc = funaddr; - *bp_addr = current_program_space->entry_point_address (); + *bp_addr = current_program_space->exec_entry_point_address (); return sp; } diff --git a/gdb/arch-utils.c b/gdb/arch-utils.c index e959788bd3b..88047731ce6 100644 --- a/gdb/arch-utils.c +++ b/gdb/arch-utils.c @@ -57,7 +57,7 @@ displaced_step_at_entry_point (struct gdbarch *gdbarch) CORE_ADDR addr; int bp_len; - addr = current_program_space->entry_point_address (); + addr = current_program_space->exec_entry_point_address (); /* Inferior calls also use the entry point as a breakpoint location. We don't want displaced stepping to interfere with those diff --git a/gdb/frame.c b/gdb/frame.c index cefdde5ed1e..9881dd54a1f 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2695,7 +2695,7 @@ static bool inside_entry_func (const frame_info_ptr &this_frame) { std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) return false; @@ -2771,7 +2771,7 @@ get_prev_frame (const frame_info_ptr &this_frame) added to work around that (now fixed) case. */ /* NOTE: cagney/2003-07-15: danielj (if I'm reading it right) suggested having the inside_entry_func test use the - inside_main_func() msymbol trick (along with entry_point_address() + inside_main_func() msymbol trick (along with exec_entry_point_address() I guess) to determine the address range of the start function. That should provide a far better stopper than the current heuristics. */ diff --git a/gdb/infcall.c b/gdb/infcall.c index e6b24ff5310..6d26841ede3 100644 --- a/gdb/infcall.c +++ b/gdb/infcall.c @@ -1300,7 +1300,7 @@ call_function_by_hand_dummy (struct value *function, CORE_ADDR dummy_addr; real_pc = funaddr; - dummy_addr = current_program_space->entry_point_address (); + dummy_addr = current_program_space->exec_entry_point_address (); /* A call dummy always consists of just a single breakpoint, so its address is the same as the address of the dummy. diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c index a7381677498..d03e4768792 100644 --- a/gdb/linux-tdep.c +++ b/gdb/linux-tdep.c @@ -2952,7 +2952,7 @@ linux_displaced_step_location (struct gdbarch *gdbarch) /* Determine entry point from target auxiliary vector. This avoids the need for symbols. Also, when debugging a stand-alone SPU - executable, entry_point_address () will point to an SPU + executable, exec_entry_point_address () will point to an SPU local-store address and is thus not usable as displaced stepping location. The auxiliary vector gets us the PowerPC-side entry point address instead. */ diff --git a/gdb/progspace.c b/gdb/progspace.c index 1407b058dfd..0e8516f4640 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -263,7 +263,7 @@ program_space::empty () /* See progspace.h. */ std::optional<CORE_ADDR> -program_space::entry_point_address_query () const +program_space::exec_entry_point_address_if_available () const { objfile *objf = symfile_object_file; if (objf == NULL || !objf->per_bfd->ei.entry_point_p) @@ -276,9 +276,9 @@ program_space::entry_point_address_query () const /* See progspace.h. */ CORE_ADDR -program_space::entry_point_address () const +program_space::exec_entry_point_address () const { - std::optional<CORE_ADDR> retval = entry_point_address_query (); + std::optional<CORE_ADDR> retval = exec_entry_point_address_if_available (); if (!retval.has_value ()) error (_("Entry point address is not known.")); diff --git a/gdb/progspace.h b/gdb/progspace.h index e9261ff8590..1ae1e42f3bb 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,13 +323,13 @@ struct program_space return m_target_sections; } - /* If there is a valid and known entry point in this program space, - return it. Otherwise return an empty optional. */ - std::optional<CORE_ADDR> entry_point_address_query () const; + /* If there is a valid and known entry point in the main executable of + this program space, return it. Otherwise return an empty optional. */ + std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; - /* Get the entry point address in this program space. Call error if - it is not known. */ - CORE_ADDR entry_point_address () const; + /* Get the entry point address for the main executable in this program + space. Call error if it is not known. */ + CORE_ADDR exec_entry_point_address () const; /* Return true if any objfile of this program space has partial symbols. */ diff --git a/gdb/solib-frv.c b/gdb/solib-frv.c index 4f0aac31e73..69b882e534a 100644 --- a/gdb/solib-frv.c +++ b/gdb/solib-frv.c @@ -689,7 +689,7 @@ enable_break (void) } std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) { solib_debug_printf ("Symbol file has no entry point."); -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv6 2/4] gdb: introduce program_space::get_entry_point_info function 2026-07-10 14:24 ` [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess @ 2026-07-10 14:24 ` Andrew Burgess 2026-07-10 15:39 ` Simon Marchi 2026-07-10 14:24 ` [PATCHv6 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 subsequent siblings) 4 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-07-10 14:24 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Eli Zaretskii I noticed that when debugging a dynamically linked executable, if I started the inferior with 'starti' then used 'bt' I would see some bogus frames: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) This surprised me as 'backtrace past-entry' was off: (gdb) show backtrace past-entry Whether backtraces should continue past the entry point of a program is off. I was expecting GDB to stop the backtrace at the inferior's entry address. Frame unwinding starts in get_prev_frame, and in here we find this block: 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)) { frame_debug_got_null_frame (this_frame, "inside entry func"); return NULL; } Which uses inside_entry_func to terminate the backtrace when we reach the entry frame. The inside_entry_func function calls current_program_space->exec_entry_point_address_if_available and uses the result to figure out if we are in the entry frame. And here the problem becomes obvious, we are only checking if we are in the "entry frame" for the main executable, not for the inferior as a whole. And indeed, if I compile the same test program as a static binary, where there will be no run-time linker, and the entry point of the executable is the entry point for the inferior, then the 'bt' problem I saw above goes away. This suggests, I think, that we need to track two different entry addresses, the entry address for the executable file, and the entry address for the entire inferior. Then, for dynamically linked executables, these two addresses can be different, the former will still be the same address within the main executable file, while the latter will be the address of the entry point within the run-time linker. If we had this information then we could extend inside_entry_func to check both addresses, and the 'bt' problem seen above will be resolved. To make this information available I added a new solib_ops method, solib_ops::inferior_entry_point_address, for svr4 targets this figures out if the main executable is dynamically linked, and if it is, uses the AT_BASE auxv entry and the entry address pulled from the ELF header to compute the inferior entry address. I then added program_space::get_entry_point_info, which returns a struct containing the two entry point addresses, one comes from the new solib_ops method, and one comes from the existing method program_space::exec_entry_point_address_if_available. With the infrastructure in place I can then update inside_entry_func to check against both entry addresses. To aid in debugging GDB, I added a new maintenance command: maintenance info entry-address which just calls program_space::get_entry_point_info and then prints the two addresses. I left this as a maintenance command as I don't see much user utility in this right now, but it made it easier for me to see what GDB was doing, so I left the command in this commit. Reviewed-By: Eli Zaretskii <eliz@gnu.org> --- gdb/NEWS | 7 + gdb/doc/gdb.texinfo | 22 +++ gdb/frame.c | 10 +- gdb/progspace.c | 60 ++++++ gdb/progspace.h | 48 +++++ gdb/solib-svr4.c | 85 ++++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 215 +++++++++++++++++++++ gdb/testsuite/lib/gdb.exp | 39 ++++ 10 files changed, 496 insertions(+), 5 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp diff --git a/gdb/NEWS b/gdb/NEWS index ec9b5a33787..9989c852b23 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -179,6 +179,13 @@ disable skip These are new aliases for 'skip delete', 'skip enable', and 'skip disable' respectively. +maint info entry-address + Display the inferior and main executable entry addresses for the + current inferior. The inferior entry address is the address of the + first instruction in the inferior that was executed. The main + executable entry address is the address of the first instruction in + the main executable that will be executed. + * MI changes ** The "-trace-save" command no longer supports the "-ctf" flag. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index a698b2b8451..4c0c4709a23 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -43212,6 +43212,28 @@ Maintenance Commands Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M @end smallexample + +@kindex maint info entry-address +@item maint info entry-address +Display the entry addresses for the currently selected inferior. Two +addresses are displayed, the inferior entry address and the executable +entry address. + +Neither address is the address of @code{main}. When a program is +started, low-level startup code (typically a function called +@code{_start}) runs before @code{main} is called. + +The executable entry address is the address of this startup code +within the main executable (@pxref{Files, ,Commands to Specify +Files}). + +The inferior entry address is the address of the very first +instruction executed when the inferior is started. For statically +linked programs this is the same as the executable entry address. For +dynamically linked programs the run-time linker must execute first in +order to load shared libraries, so the inferior entry address will be +an address within the run-time linker rather than within the main +executable. @end table The following command is useful for non-interactive invocations of diff --git a/gdb/frame.c b/gdb/frame.c index 9881dd54a1f..782ca33abd8 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2694,12 +2694,12 @@ inside_main_func (const frame_info_ptr &this_frame) static bool inside_entry_func (const frame_info_ptr &this_frame) { - std::optional<CORE_ADDR> entry_point - = current_program_space->exec_entry_point_address_if_available (); - if (!entry_point.has_value ()) - return false; + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); - return get_frame_func (this_frame) == *entry_point; + CORE_ADDR frame_func_addr = get_frame_func (this_frame); + return (ep_info.exec_entry_address () == frame_func_addr + || ep_info.inferior_entry_address () == frame_func_addr); } /* Return a structure containing various interesting information about diff --git a/gdb/progspace.c b/gdb/progspace.c index 0e8516f4640..26e16cae8d0 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -26,6 +26,7 @@ #include <algorithm> #include "cli/cli-style.h" #include "observable.h" +#include "arch-utils.h" /* The last program space number assigned. */ static int last_program_space_num = 0; @@ -288,6 +289,55 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ +program_space::entry_point_info +program_space::get_entry_point_info () const +{ + std::optional<CORE_ADDR> exec_entry_address + = this->exec_entry_point_address_if_available (); + + std::optional<CORE_ADDR> inferior_entry_address; + if (m_solib_ops != nullptr) + inferior_entry_address = m_solib_ops->inferior_entry_point_address (); + + return entry_point_info (std::move (inferior_entry_address), + std::move (exec_entry_address)); +} + +/* Implement the 'maint info entry-address' command. */ + +static void +maintenance_info_entry_address (const char *args, int from_tty) +{ + if (args != nullptr && *args != '\0') + error (_("unknown argument: %s"), args); + + struct gdbarch *gdbarch = get_current_arch (); + + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + /* Display a single entry address ADDR with TITLE as a description. */ + auto display_entry_address + = [&gdbarch] (const char *title, + const std::optional<CORE_ADDR> &addr) -> void + { + gdb_puts (title); + if (addr.has_value ()) + fputs_styled (paddress (gdbarch, addr.value ()), + address_style.style (), gdb_stdout); + else + fputs_styled ("<unknown>", metadata_style.style (), gdb_stdout); + gdb_puts ("\n"); + }; + + display_entry_address (_("Inferior entry address: "), + ep_info.inferior_entry_address ()); + display_entry_address (_("Executable entry address: "), + ep_info.exec_entry_address ()); +} + +/* See progspace.h. */ + bool program_space::has_partial_symbols () { @@ -498,6 +548,16 @@ initialize_progspace () _("Info about currently known program spaces."), &maintenanceinfolist); + add_cmd ("entry-address", class_maintenance, + maintenance_info_entry_address, + _("Information about the current inferior's entry addresses.\n\ +Display the address of the first instruction executed within the\n\ +inferior, and the first instruction executed within the main executable.\n\ +The entry address of the whole inferior will be <unknown> prior to the\n\ +inferior starting, and either or both addresses can be <unknown> if\n\ +GDB is unable to find the required information."), + &maintenanceinfolist); + /* There's always one program space. Note that this function isn't an automatic _initialize_foo function, since other _initialize_foo routines may need to install their per-pspace diff --git a/gdb/progspace.h b/gdb/progspace.h index 1ae1e42f3bb..d75379fe621 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,6 +323,54 @@ struct program_space return m_target_sections; } + /* Class for tracking two possible entry points that an inferior might + have, the entry point for the entire inferior, and the entry point + within the main executable. */ + struct entry_point_info + { + explicit entry_point_info (std::optional<CORE_ADDR> inferior_entry_address, + std::optional<CORE_ADDR> exec_entry_address) + : m_inferior_entry_address (std::move (inferior_entry_address)), + m_exec_entry_address (std::move (exec_entry_address)) + { /* Nothing. */ } + + DISABLE_COPY_AND_ASSIGN (entry_point_info); + + /* The entry address within the inferior as a whole. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &inferior_entry_address () const + { + return m_inferior_entry_address; + } + + /* The entry address within the main executable. See the member + variable definition below for more details. */ + const std::optional<CORE_ADDR> &exec_entry_address () const + { + return m_exec_entry_address; + } + + private: + /* The entry address within the inferior. This can be outside of the + main executable, e.g. for dynamically linked executables this could + be the entry address for the run-time linker. For statically linked + executables this will be the entry address of the main executable. + This can be empty if GDB doesn't know how to figure out the correct + entry address for any reason. */ + std::optional<CORE_ADDR> m_inferior_entry_address; + + /* The entry address within the main executable. This can be empty if + the main executable is not set yet, or GDB doesn't have an objfile + associated with the main executable, e.g. in some attach, or remote + debug cases. */ + std::optional<CORE_ADDR> m_exec_entry_address; + }; + + /* Return information about the entry point in the main executable, and + the entry point for the inferior, which might be different from the + main executable. */ + entry_point_info get_entry_point_info () const; + /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c index 8e3de4d3ea1..12f3e783744 100644 --- a/gdb/solib-svr4.c +++ b/gdb/solib-svr4.c @@ -3777,6 +3777,91 @@ svr4_solib_ops::get_solibs_in_ns (int nsid) const return ns_solibs; } +/* See solib.h. */ + +std::optional<CORE_ADDR> +svr4_solib_ops::inferior_entry_point_address () const +{ + std::optional<gdb::byte_vector> interp_name_holder + = svr4_find_program_interpreter (); + + /* No interpreter means this is a static executable. Ask the + program_space for the entry address within the main executable. */ + if (!interp_name_holder.has_value ()) + return m_pspace->exec_entry_point_address_if_available (); + + /* For a dynamically linked executable the inferior's true entry point + is the entry point of the dynamic linker. We find this using the + AT_BASE auxiliary vector entry, which gives the dynamic linker's + load address, combined with e_entry address pulled from the + inferior. We assume that the ELF header can be read from AT_BASE. */ + CORE_ADDR at_base_addr; + if (target_auxv_search (AT_BASE, &at_base_addr) <= 0) + return {}; + + /* Determine ELF architecture type. Use the size of a program header + entry to determine which ELF header we can expect to find. */ + size_t e_entry_offset = 0; + size_t e_entry_size = 0; + CORE_ADDR at_phent; + if (target_auxv_search (AT_PHENT, &at_phent) <= 0) + return {}; + if (at_phent == sizeof (Elf32_External_Phdr)) + { + e_entry_offset = offsetof (Elf32_External_Ehdr, e_entry); + e_entry_size = 4; + } + else if (at_phent == sizeof (Elf64_External_Phdr)) + { + e_entry_offset = offsetof (Elf64_External_Ehdr, e_entry); + e_entry_size = 8; + } + else + return {}; + + /* Architecture of the current inferior. */ + gdbarch *gdbarch = current_inferior ()->arch (); + + /* Read the entry address from the ELF header at AT_BASE_ADDR. */ + CORE_ADDR e_entry; + gdb_byte buffer[sizeof (CORE_ADDR)]; + if (target_read_memory (at_base_addr + e_entry_offset, buffer, e_entry_size)) + return {}; + bfd_endian byte_order = gdbarch_byte_order (gdbarch); + e_entry = extract_unsigned_integer (buffer, e_entry_size, byte_order); + + /* Ensure AT_BASE_ADDR has proper sign in its possible upper bits so + that `+ at_base_addr' will overflow CORE_ADDR width not creating + invalid addresses like 0x101234567 for 32bit inferiors on 64bit + GDB. */ + int addr_bit = gdbarch_addr_bit (gdbarch); + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) + { + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; + + gdb_assert (at_base_addr < space_size); + + /* E_ENTRY exceeding SPACE_SIZE would be for prelinked + 64bit ld.so with 32bit executable, it should not happen. */ + if (e_entry < space_size + && e_entry + at_base_addr >= space_size) + at_base_addr -= space_size; + } + + /* Compute the entry address. */ + e_entry = at_base_addr + e_entry; + + /* Handle the case where E_ENTRY is a function descriptor. Also remove + any non-address (e.g. tag) bits from E_ENTRY. */ + e_entry + = gdbarch_convert_from_func_ptr_addr (gdbarch, e_entry, + current_inferior ()->top_target ()); + e_entry + = gdbarch_addr_bits_remove (gdbarch, e_entry); + + return e_entry; +} + INIT_GDB_FILE (svr4_solib) { gdb::observers::free_objfile.attach (svr4_free_objfile_observer, diff --git a/gdb/solib-svr4.h b/gdb/solib-svr4.h index 3078a092778..f76f350d951 100644 --- a/gdb/solib-svr4.h +++ b/gdb/solib-svr4.h @@ -115,6 +115,7 @@ struct svr4_solib_ops : public solib_ops void iterate_over_objfiles_in_search_order (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const override; + std::optional<CORE_ADDR> inferior_entry_point_address () const override; /* Return the appropriate link map offsets table for the architecture. */ virtual link_map_offsets *fetch_link_map_offsets () const = 0; diff --git a/gdb/solib.h b/gdb/solib.h index 662d467b0cc..2db836dc005 100644 --- a/gdb/solib.h +++ b/gdb/solib.h @@ -283,6 +283,20 @@ struct solib_ops (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const; + /* Return the inferior entry point address. This isn't always the entry + point of the main executable, but will be the actual entry point where + the inferior starts (or started) executing. + + For example on SVR4 targets, for dynamically linked executables, this + will be the entry address of the dynamic linker. But for statically + linked executables, this will be the entry point of the main + executable. + + If the entry address cannot be found then an empty optional is + returned. */ + virtual std::optional<CORE_ADDR> inferior_entry_point_address () const + { return {}; } + protected: /* The program space for which this solib_ops was created. */ program_space *m_pspace; diff --git a/gdb/testsuite/gdb.base/bt-after-starti.exp b/gdb/testsuite/gdb.base/bt-after-starti.exp new file mode 100644 index 00000000000..c2a78a05cf4 --- /dev/null +++ b/gdb/testsuite/gdb.base/bt-after-starti.exp @@ -0,0 +1,215 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'bt' from the very first instruction (where 'starti' +# stops GDB) doesn't display any unexpected frames. + +require !use_gdb_stub + +standard_testfile main.c + +# Build default executable, hopefully dynamically linked, but we check +# this below. +foreach pie_mode { pie nopie } { + if { [build_executable "failed to build" $testfile-$pie_mode $srcfile \ + [list debug $pie_mode]] } { + return + } +} + +# Build a statically linked executable. +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + {debug additional_flags=-static}] } { + return +} + +# Run the 'maint info entry-address' command. Expect INF_RE to match +# the inferior entry address and EXE_RE to match the executable entry +# address. TESTNAME is used as the test name. +proc check_maint_info_entry_addr { inf_re exe_re testname } { + gdb_test "maint info entry-address" \ + [multi_line \ + "Inferior entry address: ${inf_re}" \ + "Executable entry address: ${exe_re}"] \ + $testname +} + +# Use 'info proc mappings' to find the address of the first mapping of +# TESTFILE, return this address. If the address cannot be found then +# the empty string is returned. +# +# This emits a PASS or FAIL depending on whether the address is found +# or not. +proc get_mapped_file_base_addr { testfile } { + set base_addr "" + gdb_test_multiple "info proc mappings" "" -lbl { + -re "\r\n($::hex) \[^\r\n\]+/${testfile}\\s*(?=\r\n)" { + if { $base_addr eq "" } { + set base_addr $expect_out(1,string) + } + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $base_addr ne "" } \ + "find base address of $testfile" + } + } + return $base_addr +} + +# Use 'objdump' to extract the entry point address from TESTFILE. +proc get_exec_entry_address { testfile } { + set objdump_program [gdb_find_objdump] + set command "exec $objdump_program -f [standard_output_file $testfile]" + verbose -log "command is $command" + set result [catch {{*}$command} output] + verbose -log "result is $result" + verbose -log "output is $output" + + set testname "get_exec_entry_address" + + if {$result != 0} { + fail $testname + return "UNKNOWN" + } + + if {![regexp "\nstart address ($::hex)\n" $output trash addr]} { + fail "$testname (no entry point address header)" + return "UNKNOWN" + } + + return 0x[format %x $addr] +} + +# Start TESTFILE using 'starti'. Check if the 'backtrace' command +# prints any additional frames; we don't expect any. Create a core +# file from the running inferior. Restart GDB and load the core file, +# check that GDB can still figure out the entry addresses. +# +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked +# executable, otherwise TESTFILE is statically linked. +proc run_test { testfile is_dynamic } { + clean_restart $testfile + + set exe_entry_addr [get_exec_entry_address $testfile] + + # The inferior entry address is always unknown before starting the + # inferior because GDB has no solib_ops yet, and it is only + # through that that we figure out the inferior entry address. + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ + "check 'maint info entry-address' before inferior starts" + + # Start inferior with 'starti' and then wait for a prompt. + gdb_starti_cmd + gdb_test "" ".*" "prompt after starti" + + # Not every target supports finding the process entry point. + # Targets that don't support this report '<unknown>', we check for + # this too on those targets. + if {[supports_process_entry_point]} { + set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN" \ + "get current program counter"] + + if { !$is_dynamic } { + # In static binaries, the first address should be the first + # address in the executable. + gdb_assert { $pc == $exe_entry_addr } \ + "stopped at executable entry address" + } + + set inf_entry_addr $pc + } else { + set inf_entry_addr "<unknown>" + } + + # If using PIE then the executable entry address we looked up from + # the objfile will have been relocated. Attempt to compute the + # relocated address and updated EXE_ENTRY_ADDR, otherwise set + # EXE_ENTRY_ADDR to a generic pattern that matches any address. + if {[exec_is_pie [standard_output_file $testfile]]} { + set base_addr [get_mapped_file_base_addr $testfile] + if { $base_addr ne "" } { + set exe_entry_addr \ + 0x[format %x [expr {$base_addr + $exe_entry_addr}]] + } else { + # We failed to find the base address. The + # get_mapped_file_base_addr will have already emitted a + # FAIL. Set the expected pattern to something generic and + # push on, we can still test a bunch of things, we just + # don't know the address is correct. + set exe_entry_addr $::hex + } + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after starti" + + # Allow backtrace past the entry frame, count how many frames GDB + # finds. If on this target there are no additional frames then + # the following test isn't going to tell us much, so skip it. + gdb_test_no_output "set backtrace past-entry on" + set frame_count 0 + gdb_test_multiple "bt" "count possible frames" -lbl { + -re "\r\n#($::decimal)\\s+\[^\r\n\]+(?=\r\n)" { + incr frame_count + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $frame_count > 0 } $gdb_test_name + } + } + + if { $frame_count > 1 } { + # Turn off backtrace past the entry frame. Use the 'bt' command, + # and check we see only a single frame. + gdb_test_no_output "set backtrace past-entry off" + gdb_test "bt" "^#0 \[^\r\n\]+" \ + "single frame when backtrace past-entry is off" + } else { + unsupported "no additional frames that GDB can hide" + } + + # Create a core file. Restart GDB. Load the core file. Check + # that 'maint info entry-address' gives the correct output. + set corefile [standard_output_file $testfile].core + if {![gdb_gcore_cmd $corefile "generate corefile"]} { + return + } + + clean_restart $testfile + + if {[gdb_core_cmd $corefile "load corefile"] != 1} { + return + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after loading core file" +} + +with_test_prefix "dynamic" { + foreach_with_prefix pie_mode { pie nopie } { + set dyln_name [section_get $binfile-$pie_mode .interp] + if { $dyln_name eq "" } { + unsupported "couldn't find dynamic linker name" + } else { + run_test $testfile-$pie_mode true + } + } +} + +with_test_prefix "static" { + run_test $testfile_static false +} diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp index a19e8ba6729..085f098fd6e 100644 --- a/gdb/testsuite/lib/gdb.exp +++ b/gdb/testsuite/lib/gdb.exp @@ -4217,6 +4217,45 @@ proc is_aarch64_target {} { return [expr {![is_aarch32_target]}] } +# Return true for svr4 targets, otherwise, return false. + +proc is_svr4_target {} { + return [expr {[istarget *-linux*] || [istarget *-freebsd*] + || [istarget *-netbsd*] || [istarget *-openbsd*] + || [istarget *-solaris*] || [istarget *-gnu]}] +} + +# Return false for targets that don't support finding the whole +# process entry address, otherwise, return true. + +proc supports_process_entry_point {} { + # SVR4 targets support finding the entry point. This is done + # within GDB so will work even for remote targets. + if {[is_svr4_target]} { + return true + } + + # Windows and Darwin don't currently support this. + if {[istarget *-*-mingw*] + || [istarget *-*-cygwin*] + || [istarget *-*-pe*] + || [istarget *-*-darwin*]} { + return false + } + + # For remote targets there is no RSP packet to retrieve the entry + # point, so this won't work unless the solib code can handle this + # within GDB, see the svr4 check above. + if {[gdb_protocol_is_remote]} { + return false + } + + # Assume everything else supports this by default. If a test + # fails because we get here then either fix GDB to support this + # feature, or add a new deny list entry above. + return true +} + # Return 1 if displaced stepping is supported on target, otherwise, return 0. proc support_displaced_stepping {} { -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv6 2/4] gdb: introduce program_space::get_entry_point_info function 2026-07-10 14:24 ` [PATCHv6 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-07-10 15:39 ` Simon Marchi 0 siblings, 0 replies; 54+ messages in thread From: Simon Marchi @ 2026-07-10 15:39 UTC (permalink / raw) To: Andrew Burgess, gdb-patches; +Cc: Eli Zaretskii Some comments below, I think they are pretty straightfowrard, so with those fixed (and if you don't end up making more major changes): Approved-By: Simon Marchi <simon.marchi@efficios.com> On 7/10/26 10:24 AM, Andrew Burgess wrote: > diff --git a/gdb/progspace.c b/gdb/progspace.c > index 0e8516f4640..26e16cae8d0 100644 > --- a/gdb/progspace.c > +++ b/gdb/progspace.c > @@ -26,6 +26,7 @@ > #include <algorithm> > #include "cli/cli-style.h" > #include "observable.h" > +#include "arch-utils.h" > > /* The last program space number assigned. */ > static int last_program_space_num = 0; > @@ -288,6 +289,55 @@ program_space::exec_entry_point_address () const > > /* See progspace.h. */ > > +program_space::entry_point_info > +program_space::get_entry_point_info () const > +{ > + std::optional<CORE_ADDR> exec_entry_address > + = this->exec_entry_point_address_if_available (); > + > + std::optional<CORE_ADDR> inferior_entry_address; > + if (m_solib_ops != nullptr) > + inferior_entry_address = m_solib_ops->inferior_entry_point_address (); > + > + return entry_point_info (std::move (inferior_entry_address), > + std::move (exec_entry_address)); I wouldn't std::move those (I think it's the same as cpying on this type). > diff --git a/gdb/progspace.h b/gdb/progspace.h > index 1ae1e42f3bb..d75379fe621 100644 > --- a/gdb/progspace.h > +++ b/gdb/progspace.h > @@ -323,6 +323,54 @@ struct program_space > return m_target_sections; > } > > + /* Class for tracking two possible entry points that an inferior might > + have, the entry point for the entire inferior, and the entry point > + within the main executable. */ > + struct entry_point_info I think it would be preferable to define this class outside of struct program_space. I think that having (unnecessarily) nested classes just makes them harder to read. > + { > + explicit entry_point_info (std::optional<CORE_ADDR> inferior_entry_address, You can drop explicit, as the constructor has two parameters. > + std::optional<CORE_ADDR> exec_entry_address) > + : m_inferior_entry_address (std::move (inferior_entry_address)), > + m_exec_entry_address (std::move (exec_entry_address)) I'm not sure it helps to std::move optional<CORE_ADDR>. > + { /* Nothing. */ } > + > + DISABLE_COPY_AND_ASSIGN (entry_point_info); > + > + /* The entry address within the inferior as a whole. See the member > + variable definition below for more details. */ > + const std::optional<CORE_ADDR> &inferior_entry_address () const > + { > + return m_inferior_entry_address; > + } > + > + /* The entry address within the main executable. See the member > + variable definition below for more details. */ > + const std::optional<CORE_ADDR> &exec_entry_address () const > + { > + return m_exec_entry_address; > + } std::optional<CORE_ADDR> is small enough (16 bytes) that I would return it by value. > diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c > index 8e3de4d3ea1..12f3e783744 100644 > --- a/gdb/solib-svr4.c > +++ b/gdb/solib-svr4.c > @@ -3777,6 +3777,91 @@ svr4_solib_ops::get_solibs_in_ns (int nsid) const > return ns_solibs; > } > > +/* See solib.h. */ > + > +std::optional<CORE_ADDR> > +svr4_solib_ops::inferior_entry_point_address () const > +{ > + std::optional<gdb::byte_vector> interp_name_holder > + = svr4_find_program_interpreter (); > + > + /* No interpreter means this is a static executable. Ask the > + program_space for the entry address within the main executable. */ > + if (!interp_name_holder.has_value ()) > + return m_pspace->exec_entry_point_address_if_available (); > + > + /* For a dynamically linked executable the inferior's true entry point > + is the entry point of the dynamic linker. We find this using the > + AT_BASE auxiliary vector entry, which gives the dynamic linker's > + load address, combined with e_entry address pulled from the > + inferior. We assume that the ELF header can be read from AT_BASE. */ > + CORE_ADDR at_base_addr; > + if (target_auxv_search (AT_BASE, &at_base_addr) <= 0) > + return {}; > + > + /* Determine ELF architecture type. Use the size of a program header > + entry to determine which ELF header we can expect to find. */ > + size_t e_entry_offset = 0; > + size_t e_entry_size = 0; > + CORE_ADDR at_phent; > + if (target_auxv_search (AT_PHENT, &at_phent) <= 0) > + return {}; > + if (at_phent == sizeof (Elf32_External_Phdr)) > + { > + e_entry_offset = offsetof (Elf32_External_Ehdr, e_entry); > + e_entry_size = 4; > + } > + else if (at_phent == sizeof (Elf64_External_Phdr)) > + { > + e_entry_offset = offsetof (Elf64_External_Ehdr, e_entry); > + e_entry_size = 8; > + } > + else > + return {}; > + > + /* Architecture of the current inferior. */ > + gdbarch *gdbarch = current_inferior ()->arch (); > + > + /* Read the entry address from the ELF header at AT_BASE_ADDR. */ > + CORE_ADDR e_entry; > + gdb_byte buffer[sizeof (CORE_ADDR)]; > + if (target_read_memory (at_base_addr + e_entry_offset, buffer, e_entry_size)) > + return {}; > + bfd_endian byte_order = gdbarch_byte_order (gdbarch); > + e_entry = extract_unsigned_integer (buffer, e_entry_size, byte_order); > + > + /* Ensure AT_BASE_ADDR has proper sign in its possible upper bits so > + that `+ at_base_addr' will overflow CORE_ADDR width not creating > + invalid addresses like 0x101234567 for 32bit inferiors on 64bit > + GDB. */ > + int addr_bit = gdbarch_addr_bit (gdbarch); > + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) > + { > + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; > + > + gdb_assert (at_base_addr < space_size); > + > + /* E_ENTRY exceeding SPACE_SIZE would be for prelinked > + 64bit ld.so with 32bit executable, it should not happen. */ > + if (e_entry < space_size > + && e_entry + at_base_addr >= space_size) > + at_base_addr -= space_size; > + } > + > + /* Compute the entry address. */ > + e_entry = at_base_addr + e_entry; > + > + /* Handle the case where E_ENTRY is a function descriptor. Also remove > + any non-address (e.g. tag) bits from E_ENTRY. */ > + e_entry > + = gdbarch_convert_from_func_ptr_addr (gdbarch, e_entry, > + current_inferior ()->top_target ()); > + e_entry > + = gdbarch_addr_bits_remove (gdbarch, e_entry); > + > + return e_entry; > +} Is there some code to be shared with svr4_solib_ops::enable_break, regarding fetching AT_BASE and the checks / post processing that follows? > diff --git a/gdb/testsuite/gdb.base/bt-after-starti.exp b/gdb/testsuite/gdb.base/bt-after-starti.exp > new file mode 100644 > index 00000000000..c2a78a05cf4 > --- /dev/null > +++ b/gdb/testsuite/gdb.base/bt-after-starti.exp > @@ -0,0 +1,215 @@ > +# 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 <http://www.gnu.org/licenses/>. > + > +# Check that 'bt' from the very first instruction (where 'starti' > +# stops GDB) doesn't display any unexpected frames. > + > +require !use_gdb_stub I think you used this because of the use of the gdb_starti_cmd proc. It might be possible to make it work for native-gdbserver: after connection, the inferior should be at the very first instruction. But coverage with native-extended-gdbserver is perhaps sufficient too. > diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp > index a19e8ba6729..085f098fd6e 100644 > --- a/gdb/testsuite/lib/gdb.exp > +++ b/gdb/testsuite/lib/gdb.exp > @@ -4217,6 +4217,45 @@ proc is_aarch64_target {} { > return [expr {![is_aarch32_target]}] > } > > +# Return true for svr4 targets, otherwise, return false. > + > +proc is_svr4_target {} { > + return [expr {[istarget *-linux*] || [istarget *-freebsd*] > + || [istarget *-netbsd*] || [istarget *-openbsd*] > + || [istarget *-solaris*] || [istarget *-gnu]}] > +} > + > +# Return false for targets that don't support finding the whole > +# process entry address, otherwise, return true. I find it surprising to read that you use the negative first, it would be more typical to read: # Return true for targets that support finding the whole process entry # address, otherwise, return false. > + > +proc supports_process_entry_point {} { > + # SVR4 targets support finding the entry point. This is done > + # within GDB so will work even for remote targets. > + if {[is_svr4_target]} { > + return true > + } > + > + # Windows and Darwin don't currently support this. > + if {[istarget *-*-mingw*] > + || [istarget *-*-cygwin*] > + || [istarget *-*-pe*] > + || [istarget *-*-darwin*]} { > + return false > + } > + > + # For remote targets there is no RSP packet to retrieve the entry > + # point, so this won't work unless the solib code can handle this > + # within GDB, see the svr4 check above. > + if {[gdb_protocol_is_remote]} { > + return false > + } > + > + # Assume everything else supports this by default. If a test > + # fails because we get here then either fix GDB to support this > + # feature, or add a new deny list entry above. > + return true > +} I can't verify the logic here, but from the comments it sounds fine. Simon ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv6 3/4] gdb: allow 'until' to work in outermost frame 2026-07-10 14:24 ` [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-07-10 14:24 ` Andrew Burgess 2026-07-10 17:00 ` Simon Marchi 2026-07-10 14:24 ` [PATCHv6 4/4] gdb: cache program space entry point information Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-07-10 14:24 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess 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, if the until breakpoint was not hit. 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, where we call inside_entry_func. This checks if a frame is one of the two possible entry frames, the inferior entry frame or the executable entry frame. See the previous commit for more details. The important thing is that the inferior entry frame is the absolute outer frame, the very first frame that the inferior executed when starting, while the executable entry frame is just the first frame within the main executable. There are a number of user configurable filters in get_prev_frame, the backtrace past-main filter, the backtrace frame limit filter, and the backtrace past-entry filter that we are discussing here. When creating the caller frame breakpoint, the 'until' command doesn't use get_prev_frame, it uses get_prev_frame_always. This is so that the user configurable filters don't prevent the caller frame breakpoint from being created. But this means that when creating the breakpoint, we skip the entry frame check. 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 split the inside_entry_func handling in to two parts. In get_prev_frame we will retain the check for the executable entry frame. In theory it is possible that there are frames between the executable entry frame and the inferior entry frame. These are the frames the 'backtrace past-entry' can hide or reveal (if the frames can be discovered). The check for the inferior entry frame I propose moving into get_prev_frame_always. I think this is a better place for it because any frames before the inferior entry frame are almost certainly bogus. As such we want to elide those frames even for "inner" use cases, like the caller frame breakpoint of the 'until' command. 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 | 103 +++++++---- gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 166 ++++++++++++++++++ 3 files changed, 259 insertions(+), 32 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 782ca33abd8..a84cca5b81c 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2232,6 +2232,63 @@ frame_register_unwind_location (const frame_info_ptr &initial_this_frame, } } +/* When checking if a frame is an entry frame, there are two different + entry frames to consider. This enum is used to choose between them. */ + +enum class entry_address_type +{ + /* The executable's entry frame. This is the first frame within the main + executable. */ + executable, + + /* The inferior's entry frame. This is the first frame within the + inferior, this can be outside the main executable, e.g. for a + dynamically linked executable, this will be the first frame in the + dynamic linker. */ + inferior, +}; + +/* Test whether THIS_FRAME is inside the TYPE process entry point + function. */ + +static bool +inside_entry_func (const frame_info_ptr &this_frame, + entry_address_type type) +{ + const program_space::entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + CORE_ADDR frame_func_addr; + if (!get_frame_func_if_available (this_frame, &frame_func_addr)) + return false; + + switch (type) + { + case entry_address_type::executable: + return ep_info.exec_entry_address () == frame_func_addr; + + case entry_address_type::inferior: + return ep_info.inferior_entry_address () == frame_func_addr; + } + + gdb_assert_not_reached ("unknown entry address type: %d", ((int) type)); +} + +/* Debug routine to print a NULL frame being returned. */ + +static void +frame_debug_got_null_frame (const frame_info_ptr &this_frame, + const char *reason) +{ + if (frame_debug) + { + if (this_frame != NULL) + frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); + else + frame_debug_printf ("this_frame=nullptr -> %s", reason); + } +} + /* Get the previous raw frame, and check that it is not identical to same other frame frame already in the chain. If it is, there is most likely a stack cycle, so we discard it, and mark THIS_FRAME as @@ -2538,6 +2595,17 @@ get_prev_frame_always_1 (const frame_info_ptr &this_frame) frame_info_ptr get_prev_frame_always (const frame_info_ptr &this_frame) { + if (this_frame->level >= 0 + && get_frame_type (this_frame) == NORMAL_FRAME + && !user_set_backtrace_options.backtrace_past_entry + && inside_entry_func (this_frame, entry_address_type::inferior)) + { + this_frame->prev_p = true; + this_frame->stop_reason = UNWIND_OUTERMOST; + frame_debug_got_null_frame (this_frame, "inside inferior entry func"); + return nullptr; + } + frame_info_ptr prev_frame = NULL; try @@ -2626,21 +2694,6 @@ get_prev_frame_raw (const frame_info_ptr &this_frame) return frame_info_ptr (prev_frame); } -/* Debug routine to print a NULL frame being returned. */ - -static void -frame_debug_got_null_frame (const frame_info_ptr &this_frame, - const char *reason) -{ - if (frame_debug) - { - if (this_frame != NULL) - frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); - else - frame_debug_printf ("this_frame=nullptr -> %s", reason); - } -} - /* Is this (non-sentinel) frame in the "main"() function? */ static bool @@ -2689,19 +2742,6 @@ inside_main_func (const frame_info_ptr &this_frame) return sym_addr == get_frame_func (this_frame); } -/* Test whether THIS_FRAME is inside the process entry point function. */ - -static bool -inside_entry_func (const frame_info_ptr &this_frame) -{ - const program_space::entry_point_info &ep_info - = current_program_space->get_entry_point_info (); - - CORE_ADDR frame_func_addr = get_frame_func (this_frame); - return (ep_info.exec_entry_address () == frame_func_addr - || ep_info.inferior_entry_address () == frame_func_addr); -} - /* Return a structure containing various interesting information about the frame that called THIS_FRAME. Returns NULL if there is either no such frame or the frame fails any of a set of target-independent @@ -2783,11 +2823,10 @@ get_prev_frame (const frame_info_ptr &this_frame) 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)) + && inside_entry_func (this_frame, entry_address_type::executable)) { - frame_debug_got_null_frame (this_frame, "inside entry func"); - return NULL; + frame_debug_got_null_frame (this_frame, "inside executable entry func"); + return nullptr; } /* Assume that the only way to get a zero PC is through something 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 <http://www.gnu.org/licenses/>. */ + +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..3da8bc7222a --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp @@ -0,0 +1,166 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'until' in the outermost frame works as expected. + +require !use_gdb_stub + +standard_testfile + +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + { debug additional_flags=-static } ] } { + return +} + +# Use 'frame' 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 "frame" $testname { + -re -wrap "#0\\s+(?:$::hex in )?(\[^( \]+) \\(.*" { + set func_name $expect_out(1,string) + } + + -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 testfile } { + clean_restart $testfile + + if { [gdb_starti_cmd] < 0 } { + untested starti + return + } + + # Consume up to the first prompt after 'starti' command. + gdb_test "" "" "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" -lbl { + -re "\r\n=> $::hex \[^\r\n\]+(?=\r\n)" { + set capture_address true + exp_continue + } + + -re "\r\n\\s+($::hex) \[^\r\n\]+(?=\r\n)" { + if { $capture_address } { + set until_address $expect_out(1,string) + set capture_address false + } + + exp_continue + } + + -re "\r\n$::gdb_prompt $" { + set found_address [expr { $until_address ne "" }] + if { !$found_address } { + unsupported "$gdb_test_name (no instruction found)" + return + } + pass $gdb_test_name + } + } + + # 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 "\r\n(?:$::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 $testfile + + with_test_prefix "static" { + run_test $use_stepi $past_entry $testfile_static + } + } +} -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv6 3/4] gdb: allow 'until' to work in outermost frame 2026-07-10 14:24 ` [PATCHv6 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-07-10 17:00 ` Simon Marchi 2026-07-10 17:01 ` Simon Marchi 2026-07-16 15:06 ` Andrew Burgess 0 siblings, 2 replies; 54+ messages in thread From: Simon Marchi @ 2026-07-10 17:00 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 7/10/26 10:24 AM, Andrew Burgess wrote: > 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, > if the until breakpoint was not hit. > > 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, where we call > inside_entry_func. This checks if a frame is one of the two possible > entry frames, the inferior entry frame or the executable entry frame. > See the previous commit for more details. The important thing is that > the inferior entry frame is the absolute outer frame, the very first > frame that the inferior executed when starting, while the executable > entry frame is just the first frame within the main executable. > > There are a number of user configurable filters in get_prev_frame, the > backtrace past-main filter, the backtrace frame limit filter, and the > backtrace past-entry filter that we are discussing here. > > When creating the caller frame breakpoint, the 'until' command doesn't > use get_prev_frame, it uses get_prev_frame_always. This is so that > the user configurable filters don't prevent the caller frame > breakpoint from being created. But this means that when creating the > breakpoint, we skip the entry frame check. 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 split the inside_entry_func handling > in to two parts. In get_prev_frame we will retain the check for the > executable entry frame. In theory it is possible that there are > frames between the executable entry frame and the inferior entry > frame. These are the frames the 'backtrace past-entry' can hide or > reveal (if the frames can be discovered). > > The check for the inferior entry frame I propose moving into > get_prev_frame_always. I think this is a better place for it because > any frames before the inferior entry frame are almost certainly > bogus. As such we want to elide those frames even for "inner" use > cases, like the caller frame breakpoint of the 'until' command. > > 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 | 103 +++++++---- > gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ > .../gdb.base/until-in-entry-frame.exp | 166 ++++++++++++++++++ > 3 files changed, 259 insertions(+), 32 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 782ca33abd8..a84cca5b81c 100644 > --- a/gdb/frame.c > +++ b/gdb/frame.c > @@ -2232,6 +2232,63 @@ frame_register_unwind_location (const frame_info_ptr &initial_this_frame, > } > } > > +/* When checking if a frame is an entry frame, there are two different > + entry frames to consider. This enum is used to choose between them. */ > + > +enum class entry_address_type > +{ > + /* The executable's entry frame. This is the first frame within the main > + executable. */ > + executable, > + > + /* The inferior's entry frame. This is the first frame within the > + inferior, this can be outside the main executable, e.g. for a > + dynamically linked executable, this will be the first frame in the > + dynamic linker. */ > + inferior, > +}; > + > +/* Test whether THIS_FRAME is inside the TYPE process entry point > + function. */ > + > +static bool > +inside_entry_func (const frame_info_ptr &this_frame, > + entry_address_type type) > +{ > + const program_space::entry_point_info &ep_info > + = current_program_space->get_entry_point_info (); > + > + CORE_ADDR frame_func_addr; > + if (!get_frame_func_if_available (this_frame, &frame_func_addr)) > + return false; > + > + switch (type) > + { > + case entry_address_type::executable: > + return ep_info.exec_entry_address () == frame_func_addr; > + > + case entry_address_type::inferior: > + return ep_info.inferior_entry_address () == frame_func_addr; > + } > + > + gdb_assert_not_reached ("unknown entry address type: %d", ((int) type)); > +} Given that this code path allows reading just one of the two entry address types, it would perhaps be good to by pass program_spaceget_entry_point_info (which returns both), and just go fetch the one that is requested. The other one will inevitably be wasted. Not a big deal if both branches are reasonably fast to fetch, but still it shouldn't be difficult to fetch just the right one. > + > +/* Debug routine to print a NULL frame being returned. */ > + > +static void > +frame_debug_got_null_frame (const frame_info_ptr &this_frame, > + const char *reason) > +{ > + if (frame_debug) > + { > + if (this_frame != NULL) > + frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); > + else > + frame_debug_printf ("this_frame=nullptr -> %s", reason); > + } > +} > + > /* Get the previous raw frame, and check that it is not identical to > same other frame frame already in the chain. If it is, there is > most likely a stack cycle, so we discard it, and mark THIS_FRAME as > @@ -2538,6 +2595,17 @@ get_prev_frame_always_1 (const frame_info_ptr &this_frame) > frame_info_ptr > get_prev_frame_always (const frame_info_ptr &this_frame) > { > + if (this_frame->level >= 0 > + && get_frame_type (this_frame) == NORMAL_FRAME > + && !user_set_backtrace_options.backtrace_past_entry > + && inside_entry_func (this_frame, entry_address_type::inferior)) get_prev_frame_always is called quite often, inside_entry_func is going to be called quite often (the other parts of the conjunction are often going to be true), inside_entry_func calls program_space::get_entry_point_info, which calls solib_ops::inferior_entry_point_address. svr4_solib_ops::inferior_entry_point_address is somewhat heavy (ELF parsing and all). I wonder if the inferior entry point should be cached, as it's not going to change often. I put a printf in svr4_solib_ops::inferior_entry_point_address, attached to gnome-calculator (with debuginfod, so debug info for all the libs) and ran a backtrace. For 10 frames, svr4_solib_ops::inferior_entry_point_address was called 242 times. Perhaps it's premature optimization, but it just feels wrong. > + { > + this_frame->prev_p = true; > + this_frame->stop_reason = UNWIND_OUTERMOST; > + frame_debug_got_null_frame (this_frame, "inside inferior entry func"); > + return nullptr; > + } I notice that this makes get_prev_frame_always modify a persistent state (stop_reason) of `this_frame` based on a setting that could change from one call to another (options.backtrace_past_entry). For instance, what happens to "bt -past-entry" after this runs? > static bool > @@ -2689,19 +2742,6 @@ inside_main_func (const frame_info_ptr &this_frame) > return sym_addr == get_frame_func (this_frame); > } > > -/* Test whether THIS_FRAME is inside the process entry point function. */ > - > -static bool > -inside_entry_func (const frame_info_ptr &this_frame) > -{ > - const program_space::entry_point_info &ep_info > - = current_program_space->get_entry_point_info (); > - > - CORE_ADDR frame_func_addr = get_frame_func (this_frame); > - return (ep_info.exec_entry_address () == frame_func_addr > - || ep_info.inferior_entry_address () == frame_func_addr); > -} > - > /* Return a structure containing various interesting information about > the frame that called THIS_FRAME. Returns NULL if there is either > no such frame or the frame fails any of a set of target-independent > @@ -2783,11 +2823,10 @@ get_prev_frame (const frame_info_ptr &this_frame) > if (this_frame->level >= 0 > && get_frame_type (this_frame) == NORMAL_FRAME > && !user_set_backtrace_options.backtrace_past_entry > - && frame_pc.has_value () Can you explain the removal of this frame_pc.has_value() line? Probably fine, but I want to be sure. Simon ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv6 3/4] gdb: allow 'until' to work in outermost frame 2026-07-10 17:00 ` Simon Marchi @ 2026-07-10 17:01 ` Simon Marchi 2026-07-16 15:06 ` Andrew Burgess 1 sibling, 0 replies; 54+ messages in thread From: Simon Marchi @ 2026-07-10 17:01 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 7/10/26 1:00 PM, Simon Marchi wrote: > get_prev_frame_always is called quite often, inside_entry_func is going > to be called quite often (the other parts of the conjunction are often > going to be true), inside_entry_func calls > program_space::get_entry_point_info, which calls > solib_ops::inferior_entry_point_address. > svr4_solib_ops::inferior_entry_point_address is somewhat heavy (ELF > parsing and all). I wonder if the inferior entry point should be > cached, as it's not going to change often. > > I put a printf in svr4_solib_ops::inferior_entry_point_address, attached > to gnome-calculator (with debuginfod, so debug info for all the libs) > and ran a backtrace. For 10 frames, > svr4_solib_ops::inferior_entry_point_address was called 242 times. > > Perhaps it's premature optimization, but it just feels wrong. Ah well... I hadn't read the next patch yet. Simon ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv6 3/4] gdb: allow 'until' to work in outermost frame 2026-07-10 17:00 ` Simon Marchi 2026-07-10 17:01 ` Simon Marchi @ 2026-07-16 15:06 ` Andrew Burgess 1 sibling, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-16 15:06 UTC (permalink / raw) To: Simon Marchi, gdb-patches Simon Marchi <simark@simark.ca> writes: > On 7/10/26 10:24 AM, Andrew Burgess wrote: >> 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, >> if the until breakpoint was not hit. >> >> 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, where we call >> inside_entry_func. This checks if a frame is one of the two possible >> entry frames, the inferior entry frame or the executable entry frame. >> See the previous commit for more details. The important thing is that >> the inferior entry frame is the absolute outer frame, the very first >> frame that the inferior executed when starting, while the executable >> entry frame is just the first frame within the main executable. >> >> There are a number of user configurable filters in get_prev_frame, the >> backtrace past-main filter, the backtrace frame limit filter, and the >> backtrace past-entry filter that we are discussing here. >> >> When creating the caller frame breakpoint, the 'until' command doesn't >> use get_prev_frame, it uses get_prev_frame_always. This is so that >> the user configurable filters don't prevent the caller frame >> breakpoint from being created. But this means that when creating the >> breakpoint, we skip the entry frame check. 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 split the inside_entry_func handling >> in to two parts. In get_prev_frame we will retain the check for the >> executable entry frame. In theory it is possible that there are >> frames between the executable entry frame and the inferior entry >> frame. These are the frames the 'backtrace past-entry' can hide or >> reveal (if the frames can be discovered). >> >> The check for the inferior entry frame I propose moving into >> get_prev_frame_always. I think this is a better place for it because >> any frames before the inferior entry frame are almost certainly >> bogus. As such we want to elide those frames even for "inner" use >> cases, like the caller frame breakpoint of the 'until' command. >> >> 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 | 103 +++++++---- >> gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ >> .../gdb.base/until-in-entry-frame.exp | 166 ++++++++++++++++++ >> 3 files changed, 259 insertions(+), 32 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 782ca33abd8..a84cca5b81c 100644 >> --- a/gdb/frame.c >> +++ b/gdb/frame.c >> @@ -2232,6 +2232,63 @@ frame_register_unwind_location (const frame_info_ptr &initial_this_frame, >> } >> } >> >> +/* When checking if a frame is an entry frame, there are two different >> + entry frames to consider. This enum is used to choose between them. */ >> + >> +enum class entry_address_type >> +{ >> + /* The executable's entry frame. This is the first frame within the main >> + executable. */ >> + executable, >> + >> + /* The inferior's entry frame. This is the first frame within the >> + inferior, this can be outside the main executable, e.g. for a >> + dynamically linked executable, this will be the first frame in the >> + dynamic linker. */ >> + inferior, >> +}; >> + >> +/* Test whether THIS_FRAME is inside the TYPE process entry point >> + function. */ >> + >> +static bool >> +inside_entry_func (const frame_info_ptr &this_frame, >> + entry_address_type type) >> +{ >> + const program_space::entry_point_info &ep_info >> + = current_program_space->get_entry_point_info (); >> + >> + CORE_ADDR frame_func_addr; >> + if (!get_frame_func_if_available (this_frame, &frame_func_addr)) >> + return false; >> + >> + switch (type) >> + { >> + case entry_address_type::executable: >> + return ep_info.exec_entry_address () == frame_func_addr; >> + >> + case entry_address_type::inferior: >> + return ep_info.inferior_entry_address () == frame_func_addr; >> + } >> + >> + gdb_assert_not_reached ("unknown entry address type: %d", ((int) type)); >> +} > > Given that this code path allows reading just one of the two entry > address types, it would perhaps be good to by pass > program_spaceget_entry_point_info (which returns both), and just go > fetch the one that is requested. The other one will inevitably be > wasted. Not a big deal if both branches are reasonably fast to fetch, > but still it shouldn't be difficult to fetch just the right one. The reason I didn't do this is the caching added in the next patch. I currently cache the whole entry_point_info structure. I could push that caching down and cache each field of the entry_point_info separately, then we can fetch each field. But as it currently stands with this complete series (including the next patch), we pay a price the first time inside_entry_func is called, but after that fetching the entry_point_info is pretty cheap. Also, we always end up calling inside_entry_func twice, once for each entry address that it can check, so the "price" we pay isn't wasted, it just means the first call has to prime the cache, then we're good. > >> + >> +/* Debug routine to print a NULL frame being returned. */ >> + >> +static void >> +frame_debug_got_null_frame (const frame_info_ptr &this_frame, >> + const char *reason) >> +{ >> + if (frame_debug) >> + { >> + if (this_frame != NULL) >> + frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason); >> + else >> + frame_debug_printf ("this_frame=nullptr -> %s", reason); >> + } >> +} >> + >> /* Get the previous raw frame, and check that it is not identical to >> same other frame frame already in the chain. If it is, there is >> most likely a stack cycle, so we discard it, and mark THIS_FRAME as >> @@ -2538,6 +2595,17 @@ get_prev_frame_always_1 (const frame_info_ptr &this_frame) >> frame_info_ptr >> get_prev_frame_always (const frame_info_ptr &this_frame) >> { >> + if (this_frame->level >= 0 >> + && get_frame_type (this_frame) == NORMAL_FRAME >> + && !user_set_backtrace_options.backtrace_past_entry >> + && inside_entry_func (this_frame, entry_address_type::inferior)) > > get_prev_frame_always is called quite often, inside_entry_func is going > to be called quite often (the other parts of the conjunction are often > going to be true), inside_entry_func calls > program_space::get_entry_point_info, which calls > solib_ops::inferior_entry_point_address. > svr4_solib_ops::inferior_entry_point_address is somewhat heavy (ELF > parsing and all). I wonder if the inferior entry point should be > cached, as it's not going to change often. > > I put a printf in svr4_solib_ops::inferior_entry_point_address, attached > to gnome-calculator (with debuginfod, so debug info for all the libs) > and ran a backtrace. For 10 frames, > svr4_solib_ops::inferior_entry_point_address was called 242 times. > > Perhaps it's premature optimization, but it just feels wrong. As you spotted, patch #4 adds the caching. I'll add a sentence to the commit message to make it clear that sub-optimal things in this patch should be resolved by the next one. > >> + { >> + this_frame->prev_p = true; >> + this_frame->stop_reason = UNWIND_OUTERMOST; >> + frame_debug_got_null_frame (this_frame, "inside inferior entry func"); >> + return nullptr; >> + } > > I notice that this makes get_prev_frame_always modify a persistent > state (stop_reason) of `this_frame` based on a setting that could change > from one call to another (options.backtrace_past_entry). For instance, > what happens to "bt -past-entry" after this runs? I got rid of this and just return NULL, which is what the entry frame detection in get_prev_frame does, and that works just fine too. I also extended the test in the previous commit to test 'bt -past-entry', which continues to pass after this patch has been applied. > >> static bool >> @@ -2689,19 +2742,6 @@ inside_main_func (const frame_info_ptr &this_frame) >> return sym_addr == get_frame_func (this_frame); >> } >> >> -/* Test whether THIS_FRAME is inside the process entry point function. */ >> - >> -static bool >> -inside_entry_func (const frame_info_ptr &this_frame) >> -{ >> - const program_space::entry_point_info &ep_info >> - = current_program_space->get_entry_point_info (); >> - >> - CORE_ADDR frame_func_addr = get_frame_func (this_frame); >> - return (ep_info.exec_entry_address () == frame_func_addr >> - || ep_info.inferior_entry_address () == frame_func_addr); >> -} >> - >> /* Return a structure containing various interesting information about >> the frame that called THIS_FRAME. Returns NULL if there is either >> no such frame or the frame fails any of a set of target-independent >> @@ -2783,11 +2823,10 @@ get_prev_frame (const frame_info_ptr &this_frame) >> if (this_frame->level >= 0 >> && get_frame_type (this_frame) == NORMAL_FRAME >> && !user_set_backtrace_options.backtrace_past_entry >> - && frame_pc.has_value () > > Can you explain the removal of this frame_pc.has_value() line? Probably > fine, but I want to be sure. It seemed redundant given that `inside_entry_func`, which is also called by this `if` check uses `get_frame_func_if_available`, which will cover the same logic. But looking again, I think there are multiple instances of this `frame_pc.has_value()` that could be removed, so this is probably better done in a separate commit. As such, I've added this check back in for now. I'm just re-running the tests, but will post a v7 shortly. Thanks, Andrew ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv6 4/4] gdb: cache program space entry point information 2026-07-10 14:24 ` [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 preceding siblings ...) 2026-07-10 14:24 ` [PATCHv6 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-07-10 14:24 ` Andrew Burgess 2026-07-10 17:07 ` Simon Marchi 2026-07-18 13:11 ` [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 1 reply; 54+ messages in thread From: Andrew Burgess @ 2026-07-10 14:24 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess After the previous two patches, every time that GDB unwinds a frame there are now two places where we check if the frame is an entry point frame, these are in get_prev_frame and get_prev_frame_always, look for the calls to `inside_entry_func`, which then calls program_space::get_entry_point_info. The calls to program_space::get_entry_point_info are not crazy expensive, but they are not free either, there are reads from target memory to read the auxv vector and the entry address offset, so on remote targets this could introduce a small delay, especially as this occurs twice per frame now. However, the information returned by program_space::get_entry_point_info is not expected to change from one call to the next. This information should be a property of the executable and libraries, so we really only need to figure it out once. This commit caches the entry point information within program_space, clearing it whenever an inferior starts or exits, or whenever the executable is updated. After clearing GDB will compute, and cache the updated information the next time it is needed, which will be whenever GDB needs to unwind a frame. It will be possible to observe this change by, for example, monitoring the remote target packets, but as far as the normal GDB output is concerned, there should be no user visible changes after this commit. --- gdb/progspace.c | 34 +++++++++++++++++++++++++++++++--- gdb/progspace.h | 9 ++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/gdb/progspace.c b/gdb/progspace.c index 26e16cae8d0..c1f3044f4c5 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -289,9 +289,12 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ -program_space::entry_point_info +const program_space::entry_point_info & program_space::get_entry_point_info () const { + if (m_entry_point_info.has_value ()) + return m_entry_point_info.value (); + std::optional<CORE_ADDR> exec_entry_address = this->exec_entry_point_address_if_available (); @@ -299,8 +302,9 @@ program_space::get_entry_point_info () const if (m_solib_ops != nullptr) inferior_entry_address = m_solib_ops->inferior_entry_point_address (); - return entry_point_info (std::move (inferior_entry_address), - std::move (exec_entry_address)); + m_entry_point_info.emplace (std::move (inferior_entry_address), + std::move (exec_entry_address)); + return m_entry_point_info.value (); } /* Implement the 'maint info entry-address' command. */ @@ -538,11 +542,35 @@ program_space::clear_solib_cache () deleted_solibs.clear (); } +/* Clear cached entry point information in the program space of INF. */ + +static void +clear_cached_entry_point_info_for_inferior (inferior *inf) +{ + inf->pspace->clear_cached_entry_point_info (); +} + +/* Clear cached entry point information in the program space PSPACE. */ + +static void +clear_cached_entry_point_info_for_pspace (program_space *pspace, + bool /* reload */) +{ + pspace->clear_cached_entry_point_info (); +} + /* See progspace.h. */ void initialize_progspace () { + gdb::observers::inferior_created.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::inferior_exit.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::executable_changed.attach + (clear_cached_entry_point_info_for_pspace, "program-space"); + add_cmd ("program-spaces", class_maintenance, maintenance_info_program_spaces_command, _("Info about currently known program spaces."), diff --git a/gdb/progspace.h b/gdb/progspace.h index d75379fe621..407d2595136 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -369,7 +369,11 @@ struct program_space /* Return information about the entry point in the main executable, and the entry point for the inferior, which might be different from the main executable. */ - entry_point_info get_entry_point_info () const; + const entry_point_info &get_entry_point_info () const; + + /* Clear any cached entry point information. */ + void clear_cached_entry_point_info () + { m_entry_point_info.reset (); } /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ @@ -462,6 +466,9 @@ struct program_space /* See `exec_filename`. */ gdb::unique_xmalloc_ptr<char> m_exec_filename; + + /* Cached entry point information. See get_entry_point_info. */ + mutable std::optional<entry_point_info> m_entry_point_info; }; /* The list of all program spaces. There's always at least one. */ -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCHv6 4/4] gdb: cache program space entry point information 2026-07-10 14:24 ` [PATCHv6 4/4] gdb: cache program space entry point information Andrew Burgess @ 2026-07-10 17:07 ` Simon Marchi 0 siblings, 0 replies; 54+ messages in thread From: Simon Marchi @ 2026-07-10 17:07 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 7/10/26 10:24 AM, Andrew Burgess wrote: > void > initialize_progspace () > { > + gdb::observers::inferior_created.attach > + (clear_cached_entry_point_info_for_inferior, "program-space"); > + gdb::observers::inferior_exit.attach > + (clear_cached_entry_point_info_for_inferior, "program-space"); > + gdb::observers::executable_changed.attach > + (clear_cached_entry_point_info_for_pspace, "program-space"); inferior_execd too? Simon ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame 2026-07-10 14:24 ` [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (3 preceding siblings ...) 2026-07-10 14:24 ` [PATCHv6 4/4] gdb: cache program space entry point information Andrew Burgess @ 2026-07-18 13:11 ` Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess ` (4 more replies) 4 siblings, 5 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-18 13:11 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess In v7: - Addressed Simon's feedback for patch #2. Changes are all minor, and this patch now has Simon's Approved-By tag. - While addressing feedback on patch #3 I realised that the approach taken wouldn't work. Simon pointed out that setting the frame stop reason was incorrect, but removing that ended up triggering some assertion failures. As a result I had to take a different approach with this patch. This is the patch that changed the most. - Commit message for patch #4 is updated after changes to patch #3. - Some improvements to testing in both #2 and #3. All pretty minor though. - Rebased to HEAD of master branch and retested. In v6: - More adjustments to the test in patch #2 to address issues the Linaro CI testing highlighted. Nothing serious, just minor tweaks to some patterns to handle different output patterns. - Rebased to HEAD. In v5: - Update to test in patch #2 to allow the test to work on machines that build PIE by default. I actually extended the test to build both PIE and non-PIE executables, and test with both. In v4: - Rebase to current HEAD of master. - In patch #2, added a supports_process_entry_point proc as suggested by Pedro, and make use of this in the new test. Updates to address review comments from v2 series: - Doc updates based on Eli's feedback. - Patch #2: + Avoid swallowing Ctrl-C in try/catch. That whole try/catch block has been removed in v3, so problem sorted! + Rewrote entry address calculation as suggested by Pedro, now pulls all required information from the inferior. + Use 'objdump -f' rather than readelf to get the entry address in the test. + Added a helper proc for is_svr4_target, which includes a larger list of targets beyond just Linux. This is then used in the test. + Test updated to use -lbl flag in gdb_test_multiple. - Patch #3: + Use 'frame' rather than 'bt 1' + Loosen the test regexp after the 'starti' command, this should now work on Windows targets, or any target with multiple threads at startup. + Test updated to use -lbl in gdb_test_multiple. - In patch #3 the entry frame detection is moved from `frame_unwind_caller_frame` into `get_prev_frame_always`, I hope this will address Guinevere's concerns about having to add the additional entry frame detection in multiple locations. Guinevere also had some concerns about the naming of existing functions (i.e. not things added by me in this commit), I don't plan to make any naming changes to the get_prev_frame* functions in this series. - Patch #4 is new, this adds some caching to avoid repeatedly looking up the entry address. The new approach requires reading target memory, which can be expensive, especially on remote targets. --- Andrew Burgess (4): gdb: rename program_space::entry_point_address* functions gdb: introduce program_space::get_entry_point_info function gdb: allow 'until' to work in outermost frame gdb: cache program space entry point information gdb/NEWS | 7 + gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/doc/gdb.texinfo | 22 ++ gdb/frame.c | 74 +++-- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 105 +++++++- gdb/progspace.h | 68 ++++- gdb/solib-frv.c | 2 +- gdb/solib-svr4.c | 85 ++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 + gdb/testsuite/gdb.base/bt-after-starti.exp | 253 ++++++++++++++++++ gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 ++ .../gdb.base/until-in-entry-frame.exp | 173 ++++++++++++ gdb/testsuite/lib/gdb.exp | 39 +++ 17 files changed, 844 insertions(+), 29 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.c create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.exp base-commit: ca8bf5f5dd69eba877e74ca8bc0796388070401f -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv7 1/4] gdb: rename program_space::entry_point_address* functions 2026-07-18 13:11 ` [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-07-18 13:11 ` Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess ` (3 subsequent siblings) 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-18 13:11 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves Rename program_space::entry_point_address to program_space::exec_entry_point_address and program_space::entry_point_address_query to program_space::exec_entry_point_address_if_available. There are two aspects to this renaming. First I replace 'query' with 'if_available' in one of the functions. I feel this better describes the function, and also is inline with how other, similar, functions are named in GDB. The second part of the renaming is to add the 'exec_' prefix to the front of both function names. When a dynamically linked inferior is started the first instruction executed is actually within the run-time linker, not within the main executable, so it could be argued that the actual entry address for the inferior is not the entry address of the main executable. However, there is an equally valid argument that the entry address of the executable is also something worth finding. The existing entry address within the inferior is used for a number of tasks in GDB, for example displaced stepping (arch-utils.c), inferior function calls (arc-tdep.c and infcall.c), and for detecting the "entry" frame. This last one, the entry frame detection is interesting. In a dynamically linked executable it could be argued that there are two entry frames. The very first frame that is executed in the inferior, this is where 'starti' stops the inferior. And then the very first frame within the main executable. I think that both of these are valid. Currently, as we can only find the entry address within the main executable, we can only identify the first frame of the main executable. And this can cause some problems, consider: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) Here frames #1 to #3 are all bogus, created by GDB based on whatever values happen to be in the registers when the inferior starts. In a later commit I'd like to fix this problem, however, I would prefer that the other users of program_space::entry_point_address continue to use the address within the main executable. So, in order to keep the distinction between the two different types of entry point, I'm renaming the existing functions with the 'exec_' prefix. There should be no user visible changes after this commit. Approved-By: Pedro Alves <pedro@palves.net> --- gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/frame.c | 4 ++-- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 6 +++--- gdb/progspace.h | 12 ++++++------ gdb/solib-frv.c | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gdb/arc-tdep.c b/gdb/arc-tdep.c index 4936a5c8fbb..7ed83c4cae6 100644 --- a/gdb/arc-tdep.c +++ b/gdb/arc-tdep.c @@ -860,7 +860,7 @@ arc_push_dummy_code (struct gdbarch *gdbarch, CORE_ADDR sp, CORE_ADDR funaddr, struct regcache *regcache) { *real_pc = funaddr; - *bp_addr = current_program_space->entry_point_address (); + *bp_addr = current_program_space->exec_entry_point_address (); return sp; } diff --git a/gdb/arch-utils.c b/gdb/arch-utils.c index e959788bd3b..88047731ce6 100644 --- a/gdb/arch-utils.c +++ b/gdb/arch-utils.c @@ -57,7 +57,7 @@ displaced_step_at_entry_point (struct gdbarch *gdbarch) CORE_ADDR addr; int bp_len; - addr = current_program_space->entry_point_address (); + addr = current_program_space->exec_entry_point_address (); /* Inferior calls also use the entry point as a breakpoint location. We don't want displaced stepping to interfere with those diff --git a/gdb/frame.c b/gdb/frame.c index b91e18fad99..a83f0b20686 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2704,7 +2704,7 @@ static bool inside_entry_func (const frame_info_ptr &this_frame) { std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) return false; @@ -2780,7 +2780,7 @@ get_prev_frame (const frame_info_ptr &this_frame) added to work around that (now fixed) case. */ /* NOTE: cagney/2003-07-15: danielj (if I'm reading it right) suggested having the inside_entry_func test use the - inside_main_func() msymbol trick (along with entry_point_address() + inside_main_func() msymbol trick (along with exec_entry_point_address() I guess) to determine the address range of the start function. That should provide a far better stopper than the current heuristics. */ diff --git a/gdb/infcall.c b/gdb/infcall.c index e6b24ff5310..6d26841ede3 100644 --- a/gdb/infcall.c +++ b/gdb/infcall.c @@ -1300,7 +1300,7 @@ call_function_by_hand_dummy (struct value *function, CORE_ADDR dummy_addr; real_pc = funaddr; - dummy_addr = current_program_space->entry_point_address (); + dummy_addr = current_program_space->exec_entry_point_address (); /* A call dummy always consists of just a single breakpoint, so its address is the same as the address of the dummy. diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c index 25d625db595..ea88f35ff5c 100644 --- a/gdb/linux-tdep.c +++ b/gdb/linux-tdep.c @@ -2952,7 +2952,7 @@ linux_displaced_step_location (struct gdbarch *gdbarch) /* Determine entry point from target auxiliary vector. This avoids the need for symbols. Also, when debugging a stand-alone SPU - executable, entry_point_address () will point to an SPU + executable, exec_entry_point_address () will point to an SPU local-store address and is thus not usable as displaced stepping location. The auxiliary vector gets us the PowerPC-side entry point address instead. */ diff --git a/gdb/progspace.c b/gdb/progspace.c index 1407b058dfd..0e8516f4640 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -263,7 +263,7 @@ program_space::empty () /* See progspace.h. */ std::optional<CORE_ADDR> -program_space::entry_point_address_query () const +program_space::exec_entry_point_address_if_available () const { objfile *objf = symfile_object_file; if (objf == NULL || !objf->per_bfd->ei.entry_point_p) @@ -276,9 +276,9 @@ program_space::entry_point_address_query () const /* See progspace.h. */ CORE_ADDR -program_space::entry_point_address () const +program_space::exec_entry_point_address () const { - std::optional<CORE_ADDR> retval = entry_point_address_query (); + std::optional<CORE_ADDR> retval = exec_entry_point_address_if_available (); if (!retval.has_value ()) error (_("Entry point address is not known.")); diff --git a/gdb/progspace.h b/gdb/progspace.h index e9261ff8590..1ae1e42f3bb 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,13 +323,13 @@ struct program_space return m_target_sections; } - /* If there is a valid and known entry point in this program space, - return it. Otherwise return an empty optional. */ - std::optional<CORE_ADDR> entry_point_address_query () const; + /* If there is a valid and known entry point in the main executable of + this program space, return it. Otherwise return an empty optional. */ + std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; - /* Get the entry point address in this program space. Call error if - it is not known. */ - CORE_ADDR entry_point_address () const; + /* Get the entry point address for the main executable in this program + space. Call error if it is not known. */ + CORE_ADDR exec_entry_point_address () const; /* Return true if any objfile of this program space has partial symbols. */ diff --git a/gdb/solib-frv.c b/gdb/solib-frv.c index 08ee0a9578f..4284a711860 100644 --- a/gdb/solib-frv.c +++ b/gdb/solib-frv.c @@ -689,7 +689,7 @@ enable_break (void) } std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) { solib_debug_printf ("Symbol file has no entry point."); -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv7 2/4] gdb: introduce program_space::get_entry_point_info function 2026-07-18 13:11 ` [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess @ 2026-07-18 13:11 ` Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 subsequent siblings) 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-18 13:11 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Eli Zaretskii, Simon Marchi I noticed that when debugging a dynamically linked executable, if I started the inferior with 'starti' then used 'bt' I would see some bogus frames: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) This surprised me as 'backtrace past-entry' was off: (gdb) show backtrace past-entry Whether backtraces should continue past the entry point of a program is off. I was expecting GDB to stop the backtrace at the inferior's entry address. Frame unwinding starts in get_prev_frame, and in here we find this block: 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)) { frame_debug_got_null_frame (this_frame, "inside entry func"); return NULL; } Which uses inside_entry_func to terminate the backtrace when we reach the entry frame. The inside_entry_func function calls current_program_space->exec_entry_point_address_if_available and uses the result to figure out if we are in the entry frame. And here the problem becomes obvious, we are only checking if we are in the "entry frame" for the main executable, not for the inferior as a whole. And indeed, if I compile the same test program as a static binary, where there will be no run-time linker, and the entry point of the executable is the entry point for the inferior, then the 'bt' problem I saw above goes away. This suggests, I think, that we need to track two different entry addresses, the entry address for the executable file, and the entry address for the entire inferior. Then, for dynamically linked executables, these two addresses can be different, the former will still be the same address within the main executable file, while the latter will be the address of the entry point within the run-time linker. If we had this information then we could extend inside_entry_func to check both addresses, and the 'bt' problem seen above will be resolved. To make this information available I added a new solib_ops method, solib_ops::inferior_entry_point_address, for svr4 targets this figures out if the main executable is dynamically linked, and if it is, uses the AT_BASE auxv entry and the entry address pulled from the ELF header to compute the inferior entry address. I then added program_space::get_entry_point_info, which returns a struct containing the two entry point addresses, one comes from the new solib_ops method, and one comes from the existing method program_space::exec_entry_point_address_if_available. With the infrastructure in place I can then update inside_entry_func to check against both entry addresses. To aid in debugging GDB, I added a new maintenance command: maintenance info entry-address which just calls program_space::get_entry_point_info and then prints the two addresses. I left this as a maintenance command as I don't see much user utility in this right now, but it made it easier for me to see what GDB was doing, so I left the command in this commit. Reviewed-By: Eli Zaretskii <eliz@gnu.org> Approved-By: Simon Marchi <simon.marchi@efficios.com> --- gdb/NEWS | 7 + gdb/doc/gdb.texinfo | 22 ++ gdb/frame.c | 10 +- gdb/progspace.c | 60 +++++ gdb/progspace.h | 49 ++++ gdb/solib-svr4.c | 85 +++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 253 +++++++++++++++++++++ gdb/testsuite/lib/gdb.exp | 39 ++++ 10 files changed, 535 insertions(+), 5 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp diff --git a/gdb/NEWS b/gdb/NEWS index 8f40ca5cb11..9c51f476c84 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -179,6 +179,13 @@ disable skip These are new aliases for 'skip delete', 'skip enable', and 'skip disable' respectively. +maint info entry-address + Display the inferior and main executable entry addresses for the + current inferior. The inferior entry address is the address of the + first instruction in the inferior that was executed. The main + executable entry address is the address of the first instruction in + the main executable that will be executed. + * MI changes ** The "-trace-save" command no longer supports the "-ctf" flag. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index 0030698dcee..35fb9f78b08 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -43260,6 +43260,28 @@ Maintenance Commands Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M @end smallexample + +@kindex maint info entry-address +@item maint info entry-address +Display the entry addresses for the currently selected inferior. Two +addresses are displayed, the inferior entry address and the executable +entry address. + +Neither address is the address of @code{main}. When a program is +started, low-level startup code (typically a function called +@code{_start}) runs before @code{main} is called. + +The executable entry address is the address of this startup code +within the main executable (@pxref{Files, ,Commands to Specify +Files}). + +The inferior entry address is the address of the very first +instruction executed when the inferior is started. For statically +linked programs this is the same as the executable entry address. For +dynamically linked programs the run-time linker must execute first in +order to load shared libraries, so the inferior entry address will be +an address within the run-time linker rather than within the main +executable. @end table The following command is useful for non-interactive invocations of diff --git a/gdb/frame.c b/gdb/frame.c index a83f0b20686..e61bfd8d1f7 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2703,12 +2703,12 @@ inside_main_func (const frame_info_ptr &this_frame) static bool inside_entry_func (const frame_info_ptr &this_frame) { - std::optional<CORE_ADDR> entry_point - = current_program_space->exec_entry_point_address_if_available (); - if (!entry_point.has_value ()) - return false; + const entry_point_info &ep_info + = current_program_space->get_entry_point_info (); - return get_frame_func (this_frame) == *entry_point; + CORE_ADDR frame_func_addr = get_frame_func (this_frame); + return (ep_info.exec_entry_address () == frame_func_addr + || ep_info.inferior_entry_address () == frame_func_addr); } /* Return a structure containing various interesting information about diff --git a/gdb/progspace.c b/gdb/progspace.c index 0e8516f4640..4de32bc522f 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -26,6 +26,7 @@ #include <algorithm> #include "cli/cli-style.h" #include "observable.h" +#include "arch-utils.h" /* The last program space number assigned. */ static int last_program_space_num = 0; @@ -288,6 +289,55 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ +entry_point_info +program_space::get_entry_point_info () const +{ + std::optional<CORE_ADDR> exec_entry_address + = this->exec_entry_point_address_if_available (); + + std::optional<CORE_ADDR> inferior_entry_address; + if (m_solib_ops != nullptr) + inferior_entry_address = m_solib_ops->inferior_entry_point_address (); + + return entry_point_info (std::move (inferior_entry_address), + std::move (exec_entry_address)); +} + +/* Implement the 'maint info entry-address' command. */ + +static void +maintenance_info_entry_address (const char *args, int from_tty) +{ + if (args != nullptr && *args != '\0') + error (_("unknown argument: %s"), args); + + struct gdbarch *gdbarch = get_current_arch (); + + const entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + /* Display a single entry address ADDR with TITLE as a description. */ + auto display_entry_address + = [&gdbarch] (const char *title, + const std::optional<CORE_ADDR> &addr) -> void + { + gdb_puts (title); + if (addr.has_value ()) + fputs_styled (paddress (gdbarch, addr.value ()), + address_style.style (), gdb_stdout); + else + fputs_styled ("<unknown>", metadata_style.style (), gdb_stdout); + gdb_puts ("\n"); + }; + + display_entry_address (_("Inferior entry address: "), + ep_info.inferior_entry_address ()); + display_entry_address (_("Executable entry address: "), + ep_info.exec_entry_address ()); +} + +/* See progspace.h. */ + bool program_space::has_partial_symbols () { @@ -498,6 +548,16 @@ initialize_progspace () _("Info about currently known program spaces."), &maintenanceinfolist); + add_cmd ("entry-address", class_maintenance, + maintenance_info_entry_address, + _("Information about the current inferior's entry addresses.\n\ +Display the address of the first instruction executed within the\n\ +inferior, and the first instruction executed within the main executable.\n\ +The entry address of the whole inferior will be <unknown> prior to the\n\ +inferior starting, and either or both addresses can be <unknown> if\n\ +GDB is unable to find the required information."), + &maintenanceinfolist); + /* There's always one program space. Note that this function isn't an automatic _initialize_foo function, since other _initialize_foo routines may need to install their per-pspace diff --git a/gdb/progspace.h b/gdb/progspace.h index 1ae1e42f3bb..be2d3f7e88c 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -67,6 +67,50 @@ new_address_space () return address_space_ref_ptr::new_reference (new address_space); } +/* Class for tracking two possible entry points that an inferior might + have, the entry point for the entire inferior, and the entry point + within the main executable. */ + +struct entry_point_info +{ + entry_point_info (std::optional<CORE_ADDR> inferior_entry_address, + std::optional<CORE_ADDR> exec_entry_address) + : m_inferior_entry_address (inferior_entry_address), + m_exec_entry_address (exec_entry_address) + { /* Nothing. */ } + + DISABLE_COPY_AND_ASSIGN (entry_point_info); + + /* The entry address within the inferior as a whole. See the member + variable definition below for more details. */ + std::optional<CORE_ADDR> inferior_entry_address () const + { + return m_inferior_entry_address; + } + + /* The entry address within the main executable. See the member + variable definition below for more details. */ + std::optional<CORE_ADDR> exec_entry_address () const + { + return m_exec_entry_address; + } + +private: + /* The entry address within the inferior. This can be outside of the + main executable, e.g. for dynamically linked executables this could + be the entry address for the run-time linker. For statically linked + executables this will be the entry address of the main executable. + This can be empty if GDB doesn't know how to figure out the correct + entry address for any reason. */ + std::optional<CORE_ADDR> m_inferior_entry_address; + + /* The entry address within the main executable. This can be empty if + the main executable is not set yet, or GDB doesn't have an objfile + associated with the main executable, e.g. in some attach, or remote + debug cases. */ + std::optional<CORE_ADDR> m_exec_entry_address; +}; + /* A program space represents a symbolic view of an address space. Roughly speaking, it holds all the data associated with a non-running-yet program (main executable, main symbols), and when @@ -323,6 +367,11 @@ struct program_space return m_target_sections; } + /* Return information about the entry point in the main executable, and + the entry point for the inferior, which might be different from the + main executable. */ + entry_point_info get_entry_point_info () const; + /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c index 8e3de4d3ea1..12f3e783744 100644 --- a/gdb/solib-svr4.c +++ b/gdb/solib-svr4.c @@ -3777,6 +3777,91 @@ svr4_solib_ops::get_solibs_in_ns (int nsid) const return ns_solibs; } +/* See solib.h. */ + +std::optional<CORE_ADDR> +svr4_solib_ops::inferior_entry_point_address () const +{ + std::optional<gdb::byte_vector> interp_name_holder + = svr4_find_program_interpreter (); + + /* No interpreter means this is a static executable. Ask the + program_space for the entry address within the main executable. */ + if (!interp_name_holder.has_value ()) + return m_pspace->exec_entry_point_address_if_available (); + + /* For a dynamically linked executable the inferior's true entry point + is the entry point of the dynamic linker. We find this using the + AT_BASE auxiliary vector entry, which gives the dynamic linker's + load address, combined with e_entry address pulled from the + inferior. We assume that the ELF header can be read from AT_BASE. */ + CORE_ADDR at_base_addr; + if (target_auxv_search (AT_BASE, &at_base_addr) <= 0) + return {}; + + /* Determine ELF architecture type. Use the size of a program header + entry to determine which ELF header we can expect to find. */ + size_t e_entry_offset = 0; + size_t e_entry_size = 0; + CORE_ADDR at_phent; + if (target_auxv_search (AT_PHENT, &at_phent) <= 0) + return {}; + if (at_phent == sizeof (Elf32_External_Phdr)) + { + e_entry_offset = offsetof (Elf32_External_Ehdr, e_entry); + e_entry_size = 4; + } + else if (at_phent == sizeof (Elf64_External_Phdr)) + { + e_entry_offset = offsetof (Elf64_External_Ehdr, e_entry); + e_entry_size = 8; + } + else + return {}; + + /* Architecture of the current inferior. */ + gdbarch *gdbarch = current_inferior ()->arch (); + + /* Read the entry address from the ELF header at AT_BASE_ADDR. */ + CORE_ADDR e_entry; + gdb_byte buffer[sizeof (CORE_ADDR)]; + if (target_read_memory (at_base_addr + e_entry_offset, buffer, e_entry_size)) + return {}; + bfd_endian byte_order = gdbarch_byte_order (gdbarch); + e_entry = extract_unsigned_integer (buffer, e_entry_size, byte_order); + + /* Ensure AT_BASE_ADDR has proper sign in its possible upper bits so + that `+ at_base_addr' will overflow CORE_ADDR width not creating + invalid addresses like 0x101234567 for 32bit inferiors on 64bit + GDB. */ + int addr_bit = gdbarch_addr_bit (gdbarch); + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) + { + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; + + gdb_assert (at_base_addr < space_size); + + /* E_ENTRY exceeding SPACE_SIZE would be for prelinked + 64bit ld.so with 32bit executable, it should not happen. */ + if (e_entry < space_size + && e_entry + at_base_addr >= space_size) + at_base_addr -= space_size; + } + + /* Compute the entry address. */ + e_entry = at_base_addr + e_entry; + + /* Handle the case where E_ENTRY is a function descriptor. Also remove + any non-address (e.g. tag) bits from E_ENTRY. */ + e_entry + = gdbarch_convert_from_func_ptr_addr (gdbarch, e_entry, + current_inferior ()->top_target ()); + e_entry + = gdbarch_addr_bits_remove (gdbarch, e_entry); + + return e_entry; +} + INIT_GDB_FILE (svr4_solib) { gdb::observers::free_objfile.attach (svr4_free_objfile_observer, diff --git a/gdb/solib-svr4.h b/gdb/solib-svr4.h index 3078a092778..f76f350d951 100644 --- a/gdb/solib-svr4.h +++ b/gdb/solib-svr4.h @@ -115,6 +115,7 @@ struct svr4_solib_ops : public solib_ops void iterate_over_objfiles_in_search_order (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const override; + std::optional<CORE_ADDR> inferior_entry_point_address () const override; /* Return the appropriate link map offsets table for the architecture. */ virtual link_map_offsets *fetch_link_map_offsets () const = 0; diff --git a/gdb/solib.h b/gdb/solib.h index 662d467b0cc..2db836dc005 100644 --- a/gdb/solib.h +++ b/gdb/solib.h @@ -283,6 +283,20 @@ struct solib_ops (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const; + /* Return the inferior entry point address. This isn't always the entry + point of the main executable, but will be the actual entry point where + the inferior starts (or started) executing. + + For example on SVR4 targets, for dynamically linked executables, this + will be the entry address of the dynamic linker. But for statically + linked executables, this will be the entry point of the main + executable. + + If the entry address cannot be found then an empty optional is + returned. */ + virtual std::optional<CORE_ADDR> inferior_entry_point_address () const + { return {}; } + protected: /* The program space for which this solib_ops was created. */ program_space *m_pspace; diff --git a/gdb/testsuite/gdb.base/bt-after-starti.exp b/gdb/testsuite/gdb.base/bt-after-starti.exp new file mode 100644 index 00000000000..778c8030d26 --- /dev/null +++ b/gdb/testsuite/gdb.base/bt-after-starti.exp @@ -0,0 +1,253 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'bt' from the very first instruction (where 'starti' +# stops GDB) doesn't display any unexpected frames. + +standard_testfile main.c + +# Build default executable, hopefully dynamically linked, but we check +# this below. +foreach pie_mode { pie nopie } { + if { [build_executable "failed to build" $testfile-$pie_mode $srcfile \ + [list debug $pie_mode]] } { + return + } +} + +# Build a statically linked executable. +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + {debug additional_flags=-static}] } { + return +} + +# Run the 'maint info entry-address' command. Expect INF_RE to match +# the inferior entry address and EXE_RE to match the executable entry +# address. TESTNAME is used as the test name. +proc check_maint_info_entry_addr { inf_re exe_re testname } { + gdb_test "maint info entry-address" \ + [multi_line \ + "Inferior entry address: ${inf_re}" \ + "Executable entry address: ${exe_re}"] \ + $testname +} + +# Use 'info proc mappings' to find the address of the first mapping of +# TESTFILE, return this address. If the address cannot be found then +# the empty string is returned. +# +# This emits a PASS or FAIL depending on whether the address is found +# or not. +proc get_mapped_file_base_addr { testfile } { + set base_addr "" + gdb_test_multiple "info proc mappings" "" -lbl { + -re "\r\n($::hex) \[^\r\n\]+/${testfile}\\s*(?=\r\n)" { + if { $base_addr eq "" } { + set base_addr $expect_out(1,string) + } + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $base_addr ne "" } \ + "find base address of $testfile" + } + } + return $base_addr +} + +# Use 'objdump' to extract the entry point address from TESTFILE. +proc get_exec_entry_address { testfile } { + set objdump_program [gdb_find_objdump] + set command "exec $objdump_program -f [standard_output_file $testfile]" + verbose -log "command is $command" + set result [catch {{*}$command} output] + verbose -log "result is $result" + verbose -log "output is $output" + + set testname "get_exec_entry_address" + + if {$result != 0} { + fail $testname + return "UNKNOWN" + } + + if {![regexp "\nstart address ($::hex)\n" $output trash addr]} { + fail "$testname (no entry point address header)" + return "UNKNOWN" + } + + return 0x[format %lx $addr] +} + +# Start TESTFILE using 'starti'. Check if the 'backtrace' command +# prints any additional frames; we don't expect any. Create a core +# file from the running inferior. Restart GDB and load the core file, +# check that GDB can still figure out the entry addresses. +# +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked +# executable, otherwise TESTFILE is statically linked. +proc run_test { testfile is_dynamic } { + clean_restart $testfile + + set exe_entry_addr [get_exec_entry_address $testfile] + + # The inferior entry address is always unknown before starting the + # inferior because GDB has no solib_ops yet, and it is only + # through that that we figure out the inferior entry address. + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ + "check 'maint info entry-address' before inferior starts" + + if {$::use_gdb_stub && [target_info exists gdb,do_reload_on_run]} { + # This is the path taken by the 'native-gdbserver' board. + # Start gdbserver and connect, this should leave the inferior + # at the first instruction. + if { [gdb_reload] != 0 } { + return -1 + } + pass "connected to gdbserver" + } else { + # Start inferior with 'starti' and then wait for a prompt. + if { [gdb_starti_cmd] < 0 } { + fail "could not issue starti command" + return -1 + } + gdb_test "" "Program stopped.*" "prompt after starti" + } + + # Not every target supports finding the process entry point. + # Targets that don't support this report '<unknown>', we check for + # this too on those targets. + if {[supports_process_entry_point]} { + set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN" \ + "get current program counter"] + + if { !$is_dynamic } { + # In static binaries, the first address should be the first + # address in the executable. + gdb_assert { $pc == $exe_entry_addr } \ + "stopped at executable entry address" + } + + set inf_entry_addr $pc + } else { + set inf_entry_addr "<unknown>" + } + + # If using PIE then the executable entry address we looked up from + # the objfile will have been relocated. Attempt to compute the + # relocated address and updated EXE_ENTRY_ADDR, otherwise set + # EXE_ENTRY_ADDR to a generic pattern that matches any address. + if {[exec_is_pie [standard_output_file $testfile]]} { + set base_addr [get_mapped_file_base_addr $testfile] + if { $base_addr ne "" } { + set exe_entry_addr \ + 0x[format %lx [expr {$base_addr + $exe_entry_addr}]] + } else { + # We failed to find the base address. The + # get_mapped_file_base_addr will have already emitted a + # FAIL. Set the expected pattern to something generic and + # push on, we can still test a bunch of things, we just + # don't know the address is correct. + set exe_entry_addr $::hex + } + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after starti" + + # Allow backtrace past the entry frame, count how many frames GDB + # finds. If on this target there are no additional frames then + # the following test isn't going to tell us much, so skip it. + gdb_test_no_output "set backtrace past-entry on" + set frame_count 0 + set first_frame_is_unnamed false + gdb_test_multiple "bt" "count possible frames" -lbl { + -re "\r\n#0\\s+$::hex in \\?\\? \\(\\) \[^\r\n\]+(?=\r\n)" { + incr frame_count + set first_frame_is_unnamed true + exp_continue + } + -re "\r\n#($::decimal)\\s+\[^\r\n\]+(?=\r\n)" { + incr frame_count + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $frame_count > 0 } $gdb_test_name + } + } + + if { $frame_count <= 1 } { + unsupported "no additional frames that GDB can hide" + } elseif { $first_frame_is_unnamed } { + # GDB's entry frame detection relies on figuring out the $pc + # for the start of the frame. We know that frame #0 should be + # the entry frame, but if it is nameless then GDB will have + # failed to find the start of the frame, and so will not be + # able to perform entry frame detection. + unsupported "first frame is unnamed" + } else { + # Turn off backtrace past the entry frame. Use the 'bt' command, + # and check we see only a single frame. + gdb_test_no_output "set backtrace past-entry off" + gdb_test "bt" "^#0 \[^\r\n\]+" \ + "single frame when backtrace past-entry is off" + + # Check 'bt -past-entry'. + set frame_count_with_past_entry_flag 0 + gdb_test_multiple "bt -past-entry" "count frames again" -lbl { + -re "\r\n#($::decimal)\\s+\[^\r\n\]+(?=\r\n)" { + incr frame_count_with_past_entry_flag + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert \ + { $frame_count == $frame_count_with_past_entry_flag } \ + $gdb_test_name + } + } + } + + # Create a core file. Restart GDB. Load the core file. Check + # that 'maint info entry-address' gives the correct output. + set corefile [host_standard_output_file $testfile].core + if {![gdb_gcore_cmd $corefile "generate corefile"]} { + return + } + + clean_restart $testfile + + if {[gdb_core_cmd $corefile "load corefile"] != 1} { + return + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after loading core file" +} + +with_test_prefix "dynamic" { + foreach_with_prefix pie_mode { pie nopie } { + set dyln_name [section_get $binfile-$pie_mode .interp] + if { $dyln_name eq "" } { + unsupported "couldn't find dynamic linker name" + } else { + run_test $testfile-$pie_mode true + } + } +} + +with_test_prefix "static" { + run_test $testfile_static false +} diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp index a40c87c6727..11aa6bfbf35 100644 --- a/gdb/testsuite/lib/gdb.exp +++ b/gdb/testsuite/lib/gdb.exp @@ -4217,6 +4217,45 @@ proc is_aarch64_target {} { return [expr {![is_aarch32_target]}] } +# Return true for svr4 targets, otherwise, return false. + +proc is_svr4_target {} { + return [expr {[istarget *-linux*] || [istarget *-freebsd*] + || [istarget *-netbsd*] || [istarget *-openbsd*] + || [istarget *-solaris*] || [istarget *-gnu]}] +} + +# Return true for targets that support finding the whole process entry +# address, otherwise, return false. + +proc supports_process_entry_point {} { + # SVR4 targets support finding the entry point. This is done + # within GDB so will work even for remote targets. + if {[is_svr4_target]} { + return true + } + + # Windows and Darwin don't currently support this. + if {[istarget *-*-mingw*] + || [istarget *-*-cygwin*] + || [istarget *-*-pe*] + || [istarget *-*-darwin*]} { + return false + } + + # For remote targets there is no RSP packet to retrieve the entry + # point, so this won't work unless the solib code can handle this + # within GDB, see the svr4 check above. + if {[gdb_protocol_is_remote]} { + return false + } + + # Assume everything else supports this by default. If a test + # fails because we get here then either fix GDB to support this + # feature, or add a new deny list entry above. + return true +} + # Return 1 if displaced stepping is supported on target, otherwise, return 0. proc support_displaced_stepping {} { -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv7 3/4] gdb: allow 'until' to work in outermost frame 2026-07-18 13:11 ` [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-07-18 13:11 ` Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 4/4] gdb: cache program space entry point information Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-18 13:11 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess The 'until' command with an argument, e.g. 'until *ADDRESS', is implemented by 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, if the until breakpoint was not hit. 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, where we call inside_entry_func. This checks if a frame is one of the two possible entry frames, the inferior entry frame or the executable entry frame. See the previous commit for more details. The important thing is that the inferior entry frame is the absolute outer frame, the very first frame that the inferior executed when starting, while the executable entry frame is just the first frame within the main executable. There are a number of user configurable filters in get_prev_frame, the backtrace past-main filter, the backtrace frame limit filter, and the backtrace past-entry filter that we are discussing here. When creating the caller frame breakpoint, the 'until' command doesn't use get_prev_frame, it uses frame_unwind_caller_frame, which calls get_prev_frame_always. This is so that the user configurable filters don't prevent the caller frame breakpoint from being created. But this means that when creating the breakpoint, we skip the entry frame check. 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 add an entry frame check into frame_unwind_caller_frame, similar to the one found in get_prev_frame. However, while get_prev_frame checks for both the inferior and executable entry frames (see previous commit for details on the differences), in frame_unwind_caller_frame I think we only need to check for the inferior entry frame. My reasoning here is that, in most cases, any frames before the inferior entry frame are likely to be invalid, and any attempt to write to them will trigger an error. In contrast, frames before the executable entry frame sit between standard libraries entry code and the executable's entry function, these frames, if GDB can find them, are likely to be valid. An earlier version of this patch tried placing this check into get_prev_frame_always, however, if get_prev_frame_always is unable to unwind a frame then it must set the stop reason on the last frame, this can be seen in `get_frame_unwind_stop_reason` where the assert `gdb_assert (frame->prev_p);` will trigger if the previous frame is not setup correctly. However, setting the stop reason means the decision about whether there's a previous frame or not is permanent (at least until the next frame cache flush), but we need commands like 'bt -past-entry' to work, which means the choice for whether there's a previous frame or not needs to remain dynamic. Placing the check in frame_unwind_caller_frame seemed like the best solution. This is the function that is called by other parts of GDB when they need 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 | 72 ++++++-- gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 173 ++++++++++++++++++ 3 files changed, 253 insertions(+), 14 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 e61bfd8d1f7..caa208c6d42 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -734,6 +734,52 @@ get_stack_frame_id (const frame_info_ptr &next_frame) return get_frame_id (skip_artificial_frames (next_frame)); } +/* When checking if a frame is an entry frame, there are two different + entry frames to consider. This enum is used to select which entry + frame(s) we wish to consider. See the inside_entry_func function. */ + +enum class entry_address_type_flag +{ + /* The executable's entry frame. This is the first frame within the main + executable. */ + executable = (1 << 0), + + /* The inferior's entry frame. This is the first frame within the + inferior, this can be outside the main executable, e.g. for a + dynamically linked executable, this will be the first frame in the + dynamic linker. */ + inferior = (1 << 1), +}; +DEF_ENUM_FLAGS_TYPE (enum entry_address_type_flag, entry_address_type_flags); + +/* Return true if THIS_FRAME is inside the either of the possible entry + frames based on ENTRY_ADDR_TYPES, otherwise return false. */ + +static bool +inside_entry_func (const frame_info_ptr &this_frame, + entry_address_type_flags entry_addr_types) +{ + /* It doesn't make sense to call this with no flag bits set. */ + gdb_assert (entry_addr_types != (entry_address_type_flags) 0); + + CORE_ADDR frame_func_addr; + if (!get_frame_func_if_available (this_frame, &frame_func_addr)) + return false; + + const entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + /* For each flag set in ENTRY_ADDR_TYPES if FRAME_FUNC_ADDR matches the + corresponding entry address from EP_INFO then THIS_FRAME is an entry + frame. */ + return (((entry_addr_types & entry_address_type_flag::executable) + == entry_address_type_flag::executable + && ep_info.exec_entry_address () == frame_func_addr) + || ((entry_addr_types & entry_address_type_flag::inferior) + == entry_address_type_flag::inferior + && ep_info.inferior_entry_address () == frame_func_addr)); +} + /* 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. @@ -744,6 +790,15 @@ 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) { + /* Avoid returning a frame which is possibly before the inferior's entry + frame, any such frame is likely invalid. However, if the user has + turned on 'backtrace past-entry' then we assume they know what they + are doing and allow these frames. */ + if (!user_set_backtrace_options.backtrace_past_entry + && inside_entry_func (initial_next_frame, + entry_address_type_flag::inferior)) + return nullptr; + frame_info_ptr this_frame = get_prev_frame_always (initial_next_frame); if (this_frame == nullptr) return nullptr; @@ -2698,19 +2753,6 @@ inside_main_func (const frame_info_ptr &this_frame) return sym_addr == get_frame_func (this_frame); } -/* Test whether THIS_FRAME is inside the process entry point function. */ - -static bool -inside_entry_func (const frame_info_ptr &this_frame) -{ - const entry_point_info &ep_info - = current_program_space->get_entry_point_info (); - - CORE_ADDR frame_func_addr = get_frame_func (this_frame); - return (ep_info.exec_entry_address () == frame_func_addr - || ep_info.inferior_entry_address () == frame_func_addr); -} - /* Return a structure containing various interesting information about the frame that called THIS_FRAME. Returns NULL if there is either no such frame or the frame fails any of a set of target-independent @@ -2793,7 +2835,9 @@ get_prev_frame (const frame_info_ptr &this_frame) && get_frame_type (this_frame) == NORMAL_FRAME && !user_set_backtrace_options.backtrace_past_entry && frame_pc.has_value () - && inside_entry_func (this_frame)) + && inside_entry_func (this_frame, + (entry_address_type_flag::inferior + | entry_address_type_flag::executable))) { 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..6a0e311ef41 --- /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 <http://www.gnu.org/licenses/>. */ + +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..e863c070bd4 --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp @@ -0,0 +1,173 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'until' in the outermost frame works as expected. + +standard_testfile + +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + { debug additional_flags=-static } ] } { + return +} + +# Use 'frame' 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 "frame" $testname { + -re -wrap "#0\\s+(?:$::hex in )?(\[^( \]+) \\(.*" { + set func_name $expect_out(1,string) + } + + -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 testfile } { + clean_restart $testfile + + if {$::use_gdb_stub && [target_info exists gdb,do_reload_on_run]} { + # This is the path taken by the 'native-gdbserver' board. + # Start gdbserver and connect, this should leave the inferior + # at the first instruction. + if { [gdb_reload] != 0 } { + return -1 + } + pass "connected to gdbserver" + } else { + # Start inferior with 'starti' and then wait for a prompt. + if { [gdb_starti_cmd] < 0 } { + untested starti + return + } + gdb_test "" "Program stopped.*" "prompt after 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" -lbl { + -re "\r\n=> $::hex \[^\r\n\]+(?=\r\n)" { + set capture_address true + exp_continue + } + + -re "\r\n\\s+($::hex) \[^\r\n\]+(?=\r\n)" { + if { $capture_address } { + set until_address $expect_out(1,string) + set capture_address false + } + + exp_continue + } + + -re "\r\n$::gdb_prompt $" { + set found_address [expr { $until_address ne "" }] + if { !$found_address } { + unsupported "$gdb_test_name (no instruction found)" + return + } + pass $gdb_test_name + } + } + + # 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 "\r\n(?:$::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 $testfile + + with_test_prefix "static" { + run_test $use_stepi $past_entry $testfile_static + } + } +} -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv7 4/4] gdb: cache program space entry point information 2026-07-18 13:11 ` [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 preceding siblings ...) 2026-07-18 13:11 ` [PATCHv7 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-07-18 13:11 ` Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 4 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-18 13:11 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess After the previous two patches, there are now two places where we check if a frame is an entry point frame, these are in get_prev_frame and frame_unwind_caller_frame. Both of these locations call inside_entry_func, which then calls program_space::get_entry_point_info. The calls to program_space::get_entry_point_info are not crazy expensive, but they are not free either, there are reads from target memory to read the auxv vector and the entry address offset, so on remote targets this could introduce a small delay. However, the result from program_space::get_entry_point_info is not expected to change from one call to the next. This information should be a property of the executable and libraries, so we really only need to figure it out once. This commit caches the entry point information within program_space, clearing it whenever an inferior starts or exits, or whenever the executable is updated. After clearing GDB will compute, and cache the updated information the next time it is needed, which will be whenever GDB needs to unwind a frame. It will be possible to observe this change by, for example, monitoring the remote target packets, but as far as the normal GDB output is concerned, there should be no user visible changes after this commit. --- gdb/progspace.c | 45 ++++++++++++++++++++++++++++++++++++++++++--- gdb/progspace.h | 9 ++++++++- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/gdb/progspace.c b/gdb/progspace.c index 4de32bc522f..b1e313f97ff 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -289,9 +289,12 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ -entry_point_info +const entry_point_info & program_space::get_entry_point_info () const { + if (m_entry_point_info.has_value ()) + return m_entry_point_info.value (); + std::optional<CORE_ADDR> exec_entry_address = this->exec_entry_point_address_if_available (); @@ -299,8 +302,9 @@ program_space::get_entry_point_info () const if (m_solib_ops != nullptr) inferior_entry_address = m_solib_ops->inferior_entry_point_address (); - return entry_point_info (std::move (inferior_entry_address), - std::move (exec_entry_address)); + m_entry_point_info.emplace (std::move (inferior_entry_address), + std::move (exec_entry_address)); + return m_entry_point_info.value (); } /* Implement the 'maint info entry-address' command. */ @@ -538,11 +542,46 @@ program_space::clear_solib_cache () deleted_solibs.clear (); } +/* Clear cached entry point information in the program space of INF. */ + +static void +clear_cached_entry_point_info_for_inferior (inferior *inf) +{ + inf->pspace->clear_cached_entry_point_info (); +} + +/* Clear cached entry point information in the program space PSPACE. */ + +static void +clear_cached_entry_point_info_for_pspace (program_space *pspace, + bool /* reload */) +{ + pspace->clear_cached_entry_point_info (); +} + +/* Clear cached entry point information in the program space of EXEC_INF. */ + +static void +clear_cached_entry_point_info_after_exec (inferior *exec_inf, + inferior */* follow_inf */) +{ + exec_inf->pspace->clear_cached_entry_point_info (); +} + /* See progspace.h. */ void initialize_progspace () { + gdb::observers::inferior_created.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::inferior_exit.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::executable_changed.attach + (clear_cached_entry_point_info_for_pspace, "program-space"); + gdb::observers::inferior_execd.attach + (clear_cached_entry_point_info_after_exec, "program-space"); + add_cmd ("program-spaces", class_maintenance, maintenance_info_program_spaces_command, _("Info about currently known program spaces."), diff --git a/gdb/progspace.h b/gdb/progspace.h index be2d3f7e88c..bfe0e5ec36f 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -370,7 +370,11 @@ struct program_space /* Return information about the entry point in the main executable, and the entry point for the inferior, which might be different from the main executable. */ - entry_point_info get_entry_point_info () const; + const entry_point_info &get_entry_point_info () const; + + /* Clear any cached entry point information. */ + void clear_cached_entry_point_info () + { m_entry_point_info.reset (); } /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ @@ -463,6 +467,9 @@ struct program_space /* See `exec_filename`. */ gdb::unique_xmalloc_ptr<char> m_exec_filename; + + /* Cached entry point information. See get_entry_point_info. */ + mutable std::optional<entry_point_info> m_entry_point_info; }; /* The list of all program spaces. There's always at least one. */ -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv8 0/4] gdb: allow 'until' to work in outermost frame 2026-07-18 13:11 ` [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (3 preceding siblings ...) 2026-07-18 13:11 ` [PATCHv7 4/4] gdb: cache program space entry point information Andrew Burgess @ 2026-07-20 9:52 ` Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess ` (3 more replies) 4 siblings, 4 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-20 9:52 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess In v8: - Fix for an ARM issue that Linaro CI found. Strip the THUMB bit from 'objdump -f' entry pc value before comparing it to addresses read from the $pc register inside GDB. In v7: - Addressed Simon's feedback for patch #2. Changes are all minor, and this patch now has Simon's Approved-By tag. - While addressing feedback on patch #3 I realised that the approach taken wouldn't work. Simon pointed out that setting the frame stop reason was incorrect, but removing that ended up triggering some assertion failures. As a result I had to take a different approach with this patch. This is the patch that changed the most. - Commit message for patch #4 is updated after changes to patch #3. - Some improvements to testing in both #2 and #3. All pretty minor though. - Rebased to HEAD of master branch and retested. In v6: - More adjustments to the test in patch #2 to address issues the Linaro CI testing highlighted. Nothing serious, just minor tweaks to some patterns to handle different output patterns. - Rebased to HEAD. In v5: - Update to test in patch #2 to allow the test to work on machines that build PIE by default. I actually extended the test to build both PIE and non-PIE executables, and test with both. In v4: - Rebase to current HEAD of master. - In patch #2, added a supports_process_entry_point proc as suggested by Pedro, and make use of this in the new test. Updates to address review comments from v2 series: - Doc updates based on Eli's feedback. - Patch #2: + Avoid swallowing Ctrl-C in try/catch. That whole try/catch block has been removed in v3, so problem sorted! + Rewrote entry address calculation as suggested by Pedro, now pulls all required information from the inferior. + Use 'objdump -f' rather than readelf to get the entry address in the test. + Added a helper proc for is_svr4_target, which includes a larger list of targets beyond just Linux. This is then used in the test. + Test updated to use -lbl flag in gdb_test_multiple. - Patch #3: + Use 'frame' rather than 'bt 1' + Loosen the test regexp after the 'starti' command, this should now work on Windows targets, or any target with multiple threads at startup. + Test updated to use -lbl in gdb_test_multiple. - In patch #3 the entry frame detection is moved from `frame_unwind_caller_frame` into `get_prev_frame_always`, I hope this will address Guinevere's concerns about having to add the additional entry frame detection in multiple locations. Guinevere also had some concerns about the naming of existing functions (i.e. not things added by me in this commit), I don't plan to make any naming changes to the get_prev_frame* functions in this series. - Patch #4 is new, this adds some caching to avoid repeatedly looking up the entry address. The new approach requires reading target memory, which can be expensive, especially on remote targets. --- Andrew Burgess (4): gdb: rename program_space::entry_point_address* functions gdb: introduce program_space::get_entry_point_info function gdb: allow 'until' to work in outermost frame gdb: cache program space entry point information gdb/NEWS | 7 + gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/doc/gdb.texinfo | 22 ++ gdb/frame.c | 74 ++++- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 105 ++++++- gdb/progspace.h | 68 ++++- gdb/solib-frv.c | 2 +- gdb/solib-svr4.c | 85 ++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 + gdb/testsuite/gdb.base/bt-after-starti.exp | 259 ++++++++++++++++++ gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 ++ .../gdb.base/until-in-entry-frame.exp | 173 ++++++++++++ gdb/testsuite/lib/gdb.exp | 39 +++ 17 files changed, 850 insertions(+), 29 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.c create mode 100644 gdb/testsuite/gdb.base/until-in-entry-frame.exp base-commit: ca8bf5f5dd69eba877e74ca8bc0796388070401f -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv8 1/4] gdb: rename program_space::entry_point_address* functions 2026-07-20 9:52 ` [PATCHv8 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-07-20 9:52 ` Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess ` (2 subsequent siblings) 3 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-20 9:52 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Pedro Alves Rename program_space::entry_point_address to program_space::exec_entry_point_address and program_space::entry_point_address_query to program_space::exec_entry_point_address_if_available. There are two aspects to this renaming. First I replace 'query' with 'if_available' in one of the functions. I feel this better describes the function, and also is inline with how other, similar, functions are named in GDB. The second part of the renaming is to add the 'exec_' prefix to the front of both function names. When a dynamically linked inferior is started the first instruction executed is actually within the run-time linker, not within the main executable, so it could be argued that the actual entry address for the inferior is not the entry address of the main executable. However, there is an equally valid argument that the entry address of the executable is also something worth finding. The existing entry address within the inferior is used for a number of tasks in GDB, for example displaced stepping (arch-utils.c), inferior function calls (arc-tdep.c and infcall.c), and for detecting the "entry" frame. This last one, the entry frame detection is interesting. In a dynamically linked executable it could be argued that there are two entry frames. The very first frame that is executed in the inferior, this is where 'starti' stops the inferior. And then the very first frame within the main executable. I think that both of these are valid. Currently, as we can only find the entry address within the main executable, we can only identify the first frame of the main executable. And this can cause some problems, consider: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) Here frames #1 to #3 are all bogus, created by GDB based on whatever values happen to be in the registers when the inferior starts. In a later commit I'd like to fix this problem, however, I would prefer that the other users of program_space::entry_point_address continue to use the address within the main executable. So, in order to keep the distinction between the two different types of entry point, I'm renaming the existing functions with the 'exec_' prefix. There should be no user visible changes after this commit. Approved-By: Pedro Alves <pedro@palves.net> --- gdb/arc-tdep.c | 2 +- gdb/arch-utils.c | 2 +- gdb/frame.c | 4 ++-- gdb/infcall.c | 2 +- gdb/linux-tdep.c | 2 +- gdb/progspace.c | 6 +++--- gdb/progspace.h | 12 ++++++------ gdb/solib-frv.c | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gdb/arc-tdep.c b/gdb/arc-tdep.c index 4936a5c8fbb..7ed83c4cae6 100644 --- a/gdb/arc-tdep.c +++ b/gdb/arc-tdep.c @@ -860,7 +860,7 @@ arc_push_dummy_code (struct gdbarch *gdbarch, CORE_ADDR sp, CORE_ADDR funaddr, struct regcache *regcache) { *real_pc = funaddr; - *bp_addr = current_program_space->entry_point_address (); + *bp_addr = current_program_space->exec_entry_point_address (); return sp; } diff --git a/gdb/arch-utils.c b/gdb/arch-utils.c index e959788bd3b..88047731ce6 100644 --- a/gdb/arch-utils.c +++ b/gdb/arch-utils.c @@ -57,7 +57,7 @@ displaced_step_at_entry_point (struct gdbarch *gdbarch) CORE_ADDR addr; int bp_len; - addr = current_program_space->entry_point_address (); + addr = current_program_space->exec_entry_point_address (); /* Inferior calls also use the entry point as a breakpoint location. We don't want displaced stepping to interfere with those diff --git a/gdb/frame.c b/gdb/frame.c index b91e18fad99..a83f0b20686 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2704,7 +2704,7 @@ static bool inside_entry_func (const frame_info_ptr &this_frame) { std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) return false; @@ -2780,7 +2780,7 @@ get_prev_frame (const frame_info_ptr &this_frame) added to work around that (now fixed) case. */ /* NOTE: cagney/2003-07-15: danielj (if I'm reading it right) suggested having the inside_entry_func test use the - inside_main_func() msymbol trick (along with entry_point_address() + inside_main_func() msymbol trick (along with exec_entry_point_address() I guess) to determine the address range of the start function. That should provide a far better stopper than the current heuristics. */ diff --git a/gdb/infcall.c b/gdb/infcall.c index e6b24ff5310..6d26841ede3 100644 --- a/gdb/infcall.c +++ b/gdb/infcall.c @@ -1300,7 +1300,7 @@ call_function_by_hand_dummy (struct value *function, CORE_ADDR dummy_addr; real_pc = funaddr; - dummy_addr = current_program_space->entry_point_address (); + dummy_addr = current_program_space->exec_entry_point_address (); /* A call dummy always consists of just a single breakpoint, so its address is the same as the address of the dummy. diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c index 25d625db595..ea88f35ff5c 100644 --- a/gdb/linux-tdep.c +++ b/gdb/linux-tdep.c @@ -2952,7 +2952,7 @@ linux_displaced_step_location (struct gdbarch *gdbarch) /* Determine entry point from target auxiliary vector. This avoids the need for symbols. Also, when debugging a stand-alone SPU - executable, entry_point_address () will point to an SPU + executable, exec_entry_point_address () will point to an SPU local-store address and is thus not usable as displaced stepping location. The auxiliary vector gets us the PowerPC-side entry point address instead. */ diff --git a/gdb/progspace.c b/gdb/progspace.c index 1407b058dfd..0e8516f4640 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -263,7 +263,7 @@ program_space::empty () /* See progspace.h. */ std::optional<CORE_ADDR> -program_space::entry_point_address_query () const +program_space::exec_entry_point_address_if_available () const { objfile *objf = symfile_object_file; if (objf == NULL || !objf->per_bfd->ei.entry_point_p) @@ -276,9 +276,9 @@ program_space::entry_point_address_query () const /* See progspace.h. */ CORE_ADDR -program_space::entry_point_address () const +program_space::exec_entry_point_address () const { - std::optional<CORE_ADDR> retval = entry_point_address_query (); + std::optional<CORE_ADDR> retval = exec_entry_point_address_if_available (); if (!retval.has_value ()) error (_("Entry point address is not known.")); diff --git a/gdb/progspace.h b/gdb/progspace.h index e9261ff8590..1ae1e42f3bb 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -323,13 +323,13 @@ struct program_space return m_target_sections; } - /* If there is a valid and known entry point in this program space, - return it. Otherwise return an empty optional. */ - std::optional<CORE_ADDR> entry_point_address_query () const; + /* If there is a valid and known entry point in the main executable of + this program space, return it. Otherwise return an empty optional. */ + std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; - /* Get the entry point address in this program space. Call error if - it is not known. */ - CORE_ADDR entry_point_address () const; + /* Get the entry point address for the main executable in this program + space. Call error if it is not known. */ + CORE_ADDR exec_entry_point_address () const; /* Return true if any objfile of this program space has partial symbols. */ diff --git a/gdb/solib-frv.c b/gdb/solib-frv.c index 08ee0a9578f..4284a711860 100644 --- a/gdb/solib-frv.c +++ b/gdb/solib-frv.c @@ -689,7 +689,7 @@ enable_break (void) } std::optional<CORE_ADDR> entry_point - = current_program_space->entry_point_address_query (); + = current_program_space->exec_entry_point_address_if_available (); if (!entry_point.has_value ()) { solib_debug_printf ("Symbol file has no entry point."); -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv8 2/4] gdb: introduce program_space::get_entry_point_info function 2026-07-20 9:52 ` [PATCHv8 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess @ 2026-07-20 9:52 ` Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 4/4] gdb: cache program space entry point information Andrew Burgess 3 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-20 9:52 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess, Eli Zaretskii, Simon Marchi I noticed that when debugging a dynamically linked executable, if I started the inferior with 'starti' then used 'bt' I would see some bogus frames: (gdb) starti Starting program: /tmp/hello Program stopped. 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7fd3110 in _start () from /lib64/ld-linux-x86-64.so.2 #1 0x0000000000000001 in ?? () #2 0x00007fffffffac13 in ?? () #3 0x0000000000000000 in ?? () (gdb) This surprised me as 'backtrace past-entry' was off: (gdb) show backtrace past-entry Whether backtraces should continue past the entry point of a program is off. I was expecting GDB to stop the backtrace at the inferior's entry address. Frame unwinding starts in get_prev_frame, and in here we find this block: 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)) { frame_debug_got_null_frame (this_frame, "inside entry func"); return NULL; } Which uses inside_entry_func to terminate the backtrace when we reach the entry frame. The inside_entry_func function calls current_program_space->exec_entry_point_address_if_available and uses the result to figure out if we are in the entry frame. And here the problem becomes obvious, we are only checking if we are in the "entry frame" for the main executable, not for the inferior as a whole. And indeed, if I compile the same test program as a static binary, where there will be no run-time linker, and the entry point of the executable is the entry point for the inferior, then the 'bt' problem I saw above goes away. This suggests, I think, that we need to track two different entry addresses, the entry address for the executable file, and the entry address for the entire inferior. Then, for dynamically linked executables, these two addresses can be different, the former will still be the same address within the main executable file, while the latter will be the address of the entry point within the run-time linker. If we had this information then we could extend inside_entry_func to check both addresses, and the 'bt' problem seen above will be resolved. To make this information available I added a new solib_ops method, solib_ops::inferior_entry_point_address, for svr4 targets this figures out if the main executable is dynamically linked, and if it is, uses the AT_BASE auxv entry and the entry address pulled from the ELF header to compute the inferior entry address. I then added program_space::get_entry_point_info, which returns a struct containing the two entry point addresses, one comes from the new solib_ops method, and one comes from the existing method program_space::exec_entry_point_address_if_available. With the infrastructure in place I can then update inside_entry_func to check against both entry addresses. To aid in debugging GDB, I added a new maintenance command: maintenance info entry-address which just calls program_space::get_entry_point_info and then prints the two addresses. I left this as a maintenance command as I don't see much user utility in this right now, but it made it easier for me to see what GDB was doing, so I left the command in this commit. Reviewed-By: Eli Zaretskii <eliz@gnu.org> Approved-By: Simon Marchi <simon.marchi@efficios.com> --- gdb/NEWS | 7 + gdb/doc/gdb.texinfo | 22 ++ gdb/frame.c | 10 +- gdb/progspace.c | 60 +++++ gdb/progspace.h | 49 ++++ gdb/solib-svr4.c | 85 +++++++ gdb/solib-svr4.h | 1 + gdb/solib.h | 14 ++ gdb/testsuite/gdb.base/bt-after-starti.exp | 259 +++++++++++++++++++++ gdb/testsuite/lib/gdb.exp | 39 ++++ 10 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 gdb/testsuite/gdb.base/bt-after-starti.exp diff --git a/gdb/NEWS b/gdb/NEWS index 8f40ca5cb11..9c51f476c84 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -179,6 +179,13 @@ disable skip These are new aliases for 'skip delete', 'skip enable', and 'skip disable' respectively. +maint info entry-address + Display the inferior and main executable entry addresses for the + current inferior. The inferior entry address is the address of the + first instruction in the inferior that was executed. The main + executable entry address is the address of the first instruction in + the main executable that will be executed. + * MI changes ** The "-trace-save" command no longer supports the "-ctf" flag. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index 0030698dcee..35fb9f78b08 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -43260,6 +43260,28 @@ Maintenance Commands Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M Ignoring SystemTap probe libc longjmp in /lib64/libc.so.6.^M @end smallexample + +@kindex maint info entry-address +@item maint info entry-address +Display the entry addresses for the currently selected inferior. Two +addresses are displayed, the inferior entry address and the executable +entry address. + +Neither address is the address of @code{main}. When a program is +started, low-level startup code (typically a function called +@code{_start}) runs before @code{main} is called. + +The executable entry address is the address of this startup code +within the main executable (@pxref{Files, ,Commands to Specify +Files}). + +The inferior entry address is the address of the very first +instruction executed when the inferior is started. For statically +linked programs this is the same as the executable entry address. For +dynamically linked programs the run-time linker must execute first in +order to load shared libraries, so the inferior entry address will be +an address within the run-time linker rather than within the main +executable. @end table The following command is useful for non-interactive invocations of diff --git a/gdb/frame.c b/gdb/frame.c index a83f0b20686..e61bfd8d1f7 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -2703,12 +2703,12 @@ inside_main_func (const frame_info_ptr &this_frame) static bool inside_entry_func (const frame_info_ptr &this_frame) { - std::optional<CORE_ADDR> entry_point - = current_program_space->exec_entry_point_address_if_available (); - if (!entry_point.has_value ()) - return false; + const entry_point_info &ep_info + = current_program_space->get_entry_point_info (); - return get_frame_func (this_frame) == *entry_point; + CORE_ADDR frame_func_addr = get_frame_func (this_frame); + return (ep_info.exec_entry_address () == frame_func_addr + || ep_info.inferior_entry_address () == frame_func_addr); } /* Return a structure containing various interesting information about diff --git a/gdb/progspace.c b/gdb/progspace.c index 0e8516f4640..4de32bc522f 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -26,6 +26,7 @@ #include <algorithm> #include "cli/cli-style.h" #include "observable.h" +#include "arch-utils.h" /* The last program space number assigned. */ static int last_program_space_num = 0; @@ -288,6 +289,55 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ +entry_point_info +program_space::get_entry_point_info () const +{ + std::optional<CORE_ADDR> exec_entry_address + = this->exec_entry_point_address_if_available (); + + std::optional<CORE_ADDR> inferior_entry_address; + if (m_solib_ops != nullptr) + inferior_entry_address = m_solib_ops->inferior_entry_point_address (); + + return entry_point_info (std::move (inferior_entry_address), + std::move (exec_entry_address)); +} + +/* Implement the 'maint info entry-address' command. */ + +static void +maintenance_info_entry_address (const char *args, int from_tty) +{ + if (args != nullptr && *args != '\0') + error (_("unknown argument: %s"), args); + + struct gdbarch *gdbarch = get_current_arch (); + + const entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + /* Display a single entry address ADDR with TITLE as a description. */ + auto display_entry_address + = [&gdbarch] (const char *title, + const std::optional<CORE_ADDR> &addr) -> void + { + gdb_puts (title); + if (addr.has_value ()) + fputs_styled (paddress (gdbarch, addr.value ()), + address_style.style (), gdb_stdout); + else + fputs_styled ("<unknown>", metadata_style.style (), gdb_stdout); + gdb_puts ("\n"); + }; + + display_entry_address (_("Inferior entry address: "), + ep_info.inferior_entry_address ()); + display_entry_address (_("Executable entry address: "), + ep_info.exec_entry_address ()); +} + +/* See progspace.h. */ + bool program_space::has_partial_symbols () { @@ -498,6 +548,16 @@ initialize_progspace () _("Info about currently known program spaces."), &maintenanceinfolist); + add_cmd ("entry-address", class_maintenance, + maintenance_info_entry_address, + _("Information about the current inferior's entry addresses.\n\ +Display the address of the first instruction executed within the\n\ +inferior, and the first instruction executed within the main executable.\n\ +The entry address of the whole inferior will be <unknown> prior to the\n\ +inferior starting, and either or both addresses can be <unknown> if\n\ +GDB is unable to find the required information."), + &maintenanceinfolist); + /* There's always one program space. Note that this function isn't an automatic _initialize_foo function, since other _initialize_foo routines may need to install their per-pspace diff --git a/gdb/progspace.h b/gdb/progspace.h index 1ae1e42f3bb..be2d3f7e88c 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -67,6 +67,50 @@ new_address_space () return address_space_ref_ptr::new_reference (new address_space); } +/* Class for tracking two possible entry points that an inferior might + have, the entry point for the entire inferior, and the entry point + within the main executable. */ + +struct entry_point_info +{ + entry_point_info (std::optional<CORE_ADDR> inferior_entry_address, + std::optional<CORE_ADDR> exec_entry_address) + : m_inferior_entry_address (inferior_entry_address), + m_exec_entry_address (exec_entry_address) + { /* Nothing. */ } + + DISABLE_COPY_AND_ASSIGN (entry_point_info); + + /* The entry address within the inferior as a whole. See the member + variable definition below for more details. */ + std::optional<CORE_ADDR> inferior_entry_address () const + { + return m_inferior_entry_address; + } + + /* The entry address within the main executable. See the member + variable definition below for more details. */ + std::optional<CORE_ADDR> exec_entry_address () const + { + return m_exec_entry_address; + } + +private: + /* The entry address within the inferior. This can be outside of the + main executable, e.g. for dynamically linked executables this could + be the entry address for the run-time linker. For statically linked + executables this will be the entry address of the main executable. + This can be empty if GDB doesn't know how to figure out the correct + entry address for any reason. */ + std::optional<CORE_ADDR> m_inferior_entry_address; + + /* The entry address within the main executable. This can be empty if + the main executable is not set yet, or GDB doesn't have an objfile + associated with the main executable, e.g. in some attach, or remote + debug cases. */ + std::optional<CORE_ADDR> m_exec_entry_address; +}; + /* A program space represents a symbolic view of an address space. Roughly speaking, it holds all the data associated with a non-running-yet program (main executable, main symbols), and when @@ -323,6 +367,11 @@ struct program_space return m_target_sections; } + /* Return information about the entry point in the main executable, and + the entry point for the inferior, which might be different from the + main executable. */ + entry_point_info get_entry_point_info () const; + /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ std::optional<CORE_ADDR> exec_entry_point_address_if_available () const; diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c index 8e3de4d3ea1..12f3e783744 100644 --- a/gdb/solib-svr4.c +++ b/gdb/solib-svr4.c @@ -3777,6 +3777,91 @@ svr4_solib_ops::get_solibs_in_ns (int nsid) const return ns_solibs; } +/* See solib.h. */ + +std::optional<CORE_ADDR> +svr4_solib_ops::inferior_entry_point_address () const +{ + std::optional<gdb::byte_vector> interp_name_holder + = svr4_find_program_interpreter (); + + /* No interpreter means this is a static executable. Ask the + program_space for the entry address within the main executable. */ + if (!interp_name_holder.has_value ()) + return m_pspace->exec_entry_point_address_if_available (); + + /* For a dynamically linked executable the inferior's true entry point + is the entry point of the dynamic linker. We find this using the + AT_BASE auxiliary vector entry, which gives the dynamic linker's + load address, combined with e_entry address pulled from the + inferior. We assume that the ELF header can be read from AT_BASE. */ + CORE_ADDR at_base_addr; + if (target_auxv_search (AT_BASE, &at_base_addr) <= 0) + return {}; + + /* Determine ELF architecture type. Use the size of a program header + entry to determine which ELF header we can expect to find. */ + size_t e_entry_offset = 0; + size_t e_entry_size = 0; + CORE_ADDR at_phent; + if (target_auxv_search (AT_PHENT, &at_phent) <= 0) + return {}; + if (at_phent == sizeof (Elf32_External_Phdr)) + { + e_entry_offset = offsetof (Elf32_External_Ehdr, e_entry); + e_entry_size = 4; + } + else if (at_phent == sizeof (Elf64_External_Phdr)) + { + e_entry_offset = offsetof (Elf64_External_Ehdr, e_entry); + e_entry_size = 8; + } + else + return {}; + + /* Architecture of the current inferior. */ + gdbarch *gdbarch = current_inferior ()->arch (); + + /* Read the entry address from the ELF header at AT_BASE_ADDR. */ + CORE_ADDR e_entry; + gdb_byte buffer[sizeof (CORE_ADDR)]; + if (target_read_memory (at_base_addr + e_entry_offset, buffer, e_entry_size)) + return {}; + bfd_endian byte_order = gdbarch_byte_order (gdbarch); + e_entry = extract_unsigned_integer (buffer, e_entry_size, byte_order); + + /* Ensure AT_BASE_ADDR has proper sign in its possible upper bits so + that `+ at_base_addr' will overflow CORE_ADDR width not creating + invalid addresses like 0x101234567 for 32bit inferiors on 64bit + GDB. */ + int addr_bit = gdbarch_addr_bit (gdbarch); + if (addr_bit < (sizeof (CORE_ADDR) * HOST_CHAR_BIT)) + { + CORE_ADDR space_size = (CORE_ADDR) 1 << addr_bit; + + gdb_assert (at_base_addr < space_size); + + /* E_ENTRY exceeding SPACE_SIZE would be for prelinked + 64bit ld.so with 32bit executable, it should not happen. */ + if (e_entry < space_size + && e_entry + at_base_addr >= space_size) + at_base_addr -= space_size; + } + + /* Compute the entry address. */ + e_entry = at_base_addr + e_entry; + + /* Handle the case where E_ENTRY is a function descriptor. Also remove + any non-address (e.g. tag) bits from E_ENTRY. */ + e_entry + = gdbarch_convert_from_func_ptr_addr (gdbarch, e_entry, + current_inferior ()->top_target ()); + e_entry + = gdbarch_addr_bits_remove (gdbarch, e_entry); + + return e_entry; +} + INIT_GDB_FILE (svr4_solib) { gdb::observers::free_objfile.attach (svr4_free_objfile_observer, diff --git a/gdb/solib-svr4.h b/gdb/solib-svr4.h index 3078a092778..f76f350d951 100644 --- a/gdb/solib-svr4.h +++ b/gdb/solib-svr4.h @@ -115,6 +115,7 @@ struct svr4_solib_ops : public solib_ops void iterate_over_objfiles_in_search_order (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const override; + std::optional<CORE_ADDR> inferior_entry_point_address () const override; /* Return the appropriate link map offsets table for the architecture. */ virtual link_map_offsets *fetch_link_map_offsets () const = 0; diff --git a/gdb/solib.h b/gdb/solib.h index 662d467b0cc..2db836dc005 100644 --- a/gdb/solib.h +++ b/gdb/solib.h @@ -283,6 +283,20 @@ struct solib_ops (iterate_over_objfiles_in_search_order_cb_ftype cb, objfile *current_objfile) const; + /* Return the inferior entry point address. This isn't always the entry + point of the main executable, but will be the actual entry point where + the inferior starts (or started) executing. + + For example on SVR4 targets, for dynamically linked executables, this + will be the entry address of the dynamic linker. But for statically + linked executables, this will be the entry point of the main + executable. + + If the entry address cannot be found then an empty optional is + returned. */ + virtual std::optional<CORE_ADDR> inferior_entry_point_address () const + { return {}; } + protected: /* The program space for which this solib_ops was created. */ program_space *m_pspace; diff --git a/gdb/testsuite/gdb.base/bt-after-starti.exp b/gdb/testsuite/gdb.base/bt-after-starti.exp new file mode 100644 index 00000000000..2b452365f89 --- /dev/null +++ b/gdb/testsuite/gdb.base/bt-after-starti.exp @@ -0,0 +1,259 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'bt' from the very first instruction (where 'starti' +# stops GDB) doesn't display any unexpected frames. + +standard_testfile main.c + +# Build default executable, hopefully dynamically linked, but we check +# this below. +foreach pie_mode { pie nopie } { + if { [build_executable "failed to build" $testfile-$pie_mode $srcfile \ + [list debug $pie_mode]] } { + return + } +} + +# Build a statically linked executable. +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + {debug additional_flags=-static}] } { + return +} + +# Run the 'maint info entry-address' command. Expect INF_RE to match +# the inferior entry address and EXE_RE to match the executable entry +# address. TESTNAME is used as the test name. +proc check_maint_info_entry_addr { inf_re exe_re testname } { + gdb_test "maint info entry-address" \ + [multi_line \ + "Inferior entry address: ${inf_re}" \ + "Executable entry address: ${exe_re}"] \ + $testname +} + +# Use 'info proc mappings' to find the address of the first mapping of +# TESTFILE, return this address. If the address cannot be found then +# the empty string is returned. +# +# This emits a PASS or FAIL depending on whether the address is found +# or not. +proc get_mapped_file_base_addr { testfile } { + set base_addr "" + gdb_test_multiple "info proc mappings" "" -lbl { + -re "\r\n($::hex) \[^\r\n\]+/${testfile}\\s*(?=\r\n)" { + if { $base_addr eq "" } { + set base_addr $expect_out(1,string) + } + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $base_addr ne "" } \ + "find base address of $testfile" + } + } + return $base_addr +} + +# Use 'objdump' to extract the entry point address from TESTFILE. +proc get_exec_entry_address { testfile } { + set objdump_program [gdb_find_objdump] + set command "exec $objdump_program -f [standard_output_file $testfile]" + verbose -log "command is $command" + set result [catch {{*}$command} output] + verbose -log "result is $result" + verbose -log "output is $output" + + set testname "get_exec_entry_address" + + if {$result != 0} { + fail $testname + return "UNKNOWN" + } + + if {![regexp "\nstart address ($::hex)\n" $output trash addr]} { + fail "$testname (no entry point address header)" + return "UNKNOWN" + } + + # For ARM the least significant bit will be set to 1 for thumb + # code, clear that now. + if { [istarget "arm*-*-*"] } { + set addr [expr {$addr & ~0x1}] + } + + return 0x[format %lx $addr] +} + +# Start TESTFILE using 'starti'. Check if the 'backtrace' command +# prints any additional frames; we don't expect any. Create a core +# file from the running inferior. Restart GDB and load the core file, +# check that GDB can still figure out the entry addresses. +# +# When IS_DYNAMIC is true then TESTFILE is a dynamically linked +# executable, otherwise TESTFILE is statically linked. +proc run_test { testfile is_dynamic } { + clean_restart $testfile + + set exe_entry_addr [get_exec_entry_address $testfile] + + # The inferior entry address is always unknown before starting the + # inferior because GDB has no solib_ops yet, and it is only + # through that that we figure out the inferior entry address. + check_maint_info_entry_addr "<unknown>" $exe_entry_addr \ + "check 'maint info entry-address' before inferior starts" + + if {$::use_gdb_stub && [target_info exists gdb,do_reload_on_run]} { + # This is the path taken by the 'native-gdbserver' board. + # Start gdbserver and connect, this should leave the inferior + # at the first instruction. + if { [gdb_reload] != 0 } { + return -1 + } + pass "connected to gdbserver" + } else { + # Start inferior with 'starti' and then wait for a prompt. + if { [gdb_starti_cmd] < 0 } { + fail "could not issue starti command" + return -1 + } + gdb_test "" "Program stopped.*" "prompt after starti" + } + + # Not every target supports finding the process entry point. + # Targets that don't support this report '<unknown>', we check for + # this too on those targets. + if {[supports_process_entry_point]} { + set pc [get_hexadecimal_valueof "\$pc" "UNKNOWN" \ + "get current program counter"] + + if { !$is_dynamic } { + # In static binaries, the first address should be the first + # address in the executable. + gdb_assert { $pc == $exe_entry_addr } \ + "stopped at executable entry address" + } + + set inf_entry_addr $pc + } else { + set inf_entry_addr "<unknown>" + } + + # If using PIE then the executable entry address we looked up from + # the objfile will have been relocated. Attempt to compute the + # relocated address and updated EXE_ENTRY_ADDR, otherwise set + # EXE_ENTRY_ADDR to a generic pattern that matches any address. + if {[exec_is_pie [standard_output_file $testfile]]} { + set base_addr [get_mapped_file_base_addr $testfile] + if { $base_addr ne "" } { + set exe_entry_addr \ + 0x[format %lx [expr {$base_addr + $exe_entry_addr}]] + } else { + # We failed to find the base address. The + # get_mapped_file_base_addr will have already emitted a + # FAIL. Set the expected pattern to something generic and + # push on, we can still test a bunch of things, we just + # don't know the address is correct. + set exe_entry_addr $::hex + } + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after starti" + + # Allow backtrace past the entry frame, count how many frames GDB + # finds. If on this target there are no additional frames then + # the following test isn't going to tell us much, so skip it. + gdb_test_no_output "set backtrace past-entry on" + set frame_count 0 + set first_frame_is_unnamed false + gdb_test_multiple "bt" "count possible frames" -lbl { + -re "\r\n#0\\s+$::hex in \\?\\? \\(\\) \[^\r\n\]+(?=\r\n)" { + incr frame_count + set first_frame_is_unnamed true + exp_continue + } + -re "\r\n#($::decimal)\\s+\[^\r\n\]+(?=\r\n)" { + incr frame_count + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert { $frame_count > 0 } $gdb_test_name + } + } + + if { $frame_count <= 1 } { + unsupported "no additional frames that GDB can hide" + } elseif { $first_frame_is_unnamed } { + # GDB's entry frame detection relies on figuring out the $pc + # for the start of the frame. We know that frame #0 should be + # the entry frame, but if it is nameless then GDB will have + # failed to find the start of the frame, and so will not be + # able to perform entry frame detection. + unsupported "first frame is unnamed" + } else { + # Turn off backtrace past the entry frame. Use the 'bt' command, + # and check we see only a single frame. + gdb_test_no_output "set backtrace past-entry off" + gdb_test "bt" "^#0 \[^\r\n\]+" \ + "single frame when backtrace past-entry is off" + + # Check 'bt -past-entry'. + set frame_count_with_past_entry_flag 0 + gdb_test_multiple "bt -past-entry" "count frames again" -lbl { + -re "\r\n#($::decimal)\\s+\[^\r\n\]+(?=\r\n)" { + incr frame_count_with_past_entry_flag + exp_continue + } + -re "\r\n$::gdb_prompt $" { + gdb_assert \ + { $frame_count == $frame_count_with_past_entry_flag } \ + $gdb_test_name + } + } + } + + # Create a core file. Restart GDB. Load the core file. Check + # that 'maint info entry-address' gives the correct output. + set corefile [host_standard_output_file $testfile].core + if {![gdb_gcore_cmd $corefile "generate corefile"]} { + return + } + + clean_restart $testfile + + if {[gdb_core_cmd $corefile "load corefile"] != 1} { + return + } + + check_maint_info_entry_addr $inf_entry_addr $exe_entry_addr \ + "check 'maint info entry-address' after loading core file" +} + +with_test_prefix "dynamic" { + foreach_with_prefix pie_mode { pie nopie } { + set dyln_name [section_get $binfile-$pie_mode .interp] + if { $dyln_name eq "" } { + unsupported "couldn't find dynamic linker name" + } else { + run_test $testfile-$pie_mode true + } + } +} + +with_test_prefix "static" { + run_test $testfile_static false +} diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp index a40c87c6727..11aa6bfbf35 100644 --- a/gdb/testsuite/lib/gdb.exp +++ b/gdb/testsuite/lib/gdb.exp @@ -4217,6 +4217,45 @@ proc is_aarch64_target {} { return [expr {![is_aarch32_target]}] } +# Return true for svr4 targets, otherwise, return false. + +proc is_svr4_target {} { + return [expr {[istarget *-linux*] || [istarget *-freebsd*] + || [istarget *-netbsd*] || [istarget *-openbsd*] + || [istarget *-solaris*] || [istarget *-gnu]}] +} + +# Return true for targets that support finding the whole process entry +# address, otherwise, return false. + +proc supports_process_entry_point {} { + # SVR4 targets support finding the entry point. This is done + # within GDB so will work even for remote targets. + if {[is_svr4_target]} { + return true + } + + # Windows and Darwin don't currently support this. + if {[istarget *-*-mingw*] + || [istarget *-*-cygwin*] + || [istarget *-*-pe*] + || [istarget *-*-darwin*]} { + return false + } + + # For remote targets there is no RSP packet to retrieve the entry + # point, so this won't work unless the solib code can handle this + # within GDB, see the svr4 check above. + if {[gdb_protocol_is_remote]} { + return false + } + + # Assume everything else supports this by default. If a test + # fails because we get here then either fix GDB to support this + # feature, or add a new deny list entry above. + return true +} + # Return 1 if displaced stepping is supported on target, otherwise, return 0. proc support_displaced_stepping {} { -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv8 3/4] gdb: allow 'until' to work in outermost frame 2026-07-20 9:52 ` [PATCHv8 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess @ 2026-07-20 9:52 ` Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 4/4] gdb: cache program space entry point information Andrew Burgess 3 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-20 9:52 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess The 'until' command with an argument, e.g. 'until *ADDRESS', is implemented by 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, if the until breakpoint was not hit. 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, where we call inside_entry_func. This checks if a frame is one of the two possible entry frames, the inferior entry frame or the executable entry frame. See the previous commit for more details. The important thing is that the inferior entry frame is the absolute outer frame, the very first frame that the inferior executed when starting, while the executable entry frame is just the first frame within the main executable. There are a number of user configurable filters in get_prev_frame, the backtrace past-main filter, the backtrace frame limit filter, and the backtrace past-entry filter that we are discussing here. When creating the caller frame breakpoint, the 'until' command doesn't use get_prev_frame, it uses frame_unwind_caller_frame, which calls get_prev_frame_always. This is so that the user configurable filters don't prevent the caller frame breakpoint from being created. But this means that when creating the breakpoint, we skip the entry frame check. 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 add an entry frame check into frame_unwind_caller_frame, similar to the one found in get_prev_frame. However, while get_prev_frame checks for both the inferior and executable entry frames (see previous commit for details on the differences), in frame_unwind_caller_frame I think we only need to check for the inferior entry frame. My reasoning here is that, in most cases, any frames before the inferior entry frame are likely to be invalid, and any attempt to write to them will trigger an error. In contrast, frames before the executable entry frame sit between standard libraries entry code and the executable's entry function, these frames, if GDB can find them, are likely to be valid. An earlier version of this patch tried placing this check into get_prev_frame_always, however, if get_prev_frame_always is unable to unwind a frame then it must set the stop reason on the last frame, this can be seen in `get_frame_unwind_stop_reason` where the assert `gdb_assert (frame->prev_p);` will trigger if the previous frame is not setup correctly. However, setting the stop reason means the decision about whether there's a previous frame or not is permanent (at least until the next frame cache flush), but we need commands like 'bt -past-entry' to work, which means the choice for whether there's a previous frame or not needs to remain dynamic. Placing the check in frame_unwind_caller_frame seemed like the best solution. This is the function that is called by other parts of GDB when they need 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 | 72 ++++++-- gdb/testsuite/gdb.base/until-in-entry-frame.c | 22 +++ .../gdb.base/until-in-entry-frame.exp | 173 ++++++++++++++++++ 3 files changed, 253 insertions(+), 14 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 e61bfd8d1f7..caa208c6d42 100644 --- a/gdb/frame.c +++ b/gdb/frame.c @@ -734,6 +734,52 @@ get_stack_frame_id (const frame_info_ptr &next_frame) return get_frame_id (skip_artificial_frames (next_frame)); } +/* When checking if a frame is an entry frame, there are two different + entry frames to consider. This enum is used to select which entry + frame(s) we wish to consider. See the inside_entry_func function. */ + +enum class entry_address_type_flag +{ + /* The executable's entry frame. This is the first frame within the main + executable. */ + executable = (1 << 0), + + /* The inferior's entry frame. This is the first frame within the + inferior, this can be outside the main executable, e.g. for a + dynamically linked executable, this will be the first frame in the + dynamic linker. */ + inferior = (1 << 1), +}; +DEF_ENUM_FLAGS_TYPE (enum entry_address_type_flag, entry_address_type_flags); + +/* Return true if THIS_FRAME is inside the either of the possible entry + frames based on ENTRY_ADDR_TYPES, otherwise return false. */ + +static bool +inside_entry_func (const frame_info_ptr &this_frame, + entry_address_type_flags entry_addr_types) +{ + /* It doesn't make sense to call this with no flag bits set. */ + gdb_assert (entry_addr_types != (entry_address_type_flags) 0); + + CORE_ADDR frame_func_addr; + if (!get_frame_func_if_available (this_frame, &frame_func_addr)) + return false; + + const entry_point_info &ep_info + = current_program_space->get_entry_point_info (); + + /* For each flag set in ENTRY_ADDR_TYPES if FRAME_FUNC_ADDR matches the + corresponding entry address from EP_INFO then THIS_FRAME is an entry + frame. */ + return (((entry_addr_types & entry_address_type_flag::executable) + == entry_address_type_flag::executable + && ep_info.exec_entry_address () == frame_func_addr) + || ((entry_addr_types & entry_address_type_flag::inferior) + == entry_address_type_flag::inferior + && ep_info.inferior_entry_address () == frame_func_addr)); +} + /* 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. @@ -744,6 +790,15 @@ 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) { + /* Avoid returning a frame which is possibly before the inferior's entry + frame, any such frame is likely invalid. However, if the user has + turned on 'backtrace past-entry' then we assume they know what they + are doing and allow these frames. */ + if (!user_set_backtrace_options.backtrace_past_entry + && inside_entry_func (initial_next_frame, + entry_address_type_flag::inferior)) + return nullptr; + frame_info_ptr this_frame = get_prev_frame_always (initial_next_frame); if (this_frame == nullptr) return nullptr; @@ -2698,19 +2753,6 @@ inside_main_func (const frame_info_ptr &this_frame) return sym_addr == get_frame_func (this_frame); } -/* Test whether THIS_FRAME is inside the process entry point function. */ - -static bool -inside_entry_func (const frame_info_ptr &this_frame) -{ - const entry_point_info &ep_info - = current_program_space->get_entry_point_info (); - - CORE_ADDR frame_func_addr = get_frame_func (this_frame); - return (ep_info.exec_entry_address () == frame_func_addr - || ep_info.inferior_entry_address () == frame_func_addr); -} - /* Return a structure containing various interesting information about the frame that called THIS_FRAME. Returns NULL if there is either no such frame or the frame fails any of a set of target-independent @@ -2793,7 +2835,9 @@ get_prev_frame (const frame_info_ptr &this_frame) && get_frame_type (this_frame) == NORMAL_FRAME && !user_set_backtrace_options.backtrace_past_entry && frame_pc.has_value () - && inside_entry_func (this_frame)) + && inside_entry_func (this_frame, + (entry_address_type_flag::inferior + | entry_address_type_flag::executable))) { 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..6a0e311ef41 --- /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 <http://www.gnu.org/licenses/>. */ + +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..e863c070bd4 --- /dev/null +++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp @@ -0,0 +1,173 @@ +# 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 <http://www.gnu.org/licenses/>. + +# Check that 'until' in the outermost frame works as expected. + +standard_testfile + +if { [build_executable "failed to build" $testfile $srcfile] } { + return +} + +set testfile_static ${testfile}-static +if { [build_executable "failed to build" $testfile_static $srcfile \ + { debug additional_flags=-static } ] } { + return +} + +# Use 'frame' 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 "frame" $testname { + -re -wrap "#0\\s+(?:$::hex in )?(\[^( \]+) \\(.*" { + set func_name $expect_out(1,string) + } + + -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 testfile } { + clean_restart $testfile + + if {$::use_gdb_stub && [target_info exists gdb,do_reload_on_run]} { + # This is the path taken by the 'native-gdbserver' board. + # Start gdbserver and connect, this should leave the inferior + # at the first instruction. + if { [gdb_reload] != 0 } { + return -1 + } + pass "connected to gdbserver" + } else { + # Start inferior with 'starti' and then wait for a prompt. + if { [gdb_starti_cmd] < 0 } { + untested starti + return + } + gdb_test "" "Program stopped.*" "prompt after 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" -lbl { + -re "\r\n=> $::hex \[^\r\n\]+(?=\r\n)" { + set capture_address true + exp_continue + } + + -re "\r\n\\s+($::hex) \[^\r\n\]+(?=\r\n)" { + if { $capture_address } { + set until_address $expect_out(1,string) + set capture_address false + } + + exp_continue + } + + -re "\r\n$::gdb_prompt $" { + set found_address [expr { $until_address ne "" }] + if { !$found_address } { + unsupported "$gdb_test_name (no instruction found)" + return + } + pass $gdb_test_name + } + } + + # 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 "\r\n(?:$::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 $testfile + + with_test_prefix "static" { + run_test $use_stepi $past_entry $testfile_static + } + } +} -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCHv8 4/4] gdb: cache program space entry point information 2026-07-20 9:52 ` [PATCHv8 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess ` (2 preceding siblings ...) 2026-07-20 9:52 ` [PATCHv8 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess @ 2026-07-20 9:52 ` Andrew Burgess 3 siblings, 0 replies; 54+ messages in thread From: Andrew Burgess @ 2026-07-20 9:52 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess After the previous two patches, there are now two places where we check if a frame is an entry point frame, these are in get_prev_frame and frame_unwind_caller_frame. Both of these locations call inside_entry_func, which then calls program_space::get_entry_point_info. The calls to program_space::get_entry_point_info are not crazy expensive, but they are not free either, there are reads from target memory to read the auxv vector and the entry address offset, so on remote targets this could introduce a small delay. However, the result from program_space::get_entry_point_info is not expected to change from one call to the next. This information should be a property of the executable and libraries, so we really only need to figure it out once. This commit caches the entry point information within program_space, clearing it whenever an inferior starts or exits, or whenever the executable is updated. After clearing GDB will compute, and cache the updated information the next time it is needed, which will be whenever GDB needs to unwind a frame. It will be possible to observe this change by, for example, monitoring the remote target packets, but as far as the normal GDB output is concerned, there should be no user visible changes after this commit. --- gdb/progspace.c | 45 ++++++++++++++++++++++++++++++++++++++++++--- gdb/progspace.h | 9 ++++++++- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/gdb/progspace.c b/gdb/progspace.c index 4de32bc522f..b1e313f97ff 100644 --- a/gdb/progspace.c +++ b/gdb/progspace.c @@ -289,9 +289,12 @@ program_space::exec_entry_point_address () const /* See progspace.h. */ -entry_point_info +const entry_point_info & program_space::get_entry_point_info () const { + if (m_entry_point_info.has_value ()) + return m_entry_point_info.value (); + std::optional<CORE_ADDR> exec_entry_address = this->exec_entry_point_address_if_available (); @@ -299,8 +302,9 @@ program_space::get_entry_point_info () const if (m_solib_ops != nullptr) inferior_entry_address = m_solib_ops->inferior_entry_point_address (); - return entry_point_info (std::move (inferior_entry_address), - std::move (exec_entry_address)); + m_entry_point_info.emplace (std::move (inferior_entry_address), + std::move (exec_entry_address)); + return m_entry_point_info.value (); } /* Implement the 'maint info entry-address' command. */ @@ -538,11 +542,46 @@ program_space::clear_solib_cache () deleted_solibs.clear (); } +/* Clear cached entry point information in the program space of INF. */ + +static void +clear_cached_entry_point_info_for_inferior (inferior *inf) +{ + inf->pspace->clear_cached_entry_point_info (); +} + +/* Clear cached entry point information in the program space PSPACE. */ + +static void +clear_cached_entry_point_info_for_pspace (program_space *pspace, + bool /* reload */) +{ + pspace->clear_cached_entry_point_info (); +} + +/* Clear cached entry point information in the program space of EXEC_INF. */ + +static void +clear_cached_entry_point_info_after_exec (inferior *exec_inf, + inferior */* follow_inf */) +{ + exec_inf->pspace->clear_cached_entry_point_info (); +} + /* See progspace.h. */ void initialize_progspace () { + gdb::observers::inferior_created.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::inferior_exit.attach + (clear_cached_entry_point_info_for_inferior, "program-space"); + gdb::observers::executable_changed.attach + (clear_cached_entry_point_info_for_pspace, "program-space"); + gdb::observers::inferior_execd.attach + (clear_cached_entry_point_info_after_exec, "program-space"); + add_cmd ("program-spaces", class_maintenance, maintenance_info_program_spaces_command, _("Info about currently known program spaces."), diff --git a/gdb/progspace.h b/gdb/progspace.h index be2d3f7e88c..bfe0e5ec36f 100644 --- a/gdb/progspace.h +++ b/gdb/progspace.h @@ -370,7 +370,11 @@ struct program_space /* Return information about the entry point in the main executable, and the entry point for the inferior, which might be different from the main executable. */ - entry_point_info get_entry_point_info () const; + const entry_point_info &get_entry_point_info () const; + + /* Clear any cached entry point information. */ + void clear_cached_entry_point_info () + { m_entry_point_info.reset (); } /* If there is a valid and known entry point in the main executable of this program space, return it. Otherwise return an empty optional. */ @@ -463,6 +467,9 @@ struct program_space /* See `exec_filename`. */ gdb::unique_xmalloc_ptr<char> m_exec_filename; + + /* Cached entry point information. See get_entry_point_info. */ + mutable std::optional<entry_point_info> m_entry_point_info; }; /* The list of all program spaces. There's always at least one. */ -- 2.25.4 ^ permalink raw reply [flat|nested] 54+ messages in thread
end of thread, other threads:[~2026-07-20 9:54 UTC | newest] Thread overview: 54+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-06-08 18:14 [PATCH] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-11 19:24 ` Guinevere Larsen 2026-06-11 20:40 ` Andrew Burgess 2026-06-12 12:49 ` Guinevere Larsen 2026-06-11 21:59 ` [PATCHv2 0/3] " Andrew Burgess 2026-06-11 21:59 ` [PATCHv2 1/3] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-06-12 15:41 ` Pedro Alves 2026-06-11 21:59 ` [PATCHv2 2/3] gdb: introduce program_space::get_entry_point_info function Andrew Burgess 2026-06-12 6:07 ` Eli Zaretskii 2026-06-15 10:14 ` Andrew Burgess 2026-06-15 12:01 ` Eli Zaretskii 2026-06-12 15:41 ` Pedro Alves 2026-06-15 10:29 ` Andrew Burgess 2026-06-16 18:36 ` Pedro Alves 2026-06-23 9:46 ` Andrew Burgess 2026-06-23 10:20 ` Pedro Alves 2026-06-11 21:59 ` [PATCHv2 3/3] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-12 15:57 ` Pedro Alves 2026-06-16 19:47 ` [PATCHv3 0/4] " Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess 2026-06-17 11:52 ` Eli Zaretskii 2026-06-16 19:47 ` [PATCHv3 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-16 19:47 ` [PATCHv3 4/4] gdb: cache program space entry point information Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-23 10:47 ` [PATCHv4 4/4] gdb: cache program space entry point information Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-06-25 15:26 ` [PATCHv5 4/4] gdb: cache program space entry point information Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess 2026-07-10 15:39 ` Simon Marchi 2026-07-10 14:24 ` [PATCHv6 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-10 17:00 ` Simon Marchi 2026-07-10 17:01 ` Simon Marchi 2026-07-16 15:06 ` Andrew Burgess 2026-07-10 14:24 ` [PATCHv6 4/4] gdb: cache program space entry point information Andrew Burgess 2026-07-10 17:07 ` Simon Marchi 2026-07-18 13:11 ` [PATCHv7 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-18 13:11 ` [PATCHv7 4/4] gdb: cache program space entry point information Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 0/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 1/4] gdb: rename program_space::entry_point_address* functions Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 2/4] gdb: introduce program_space::get_entry_point_info function Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 3/4] gdb: allow 'until' to work in outermost frame Andrew Burgess 2026-07-20 9:52 ` [PATCHv8 4/4] gdb: cache program space entry point information Andrew Burgess
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox