Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 22 Aug 2019 00:34:11 +0000 (UTC)
From:      Conrad Meyer <cem@FreeBSD.org>
To:        src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-head@freebsd.org
Subject:   svn commit: r351368 - head/sys/gdb
Message-ID:  <201908220034.x7M0YBOw088032@repo.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: cem
Date: Thu Aug 22 00:34:11 2019
New Revision: 351368
URL: https://svnweb.freebsd.org/changeset/base/351368

Log:
  gdb(4): Implement qXfer:threads:read
  
  This streams out an XML document over several GDB packets describing all
  threads in the system; their ids, name, and any loosely defined "extra info"
  we feel like including.  For now, I have included a string version of the run
  state, similar to some of the DDB logic to stringify thread state.
  
  The benefit of supporting this in addition to the qfThreadInfo/qsThreadInfo
  packing is that in this mode, the host gdb does not ask for every thread's
  "qThreadExtraInfo," saving per-thread round-trips on "info threads."
  
  To use this feature, (k)gdb needs to be built with the --with-expat option.
  I would encourage enabling this option by default in our GDB port, if it is
  not already.
  
  Finally, there is another optional attribute you can specify per-thread
  called a "handle."  Handles are arbitrarily long sequences of bytes,
  represented in the XML as hexadecimal.  It is unclear to me how or if GDB
  actually uses handles for anything.  So I have left them out.

Modified:
  head/sys/gdb/gdb_main.c

Modified: head/sys/gdb/gdb_main.c
==============================================================================
--- head/sys/gdb/gdb_main.c	Thu Aug 22 00:33:22 2019	(r351367)
+++ head/sys/gdb/gdb_main.c	Thu Aug 22 00:34:11 2019	(r351368)
@@ -36,6 +36,7 @@ __FBSDID("$FreeBSD$");
 #include <sys/pcpu.h>
 #include <sys/proc.h>
 #include <sys/reboot.h>
+#include <sys/sbuf.h>
 
 #include <machine/gdb_machdep.h>
 #include <machine/kdb.h>
@@ -252,11 +253,12 @@ gdb_do_qsupported(uint32_t *feat)
 	 */
 	gdb_tx_varhex(GDB_BUFSZ + strlen("$#nn") - 1);
 
+	gdb_tx_str(";qXfer:threads:read+");
+
 	/*
 	 * Future consideration:
 	 *   - vCont
 	 *   - multiprocess
-	 *   - qXfer:threads:read
 	 */
 	gdb_tx_end();
 	return;
@@ -266,7 +268,306 @@ error:
 	gdb_tx_err(EINVAL);
 }
 
+/*
+ * A qXfer_context provides a vaguely generic way to generate a multi-packet
+ * response on the fly, making some assumptions about the size of sbuf writes
+ * vs actual packet length constraints.  A non-byzantine gdb host should allow
+ * hundreds of bytes per packet or more.
+ *
+ * Upper layers are considered responsible for escaping the four forbidden
+ * characters '# $ } *'.
+ */
+struct qXfer_context {
+	struct sbuf sb;
+	size_t last_offset;
+	bool flushed;
+	bool lastmessage;
+	char xfer_buf[GDB_BUFSZ];
+};
+
 static int
+qXfer_drain(void *v, const char *buf, int len)
+{
+	struct qXfer_context *qx;
+
+	if (len < 0)
+		return (-EINVAL);
+
+	qx = v;
+	if (qx->flushed) {
+		/*
+		 * Overflow.  We lost some message.  Maybe the packet size is
+		 * ridiculously small.
+		 */
+		printf("%s: Overflow in qXfer detected.\n", __func__);
+		return (-ENOBUFS);
+	}
+
+	qx->last_offset += len;
+	qx->flushed = true;
+
+	if (qx->lastmessage)
+		gdb_tx_begin('l');
+	else
+		gdb_tx_begin('m');
+
+	memcpy(gdb_txp, buf, len);
+	gdb_txp += len;
+
+	gdb_tx_end();
+	return (len);
+}
+
+static int
+init_qXfer_ctx(struct qXfer_context *qx, uintmax_t len)
+{
+
+	/* Protocol (max) length field includes framing overhead. */
+	if (len < sizeof("$m#nn"))
+		return (ENOSPC);
+
+	len -= 4;
+	len = ummin(len, GDB_BUFSZ - 1);
+
+	qx->last_offset = 0;
+	qx->flushed = false;
+	qx->lastmessage = false;
+	sbuf_new(&qx->sb, qx->xfer_buf, len, SBUF_FIXEDLEN);
+	sbuf_set_drain(&qx->sb, qXfer_drain, qx);
+	return (0);
+}
+
+/*
+ * dst must be 2x strlen(max_src) + 1.
+ *
+ * Squashes invalid XML characters down to _.  Sorry.  Then escapes for GDB.
+ */
+static void
+qXfer_escape_xmlattr_str(char *dst, size_t dstlen, const char *src)
+{
+	static const char *forbidden = "#$}*";
+
+	size_t i;
+	char c;
+
+	for (i = 0; i < dstlen - 1 && *src != 0; src++, i++) {
+		c = *src;
+		/* XML attr filter */
+		if (c < 32)
+			c = '_';
+		/* We assume attributes will be "" quoted. */
+		if (c == '<' || c == '&' || c == '"')
+			c = '_';
+
+		/* GDB escape. */
+		if (strchr(forbidden, c) != NULL) {
+			*dst++ = '}';
+			c ^= 0x20;
+		}
+		*dst++ = c;
+	}
+	if (*src != 0)
+		printf("XXX%s: overflow; API misuse\n", __func__);
+
+	*dst = 0;
+}
+
+/*
+ * Dynamically generate qXfer:threads document, one packet at a time.
+ *
+ * The format is loosely described[0], although it does not seem that the
+ * <?xml?> mentioned on that page is required.
+ *
+ * [0]: https://sourceware.org/gdb/current/onlinedocs/gdb/Thread-List-Format.html
+ */
+static void
+do_qXfer_threads_read(void)
+{
+	/* Kludgy context */
+	static struct {
+		struct qXfer_context qXfer;
+		/* Kludgy state machine */
+		struct thread *iter;
+		enum {
+			XML_START_THREAD,	/* '<thread' */
+			XML_THREAD_ID,		/* ' id="xxx"' */
+			XML_THREAD_CORE,	/* ' core="yyy"' */
+			XML_THREAD_NAME,	/* ' name="zzz"' */
+			XML_THREAD_EXTRA,	/* '> ...' */
+			XML_END_THREAD,		/* '</thread>' */
+			XML_SENT_END_THREADS,	/* '</threads>' */
+		} next_step;
+	} ctx;
+	static char td_name_escape[MAXCOMLEN * 2 + 1];
+
+	const char *name_src;
+	uintmax_t offset, len;
+	int error;
+
+	/* Annex part must be empty. */
+	if (gdb_rx_char() != ':')
+		goto misformed_request;
+
+	if (gdb_rx_varhex(&offset) != 0 ||
+	    gdb_rx_char() != ',' ||
+	    gdb_rx_varhex(&len) != 0)
+		goto misformed_request;
+
+	/*
+	 * Validate resume xfers.
+	 */
+	if (offset != 0) {
+		if (offset != ctx.qXfer.last_offset) {
+			printf("%s: Resumed offset %ju != expected %ju\n",
+			    __func__, offset, ctx.qXfer.last_offset);
+			error = ESPIPE;
+			goto request_error;
+		}
+		ctx.qXfer.flushed = false;
+	}
+
+	if (offset == 0) {
+		ctx.iter = kdb_thr_first();
+		ctx.next_step = XML_START_THREAD;
+		error = init_qXfer_ctx(&ctx.qXfer, len);
+		if (error != 0)
+			goto request_error;
+
+		sbuf_cat(&ctx.qXfer.sb, "<threads>");
+	}
+
+	while (!ctx.qXfer.flushed && ctx.iter != NULL) {
+		switch (ctx.next_step) {
+		case XML_START_THREAD:
+			ctx.next_step = XML_THREAD_ID;
+			sbuf_cat(&ctx.qXfer.sb, "<thread");
+			continue;
+
+		case XML_THREAD_ID:
+			ctx.next_step = XML_THREAD_CORE;
+			sbuf_printf(&ctx.qXfer.sb, " id=\"%jx\"",
+			    (uintmax_t)ctx.iter->td_tid);
+			continue;
+
+		case XML_THREAD_CORE:
+			ctx.next_step = XML_THREAD_NAME;
+			if (ctx.iter->td_oncpu != NOCPU) {
+				sbuf_printf(&ctx.qXfer.sb, " core=\"%d\"",
+				    ctx.iter->td_oncpu);
+			}
+			continue;
+
+		case XML_THREAD_NAME:
+			ctx.next_step = XML_THREAD_EXTRA;
+
+			if (ctx.iter->td_name[0] != 0)
+				name_src = ctx.iter->td_name;
+			else if (ctx.iter->td_proc != NULL &&
+			    ctx.iter->td_proc->p_comm[0] != 0)
+				name_src = ctx.iter->td_proc->p_comm;
+			else
+				continue;
+
+			qXfer_escape_xmlattr_str(td_name_escape,
+			    sizeof(td_name_escape), name_src);
+			sbuf_printf(&ctx.qXfer.sb, " name=\"%s\"",
+			    td_name_escape);
+			continue;
+
+		case XML_THREAD_EXTRA:
+			ctx.next_step = XML_END_THREAD;
+
+			sbuf_putc(&ctx.qXfer.sb, '>');
+
+			if (ctx.iter->td_state == TDS_RUNNING)
+				sbuf_cat(&ctx.qXfer.sb, "Running");
+			else if (ctx.iter->td_state == TDS_RUNQ)
+				sbuf_cat(&ctx.qXfer.sb, "RunQ");
+			else if (ctx.iter->td_state == TDS_CAN_RUN)
+				sbuf_cat(&ctx.qXfer.sb, "CanRun");
+			else if (TD_ON_LOCK(ctx.iter))
+				sbuf_cat(&ctx.qXfer.sb, "Blocked");
+			else if (TD_IS_SLEEPING(ctx.iter))
+				sbuf_cat(&ctx.qXfer.sb, "Sleeping");
+			else if (TD_IS_SWAPPED(ctx.iter))
+				sbuf_cat(&ctx.qXfer.sb, "Swapped");
+			else if (TD_AWAITING_INTR(ctx.iter))
+				sbuf_cat(&ctx.qXfer.sb, "IthreadWait");
+			else if (TD_IS_SUSPENDED(ctx.iter))
+				sbuf_cat(&ctx.qXfer.sb, "Suspended");
+			else
+				sbuf_cat(&ctx.qXfer.sb, "???");
+			continue;
+
+		case XML_END_THREAD:
+			ctx.next_step = XML_START_THREAD;
+			sbuf_cat(&ctx.qXfer.sb, "</thread>");
+			ctx.iter = kdb_thr_next(ctx.iter);
+			continue;
+
+		/*
+		 * This one isn't part of the looping state machine,
+		 * but GCC complains if you leave an enum value out of the
+		 * select.
+		 */
+		case XML_SENT_END_THREADS:
+			/* NOTREACHED */
+			break;
+		}
+	}
+	if (ctx.qXfer.flushed)
+		return;
+
+	if (ctx.next_step != XML_SENT_END_THREADS) {
+		ctx.next_step = XML_SENT_END_THREADS;
+		sbuf_cat(&ctx.qXfer.sb, "</threads>");
+	}
+	if (ctx.qXfer.flushed)
+		return;
+
+	ctx.qXfer.lastmessage = true;
+	sbuf_finish(&ctx.qXfer.sb);
+	sbuf_delete(&ctx.qXfer.sb);
+	ctx.qXfer.last_offset = 0;
+	return;
+
+misformed_request:
+	/*
+	 * GDB "General-Query-Packets.html" qXfer-read anchor specifically
+	 * documents an E00 code for malformed requests or invalid annex.
+	 * Non-zero codes indicate invalid offset or "error reading the data."
+	 */
+	error = 0;
+request_error:
+	gdb_tx_err(error);
+	return;
+}
+
+/*
+ * A set of standardized transfers from "special data areas."
+ *
+ * We've already matched on "qXfer:" and advanced the rx packet buffer past
+ * that bit.  Parse out the rest of the packet and generate an appropriate
+ * response.
+ */
+static void
+do_qXfer(void)
+{
+	if (!gdb_rx_equal("threads:"))
+		goto unrecognized;
+
+	if (!gdb_rx_equal("read:"))
+		goto unrecognized;
+
+	do_qXfer_threads_read();
+	return;
+
+unrecognized:
+	gdb_tx_empty();
+	return;
+}
+
+static int
 gdb_trap(int type, int code)
 {
 	jmp_buf jb;
@@ -425,6 +726,8 @@ gdb_trap(int type, int code)
 				gdb_do_threadinfo(&thr_iter);
 			} else if (gdb_rx_equal("sThreadInfo")) {
 				gdb_do_threadinfo(&thr_iter);
+			} else if (gdb_rx_equal("Xfer:")) {
+				do_qXfer();
 			} else if (gdb_rx_equal("Search:memory:")) {
 				gdb_do_mem_search();
 			} else if (!gdb_cpu_query())



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201908220034.x7M0YBOw088032>