We succeeded in setting up a fake GDB-Server using Node.js in the previous post. The server was able to accept incoming queries from GDB and ruthlessly replies “NO” to all of them. That’s where we left off in the previous post. Today, I’d like us to try having more meaningful conversation with GDB.

2.1: Parsing a packet

So far we’ve learned how the RDB Remote Debug protocol looks like. The protocol was designed way before fancy methods like REST, Protocol Buffers or gRPC existed, so it is just a bunch of raw ASCII texts going back and forth. A message is either a packet or an acknowledgement described as following:

A packet is introduced with the character ‘$’, the actual packet-data, and the terminating character ‘#’ followed by a two-digit checksum:

$packet-data#checksum

When either the host or the target machine receives a packet, the first response expected is an acknowledgment: either ‘+’ (to indicate the package was received correctly) or ‘-’ (to request retransmission):

https://sourceware.org/gdb/current/onlinedocs/gdb/Overview.html#Overview

An acknowledgement is just one character + or - so it is easy to handle. A packet also can be easily parsed by a straight forward regex:

/* $packet-data#xx */
/^\$([^#]*)#([0-9a-zA-Z]{2})/

Let’s update our code to parse acknowledgement, packet and its checksum.

And here’s the log when GDB is attached:

$ node src/index.js
Started a server at 2424
Connection accepted: 127.0.0.1:55705
<-:+
<-:$qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+;xmlRegisters=i386#6a
->:+$#00
<-:+
<-:$vMustReplyEmpty#3a
->:+$#00
<-:+
<-:$Hg0#df
->:+$#00
<-:+
<-:$qTStatus#49
->:+$#00
<-:+
<-:$?#3f
->:+$#00
<-:+
<-:$qfThreadInfo#bb
->:+$#00
<-:+
<-:$qL1200000000000000000#50
->:+$#00
<-:+
<-:$Hc-1#09
->:+$#00
<-:+
<-:$qC#b4
->:+$#00
<-:+
<-:$qAttached#8f
->:+$#00
<-:+
Connection closed

2.2: Parsing GDB commands

Now that we can extract packet-data from the incoming data, we can move forward to try parsing various commands from the packet-data. So far we’ve seen the following list of commands based on the console log:

  • qSupported
  • vMustReplyEmpty
  • H
  • qTStatus
  • ?
  • qfThreadInfo
  • qL
  • qC
  • qAttached

Some of these commands are optional that we don’t need to support. The minimum set of commands we need to support is described in the spec’s overview section.

At a minimum, a stub is required to support the ‘?’ command to tell GDB the reason for halting, ‘g’ and ‘G’ commands for register access, and the ‘m’ and ‘M’ commands for memory access. Stubs that only control single-threaded targets can implement run control with the ‘c’ (continue) command, and if the target architecture supports hardware-assisted single-stepping, the ‘s’ (step) command. Stubs that support multi-threading targets should support the ‘vCont’ command. All other commands are optional.

It seems that ? is the only command that we need to support for now. To reply to a ? command, we need to send a Stop reply packet describing the reason for halting. Because this is fake server and it’s not really halting per se, we can tell the GDB whatever reasons we want. So let’s reply with S05 for now where S indicates it halted because of a signal, and 05 is the number of the signal specifically SIGTRAP which is often used for break points.

Let’s try attaching a GDB to this version to see how far is it going to take us.

$ node src/index.js
Started a server at 2424
Connection accepted: 127.0.0.1:58837
<-:+
<-:$qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+;xmlRegisters=i386#6a
->:+$#00
<-:+
<-:$vMustReplyEmpty#3a
->:+$#00
<-:+
<-:$Hg0#df
->:+$#00
<-:+
<-:$qTStatus#49
->:+$#00
<-:+
<-:$?#3f
->:+$S05#b8
<-:+
<-:$qfThreadInfo#bb
->:+$#00
<-:+
<-:$qL1200000000000000000#50
->:+$#00
<-:+
<-:$Hc-1#09
->:+$#00
<-:+
<-:$qC#b4
->:+$#00
<-:+
<-:$qAttached#8f
->:+$#00
<-:+
<-:$g#67
->:+$#00
<-:+
<-:-
Unkown incoming message: -
<-:-
Unkown incoming message: -
<-:-+
Unkown incoming message: -+
<-:-

Now it seemed that GDB stopped at a new command g. It is a register read command that was listed in the minimum support list we previously discussed. For our fake GDB Server, it doesn’t have real CPU behind it at all. So what are we going to reply as the register values? Because what got me started this project is my Play Station emulator project, I’m going to pretend that this GDB Server is running on a 32-bit MIPS chip, specifically R3000.

2.3: Registers command

The following is a quick introduction to the registers in R3000.

  • 32 GPRs (General Purposes Registers) called r0 — r31. They can be used for anything except that r0, which is also called “zero register”, is hardwired to zero and is always zero.
  • “sr”, system status register used to control CPU states.
  • hi” and “lo” register used to store multiplication results.
  • “bad” register indicates the virtual memory address of the most recent exception occurred.
  • “cause” register indicates the reason of the most recent exception occurred.
  • pc”, the program counter.
  • “fcsr”, Floating Point Control/Status register. For some reasons GDB calls it FSR instead.
  • “fir”, Floating Point Implementation Register indicates the capabilities of the floating processing unit.

To be more specific, some of these registers are actually located in the Coprocessors attached to the R3000 called Cop0 and Cop1. Ok, enough lecture about R3000. Let’s get back on business on getting our GDB Server Stub working.

We added a simple readRegisters function that returns the “content” of the R3000 registers. 0xbfc00000 is the reset vector for MIPS. In another word, it is the memory address where the CPU loads the first instruction from.

Let’s start the server and connect a GDB to it. This time, we’ll need to run an additional command on GDB to tell it that we’re debugging a MIPS CPU.

$ gdb
(gdb) set architecture mips:3000
The target architecture is assumed to be mips:3000
(gdb) target remote localhost:2424
Remote debugging using localhost:2424
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0xbfc00000 in ?? ()

Wow, looks like we have convinced GDB that it is attaching to a GDB Server! It stopped at where the PC pointed: 0xbfc00000 and is waiting for our input. Let’s try one more command to see the register values

(gdb) i r
zero at v0 v1 a0 a1 a2 a3
R0 00000000 00000001 00000002 00000003 00000004 00000005 00000006 00000007
t0 t1 t2 t3 t4 t5 t6 t7
R8 00000008 00000009 0000000a 0000000b 0000000c 0000000d 0000000e 0000000f
s0 s1 s2 s3 s4 s5 s6 s7
R16 00000010 00000011 00000012 00000013 00000014 00000015 00000016 00000017
t8 t9 k0 k1 gp sp s8 ra
R24 00000018 00000019 0000001a 0000001b 0000001c 0000001d 0000001e 0000001f
sr lo hi bad cause pc
00000000 00000000 00000000 ffffffff 00000000 bfc00000
fsr fir
<unavl> <unavl>

Indeed it is displaying the values we sent. I’m not sure why fsr and fir are <unavl>, but I don’t really care about them much either because they don’t exist on Play Station. Ok, let’s finish it on a high note today before we hit another wall.

To be continued…

Fan Fact

MIPS expert Yuri Panchul expert explained the reason for why MIPS’s reset vector is bfc00000.

bfc00000 happened because of historical reasons. According to Mike Gupta, a member of early MIPS design teams:

Reset Vector being at non-zero value:
Reset is in ROM space. ROM space is generally associated with I/O space and generally fragmented. In 1985 (or so), we thought 4MB would be more than enough to boot form [and we decided to put it at the end of physical space: 0x1fc00000–0x1fffffff = 0x400000 = 4,194,304 bytes]. Also, it allows for the DRAMs (generally much bigger) to be naturally aligned at address 0. The newer processor do not require BFC00000 / 1FC00000 as this address is programmable.

https://github.com/MIPSfpga/mipsfpga_2_0_sandbox/issues/1

--

--