diff options
-rw-r--r-- | gdb/infrun.c | 88 | ||||
-rw-r--r-- | gdb/testsuite/gdb.threads/foll-fork-other-thread.c | 84 | ||||
-rw-r--r-- | gdb/testsuite/gdb.threads/foll-fork-other-thread.exp | 172 |
3 files changed, 335 insertions, 9 deletions
diff --git a/gdb/infrun.c b/gdb/infrun.c index 04d7689..beb9ca7 100644 --- a/gdb/infrun.c +++ b/gdb/infrun.c @@ -757,13 +757,56 @@ follow_fork () if (tp == cur_thr) continue; - if (tp->pending_follow.kind () != TARGET_WAITKIND_SPURIOUS) + /* follow_fork_inferior clears tp->pending_follow, and below + we'll need the value after the follow_fork_inferior + call. */ + target_waitkind kind = tp->pending_follow.kind (); + + if (kind != TARGET_WAITKIND_SPURIOUS) { infrun_debug_printf ("need to follow-fork [%s] first", tp->ptid.to_string ().c_str ()); switch_to_thread (tp); - should_resume = false; + + /* Set up inferior(s) as specified by the caller, and + tell the target to do whatever is necessary to follow + either parent or child. */ + if (follow_child) + { + /* The thread that started the execution command + won't exist in the child. Abort the command and + immediately stop in this thread, in the child, + inside fork. */ + should_resume = false; + } + else + { + /* Following the parent, so let the thread fork its + child freely, it won't influence the current + execution command. */ + if (follow_fork_inferior (follow_child, detach_fork)) + { + /* Target refused to follow, or there's some + other reason we shouldn't resume. */ + switch_to_thread (cur_thr); + set_last_target_status_stopped (cur_thr); + return false; + } + + /* If we're following a vfork, when we need to leave + the just-forked thread as selected, as we need to + solo-resume it to collect the VFORK_DONE event. + If we're following a fork, however, switch back + to the original thread that we continue stepping + it, etc. */ + if (kind != TARGET_WAITKIND_VFORKED) + { + gdb_assert (kind == TARGET_WAITKIND_FORKED); + switch_to_thread (cur_thr); + } + } + break; } } @@ -2201,6 +2244,29 @@ user_visible_resume_target (ptid_t resume_ptid) : current_inferior ()->process_target ()); } +/* Find a thread from the inferiors that we'll resume that is waiting + for a vfork-done event. */ + +static thread_info * +find_thread_waiting_for_vfork_done () +{ + gdb_assert (!target_is_non_stop_p ()); + + if (sched_multi) + { + for (inferior *inf : all_non_exited_inferiors ()) + if (inf->thread_waiting_for_vfork_done != nullptr) + return inf->thread_waiting_for_vfork_done; + } + else + { + inferior *cur_inf = current_inferior (); + if (cur_inf->thread_waiting_for_vfork_done != nullptr) + return cur_inf->thread_waiting_for_vfork_done; + } + return nullptr; +} + /* Return a ptid representing the set of threads that we will resume, in the perspective of the target, assuming run control handling does not require leaving some threads stopped (e.g., stepping past @@ -2241,14 +2307,18 @@ internal_resume_ptid (int user_step) Since we don't have that flexibility (we can only pass one ptid), just resume the first thread waiting for a vfork-done event we find (e.g. thread 2.1). */ - if (sched_multi) - { - for (inferior *inf : all_non_exited_inferiors ()) - if (inf->thread_waiting_for_vfork_done != nullptr) - return inf->thread_waiting_for_vfork_done->ptid; + thread_info *thr = find_thread_waiting_for_vfork_done (); + if (thr != nullptr) + { + /* If we have a thread that is waiting for a vfork-done event, + then we should have switched to it earlier. Calling + target_resume with thread scope is only possible when the + current thread matches the thread scope. */ + gdb_assert (thr->ptid == inferior_ptid); + gdb_assert (thr->inf->process_target () + == inferior_thread ()->inf->process_target ()); + return thr->ptid; } - else if (current_inferior ()->thread_waiting_for_vfork_done != nullptr) - return current_inferior ()->thread_waiting_for_vfork_done->ptid; return user_visible_resume_ptid (user_step); } diff --git a/gdb/testsuite/gdb.threads/foll-fork-other-thread.c b/gdb/testsuite/gdb.threads/foll-fork-other-thread.c new file mode 100644 index 0000000..47e8b1c --- /dev/null +++ b/gdb/testsuite/gdb.threads/foll-fork-other-thread.c @@ -0,0 +1,84 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2022 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/>. */ + +#include <pthread.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <errno.h> +#include <assert.h> +#include <limits.h> + +/* Set by GDB. */ +volatile int stop_looping = 0; + +static void * +gdb_forker_thread (void *arg) +{ + int ret; + int stat; + pid_t pid = FORK_FUNC (); + + if (pid == 0) + _exit (0); + + assert (pid > 0); + + /* Wait for child to exit. */ + do + { + ret = waitpid (pid, &stat, 0); + } + while (ret == -1 && errno == EINTR); + + assert (ret == pid); + assert (WIFEXITED (stat)); + assert (WEXITSTATUS (stat) == 0); + + stop_looping = 1; + + return NULL; +} + +static void +sleep_a_bit (void) +{ + usleep (1000 * 50); +} + +int +main (void) +{ + int i; + int ret; + pthread_t thread; + + alarm (60); + + ret = pthread_create (&thread, NULL, gdb_forker_thread, NULL); + assert (ret == 0); + + while (!stop_looping) /* while loop */ + { + sleep_a_bit (); /* break here */ + sleep_a_bit (); /* other line */ + } + + pthread_join (thread, NULL); + + return 0; /* exiting here */ +} diff --git a/gdb/testsuite/gdb.threads/foll-fork-other-thread.exp b/gdb/testsuite/gdb.threads/foll-fork-other-thread.exp new file mode 100644 index 0000000..ed62bfc --- /dev/null +++ b/gdb/testsuite/gdb.threads/foll-fork-other-thread.exp @@ -0,0 +1,172 @@ +# Copyright 2022 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/>. + +# Test catching a vfork/fork in one thread, and then doing a "next" in +# another thread, in different combinations of "set follow-fork +# parent/child", and other execution modes. + +standard_testfile + +# Line where to stop the main thread. +set break_here_line [gdb_get_line_number "break here"] + +# Build executables, one for each fork flavor. +foreach_with_prefix fork_func {fork vfork} { + set opts [list debug pthreads additional_flags=-DFORK_FUNC=${fork_func}] + if { [build_executable "failed to prepare" \ + ${testfile}-${fork_func} ${srcfile} $opts] } { + return + } +} + +# Run the test with the given parameters: +# +# - FORK_FUNC: fork flavor, "fork" or "vfork". +# - FOLLOW: "set follow-fork" value, either "parent" or "child". +# - TARGET-NON-STOP: "maintenance set target-non-stop" value, "auto", "on" or +# "off". +# - NON-STOP: "set non-stop" value, "on" or "off". +# - DISPLACED-STEPPING: "set displaced-stepping" value, "auto", "on" or "off". + +proc do_test { fork_func follow target-non-stop non-stop displaced-stepping } { + save_vars { ::GDBFLAGS } { + append ::GDBFLAGS " -ex \"maintenance set target-non-stop ${target-non-stop}\"" + append ::GDBFLAGS " -ex \"set non-stop ${non-stop}\"" + clean_restart ${::binfile}-${fork_func} + } + + gdb_test_no_output "set displaced-stepping ${displaced-stepping}" + + if { ![runto_main] } { + return + } + + delete_breakpoints + + gdb_test "catch $fork_func" "Catchpoint .*" + + # Verify that the catchpoint is mentioned in an "info breakpoints", + # and further that the catchpoint mentions no process id. + gdb_test "info breakpoints" \ + ".*catchpoint.*keep y.*fork\[\r\n\]+" \ + "info breakpoints before fork" + + gdb_test "continue" \ + "Catchpoint \[0-9\]* \\(.?forked process \[0-9\]*\\),.*" \ + "explicit child follow, catch fork" + + # Verify that the catchpoint is mentioned in an "info breakpoints", + # and further that the catchpoint managed to capture a process id. + gdb_test "info breakpoints" \ + ".*catchpoint.*keep y.*fork, process.*" \ + "info breakpoints after fork" + + gdb_test "thread 1" "Switching to .*" + + gdb_test_no_output "set scheduler-locking on" + + # Advance the next-ing thread to the point where we'll execute the + # next. + gdb_test "break $::srcfile:$::break_here_line" "Breakpoint $::decimal at $::hex.*" + gdb_test "continue" "hit Breakpoint $::decimal, main.*" + + # Disable schedlock and step. The pending fork should no longer + # be pending afterwards. + + gdb_test "set scheduler-locking off" + + # Make sure GDB doesn't try to step over the breakpoint at PC + # first, we want to make sure that GDB doesn't lose focus of the + # step/next in this thread. A breakpoint would make GDB switch + # focus anyhow, thus hide a potential bug. + delete_breakpoints + + gdb_test_no_output "set follow-fork $follow" + + set any "\[^\r\n\]*" + + if {$follow == "child"} { + + # For fork, GDB detaches from the parent at follow-fork time. + # For vfork, GDB detaches from the parent at child exit/exec + # time. + if {$fork_func == "fork"} { + set detach_parent \ + [multi_line \ + "\\\[Detaching after $fork_func from parent process $any\\\]" \ + "\\\[Inferior 1 $any detached\\\]"] + } else { + set detach_parent "" + } + + gdb_test "next" \ + [multi_line \ + "\\\[Attaching after $any $fork_func to child $any\\\]" \ + "\\\[New inferior 2 $any\\\]" \ + "$detach_parent.*warning: Not resuming: switched threads before following fork child\\." \ + "\\\[Switching to $any\\\]" \ + ".*"] \ + "next aborts resumption" + + # The child should be stopped inside the fork implementation + # in the runtime. Exactly at which instruction/function is + # system dependent, but we can check that our + # "gdb_forker_thread" function appears in the backtrace. + gdb_test "bt" " in gdb_forker_thread ${any} at ${any}${::srcfile}:.*" + + # The child is now thread 1. + gdb_test "print \$_thread" " = 1" + + if {$fork_func == "fork"} { + gdb_test "continue" \ + [multi_line \ + "Continuing." \ + "\\\[Inferior 2 \\\(process $any\\\) exited normally\\\]"] \ + "continue to exit" + } else { + gdb_test "continue" \ + [multi_line \ + "Continuing." \ + "\\\[Detaching vfork parent process $any after child exit\\\]" \ + "\\\[Inferior 1 \\\(process $any\\\) detached\\\]" \ + "\\\[Inferior 2 \\\(process $any\\\) exited normally\\\]"] \ + "continue to exit" + } + } else { + gdb_test "next" \ + "\\\[Detaching after $fork_func from child process ${any}\\\].* other line .*" \ + "next to other line" + + gdb_test "print \$_thread" " = 1" + + gdb_test "continue" \ + [multi_line \ + "Continuing." \ + "\\\[Inferior 1 \\\(process $any\\\) exited normally\\\]"] \ + "continue to exit" + } +} + +foreach_with_prefix fork_func {fork vfork} { + foreach_with_prefix follow {child} { + foreach_with_prefix target-non-stop {auto on off} { + foreach_with_prefix non-stop {off} { + foreach_with_prefix displaced-stepping {auto on off} { + do_test ${fork_func} ${follow} ${target-non-stop} ${non-stop} ${displaced-stepping} + } + } + } + } +} |