#include "gdb_stub.h" #include "interpreter.h" #include "os.hpp" #include "utility/simple_tcp.hpp" #include "framework/logging.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Interpreter { HLE::OS::Process& GDBStub::GetCurrentProcess() const { return GetCurrentThread().GetParentProcess(); } HLE::OS::Thread& GDBStub::GetCurrentThread() const { return *env.active_thread; } std::vector> GDBStub::GetThreadList() const { throw std::runtime_error("Deprecated."); return env.GetThreadList(); } /* * Return the combined process-thread ID for the given OS thread according to * the GDB remote protocol (using the format "p."). * @todo const-ify the "thread" parameter */ static std::string GetThreadIdString(HLE::OS::Thread& thread) { std::stringstream ss; ss << std::hex << std::setfill('0') << "p" << thread.GetParentProcess().GetId() << "." << thread.GetId(); return ss.str(); } static unsigned char GetChecksum(const std::string& buffer) { return boost::accumulate(buffer, (unsigned char)0); } static std::optional HexCharToInt(char ch) { if (ch >= 'a' && ch <= 'f') return ch - 'a' + 0xa; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 0xa; if (ch >= '0' && ch <= '9') return ch - '0'; return {}; } // Optionally returns a number + the number of characters parsed template static std::optional> ReadHexNumber(const RangeType& str) { uint32_t ret = 0; size_t index = 0; // Reject pathological inputs if (boost::empty(str) || !HexCharToInt(*boost::begin(str))) return {}; for (const auto& c : str) { auto digit = HexCharToInt(c); // Stop once we reach a character which can't be converted if (!digit) return std::make_pair(ret, index); // Check if there is room for one more digit, return failure otherwise if (ret & 0xF0000000) return {}; ret <<= 4; ret |= *digit; ++index; } return std::make_pair(ret, (size_t)boost::size(str)); } using CombinedProcessThreadId = std::pair; // Parse a combined process and thread id (format "pPID.TID") from the given string. static std::optional ParseProcessThreadId(const std::string& str) { std::stringstream ss(str, std::ios_base::in); uint32_t process_id; uint32_t thread_id; ss.ignore(1); // Skip "p" (TODO: Assert that it's there!) ss >> std::hex >> process_id; ss.ignore(1); // Skip period (TODO: Assert that it's there!) ss >> std::hex >> thread_id; return {{process_id, thread_id}}; } /** * Return true if the given process id refers to "any" process according to the GDB protocol. * @todo we shouldn't be using the HLE::OS::ProcessId type for this protocol-specific quantity! */ static bool IsAnyProcess(HLE::OS::ProcessId pid) { return pid == 0 || pid == -1; } /** * Return true if the given thread id refers to "any" thread according to the GDB protocol. * @todo we shouldn't be using the HLE::OS::ThreadId type for this protocol-specific quantity! */ static bool IsAnyThread(HLE::OS::ThreadId tid) { return tid == 0 || tid == -1; } /** * Return true if the given id matches the given process id according to the GDB protocol (i.e. considering that "-1" should match any process). * @todo we shouldn't be using the HLE::OS::ProcessId type for the protocol-specific first parameter! */ static bool IsSameProcess(HLE::OS::ProcessId gdb_pid, HLE::OS::ProcessId pid) { if (IsAnyProcess(gdb_pid)) { return true; } else { return gdb_pid == pid; } } /** * Return true if the given id matches the given thread id according to the GDB protocol (i.e. considering that "-1" should match any thread). * @todo we shouldn't be using the HLE::OS::ThreadId type for the protocol-specific first parameter! */ static bool IsSameThread(HLE::OS::ThreadId gdb_tid, HLE::OS::ThreadId tid) { if (IsAnyThread(gdb_tid)) { return true; } else { return gdb_tid == tid; } } /** * GDB expects these register indices: * reg_index = N < 16: rN * reg_index = 25: CPSR * reg_index = 58: FPSCR * * Returns no value if the corresponding register hasn't been implemented, yet. */ static std::optional GetRegisterValue(HLE::OS::Thread& thread, size_t reg_index) { if (reg_index < 16) return thread.GetCPURegisterValue(reg_index); if (reg_index == 25) return thread.GetCPURegisterValue(17); // s0-s31 and fpscr if (reg_index >= 26 && reg_index <= 58) return thread.GetCPURegisterValue(reg_index - 26 + 18); // Unknown register return {}; } // Refer to the GetRegisterValue documentation static void SetRegisterValue(HLE::OS::Thread& thread, size_t reg_index, uint32_t value) { if (reg_index < 16) return thread.SetCPURegisterValue(reg_index, value); } class GDBStubTCPServer : public SimpleTCPServer { GDBStub& stub; std::optional client; boost::asio::streambuf streambuf {}; char token; void OnClientConnected(boost::asio::ip::tcp::socket socket) override { client = std::move(socket); SetupTokenReceivedHandler(); } void SetupTokenReceivedHandler(); void SetupCommandLineReceivedHandler(); void AckReply() { char data = '+'; boost::asio::write(*client, boost::asio::buffer(&data, sizeof(data))); } void NackReply() { char data = '-'; boost::asio::write(*client, boost::asio::buffer(&data, sizeof(data))); } void SendPacket(const std::string& message) { auto encoded_message = fmt::format("${}#{:02x}", message, GetChecksum(message)); boost::asio::write(*client, boost::asio::buffer(encoded_message.data(), encoded_message.size())); stub.logger->info("Stub{} sending message \"{}\" (length {})", stub.GetCPUId(), encoded_message, encoded_message.length()); } void ReportSignal(HLE::OS::ProcessId pid, HLE::OS::ThreadId tid, int signum) { // Format: T05thread:p.; // TODO: In addition to the signalled thread, we can (and should) send // register values along with this message. GDB usually requests // those in reaction to this message anyway, hence sending them // directly reduces network bandwidth (especially when stepping // through the program). std::stringstream ss; ss << "T" << std::hex << std::setfill('0') << std::setw(2) << signum << "thread:p" << std::setw(2) << pid << "." << std::setw(2) << tid << ";"; SendPacket(ss.str()); // Reporting signals seem to reset the thread defined by 'H' operations. // TODO: Not sure if this is correct // TODO: Especially not sure if this is correct when pid and tid are 0 or -1 (i.e. the "any" IDs) for (auto& thread_for_op : stub.thread_for_operation) { thread_for_op.second = std::make_pair(pid, tid); } } void ReportSignal(HLE::OS::Thread& thread, int signum) { // Format: T05thread:p.; // TODO: In addition to the signalled thread, we can (and should) send // register values along with this message. GDB usually requests // those in reaction to this message anyway, hence sending them // directly reduces network bandwidth (especially when stepping // through the program). std::stringstream ss; ss << "T" << std::hex << std::setfill('0') << std::setw(2) << signum << "thread:p" << std::setw(2) << thread.GetParentProcess().GetId() << "." << std::setw(2) << thread.GetId() << ";"; SendPacket(ss.str()); // Reporting signals seem to reset the thread defined by 'H' operations. // TODO: Not sure if this is correct for (auto& thread_for_op : stub.thread_for_operation) { thread_for_op.second = std::make_pair(thread.GetParentProcess().GetId(), thread.GetId()); } } void ReportWatchpointSignal(HLE::OS::Thread& thread, uint32_t address/*, bool read, bool write*/) { auto message = fmt::format("T05thread:p{:02x}.{:02x};watch:{:08x};", thread.GetParentProcess().GetId(), thread.GetId(), address); SendPacket(message); // Reporting signals seem to reset the thread defined by 'H' operations. // TODO: Not sure if this is correct for (auto& thread_for_op : stub.thread_for_operation) { thread_for_op.second = std::make_pair(thread.GetParentProcess().GetId(), thread.GetId()); } } public: void QueuePacket(std::string message) { boost::asio::post(io_context, [this, message=std::move(message)]() { SendPacket(message); }); } void QueueReportSignal(HLE::OS::Thread& thread, int signum) { boost::asio::post(io_context, [&thread, signum, this]() { ReportSignal(thread, signum); }); } GDBStubTCPServer(GDBStub& stub, uint16_t port) : SimpleTCPServer(port), stub(stub) { } }; GDBStub::GDBStub(HLE::OS::OS& os, LogManager& log_manager, unsigned port) : env(os), server(std::make_unique(*this, port)), logger(log_manager.RegisterLogger("GDB")) { logger->set_pattern("[%T.%e] [%n] [%l] %v"); logger->info("Stub{} connected!", GetCPUId()); // Be paused after startup. paused = false; request_pause = false; } GDBStub::~GDBStub() = default; // TODO: Get rid of this easy-to-get-rid-of global! static bool core_running = false; void GDBStub::Continue(bool single_step) { if (single_step) { RequestStep(); } else { RequestContinue(); core_running = true; } } std::shared_ptr GDBStub::GetThreadForOperation(char operation) { switch (operation) { case 'g': case 'm': case 'p': case 'P': case 'X': case 'Z': case 'z': { // NOTE: 'g' is the index used for most non-stepping/non-continuing operations. // TODO: Input verification (the process ID may be invalid!) auto combined_id = thread_for_operation.at('g'); auto& process = IsAnyProcess(combined_id.first) ? GetCurrentProcess() : *env.GetProcessFromId(combined_id.first); auto thread = IsAnyThread(combined_id.second) ? process.threads.back() : process.GetThreadFromId(combined_id.second); return thread; } default: return nullptr; } } std::optional GDBStub::HandlePacket(const std::string& command) { auto MatchesSubcmd = [&command](const char* subcmd) -> bool{ return command.compare(1, std::strlen(subcmd), subcmd) == 0; }; // TODO: Assert that the OS is paused! switch (command[0]) { case '!': { // Enable extended mode (remote server is persistent) // SendPacket("!"); return "OK"; } case '?': { // Sent by GDB upon connecting. This command is fairly ill-documented, // but essentially GDB assumes that when attaching to a process, // execution of a running process was paused. Hence, we reply with // a stop reply. // Reply format: T05thread:p.; // Let's assume for now this is indeed only used upon connecting, so we won't ever be attached to any processes here. assert(attached_processes.empty()); return "W00"; // std::stringstream ss; // ss << "T" << std::hex << std::setfill('0') << std::setw(2) << 5/*SIGTRAP*/ << "thread:" << GetThreadIdString(GetCurrentThread()) << ";"; // SendPacket(ss.str()); // break; } // NOTE: The following commands have been superseded by "vCont" /* case 'c': { // continue until we find a reason to stop Continue(); return true; }*/ /* case 'C': { auto signum = (!!HexCharToInt(command[2])) ? ((*HexCharToInt(command[1]) << 4) | *HexCharToInt(command[2])) : *HexCharToInt(command[1]); // TODO: Actually handle this properly, for now we just do the same as if 'c' had been sent... Continue(); return true; }*/ /* case 's': { // single step Continue(true); ReportSignal(GetCurrentThread(), 5); return true; // Let the other CPU core advance by one instruction, too }*/ case 'g': { if (attached_processes.empty()) { return "E01"; } auto thread = GetThreadForOperation('g'); // Report register values std::stringstream ss; auto AddU32Value = [&ss](uint32_t val) { // NOTE: Register values need to be output in guest byte order! boost::endian::endian_buffer le_val(val); for (auto digit_ptr = le_val.data(); digit_ptr < le_val.data() + sizeof(le_val); ++digit_ptr) { ss << std::hex << std::setfill('0') << std::setw(2) << +*(unsigned char*)digit_ptr; } }; // Send each register to the server individually // (order specified by gdb's internal arm_register_names variable, // see GDB's arm-tdep.c; no, this does not seem to be documented elsewhere) for (unsigned i = 0; i < 16; ++i) { AddU32Value(*GetRegisterValue(*thread, i)); } AddU32Value(*GetRegisterValue(*thread, 25)); for (unsigned i = 26; i < 58; ++i) { AddU32Value(*GetRegisterValue(*thread, i)); } AddU32Value(*GetRegisterValue(*thread, 58)); // FPSCR if (ss.str().length()) return ss.str(); break; } case 'H': { // Set thread for subsequent operations // TODO: This needs more testing! logger->info("Stub{} set thread for subsequent operations: {}", GetCPUId(), command); if (command.length() < 3) return "E00"; auto operation = command[1]; // "c" for continue/step operations, "g" for others auto combined_id = ParseProcessThreadId(command.data() + 2); if (!combined_id) return "E01"; // NOTE: gdbserver returns an error when the debugger is attached to no threads, so we're doing the same... if (attached_processes.empty()) { return "E02"; } else { thread_for_operation[operation] = *combined_id; return "OK"; } break; } case 'm': { auto thread = GetThreadForOperation('m'); if (!thread) return "E00"; // Read "length" bytes from the given address std::stringstream ss(command, std::ios_base::in); uint32_t address; uint32_t length; ss.ignore(1, 'm'); ss >> std::hex >> address; ss.ignore(1, ','); ss >> std::hex >> length; logger->info("Stub{} reading from {:#x} to {:#x}", GetCPUId(), address, address + length); std::stringstream ss_out; try { for (; length;) { // TODO: Optimize this by reading as much data as possible in // 32/16-bit chunks. Make sure to consider GDB's // expected byte order for this command, though. uint8_t val = thread->ReadMemory(address); ss_out << std::hex << std::setfill('0') << std::setw(2) << +val; length--; address++; } return ss_out.str(); } catch (...) { auto message = fmt::format("Stub{} invalid memory access", GetCPUId()); if (!ss_out.str().empty()) { logger->warn(message + " (returning partial region)"); return ss_out.str(); } else { logger->warn(message + " (returning error)"); return "E00"; } } break; } case 'P': { auto thread = GetThreadForOperation('P'); if (!thread) return "E00"; // Set value of register N auto index = (!!HexCharToInt(command[2])) ? ((*HexCharToInt(command[1]) << 4) | *HexCharToInt(command[2])) : *HexCharToInt(command[1]); auto value_pos = command.find('=', 2); if (value_pos == std::string::npos) { return "E00"; } ++value_pos; // TODO: Setting floating-point registers is untested if (index >= 26) { throw 5; } uint32_t value = 0; for (unsigned digit_pair = 0; digit_pair < 4; ++digit_pair) { uint32_t byte = (*HexCharToInt(command[value_pos + digit_pair * 2]) << 4) | *HexCharToInt(command[value_pos + digit_pair * 2 + 1]); value |= byte << (digit_pair * 8); } SetRegisterValue(*thread, index, value); return "OK"; } // Read value of register N case 'p': { auto thread = GetThreadForOperation('p'); if (!thread) return "E00"; auto index = (!!HexCharToInt(command[2])) ? ((*HexCharToInt(command[1]) << 4) | *HexCharToInt(command[2])) : *HexCharToInt(command[1]); std::stringstream ss; // TODO: Reading floating-point registers is untested if (index >= 26) { throw 5; } auto val = GetRegisterValue(*thread, index); if (val) { boost::endian::endian_buffer le_val(*val); for (auto digit_ptr = le_val.data(); digit_ptr < le_val.data() + sizeof(le_val); ++digit_ptr) { ss << std::hex << std::setfill('0') << std::setw(2) << +*(unsigned char*)digit_ptr; } } else { ss << "xxxxxxxx"; } return ss.str(); } // Query request case 'q': { if (MatchesSubcmd("Supported")) { // PacketSize: We don't have any limit on the maximal packet size - hence report a really large value to speed up memory dumping. // multiprocess: Make thread-ids use the syntax "process_id.thread_id" (TODO: Abort if the debugger does not specify "multiprocess+") // qXfer:features:read: The command 'qXfer:features:read:...' may be used to obtain register description // qXfer:memory-map:read: The command 'qXfer:memory-map:read:...' may be used to query memory mappings // exec-events/fork-events: We "support" reporting process forks and execs. We actually abuse this feature to report process launches, since any other method to do so hits unstable code paths in GDB. return "PacketSize=16000;multiprocess+;qXfer:features:read+;qXfer:memory-map:read+;exec-events+;fork-events+"; //SendPacket("PacketSize=200000;multiprocess+"); } else if (MatchesSubcmd("C")) { // TODO: Report current thread ID // Send an empty reply until implemented // TODO: This causes an assertion failure in GDB 11.2 return ""; } else if (MatchesSubcmd("TStatus")) { return "T0;tnotrun:0"; } else if (MatchesSubcmd("Attached:")) { // Return whether gdb created a new process (0) or attached to an existing one (1) // For now, we always have the remote server attach to a FakeDebugProcess // NOTE: This code is disabled for now, since GDB seems to deal just fine with an empty reply // SendPacket("E01"); auto pid_opt = ReadHexNumber(command | boost::adaptors::sliced(10, command.length())); if (!pid_opt) { return "E01"; } // bool attached = (attached_processes.end() != boost::find(attached_processes, pid_opt->first)); bool attached = (attached_processes.end() != ranges::find(attached_processes, pid_opt->first)); // if (!attached) // attached_processes.push_back(*pid_opt); // SendPacket("1"); return attached ? "1" : "0"; // SendPacket(attached ? "1" : "0"); } else if (MatchesSubcmd("Offsets")) { // Contrary to GDB documentation, GDB 7.10 does require a Bss // section offset to be given, and oddly enough the offset must // match the one given for Data. // TODO: Either way, the Data and Bss values just placeholder // constants for now and should eventually be retrieved via a // proper interface instead. return "Text=00100000;Data=00200000;Bss=00200000"; } else if (MatchesSubcmd("fThreadInfo")) { if (attached_processes.empty()) { return "l"; } else { // Build and return a list of thread IDs of the format "mp1.2,p2.1,p2.2,p2.3,p5.1" using boost::accumulate; using boost::adaptors::indirected; using boost::adaptors::transformed; // auto message = accumulate(GetThreadList() | indirected | transformed(GetThreadIdString), auto message = std::string("m"); for (auto& process : attached_processes) { auto& process_ref = *env.GetProcessFromId(process); message += accumulate(process_ref.threads | indirected | transformed(GetThreadIdString), std::string(""), [](const auto& str, const auto& next) { return str + next + ","; }); } // auto message = accumulate(attached_processes | transformed(GetThreadIdString), // std::string("m"), [](const auto& str, const auto& next) { return str + next + ","; }); message.pop_back(); // Drop trailing comma return message; } } else if (MatchesSubcmd("sThreadInfo")) { // Continue list of thread IDs submitted by qfThreadInfo or the previous qsThreadInfo replies. // (in our case, we just end the list since we assume the qfThreadInfo reply contained all threads) return "l"; } else if (MatchesSubcmd("ThreadExtraInfo,p")) { auto combined_id = ParseProcessThreadId(command.data() + std::string("qThreadExtraInfo,").length()); if (!combined_id) return "E00"; HLE::OS::ProcessId process_id = combined_id->first; HLE::OS::ThreadId thread_id = combined_id->second; auto process = env.GetProcessFromId(process_id); auto thread = process->GetThreadFromId(thread_id); auto raw_reply = process->GetName() + "(" + thread->GetName() + ")"; // Convert the thread name to a hex encoding of ASCII data // NOTE: The ostream_iterator is intentionally using "unsigned" // because otherwise the characters would be output as // actual characters rather than as hex values. std::stringstream message_stream; message_stream << std::hex; boost::copy(raw_reply, std::ostream_iterator(message_stream)); return message_stream.str(); } else if (MatchesSubcmd("Xfer:features:read:target.xml")) { // Reply with a list of registers present in our emulated CPU // NOTE: Do *not* include any '#' in this xml! GDB might interpret that as a message terminator instead static const char* xml=R"(l arm )"; return xml; } else if (MatchesSubcmd("Xfer:memory-map:read::")) { // Reply with a list of registers present in our emulated CPU // NOTE: Do *not* include any '#' in this xml! GDB might interpret that as a message terminator instead static const char* xml=R"(l )"; return xml; } else { // Send an empty reply return ""; } break; } case 'T': { // Check if a given thread is still alive auto combined_id = ParseProcessThreadId(command.data() + 1); if (!combined_id) return "E00"; // TODO: Should we recognize the special ids 0 and -1 here? auto process = env.GetProcessFromId(combined_id->first); if (!process) return "E01"; auto thread = env.GetProcessFromId(combined_id->first); if (!process) return "E02"; // TODO: Should check whether "thread" has actually not yet reached the end of its life return "OK"; } case 'v': { if (MatchesSubcmd("Cont?")) { // a "vCont?" packet is sent to query what kind of operations are supported for vCont. return "vCont;c;s;C;S"; } else if (MatchesSubcmd("Cont")) { // TODO: We currently only pay attention to a single continue // action. Other actions may be used e.g. to resume other // threads of the current process, while the current thread // may only be stepping. // Example: "vCont;s:p2.1;c:p2.-1". // TODO: Make sure command[5] == ';' // Same as "c", "s", ... if (command[6] == 'c') { Continue(); } else if (command[6] == 's' || command[6] == 'S') { auto colon_it = boost::find(command, ':'); if (colon_it == command.end()) throw std::runtime_error("Malformed message: No colon found"); auto ptid_opt = ParseProcessThreadId(std::string(colon_it+1, command.end())); assert(ptid_opt); // If the ptid refers to our FakeDebugProcess, allow pausing in any thread. // if (*ptid_opt == std::make_pair(1, 1)) // *ptid_opt = std::make_pair(0, 0); // Step until we enter the given thread. do { // TODO: Breakpoints in other threads may mess this up! if (IsAnyProcess(ptid_opt->first)) RequestStep(std::nullopt); else if (IsAnyThread(ptid_opt->second)) RequestStep({{ ptid_opt->first, std::nullopt }}); else RequestStep({{ ptid_opt->first, ptid_opt->second }}); if (!core_running) break; } while (!IsSameProcess(ptid_opt->first, GetCurrentProcess().GetId()) || !IsSameThread(ptid_opt->second, GetCurrentThread().GetId())); thread_to_pause = std::nullopt; // Send stop packet if and only if we stopped in the thread we were supposed to step in // If this is not the case, the stop trigger was something else and the corresponding code should have taken care of sending a stop signal // NOTE: No idea how nice this place if we step "onto" a breakpoint: Two stop packets might be sent in such case? if (IsSameProcess(ptid_opt->first, GetCurrentProcess().GetId()) && IsSameThread(ptid_opt->second, GetCurrentThread().GetId())) server->QueueReportSignal(GetCurrentThread(), 5); } else if (command[6] == 'C') { Continue(); } else { throw std::runtime_error("TODO: Unexpected character"); } } else if (MatchesSubcmd("Attach;")) { auto pid_opt = ReadHexNumber(command | boost::adaptors::sliced(8, command.length())); if (!pid_opt) { return "E00"; } auto process = env.GetProcessFromId(pid_opt->first); if (!process) { logger->warn("Debugger attempting to attach to nonexistent process {}", pid_opt->first); return "E01"; } auto emu_process = std::dynamic_pointer_cast(process); if (!emu_process) { logger->warn("Debugger attempting to attach to non-emulated process {} ({})", pid_opt->first, process); return "E02"; } logger->info("Debugger attached to process {} ({})", pid_opt->first, emu_process->GetName()); thread_to_pause = {std::make_pair(HLE::OS::ProcessId{pid_opt->first}, std::optional{})}; request_pause = true; for (auto& thread : emu_process->threads) { auto emu_thread = std::static_pointer_cast(thread); emu_thread->context->SetDebuggingEnabled(true); } // Pause the specified process (if the process was just created, tell the main thread to stop waiting for us to attach) (void)TryAccess(attach_info, [&](const AttachInfo& info) { return pid_opt->first == info.pid; }); // TODO: Allow this to timeout or something while (!paused) { } request_pause = false; assert(paused); attached_processes.push_back(pid_opt->first); // TODO: Potential race condition here: The process might have exited since we send the interrupt request server->QueueReportSignal(*process->threads.front(), 5); } else if (MatchesSubcmd("File:setfs")) { // Send empty reply to signal we don't support this! return ""; } else if (MatchesSubcmd("File")) { // Apparently, this is how you report errors for file IO operations return "F-1,16"; // 16 = FILEIO_EINVAL } else if (MatchesSubcmd("MustReplyEmpty")) { // Apparently, this command is sent as a workaround for buggy (and nowadays fixed) gdbserver behavior, // where gdbserver would send "OK" as reply to unknown "v" commands. Instead, we just return an empty reply. return ""; } break; } case 'X': { // Write data to memory (used e.g. for breakpoints) size_t start = 1; auto addr_opt = ReadHexNumber(command | boost::adaptors::sliced(start, command.length())); if (!addr_opt) { return "E00"; } // Skip address and comma start += addr_opt->second + 1; auto length_opt = ReadHexNumber(command | boost::adaptors::sliced(start, command.length())); if (!length_opt) { return "E01"; } // skip length and double colon start += length_opt->second + 1; auto thread = GetThreadForOperation('X'); std::string log_message = "'X' data: "; for (auto c : command.substr(start, std::string::npos)) { log_message += fmt::format("{:02x}", static_cast(c)); } logger->info(log_message); log_message += "'X' writing "; for (uint32_t offset = 0; offset < length_opt->first; ++offset) { // Check for escape character auto value = command[start + offset]; if (value == 0x7d) { // The escaped character is the following byte XOR 0x20 value = command[++start + offset] ^ 0x20; } thread->WriteMemory(addr_opt->first + offset, value); log_message += fmt::format("{:02x}", static_cast(value)); } log_message += fmt::format(" to address {:#x}", addr_opt->first); logger->info(log_message); return "OK"; } case 'z': case 'Z': { // Add breakpoint at the given address // Zn,addr,kind // n = 0: memory breakpoint (replace instruction at given address with SIGTRAP), kind = size of the breakpoint instruction in bytes // n = 1: hardware breakpoint, kind = same as memory breakpoint // n = 2: write watchpoint, kind = number of bytes to watch // n = 3: read watchpoint, kind = same as write watchpoints // n = 4: access watchpoint, kind = same as write watchpoints auto thread = GetThreadForOperation(command[0]); if (!thread) { return "E00"; } size_t start = 1; uint32_t type; uint32_t addr; uint32_t length; // Read type auto hexnum = ReadHexNumber(command | boost::adaptors::sliced(start, command.length())); if (hexnum) { type = hexnum->first; start += hexnum->second; } if (!hexnum || command.length() < start || command[start] != ',') { // Malformed request, error return "E xx"; } // Skip comma ++start; // Read address hexnum = ReadHexNumber(command | boost::adaptors::sliced(start, command.length())); if (hexnum) { addr = hexnum->first; start += hexnum->second; } if (!hexnum || command.length() < start || command[start] != ',') { // Malformed request, error return "E xx"; } // Skip comma ++start; // Read length hexnum = ReadHexNumber(command | boost::adaptors::sliced(start, command.length())); if (hexnum) { length = hexnum->first; } else { // Malformed request, error return "E xx"; } bool add_breakpoint = (command[0] == 'Z'); if (type == 1) { // Hardware breakpoint const auto handler = add_breakpoint ? &HLE::OS::Thread::AddBreakpoint : &HLE::OS::Thread::RemoveBreakpoint; const auto action = add_breakpoint ? "added" : "removed"; for (auto process_thread : thread->GetParentProcess().threads) (process_thread.get()->*handler)(addr); logger->info("Stub{} {} breakpoint {:#x} in thread {}", GetCPUId(), action, addr, GetThreadIdString(*thread)); } else if (type >= 2 && type <= 4) { // Create read or write watch points (or both if type == 4) if (type == 2 || type == 4) { // Write watchpoint const auto handler = add_breakpoint ? &HLE::OS::Thread::AddWriteWatchpoint : &HLE::OS::Thread::RemoveWriteWatchpoint; const auto action = add_breakpoint ? "added" : "removed"; for (auto process_thread : thread->GetParentProcess().threads) (process_thread.get()->*handler)(addr); logger->info("Stub{} {} write watchpoint {:#x} in thread {}", GetCPUId(), action, addr, GetThreadIdString(*thread)); } if (type == 3 || type == 4) { // Read watchpoint const auto handler = add_breakpoint ? &HLE::OS::Thread::AddReadWatchpoint : &HLE::OS::Thread::RemoveReadWatchpoint; const auto action = add_breakpoint ? "added" : "removed"; for (auto process_thread : thread->GetParentProcess().threads) (process_thread.get()->*handler)(addr); logger->info("Stub{} {} read watchpoint {:#x} in thread {}", GetCPUId(), action, addr, GetThreadIdString(*thread)); } } else { // Empty reply = breakpoint type not supported return ""; } return "OK"; } default: logger->info("Stub{} received unknown command: \"{}\"", GetCPUId(), command); return "E xx"; } return {}; } unsigned GDBStub::GetCPUId() { // TODO: Implement. Returning 0 for now. // return ctx.cpu.cp15.CPUId().CPUID; return 0; } void GDBStub::ProcessQueue() { server->RunTCPServer(); } void GDBStubTCPServer::SetupTokenReceivedHandler() { boost::asio::async_read(*client, boost::asio::buffer(&token, 1), [this](boost::system::error_code ec, std::size_t /* bytes_read */) { if (ec) { // TODO return; } if (core_running && token != 3) { throw std::runtime_error("Should never receive anything other than SIGINT while emulator core is running!"); } switch (token) { case '+': stub.logger->info("Stub{} received ACK", stub.GetCPUId()); SetupTokenReceivedHandler(); break; case '-': stub.logger->info("Stub{} received NACK", stub.GetCPUId()); SetupTokenReceivedHandler(); break; case 3: { if (core_running) { stub.logger->info("Stub{} received SIGINT", stub.GetCPUId()); stub.RequestPause(); // TODO:REVISE:Report that all threads have paused by using pid/tid 0 // stub.ReportSignal(1, 1, 2); auto&& current_thread = stub.GetCurrentThread(); stub.logger->info("Reporting that we stopped in thread {}.{}", current_thread.GetParentProcess().GetId(), current_thread.GetId()); ReportSignal(current_thread, 2); // stub.ReportSignal(14, 1, 2); // SIGINT with ID 0 seems to make GDB assume we are in thread 1.1 TODO REVISE // TODO: Not sure if this is correct for (auto& thread : stub.thread_for_operation) { thread.second = std::make_pair(current_thread.GetParentProcess().GetId(), current_thread.GetId()); // thread.second = std::make_pair(14, 1); } core_running = false; } else { // TODO: This may actually happen when stepping by LOTS instructions ("si 500") and then sending SIGINT... throw std::runtime_error("Should only receive SIGINT when running!"); // stub.logger->info("Stub{} received SIGINT", stub.GetCPUId()); // std::stringstream ss; // ss << std::hex << "T" << std::setw(2) << std::setfill('0') << +token; // SendPacket(ss.str()); } SetupTokenReceivedHandler(); break; } case '$': SetupCommandLineReceivedHandler(); break; default: stub.logger->info("Stub{} skipping char '{}' ({:#02x})", stub.GetCPUId(), token, token); SetupTokenReceivedHandler(); break; } }); // TODO: Integrate this as well // try { // ProcessEventQueue(); // } catch (std::runtime_error& e) { // logger->info("Stub{} reporting SIGABRT due to exception in CPU emulator: {}", GetCPUId(), e.what()); // ReportSignal(GetCurrentThread(), 6); // return; // } } void GDBStubTCPServer::SetupCommandLineReceivedHandler() { auto handler = [&](boost::system::error_code ec, std::size_t /*bytes_received*/) { if (!ec) { std::istream stream(&streambuf); std::string command; std::getline(stream, command, '#'); // streambuf.consume(command.length()); stub.logger->info("Stub{} command received: {}", stub.GetCPUId(), command); unsigned char checksum_bytes[2]; int checksum_bytes_read = 0; while (streambuf.size() > 0 && checksum_bytes_read != 2) { checksum_bytes[checksum_bytes_read] = stream.get(); ++checksum_bytes_read; } if (checksum_bytes_read != 2) { boost::asio::read(*client, boost::asio::buffer(&checksum_bytes[checksum_bytes_read], 2 - checksum_bytes_read), ec); if (ec) { // TODO std::abort(); } } unsigned char expected_checksum = (*HexCharToInt(checksum_bytes[0]) << 4); expected_checksum |= *HexCharToInt(checksum_bytes[1]); if (expected_checksum != GetChecksum(command)) { stub.logger->info("Stub{} checksum mismatch! Expected {:#02x}, got {:#02x}", stub.GetCPUId(), expected_checksum, GetChecksum(command)); NackReply(); } else { stub.logger->info("Stub{} sends ACK", stub.GetCPUId()); AckReply(); } auto reply = stub.HandlePacket(command); if (reply) { SendPacket(*reply); } if (reply && command[0] == 's') { // TODO: Transition to stopped state fprintf(stderr, "TODO: Transition to stopped state\n"); std::abort(); } else { SetupTokenReceivedHandler(); } } }; boost::asio::async_read_until(*client, streambuf, '#', handler); } void GDBStub::OnSegfault(uint32_t process_id, uint32_t thread_id) { server->QueueReportSignal(*env.GetProcessFromId(process_id)->GetThreadFromId(thread_id), 6); core_running = false; } void GDBStub::OnBreakpoint(uint32_t process_id, uint32_t thread_id) { logger->info("Stub{} was notified about a breakpoint in thread {:#x}.{:#x}", GetCPUId(), process_id, thread_id); server->QueueReportSignal(*env.GetProcessFromId(process_id)->GetThreadFromId(thread_id), 5); core_running = false; } void GDBStub::OnReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t address) { throw std::runtime_error("Watchpoints are not implemented\n"); logger->info("Stub{} was notified about a read watchpoint in thread {:#x}.{:#x}", GetCPUId(), process_id, thread_id); // GDBStub::ReportWatchpointSignal(*env.GetProcessFromId(process_id)->GetThreadFromId(thread_id), address); core_running = false; } void GDBStub::OnWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t address) { throw std::runtime_error("Watchpoints are not implemented\n"); logger->info("Stub{} was notified about a write watchpoint in thread {:#x}.{:#x}", GetCPUId(), process_id, thread_id); // GDBStub::ReportWatchpointSignal(*env.GetProcessFromId(process_id)->GetThreadFromId(thread_id), address); core_running = false; } void GDBStub::OnProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) { logger->info("Stub{} was notified about a launched process: .{:#x}", GetCPUId(), launched_process_id); // Pause the emulator: We report launched processes as forks of the // current process, which GDB assumes to pause the current process. // Hence, we request the emulator core to pause here until the debugger // resumes execution. RequestPause(); // Format: T05thread:p.;fork:p.1;) std::stringstream ss; auto thread = env.GetProcessFromId(process_id)->GetThreadFromId(thread_id); auto launched_thread = env.GetProcessFromId(launched_process_id)->threads.front(); ss << "T" << std::hex << std::setfill('0') << std::setw(2) << 5 /*SIGTRAP*/ << "thread:" << GetThreadIdString(*thread) << ";fork:" << GetThreadIdString(*launched_thread) << ";"; server->QueuePacket(ss.str()); // Spawning processes seem to reset the thread defined by 'H' operations. // TODO: Not sure if this is correct for (auto& thread_for_op : thread_for_operation) { thread->GetLogger()->info("BREAKPOINTREMOVEME resetting thread for operations to {}", GetThreadIdString(*launched_thread)); thread_for_op.second = std::make_pair(launched_process_id, launched_thread->GetId()); } core_running = false; } void GDBStub::OfferAttach(std::weak_ptr attach_info) { this->attach_info = attach_info; } } // namespace