diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 0a45dae..51d07af 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -21,7 +21,7 @@ jobs: - name: setup - run: nix develop .#matar-clang -c meson setup $BUILDDIR + run: nix develop .#matar-clang -c meson setup $BUILDDIR -Dgdb_debug=true - name: fmt run: nix develop .#matar-clang -c ninja clang-format-check -C $BUILDDIR diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index b61836a..30c5f31 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -21,7 +21,7 @@ jobs: - name: setup - run: nix develop .#matar -c meson setup $BUILDDIR + run: nix develop .#matar -c meson setup $BUILDDIR -Dgdb_debug=true - name: build run: nix develop .#matar -c ninja -C $BUILDDIR diff --git a/include/bus.hh b/include/bus.hh index e2b7206..6d74f77 100644 --- a/include/bus.hh +++ b/include/bus.hh @@ -31,7 +31,6 @@ class Bus { uint32_t read_word(uint32_t, bool = true); void write_word(uint32_t, uint32_t, bool = true); - // not sure what else to do? inline void internal_cycle() { cycles++; } diff --git a/include/cpu/cpu.hh b/include/cpu/cpu.hh index a752f22..2a25ecf 100644 --- a/include/cpu/cpu.hh +++ b/include/cpu/cpu.hh @@ -4,11 +4,19 @@ #include "bus.hh" #include "cpu/psr.hh" #include "thumb/instruction.hh" +#include #include -#include +#ifdef GDB_DEBUG +#include +#endif namespace matar { + +#ifdef GDB_DEBUG +class GdbRsp; +#endif + class Cpu { public: Cpu(std::shared_ptr) noexcept; @@ -16,24 +24,12 @@ class Cpu { void step(); void chg_mode(const Mode to); - bool is_flushed = false; - inline void flush_pipeline() { - is_flushed = true; - if (cpsr.state() == State::Arm) { - opcodes[0] = bus->read_word(pc, false); - advance_pc_arm(); - opcodes[1] = bus->read_word(pc); - advance_pc_arm(); - } else { - opcodes[0] = bus->read_halfword(pc, false); - advance_pc_thumb(); - opcodes[1] = bus->read_halfword(pc); - advance_pc_thumb(); - } - sequential = true; - }; + inline bool is_halted() { return halted; } + inline void resume() { halted = false; } private: + bool halted = false; + friend void arm::Instruction::exec(Cpu& cpu); friend void thumb::Instruction::exec(Cpu& cpu); @@ -93,5 +89,26 @@ class Cpu { inline void advance_pc_arm() { pc += arm::INSTRUCTION_SIZE; }; inline void advance_pc_thumb() { pc += thumb::INSTRUCTION_SIZE; } + + bool is_flushed = false; + inline void flush_pipeline() { + if (cpsr.state() == State::Arm) { + opcodes[0] = bus->read_word(pc, false); + advance_pc_arm(); + opcodes[1] = bus->read_word(pc); + advance_pc_arm(); + } else { + opcodes[0] = bus->read_halfword(pc, false); + advance_pc_thumb(); + opcodes[1] = bus->read_halfword(pc); + advance_pc_thumb(); + } + sequential = true; + }; + +#ifdef GDB_DEBUG + friend class GdbRsp; + std::unordered_set breakpoints = {}; +#endif }; } diff --git a/meson.build b/meson.build index 9bc3fce..95bc8db 100644 --- a/meson.build +++ b/meson.build @@ -7,8 +7,18 @@ project('matar', 'cpp', 'cpp_std=c++23', 'default_library=static']) +lib_cpp_args = [] compiler = meson.get_compiler('cpp') +if get_option('disassembler') + lib_cpp_args += '-DDISASSEMBLER' +endif + +if get_option('gdb_debug') + lib_cpp_args += '-DGDB_DEBUG' +endif + + subdir('include') subdir('src') subdir('apps') diff --git a/meson.options b/meson.options index 8937498..7e5ee12 100644 --- a/meson.options +++ b/meson.options @@ -1,2 +1,3 @@ option('tests', type : 'boolean', value : true, description: 'enable tests') option('disassembler', type: 'boolean', value: true, description: 'enable disassembler') +option('gdb_debug', type: 'boolean', value: false, description: 'enable GDB RSP server') diff --git a/src/bus.cc b/src/bus.cc index 83880cd..ce6213d 100644 --- a/src/bus.cc +++ b/src/bus.cc @@ -136,6 +136,7 @@ Bus::write(uint32_t address) { #undef MATCHES } + glogger.error("Invalid memory region written"); return {}; } diff --git a/src/cpu/cpu.cc b/src/cpu/cpu.cc index 22620b8..ac4b004 100644 --- a/src/cpu/cpu.cc +++ b/src/cpu/cpu.cc @@ -129,6 +129,14 @@ Cpu::step() { // word align rst_bit(pc, 1); +#ifdef GDB_DEBUG + if (breakpoints.contains(pc - 2 * arm::INSTRUCTION_SIZE)) { + glogger.info_bold("CPU halted"); + halted = true; + return; + } +#endif + arm::Instruction instruction(opcodes[0]); opcodes[0] = opcodes[1]; @@ -148,6 +156,15 @@ Cpu::step() { } else advance_pc_arm(); } else { + +#ifdef GDB_DEBUG + if (breakpoints.contains(pc - 2 * thumb::INSTRUCTION_SIZE)) { + glogger.info_bold("CPU halted"); + halted = true; + return; + } +#endif + thumb::Instruction instruction(opcodes[0]); opcodes[0] = opcodes[1]; diff --git a/src/gdb_rsp.cc b/src/gdb_rsp.cc new file mode 100644 index 0000000..cbd62c0 --- /dev/null +++ b/src/gdb_rsp.cc @@ -0,0 +1,481 @@ +#include "gdb_rsp.hh" +#include "util/log.hh" +#include +#include +#include +#include +#include + +namespace matar { + +template +static inline constexpr void +gdb_log(const std::format_string& fmt, Args&&... args) { + glogger.debug("GDB: {}", std::format(fmt, std::forward(args)...)); +} + +static inline void +append_le(std::string& str, uint32_t value) { + // little endian only + str += std::format("{:02x}", value & 0xFF); + str += std::format("{:02x}", value >> 8 & 0xFF); + str += std::format("{:02x}", value >> 16 & 0xFF); + str += std::format("{:02x}", value >> 24 & 0xFF); +} + +static inline std::string +be_to_le(std::string str) { + if (str.length() != 8) + throw std::out_of_range("string is supposed to be 8 bytes"); + + std::string current; + + for (int i = 7; i >= 0; i -= 2) { + current += str[i - 1]; + current += str[i]; + } + + return current; +} + +GdbRsp::GdbRsp(std::shared_ptr cpu) + : cpu(cpu) {} + +void +GdbRsp::start(const uint port) { + server.start(port); + server.run(); + + std::string msg; + + while (!attached) { + step(); + } +} + +void +GdbRsp::satisfy_client() { + while (server.client_waiting() && attached) { + step(); + } +} + +void +GdbRsp::step() { + std::string msg = receive(); + + switch (msg[0]) { + case '+': + break; + case '-': + break; + case '\x03': + gdb_log("ctrl+c interrupt received"); + cmd_halted(); + break; + case '$': { + if (msg.starts_with("$qSupported")) { + acknowledge(); + cmd_supported(msg); + } else if (msg.starts_with("$qAttached")) { + acknowledge(); + cmd_attached(); + } else { + switch (msg[1]) { + case '?': + acknowledge(); + cmd_halted(); + break; + case 'g': + acknowledge(); + cmd_read_registers(); + break; + case 'G': + acknowledge(); + cmd_write_registers(msg); + break; + case 'p': + acknowledge(); + cmd_read_register(msg); + break; + case 'P': + acknowledge(); + cmd_write_register(msg); + break; + case 'm': + acknowledge(); + cmd_read_memory(msg); + break; + case 'M': + acknowledge(); + cmd_write_memory(msg); + break; + case 'z': + acknowledge(); + cmd_rm_breakpoint(msg); + break; + case 'Z': + acknowledge(); + cmd_add_breakpoint(msg); + break; + case 'c': + acknowledge(); + cmd_continue(); + break; + case 'D': + acknowledge(); + cmd_detach(); + break; + default: + gdb_log("unknown command"); + send_empty(); + } + } + + break; + } + default: + gdb_log("unknown message received"); + } +} + +std::string +GdbRsp::receive() { + std::string msg = server.receive(1); + char ch = msg[0]; + int checksum = 0; + + if (ch == '$') { + while ((ch = server.receive(1)[0]) != '#') { + checksum += static_cast(ch); + msg += ch; + if (msg.length() > MAX_MSG_LEN) { + throw std::logic_error("GDB: received message is too long"); + } + } + + if (std::stoul(server.receive(2), nullptr, 16) != (checksum & 0xFF)) { + gdb_log("{}", msg); + throw std::logic_error("GDB: bad message checksum"); + } + } + + gdb_log("received message \"{}\"", msg); + return msg; +} + +std::string +GdbRsp::make_packet(std::string raw) { + uint checksum = std::accumulate(raw.begin(), raw.end(), 0); + return std::format("${}#{:02x}", raw, checksum & 0xFF); +} + +void +GdbRsp::acknowledge() { + server.send("+"); +} + +void +GdbRsp::send_empty() { + acknowledge(); + server.send(make_packet("")); +} + +void +GdbRsp::notify_breakpoint_reached() { + gdb_log("reached breakpoint, sending signal"); + server.send(make_packet(std::format("S{:02x}", SIGTRAP))); +} + +void +GdbRsp::cmd_attached() { + attached = true; + + gdb_log("server is now attached"); + server.send(make_packet("1")); +} + +void +GdbRsp::cmd_supported(std::string msg) { + std::string response; + + if (msg.find("hwbreak+;") != std::string::npos) + response += "hwbreak+;"; + + gdb_log("sending response for qSupported"); + server.send(make_packet(response)); +} + +void +GdbRsp::cmd_halted() { + gdb_log("sending reason for upcoming halt"); + server.send(make_packet(std::format("S{:02x}", SIGTRAP))); +} + +void +GdbRsp::cmd_read_registers() { + std::string response; + + for (int i = 0; i < cpu->GPR_COUNT - 1; i++) + append_le(response, cpu->gpr[i]); + + // for some reason this PC needs to be the address of executing instruction + // i.e, two instructions behind actual PC + append_le(response, + cpu->pc - 2 * (cpu->cpsr.state() == State::Arm + ? arm::INSTRUCTION_SIZE + : thumb::INSTRUCTION_SIZE)); + + gdb_log("sending register values"); + server.send(make_packet(response)); +} + +void +GdbRsp::cmd_write_registers(std::string msg) { + static std::regex rgx("\\$G([0-9A-Fa-f]+)"); + std::smatch sm; + regex_match(msg, sm, rgx); + + if (sm.size() != 2 || sm[1].str().size() != 16 * 8) { + gdb_log("invalid arguments to write registers"); + send_empty(); + return; + } + + try { + std::string values = sm[1].str(); + + for (uint i = 0, j = 0; i < values.length() - 8; i += 8, j++) { + cpu->gpr[i] = std::stoul(sm[i + 1].str(), nullptr, 16); + cpu->gpr[j] = + std::stoul(be_to_le(values.substr(i, 8)), nullptr, 16); + } + + gdb_log("register values written"); + server.send(OK_MSG); + } catch (const std::exception& e) { + gdb_log("{}", e.what()); + send_empty(); + } +} + +void +GdbRsp::cmd_read_register(std::string msg) { + std::string response; + + try { + uint reg = std::stoul(msg.substr(2), nullptr, 16); + // 25th register is CPSR in gdb ARM + if (reg == 25) + append_le(response, cpu->cpsr.raw()); + else if (reg < cpu->GPR_COUNT) + append_le(response, cpu->gpr[reg]); + else + response += "xxxxxxxx"; + + gdb_log("sending single register value"); + server.send(make_packet(response)); + } catch (const std::exception& e) { + gdb_log("{}", e.what()); + send_empty(); + } +} + +void +GdbRsp::cmd_write_register(std::string msg) { + static std::regex rgx("\\$P([0-9A-Fa-f]+)\\=([0-9A-Fa-f]+)"); + std::smatch sm; + regex_match(msg, sm, rgx); + + if (sm.size() != 3 && sm[2].str().length() != 8) { + gdb_log("invalid arguments to write single register"); + send_empty(); + return; + } + + try { + uint reg = std::stoul(sm[1].str(), nullptr, 16); + uint32_t value = std::stoul(be_to_le(sm[2].str()), nullptr, 16); + + dbg(value); + + if (reg == 25) + cpu->cpsr.set_all(value); + else if (reg < cpu->GPR_COUNT) + cpu->gpr[reg] = value; + + gdb_log("single register value written"); + server.send(OK_MSG); + } catch (const std::exception& e) { + gdb_log("{}", e.what()); + send_empty(); + } +} + +void +GdbRsp::cmd_read_memory(std::string msg) { + std::string response; + bool sequential = false; + + static std::regex rgx("\\$m([0-9A-Fa-f]+),([0-9A-Fa-f]+)"); + std::smatch sm; + regex_match(msg, sm, rgx); + + if (sm.size() != 3) { + gdb_log("invalid arguments to read memory"); + send_empty(); + return; + } + + uint32_t address = 0, length = 0; + + try { + address = std::stoul(sm[1].str(), nullptr, 16); + length = std::stoul(sm[2].str(), nullptr, 16); + } catch (const std::exception& e) { + gdb_log("{}", e.what()); + send_empty(); + return; + } + + for (uint i = 0; i < length; i++) { + response += + std::format("{:02x}", cpu->bus->read_byte(address + i), sequential); + sequential = true; + } + + cpu->sequential = false; + gdb_log("sending memory values values"); + server.send(make_packet(response)); +} + +void +GdbRsp::cmd_write_memory(std::string msg) { + bool sequential = false; + + static std::regex rgx("\\$M([0-9A-Fa-f]+),([0-9A-Fa-f]+):([0-9A-Fa-f]+)"); + std::smatch sm; + regex_match(msg, sm, rgx); + + if (sm.size() != 4) { + gdb_log("invalid arguments to write memory"); + send_empty(); + return; + } + + try { + uint32_t address = std::stoul(sm[1].str(), nullptr, 16); + uint32_t length = std::stoul(sm[2].str(), nullptr, 16); + + std::string values = sm[3].str(); + + for (uint i = 0, j = 0; i < length && j < values.size(); i++, j += 2) { + cpu->bus->write_byte(address + i, + std::stoul(values.substr(j, 2), nullptr, 16) & + 0xFF, + sequential); + glogger.warn("hi {:02x}", cpu->bus->read_byte(address + i)); + sequential = true; + } + + cpu->sequential = false; + gdb_log("register values written"); + server.send(OK_MSG); + } catch (const std::exception& e) { + gdb_log("{}", e.what()); + send_empty(); + } +} + +void +GdbRsp::cmd_rm_breakpoint(std::string msg) { + static std::regex rgx("\\$z(0|1),([0-9A-Fa-f]+),(2|3|4)"); + std::smatch sm; + regex_match(msg, sm, rgx); + + if (sm.size() != 4) { + gdb_log("invalid arguments to remove breakpoint"); + send_empty(); + return; + } + + if (sm[1].str() != "0" && sm[0].str() != "1") { + gdb_log("unrecognized breakpoint type encountered"); + send_empty(); + return; + } + + if (sm[3].str() != "3" && sm[3].str() != "4") { + gdb_log("only 32 bit breakpoints supported"); + send_empty(); + return; + } + + try { + uint32_t address = std::stoul(sm[2].str(), nullptr, 16); + + cpu->breakpoints.erase(address); + gdb_log("breakpoint {:#08x} removed", address); + server.send(OK_MSG); + } catch (const std::exception& e) { + gdb_log("{}", e.what()); + send_empty(); + } +} + +void +GdbRsp::cmd_add_breakpoint(std::string msg) { + static std::regex rgx("\\$Z(0|1),([0-9A-Fa-f]+),(2|3|4)"); + std::smatch sm; + regex_match(msg, sm, rgx); + dbg(sm.size()); + dbg(sm[0].str()); + + if (sm.size() != 4) { + gdb_log("invalid arguments to add breakpoint"); + send_empty(); + return; + } + + if (sm[1].str() != "0" && sm[0].str() != "1") { + gdb_log("unrecognized breakpoint type encountered"); + send_empty(); + return; + } + + if (sm[3].str() != "3" && sm[3].str() != "4") { + gdb_log("only 32 bit breakpoints supported"); + send_empty(); + return; + } + + try { + uint32_t address = std::stoul(sm[2].str(), nullptr, 16); + + cpu->breakpoints.insert(address); + gdb_log("breakpoint {:#08x} added", address); + server.send(OK_MSG); + } catch (const std::exception& e) { + gdb_log("{}", e.what()); + send_empty(); + } +} + +void +GdbRsp::cmd_detach() { + attached = false; + cpu->resume(); + gdb_log("detached"); + server.send(OK_MSG); +} + +void +GdbRsp::cmd_continue() { + cpu->resume(); + gdb_log("continued"); + server.send(OK_MSG); + while (true) { + cpu->step(); + } +} +} diff --git a/src/gdb_rsp.hh b/src/gdb_rsp.hh new file mode 100644 index 0000000..6438572 --- /dev/null +++ b/src/gdb_rsp.hh @@ -0,0 +1,43 @@ +#include "cpu/cpu.hh" +#include "util/tcp_server.hh" + +namespace matar { +class GdbRsp { + public: + GdbRsp(std::shared_ptr cpu); + void start(const uint port); + void satisfy_client(); + void step(); + + private: + bool attached = false; + + std::shared_ptr cpu; + net::TcpServer server; + std::string receive(); + std::string make_packet(std::string raw); + void acknowledge(); + void send_empty(); + void notify_breakpoint_reached(); + + // Commands + void cmd_attached(); + void cmd_supported(std::string msg); + void cmd_halted(); + void cmd_read_registers(); + void cmd_write_registers(std::string msg); + void cmd_read_register(std::string msg); + void cmd_write_register(std::string msg); + void cmd_read_memory(std::string msg); + void cmd_write_memory(std::string msg); + void cmd_rm_breakpoint(std::string msg); + void cmd_add_breakpoint(std::string msg); + void cmd_detach(); + void cmd_continue(); + + static constexpr std::string ATTACHED_MSG = "$qAttached#8f"; + static constexpr std::string OK_MSG = "+$OK#9a"; + + static constexpr uint MAX_MSG_LEN = 4096; +}; +} diff --git a/src/meson.build b/src/meson.build index e3c3456..b4d3ff9 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,16 +2,14 @@ lib_sources = files( 'bus.cc', ) +if get_option('gdb_debug') + lib_sources += files('gdb_rsp.cc') +endif + subdir('util') subdir('cpu') subdir('io') -lib_cpp_args = [] - -if get_option('disassembler') - lib_cpp_args += '-DDISASSEMBLER' -endif - lib = library( meson.project_name(), lib_sources, diff --git a/src/util/meson.build b/src/util/meson.build index d25f0c9..f9fac94 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -1,3 +1,8 @@ lib_sources += files( - 'log.cc' -) \ No newline at end of file + 'log.cc', + 'tcp_server.cc' +) + +if get_option('gdb_debug') + lib_sources += files('tcp_server.cc') +endif \ No newline at end of file diff --git a/src/util/tcp_server.cc b/src/util/tcp_server.cc new file mode 100644 index 0000000..0091db7 --- /dev/null +++ b/src/util/tcp_server.cc @@ -0,0 +1,80 @@ +#include "tcp_server.hh" + +#include +#include +#include +#include + +namespace net { +TcpServer::TcpServer() + : server_fd(0) + , client_fd(0) {} + +TcpServer::~TcpServer() { + close(server_fd); + close(client_fd); +} + +bool +TcpServer::client_waiting() { + int count = 0; + ioctl(client_fd, FIONREAD, &count); + return static_cast(count); +} + +void +TcpServer::run() { + socklen_t cli_addr_size = sizeof(client_addr); + + client_fd = ::accept( + server_fd, reinterpret_cast(&client_addr), &cli_addr_size); + + if (client_fd == -1) + throw std::runtime_error("accept failed"); +} + +void +TcpServer::start(uint port) { + server_fd = socket(PF_INET, SOCK_STREAM, 0); + if (server_fd == -1) { + throw std::runtime_error("creating socket failed"); + } + + int option = 1; + setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)); + + std::memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = PF_INET; + server_addr.sin_addr.s_addr = htonl(INADDR_ANY); + server_addr.sin_port = htons(port); + + if (::bind(server_fd, + reinterpret_cast(&server_addr), + sizeof(server_addr)) == -1) { + throw std::runtime_error("binding socket failed"); + } + if (::listen(server_fd, 1) == -1) { + throw std::runtime_error("listening failed"); + } +} + +void +TcpServer::send(std::string msg) { + if (::send(client_fd, msg.data(), msg.length(), 0) == -1) { + throw std::runtime_error( + std::format("failed to send message: {}\n", strerror(errno))); + } +} + +std::string +TcpServer::receive(uint length) { + char msg[MAX_PACKET_SIZE]; + ssize_t num_bytes = recv(client_fd, msg, length, 0); + msg[length] = '\0'; + if (num_bytes < 0) { + throw std::runtime_error( + std::format("failed to receive messages: {}\n", strerror(errno))); + } + return std::string(msg); +} +} diff --git a/src/util/tcp_server.hh b/src/util/tcp_server.hh new file mode 100644 index 0000000..8aba099 --- /dev/null +++ b/src/util/tcp_server.hh @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +namespace net { + +class TcpServer { + public: + TcpServer(); + ~TcpServer(); + + void run(); + void start(uint port); + void send(std::string msg); + std::string receive(uint length); + bool client_waiting(); + + private: + static constexpr uint MAX_PACKET_SIZE = 4096; + int server_fd; + int client_fd; + sockaddr_in server_addr; + sockaddr_in client_addr; +}; +} diff --git a/tests/meson.build b/tests/meson.build index a42136d..36d4437 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -9,15 +9,11 @@ tests_sources = files( 'bus.cc' ) +tests_cpp_args = lib_cpp_args + subdir('cpu') subdir('util') -tests_cpp_args = [] - -if get_option('disassembler') - tests_cpp_args += '-DDISASSEMBLER' -endif - catch2 = dependency('catch2', version: '>=3.4.0', static: true) catch2_tests = executable( 'matar_tests',