RISC-V Debug Module
Table of Contents
1. RISC-V Debug Module
https://github.com/riscv/riscv-debug-spec/blob/master/riscv-debug-stable.pdf
debug module (DM) spec 描述的是 riscv 如何支持 jtag.
spike 的相关代码涉及到几个不同的组件:
- remote bitbang, 用来与 openocd 通信
- dtm (debug transport module), 解析 jtag 数据
- dmi (debug module interface), dtm 解析出 jtag dr (data register) 后会转换为相应的 dmi 调用 (dmi_read, dmi_write)
- dm (debug module), 实现 dmi, 控制 hart 执行 debug 动作
1.1. overview
以一个简单的读寄存器的操作为例, 说明 dm 的过程:
- guest 每次 step 之后会通过 remote_bitbang 的 tick 函数看是否有 jtag 命令
- jtag_dtm 解析 remote_bitbang 收到的 jtag 命令. jtag 只有四根信号线, dtm 会用一个状态机来解析数据, 得到 jtag dr (data register) 数据
- dtm 解析到一个 dr, op 为 DMI_OP_WRITE, address 为 DM_DMCONTROL, value 为 DM_DMCONTROL_HALTREQ, 导致 processor step 时通过 enter_debug_mode 进行 debug mode, pc 跳转到 DEBUG_ROM_ENTRY
- jtag_dtm 解析另一个 dr, op 为 DMI_OP_WRITE, address 为 DM_COMMAND, value 为要执行的 command
- dm 通过 perform_abstract_command 执行这个 command, 发现它是 register access
- dm 执行 `读 x0` 的 abstract command 的方法是把 `sd x0, debug_data_start(zero)`指令写到 debug_abstract_start 处, debug mode 会执行到这段代码, 把 x0 的值写到了debug_data_start.
- dtm 解析另一个 dr, op 为 DMI_OP_READ, 且 address 为 DM_DATA0, 其中 DM_DATA0 对应 debug_data_start. dm 从 debug_data_start 读出数据, 放到 dr, 最后通过 jtag_dtm 的状态机把数据返回给 remote_bitbang
- jtag_dtm 解析到另一个 dr, DMI_OP_WRITE(DM_DMCONTROL, DM_DMCONTROL_RESUMEREQ), 表示要 resume, guest 会通过 debug_rom 中的 dret 指令退出 debug mode
整个过程的核心是:
- dtm 把 jtag dr 转换为 dmi_read/dmi_write
- dmi_write
- dm 执行 dmi_write(command) 时会把 command 对应的指令写入 debug_abstract
- guest 进入 debug mode 后会执行 debug_abstract 的代码, 结果写到 data0…
- dmi_read
- dm 响应 dmi_read(data0…), 返回 command 的结果
上面的例子是基于 abstract command 的做法, 实际上 dm 还支持 progbuf 的方式:
jtag 通过 dr 向 DM_PROGBUF0 直接写入代码, 从 DM_DATA0 读到结果.
1.2. remote bitbang
void sim_t::main(): while (!done()): step(INTERLEAVE); if (remote_bitbang): remote_bitbang->tick(); void remote_bitbang_t::tick(): if (client_fd > 0): /* 已经有 openocd 连接 */ execute_commands(); else: /* 非阻塞式的等待 openocd */ this->accept(); void remote_bitbang_t::execute_commands(): while (1): uint8_t command = recv_buf[recv_start]; // jtag 端的 remote bitbang 驱动把把命令编码成一系列的 01234..., 对应不同 // pin 上的信号, 一个简单的命令需要编码成许多 set_pins 命令, 用来表示一系列 // 的状态变化, 例如: // 状态1: 准备开始传递一个 dr; // 状态2: dr 的 bit ++ 是 x; // 状态2: dr 的 bit ++ 是 y; // 状态2: dr 的 bit ++ 是 z; // ... // 状态3: dr 传递完毕, 开始执行... switch (command): case 'B': /* fprintf(stderr, "*BLINK*\n"); */ break; case 'b': /* fprintf(stderr, "_______\n"); */ break; case 'r': tap->reset(); break; case '0': tap->set_pins(0, 0, 0); break; case '1': tap->set_pins(0, 0, 1); break; case '2': tap->set_pins(0, 1, 0); break; case '3': tap->set_pins(0, 1, 1); break; case '4': tap->set_pins(1, 0, 0); break; case '5': tap->set_pins(1, 0, 1); break; case '6': tap->set_pins(1, 1, 0); break; case '7': tap->set_pins(1, 1, 1); break; // tdo 表示 data out, 与 tdi 一样, 它一次只能表示一个 bit, 为了读出一个 // dr,需要用多个 set_pins(用来对 dr 移位后得到一个 tdo) 和 tdo() 调用 case 'R': send_buf[send_offset++] = tap->tdo() ? '1' : '0'; break; case 'Q': quit = true; break; default: unsigned sent = 0; while (sent < send_offset): ssize_t bytes = write(client_fd, send_buf + sent, send_offset); sent += bytes; recv_end = read(client_fd, recv_buf, buf_size);
其中 set_pins 部分对应的 openocd 端的 remote_bigbang driver 的代码:
static int remote_bitbang_write(int tck, int tms, int tdi) { char c = '0' + ((tck ? 0x4 : 0x0) | (tms ? 0x2 : 0x0) | (tdi ? 0x1 : 0x0)); return remote_bitbang_queue(c, NO_FLUSH); }
1.3. jtag_dtm
jtag_dtm 会把前面的 set_pins 等转换成 dmi_read, dmi_write 等, 其核心的逻辑是处理 set_pins 表示的状态机, 用来读取和发送 dr
void jtag_dtm_t::set_pins(bool tck, bool tms, bool tdi) { /* NOTE: set_pins 是一个状态机, 一个简单的通过它读取 dr 时状态的变化: * TEST_LOGIN_RESET -> RUN_TEST_IDLE -> SELECT_DR_SCAN -> CAPTURE_DR -> * SHIFT_DR -> SHIFT_DR -> SHIFT_DR -> ... -> EXIT1_DR -> UPDATE_DR -> * RUN_TEST_IDLE */ const jtag_state_t next[16][2] = { /* TEST_LOGIC_RESET */ {RUN_TEST_IDLE, TEST_LOGIC_RESET}, /* RUN_TEST_IDLE */ {RUN_TEST_IDLE, SELECT_DR_SCAN}, /* SELECT_DR_SCAN */ {CAPTURE_DR, SELECT_IR_SCAN}, /* CAPTURE_DR */ {SHIFT_DR, EXIT1_DR}, /* SHIFT_DR */ {SHIFT_DR, EXIT1_DR}, /* EXIT1_DR */ {PAUSE_DR, UPDATE_DR}, /* PAUSE_DR */ {PAUSE_DR, EXIT2_DR}, /* EXIT2_DR */ {SHIFT_DR, UPDATE_DR}, /* UPDATE_DR */ {RUN_TEST_IDLE, SELECT_DR_SCAN}, /* SELECT_IR_SCAN */ {CAPTURE_IR, TEST_LOGIC_RESET}, /* CAPTURE_IR */ {SHIFT_IR, EXIT1_IR}, /* SHIFT_IR */ {SHIFT_IR, EXIT1_IR}, /* EXIT1_IR */ {PAUSE_IR, UPDATE_IR}, /* PAUSE_IR */ {PAUSE_IR, EXIT2_IR}, /* EXIT2_IR */ {SHIFT_IR, UPDATE_IR}, /* UPDATE_IR */ {RUN_TEST_IDLE, SELECT_DR_SCAN}}; if (!_tck && tck) { switch (_state) { /* NOTE: 由于 tdi 一次只能传输一个 bit, 所以通过多个 SHIFT_DR 才能拿 * 到最终的 dr */ case SHIFT_DR: dr >>= 1; dr |= (uint64_t)_tdi << (dr_length - 1); break; } _state = next[_state][_tms]; } else { // Negative clock edge. TDO is updated. switch (_state) { /* ... */ case CAPTURE_DR: capture_dr(); break; case SHIFT_DR: _tdo = dr & 1; break; case UPDATE_DR: update_dr(); break; } } _tck = tck; _tms = tms; _tdi = tdi; }
- update_dr 会把收到的 tdi 组合成 dr 后把它解析成 dmi_read 或 dmi_write 调用
- capture_dr 会把 dmi_read 的结果 (dmi) 保存到 dr, 以便通过多个 tdo 发送出去
1.4. dm
dm 负责执行 dmi_read/dmi_write, read 和 write 时操作的是 Debug Module Registers, 例如:
- data0, …, data11
- dmcontrol
- command
- hartinfo
- progbuf0, …, progbuf15
- …
spike 使用不同的 buffer 表示这些寄存器, 例如 data0 在 debug_data_start, progbuf0 在 debug_progbuf_start.
1.4.1. dmi_write
bool debug_module_t::dmi_write(unsigned address, uint32_t value) { /* NOTE: jtag 会通过读取 data0...获得 abstract command 或 progbuf 的输出, 因 * 为它们会把结果写到 DM_DATA0... */ if (address >= DM_DATA0 && address < DM_DATA0 + abstractcs.datacount) { unsigned i = address - DM_DATA0; write32(dmdata, address - DM_DATA0, value); return true; } else if ( /* NOTE: progbuf 允许 jtags 直接指定需要在 debug_mode 执行的代码 */ address >= DM_PROGBUF0 && address < DM_PROGBUF0 + config.progbufsize) { unsigned i = address - DM_PROGBUF0; write32(program_buffer, i, value); if (!abstractcs.busy && ((abstractauto.autoexecprogbuf >> i) & 1)) { perform_abstract_command(); } return true; } else { switch (address) { case DM_DMCONTROL: { dmcontrol.haltreq = get_field(value, DM_DMCONTROL_HALTREQ); dmcontrol.resumereq = get_field(value, DM_DMCONTROL_RESUMEREQ); /* ... */ for (unsigned i = 0; i < nprocs; i++) { if (hart_selected(i)) { processor_t *proc = processor(i); if (proc) { /* NOTE: halt_request 会导致 guest 在 while (1) * step() 进行 debug_mode */ proc->halt_request = dmcontrol.haltreq ? proc->HR_REGULAR : proc->HR_NONE; if (dmcontrol.resumereq) { debug_rom_flags[i] |= (1 << DEBUG_ROM_FLAG_RESUME); hart_state[i].resumeack = false; } } } } } return true; case DM_COMMAND: /* NOTE: 读写寄存器是通过 command 进行的 */ command = value; return perform_abstract_command(); /* ... */ } } return false; }
1.4.1.1. perform_abstract_command
dm 执行 jtags 指令有两种方式:
- 通过解释 command
- 通过执行 progbuf
spike 实现上把 abstract command 转换成具体的指令让 debug_mode 去执行
bool debug_module_t::perform_abstract_command() { // register access unsigned size = get_field(command, AC_ACCESS_REGISTER_AARSIZE); bool write = get_field(command, AC_ACCESS_REGISTER_WRITE); unsigned regno = get_field(command, AC_ACCESS_REGISTER_REGNO); unsigned i = 0; if (get_field(command, AC_ACCESS_REGISTER_TRANSFER)) { if (regno >= 0x1000 && regno < 0x1020) { unsigned regnum = regno - 0x1000; switch (size) { case 2: if (write) /* NOTE: 针对 read/write regnum, 在 debug_abstract 处开 * 始生成了对应的 lw/sw 指令, 且结果写在 * debug_data_start, debug_data_start 对应的实际就是 * data0 (因为 debug_module 本身是一个 mem_t, 参考它的 * load/store 函数) */ write32( debug_abstract, i++, lw(regnum, ZERO, debug_data_start)); else write32( debug_abstract, i++, sw(regnum, ZERO, debug_data_start)); break; /* ... */ default: abstractcs.cmderr = CMDERR_NOTSUP; return true; } /* ... */ } /* ... */ } if (get_field(command, AC_ACCESS_REGISTER_POSTEXEC)) { /* NOTE: 这里会生成指令跳到 debug_progbuf, 以支持 progbuf 功能, 且 jtag * 需要保证 progbuf 最后需要以 ebreak 结束 */ write32( debug_abstract, i, jal(ZERO, debug_progbuf_start - debug_abstract_start - 4 * i)); i++; } else { /* NOTE: ebreak 会导致 debug_abstract 执行完后跳转到 debug_rom 的 entry * 以重新开始 loop */ write32(debug_abstract, i++, ebreak()); } debug_rom_flags[dmcontrol.hartsel] |= 1 << DEBUG_ROM_FLAG_GO; return true; }
1.4.2. dmi_read
dmi_read 相对 dmi_write 比较简单, 它用来读取 dm register, 特别的, 通过读取 DM_HARTINFO 可以得到 data0 的地址, 以便写 progbuf 时可以指定这个地址, 后续可以通过 dmi_read 读 data0 从而得到 progbuf 的结果
1.4.3. example
使用 dmi_write 和 dmi_read 查看寄存器的例子:
测试环境参考 https://github.com/riscv-software-src/riscv-isa-sim#debugging-with-gdb
gdb: (gdb) info reg ra 0x0 0x0 sp 0x0 0x0 gp 0x0 0x0 tp 0x0 0x0 t0 0x10110000 269549568 t1 0x0 0 t2 0x0 0 fp 0x0 0x0 s1 0x0 0 a0 0x0 0 a1 0x1020 4128 a2 0x0 0 a3 0x0 0 a4 0x0 0 a5 0x10110000 269549568 dm: dmi_write(0x17, 0x321005) dmi_read(0x16) -> 0x2000002 dmi_read(0x5) -> 0x0 dmi_read(0x4) -> 0x10110000 ... dmi_write(0x17, 0x32100f) dmi_read(0x16) -> 0x2000002 dmi_read(0x5) -> 0x0 dmi_read(0x4) -> 0x10110000
- 0x17 是 DM_COMMAND
- 0x321005 表示要读取 gpr 且 regno 为 5, 对应 t0 (x5)
- 0x32100f 表示要读取 gpr 且 regno 为 15, 对应 a5 (x15)
- 0x5 是读取 data1, 0x4 是读取 data0, 因为 perform_abstract_command 会把结果写到 data0… 中
1.4.4. debug_mode
dmi_write 时会生成指令写在 debug_abstract 处, 会通过 debug_mode 跳转到这里
1.4.4.1. debug_abstract_start
debug_module_t::debug_module_t(sim_t *sim, const debug_module_config_t &config) : { /* ... */ write32( debug_rom_whereto, 0, jal(ZERO, debug_abstract_start - DEBUG_ROM_WHERETO)); reset(); } /* NOTE: 其中 debug_rom_whereto 对应 DEBUG_ROM_WHERETO (0x300) 地址的数据 */ /* NOTE: 而 0x300 在 debug_rom.S 对应的 whereto 这个符号 */ bool debug_module_t::load(reg_t addr, size_t len, uint8_t *bytes) { /* ... */ if (addr >= DEBUG_ROM_WHERETO && (addr + len) <= (DEBUG_ROM_WHERETO + 4)) { memcpy(bytes, debug_rom_whereto + addr - DEBUG_ROM_WHERETO, len); return true; } /* ... */ return false; }
1.4.4.2. enter_debug_mode
void processor_t::step(size_t n) { if (!state.debug_mode) { /* NOTE: DM_DMCONTROL_HALTREQ */ if (halt_request == HR_REGULAR) { enter_debug_mode(DCSR_CAUSE_DEBUGINT); } /* ... */ } /* execute_insn */ } void processor_t::enter_debug_mode(uint8_t cause) { state.debug_mode = true; state.dcsr->write_cause_and_prv(cause, state.prv); set_privilege(PRV_M); /* NOTE: 在退出 debug_mode 时需要用到 dpc */ state.dpc->write(state.pc); /* NOTE: DEBUG_ROM_ENTRY 是 debug_rom.S 中的 entry */ state.pc = DEBUG_ROM_ENTRY; }
1.4.4.3. debug_rom.S
debug_rom.S 是 enter_debug_mode 时使用的 rom, 它会跳转到 debug_abstract_start:
.option norvc .global entry .global exception # NOTE: entry 地址是 DEBUG_ROM_ENTRY, 它是 enter_debug_mode 时指定的 pc entry: jal zero, _entry resume: jal zero, _resume _entry: fence csrw CSR_DSCRATCH, s0 // Save s0 to allow signaling MHARTID entry_loop: csrr s0, CSR_MHARTID sw s0, DEBUG_ROM_HALTED(zero) lbu s0, DEBUG_ROM_FLAGS(s0) // 1 byte flag per hart. Only one hart advances here. andi s0, s0, (1 << DEBUG_ROM_FLAG_GO) # NOTE: going 会执行 abstract command bnez s0, going # ... jal zero, entry_loop going: # ... # NOTE: whereto 里的指令是 debug_module_t 初始化时写入的 jal debug_abstract_start jalr zero, zero, %lo(whereto) _resume: # ... #NOTE: jtag 的 DM_DMCONTROL_RESUMEREQ 最终会调用到这里, 通过 dret 退出 debug mode dret
另外, 当 debug_abstract (及 progbuf) 执行完以后, 其最后一个指令必然是 ebreak, spike 针对 ebreak 有特殊的处理:
/* ebreak.h: */ throw trap_breakpoint(STATE.v, pc); /* processor.cc: */ void processor_t::take_trap(trap_t& t, reg_t epc) { if (state.debug_mode) { if (t.cause() == CAUSE_BREAKPOINT) { /* NOTE: debug_abstract 或 progbuf 执行完以后重新开始 debug_rom 的 * loop */ state.pc = DEBUG_ROM_ENTRY; } else { state.pc = DEBUG_ROM_TVEC; } return; } /* ... */ }
1.5. debug with debug module
1.5.1. access register
读写寄存器需要使用 abstract command 或 progbuf, 参数/结果使用 data0 中, 通过 write(data0)/dmi_read(data0) 提供参数/返回结果
1.5.2. step
- 先通过 access register 向 dcsr 写入 DCSR_STEP, 表示需要单步执行.
debug mode resume 时的 dret 会根据 dcsr 设置 state.STEP_STEPPING flag
require(STATE.debug_mode); set_pc_and_serialize(STATE.dpc->read()); p->set_privilege(STATE.dcsr->prv); /* We're not in Debug Mode anymore. */ STATE.debug_mode = false; if (STATE.dcsr->step) STATE.single_step = STATE.STEP_STEPPING;
processor step 时会根据这个 flag 重新进入 debug mode
void processor_t::step(size_t n) { while (n > 0) { /* ... */ if (unlikely(slow_path())) { // Main simulation loop, slow path. while (instret < n) { if (unlikely( !state.serialized && state.single_step == state.STEP_STEPPED)) { state.single_step = state.STEP_NONE; if (!state.debug_mode) { enter_debug_mode(DCSR_CAUSE_STEP); break; } } /* NOTE: 通过 STEPPING, STEPPED 两个状态来支持 * step->debug->step->debug */ if (unlikely(state.single_step == state.STEP_STEPPING)) { state.single_step = state.STEP_STEPPED; } /* ... */ pc = execute_insn(this, pc, fetch); } } /* ... */ } }
例如:
# gdb: 0x0000000010110004 in main () at rot13.c:8 8 while (wait) (gdb) p wait=1 $1 = 1 (gdb) si [riscv.cpu] Found 4 triggers 0x0000000010110008 8 while (wait) (gdb) si 8 while (wait) (gdb) c Continuing. # dm dmi_write(0x5, 0x0) # NOTE: 0x4000b107 是写入的 dcsr 的值, 其 bit 2 为 1, 表示 step dmi_write(0x4, 0x4000b107) # NOTE: 7b0 是 dcsr dmi_write(0x17, 0x3307b0) resume hart 0 # ... dmi_write(0x5, 0x0) # NOTE: 0x4000b103 的 bit 2 为 0, 表示不再 step dmi_write(0x4, 0x4000b103) dmi_write(0x17, 0x3307b0)
另外, 通过 trigger 的 icount 看起来也可以实现 hw step
1.5.3. breakpoint
software breakpoint 通过 ebreak 支持, hardware breakpoint 通过 dm trigger 实现
1.5.3.1. ebreak
debug mode 下执行 ebreak (例如 debug_abstract 末尾自动插入的 ebreak) 会导致 debug mode 重新开始 loop.
普通模式下执行 ebreak 会导致 guest 进行 debug mode. 所以 debugger 可以通过修改指令为 ebreak 来设置 software break.
另外, gdb native debug 时 ebreak 需要触发 trap 而不是进入 debug mode (类似于 x86 的 int3, 参考 GDB Breakpoint), 这个通过 dcsr 可以配置
/* ebreak.h */ throw trap_breakpoint(STATE.v, pc); /* processor.cc */ void processor_t::take_trap(trap_t& t, reg_t epc) { if (t.cause() == CAUSE_BREAKPOINT && ((state.prv == PRV_M && state.dcsr->ebreakm) || /* NOTE: ebreak 是进入 debug mode 还是作为 trap 是通过 dcsr 配置的, * 使用 jtags 时需要配置该项, 使用 gdb native debug 时则不能配置该项 */ (state.prv == PRV_S && state.dcsr->ebreaks) || (state.prv == PRV_U && state.dcsr->ebreaku))) { enter_debug_mode(DCSR_CAUSE_SWBP); return; } /* ... */ }
Backlinks
GDB Breakpoint (GDB Breakpoint > software breakpoint): 插入 software breakpoint 时, gdb 会把断点处的指令替换为会触发 trap 的指令, 例如 x86 上的 INT3 (0xcc), 以及 riscv 上的 ebreak. 同时记下原来的旧指令.
1.5.3.2. trigger
通过操作 tselect, tdata1, tdata2 等 csr 来设置 trigger 的 action, match 等信息. 然后 mmu 在访存时会处理这些 trigger
inline tlb_entry_t translate_insn_addr(reg_t addr) { /* ... */ if (tlb_insn_tag[vpn % TLB_ENTRIES] == (vpn | TLB_CHECK_TRIGGERS)) { /* ... */ triggers::action_t action; /* NOTE: trigger 是否与 addr match */ auto match = proc->TM.memory_access_match( &action, triggers::OPERATION_EXECUTE, addr, from_target(*ptr)); if (match != triggers::MATCH_NONE) { throw triggers::matched_t( triggers::OPERATION_EXECUTE, addr, from_target(*ptr), action); } } return result; } void processor_t::step(size_t n) { while (n > 0) { size_t instret = 0; reg_t pc = state.pc; mmu_t* _mmu = mmu; try { /* execute_insn... */ } catch (triggers::matched_t& t) { switch (t.action) { case triggers::ACTION_DEBUG_MODE: enter_debug_mode(DCSR_CAUSE_HWBP); break; /* ... */ } } } }
例如:
# gdb (gdb) hbreak *0x10110000 (gdb) hbreak *0x10110004 # dm # 通过 abstract command 写 tselect 寄存器 (7a0), 选择 trigger 0 dmi_write(0x5, 0x0) dmi_write(0x4, 0x0) dmi_write(0x17, 0x3307a0) # 通过 abstract command 写 tdata0 (7a1) dmi_read(0x16) -> 0x2000002 dmi_write(0x5, 0x28000000) dmi_write(0x4, 0x105c) dmi_write(0x17, 0x3307a1) # 通过 abstract command 写 tdata1 (7a2), 写入的是要 break 的地址 dmi_write(0x5, 0x0) dmi_write(0x4, 0x10110000) dmi_write(0x17, 0x3307a2) # 通过 abstract command 写 tselect 寄存器 (7a0), 选择 trigger 1 dmi_write(0x5, 0x0) dmi_write(0x4, 0x1) dmi_write(0x17, 0x3307a0) # 通过 abstract command 写 tdata0 (7a1) dmi_write(0x5, 0x28000000) dmi_write(0x4, 0x105c) dmi_write(0x17, 0x3307a1) # 通过 abstract command 写 tdata1 (7a2), 写入的是要 break 的地址 dmi_write(0x5, 0x0) dmi_write(0x4, 0x10110004) dmi_write(0x17, 0x3307a2)
Backlinks
GDB Breakpoint (GDB Breakpoint > hardware breakpoint): 参考 riscv trigger
1.5.4. debug module vs. gdb native debug
debug module 是做为 external debugger (jtag) 的 stub 来使用的. gdb native debug 使用 ptrace, 并不依赖 debug module:
- access register 使用 kernel stack 里保存的上下文
- step 和 breakpoint 通常是软件实现的, 例如修改内存插入 ebreak 指令. 有时可以使用 hw step 和 hw breakpoint 支持. 例如, 通过配置 tdata 的 action 字段, RISC-V 的 trigger 可以触发 trap (而不进入 debug mode), 从而给 native debug 提供 hw breakpoint 支持.
Backlinks
GDB Remote Serial Protocal (GDB Remote Serial Protocal > Overview): RSP 的 server 端运行在被调试的设备上, server 可以在遵守 RSP 协议的基础上自由开发, 例如 qemu 和 openocd 自带的 server. 如果被调试的设备本身就有 gdb 的支持, 则可以 使用 gdb 自带的 gdbserver, 例如 android 上的 gdbserver
Spike (Spike > debug module): debug module