git: 9front

Download patch

ref: ccef7a8bdca9d58397ab9ebe6af7ccf29edd6dad
parent: 4cabbe15f6cde5fac863ace9468361507b8d945a
author: David Arroyo <david@arroyo.cc>
date: Wed Mar 11 13:47:51 EDT 2026

Add gdbfs(4)

gdbfs connects to a gdbserver and presents enough
proc(3) files for acid(1) and db(1) to debug a 9
program running under gdb, such as a 9 kernel
running in a hypervisor.

--- /dev/null
+++ b/sys/man/4/gdbfs
@@ -1,0 +1,96 @@
+.TH GDBFS 4
+.SH NAME
+gdbfs \- GNU debugger file system
+.SH SYNOPSIS
+.B gdbfs
+[
+.B -Dd
+]
+[
+.B -s
+.I srvname
+]
+[
+.B -m
+.I arch
+]
+[
+.B -p
+.I pid
+]
+[
+.B -t
+.I text
+]
+[
+.I addr
+]
+.SH DESCRIPTION
+.I Gdbfs
+presents a set of
+.IR proc (3)
+files under
+.BI /proc/ pid
+for debugging a remote process through the GNU debugger stub listening at
+.IR addr ,
+or connected to standard input/output if
+.I addr
+is not specified.
+If the
+.B -s
+option is given,
+.I gdbfs
+will post its channel at
+.BI /srv/ srvname
+(see
+.IR srv (3)),
+allowing the session to be shared or reattached later.
+.PP
+If the
+.B -D
+flag is specified, 9P messages will be logged to standard error.
+If the
+.B -d
+flag is specified,
+.IR gdbfs (4)
+will log gdbserver protocol messages exchanged with the remote gdbserver, along with other diagnostic information.
+.PP
+.IR Text ,
+if provided, should be a copy of the binary running on the target, in
+.IR a.out (6)
+format.
+It will be used to determine the architecture of the running target, and will be served at
+.BI /proc/ pid /text.
+If
+.I text
+is not provided, the target architecture must be specified with the
+.B -m
+flag.
+
+.SH EXAMPLES
+On drawterm, use the host unix network stack to connect to the gdbserver listening at
+.BR localhost:1234 ,
+which is a hypervisor running a a 9pc64 guest VM:
+.IP
+.EX
+	bind /mnt/term/net /net
+	gdbfs -t /amd64/9pc64 tcp!localhost!1234
+	acid 1
+.EE
+.PP
+Connect to gdbserver over a serial interface to debug an arm system:
+.IP
+.EX
+	gdbfs -m arm <>/dev/eia0
+.EE
+.SH BUGS
+Gdbfs does not consistently respond to or emit retransmit requests (character '-').
+Use a reliable transport such as TCP or a pipe for best results.
+
+Stubs which emit more than one reply per request may cause gdbfs to become stuck.
+.SH SOURCE
+/sys/src/cmd/gdbfs
+.SH "SEE ALSO"
+.IR acid (1),
+.IR db (1),
+.IR rdbfs (4)
--- /dev/null
+++ b/sys/src/cmd/gdbfs/dat.h
@@ -1,0 +1,36 @@
+int debug;
+void dbg(char *fmt, ...);
+
+enum {
+	/* counting the terminating NUL is intentional */
+	Minwrite = sizeof "M0000000000000000,00000000:#00",
+};
+
+enum state {
+	Running = 1,
+	Stopped,
+	Shutdown,
+};
+
+struct
+{
+	int tid;
+	QLock;	/* guards state transitions */
+	int state;
+	int pktlen;
+	int wfd;
+	Biobuf *rb;
+	Channel *c;
+} gdb;
+
+void gdbinit(int rfd, int wfd);
+void gdbshutdown(void);
+
+void gdbreadmem(Req *r);
+void gdbwritemem(Req *r);
+void gdbreadreg(Req *r);
+void gdbwritereg(Req *r);
+void gdbstart(Req *r);
+void gdbwaitstop(Req *r);
+void gdbstartstop(Req *r);
+void gdbstop(Req *r);
--- /dev/null
+++ b/sys/src/cmd/gdbfs/gdb.c
@@ -1,0 +1,655 @@
+/* client for the GDB remote serial protocol, documented here:
+	https://sourceware.org/gdb/current/onlinedocs/gdb.html/Remote-Protocol.html
+*/
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <mach.h>
+#include <thread.h>
+#include <fcall.h>
+#include <9p.h>
+#include "dat.h"
+
+static char Ebadctl[] = "bad process or channel control request";
+static char Erunning[] = "target is running";
+static char hex[] = "0123456789abcdef";
+
+/* /proc/$pid/[k]regs holds a Ureg structure for the target platform.
+   However, its definition will not always match the output of the
+   gdbserver's "g" command, so we need to conversion tables for each
+   machine type */
+
+typedef struct Gdbreg Gdbreg;
+struct Gdbreg
+{
+	char *name;	/* matches Reglist[].name */
+	int size;	/* may not always agree with libmach */
+};
+
+/* registers listed in the order output by gdbserver, no gaps */
+static Gdbreg armregs[] = {
+	{ "R0", 4 },
+	{ "R1", 4 },
+	{ "R2", 4 },
+	{ "R3", 4 },
+	{ "R4", 4 },
+	{ "R5", 4 },
+	{ "R6", 4 },
+	{ "R7", 4 },
+	{ "R8", 4 },
+	{ "R9", 4 },
+	{ "R10", 4 },
+	{ "R11", 4 },
+	{ "R12", 4 },
+	{ "R13", 4 },
+	{ "R14", 4 },
+	{ "R15", 4 },
+	{0},
+};
+
+static Gdbreg amd64regs[] = {
+	{ "AX", 8 },
+	{ "BX", 8 },
+	{ "CX", 8 },
+	{ "DX", 8 },
+	{ "SI", 8 },
+	{ "DI", 8 },
+	{ "BP", 8 },
+	{ "SP", 8 },
+	{ "R8", 8 },
+	{ "R9", 8 },
+	{ "R10", 8 },
+	{ "R11", 8 },
+	{ "R12", 8 },
+	{ "R13", 8 },
+	{ "R14", 8 },
+	{ "R15", 8 },
+	{ "PC", 8 },
+	{ "EFLAGS", 4 },
+	{ "CS", 4},
+	{ "SS", 4},
+	{ "DS", 4},
+	{ "ES", 4},
+	{ "FS", 4},
+	{ "GS", 4},
+	{0},
+};
+
+static Gdbreg *gdbregs[] =
+{
+	[MARM]		= armregs,
+	[MAMD64]	= amd64regs,
+};
+
+static Reglist *
+findreg(char *rname)
+{
+	Reglist *r;
+	for(r = mach->reglist; r->rname != nil; r++){
+		if(strcmp(r->rname, rname) == 0)
+			return r;
+	}
+	return nil;
+}
+
+uchar *
+bin2ureg(uchar *src)
+{
+	uchar *ureg;
+	Gdbreg *greg;
+	Reglist *reg;
+
+	union {
+		u16int u16;
+		u32int u32;
+		u64int u64;
+	} v;
+
+	if(mach->mtype > nelem(gdbregs) || gdbregs[mach->mtype] == nil){
+		werrstr("no register map for %s", mach->name);
+		return nil;
+	}
+	ureg = mallocz(mach->regsize, 1);
+	if(ureg == nil)
+		return nil;
+
+	for(greg = gdbregs[mach->mtype]; greg->name != nil; greg++){
+		if((reg = findreg(greg->name)) == nil)
+			continue;
+
+		memmove(&v, src, greg->size);
+		src += greg->size;
+
+		switch(reg->rformat){
+		case 'x':
+			memmove(&ureg[reg->roffs], &v.u16, sizeof v.u16);
+			break;
+		case 'X': case 'W': case 'f':
+			memmove(&ureg[reg->roffs], &v.u32, sizeof v.u32);
+			break;
+		case 'Y': case 'F':
+			memmove(&ureg[reg->roffs], &v.u64, sizeof v.u64);
+			break;
+		}
+	}
+	return ureg;
+}
+
+static uintptr
+off2addr(vlong off)
+{
+	off <<= 1;
+	off >>= 1;
+	return off;
+}
+
+static int
+badsum(char *p, uint sum)
+{
+	while(*p)
+		sum -= *p++;
+	return sum & 0xff;
+}
+
+static long
+hex2bin(char *dst, char *src, long len)
+{
+	int i, n, v;
+
+	for(i = n = 0; n < len && src[i]; i++){
+		switch(src[i]){
+		case '0' ... '9':
+			v = src[i] - '0';
+			break;
+		case 'a' ... 'f':
+			v = src[i] - 'a' + 10;
+			break;
+		case 'A' ... 'F':
+			v = src[i] - 'A' + 10;
+			break;
+		default:
+			/* todo: run-length encoding */
+			werrstr("bad hex digit %c", src[i]);
+			return -1;
+		}
+		dst[n] = (dst[n] << 4) + v;
+		n += i & 1;
+	}
+	if(i & 1){
+		werrstr("incomplete hex digit");
+		return -1;
+	}
+	return n;
+}
+
+static Channel *
+cmdstr(char *str)
+{
+	Channel *rc;
+	char *err;
+	
+	rc = chancreate(sizeof(char*), 0);
+	if(sendp(gdb.c, rc) < 0){
+		werrstr("interrupted");
+		free(str);
+		chanfree(rc);
+		return nil;
+	}
+	if(sendp(rc, str) < 0)
+		sysfatal("cmdstr: sendp %s failed", str);
+
+	/* ack */
+	if(recv(rc, &err) < 0){
+		werrstr("interrupted");
+		chanclose(rc);
+		return nil;
+	}
+	if(err != nil){
+		werrstr("%s", err);
+		free(err);
+		chanfree(rc);
+		return nil;
+	}
+	return rc;
+}
+
+static Channel *
+vcmd(char *fmt, va_list args)
+{
+	int sum;
+	char buf[512], *s, *e, *p;
+
+	e = buf + sizeof buf;
+	s = p = seprint(buf, e, "$");
+	s = vseprint(s, e, fmt, args);
+
+	for(sum = 0; *p != 0; p++)
+		sum += *p;
+	seprint(s, e, "#%02x", sum & 0xff);
+	return cmdstr(estrdup9p(buf));
+}
+
+static Channel *
+cmd(char *fmt, ...)
+{
+	Channel *rc;
+	va_list args;
+	va_start(args, fmt);
+	rc = vcmd(fmt, args);
+	va_end(args);
+	return rc;
+}
+
+static char *
+reply(Channel *rc)
+{
+	char *rsp;
+	if(recv(rc, &rsp) < 0){
+		werrstr("interrupted");
+		chanclose(rc);
+		return nil;
+	}
+	chanfree(rc);
+
+	if(*rsp == 'E'){
+		werrstr("%s", rsp);
+		free(rsp);
+		return nil;
+	}
+	return rsp;
+}
+
+static char *
+cmdreply(char *fmt, ...)
+{
+	Channel *rc;
+	va_list args;
+
+	va_start(args, fmt);
+	rc = vcmd(fmt, args);
+	va_end(args);
+
+	if(rc != nil)
+		return reply(rc);
+	return nil;
+}
+
+/* Protocol between gdbproc (G) and request handler (R):
+
+	R → G command (char*)
+	R ← G ack (nil) or err (char*)
+	if !err:
+		R ← G reply (char*) or err ("E...")
+	R ← G close
+
+   R may free the channel after the final message.
+   R may close the channel if it is interrupted.
+   G may free the channel if send() or recv() fail. 
+*/
+static void
+gdbproc(void *)
+{
+	int c, tries;
+	Channel *rc;
+	char *req, *rsp, sum[4];
+
+	memset(sum, 0, sizeof sum);
+	while(rc = recvp(gdb.c)){
+		if(recv(rc, &req) < 0){
+			chanfree(rc);
+			continue;
+		}
+		tries = 5;
+		do{
+			dbg("→ %s\n", req);
+			if(fprint(gdb.wfd, "%s", req) < 0){
+				chanprint(rc, "E.%r");
+				goto next;
+			}
+			c = Bgetc(gdb.rb);
+			dbg("← %c\n", c);
+		} while(c == '-' && tries --> 0);
+		free(req);
+
+		if(c != '+'){
+			chanprint(rc, "E.wanted '+', got '%c'", c);
+			goto next;
+		}
+
+		/* command ACKed. "start" needs this to end 9P req.
+		   we must consume the response, so continue on
+		   unconditionally, even if caller was interrupted */
+		sendp(rc, nil);
+		
+		/* Skip notification packets. These are unsolicited,
+		   and must not contain the start-of-packet marker '$' */
+		if(Brdline(gdb.rb, '$') == nil
+		|| Blinelen(gdb.rb) > 0 && fprint(gdb.wfd, "+") < 0)
+		{
+			chanprint(rc, "E.%r");
+			goto next;
+		}
+
+		rsp = Brdstr(gdb.rb, '#', 1);
+		if(rsp != nil) dbg("← %s\n", rsp);
+
+		if(rsp == nil)
+			rsp = smprint("E.%r");
+		else if(Bread(gdb.rb, sum, 2) < 0){
+			free(rsp);
+			rsp = smprint("E.%r");
+		}
+		else if(badsum(rsp, strtoul(sum, nil, 16))){
+			free(rsp);
+			rsp = smprint("E.bad checksum %s", sum);
+		}
+		else if(fprint(gdb.wfd, "+") < 0){
+			free(rsp);
+			rsp = smprint("E.ack %r");
+		}
+		if(sendp(rc, rsp) < 0){
+			chanfree(rc);
+			free(rsp);
+			continue;
+		}
+next:		chanclose(rc);
+	}
+	chanclose(gdb.c);
+}
+
+void
+gdbinit(int rfd, int wfd)
+{
+	static char qSupported[] = "qSupported:error-message+";
+	int i, n;
+	char *rsp, *features[64], *kv[2];
+	
+	gdb.wfd = wfd;
+	gdb.rb = Bfdopen(rfd, OREAD);
+	if(gdb.rb == nil)
+		sysfatal("gdbinit: %r");
+
+	gdb.c = chancreate(sizeof(Channel*), 0);
+	gdb.tid = proccreate(gdbproc, nil, 8192);
+	
+	if((rsp = cmdreply("%s", qSupported)) == nil)
+		sysfatal("gdbinit: %r");
+
+	n = getfields(rsp, features, nelem(features), 1, ";");
+	for(i = 0; i < n; i++){
+		if(getfields(features[i], kv, nelem(kv), 1, "=") != 2)
+			continue;
+		if(strcmp(kv[0], "PacketSize") == 0)
+			gdb.pktlen = strtol(kv[1], nil, 16);
+	}
+	free(rsp);
+	qlock(&gdb);
+	gdb.state = Stopped;
+	qunlock(&gdb);
+}
+
+void
+gdbshutdown(void)
+{
+	qlock(&gdb);
+	gdb.state = Shutdown;
+	qunlock(&gdb);
+
+	threadint(gdb.tid);
+	if(sendp(gdb.c, nil) < 0)
+		sysfatal("gdbshutdown: %r");
+	recv(gdb.c, nil);
+	chanfree(gdb.c);
+
+	if(fprint(gdb.wfd, "$D#44") < 0)
+		sysfatal("gdbshutdown detach: %r");
+	Bterm(gdb.rb);
+	close(gdb.wfd);
+}
+
+void
+gdbwritemem(Req *r)
+{
+	Channel *rc;
+	int i, sum, lo, hi;
+	ulong count;
+	char *rsp, *req, *s, *e, *b;
+
+	count = r->ifcall.count;
+	if(gdb.pktlen > Minwrite && count > (gdb.pktlen - Minwrite)/2)
+		count = (gdb.pktlen - Minwrite)/2;
+
+	s = req = emalloc9p(count + Minwrite);
+	e = req + count + Minwrite;
+	s = seprint(s, e, "$M%llux,%lux:", off2addr(r->ifcall.offset), count);
+
+	for(b = req + 1, sum = 0; b < s; b++)
+		sum += *b;
+	for(i = 0; i < count; i++){
+		hi = hex[(r->ifcall.data[i] & 0xf0) >> 4];
+		lo = hex[(r->ifcall.data[i] & 0x0f) >> 0];
+		sum += hi + lo;
+		*s++ = hi;
+		*s++ = lo;
+	}
+	seprint(s, e, "#%02x", sum & 0xff);
+
+	qlock(&gdb);
+	if(gdb.state != Stopped){
+		respond(r, Ebadctl);
+		qunlock(&gdb);
+		free(req);
+		return;
+	}
+	if((rc = cmdstr(req)) == nil || (rsp = reply(rc)) == nil)
+		responderror(r);
+	else {
+		r->ofcall.count = count;
+		respond(r, nil);
+		free(rsp);
+	}
+	qunlock(&gdb);
+}
+
+void
+gdbreadreg(Req *r)
+{
+	char *rsp;
+	uchar *ureg;
+
+	qlock(&gdb);
+	if(gdb.state != Stopped){
+		respond(r, Ebadctl);
+		qunlock(&gdb);
+		return;
+	}
+	rsp = cmdreply("g");
+	qunlock(&gdb);
+
+	if(rsp == nil){
+		responderror(r);
+		return;
+	}
+	if(hex2bin(rsp, rsp, strlen(rsp)) < 0)
+		responderror(r);
+	else if((ureg = bin2ureg((uchar*)rsp)) == nil)
+		responderror(r);
+	else {
+		readbuf(r, ureg, mach->regsize);
+		respond(r, nil);
+		free(ureg);
+	}
+	free(rsp);
+}
+
+void
+gdbwritereg(Req *r)
+{
+	respond(r, "not implemented");
+}
+
+void
+gdbreadmem(Req *r)
+{
+	long n;
+	ulong len;
+	char *rsp;
+
+	len = r->ifcall.count;
+	if(gdb.pktlen > 0 && len > (gdb.pktlen - Minwrite)/2)
+		len = (gdb.pktlen - Minwrite)/2;
+
+	qlock(&gdb);
+	if(gdb.state != Stopped){
+		respond(r, Ebadctl);
+		qunlock(&gdb);
+		return;
+	}
+	rsp = cmdreply("m%llux,%lux", off2addr(r->ifcall.offset), len);
+	qunlock(&gdb);
+
+	if(rsp == nil){
+		responderror(r);
+		return;
+	}
+	if((n = hex2bin(r->ofcall.data, rsp, len)) < 0)
+		responderror(r);
+	else {
+		r->ofcall.count = n;
+		respond(r, nil);
+	}
+	free(rsp);
+}
+
+void
+gdbstart(Req *r)
+{
+	static char haltcodes[] = "STXWN";
+	char *rsp;
+	Srv *srv;
+	Channel *rc;
+
+	srv = r->srv;
+
+	qlock(&gdb);
+	if(gdb.state != Stopped){
+		respond(r, Ebadctl);
+		qunlock(&gdb);
+		return;
+	}
+	if((rc = cmd("c")) == nil){
+		responderror(r);
+		qunlock(&gdb);
+		return;
+	}
+	gdb.state = Running;
+	respond(r, nil);
+	qunlock(&gdb);
+
+	/* we need to stick around to track the state of
+	   the target, even though 9P request is done */
+	srvrelease(srv);
+	rsp = reply(rc);
+	srvacquire(srv);
+
+	if(rsp == nil)
+		sysfatal("gdbstart reply: %r");
+	if(strchr(haltcodes, *rsp) == nil)
+		sysfatal("gdbstart: bad response %s", rsp);
+	free(rsp);
+	qlock(&gdb);
+	gdb.state = Stopped;
+	qunlock(&gdb);
+}
+
+static int
+interrupt(void)
+{
+	dbg("→ \\x03\n");
+
+	/* we should probably have a lock guarding writes,
+	   but this program makes small writes; it's unlikely
+	   this would wind up in the middle of another one */
+	return write(gdb.wfd, "\x03", 1);
+}
+
+void
+gdbstop(Req *r)
+{
+	char *rsp;
+
+	qlock(&gdb);
+	if(gdb.state == Stopped)
+		respond(r, nil);
+
+	else if(interrupt() < 0)
+		responderror(r);
+
+	else if((rsp = cmdreply("?")) == nil)
+		responderror(r);
+	else {
+		gdb.state = Stopped;
+		free(rsp);
+		respond(r, nil);
+	}
+	qunlock(&gdb);
+}
+
+void
+gdbstartstop(Req *r)
+{
+	Channel *rc;
+	char *rsp, err[sizeof "interrupted"];
+
+	qlock(&gdb);
+	if(gdb.state != Stopped){
+		respond(r, Ebadctl);
+		qunlock(&gdb);
+		return;
+	}
+	if((rc = cmd("c")) == nil){
+		responderror(r);
+		qunlock(&gdb);
+		return;
+	}
+	gdb.state = Running;
+	qunlock(&gdb);
+
+	/* target could run indefinitely, or until another
+	   request stops it, so we need to unblock srv */
+	srvrelease(r->srv);
+	rsp = reply(rc);
+	rerrstr(err, sizeof err);
+	srvacquire(r->srv);
+
+	if(rsp == nil && strcmp(err, "interrupted") == 0)
+		interrupt();
+	
+	qlock(&gdb);
+	gdb.state = Stopped;
+	qunlock(&gdb);
+
+	if(rsp == nil){
+		responderror(r);
+	} else {
+		respond(r, nil);
+		free(rsp);
+	}
+}
+
+void
+gdbwaitstop(Req *r)
+{
+	char *rsp;
+
+	srvrelease(r->srv);
+	rsp = cmdreply("?");
+	srvacquire(r->srv);
+
+	if(rsp  == nil)
+		responderror(r);
+	else {
+		respond(r, nil);
+		free(rsp);
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/gdbfs/main.c
@@ -1,0 +1,330 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <mach.h>
+#include <thread.h>
+#include <fcall.h>
+#include <9p.h>
+#include "dat.h"
+
+int debug;
+void
+dbg(char *fmt, ...)
+{
+	va_list args;
+	if(debug){
+		va_start(args, fmt);
+		vfprint(2, fmt, args);
+		va_end(args);
+	}
+}
+
+int textfd = -1;
+char *srvname;
+char *procname = "1";
+
+void
+usage(void)
+{
+	fprint(2, "usage: gdbfs [-s srvname] [-p pid] [-t text] [addr]\n");
+	threadexitsall("usage");
+}
+
+enum
+{
+	Xctl	= 1,
+	Xfpregs,
+	Xkregs,
+	Xmem,
+	Xproc,
+	Xregs,
+	Xtext,
+	Xstatus,
+};
+
+struct {
+	char *s;
+	int id;
+	int mode;
+} tab[] = {
+	"ctl",		Xctl,		0666,
+	"fpregs",	Xfpregs,	0666,
+	"kregs",	Xkregs,		0666,
+	"mem",		Xmem,		0666,
+	"regs",		Xregs,		0666,
+	"text",		Xtext,		0444,
+	"status",	Xstatus,	0444,
+};
+
+void
+die(Srv*)
+{
+	threadint(gdb.tid);
+	gdbshutdown();
+}
+
+void
+fsopen(Req *r)
+{
+	switch((uintptr)r->fid->file->aux){
+	case Xfpregs:
+	case Xkregs:
+	case Xregs:
+	case Xmem:
+		/* data is hex encoded, so packet size is ×2 iounit */
+		if(gdb.pktlen > Minwrite)
+			r->ofcall.iounit = (gdb.pktlen - Minwrite)/2;
+		break;
+	}
+	respond(r, nil);
+}
+
+void
+fsread(Req *r)
+{
+	int n, i;
+	char buf[512], *status;
+	
+	switch((uintptr)r->fid->file->aux){
+	case Xctl:
+		readstr(r, procname);
+		respond(r, nil);
+		break;
+	case Xfpregs:
+		respond(r, "not implemented");
+		break;
+	case Xkregs:
+	case Xregs:
+		gdbreadreg(r);
+		break;
+	case Xmem:
+		gdbreadmem(r);
+		break;
+	case Xtext:
+		if(textfd != -1)
+			n = pread(textfd, r->ofcall.data, r->ifcall.count, r->ifcall.offset);
+		else
+			n = 0;
+		if(n < 0)
+			responderror(r);
+		else {
+			r->ofcall.count = n;
+			respond(r, nil);
+		}
+		break;
+	case Xstatus:
+		qlock(&gdb);
+		switch(gdb.state){
+		case Stopped:
+			status = "Stopped";
+			break;
+		case Running:
+			status = "Running";
+			break;
+		case Shutdown:
+			status = "Moribund";
+			break;
+		default:
+			status = "New";
+			break;
+		}
+		qunlock(&gdb);
+		n = sprint(buf, "%-28s%-28s%-28s", "remote", "system", status);
+		for(i = 0; i < 9; i++)
+			n += sprint(buf+n, "%-12d", 0);
+		readstr(r, buf);
+		respond(r, nil);
+		break;
+	default:
+		respond(r, "Egreg");
+	}
+}
+
+void
+doctl(Req *r)
+{
+	enum {
+		/* see proc(3) */
+		Stop,
+		Start,
+		Waitstop,
+		Startstop,
+	};
+	Cmdbuf *cb;
+	Cmdtab *ct;
+	static Cmdtab cmds[] = {
+		{ Stop, "stop", 1 },
+		{ Start, "start", 1 },
+		{ Waitstop, "waitstop", 1 },
+		{ Startstop, "startstop", 1 },
+	};
+	
+	cb = parsecmd(r->ifcall.data, r->ifcall.count);
+	ct = lookupcmd(cb, cmds, nelem(cmds));
+	free(cb);
+	if(ct == nil){
+		responderror(r);
+		free(cb);
+		return;
+	}
+	r->ofcall.count = r->ifcall.count;
+
+	switch(ct->index){
+	default:
+		respond(r, "not implemented");
+		break;
+	case Stop:
+		gdbstop(r);
+		break;
+	case Start:
+		gdbstart(r);
+		break;
+	/* remaining commands block, so we may interrupt them */
+	case Waitstop:
+		r->aux = (void*)threadid();
+		gdbwaitstop(r);
+		break;
+	case Startstop:
+		r->aux = (void*)threadid();
+		gdbstartstop(r);
+		break;
+	}
+}
+
+void
+fswrite(Req *r)
+{
+	switch((uintptr)r->fid->file->aux){
+	case Xctl:
+		doctl(r);
+		break;
+	case Xfpregs:
+	case Xkregs:
+	case Xregs:
+		gdbwritereg(r);
+		break;
+	case Xmem:
+		gdbwritemem(r);
+		break;
+	case Xtext:
+	case Xstatus:
+	default:
+		respond(r, "Egreg");
+		break;
+	}
+}
+
+void
+fsflush(Req *r)
+{
+	if(r->oldreq->aux != 0)
+		threadint((uintptr)r->oldreq->aux);
+	respond(r, nil);
+}
+
+void
+fsstat(Req *r)
+{
+	Dir *d;
+	
+	switch((uintptr)r->fid->file->aux) {
+	default:
+		respond(r, nil);
+		break;
+	case Xregs:
+	case Xkregs:
+		r->d.length = mach->regsize;
+		break;
+	case Xtext:
+		/* setting the correct size here allows libmach to seek
+		   to the end of the file in thumbpctab(), and probably
+		   elsewhere */
+		if((d = dirfstat(textfd)) == nil){
+			responderror(r);
+			break;
+		}
+		r->d.length = d->length;
+		respond(r, nil);
+		free(d);
+		break;
+	}
+}
+
+Srv fs = {
+	.open	= fsopen,
+	.read	= fsread,
+	.write	= fswrite,
+	.flush	= fsflush,
+	.stat	= fsstat,
+	.end	= die,
+};
+
+void
+threadmain(int argc, char *argv[])
+{
+	int i, rfd, wfd;
+	File *dir;
+	Fhdr hdr;
+	char *textfile, *arch;
+
+	fmtinstall('F', fcallfmt);
+	textfile = arch = nil;
+	ARGBEGIN{
+	case 'D':
+		chatty9p++;
+		break;
+	case 'd':
+		debug = 1;
+		break;
+	case 'p':
+		procname = EARGF(usage());
+		break;
+	case 'm':
+		arch = EARGF(usage());
+		break;
+	case 't':
+		textfile = EARGF(usage());
+		break;
+	case 's':
+		srvname = EARGF(usage());
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	if(textfile != nil){
+		textfd = open(textfile, OREAD);
+		if(textfd == -1)
+			sysfatal("open %s: %r", textfile);
+		if(crackhdr(textfd, &hdr) < 0)
+			sysfatal("crackhdr: %r");
+	} else if(arch != nil){
+		if(machbyname(arch) < 0)
+			sysfatal("machbyname: %r");
+	} else
+		sysfatal("-t or -m required");
+	
+	rfd = 0;
+	wfd = 1;
+	switch(argc){
+	case 1:
+		rfd = dial(argv[0], nil, nil, nil);
+		if(rfd == -1)
+			sysfatal("dial %s: %r", argv[1]);
+		wfd = rfd;
+		break;
+	case 0:
+		break;
+	default:
+		usage();
+	}
+	
+	gdbinit(rfd, wfd);
+	fs.tree = alloctree("gdbfs", "gdbfs", DMDIR|0555, nil);
+	dir = createfile(fs.tree->root, procname, "gdbfs", DMDIR|0555, 0);
+	for(i = 0; i < nelem(tab); i++)
+		closefile(createfile(dir, tab[i].s, "gdbfs", tab[i].mode, (void*)tab[i].id));
+	closefile(dir);
+	threadpostmountsrv(&fs, srvname, "/proc", MBEFORE);
+	exits(0);
+}
--- /dev/null
+++ b/sys/src/cmd/gdbfs/mkfile
@@ -1,0 +1,12 @@
+</$objtype/mkfile
+
+TARG=gdbfs
+
+OFILES=\
+	main.$O\
+	gdb.$O\
+
+HFILES=dat.h
+
+BIN=/$objtype/bin
+</sys/src/cmd/mkone
--