Skip to content

Commit b1c0ab2

Browse files
committed
gdb: avoid double stop after failed breakpoint condition check
This commit replaces this earlier commit: commit 2e411b8 Date: Fri Oct 14 14:53:15 2022 +0100 gdb: don't always print breakpoint location after failed condition check and is a result of feedback received here[1]. The original commit addressed a problem where, if a breakpoint condition included an inferior function call, and if the inferior function call failed, then GDB would announce the stop twice. Here's an example of GDB's output before the above commit that shows the problem being addressed: (gdb) break foo if (some_func ()) Breakpoint 1 at 0x40111e: file bpcond.c, line 11. (gdb) r Starting program: /tmp/bpcond Program received signal SIGSEGV, Segmentation fault. 0x0000000000401116 in some_func () at bpcond.c:5 5 return *p; Error in testing condition for breakpoint 1: The program being debugged stopped while in a function called from GDB. Evaluation of the expression containing the function (some_func) will be abandoned. When the function is done executing, GDB will silently stop. Breakpoint 1, 0x0000000000401116 in some_func () at bpcond.c:5 5 return *p; (gdb) The original commit addressed this issue in breakpoint.c, by spotting that the $pc had changed while evaluating the breakpoint condition, and inferring from this that GDB must have stopped elsewhere. However, the way in which the original commit suppressed the second stop announcement was to set bpstat::print to true -- this tells GDB not to print the frame during the stop announcement, and for the CLI this is fine, however, it was pointed out that for the MI this still isn't really enough. Below is an example from an MI session after the above commit was applied, this shows the problem with the above commit: -break-insert -c "cond_fail()" foo ^done,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x000000000040111e",func="foo",file="/tmp/mi-condbreak-fail.c",line="30",thread-groups=["i1"],cond="cond_fail()",times="0",original-location="foo"} (gdb) -exec-run =thread-group-started,id="i1",pid="2636270" =thread-created,id="1",group-id="i1" =library-loaded,id="/lib64/ld-linux-x86-64.so.2",target-name="/lib64/ld-linux-x86-64.so.2",host-name="/lib64/ld-linux-x86-64.so.2",symbols-loaded="0",thread-group="i1",ranges=[{from="0x00007ffff7fd3110",to="0x00007ffff7ff2bb4"}] ^running *running,thread-id="all" (gdb) =library-loaded,id="/lib64/libm.so.6",target-name="/lib64/libm.so.6",host-name="/lib64/libm.so.6",symbols-loaded="0",thread-group="i1",ranges=[{from="0x00007ffff7e59390",to="0x00007ffff7ef4f98"}] =library-loaded,id="/lib64/libc.so.6",target-name="/lib64/libc.so.6",host-name="/lib64/libc.so.6",symbols-loaded="0",thread-group="i1",ranges=[{from="0x00007ffff7ca66b0",to="0x00007ffff7df3c5f"}] ~"\nProgram" ~" received signal SIGSEGV, Segmentation fault.\n" ~"0x0000000000401116 in cond_fail () at /tmp/mi-condbreak-fail.c:24\n" ~"24\t return *p;\t\t\t/* Crash here. */\n" *stopped,reason="signal-received",signal-name="SIGSEGV",signal-meaning="Segmentation fault",frame={addr="0x0000000000401116",func="cond_fail",args=[],file="/tmp/mi-condbreak-fail.c",fullname="/tmp/mi-condbreak-fail.c",line="24",arch="i386:x86-64"},thread-id="1",stopped-threads="all",core="9" &"Error in testing condition for breakpoint 1:\n" &"The program being debugged was signaled while in a function called from GDB.\n" &"GDB remains in the frame where the signal was received.\n" &"To change this behavior use \"set unwindonsignal on\".\n" &"Evaluation of the expression containing the function\n" &"(cond_fail) will be abandoned.\n" &"When the function is done executing, GDB will silently stop.\n" =breakpoint-modified,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x000000000040111e",func="foo",file="/tmp/mi-condbreak-fail.c",fullname="/tmp/mi-condbreak-fail.c",line="30",thread-groups=["i1"],cond="cond_fail()",times="1",original-location="foo"} *stopped (gdb) Notice that we still see two '*stopped' lines, the first includes the full frame information, while the second has no frame information, this is a result of bpstat::print having been set. Ideally, the second '*stopped' line should not be present. By setting bpstat::print I was addressing the problem too late, this flag really only changes how interp::on_normal_stop prints the stop event, and interp::on_normal_stop is called (indirectly) from the normal_stop function in infrun.c. A better solution is to avoid calling normal_stop at all for the stops which should not be reported to the user, and this is what I do in this commit. This commit has 3 parts: 1. In breakpoint.c, revert the above commit, 2. In fetch_inferior_event (infrun.c), capture the stop-id before calling handle_inferior_event. If, after calling handle_inferior_event, the stop-id has changed, then this indicates that somewhere within handle_inferior_event, a stop was announced to the user. If this is the case then GDB should not call normal_stop, and we should rely on whoever announced the stop to ensure that we are in a PROMPT_NEEDED state, which means the prompt will be displayed once fetch_inferior_event returns. And, 3. In infcall.c, do two things: (a) In run_inferior_call, after making the inferior call, ensure that either async_disable_stdin or async_enable_stdin is called to put the prompt state, and stdin handling into the correct state based on whether the inferior call completed successfully or not, and (b) In call_thread_fsm::should_stop, call async_enable_stdin rather than changing the prompt state directly. This isn't strictly necessary, but helped me understand this code more. This async_enable_stdin call is only reached if normal_stop is not going to be called, and replaces the async_enable_stdin call that exists in normal_stop. Though we could just adjust the prompt state if felt (to me) much easier to understand when I could see this call and the corresponding call in normal_stop. With these changes in place now, when the inferior call (from the breakpoint condition) fails, infcall.c leaves the prompt state as PROMPT_NEEDED, and leaves stdin registered with the event loop. Back in fetch_inferior_event GDB notices that the stop-id has changed and so avoids calling normal_stop. And on return from fetch_inferior_event GDB will display the prompt and handle input from stdin. As normal_stop is not called the MI problem is solved, and the test added in the earlier mentioned commit still passes just fine, so the CLI has not regressed. [1] https://inbox.sourceware.org/gdb-patches/6fd4aa13-6003-2563-5841-e80d5a55d59e@palves.net/
1 parent f559e52 commit b1c0ab2

File tree

5 files changed

+152
-23
lines changed

5 files changed

+152
-23
lines changed

gdb/breakpoint.c

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5535,7 +5535,6 @@ bpstat_check_breakpoint_conditions (bpstat *bs, thread_info *thread)
55355535
else
55365536
within_current_scope = false;
55375537
}
5538-
CORE_ADDR pc_before_check = get_frame_pc (get_selected_frame (nullptr));
55395538
if (within_current_scope)
55405539
{
55415540
try
@@ -5555,17 +5554,6 @@ bpstat_check_breakpoint_conditions (bpstat *bs, thread_info *thread)
55555554
(gdb_stderr, ex,
55565555
"Error in testing condition for breakpoint %d:\n",
55575556
b->number);
5558-
5559-
/* If the pc value changed as a result of evaluating the
5560-
condition then we probably stopped within an inferior
5561-
function call due to some unexpected stop, e.g. the thread
5562-
hit another breakpoint, or the thread received an
5563-
unexpected signal. In this case we don't want to also
5564-
print the information about this breakpoint. */
5565-
CORE_ADDR pc_after_check
5566-
= get_frame_pc (get_selected_frame (nullptr));
5567-
if (pc_before_check != pc_after_check)
5568-
bs->print = 0;
55695557
}
55705558
}
55715559
else

gdb/infcall.c

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -564,10 +564,13 @@ call_thread_fsm::should_stop (struct thread_info *thread)
564564
call.. */
565565
return_value = get_call_return_value (&return_meta_info);
566566

567-
/* Break out of wait_sync_command_done. */
567+
/* Break out of wait_sync_command_done. This is similar to the
568+
async_enable_stdin call in normal_stop (which we don't call),
569+
however, in this case we only change the WAITING_UI. This is
570+
enough for wait_sync_command_done. */
568571
scoped_restore save_ui = make_scoped_restore (&current_ui, waiting_ui);
569-
target_terminal::ours ();
570-
waiting_ui->prompt_state = PROMPT_NEEDED;
572+
gdb_assert (current_ui->prompt_state == PROMPT_BLOCKED);
573+
async_enable_stdin ();
571574
}
572575

573576
return true;
@@ -661,14 +664,32 @@ run_inferior_call (std::unique_ptr<call_thread_fsm> sm,
661664
infcall_debug_printf ("thread is now: %s",
662665
inferior_ptid.to_string ().c_str ());
663666

664-
/* If GDB has the prompt blocked before, then ensure that it remains
665-
so. normal_stop calls async_enable_stdin, so reset the prompt
666-
state again here. In other cases, stdin will be re-enabled by
667-
inferior_event_handler, when an exception is thrown. */
667+
/* After the inferior call finished, async_enable_stdin has been
668+
called, either from normal_stop or from
669+
call_thread_fsm::should_stop, and the prompt state has been
670+
restored by the scoped_restore in the try block above.
671+
672+
If the inferior call finished successfully, then we should
673+
disable stdin as we don't know yet whether the inferior will be
674+
stopping. Calling async_disable_stdin restores things to how
675+
they were when this function was called.
676+
677+
If the inferior call didn't complete successfully, then
678+
normal_stop has already been called, and we know for sure that we
679+
are going to present this stop to the user. In this case, we
680+
call async_enable_stdin. This changes the prompt state to
681+
PROMPT_NEEDED.
682+
683+
If the previous prompt state was PROMPT_NEEDED, then as
684+
async_enable_stdin has already been called, nothing additional
685+
needs to be done here. */
668686
if (current_ui->prompt_state == PROMPT_BLOCKED)
669-
current_ui->unregister_file_handler ();
670-
else
671-
current_ui->register_file_handler ();
687+
{
688+
if (call_thread->thread_fsm ()->finished_p ())
689+
async_disable_stdin ();
690+
else
691+
async_enable_stdin ();
692+
}
672693

673694
/* If the infcall does NOT succeed, normal_stop will have already
674695
finished the thread states. However, on success, normal_stop

gdb/infrun.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4458,6 +4458,8 @@ fetch_inferior_event ()
44584458
auto defer_delete_threads
44594459
= make_scope_exit (delete_just_stopped_threads_infrun_breakpoints);
44604460

4461+
int stop_id = get_stop_id ();
4462+
44614463
/* Now figure out what to do with the result of the result. */
44624464
handle_inferior_event (&ecs);
44634465

@@ -4485,7 +4487,19 @@ fetch_inferior_event ()
44854487

44864488
clean_up_just_stopped_threads_fsms (&ecs);
44874489

4488-
if (thr != nullptr && thr->thread_fsm () != nullptr)
4490+
if (stop_id != get_stop_id ())
4491+
{
4492+
/* If the stop-id has changed then a stop has already been
4493+
presented to the user in handle_inferior_event, this is
4494+
likely a failed inferior call. As the stop has already
4495+
been announced then we should not notify again.
4496+
4497+
Also, if the prompt state is not PROMPT_NEEDED then GDB
4498+
will not be ready for user input after this function. */
4499+
should_notify_stop = false;
4500+
gdb_assert (current_ui->prompt_state == PROMPT_NEEDED);
4501+
}
4502+
else if (thr != nullptr && thr->thread_fsm () != nullptr)
44894503
should_notify_stop
44904504
= thr->thread_fsm ()->should_notify_stop ();
44914505

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* Copyright 2023 Free Software Foundation, Inc.
2+
3+
This file is part of GDB.
4+
5+
This program is free software; you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published by
7+
the Free Software Foundation; either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program. If not, see <http://www.gnu.org/licenses/>. */
17+
18+
volatile int global_counter = 0;
19+
20+
int
21+
cond_fail ()
22+
{
23+
volatile int *p = 0;
24+
return *p; /* Crash here. */
25+
}
26+
27+
int
28+
foo ()
29+
{
30+
global_counter += 1; /* Set breakpoint here. */
31+
return 0;
32+
}
33+
34+
int
35+
main ()
36+
{
37+
int res = foo ();
38+
return res;
39+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (C) 2023 Free Software Foundation, Inc.
2+
3+
# This program is free software; you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation; either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
# Check that when GDB fails to evaluate the condition of a conditional
17+
# breakpoint we only get one *stopped notification.
18+
19+
load_lib mi-support.exp
20+
set MIFLAGS "-i=mi"
21+
22+
standard_testfile
23+
24+
if [build_executable ${testfile}.exp ${binfile} ${srcfile}] {
25+
return -1
26+
}
27+
28+
if {[mi_clean_restart $binfile]} {
29+
return
30+
}
31+
32+
if {[mi_runto_main] == -1} {
33+
return
34+
}
35+
36+
# Create the conditional breakpoint.
37+
set bp_location [gdb_get_line_number "Set breakpoint here"]
38+
mi_create_breakpoint "-c \"cond_fail ()\" $srcfile:$bp_location" \
39+
"insert conditional breakpoint" \
40+
-func foo -file ".*$srcfile" -line "$bp_location" \
41+
-cond "cond_fail \\(\\)"
42+
43+
# Number of the previous breakpoint.
44+
set bpnum [mi_get_valueof "/d" "\$bpnum" "INVALID" \
45+
"get number for breakpoint"]
46+
47+
# The line where we expect the inferior to crash.
48+
set crash_linenum [gdb_get_line_number "Crash here"]
49+
50+
# Run the inferior and wait for it to stop.
51+
mi_send_resuming_command "exec-continue" "continue the inferior"
52+
mi_gdb_test "" \
53+
[multi_line \
54+
"~\"\\\\nProgram\"" \
55+
"~\" received signal SIGSEGV, Segmentation fault\\.\\\\n\"" \
56+
"~\"$hex in cond_fail \\(\\) at \[^\r\n\]+\"" \
57+
"~\"${crash_linenum}\\\\t\\s+return \\*p;\[^\r\n\]+\\\\n\"" \
58+
"\\*stopped,reason=\"signal-received\",signal-name=\"SIGSEGV\"\[^\r\n\]+" \
59+
"&\"Error in testing condition for breakpoint $bpnum:\\\\n\"" \
60+
"&\"The program being debugged was signaled while in a function called from GDB\\.\\\\n\"" \
61+
"&\"GDB remains in the frame where the signal was received\\.\\\\n\"" \
62+
"&\"To change this behavior use \\\\\"set unwindonsignal on\\\\\"\\.\\\\n\"" \
63+
"&\"Evaluation of the expression containing the function\\\\n\"" \
64+
"&\"\\(cond_fail\\) will be abandoned\\.\\\\n\"" \
65+
"&\"When the function is done executing, GDB will silently stop\\.\\\\n\"" \
66+
"=breakpoint-modified,bkpt={number=\"$bpnum\",type=\"breakpoint\",\[^\r\n\]+times=\"1\",\[^\r\n\]+}"] \
67+
"wait for stop"

0 commit comments

Comments
 (0)