// SPDX-License-Identifier: MIT /* $info$ tags: glue|gdbserver desc: Provides a gdb interface to the guest state $end_info$ */ #include "CodeLoader.h" #include "LinuxSyscalls/NetStream.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include "LinuxSyscalls/GdbServer.h" namespace FEX { constexpr std::array FlagNames = { "CF", "", "PF", "", "AF", "", "ZF", "SF", "TF", "IF", "DF", "OF", "IOPL", "", "NT", "", "RF", "VM", "AC", "VIF", "VIP", "ID", }; static const std::string_view& GetFlagName(unsigned Flag) { return FlagNames[Flag]; } static std::string_view const GetGRegName(unsigned Reg) { switch (Reg) { case FEXCore::X86State::REG_RAX: return "rax"; case FEXCore::X86State::REG_RBX: return "rbx"; case FEXCore::X86State::REG_RCX: return "rcx"; case FEXCore::X86State::REG_RDX: return "rdx"; case FEXCore::X86State::REG_RSP: return "rsp"; case FEXCore::X86State::REG_RBP: return "rbp"; case FEXCore::X86State::REG_RSI: return "rsi"; case FEXCore::X86State::REG_RDI: return "rdi"; case FEXCore::X86State::REG_R8: return "r8"; case FEXCore::X86State::REG_R9: return "r9"; case FEXCore::X86State::REG_R10: return "r10"; case FEXCore::X86State::REG_R11: return "r11"; case FEXCore::X86State::REG_R12: return "r12"; case FEXCore::X86State::REG_R13: return "r13"; case FEXCore::X86State::REG_R14: return "r14"; case FEXCore::X86State::REG_R15: return "r15"; default: FEX_UNREACHABLE; } } #ifndef _WIN32 void GdbServer::Break(int signal) { std::lock_guard lk(sendMutex); if (!CommsStream) { return; } const fextl::string str = fextl::fmt::format("S{:02x}", signal); SendPacket(*CommsStream, str); } void GdbServer::WaitForThreadWakeup() { // Wait for gdbserver to tell us to wake up ThreadBreakEvent.Wait(); } GdbServer::~GdbServer() { CloseListenSocket(); CoreShuttingDown = true; if (gdbServerThread->joinable()) { gdbServerThread->join(nullptr); } } GdbServer::GdbServer(FEXCore::Context::Context* ctx, FEX::HLE::SignalDelegator* SignalDelegation, FEX::HLE::SyscallHandler* const SyscallHandler) : CTX(ctx) , SyscallHandler {SyscallHandler} { // Pass all signals by default std::fill(PassSignals.begin(), PassSignals.end(), true); ctx->SetExitHandler([this](uint64_t ThreadId, FEXCore::Context::ExitReason ExitReason) { if (ExitReason == FEXCore::Context::ExitReason::EXIT_DEBUG) { this->Break(SIGTRAP); } if (ExitReason == FEXCore::Context::ExitReason::EXIT_SHUTDOWN) { CoreShuttingDown = true; } }); // This is a total hack as there is currently no way to resume once hitting a segfault // But it's semi-useful for debugging. for (uint32_t Signal = 0; Signal <= FEX::HLE::SignalDelegator::MAX_SIGNALS; ++Signal) { SignalDelegation->RegisterHostSignalHandler( Signal, [this](FEXCore::Core::InternalThreadState* Thread, int Signal, void* info, void* ucontext) { if (PassSignals[Signal]) { // Pass signal to the guest return false; } // Let GDB know that we have a signal this->Break(Signal); WaitForThreadWakeup(); return true; }, true); } StartThread(); } static int calculateChecksum(const fextl::string& packet) { unsigned char checksum = 0; for (const char& c : packet) { checksum += c; } return checksum; } static fextl::string hexstring(fextl::istringstream& ss, int delm) { fextl::string ret; char hexString[3] = {0, 0, 0}; while (ss.peek() != delm) { ss.read(hexString, 2); int c = std::strtoul(hexString, nullptr, 16); ret.push_back((char)c); } if (delm != -1) { ss.get(); } return ret; } static fextl::string encodeHex(const unsigned char* data, size_t length) { fextl::ostringstream ss; for (size_t i = 0; i < length; i++) { ss << std::setfill('0') << std::setw(2) << std::hex << int(data[i]); } return ss.str(); } static fextl::string encodeHex(std::string_view str) { return encodeHex(reinterpret_cast(str.data()), str.size()); } static fextl::string getThreadName(uint32_t ThreadID) { const auto ThreadFile = fextl::fmt::format("/proc/{}/task/{}/comm", getpid(), ThreadID); fextl::string ThreadName; FEXCore::FileLoading::LoadFile(ThreadName, ThreadFile); return ThreadName; } // Packet parser // Takes a serial stream and reads a single packet // Un-escapes chars, checks the checksum and request a retransmit if it fails. // Once the checksum is validated, it acknowledges and returns the packet in a string fextl::string GdbServer::ReadPacket(std::iostream& stream) { fextl::string packet {}; // The GDB "Remote Serial Protocal" was originally 7bit clean for use on serial ports. // Binary data is useally hex encoded. However some later extentions just put // raw 8bit binary data. // Packets are in the format // $# // where any $ or # in the packet body are escaped ('}' followed by the char XORed with 0x20) // The checksum is a single unsigned byte sum of the data, hex encoded. int c; while ((c = stream.get()) > 0) { switch (c) { case '$': // start of packet if (packet.size() != 0) { LogMan::Msg::EFmt("Dropping unexpected data: \"{}\"", packet); } // clear any existing data, must have been a mistake. packet = fextl::string(); break; case '}': // escape char { char escaped; stream >> escaped; packet.push_back(escaped ^ 0x20); break; } case '#': // end of packet { char hexString[3] = {0, 0, 0}; stream.read(hexString, 2); int expected_checksum = std::strtoul(hexString, nullptr, 16); if (calculateChecksum(packet) == expected_checksum) { return packet; } else { LogMan::Msg::EFmt("Received Invalid Packet: ${}#{:02x}", packet, expected_checksum); } break; } default: packet.push_back((char)c); break; } } return ""; } static fextl::string escapePacket(const fextl::string& packet) { fextl::ostringstream ss; for (const auto& c : packet) { switch (c) { case '$': case '#': case '*': case '}': { char escaped = c ^ 0x20; ss << '}' << (escaped); break; } default: ss << c; break; } } return ss.str(); } void GdbServer::SendPacket(std::ostream& stream, const fextl::string& packet) { const auto escaped = escapePacket(packet); const auto str = fextl::fmt::format("${}#{:02x}", escaped, calculateChecksum(escaped)); stream << str << std::flush; } void GdbServer::SendACK(std::ostream& stream, bool NACK) { if (NoAckMode) { return; } if (NACK) { stream << "-" << std::flush; } else { stream << "+" << std::flush; } if (SettingNoAckMode) { NoAckMode = true; SettingNoAckMode = false; } } struct X80Float { uint8_t Data[10]; }; struct FEX_PACKED GDBContextDefinition { uint64_t gregs[FEXCore::Core::CPUState::NUM_GPRS]; uint64_t rip; uint32_t eflags; uint32_t cs, ss, ds, es, fs, gs; X80Float mm[FEXCore::Core::CPUState::NUM_MMS]; uint32_t fctrl; uint32_t fstat; uint32_t dummies[6]; uint64_t xmm[FEXCore::Core::CPUState::NUM_XMMS][4]; uint32_t mxcsr; }; fextl::string GdbServer::readRegs() { GDBContextDefinition GDB {}; FEXCore::Core::CPUState state {}; auto Threads = SyscallHandler->TM.GetThreads(); FEXCore::Core::InternalThreadState* CurrentThread {Threads->at(0)}; bool Found = false; for (auto& Thread : *Threads) { if (Thread->ThreadManager.GetTID() != CurrentDebuggingThread) { continue; } memcpy(&state, Thread->CurrentFrame, sizeof(state)); CurrentThread = Thread; Found = true; break; } if (!Found) { // If set to an invalid thread then just get the parent thread ID memcpy(&state, CurrentThread->CurrentFrame, sizeof(state)); } // Encode the GDB context definition memcpy(&GDB.gregs[0], &state.gregs[0], sizeof(GDB.gregs)); memcpy(&GDB.rip, &state.rip, sizeof(GDB.rip)); GDB.eflags = CTX->ReconstructCompactedEFLAGS(CurrentThread, false, nullptr, 0); for (size_t i = 0; i < FEXCore::Core::CPUState::NUM_MMS; ++i) { memcpy(&GDB.mm[i], &state.mm[i], sizeof(GDB.mm)); } // Currently unsupported GDB.fctrl = 0x37F; GDB.fstat = static_cast(state.flags[FEXCore::X86State::X87FLAG_TOP_LOC]) << 11; GDB.fstat |= static_cast(state.flags[FEXCore::X86State::X87FLAG_C0_LOC]) << 8; GDB.fstat |= static_cast(state.flags[FEXCore::X86State::X87FLAG_C1_LOC]) << 9; GDB.fstat |= static_cast(state.flags[FEXCore::X86State::X87FLAG_C2_LOC]) << 10; GDB.fstat |= static_cast(state.flags[FEXCore::X86State::X87FLAG_C3_LOC]) << 14; memcpy(&GDB.xmm[0], &state.xmm.avx.data[0], sizeof(GDB.xmm)); return encodeHex((unsigned char*)&GDB, sizeof(GDBContextDefinition)); } GdbServer::HandledPacketType GdbServer::readReg(const fextl::string& packet) { size_t addr; auto ss = fextl::istringstream(packet); ss.get(); // Drop first letter ss >> std::hex >> addr; FEXCore::Core::CPUState state {}; auto Threads = SyscallHandler->TM.GetThreads(); FEXCore::Core::InternalThreadState* CurrentThread {Threads->at(0)}; bool Found = false; for (auto& Thread : *Threads) { if (Thread->ThreadManager.GetTID() != CurrentDebuggingThread) { continue; } memcpy(&state, Thread->CurrentFrame, sizeof(state)); CurrentThread = Thread; Found = true; break; } if (!Found) { // If set to an invalid thread then just get the parent thread ID memcpy(&state, CurrentThread->CurrentFrame, sizeof(state)); } if (addr >= offsetof(GDBContextDefinition, gregs[0]) && addr < offsetof(GDBContextDefinition, gregs[16])) { return {encodeHex((unsigned char*)(&state.gregs[addr / sizeof(uint64_t)]), sizeof(uint64_t)), HandledPacketType::TYPE_ACK}; } else if (addr == offsetof(GDBContextDefinition, rip)) { return {encodeHex((unsigned char*)(&state.rip), sizeof(uint64_t)), HandledPacketType::TYPE_ACK}; } else if (addr == offsetof(GDBContextDefinition, eflags)) { uint32_t eflags = CTX->ReconstructCompactedEFLAGS(CurrentThread, false, nullptr, 0); return {encodeHex((unsigned char*)(&eflags), sizeof(uint32_t)), HandledPacketType::TYPE_ACK}; } else if (addr >= offsetof(GDBContextDefinition, cs) && addr < offsetof(GDBContextDefinition, mm[0])) { uint32_t Empty {}; return {encodeHex((unsigned char*)(&Empty), sizeof(uint32_t)), HandledPacketType::TYPE_ACK}; } else if (addr >= offsetof(GDBContextDefinition, mm[0]) && addr < offsetof(GDBContextDefinition, mm[8])) { return {encodeHex((unsigned char*)(&state.mm[(addr - offsetof(GDBContextDefinition, mm[0])) / sizeof(X80Float)]), sizeof(X80Float)), HandledPacketType::TYPE_ACK}; } else if (addr == offsetof(GDBContextDefinition, fctrl)) { // XXX: We don't support this yet uint32_t FCW = 0x37F; return {encodeHex((unsigned char*)(&FCW), sizeof(uint32_t)), HandledPacketType::TYPE_ACK}; } else if (addr == offsetof(GDBContextDefinition, fstat)) { uint32_t FSW {}; FSW = static_cast(state.flags[FEXCore::X86State::X87FLAG_TOP_LOC]) << 11; FSW |= static_cast(state.flags[FEXCore::X86State::X87FLAG_C0_LOC]) << 8; FSW |= static_cast(state.flags[FEXCore::X86State::X87FLAG_C1_LOC]) << 9; FSW |= static_cast(state.flags[FEXCore::X86State::X87FLAG_C2_LOC]) << 10; FSW |= static_cast(state.flags[FEXCore::X86State::X87FLAG_C3_LOC]) << 14; return {encodeHex((unsigned char*)(&FSW), sizeof(uint32_t)), HandledPacketType::TYPE_ACK}; } else if (addr >= offsetof(GDBContextDefinition, dummies[0]) && addr < offsetof(GDBContextDefinition, dummies[6])) { uint32_t Empty {}; return {encodeHex((unsigned char*)(&Empty), sizeof(uint32_t)), HandledPacketType::TYPE_ACK}; } else if (addr >= offsetof(GDBContextDefinition, xmm[0][0]) && addr < offsetof(GDBContextDefinition, xmm[16][0])) { const auto XmmIndex = (addr - offsetof(GDBContextDefinition, xmm[0][0])) / FEXCore::Core::CPUState::XMM_AVX_REG_SIZE; const auto* Data = (unsigned char*)&state.xmm.avx.data[XmmIndex][0]; return {encodeHex(Data, FEXCore::Core::CPUState::XMM_AVX_REG_SIZE), HandledPacketType::TYPE_ACK}; } else if (addr == offsetof(GDBContextDefinition, mxcsr)) { uint32_t Empty {}; return {encodeHex((unsigned char*)(&Empty), sizeof(uint32_t)), HandledPacketType::TYPE_ACK}; } LogMan::Msg::EFmt("Unknown GDB register 0x{:x}", addr); return {"E00", HandledPacketType::TYPE_ACK}; } fextl::string buildTargetXML() { fextl::ostringstream xml; xml << "\n"; xml << "\n"; xml << "\n"; xml << "i386:x86-64\n"; xml << "GNU/Linux\n"; xml << "\n"; xml << "\n"; // flags register for (int i = 0; i < 22; i++) { auto name = GetFlagName(i); if (name.empty()) { continue; } xml << "\t\n"; } xml << "\n"; int32_t TargetSize {}; auto reg = [&](std::string_view name, std::string_view type, int size) { TargetSize += size; xml << "" << std::endl; }; // Register ordering. // We want to just memcpy our x86 state to gdb, so we tell it the ordering. // GPRs for (uint32_t i = 0; i < FEXCore::Core::CPUState::NUM_GPRS; i++) { reg(GetGRegName(i), "int64", 64); } reg("rip", "code_ptr", 64); reg("eflags", "fex_eflags", 32); // Fake registers which GDB requires, but we don't support; // We stick them past the end of our cpu state. // non-userspace segment registers reg("cs", "int32", 32); reg("ss", "int32", 32); reg("ds", "int32", 32); reg("es", "int32", 32); reg("fs", "int32", 32); reg("gs", "int32", 32); // x87 stack for (int i = 0; i < 8; i++) { reg(fextl::fmt::format("st{}", i), "i387_ext", 80); } // x87 control reg("fctrl", "int32", 32); reg("fstat", "int32", 32); reg("ftag", "int32", 32); reg("fiseg", "int32", 32); reg("fioff", "int32", 32); reg("foseg", "int32", 32); reg("fooff", "int32", 32); reg("fop", "int32", 32); xml << "\n"; xml << "\n"; xml << R"( )"; // SSE regs for (size_t i = 0; i < FEXCore::Core::CPUState::NUM_XMMS; i++) { reg(fextl::fmt::format("xmm{}", i), "vec128", 128); } reg("mxcsr", "int", 32); xml << "\n"; xml << ""; xml << R"( )"; for (size_t i = 0; i < FEXCore::Core::CPUState::NUM_XMMS; i++) { reg(fmt::format("ymm{}h", i), "vec128", 128); } xml << "\n"; xml << ""; xml << std::flush; return xml.str(); } fextl::string buildOSData() { fextl::ostringstream xml; xml << "\n"; xml << "\n"; xml << ""; // XXX xml << ""; xml << std::flush; return xml.str(); } void GdbServer::buildLibraryMap() { if (!LibraryMapChanged) { // No need to update return; } fextl::ostringstream xml; fextl::string MapsFile; FEXCore::FileLoading::LoadFile(MapsFile, "/proc/self/maps"); fextl::istringstream MapsStream(MapsFile); fextl::string Line; struct FileData { uint64_t Begin; }; fextl::map> SegmentMaps; // 7ff5dd6d2000-7ff5dd6d3000 rw-p 0000a000 103:0b 1881447 /usr/lib/x86_64-linux-gnu/libnss_compat.so.2 const fextl::string& RuntimeExecutable = Filename(); while (std::getline(MapsStream, Line)) { auto ss = fextl::istringstream(Line); fextl::string Tmp; fextl::string Begin; fextl::string Name; std::getline(ss, Begin, '-'); std::getline(ss, Tmp, ' '); // End std::getline(ss, Tmp, ' '); // Perm std::getline(ss, Tmp, ' '); // Inode std::getline(ss, Tmp, ' '); // devid std::getline(ss, Tmp, ' '); // Some garbage std::getline(ss, Name, '\n'); if (strstr(Name.c_str(), "aarch64") != nullptr) { // If the library comes from aarch64, just skip it // Reduces the amount of memory gdb fetches continue; } Name = FEXCore::StringUtils::Trim(Name); struct stat sb {}; if (stat(Name.c_str(), &sb) != -1) { if (S_ISCHR(sb.st_mode)) { // Skip this special file type // Fixes GDB trying to read dri render nodes continue; } } // Skip empty entries, the entry from the process, and also anything like [heap] if (!Name.empty() && Name != RuntimeExecutable && Name[0] != '[') { FileData data { .Begin = std::strtoul(Begin.c_str(), nullptr, 16), }; SegmentMaps[Name].emplace_back(data); } } xml << "\n"; for (auto& Array : SegmentMaps) { xml << "\t\n"; for (auto& Data : Array.second) { xml << "\t\t\n"; } xml << "\t\n"; } xml << "\n"; LibraryMapString = xml.str(); LibraryMapChanged = false; } GdbServer::HandledPacketType GdbServer::handleXfer(const fextl::string& packet) { fextl::string object; fextl::string rw; fextl::string annex; int annex_pid; int offset; int length; // Parse Xfer message { auto ss = fextl::istringstream(packet); fextl::string expectXfer; char expectComma; std::getline(ss, expectXfer, ':'); std::getline(ss, object, ':'); std::getline(ss, rw, ':'); std::getline(ss, annex, ':'); if (annex == "") { annex_pid = getpid(); } else { auto ss_pid = fextl::istringstream(annex); ss_pid >> std::hex >> annex_pid; } ss >> std::hex >> offset; ss.get(expectComma); ss >> std::hex >> length; // Bail on any errors if (ss.fail() || !ss.eof() || expectXfer != "qXfer" || rw != "read" || expectComma != ',') { return {"E00", HandledPacketType::TYPE_ACK}; } } // Lambda to correctly encode any reply auto encode = [&](fextl::string data) -> fextl::string { if (offset == data.size()) { return "l"; } if (offset >= data.size()) { return "E34"; // ERANGE } if ((data.size() - offset) > length) { return "m" + data.substr(offset, length); } return "l" + data.substr(offset); }; if (object == "exec-file") { if (annex_pid == getpid()) { return {encode(Filename()), HandledPacketType::TYPE_ACK}; } return {"E00", HandledPacketType::TYPE_ACK}; } if (object == "features") { if (annex == "target.xml") { return {encode(buildTargetXML()), HandledPacketType::TYPE_ACK}; } return {"E00", HandledPacketType::TYPE_ACK}; } if (object == "threads") { if (offset == 0) { auto Threads = SyscallHandler->TM.GetThreads(); ThreadString.clear(); fextl::ostringstream ss; ss << "\n"; for (auto& Thread : *Threads) { // Thread id is in hex without 0x prefix const auto ThreadName = getThreadName(Thread->ThreadManager.GetTID()); ss << "ThreadManager.GetTID() << "\""; if (!ThreadName.empty()) { ss << " name=\"" << ThreadName << "\""; } ss << "/>\n"; } ss << "\n"; ss << std::flush; ThreadString = ss.str(); } return {encode(ThreadString), HandledPacketType::TYPE_ACK}; } if (object == "osdata") { if (offset == 0) { OSDataString = buildOSData(); } return {encode(OSDataString), HandledPacketType::TYPE_ACK}; } if (object == "libraries") { if (offset == 0) { // Attempt to rebuild when reading from zero buildLibraryMap(); } return {encode(LibraryMapString), HandledPacketType::TYPE_ACK}; } if (object == "auxv") { auto CodeLoader = SyscallHandler->GetCodeLoader(); uint64_t auxv_ptr, auxv_size; CodeLoader->GetAuxv(auxv_ptr, auxv_size); fextl::string data; if (Is64BitMode()) { data.resize(auxv_size); memcpy(data.data(), reinterpret_cast(auxv_ptr), data.size()); } else { // We need to transcode from 32-bit auxv_t to 64-bit data.resize(auxv_size / sizeof(Elf32_auxv_t) * sizeof(Elf64_auxv_t)); size_t NumAuxv = auxv_size / sizeof(Elf32_auxv_t); for (size_t i = 0; i < NumAuxv; ++i) { Elf32_auxv_t* auxv = reinterpret_cast(auxv_ptr + i * sizeof(Elf32_auxv_t)); Elf64_auxv_t tmp; tmp.a_type = auxv->a_type; tmp.a_un.a_val = auxv->a_un.a_val; memcpy(data.data() + i * sizeof(Elf64_auxv_t), &tmp, sizeof(Elf64_auxv_t)); } } return {encode(data), HandledPacketType::TYPE_ACK}; } return {"", HandledPacketType::TYPE_UNKNOWN}; } static size_t CheckMemMapping(uint64_t Address, size_t Size) { uint64_t AddressEnd = Address + Size; fextl::string MapsFile; FEXCore::FileLoading::LoadFile(MapsFile, "/proc/self/maps"); fextl::istringstream MapsStream(MapsFile); fextl::string Line; while (std::getline(MapsStream, Line)) { if (MapsStream.eof()) { break; } uint64_t Begin, End; char r, w, x, p; if (sscanf(Line.c_str(), "%lx-%lx %c%c%c%c", &Begin, &End, &r, &w, &x, &p) == 6) { if (Begin <= Address && End > Address) { ssize_t Overrun {}; if (AddressEnd > End) { Overrun = AddressEnd - End; } return Size - Overrun; } } } return 0; } GdbServer::HandledPacketType GdbServer::handleProgramOffsets() { auto CodeLoader = SyscallHandler->GetCodeLoader(); uint64_t BaseOffset = CodeLoader->GetBaseOffset(); fextl::string str = fextl::fmt::format("Text={:x};Data={:x};Bss={:x}", BaseOffset, BaseOffset, BaseOffset); return {std::move(str), HandledPacketType::TYPE_ACK}; } GdbServer::HandledPacketType GdbServer::handleMemory(const fextl::string& packet) { bool write; size_t addr; size_t length; fextl::string data; auto ss = fextl::istringstream(packet); write = ss.get() == 'M'; ss >> std::hex >> addr; ss.get(); // discard comma ss >> std::hex >> length; if (write) { ss.get(); // discard colon data = hexstring(ss, -1); // grab data until end of file. } // validate packet if (ss.fail() || !ss.eof() || (write && (data.length() != length))) { return {"E00", HandledPacketType::TYPE_ACK}; } length = CheckMemMapping(addr, length); if (length == 0) { return {"E00", HandledPacketType::TYPE_ACK}; } // TODO: check we are in a valid memory range // Also, clamp length void* ptr = reinterpret_cast(addr); if (write) { std::memcpy(ptr, data.data(), data.length()); // TODO: invalidate any code return {"OK", HandledPacketType::TYPE_ACK}; } else { return {encodeHex((unsigned char*)ptr, length), HandledPacketType::TYPE_ACK}; } } GdbServer::HandledPacketType GdbServer::handleQuery(const fextl::string& packet) { const auto match = [&](const char* str) -> bool { return packet.rfind(str, 0) == 0; }; const auto MatchStr = [](const fextl::string& Str, const char* str) -> bool { return Str.rfind(str, 0) == 0; }; const auto split = [](const fextl::string& Str, char deliminator) -> fextl::vector { fextl::vector Elements; fextl::istringstream Input(Str); for (fextl::string line; std::getline(Input, line); Elements.emplace_back(line)) ; return Elements; }; if (match("QNonStop:")) { auto ss = fextl::istringstream(packet); ss.seekg(fextl::string("QNonStop:").size()); ss.get(); // discard colon ss >> NonStopMode; return {"OK", HandledPacketType::TYPE_ACK}; } if (match("qSupported:")) { // eg: qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+;memory-tagging+;xmlRegisters=i386 auto Features = split(packet.substr(strlen("qSupported:")), ';'); // For feature documentation // https://sourceware.org/gdb/current/onlinedocs/gdb/General-Query-Packets.html#qSupported fextl::string SupportedFeatures {}; // Required features SupportedFeatures += "PacketSize=32768;"; SupportedFeatures += "xmlRegisters=i386;"; SupportedFeatures += "qXfer:auxv:read+;"; SupportedFeatures += "qXfer:exec-file:read+;"; SupportedFeatures += "qXfer:features:read+;"; SupportedFeatures += "qXfer:libraries:read+;"; // Don't enable this feature. If enabled then gdb doesn't query for // memory-map updates post-launch. Resulting in the inability to // disassemble code from loaded libraries. // gdbserver running on a true host also doesn't use this feature. // It is likely used for embedded environments where you have a fixed // memory map. // SupportedFeatures += "qXfer:memory-map:read+;"; SupportedFeatures += "qXfer:siginfo:read+;"; SupportedFeatures += "qXfer:siginfo:write+;"; SupportedFeatures += "qXfer:threads:read+;"; SupportedFeatures += "QCatchSignals+;"; SupportedFeatures += "QPassSignals+;"; SupportedFeatures += "QNonStop+;"; SupportedFeatures += "qXfer:osdata:read+;"; SupportedFeatures += "QStartNoAckMode+;"; // TODO: Support breakpoints // SupportedFeatures += "swbreak+;"; // SupportedFeatures += "hwbreak+;"; // SupportedFeatures += "BreakpointCommands+;"; // TODO: If we want to support conditional breakpoints then we need to support single stepping. // SupportedFeatures += "ConditionalBreakpoints+;"; for (auto& Feature : Features) { if (MatchStr(Feature, "swbreak+")) { SupportedFeatures += "swbreak+;"; } if (MatchStr(Feature, "hwbreak+")) { SupportedFeatures += "hwbreak+;"; } if (MatchStr(Feature, "vContSupported+")) { SupportedFeatures += "vContSupported+;"; } // Unsupported: // multiprocess // qRelocInsn // fork-events // vfork-events // exec-events // QThreadEvents // no-resumed // memory-tagging } return {SupportedFeatures, HandledPacketType::TYPE_ACK}; } if (match("qAttached")) { return {"tnotrun:0", HandledPacketType::TYPE_ACK}; // We don't currently support launching executables from gdb. } if (match("qXfer")) { return handleXfer(packet); } if (match("qOffsets")) { return handleProgramOffsets(); } if (match("qTStatus")) { // We don't support trace experiments return {"", HandledPacketType::TYPE_ACK}; } if (match("qfThreadInfo")) { auto Threads = SyscallHandler->TM.GetThreads(); fextl::ostringstream ss; ss << "m"; for (size_t i = 0; i < Threads->size(); ++i) { auto Thread = Threads->at(i); ss << std::hex << Thread->ThreadManager.TID; if (i != (Threads->size() - 1)) { ss << ","; } } return {ss.str(), HandledPacketType::TYPE_ACK}; } if (match("qsThreadInfo")) { return {"l", HandledPacketType::TYPE_ACK}; } if (match("qThreadExtraInfo")) { auto ss = fextl::istringstream(packet); ss.seekg(fextl::string("qThreadExtraInfo").size()); ss.get(); // discard comma uint32_t ThreadID; ss >> std::hex >> ThreadID; auto ThreadName = getThreadName(ThreadID); return {encodeHex((unsigned char*)ThreadName.data(), ThreadName.size()), HandledPacketType::TYPE_ACK}; } if (match("qC")) { // Returns the current Thread ID auto Threads = SyscallHandler->TM.GetThreads(); fextl::ostringstream ss; ss << "m" << std::hex << Threads->at(0)->ThreadManager.TID; return {ss.str(), HandledPacketType::TYPE_ACK}; } if (match("QStartNoAckMode")) { SettingNoAckMode = true; return {"OK", HandledPacketType::TYPE_ACK}; } if (match("qSymbol")) { auto ss = fextl::istringstream(packet); ss.seekg(fextl::string("qSymbol").size()); ss.get(); // discard colon fextl::string Symbol_Val, Symbol_name; std::getline(ss, Symbol_Val, ':'); std::getline(ss, Symbol_name, ':'); if (Symbol_Val.empty() && Symbol_name.empty()) { return {"OK", HandledPacketType::TYPE_ACK}; } else { return {"", HandledPacketType::TYPE_UNKNOWN}; } } if (match("QPassSignals")) { // First set all signals as unpassed std::fill(PassSignals.begin(), PassSignals.end(), false); // eg: QPassSignals:e;10;14;17;1a;1b;1c;21;24;25;2c;4c;97; auto ss = fextl::istringstream(packet); ss.seekg(fextl::string("QPassSignals").size()); ss.get(); // discard colon // We now have a semi-colon deliminated list of signals to pass to the guest process for (fextl::string tmp; std::getline(ss, tmp, ';');) { uint32_t Signal = std::stoi(tmp.c_str(), nullptr, 16); if (Signal < FEX::HLE::SignalDelegator::MAX_SIGNALS) { PassSignals[Signal] = true; } } return {"OK", HandledPacketType::TYPE_ACK}; } // lldb specific queries if (match("qHostInfo")) { // Returns Key:Value pairs separated by ; // eg: // triple:7838365f36342d70632d6c696e75782d676e75; // ptrsize:8; // distribution_id:7562756e7475; // watchpoint_exceptions_received:after; // endian:little; // os_version:6.3.3; // os_build:362e332e332d3036303330332d67656e65726963; // os_kernel:2332303233303531373133333620534d5020505245454d50545f44594e414d494320576564204d61792031372031333a34353a3139205554432032303233; // hostname:7279616e682d545235303030; fextl::string HostFeatures {}; // 64-bit always returned for the host environment. // qProcessInfo will return i386 or not. HostFeatures += fextl::fmt::format("triple:{};", encodeHex("x86_64-pc-linux-gnu")); HostFeatures += "ptrsize:8;"; // Always little-endian. HostFeatures += "endian:little;"; struct utsname buf {}; if (uname(&buf) != -1) { uint32_t Major {}; uint32_t Minor {}; uint32_t Patch {}; // Parse kernel version in the form of `..[Optional Data]` const auto End = buf.release + sizeof(buf.release); auto Results = std::from_chars(buf.release, End, Major, 10); Results = std::from_chars(Results.ptr + 1, End, Minor, 10); Results = std::from_chars(Results.ptr + 1, End, Patch, 10); HostFeatures += fextl::fmt::format("os_version:{}.{}.{};", Major, Minor, Patch); // os_build returns the release untouched. HostFeatures += fextl::fmt::format("os_build:{};", encodeHex(buf.release)); HostFeatures += fextl::fmt::format("os_kernel:{};", encodeHex(buf.version)); HostFeatures += fextl::fmt::format("hostname:{};", encodeHex(buf.nodename)); } // TODO: distribution_id should be fetched with `lsb_release -i` // TODO: watchpoint_exceptions_received is unsupported return {std::move(HostFeatures), HandledPacketType::TYPE_ACK}; } if (match("qGetWorkingDir")) { char Tmp[PATH_MAX]; if (getcwd(Tmp, PATH_MAX)) { return {encodeHex(Tmp), HandledPacketType::TYPE_ACK}; } return {"E00", HandledPacketType::TYPE_ACK}; } return {"", HandledPacketType::TYPE_UNKNOWN}; } GdbServer::HandledPacketType GdbServer::ThreadAction(char action, uint32_t tid) { switch (action) { case 'c': { SyscallHandler->TM.Run(); ThreadBreakEvent.NotifyAll(); SyscallHandler->TM.WaitForThreadsToRun(); return {"", HandledPacketType::TYPE_ONLYACK}; } case 's': { SyscallHandler->TM.Step(); SendPacketPair({"OK", HandledPacketType::TYPE_ACK}); fextl::string str = fextl::fmt::format("T05thread:{:02x};", getpid()); if (LibraryMapChanged) { // If libraries have changed then let gdb know str += "library:1;"; } SendPacketPair({std::move(str), HandledPacketType::TYPE_ACK}); return {"OK", HandledPacketType::TYPE_ACK}; } case 't': // This thread isn't part of the thread pool SyscallHandler->TM.Stop(); return {"OK", HandledPacketType::TYPE_ACK}; default: return {"E00", HandledPacketType::TYPE_ACK}; } } GdbServer::HandledPacketType GdbServer::handleV(const fextl::string& packet) { const auto match = [&](const fextl::string& str) -> std::optional { if (packet.rfind(str, 0) == 0) { auto ss = fextl::istringstream(packet); ss.seekg(str.size()); return ss; } return std::nullopt; }; const auto F = [](int result) -> fextl::string { return fextl::fmt::format("F{:x}", result); }; const auto F_error = []() -> fextl::string { return fextl::fmt::format("F-1,{:x}", errno); }; const auto F_data = [](int result, const fextl::string& data) -> fextl::string { // Binary encoded data is raw appended to the end return fextl::fmt::format("F{:#x};", result) + data; }; std::optional ss; if ((ss = match("vFile:open:"))) { fextl::string filename; int flags; int mode; filename = hexstring(*ss, ','); *ss >> std::hex >> flags; ss->get(); // discard comma *ss >> std::hex >> mode; return {F(open(filename.c_str(), flags, mode)), HandledPacketType::TYPE_ACK}; } if ((ss = match("vFile:setfs:"))) { int pid; *ss >> pid; return {F(pid == 0 ? 0 : -1), HandledPacketType::TYPE_ACK}; // Only support the common filesystem } if ((ss = match("vFile:close:"))) { int fd; *ss >> std::hex >> fd; close(fd); return {F(0), HandledPacketType::TYPE_ACK}; } if ((ss = match("vFile:pread:"))) { int fd, count, offset; *ss >> std::hex >> fd; ss->get(); // discard comma *ss >> std::hex >> count; ss->get(); // discard comma *ss >> std::hex >> offset; fextl::string data(count, '\0'); if (lseek(fd, offset, SEEK_SET) < 0) { return {F_error(), HandledPacketType::TYPE_ACK}; } int ret = read(fd, data.data(), count); if (ret < 0) { return {F_error(), HandledPacketType::TYPE_ACK}; } if (ret == 0) { return {F(0), HandledPacketType::TYPE_ACK}; } data.resize(ret); return {F_data(ret, data), HandledPacketType::TYPE_ACK}; } if ((ss = match("vCont?"))) { return {"vCont;c;t;s;r", HandledPacketType::TYPE_ACK}; // We support continue, step and terminate // FIXME: We also claim to support continue with signal... because it's compulsory } if ((ss = match("vCont;"))) { char action {}; int thread {}; action = ss->get(); if (ss->peek() == ':') { ss->get(); *ss >> std::hex >> thread; } if (ss->fail()) { return {"E00", HandledPacketType::TYPE_ACK}; } return ThreadAction(action, thread); } return {"", HandledPacketType::TYPE_ACK}; } GdbServer::HandledPacketType GdbServer::handleThreadOp(const fextl::string& packet) { const auto match = [&](const char* str) -> bool { return packet.rfind(str, 0) == 0; }; if (match("Hc")) { // Sets thread to this ID for stepping // This is deprecated and vCont should be used instead auto ss = fextl::istringstream(packet); ss.seekg(fextl::string("Hc").size()); ss >> std::hex >> CurrentDebuggingThread; SyscallHandler->TM.Pause(); return {"OK", HandledPacketType::TYPE_ACK}; } if (match("Hg")) { // Sets thread for "other" operations auto ss = fextl::istringstream(packet); ss.seekg(std::string_view("Hg").size()); ss >> std::hex >> CurrentDebuggingThread; // This must return quick otherwise IDA complains SyscallHandler->TM.Pause(); return {"OK", HandledPacketType::TYPE_ACK}; } return {"", HandledPacketType::TYPE_UNKNOWN}; } GdbServer::HandledPacketType GdbServer::handleBreakpoint(const fextl::string& packet) { auto ss = fextl::istringstream(packet); // Don't do anything with set breakpoints yet [[maybe_unused]] bool Set {}; uint64_t Addr; uint64_t Type; Set = ss.get() == 'Z'; ss >> std::hex >> Addr; ss.get(); // discard comma ss >> std::hex >> Type; SyscallHandler->TM.Pause(); return {"OK", HandledPacketType::TYPE_ACK}; } GdbServer::HandledPacketType GdbServer::ProcessPacket(const fextl::string& packet) { switch (packet[0]) { case '?': { // Indicates the reason that the thread has stopped // Behaviour changes if the target is in non-stop mode // Binja doesn't support S response here fextl::string str = fextl::fmt::format("T00thread:{:x};", getpid()); return {std::move(str), HandledPacketType::TYPE_ACK}; } case 'c': // Continue return ThreadAction('c', 0); case 'D': // Detach // Ensure the threads are back in running state on detach SyscallHandler->TM.Run(); SyscallHandler->TM.WaitForThreadsToRun(); return {"OK", HandledPacketType::TYPE_ACK}; case 'g': // We might be running while we try reading // Pause up front SyscallHandler->TM.Pause(); return {readRegs(), HandledPacketType::TYPE_ACK}; case 'p': return readReg(packet); case 'q': case 'Q': return handleQuery(packet); case 'v': return handleV(packet); case 'm': // Memory read case 'M': // Memory write return handleMemory(packet); case 'H': // Sets thread for subsequent operations return handleThreadOp(packet); case '!': // Enable extended mode case 'T': // Is a thread alive? return {"OK", HandledPacketType::TYPE_ACK}; case 's': // Step return ThreadAction('s', 0); case 'z': // Remove breakpoint or watchpoint case 'Z': // Inserts breakpoint or watchpoint return handleBreakpoint(packet); case 'k': // Kill the process SyscallHandler->TM.Stop(); SyscallHandler->TM.WaitForIdle(); // Block until exit return {"", HandledPacketType::TYPE_NONE}; default: return {"", HandledPacketType::TYPE_UNKNOWN}; } } void GdbServer::SendPacketPair(const HandledPacketType& response) { std::lock_guard lk(sendMutex); if (response.TypeResponse == HandledPacketType::TYPE_ACK || response.TypeResponse == HandledPacketType::TYPE_ONLYACK) { SendACK(*CommsStream, false); } else if (response.TypeResponse == HandledPacketType::TYPE_NACK || response.TypeResponse == HandledPacketType::TYPE_ONLYNACK) { SendACK(*CommsStream, true); } if (response.TypeResponse == HandledPacketType::TYPE_UNKNOWN) { SendPacket(*CommsStream, ""); } else if (response.TypeResponse != HandledPacketType::TYPE_ONLYNACK && response.TypeResponse != HandledPacketType::TYPE_ONLYACK && response.TypeResponse != HandledPacketType::TYPE_NONE) { SendPacket(*CommsStream, response.Response); } } GdbServer::WaitForConnectionResult GdbServer::WaitForConnection() { while (!CoreShuttingDown.load()) { struct pollfd PollFD { .fd = ListenSocket, .events = POLLIN | POLLPRI | POLLRDHUP, .revents = 0, }; int Result = ppoll(&PollFD, 1, nullptr, nullptr); if (Result > 0) { if (PollFD.revents & POLLIN) { CommsStream = OpenSocket(); return WaitForConnectionResult::CONNECTION; } else if (PollFD.revents & (POLLHUP | POLLERR | POLLNVAL)) { // Listen socket error or shutting down LogMan::Msg::EFmt("[GdbServer] gdbserver shutting down: {}"); return WaitForConnectionResult::ERROR; } } else if (Result == -1) { LogMan::Msg::EFmt("[GdbServer] poll failure: {}", errno); } } LogMan::Msg::EFmt("[GdbServer] Shutting Down"); return WaitForConnectionResult::ERROR; } void GdbServer::GdbServerLoop() { OpenListenSocket(); if (ListenSocket == -1) { // Couldn't open socket, just exit. return; } while (!CoreShuttingDown.load()) { if (WaitForConnection() == WaitForConnectionResult::ERROR) { break; } HandledPacketType response {}; // Outer server loop. Handles packet start, ACK/NAK and break int c; while ((c = CommsStream->get()) >= 0) { switch (c) { case '$': { auto packet = ReadPacket(*CommsStream); response = ProcessPacket(packet); SendPacketPair(response); if (response.TypeResponse == HandledPacketType::TYPE_UNKNOWN) { LogMan::Msg::DFmt("Unknown packet {}", packet); } break; } case '+': // ACK, do nothing. break; case '-': // NAK, Resend requested { std::lock_guard lk(sendMutex); SendPacket(*CommsStream, response.Response); } break; case '\x03': { // ASCII EOT SyscallHandler->TM.Pause(); fextl::string str = fextl::fmt::format("T02thread:{:02x};", getpid()); if (LibraryMapChanged) { // If libraries have changed then let gdb know str += "library:1;"; } SendPacketPair({std::move(str), HandledPacketType::TYPE_ACK}); break; } default: LogMan::Msg::DFmt("GdbServer: Unexpected byte {} ({:02x})", static_cast(c), c); } } { std::lock_guard lk(sendMutex); CommsStream.reset(); } } CloseListenSocket(); } static void* ThreadHandler(void* Arg) { FEXCore::Threads::SetThreadName("FEX:gdbserver"); auto This = reinterpret_cast(Arg); This->GdbServerLoop(); return nullptr; } void GdbServer::StartThread() { uint64_t OldMask = FEXCore::Threads::SetSignalMask(~0ULL); gdbServerThread = FEXCore::Threads::Thread::Create(ThreadHandler, this); FEXCore::Threads::SetSignalMask(OldMask); } void GdbServer::OpenListenSocket() { const auto GdbUnixPath = fextl::fmt::format("{}/FEX_gdbserver/", FEXServerClient::GetTempFolder()); if (FHU::Filesystem::CreateDirectory(GdbUnixPath) == FHU::Filesystem::CreateDirectoryResult::ERROR) { LogMan::Msg::EFmt("[GdbServer] Couldn't create gdbserver folder {}", GdbUnixPath); return; } GdbUnixSocketPath = fextl::fmt::format("{}{}-gdb", GdbUnixPath, ::getpid()); ListenSocket = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (ListenSocket == -1) { LogMan::Msg::EFmt("[GdbServer] Couldn't open AF_UNIX socket {} {}", errno, strerror(errno)); return; } struct sockaddr_un addr {}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, GdbUnixSocketPath.data(), sizeof(addr.sun_path)); size_t SizeOfAddr = offsetof(sockaddr_un, sun_path) + GdbUnixSocketPath.size(); // Bind the socket to the path int Result {}; for (int attempt = 0; attempt < 2; ++attempt) { Result = bind(ListenSocket, reinterpret_cast(&addr), SizeOfAddr); if (Result == 0) { break; } // This can happen periodically with execve. unlink the path and try again. // The PID is reused but FEX likely started a gdbserver thread for the PID before execve. unlink(GdbUnixSocketPath.c_str()); } if (Result != 0) { LogMan::Msg::EFmt("[GdbServer] Couldn't bind AF_UNIX socket '{}': {} {}\n", addr.sun_path, errno, strerror(errno)); close(ListenSocket); ListenSocket = -1; return; } listen(ListenSocket, 1); LogMan::Msg::IFmt("[GdbServer] Waiting for connection on {}", GdbUnixSocketPath); LogMan::Msg::IFmt("[GdbServer] gdb-multiarch -ex \"target extended-remote {}\"", GdbUnixSocketPath); } void GdbServer::CloseListenSocket() { if (ListenSocket != -1) { close(ListenSocket); ListenSocket = -1; } unlink(GdbUnixSocketPath.c_str()); } fextl::unique_ptr GdbServer::OpenSocket() { // Block until a connection arrives struct sockaddr_storage their_addr {}; socklen_t addr_size {}; int new_fd = accept(ListenSocket, (struct sockaddr*)&their_addr, &addr_size); return fextl::make_unique(new_fd); } #endif } // namespace FEX