--- /dev/null
+# Copyright 2024 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 detaching while the inferior is running. Basically:
+#
+# (gdb) attach PID
+# (gdb) c&
+# (gdb) detach
+
+require can_spawn_for_attach
+
+standard_testfile
+
+if {[build_executable "failed to prepare" $testfile $srcfile {debug}] == -1} {
+ return
+}
+
+# The test proper. See description above.
+
+proc test {} {
+ global binfile gdb_prompt
+
+ # This test requires executing commands while the target is
+ # running, which, when testing with the remote target, requires
+ # non-stop remote protocol. Until that variant of the RSP is the
+ # default, force target non-stop mode on.
+ set is_remote \
+ [expr {[target_info exists gdb_protocol] \
+ && ([target_info gdb_protocol] == "remote" \
+ || [target_info gdb_protocol] == "extended-remote")}]
+
+ save_vars { ::GDBFLAGS } {
+ if {$is_remote} {
+ append ::GDBFLAGS " -ex \"maint set target-non-stop on\""
+ }
+ clean_restart ${binfile}
+ }
+
+ set test_spawn_id [spawn_wait_for_attach $binfile]
+ set testpid [spawn_id_get_pid $test_spawn_id]
+
+ set any "\[^\r\n\]*"
+
+ # Iterate more than once so that we test re-attaching after
+ # detaching, in case GDB incorrectly detaches and the process
+ # crashes after the detach.
+ set n_iters 2
+ for {set iter 1} {$iter <= $n_iters} {incr iter} {
+ with_test_prefix "iter=$iter" {
+ set attached 0
+
+ gdb_test_multiple "attach $testpid" "attach" {
+ -re "Attaching to program:${any}process $testpid\r\n.*$gdb_prompt " {
+ pass $gdb_test_name
+ set attached 1
+ }
+ }
+
+ if {!$attached} {
+ break
+ }
+
+ gdb_test_multiple "continue &" "" {
+ -re "Continuing\.\r\n$::gdb_prompt " {
+ pass $gdb_test_name
+ }
+ }
+
+ gdb_test "detach" "Detaching from.*"
+
+ # Sleep a bit before reattaching to let the detached
+ # process crash and exit if e.g., GDB managed to leave
+ # breakpoint traps behind.
+ if {$iter != $n_iters} {
+ sleep 1
+ }
+ }
+ }
+
+ kill_wait_spawned_process $test_spawn_id
+}
+
+test
needed. */
void wait_for_debug_event_main_thread (DEBUG_EVENT *event);
+ /* Force the process_thread thread to return from WaitForDebugEvent.
+ PROCESS_ALIVE is set to false if the inferior process exits while
+ we're trying to break out the process_thread thread. This can
+ happen because this is called while all threads are running free,
+ while we're trying to detach. */
+ void break_out_process_thread (bool &process_alive);
+
/* Queue used to send requests to process_thread. This is
implicitly locked. */
std::queue<gdb::function_view<bool ()>> m_queue;
/* True if currently in async mode. */
bool m_is_async = false;
+
+ /* True if we last called ContinueDebugEvent and the process_thread
+ thread is now waiting for events. False if WaitForDebugEvent
+ already returned an event, and we need to ContinueDebugEvent
+ again to restart the inferior. */
+ bool m_continued = false;
};
static void
wait_for_debug_event (event, INFINITE);
return false;
});
+
+ m_continued = false;
}
/* See nat/windows-nat.h. */
" - ContinueDebugEvent failed"),
*err);
+ m_continued = !last_call;
+
return TRUE;
}
target_terminal::ours ();
}
+void
+windows_nat_target::break_out_process_thread (bool &process_alive)
+{
+ /* This is called when the process_thread thread is blocked in
+ WaitForDebugEvent (unless it already returned some event we
+ haven't consumed yet), and we need to unblock it so that we can
+ have it call DebugActiveProcessStop.
+
+ To make WaitForDebugEvent return, we need to force some event in
+ the inferior. Any method that lets us do that (without
+ disturbing the other threads), injects a new thread in the
+ inferior.
+
+ We don't use DebugBreakProcess for this, because that injects a
+ thread that ends up executing a breakpoint instruction. We can't
+ let the injected thread hit that breakpoint _after_ we've
+ detached. Consuming events until we see a breakpoint trap isn't
+ 100% reliable, because we can't distinguish it from some other
+ thread itself deciding to call int3 while we're detaching, unless
+ we temporarily suspend all threads. It's just a lot of
+ complication, and there's an easier way.
+
+ Important observation: the thread creation event for the newly
+ injected thread is sufficient to unblock WaitForDebugEvent.
+
+ Instead of DebugBreakProcess, we can instead use
+ CreateRemoteThread to control the code that the injected thread
+ runs ourselves. We could consider pointing the injected thread
+ at some side-effect-free Win32 function as entry point. However,
+ finding the address of such a function requires having at least
+ minimal symbols loaded for ntdll.dll. Having a way that avoids
+ that is better, so that detach always works correctly even when
+ we don't have any symbols loaded.
+
+ So what we do is inject a thread that doesn't actually run ANY
+ userspace code, because we force-terminate it as soon as we see
+ its corresponding thread creation event. CreateRemoteThread
+ gives us the new thread's ID, which we can match with the thread
+ associated with the CREATE_THREAD_DEBUG_EVENT event. */
+
+ DWORD injected_thread_id = 0;
+ HANDLE injected_thread_handle
+ = CreateRemoteThread (windows_process.handle, NULL,
+ 0, (LPTHREAD_START_ROUTINE) 0,
+ NULL, 0, &injected_thread_id);
+
+ if (injected_thread_handle == NULL)
+ {
+ DWORD err = GetLastError ();
+
+ DEBUG_EVENTS ("CreateRemoteThread failed with %u", err);
+
+ if (err == ERROR_ACCESS_DENIED)
+ {
+ /* Creating the remote thread fails with ERROR_ACCESS_DENIED
+ if the process exited before we had a chance to inject
+ the thread. Continue with the loop below and consume the
+ process exit event anyhow, so that our caller can always
+ call windows_continue. */
+ }
+ else
+ throw_winerror_with_name (_("Can't detach from running process. "
+ "Interrupt it first."),
+ err);
+ }
+
+ process_alive = true;
+
+ /* At this point, the user has declared that they want to detach, so
+ any event that happens from this point on should be forwarded to
+ the inferior. */
+
+ for (;;)
+ {
+ DEBUG_EVENT current_event;
+ wait_for_debug_event_main_thread (¤t_event);
+
+ if (current_event.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT)
+ {
+ DEBUG_EVENTS ("got EXIT_PROCESS_DEBUG_EVENT");
+ process_alive = false;
+ break;
+ }
+
+ if (current_event.dwDebugEventCode == CREATE_THREAD_DEBUG_EVENT
+ && current_event.dwThreadId == injected_thread_id)
+ {
+ DEBUG_EVENTS ("got CREATE_THREAD_DEBUG_EVENT for injected thread");
+
+ /* Terminate the injected thread, so it doesn't run any code
+ at all. All we wanted was some event, and
+ CREATE_THREAD_DEBUG_EVENT is sufficient. */
+ CHECK (TerminateThread (injected_thread_handle, 0));
+ break;
+ }
+
+ DEBUG_EVENTS ("got unrelated event, code %u",
+ current_event.dwDebugEventCode);
+ windows_continue (DBG_CONTINUE, -1, 0);
+ }
+
+ if (injected_thread_handle != NULL)
+ CHECK (CloseHandle (injected_thread_handle));
+}
+
void
windows_nat_target::detach (inferior *inf, int from_tty)
{
+ /* If we see the process exit while unblocking the process_thread
+ helper thread, then we should skip the actual
+ DebugActiveProcessStop call. But don't report an error. Just
+ pretend the process exited shortly after the detach. */
+ bool process_alive = true;
+
+ /* The process_thread helper thread will be blocked in
+ WaitForDebugEvent waiting for events if we've resumed the target
+ before we get here, e.g., with "attach&" or "c&". We need to
+ unblock it so that we can have it call DebugActiveProcessStop
+ below, in the do_synchronously block. */
+ if (m_continued)
+ break_out_process_thread (process_alive);
+
windows_continue (DBG_CONTINUE, -1, 0, true);
std::optional<unsigned> err;
- do_synchronously ([&] ()
- {
- if (!DebugActiveProcessStop (windows_process.current_event.dwProcessId))
- err = (unsigned) GetLastError ();
- else
- DebugSetProcessKillOnExit (FALSE);
- return false;
- });
+ if (process_alive)
+ do_synchronously ([&] ()
+ {
+ if (!DebugActiveProcessStop (windows_process.current_event.dwProcessId))
+ err = (unsigned) GetLastError ();
+ else
+ DebugSetProcessKillOnExit (FALSE);
+ return false;
+ });
if (err.has_value ())
{