gdb 远程调试协议_从头开始实现gdb远程调试协议存根5

gdb 远程调试协议

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.

在上一篇文章中,我们介绍了测试框架Jest和日志记录库debug 。 在协议实施方面未取得实际进展。 我想在这篇文章中恢复协议的实现。

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.

在继续实施之前,让我们在VS Code的launch.json中添加以下配置(仅在使用VS Code的情况下),以便我们可以通过VS Code UI启动服务器。

5.1无应答模式 (5.1 No Ack Mode)

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

现在,让我们启动服务器并像往常一样附加GDB。

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

到目前为止,我们一直在忽略第一个命令qSupported

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.

qSupported是用于传达GDB支持哪些命令以及GDB服务器存根支持哪些命令的命令。 在执行命令之前,让我们编写一个单元测试。

test('qSupported command', () => {
  const handler = new GDBCommandHandler;
  const stub = new GDBServerStub(handler);
  const socket = new Socket();
  handler.handleQSupported.mockReturnValue("multiprocess+;fork-events-;xmlRegisters=mips");
  stub.handlePacket(socket, 'qSupported:multiprocess+;hwbreak-;fork-events+;no-resumed+;xmlRegisters=i386');
  expect(handler.handleQSupported).toHaveBeenCalledWith([
    {'multiprocess': true},
    {'hwbreak': false},
    {'fork-events': true},
    {'no-resumed': true},
    {'xmlRegisters': 'i386'}]);
  expect(socket.write).toHaveBeenCalledWith('+');
  expect(socket.write).toHaveBeenCalledWith('$multiprocess+;fork-events-;xmlRegisters=mips#6b');
});

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.

该单元测试将是我们实施的指导性星标。 这是一个解析命令并通过上面的测试的代码片段。

} else if (m = packet.match(/^qSupported:(.*)/)) {
      let features = m[1].split(';').map(x => {
        let key = x;
        let value;
        if (x.endsWith('+')) {
          key = x.substr(0, x.length - 1);
          value = true;
        } else if (x.endsWith('-')) {
          key = x.substr(0, x.length - 1);
          value = false;
        } else if (x.includes('=')) {
          const v = x.split('=');
          key = v[0];
          value = v[1];
        }
        let obj = {};
        obj[key] = value;
        return obj;
      });
      reply = this.handler.handleQSupported(features);

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.

现在我们可以处理qSupported命令了,我们到底应该支持什么? 我们可以选择不同的选项,但是我要实现的第一个选项是QStartNoAckMode 。 还记得我们每次收到命令时总是如何返回“ +”作为确认吗? 这对于不稳定的环境(例如嵌入式系统)很有用。 但是我们的环境运行Node JS并使用Socket进行网络连接。 我们没有理由来回发送确认,因为该确认已经在TCP层上进行了处理。 通过实现NoAckMode ,我们将能够跳过发送确认。

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.

我们可以简单地将以下3行添加到r3000.js。 告诉GDB我们支持NoAckMode。 尾随“ +”号表示该功能受支持。

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

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

一旦我们开始在答复中发送回QStartNoAckMode ,GDB稍后将发送QStartNoAckMode命令以启动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.

让我们也实现对该命令的支持。

handlePacket(socket, packet) {
    if (!this.noAckMode) {
      // Reply with an acknowledgement first.
      trace(`->:+`);
      socket.write('+');
    }


...
    } else if (m = packet.match(/^QStartNoAckMode/)) {
      reply = this.handler.handleStartNoAckMode();
      if (reply == ok()) {
        this.noAckMode = true;
      }
    }
...

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.

请注意,我们将向QStartNoAckMode命令本身发送确认,并在以下命令中将其停止。 规范没有指定是否需要这样做,但是将其放置在这里没有害处。 我在处理程序中实现了handleStartNoAckMode ,但这可能不是必需的,因为这是GDBServerStub负责处理确认的责任。 我将其放在处理程序中只是为了保持一致性。 我可能稍后再重构。

5.2线程命令 (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.

    qfThreadInfo / qsThreadInfo这两个命令可以配对使用,以获取活动线程的列表。
  • qC The current thread’s ID

    qC当前线程的ID
  • Hc xx Continue the thread xx where xx is the Thread ID. -1 indicates all threads

    Hc xx继续执行线程xx,其中xx是线程ID。 -1表示所有线程
  • Hg xx Get the registers for the thread indicated by xx.

    Hg xx获取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 。 有两个命令可以获取线程信息,因为此列表可能会很长,并且GDB的设计人员希望允许将该列表分成多个数据包。

‘qfThreadInfo’‘qsThreadInfo’

'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.

从目标(OS)获取所有活动线程ID的列表。 由于可能有太多活动线程无法容纳到一个回复​​数据包中,因此此查询会反复进行:要获得整个线程列表,可能需要多个查询/回复序列。 序列的第一个查询将是“ qfThreadInfo”查询; 序列中的后续查询将是“ qsThreadInfo”查询。

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

参见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:

对于我们的用例,我们可以将所有答复放入qfThreadInfo并返回“ l”,这表示qsThreadInfo中列表的末尾。 实现非常简单,如下所示:

} 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.

下一个命令是qC ,它查询当前线程。

} 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.

下一个命令是Hc,Hg和Hm命令,它们为下一个命令Hc(用于s和c),Hg(用于g和G),Hm(用于m和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.

请注意,不建议使用“ H”命令,而应使用“ vCont”命令。 因此,我们稍后可能必须实现“ vCont”命令。

5.3断点 (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.

在实现断点之前,我们需要使我们的小CPU保持首先运行,因为它只是逐步运行。 实现一个成熟的MIPS仿真器是一个有趣的主意,但可能超出了本文的范围,因此我将在此处作弊,仅使CPU永远运行。

The changes I made here are:

我在这里所做的更改是:

  • Change the R3000 to expose a run(cycles) method that runs the given cycles.

    更改R3000以公开run(cycles)给定周期的run(cycles)方法。

  • Call r3000.run(100) for every 100ms, this is approximately 1000 cycles/sec = 1kHz.

    每100毫秒调用r3000.run(100) ,这大约是1000个周期/秒= 1kHz。

  • Change the GDBCommandHandler to extend from EventEmitter so that it can emit stopped event

    更改GDBCommandHandler以从EventEmitter扩展,以便它可以发出停止的事件

  • Add ‘ctrl-c’ (0x03) command support to the GDBServerStub so that the user can always interrupt the execution by hitting Ctrl-c on GDB.

    在GDBServerStub中添加“ ctrl-c”(0x03)命令支持,以便用户始终可以通过在GDB上按Ctrl-c来中断执行。
  • Change handleStep/handleContinue to return ‘OK’ and let the stopped event returns stop reason

    更改handleStep / handleContinue返回“ OK”,并让Stopped事件返回停止原因
// index.js
function runServer() {
  const r3000 = new R3000();
  const server = new GDBServerStub(r3000);
  server.start("localhost", 2424);
  function runCpu() {
    r3000.run(100);
  }
  setInterval(runCpu, 100);
}


// gdb-server-stub.js
export class GDBServerStub {
...
  onData(socket, data) {
    let input = data.toString();
    while (input.length > 0) {
      let m;
      if (m = input.match(/^\+/)) {
        // ack
        trace(`<-:${m[0]}`);
      } else if (m = input.match(/^[\x03]/)) {
        this.handler.handleInterruption();
      } else if (m = input.match(/^\$([^#]*)#([0-9a-zA-Z]{2})/)) {
        trace(`<-:${m[0]}`);
        const packet = m[1];
        const checksum = parseInt(m[2], 16);
        const expected = this.computeChecksum(packet);
        if (checksum == expected) {
          this.handlePacket(socket, packet);
        } else {
          trace(`Invalid checksum. Expected ${expected.toString(16)} but received ${m[2]} for packet ${packet}`);
        }
      } else {
        trace(`<-:${input}`);
        debug(`Unkown incoming message: ${input}`);
        // Ignore the rest of the data.
        break;
      }
      input = input.substr(m[0].length);
    }
  }
...
}


// gdb-command-handler.js
export class GDBCommandHandler extends EventEmitter {
...
}


// r3000.js
export class R3000 extends GDBCommandHandler {
...
  run(cycles) {
    if (this.stopAfterCycles == 0) {
      return;
    }


    let cyclesToRun = Math.min(cycles, this.stopAfterCycles);
    while (cyclesToRun-- > 0) {
      this.stopAfterCycles--;
      this.registers.pc += 4;
    }


    if (this.stopAfterCycles == 0) {
      this.emit('stopped', stopped(5));
    }
  }


  handleInterruption() {
    this.stopAfterCycles = 0;
    this.emit('stopped', stopped(5));
  }


  handleStep(address, threadId) {
    trace("step");
    this.stopAfterCycles = 1;
    return ok();
  }


  handleContinue(address, threadId) {
    trace("continue");
    this.stopAfterCycles = Infinity;
    return ok();
  }
...
}

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’.

我们可以通过附加GDB并发送'c'命令以继续执行进行测试。 我们可以看到CPU一直运行,直到通过按'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 / Z类型,addr,类型z(用于删除)和Z(用于插入断点)。 addr是地址,种类是目标特定的,通常是断点的大小(以字节为单位)。

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

'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.

从种类地址开始插入('Z')或删除('z')类型断点或监视点。

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.

    z0 / Z0 —类型为address addr的软件断点。 对于MIPS未使用; GDB发出“ M”命令以使用中断指令(0d000500)覆盖该内存地址以实现该目的。 但是无论如何,让我们实施它。

  • z1/Z1 — A hardware breakpoint at address addr.

    z1 / Z1 —地址地址处的硬件断点。
  • z2/Z2 — A write watchpoint at addr. The number of bytes to watch is specified by kind.

    z2 / Z2 —在addr处的写入监视点。 要监视的字节数按种类指定。
  • z3/Z3 — A read watchpoint at addr. The number of bytes to watch is specified by kind.

    z3 / Z3 —在addr上的读取监视点。 要监视的字节数按种类指定。
  • z4/Z4 — An access watchpoint at addr. The number of bytes to watch is specified by kind.

    z4 / Z4 — addr上的访问监视点。 要监视的字节数按种类指定。

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.

实现非常简单。 我们将忽略类型和种类,仅在给定地址处放置一个断点。 如果存在真正的CPU仿真器,则可以轻松扩展此功能以支持观察点。

// gdb-server-stub.js
export class GDBServerStub {
  handlePacket(socket, packet) {
  ...
    } else if (m = packet.match(/^([zZ])([0-4]),([0-9a-zA-Z]+),([0-9a-zA-Z]+)/)) {
      const type = parseInt(m[2]);
      const addr = parseInt(m[3], 16);
      const kind = parseInt(m[4], 16);
      if (m[1] == 'z') {
        reply = this.handler.handleRemoveBreakpoint(type, addr, kind);
      } else if (m[1] == 'Z') {
        reply = this.handler.handleAddBreakpoint(type, addr, kind);
      }
    }
  ...
  }
}
...


// r3000.js
export class R3000 extends GDBCommandHandler {
...
  run(cycles) {
    if (this.stopAfterCycles == 0) {
      return;
    }


    let cyclesToRun = Math.min(cycles, this.stopAfterCycles);
    while (cyclesToRun-- > 0) {
      this.stopAfterCycles--;
      this.registers.pc += 4;
      if (this.registers.pc in this.breakpoints) {
        this.stopAfterCycles = 0;
        break;
      }
    }


    if (this.stopAfterCycles == 0) {
      this.emit('stopped', stopped(5));
    }
  }


  handleAddBreakpoint(type, address, kind) {
    this.breakpoints[address] = true;
    return ok();
  }


  handleRemoveBreakpoint(type, address, kind) {
    if (address in this.breakpoints) {
      delete this.breakpoints[address];
      return ok();
    } else {
      return error(1);
    }
  }
...
}

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.

让我们在GDB上进行实验,我们将在0xbfc00100处设置一个硬件断点,然后单击继续命令以查看GDB是否确实会在该地址处中断。

$ gdb
(gdb) set architecture mips:3000
(gdb) target remote localhost:2424
0xbfc00000 in ?? ()
(gdb) hb *0xbfc00100
Hardware assisted breakpoint 1 at 0xbfc00100
(gdb) c
Continuing.


Breakpoint 1, 0xbfc00100 in ?? ()
(gdb)

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

确实,GDB确实如我们预期的那样在地址0xbfc00100处停止了。

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

    解析器生成器。 我们一直在使用正则表达式来解析命令,这很好。 但是重用十六进制之类的部分正则表达式并没有那么健壮和困难。 我想迁移到使用像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.

    LLDB支持。 此时,该存根与GDB配合得很好。 我认为我们现在可以尝试添加对LLDB的支持。
  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.

    TypeScript。 我一直想将项目迁移到TypeScript,以便我们获得更好的静态类型分析。 gdb-command-handler.js或多或少是一个接口,我认为最好在TypeScript中将其定义为真实接口。

To be continued…

未完待续…

翻译自: https://medium.com/@tatsuo.nomura/implement-gdb-remote-debug-protocol-stub-from-scratch-5-8aa247251709

gdb 远程调试协议

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值