#include "tb_gdb.h" #include "tb_constants.h" #include #include #include #include #include #include #include #include #include #include static bool send_all(int fd, const uint8_t *buf, size_t len) { size_t sent = 0; while (sent < len) { #ifdef MSG_NOSIGNAL ssize_t n = send(fd, buf + sent, len - sent, MSG_NOSIGNAL); #else ssize_t n = send(fd, buf + sent, len - sent, 0); #endif if (n < 0) { if (errno == EINTR) continue; return false; } sent += (size_t)n; } return true; } tb_gdb_server::tb_gdb_server(uint16_t port, tb_gdb_target &target) : port_{port}, target_{target} { target_xml_ = "\n" "\n" "\n" " riscv:rv32\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; char mm_line[128]; snprintf( mm_line, sizeof(mm_line), " \n", (unsigned)MEM_BASE, (unsigned)MEM_SIZE ); memory_map_xml_ = "\n" "\n" "\n" + std::string(mm_line) + "\n"; } tb_gdb_server::~tb_gdb_server() { close_client_(); if (server_fd_ >= 0) { ::close(server_fd_); server_fd_ = -1; } } bool tb_gdb_server::open_listen_socket_() { server_fd_ = socket(AF_INET, SOCK_STREAM, 0); if (server_fd_ < 0) { fprintf(stderr, "tb_gdb: socket() failed: %s\n", strerror(errno)); return false; } int opt = 1; if (setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { fprintf(stderr, "tb_gdb: setsockopt(SO_REUSEADDR) failed: %s\n", strerror(errno)); return false; } #ifdef SO_REUSEPORT (void)setsockopt(server_fd_, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); #endif sockaddr_in addr{}; addr.sin_family = AF_INET; // Debug services are local by default. Remote access belongs behind an // explicit SSH tunnel, never an unauthenticated wildcard listener. addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = htons(port_); if (bind(server_fd_, (sockaddr *)&addr, sizeof(addr)) < 0) { fprintf(stderr, "tb_gdb: bind(127.0.0.1:%u) failed: %s\n", (unsigned)port_, strerror(errno)); return false; } if (listen(server_fd_, 1) < 0) { fprintf(stderr, "tb_gdb: listen() failed: %s\n", strerror(errno)); return false; } return true; } bool tb_gdb_server::accept_client_() { sockaddr_in addr{}; socklen_t addr_len = sizeof(addr); client_fd_ = accept(server_fd_, (sockaddr *)&addr, &addr_len); if (client_fd_ < 0) { fprintf(stderr, "tb_gdb: accept() failed: %s\n", strerror(errno)); return false; } int flag = 1; (void)setsockopt(client_fd_, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag)); return true; } void tb_gdb_server::close_client_() { if (client_fd_ >= 0) { ::close(client_fd_); client_fd_ = -1; } no_ack_mode_ = false; breakpoints_.clear(); ignore_breakpoint_pc_ = 0xffffffffu; } uint8_t tb_gdb_server::checksum_(const std::string &s) { uint8_t sum = 0; for (unsigned char c : s) sum = (uint8_t)(sum + c); return sum; } int tb_gdb_server::hex_val_(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); return -1; } std::string tb_gdb_server::to_hex_bytes_(const uint8_t *data, size_t len) { static const char *hex = "0123456789abcdef"; std::string out; out.reserve(len * 2); for (size_t i = 0; i < len; ++i) { out.push_back(hex[(data[i] >> 4) & 0xf]); out.push_back(hex[data[i] & 0xf]); } return out; } bool tb_gdb_server::from_hex_bytes_(const std::string &hex, std::string &out_bytes) { out_bytes.clear(); if (hex.size() % 2 != 0) return false; out_bytes.reserve(hex.size() / 2); for (size_t i = 0; i < hex.size(); i += 2) { int hi = hex_val_(hex[i]); int lo = hex_val_(hex[i + 1]); if (hi < 0 || lo < 0) return false; out_bytes.push_back((char)((hi << 4) | lo)); } return true; } void tb_gdb_server::append_u32_le_hex_(std::string &out, uint32_t v) { for (int i = 0; i < 4; ++i) { const uint8_t b = (uint8_t)((v >> (8 * i)) & 0xffu); static const char *hex = "0123456789abcdef"; out.push_back(hex[(b >> 4) & 0xf]); out.push_back(hex[b & 0xf]); } } bool tb_gdb_server::parse_u32_hex_(const std::string &s, uint32_t &out) { if (s.empty()) return false; uint32_t v = 0; for (char c : s) { int h = hex_val_(c); if (h < 0) return false; v = (v << 4) | (uint32_t)h; } out = v; return true; } bool tb_gdb_server::maybe_send_ack_(bool ok) { if (no_ack_mode_) return true; const char c = ok ? '+' : '-'; return send_all(client_fd_, (const uint8_t *)&c, 1); } bool tb_gdb_server::send_packet_(const std::string &payload) { std::string pkt; pkt.reserve(payload.size() + 4); pkt.push_back('$'); pkt += payload; pkt.push_back('#'); uint8_t cksum = checksum_(payload); static const char *hex = "0123456789abcdef"; pkt.push_back(hex[(cksum >> 4) & 0xf]); pkt.push_back(hex[cksum & 0xf]); return send_all(client_fd_, (const uint8_t *)pkt.data(), pkt.size()); } bool tb_gdb_server::check_interrupt_() { uint8_t c = 0; ssize_t n = recv(client_fd_, &c, 1, MSG_DONTWAIT | MSG_PEEK); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return false; return true; } if (n == 0) return true; if (c != 0x03) return false; (void)recv(client_fd_, &c, 1, MSG_DONTWAIT); return true; } bool tb_gdb_server::recv_packet_(std::string &out_payload, bool &got_interrupt) { out_payload.clear(); got_interrupt = false; while (true) { uint8_t c = 0; ssize_t n = recv(client_fd_, &c, 1, 0); if (n == 0) return false; if (n < 0) { if (errno == EINTR) continue; return false; } if (c == 0x03) { got_interrupt = true; return true; } if (c == '+' || c == '-') { continue; } if (c != '$') { continue; } std::string payload; while (true) { n = recv(client_fd_, &c, 1, 0); if (n == 0) return false; if (n < 0) { if (errno == EINTR) continue; return false; } if (c == '#') break; payload.push_back((char)c); } char ck[2]; for (int i = 0; i < 2; ++i) { n = recv(client_fd_, &c, 1, 0); if (n == 0) return false; if (n < 0) { if (errno == EINTR) { --i; continue; } return false; } ck[i] = (char)c; } bool ok = true; const int hi = hex_val_(ck[0]); const int lo = hex_val_(ck[1]); if (hi < 0 || lo < 0) { ok = false; } else { const uint8_t expected = (uint8_t)((hi << 4) | lo); ok = expected == checksum_(payload); } if (!maybe_send_ack_(ok)) return false; if (!ok) continue; out_payload = std::move(payload); return true; } } std::string tb_gdb_server::stop_reply_(int signo) const { char buf[8]; snprintf(buf, sizeof(buf), "S%02x", signo & 0xff); return std::string(buf); } std::string tb_gdb_server::stop_reply_exit_() const { char buf[8]; snprintf(buf, sizeof(buf), "W%02x", (unsigned)(target_.exit_code() & 0xffu)); return std::string(buf); } std::string tb_gdb_server::handle_command_(const std::string &cmd, bool &should_close) { should_close = false; if (cmd.empty()) return ""; // General queries if (cmd == "?") return stop_reply_(5); // SIGTRAP if (cmd.rfind("qSupported", 0) == 0) { return "PacketSize=4000;qXfer:features:read+;qXfer:memory-map:read+;QStartNoAckMode+;swbreak+;hwbreak+;vContSupported+;qRcmd+"; } if (cmd == "QStartNoAckMode") { no_ack_mode_ = true; return "OK"; } if (cmd == "qAttached") return "1"; if (cmd == "qC") return "QC1"; if (cmd == "qfThreadInfo") return "m1"; if (cmd == "qsThreadInfo") return "l"; if (cmd.rfind("H", 0) == 0) return "OK"; if (cmd.rfind("qThreadExtraInfo", 0) == 0) { const std::string s = "hart0"; return to_hex_bytes_((const uint8_t *)s.data(), s.size()); } if (cmd.rfind("qSymbol", 0) == 0) return "OK"; if (cmd == "qTStatus") return ""; // GDB "monitor" commands (OpenOCD-style). Payload is hex-encoded bytes. if (cmd.rfind("qRcmd,", 0) == 0) { const std::string hex = cmd.substr(strlen("qRcmd,")); std::string decoded; if (!from_hex_bytes_(hex, decoded)) return "E01"; std::string console; if (!target_.monitor_cmd(decoded, console)) return ""; if (!console.empty()) { const std::string payload = "O" + to_hex_bytes_((const uint8_t *)console.data(), console.size()); (void)send_packet_(payload); } return "OK"; } // Extended-remote mode request if (cmd == "!") return "OK"; // target.xml for riscv32 (x0..x31 + pc) if (cmd.rfind("qXfer:features:read:target.xml:", 0) == 0) { const std::string args = cmd.substr(strlen("qXfer:features:read:target.xml:")); const size_t comma = args.find(','); if (comma == std::string::npos) return "E01"; uint32_t off = 0, len = 0; if (!parse_u32_hex_(args.substr(0, comma), off) || !parse_u32_hex_(args.substr(comma + 1), len)) return "E01"; if (off >= target_xml_.size()) return "l"; const size_t end = std::min(target_xml_.size(), (size_t)off + (size_t)len); const bool more = end < target_xml_.size(); std::string out; out.reserve(1 + (end - off)); out.push_back(more ? 'm' : 'l'); out.append(target_xml_, off, end - off); return out; } // memory-map.xml (RAM region for disassembly / memory accessibility) if (cmd.rfind("qXfer:memory-map:read::", 0) == 0) { const std::string args = cmd.substr(strlen("qXfer:memory-map:read::")); const size_t comma = args.find(','); if (comma == std::string::npos) return "E01"; uint32_t off = 0, len = 0; if (!parse_u32_hex_(args.substr(0, comma), off) || !parse_u32_hex_(args.substr(comma + 1), len)) return "E01"; if (off >= memory_map_xml_.size()) return "l"; const size_t end = std::min(memory_map_xml_.size(), (size_t)off + (size_t)len); const bool more = end < memory_map_xml_.size(); std::string out; out.reserve(1 + (end - off)); out.push_back(more ? 'm' : 'l'); out.append(memory_map_xml_, off, end - off); return out; } // Registers if (cmd == "g") { std::string out; out.reserve(33 * 8); for (uint32_t r = 0; r < 33; ++r) append_u32_le_hex_(out, target_.read_reg(r)); return out; } if (cmd.size() >= 2 && cmd[0] == 'p') { uint32_t regno = 0; if (!parse_u32_hex_(cmd.substr(1), regno)) return "E01"; std::string out; out.reserve(8); append_u32_le_hex_(out, target_.read_reg(regno)); return out; } if (cmd.size() >= 3 && cmd[0] == 'P') { const size_t eq = cmd.find('='); if (eq == std::string::npos) return "E01"; uint32_t regno = 0; if (!parse_u32_hex_(cmd.substr(1, eq - 1), regno)) return "E01"; std::string bytes; if (!from_hex_bytes_(cmd.substr(eq + 1), bytes)) return "E01"; if (bytes.size() < 4) return "E01"; uint32_t v = (uint8_t)bytes[0] | ((uint32_t)(uint8_t)bytes[1] << 8) | ((uint32_t)(uint8_t)bytes[2] << 16) | ((uint32_t)(uint8_t)bytes[3] << 24); target_.write_reg(regno, v); return "OK"; } if (cmd.size() >= 1 && cmd[0] == 'G') { std::string bytes; if (!from_hex_bytes_(cmd.substr(1), bytes)) return "E01"; if (bytes.size() < 33 * 4) return "E01"; for (uint32_t r = 0; r < 33; ++r) { const size_t i = r * 4; uint32_t v = (uint8_t)bytes[i + 0] | ((uint32_t)(uint8_t)bytes[i + 1] << 8) | ((uint32_t)(uint8_t)bytes[i + 2] << 16) | ((uint32_t)(uint8_t)bytes[i + 3] << 24); target_.write_reg(r, v); } return "OK"; } // Memory if (cmd.size() >= 2 && cmd[0] == 'm') { const size_t comma = cmd.find(','); if (comma == std::string::npos) return "E01"; uint32_t addr = 0, len = 0; if (!parse_u32_hex_(cmd.substr(1, comma - 1), addr) || !parse_u32_hex_(cmd.substr(comma + 1), len)) return "E01"; if (len == 0) return ""; std::string buf(len, '\0'); if (!target_.read_mem(addr, (uint8_t *)&buf[0], (size_t)len)) return "E01"; return to_hex_bytes_((const uint8_t *)buf.data(), buf.size()); } if (cmd.size() >= 2 && cmd[0] == 'M') { const size_t comma = cmd.find(','); const size_t colon = cmd.find(':'); if (comma == std::string::npos || colon == std::string::npos || comma > colon) return "E01"; uint32_t addr = 0, len = 0; if (!parse_u32_hex_(cmd.substr(1, comma - 1), addr) || !parse_u32_hex_(cmd.substr(comma + 1, colon - comma - 1), len)) return "E01"; if (len == 0) return "OK"; std::string bytes; if (!from_hex_bytes_(cmd.substr(colon + 1), bytes)) return "E01"; if (bytes.size() < len) return "E01"; if (!target_.write_mem(addr, (const uint8_t *)bytes.data(), len)) return "E01"; return "OK"; } // Execute breakpoints. // // GDB's stock stepi implementation may place a temporary software // breakpoint (Z0) or hardware breakpoint (Z1) at the predicted next PC, // for example when stepping across an indirect jalr into a read-only text // section. This testbench doesn't patch target memory, so both packet types // are handled identically as execute breakpoints checked in the run loop. if (cmd.rfind("Z0,", 0) == 0 || cmd.rfind("z0,", 0) == 0 || cmd.rfind("Z1,", 0) == 0 || cmd.rfind("z1,", 0) == 0) { const bool set = cmd[0] == 'Z'; const size_t comma1 = cmd.find(','); const size_t comma2 = cmd.find(',', comma1 + 1); if (comma1 == std::string::npos || comma2 == std::string::npos) return "E01"; uint32_t addr = 0; if (!parse_u32_hex_(cmd.substr(comma1 + 1, comma2 - comma1 - 1), addr)) return "E01"; if (set) breakpoints_.insert(addr); else breakpoints_.erase(addr); return "OK"; } // vCont if (cmd == "vCont?") return "vCont;c;s"; auto do_step = [&](bool single_step) -> std::string { const uint32_t start_pc = target_.read_reg(32); const bool ignore_start_break = single_step || start_pc == ignore_breakpoint_pc_; const bool consume_ignore_pc = start_pc == ignore_breakpoint_pc_; if (consume_ignore_pc) ignore_breakpoint_pc_ = 0xffffffffu; if (single_step) { const tb_gdb_run_result r = target_.step_instruction(); if (r == tb_gdb_run_result::exited) { // In extended-remote mode keep the connection alive after target exit, // so the user can issue monitor commands such as "reset halt". return stop_reply_exit_(); } if (r == tb_gdb_run_result::timed_out) { should_close = true; return stop_reply_(14); // SIGALRM } ignore_breakpoint_pc_ = 0xffffffffu; return stop_reply_(5); // SIGTRAP } bool first_iter = true; while (true) { if (check_interrupt_()) { ignore_breakpoint_pc_ = 0xffffffffu; return stop_reply_(2); // SIGINT } const uint32_t pc = target_.read_reg(32); if (breakpoints_.count(pc) && !(first_iter && ignore_start_break && pc == start_pc)) { ignore_breakpoint_pc_ = pc; return stop_reply_(5); // SIGTRAP } const tb_gdb_run_result r = target_.step_instruction(); ignore_breakpoint_pc_ = 0xffffffffu; if (r == tb_gdb_run_result::exited) { // In extended-remote mode keep the connection alive after target exit, // so the user can issue monitor commands such as "reset halt". return stop_reply_exit_(); } if (r == tb_gdb_run_result::timed_out) { should_close = true; return stop_reply_(14); // SIGALRM } first_iter = false; } }; if (cmd.size() >= 1 && (cmd[0] == 'c' || cmd[0] == 's')) { // Optional resume address if (cmd.size() > 1) { uint32_t addr = 0; if (!parse_u32_hex_(cmd.substr(1), addr)) return "E01"; target_.write_reg(32, addr); } return do_step(cmd[0] == 's'); } if (cmd.rfind("vCont;", 0) == 0) { if (cmd == "vCont;c") return do_step(false); if (cmd == "vCont;s") return do_step(true); return ""; } // Detach / kill if (cmd == "D") { should_close = true; return "OK"; } if (cmd == "k") { should_close = true; return "OK"; } // Unknown/unsupported -> empty response return ""; } tb_gdb_run_result tb_gdb_server::serve() { if (!open_listen_socket_()) return tb_gdb_run_result::error; fprintf(stderr, "tb_gdb: listening on 127.0.0.1:%u\n", (unsigned)port_); if (!accept_client_()) return tb_gdb_run_result::error; fprintf(stderr, "tb_gdb: client connected\n"); if (!send_packet_(stop_reply_(5))) return tb_gdb_run_result::error; while (true) { std::string cmd; bool got_interrupt = false; if (!recv_packet_(cmd, got_interrupt)) break; if (got_interrupt) { if (!send_packet_(stop_reply_(2))) break; continue; } bool should_close = false; std::string reply = handle_command_(cmd, should_close); if (!send_packet_(reply)) break; if (should_close) { if (!reply.empty() && reply[0] == 'W') return tb_gdb_run_result::exited; break; } } close_client_(); return tb_gdb_run_result::ok; }