aboutsummaryrefslogtreecommitdiff
path: root/gdbstub/user.c
diff options
context:
space:
mode:
Diffstat (limited to 'gdbstub/user.c')
-rw-r--r--gdbstub/user.c212
1 files changed, 210 insertions, 2 deletions
diff --git a/gdbstub/user.c b/gdbstub/user.c
index 1a7b582a40..7f9f19a124 100644
--- a/gdbstub/user.c
+++ b/gdbstub/user.c
@@ -25,6 +25,61 @@
#define GDB_NR_SYSCALLS 1024
typedef unsigned long GDBSyscallsMask[BITS_TO_LONGS(GDB_NR_SYSCALLS)];
+/*
+ * Forked child talks to its parent in order to let GDB enforce the
+ * follow-fork-mode. This happens inside a start_exclusive() section, so that
+ * the other threads, which may be forking too, do not interfere. The
+ * implementation relies on GDB not sending $vCont until it has detached
+ * either from the parent (follow-fork-mode child) or from the child
+ * (follow-fork-mode parent).
+ *
+ * The parent and the child share the GDB socket; at any given time only one
+ * of them is allowed to use it, as is reflected in the respective fork_state.
+ * This is negotiated via the fork_sockets pair as a reaction to $Hg.
+ *
+ * Below is a short summary of the possible state transitions:
+ *
+ * ENABLED : Terminal state.
+ * DISABLED : Terminal state.
+ * ACTIVE : Parent initial state.
+ * INACTIVE : Child initial state.
+ * ACTIVE -> DEACTIVATING: On $Hg.
+ * ACTIVE -> ENABLING : On $D.
+ * ACTIVE -> DISABLING : On $D.
+ * ACTIVE -> DISABLED : On communication error.
+ * DEACTIVATING -> INACTIVE : On gdb_read_byte() return.
+ * DEACTIVATING -> DISABLED : On communication error.
+ * INACTIVE -> ACTIVE : On $Hg in the peer.
+ * INACTIVE -> ENABLE : On $D in the peer.
+ * INACTIVE -> DISABLE : On $D in the peer.
+ * INACTIVE -> DISABLED : On communication error.
+ * ENABLING -> ENABLED : On gdb_read_byte() return.
+ * ENABLING -> DISABLED : On communication error.
+ * DISABLING -> DISABLED : On gdb_read_byte() return.
+ */
+enum GDBForkState {
+ /* Fully owning the GDB socket. */
+ GDB_FORK_ENABLED,
+ /* Working with the GDB socket; the peer is inactive. */
+ GDB_FORK_ACTIVE,
+ /* Handing off the GDB socket to the peer. */
+ GDB_FORK_DEACTIVATING,
+ /* The peer is working with the GDB socket. */
+ GDB_FORK_INACTIVE,
+ /* Asking the peer to close its GDB socket fd. */
+ GDB_FORK_ENABLING,
+ /* Asking the peer to take over, closing our GDB socket fd. */
+ GDB_FORK_DISABLING,
+ /* The peer has taken over, our GDB socket fd is closed. */
+ GDB_FORK_DISABLED,
+};
+
+enum GDBForkMessage {
+ GDB_FORK_ACTIVATE = 'a',
+ GDB_FORK_ENABLE = 'e',
+ GDB_FORK_DISABLE = 'd',
+};
+
/* User-mode specific state */
typedef struct {
int fd;
@@ -36,6 +91,10 @@ typedef struct {
*/
bool catch_all_syscalls;
GDBSyscallsMask catch_syscalls_mask;
+ bool fork_events;
+ enum GDBForkState fork_state;
+ int fork_sockets[2];
+ pid_t fork_peer_pid, fork_peer_tid;
} GDBUserState;
static GDBUserState gdbserver_user_state;
@@ -358,6 +417,18 @@ int gdbserver_start(const char *port_or_path)
void gdbserver_fork_start(void)
{
+ if (!gdbserver_state.init || gdbserver_user_state.fd < 0) {
+ return;
+ }
+ if (!gdbserver_user_state.fork_events ||
+ qemu_socketpair(AF_UNIX, SOCK_STREAM, 0,
+ gdbserver_user_state.fork_sockets) < 0) {
+ gdbserver_user_state.fork_state = GDB_FORK_DISABLED;
+ return;
+ }
+ gdbserver_user_state.fork_state = GDB_FORK_INACTIVE;
+ gdbserver_user_state.fork_peer_pid = getpid();
+ gdbserver_user_state.fork_peer_tid = qemu_get_thread_id();
}
static void disable_gdbstub(CPUState *thread_cpu)
@@ -376,23 +447,160 @@ static void disable_gdbstub(CPUState *thread_cpu)
void gdbserver_fork_end(CPUState *cpu, pid_t pid)
{
- if (pid != 0 || !gdbserver_state.init || gdbserver_user_state.fd < 0) {
+ char b;
+ int fd;
+
+ if (!gdbserver_state.init || gdbserver_user_state.fd < 0) {
return;
}
- disable_gdbstub(cpu);
+
+ if (pid == -1) {
+ if (gdbserver_user_state.fork_state != GDB_FORK_DISABLED) {
+ g_assert(gdbserver_user_state.fork_state == GDB_FORK_INACTIVE);
+ close(gdbserver_user_state.fork_sockets[0]);
+ close(gdbserver_user_state.fork_sockets[1]);
+ }
+ return;
+ }
+
+ if (gdbserver_user_state.fork_state == GDB_FORK_DISABLED) {
+ if (pid == 0) {
+ disable_gdbstub(cpu);
+ }
+ return;
+ }
+
+ if (pid == 0) {
+ close(gdbserver_user_state.fork_sockets[0]);
+ fd = gdbserver_user_state.fork_sockets[1];
+ g_assert(gdbserver_state.process_num == 1);
+ g_assert(gdbserver_state.processes[0].pid ==
+ gdbserver_user_state.fork_peer_pid);
+ g_assert(gdbserver_state.processes[0].attached);
+ gdbserver_state.processes[0].pid = getpid();
+ } else {
+ close(gdbserver_user_state.fork_sockets[1]);
+ fd = gdbserver_user_state.fork_sockets[0];
+ gdbserver_user_state.fork_state = GDB_FORK_ACTIVE;
+ gdbserver_user_state.fork_peer_pid = pid;
+ gdbserver_user_state.fork_peer_tid = pid;
+
+ if (!gdbserver_state.allow_stop_reply) {
+ goto fail;
+ }
+ g_string_printf(gdbserver_state.str_buf,
+ "T%02xfork:p%02x.%02x;thread:p%02x.%02x;",
+ gdb_target_signal_to_gdb(gdb_target_sigtrap()),
+ pid, pid, (int)getpid(), qemu_get_thread_id());
+ gdb_put_strbuf();
+ }
+
+ gdbserver_state.state = RS_IDLE;
+ gdbserver_state.allow_stop_reply = false;
+ gdbserver_user_state.running_state = 0;
+ for (;;) {
+ switch (gdbserver_user_state.fork_state) {
+ case GDB_FORK_ENABLED:
+ if (gdbserver_user_state.running_state) {
+ return;
+ }
+ QEMU_FALLTHROUGH;
+ case GDB_FORK_ACTIVE:
+ if (read(gdbserver_user_state.fd, &b, 1) != 1) {
+ goto fail;
+ }
+ gdb_read_byte(b);
+ break;
+ case GDB_FORK_DEACTIVATING:
+ b = GDB_FORK_ACTIVATE;
+ if (write(fd, &b, 1) != 1) {
+ goto fail;
+ }
+ gdbserver_user_state.fork_state = GDB_FORK_INACTIVE;
+ break;
+ case GDB_FORK_INACTIVE:
+ if (read(fd, &b, 1) != 1) {
+ goto fail;
+ }
+ switch (b) {
+ case GDB_FORK_ACTIVATE:
+ gdbserver_user_state.fork_state = GDB_FORK_ACTIVE;
+ break;
+ case GDB_FORK_ENABLE:
+ close(fd);
+ gdbserver_user_state.fork_state = GDB_FORK_ENABLED;
+ break;
+ case GDB_FORK_DISABLE:
+ gdbserver_user_state.fork_state = GDB_FORK_DISABLED;
+ break;
+ default:
+ g_assert_not_reached();
+ }
+ break;
+ case GDB_FORK_ENABLING:
+ b = GDB_FORK_DISABLE;
+ if (write(fd, &b, 1) != 1) {
+ goto fail;
+ }
+ close(fd);
+ gdbserver_user_state.fork_state = GDB_FORK_ENABLED;
+ break;
+ case GDB_FORK_DISABLING:
+ b = GDB_FORK_ENABLE;
+ if (write(fd, &b, 1) != 1) {
+ goto fail;
+ }
+ gdbserver_user_state.fork_state = GDB_FORK_DISABLED;
+ break;
+ case GDB_FORK_DISABLED:
+ close(fd);
+ disable_gdbstub(cpu);
+ return;
+ default:
+ g_assert_not_reached();
+ }
+ }
+
+fail:
+ close(fd);
+ if (pid == 0) {
+ disable_gdbstub(cpu);
+ }
}
void gdb_handle_query_supported_user(const char *gdb_supported)
{
+ if (strstr(gdb_supported, "fork-events+")) {
+ gdbserver_user_state.fork_events = true;
+ }
+ g_string_append(gdbserver_state.str_buf, ";fork-events+");
}
bool gdb_handle_set_thread_user(uint32_t pid, uint32_t tid)
{
+ if (gdbserver_user_state.fork_state == GDB_FORK_ACTIVE &&
+ pid == gdbserver_user_state.fork_peer_pid &&
+ tid == gdbserver_user_state.fork_peer_tid) {
+ gdbserver_user_state.fork_state = GDB_FORK_DEACTIVATING;
+ gdb_put_packet("OK");
+ return true;
+ }
return false;
}
bool gdb_handle_detach_user(uint32_t pid)
{
+ bool enable;
+
+ if (gdbserver_user_state.fork_state == GDB_FORK_ACTIVE) {
+ enable = pid == gdbserver_user_state.fork_peer_pid;
+ if (enable || pid == getpid()) {
+ gdbserver_user_state.fork_state = enable ? GDB_FORK_ENABLING :
+ GDB_FORK_DISABLING;
+ gdb_put_packet("OK");
+ return true;
+ }
+ }
return false;
}