In the previous post, we introduced testing framework Jest and a logging library debug. No actual progress made in terms of protocol implementation. I’d like to resume protocol implementation in this post.

Before moving on to implementation, let’s add the following configuration to the VS Code’s launch.json (only if you use VS Code) so that we can launch the server through VS Code UI.

5.1 No Ack Mode

Now, let’s start the server and attach GDB as usual.

We have been ignoring the first command qSupported up until now.

gss:gdb-server-stub:trace <-:$qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+;xmlRegisters=i386#6a

qSupported is a command that communicates what commands GDB supports and what commands the GDB Server Stub supports. Let’s write a unit test before implementing the command.

This unit test shall be our guiding star for the implementation. And here a code snippet that parses the command and passes the test above.

This command is slightly more complex than the previous ones because of its parameter list. We can continue using regex, but I’d like to switch to a parser generator at some point before things get too complicated.

Now that we can handle qSupported command, what exactly should we support? There are different options we can go, but the first one I’m going to implement is QStartNoAckMode. Remember how we always return a “+” as an acknowledgement whenever we receive a command? This is useful for unstable environment such as embedded systems. But our environment runs Node JS and does network using Socket. There is no reason for us to send acknowledgement back and forth because that’s already handled on the TCP layer. By implementing a NoAckMode, we’ll be able to skip sending acknowledgement.

We can simply add the following 3 lines to the r3000.js. To tell GDB that we support NoAckMode. The trailing “+” sign means that the feature is supported.

handleQSupported(features) {
return ok('QStartNoAckMode+')
}

Once we started sending back QStartNoAckMode in our replies, GDB will later send a QStartNoAckMode command to start NoAckMode.

gss:gdb-server-stub Started a server at 2424
gss:gdb-server-stub Connection accepted: 127.0.0.1:59651
gss:gdb-server-stub:trace <-:+
gss:gdb-server-stub:trace <-:$qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+;xmlRegisters=i386#6a
gss:gdb-server-stub:trace ->:+
gss:gdb-server-stub:trace ->:$QStartNoAckMode+#db
gss:gdb-server-stub:trace <-:+
gss:gdb-server-stub:trace <-:$vMustReplyEmpty#3a
gss:gdb-server-stub:trace ->:+
gss:gdb-server-stub:trace ->:$#00
gss:gdb-server-stub:trace <-:+
gss:gdb-server-stub:trace <-:$QStartNoAckMode#b0

Let’s implement support for this command too.

Note that we’ll send an acknowledgement to to the QStartNoAckMode command itself and stop it for the following commands. The spec doesn’t specify whether this is needed, but there is no harm to have it there. I implemented handleStartNoAckMode in the handler, but this is probably not necessary because it’s the GDBServerStub’s responsibility to handle acknowledgement. I put it in the handler just for the sake of consistency. I’ll probably refactor it later.

5.2 Thread Commands

Let’s support thread related commands that we have been ignoring for

  • qfThreadInfo/qsThreadInfo These two commands work in pair with to get the list of active threads.
  • qC The current thread’s ID
  • Hc xx Continue the thread xx where xx is the Thread ID. -1 indicates all threads
  • Hg xx Get the registers for the thread indicated by xx.

The first command we can support is qfThreadInfo/qsThreadInfo. There are two commands to get thread info because this list can get very long and the designer of GDB wanted allow breaking the list into multiple packets.

‘qfThreadInfo’‘qsThreadInfo’

Obtain a list of all active thread IDs from the target (OS). Since there may be too many active threads to fit into one reply packet, this query works iteratively: it may require more than one query/reply sequence to obtain the entire list of threads. The first query of the sequence will be the ‘qfThreadInfo’ query; subsequent queries in the sequence will be the ‘qsThreadInfo’ query.

see https://sourceware.org/gdb/current/onlinedocs/gdb/General-Query-Packets.html

For our use case, we can just put all replies in the qfThreadInfo and return ‘l’ which denotes the end of the list in the qsThreadInfo. Implementation is as simple as the following:

} else if (m = packet.match(/^qfThreadInfo/)) {
reply = this.handler.handleThreadInfo();
} else if (m = packet.match(/^qsThreadInfo/)) {
// l indicates the end of the list.
reply = ok('l');
}
...function threadIds(ids) {
return 'm' + ids.map(x => x.toString(16)).join(',');
}
...handleThreadInfo() {
// Returns the list of Thread IDs
return threadIds([1]);
}

Next command is qC which queries the current thread.

} else if (m = packet.match(/^qC/)) {
reply = this.handler.handleCurrentThread();
}
...

function currentThreadId(id) {
return 'QC' + id.toString(16);
}
...handleCurrentThread() {
return currentThreadId(1);
}

Next commands are Hc, Hg and Hm command that select the thread for the next commands Hc for s and c, Hg for g and G, Hm for m and M.

    } else if (m = packet.match(/^H([cgm])(-?[0-9]+)/)) {
const threadId = parseInt(m[2], 16);
switch (m[1]) {
case 'c':
reply = this.handler.handleSelectExecutionThread(threadId);
break;
case 'm':
reply = this.handler.handleSelectMemoryThread(threadId);
break;
case 'g':
reply = this.handler.handleSelectRegisterThread(threadId);
break;
default:
reply = unsupported();
break;
}

Note that ‘H’ command is deprecated in favor of ‘vCont’ commands. So we might have to implement ‘vCont’ commands later on.

5.3 Breakpoints

A debugger is nearly useless without breakpoints. I probably should have implemented this before worrying about thread commands… well, it’s never too late, let’s get on it.

Before implementing breakpoints, we need to make our little CPU keep running first because it has been only running step-by-step. Implementing a full fledged MIPS emulator is a fun idea, but probably out of scope of this article, so I’m going to cheat here and just keep the CPU running forever.

The changes I made here are:

  • Change the R3000 to expose a run(cycles) method that runs the given cycles.
  • Call r3000.run(100) for every 100ms, this is approximately 1000 cycles/sec = 1kHz.
  • Change the GDBCommandHandler to extend from EventEmitter so that it can emit stopped event
  • Add ‘ctrl-c’ (0x03) command support to the GDBServerStub so that the user can always interrupt the execution by hitting Ctrl-c on GDB.
  • Change handleStep/handleContinue to return ‘OK’ and let the stopped event returns stop reason

We can test this by attaching GDB and send ‘c’ command for continue execution. We can see the CPU keeps running until we interrupt it by hitting ‘Ctrl-c’.

We are now ready to implement breakpoint commands. There are two types of breakpoints; Software Breakpoint and Hardware Break Point. Software Breakpoint is what you probably usually see in the debugger. It allows you to stop the processor at any given line of code. As the name suggests, Software Breakpoint is done through “Software” by replacing the instruction at the break point address to a trap instruction that the debugger can catch. Therefore Software Breakpoint can only break instruction execution. On the other hand, Hardware Breakpoint is a hardware assisted breakpoint which can, depending on the hardware, break at accessing to a certain address. The following article is worth reading if you are interested in more details.

The command we need to support is z/Z type,addr,kind z for removing and Z for inserting a breakpoint. addr is the address and kind is target specific, usually the breakpoint’s size in bytes.

‘z type,addr,kind’
‘Z type,addr,kind’

Insert (‘Z’) or remove (‘z’) a type breakpoint or watchpoint starting at address address of kind kind.

This is the list of breakpoint types:

  • z0/Z0 — A software breakpoint at address addr of type kind. This is unused for MIPS; GDB issues a ‘M’ command to override the memory address with a break instruction (0d000500) to achieve it. But let’s implement it anyways.
  • z1/Z1 — A hardware breakpoint at address addr.
  • z2/Z2 — A write watchpoint at addr. The number of bytes to watch is specified by kind.
  • z3/Z3 — A read watchpoint at addr. The number of bytes to watch is specified by kind.
  • z4/Z4 — An access watchpoint at addr. The number of bytes to watch is specified by kind.

Implementation is rather simple. We’ll ignore the type and kind and simply place a breakpoint at the given address. This can be easily expanded to support watchpoint if there was a real CPU emulator.

Let’s experiment this on GDB, we’ll set a Hardware Breakpoint at 0xbfc00100 and hit continue command to see if GDB will indeed break at the address.

Indeed, GDB did stop at the address 0xbfc00100 as we expected.

That’d be all for today. I’m thinking a few options for my next post:

  1. Parser generator. We’ve been using regex to parse the commands, which works okay. But it’s not that robust and hard to reuse partial regex such as hex. I wanted to migrate to using a parser generator like PEG.js
  2. LLDB support. The stub works pretty well with GDB at this point. I think we can try adding support for LLDB now.
  3. TypeScript. I’ve been wanting to migrate the project to TypeScript so that we get better static type analysis. gdb-command-handler.js is more or less an interface anyways, I think it’s better to define it in TypeScript as a real interface.

To be continued…

--

--