mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-07 01:00:17 +02:00
1524 lines
48 KiB
C++
1524 lines
48 KiB
C++
// SPDX-License-Identifier: MIT
|
|
/*
|
|
$info$
|
|
tags: glue|gdbserver
|
|
desc: Provides a gdb interface to the guest state
|
|
$end_info$
|
|
*/
|
|
|
|
#include "CodeLoader.h"
|
|
#include "GdbServer/Info.h"
|
|
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <iomanip>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string_view>
|
|
|
|
#include <Common/FEXServerClient.h>
|
|
#include <FEXCore/Config/Config.h>
|
|
#include <FEXCore/Core/Context.h>
|
|
#include <FEXCore/Core/CoreState.h>
|
|
#include <FEXCore/Core/SignalDelegator.h>
|
|
#include <FEXCore/Core/X86Enums.h>
|
|
#include <FEXCore/Debug/InternalThreadState.h>
|
|
#include <FEXCore/HLE/SyscallHandler.h>
|
|
#include <FEXCore/Utils/CompilerDefs.h>
|
|
#include <FEXCore/Utils/FileLoading.h>
|
|
#include <FEXCore/Utils/LogManager.h>
|
|
#include <FEXCore/Utils/StringUtils.h>
|
|
#include <FEXCore/Utils/Threads.h>
|
|
#include <FEXCore/fextl/fmt.h>
|
|
#include <FEXCore/fextl/map.h>
|
|
#include <FEXCore/fextl/sstream.h>
|
|
#include <FEXCore/fextl/string.h>
|
|
#include <FEXCore/fextl/vector.h>
|
|
#include <FEXHeaderUtils/Filesystem.h>
|
|
#include <FEXHeaderUtils/Syscalls.h>
|
|
|
|
#include <atomic>
|
|
#include <cstring>
|
|
#ifndef _WIN32
|
|
#include <elf.h>
|
|
#include <netdb.h>
|
|
#include <sys/socket.h>
|
|
#endif
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stddef.h>
|
|
#include <string_view>
|
|
#include <sys/stat.h>
|
|
#include <sys/un.h>
|
|
#include <sys/utsname.h>
|
|
#include <unistd.h>
|
|
#include <utility>
|
|
|
|
#include "LinuxSyscalls/GdbServer.h"
|
|
#include "LinuxSyscalls/Syscalls.h"
|
|
#include "LinuxSyscalls/ThreadManager.h"
|
|
|
|
namespace FEX {
|
|
|
|
#ifndef _WIN32
|
|
void GdbServer::Break(FEXCore::Core::InternalThreadState* Thread, int signal) {
|
|
std::lock_guard lk(sendMutex);
|
|
if (!CommsSocket) {
|
|
return;
|
|
}
|
|
|
|
auto ThreadObject = FEX::HLE::ThreadManager::GetStateObjectFromFEXCoreThread(Thread);
|
|
// Current debugging thread switches to the thread that is breaking.
|
|
CurrentDebuggingThread = ThreadObject->ThreadInfo.TID.load();
|
|
|
|
const auto str = fextl::fmt::format("T{:02x}thread:{:x};", signal, CurrentDebuggingThread);
|
|
SendPacket(*CommsSocket, str);
|
|
}
|
|
|
|
void GdbServer::WaitForThreadWakeup() {
|
|
// Wait for gdbserver to tell us to wake up
|
|
ThreadBreakEvent.Wait();
|
|
}
|
|
|
|
GdbServer::~GdbServer() {
|
|
CloseListenSocket();
|
|
|
|
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}
|
|
, SignalDelegation {SignalDelegation} {
|
|
// Pass all signals by default
|
|
std::fill(PassSignals.begin(), PassSignals.end(), 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;
|
|
}
|
|
|
|
auto ThreadObject = FEX::HLE::ThreadManager::GetStateObjectFromFEXCoreThread(Thread);
|
|
ThreadObject->GdbInfo = {};
|
|
ThreadObject->GdbInfo->Signal = Signal;
|
|
|
|
this->SignalDelegation->SpillSRA(Thread, ucontext, Thread->CurrentFrame->InSyscallInfo);
|
|
|
|
// Let GDB know that we have a signal
|
|
this->Break(Thread, Signal);
|
|
|
|
WaitForThreadWakeup();
|
|
ThreadObject->GdbInfo.reset();
|
|
|
|
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 appendHex(const char* data, size_t length) {
|
|
return fextl::fmt::format("{:02x}", fmt::join(data, data + length, ""));
|
|
}
|
|
|
|
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<const unsigned char*>(str.data()), str.size());
|
|
}
|
|
|
|
// 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(const std::span<std::byte>& RawMessage) {
|
|
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
|
|
// $<data>#<checksum>
|
|
// 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.
|
|
|
|
if (RawMessage.empty() || (char)RawMessage[0] != '$') {
|
|
ERROR_AND_DIE_FMT("Expected GDB protocol messages to start with '$'");
|
|
}
|
|
|
|
for (auto It = std::next(RawMessage.begin()); It != RawMessage.end(); ++It) {
|
|
char c = (char)*It;
|
|
switch (c) {
|
|
case '$': // start of packet
|
|
ERROR_AND_DIE_FMT("Unescaped control character");
|
|
break;
|
|
|
|
case '}': // escape char
|
|
{
|
|
if (std::next(It) == RawMessage.end()) {
|
|
ERROR_AND_DIE_FMT("Missing character after escape indicator");
|
|
}
|
|
char escaped = (char)*++It;
|
|
packet.push_back(escaped ^ 0x20);
|
|
break;
|
|
}
|
|
|
|
case '#': // end of packet
|
|
{
|
|
if (RawMessage.end() - It <= 2) {
|
|
ERROR_AND_DIE_FMT("Missing checksum at end of packet");
|
|
}
|
|
|
|
char hexString[3] = {0, 0, 0};
|
|
hexString[0] = (char)*++It;
|
|
hexString[1] = (char)*++It;
|
|
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(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(fasio::tcp_socket& Socket, const fextl::string& packet) {
|
|
const auto escaped = escapePacket(packet);
|
|
auto str = fextl::fmt::format("${}#{:02x}", escaped, calculateChecksum(escaped));
|
|
|
|
fasio::error ec;
|
|
write(Socket, fasio::mutable_buffer {std::as_writable_bytes(std::span {str})}, ec);
|
|
}
|
|
|
|
void GdbServer::SendACK(fasio::tcp_socket& Socket, bool NACK) {
|
|
if (NoAckMode) {
|
|
return;
|
|
}
|
|
|
|
if (NACK) {
|
|
std::string_view message = "-";
|
|
send(Socket.FD, message.data(), message.size(), 0);
|
|
} else {
|
|
std::string_view message = "+";
|
|
send(Socket.FD, message.data(), message.size(), 0);
|
|
}
|
|
|
|
if (SettingNoAckMode) {
|
|
NoAckMode = true;
|
|
SettingNoAckMode = false;
|
|
}
|
|
}
|
|
|
|
const FEX::HLE::ThreadStateObject* GdbServer::FindThreadByTID(uint32_t TID) {
|
|
auto Threads = SyscallHandler->TM.GetThreads();
|
|
|
|
for (auto& Thread : *Threads) {
|
|
if (Thread->ThreadInfo.TID != TID) {
|
|
continue;
|
|
}
|
|
|
|
return Thread;
|
|
}
|
|
|
|
// Return parent thread if TID isn't found.
|
|
return Threads->at(0);
|
|
}
|
|
|
|
GdbServer::GDBContextDefinition GdbServer::GenerateContextDefinition(const FEX::HLE::ThreadStateObject* ThreadObject) {
|
|
GDBContextDefinition GDB {};
|
|
FEXCore::Core::CPUState state {};
|
|
|
|
// Copy the thread state.
|
|
memcpy(&state, ThreadObject->Thread->CurrentFrame, sizeof(state));
|
|
|
|
// Encode the GDB context definition
|
|
memcpy(&GDB.gregs[0], &state.gregs[0], sizeof(GDB.gregs));
|
|
GDB.rip = ThreadObject->Thread->CurrentFrame->State.rip;
|
|
GDB.eflags = CTX->ReconstructCompactedEFLAGS(ThreadObject->Thread, 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[i]));
|
|
}
|
|
|
|
GDB.fctrl = state.FCW;
|
|
|
|
GDB.fstat = static_cast<uint32_t>(state.flags[FEXCore::X86State::X87FLAG_TOP_LOC]) << 11;
|
|
GDB.fstat |= static_cast<uint32_t>(state.flags[FEXCore::X86State::X87FLAG_C0_LOC]) << 8;
|
|
GDB.fstat |= static_cast<uint32_t>(state.flags[FEXCore::X86State::X87FLAG_C1_LOC]) << 9;
|
|
GDB.fstat |= static_cast<uint32_t>(state.flags[FEXCore::X86State::X87FLAG_C2_LOC]) << 10;
|
|
GDB.fstat |= static_cast<uint32_t>(state.flags[FEXCore::X86State::X87FLAG_C3_LOC]) << 14;
|
|
GDB.fstat |= static_cast<uint32_t>(state.flags[FEXCore::X86State::X87FLAG_IE_LOC]);
|
|
|
|
__uint128_t XMM_Low[FEXCore::Core::CPUState::NUM_XMMS];
|
|
__uint128_t YMM_High[FEXCore::Core::CPUState::NUM_XMMS];
|
|
|
|
CTX->ReconstructXMMRegisters(ThreadObject->Thread, XMM_Low, YMM_High);
|
|
for (size_t i = 0; i < FEXCore::Core::CPUState::NUM_XMMS; ++i) {
|
|
memcpy(&GDB.xmm[i][0], &XMM_Low[i], sizeof(__uint128_t));
|
|
memcpy(&GDB.xmm[i][2], &YMM_High[i], sizeof(__uint128_t));
|
|
}
|
|
|
|
return GDB;
|
|
}
|
|
|
|
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<fextl::string, fextl::vector<FileData>> 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 << "<library-list>\n";
|
|
for (auto& Array : SegmentMaps) {
|
|
xml << "\t<library name=\"" << Array.first << "\">\n";
|
|
for (auto& Data : Array.second) {
|
|
xml << "\t\t<segment address=\"0x" << std::hex << Data.Begin << "\"/>\n";
|
|
}
|
|
xml << "\t</library>\n";
|
|
}
|
|
|
|
xml << "</library-list>\n";
|
|
|
|
LibraryMapString = xml.str();
|
|
LibraryMapChanged = false;
|
|
}
|
|
|
|
// Binary data transfer handlers
|
|
|
|
GdbServer::HandledPacketType GdbServer::XferCommandExecFile(const fextl::string& annex, int offset, int length) {
|
|
int annex_pid;
|
|
if (annex.empty()) {
|
|
annex_pid = getpid();
|
|
} else {
|
|
auto ss_pid = fextl::istringstream(annex);
|
|
ss_pid >> std::hex >> annex_pid;
|
|
}
|
|
|
|
if (annex_pid == getpid()) {
|
|
return {EncodeXferString(Filename(), offset, length), HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
return {"E00", HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::XferCommandFeatures(const fextl::string& annex, int offset, int length) {
|
|
if (annex == "target.xml") {
|
|
return {EncodeXferString(GDB::Info::BuildTargetXML(Is64BitMode()), offset, length), HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
return {"E00", HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::XferCommandThreads(const fextl::string& annex, int offset, int length) {
|
|
if (offset == 0) {
|
|
auto Threads = SyscallHandler->TM.GetThreads();
|
|
|
|
ThreadString.clear();
|
|
fextl::ostringstream ss;
|
|
ss << "<threads>\n";
|
|
for (auto& Thread : *Threads) {
|
|
// Thread id is in hex without 0x prefix
|
|
const auto ThreadName = GDB::Info::GetThreadName(::getpid(), Thread->ThreadInfo.TID);
|
|
ss << "<thread id=\"" << std::hex << Thread->ThreadInfo.TID << "\"";
|
|
if (!ThreadName.empty()) {
|
|
ss << " name=\"" << ThreadName << "\"";
|
|
}
|
|
ss << "/>\n";
|
|
}
|
|
|
|
ss << "</threads>\n";
|
|
ss << std::flush;
|
|
ThreadString = ss.str();
|
|
}
|
|
|
|
return {EncodeXferString(ThreadString, offset, length), HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::XferCommandOSData(const fextl::string& annex, int offset, int length) {
|
|
if (offset == 0) {
|
|
OSDataString = GDB::Info::BuildOSXML();
|
|
}
|
|
return {EncodeXferString(OSDataString, offset, length), HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::XferCommandLibraries(const fextl::string& annex, int offset, int length) {
|
|
if (offset == 0) {
|
|
// Attempt to rebuild when reading from zero
|
|
buildLibraryMap();
|
|
}
|
|
return {EncodeXferString(LibraryMapString, offset, length), HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::XferCommandAuxv(const fextl::string& annex, int offset, int length) {
|
|
const auto* CodeLoader = SyscallHandler->GetCodeLoader();
|
|
const auto [auxv_ptr, auxv_size] = CodeLoader->GetAuxv();
|
|
|
|
fextl::string data;
|
|
if (Is64BitMode()) {
|
|
data.resize(auxv_size);
|
|
memcpy(data.data(), reinterpret_cast<void*>(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<Elf32_auxv_t*>(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 {EncodeXferString(data, offset, length), HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::handleXfer(const fextl::string& packet) {
|
|
fextl::string object;
|
|
fextl::string rw;
|
|
fextl::string annex;
|
|
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, ':');
|
|
|
|
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};
|
|
}
|
|
}
|
|
|
|
// Specific object documentation: https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html#qXfer-read
|
|
if (object == "auxv") {
|
|
return XferCommandAuxv(annex, offset, length);
|
|
}
|
|
|
|
// btrace
|
|
// btrace-conf
|
|
|
|
if (object == "exec-file") {
|
|
return XferCommandExecFile(annex, offset, length);
|
|
}
|
|
|
|
if (object == "features") {
|
|
return XferCommandFeatures(annex, offset, length);
|
|
}
|
|
|
|
if (object == "libraries") {
|
|
return XferCommandLibraries(annex, offset, length);
|
|
}
|
|
|
|
// libraries-svr4
|
|
// memory-map
|
|
// sdata
|
|
// siginfo:read
|
|
// siginfo:write
|
|
|
|
if (object == "threads") {
|
|
return XferCommandThreads(annex, offset, length);
|
|
}
|
|
|
|
// traceframe-info
|
|
// uib
|
|
// fdpic
|
|
|
|
if (object == "osdata") {
|
|
return XferCommandOSData(annex, offset, length);
|
|
}
|
|
|
|
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::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};
|
|
}
|
|
}
|
|
|
|
// Command handlers
|
|
GdbServer::HandledPacketType GdbServer::CommandEnableExtendedMode(const fextl::string& packet) {
|
|
return {"OK", HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandQueryHalted(const fextl::string& packet) {
|
|
// 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};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandContinue(const fextl::string& packet) {
|
|
// Continue
|
|
return ThreadAction('c', 0);
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandDetach(const fextl::string& packet) {
|
|
// Detach
|
|
// Ensure the threads are back in running state on detach
|
|
SyscallHandler->TM.Run();
|
|
SyscallHandler->TM.WaitForThreadsToRun();
|
|
return {"OK", HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandReadRegisters(const fextl::string& packet) {
|
|
// We might be running while we try reading
|
|
// Pause up front
|
|
SyscallHandler->TM.Pause();
|
|
const FEX::HLE::ThreadStateObject* CurrentThread = FindThreadByTID(CurrentDebuggingThread);
|
|
const size_t NumGPR = Is64BitMode() ? FEXCore::Core::CPUState::NUM_GPRS : FEXCore::Core::CPUState::NUM_GPRS / 2;
|
|
const size_t GPRSize = Is64BitMode() ? sizeof(uint64_t) : sizeof(uint32_t);
|
|
const size_t NumXMM = Is64BitMode() ? FEXCore::Core::CPUState::NUM_XMMS : FEXCore::Core::CPUState::NUM_XMMS / 2;
|
|
const size_t XMMSize = Is64BitMode() ? sizeof(__uint128_t) * 2 : sizeof(__uint128_t);
|
|
fextl::string str;
|
|
auto GDB = GenerateContextDefinition(CurrentThread);
|
|
for (size_t i = 0; i < NumGPR; ++i) {
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.gregs[i]), GPRSize);
|
|
}
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.rip), GPRSize);
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.eflags), sizeof(uint32_t));
|
|
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.cs), sizeof(uint32_t));
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.ss), sizeof(uint32_t));
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.ds), sizeof(uint32_t));
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.es), sizeof(uint32_t));
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.fs), sizeof(uint32_t));
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.gs), sizeof(uint32_t));
|
|
for (auto& mm : GDB.mm) {
|
|
str += appendHex(reinterpret_cast<const char*>(&mm), sizeof(X80Float));
|
|
}
|
|
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.fctrl), sizeof(uint32_t));
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.fstat), sizeof(uint32_t));
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.dummies), sizeof(GDB.dummies));
|
|
|
|
for (size_t i = 0; i < NumXMM; ++i) {
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.xmm[i]), XMMSize);
|
|
}
|
|
|
|
str += appendHex(reinterpret_cast<const char*>(&GDB.mxcsr), sizeof(uint32_t));
|
|
|
|
return {std::move(str), HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandThreadOp(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(strlen("Hc"));
|
|
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(strlen("Hg"));
|
|
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::CommandKill(const fextl::string& packet) {
|
|
SyscallHandler->TM.Stop();
|
|
SyscallHandler->TM.WaitForIdle(); // Block until exit
|
|
return {"", HandledPacketType::TYPE_NONE};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandMemory(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<void*>(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::CommandReadReg(const fextl::string& packet) {
|
|
size_t addr;
|
|
auto ss = fextl::istringstream(packet);
|
|
ss.get(); // Drop first letter
|
|
ss >> std::hex >> addr;
|
|
|
|
const FEX::HLE::ThreadStateObject* CurrentThread = FindThreadByTID(CurrentDebuggingThread);
|
|
auto GDB = GenerateContextDefinition(CurrentThread);
|
|
|
|
if (addr >= offsetof(GDBContextDefinition, gregs[0]) && addr < offsetof(GDBContextDefinition, gregs[16])) {
|
|
return {encodeHex((unsigned char*)(&GDB.gregs[addr / sizeof(uint64_t)]), sizeof(uint64_t)), HandledPacketType::TYPE_ACK};
|
|
} else if (addr == offsetof(GDBContextDefinition, rip)) {
|
|
return {encodeHex((unsigned char*)(&GDB.rip), sizeof(uint64_t)), HandledPacketType::TYPE_ACK};
|
|
} else if (addr == offsetof(GDBContextDefinition, eflags)) {
|
|
return {encodeHex((unsigned char*)(&GDB.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*)(&GDB.mm[(addr - offsetof(GDBContextDefinition, mm[0])) / sizeof(X80Float)]), sizeof(X80Float)),
|
|
HandledPacketType::TYPE_ACK};
|
|
} else if (addr == offsetof(GDBContextDefinition, fctrl)) {
|
|
return {encodeHex((unsigned char*)(&GDB.fctrl), sizeof(uint32_t)), HandledPacketType::TYPE_ACK};
|
|
} else if (addr == offsetof(GDBContextDefinition, fstat)) {
|
|
return {encodeHex((unsigned char*)(&GDB.fstat), sizeof(uint32_t)), HandledPacketType::TYPE_ACK};
|
|
} else if (addr >= offsetof(GDBContextDefinition, dummies[0]) && addr < offsetof(GDBContextDefinition, dummies[6])) {
|
|
return {encodeHex((unsigned char*)(&GDB.dummies[0]), 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;
|
|
return {encodeHex(reinterpret_cast<const uint8_t*>(&GDB.xmm[XmmIndex]), FEXCore::Core::CPUState::XMM_AVX_REG_SIZE), HandledPacketType::TYPE_ACK};
|
|
} else if (addr == offsetof(GDBContextDefinition, mxcsr)) {
|
|
return {encodeHex((unsigned char*)(&GDB.mxcsr), sizeof(uint32_t)), HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
LogMan::Msg::EFmt("Unknown GDB register 0x{:x}", addr);
|
|
return {"E00", HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandQuery(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::string> {
|
|
fextl::vector<fextl::string> 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 {std::move(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->ThreadInfo.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 = GDB::Info::GetThreadName(::getpid(), 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)->ThreadInfo.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 `<Major>.<Minor>.<Patch>[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::CommandSingleStep(const fextl::string& packet) {
|
|
return ThreadAction('s', 0);
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandQueryThreadAlive(const fextl::string& packet) {
|
|
return {"OK", HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::HandlevFile(const fextl::string& packet) {
|
|
const auto match = [&](const fextl::string& str) -> std::optional<fextl::istringstream> {
|
|
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<fextl::istringstream> 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};
|
|
}
|
|
|
|
return {"", HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::HandlevCont(const fextl::string& packet) {
|
|
const auto match = [&](const fextl::string& str) -> std::optional<fextl::istringstream> {
|
|
if (packet.rfind(str, 0) == 0) {
|
|
auto ss = fextl::istringstream(packet);
|
|
ss.seekg(str.size());
|
|
return ss;
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
|
|
std::optional<fextl::istringstream> ss;
|
|
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::CommandMultiLetterV(const fextl::string& packet) {
|
|
// TODO: vAttach
|
|
if (packet.starts_with("vCont")) {
|
|
return HandlevCont(packet);
|
|
}
|
|
|
|
// TODO: vCtrlC
|
|
|
|
if (packet.starts_with("vFile")) {
|
|
return HandlevFile(packet);
|
|
}
|
|
|
|
if (packet.starts_with("vKill")) {
|
|
FHU::Syscalls::tgkill(::getpid(), ::getpid(), SIGKILL);
|
|
}
|
|
|
|
// TODO: vRun
|
|
// TODO: vStopped
|
|
|
|
return {"", HandledPacketType::TYPE_ACK};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::CommandBreakpoint(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::CommandUnknown(const fextl::string& packet) {
|
|
return {"", HandledPacketType::TYPE_UNKNOWN};
|
|
}
|
|
|
|
GdbServer::HandledPacketType GdbServer::ProcessPacket(const fextl::string& packet) {
|
|
// Packet commands list: https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#Packets
|
|
|
|
switch (packet[0]) {
|
|
// Command: $!
|
|
// - Desc: Enable extended mode
|
|
// - Args: <None>
|
|
case '!': return CommandEnableExtendedMode(packet);
|
|
// Command: $?
|
|
// - Desc: Sent on connection first established to query the reason the target halted.
|
|
case '?': return CommandQueryHalted(packet);
|
|
// Command: $A
|
|
// - Desc: Initialized argv[] array passed in to the program.
|
|
// - Args: arglen,argnum,arg,...
|
|
case 'A': return CommandUnknown(packet);
|
|
// Command: $b
|
|
// - Desc: Change the serial line speed to baud
|
|
// - Args: baud
|
|
// - Deprecated: Behaviour isn't well-defined.
|
|
case 'b': return CommandUnknown(packet);
|
|
// Command: $B
|
|
// - Desc: Set or clear a breadpoint at address
|
|
// - Args: addr,mode
|
|
// - Deprecated: Use $Z and $z instead.
|
|
case 'B': return CommandUnknown(packet);
|
|
// Command: $c
|
|
// - Desc: Continue execution of process
|
|
// - Args: [addr]
|
|
// - Deprecated: See $vCont for multi-threaded support.
|
|
case 'c': return CommandContinue(packet);
|
|
// Command: $C
|
|
// - Desc: Continue execution of process with signal
|
|
// - Args: sig[;addr]
|
|
// - Deprecated: See $vCont for multi-threaded support.
|
|
case 'C': return CommandUnknown(packet);
|
|
// Command: $d
|
|
// - Desc: Toggle debug flag
|
|
// - Args: <None>
|
|
// - Deprecated: Use $q or $Q instead.
|
|
case 'd': return CommandUnknown(packet);
|
|
// Command: $D
|
|
// - Desc: Detach GDB from the remote system
|
|
// - Args: [;pid]
|
|
case 'D': return CommandDetach(packet);
|
|
// Command: $F
|
|
// - Desc: A reply from GDB to the `F` packet sent by the target. Part of the File-I/O protocol.
|
|
// - Args: RC,EE,CF;XX
|
|
case 'F': return CommandUnknown(packet);
|
|
// Command: $g
|
|
// - Desc: Read general registers
|
|
// - Args: <None>
|
|
case 'g': return CommandReadRegisters(packet);
|
|
// Command: $G
|
|
// - Desc: Write general registers
|
|
// - Args: XX...
|
|
case 'G': return CommandUnknown(packet);
|
|
// Command: $H
|
|
// - Desc: Sets thread for subsequent operations
|
|
// - Args: op thread-id
|
|
case 'H': return CommandThreadOp(packet);
|
|
// Command: $i
|
|
// - Desc: Step the remote target by a single clock cycle
|
|
// - Args: [addr[,nnn]]
|
|
case 'i': return CommandUnknown(packet);
|
|
// Command: $I
|
|
// - Desc: Signal, then cycle step
|
|
// - Args: <None>
|
|
case 'I': return CommandUnknown(packet);
|
|
// Command: $k
|
|
// - Desc: kill process
|
|
case 'k': return CommandKill(packet);
|
|
// Command: $m
|
|
// - Desc: Read addressable memory
|
|
// - Args: addr length
|
|
case 'm':
|
|
// Command: $M
|
|
// - Desc: Write addressable memory
|
|
// - Args: addr length
|
|
case 'M': return CommandMemory(packet);
|
|
// Command: $p
|
|
// - Desc: Read the value of a register
|
|
// - Args: index
|
|
case 'p': return CommandReadReg(packet);
|
|
// Command: $q
|
|
// - Desc: General query fetching
|
|
// - Args: Name params...
|
|
case 'q':
|
|
// Command: $Q
|
|
// - Desc: General query setting
|
|
// - Args: Name params...
|
|
case 'Q': return CommandQuery(packet);
|
|
// Command: $r
|
|
// - Desc: Reset the entire system
|
|
// - Args: <None>
|
|
// - Deprecated: Use $R instead.
|
|
case 'r': return CommandUnknown(packet);
|
|
// Command: $R
|
|
// - Desc: Restart the program beging debugged
|
|
// - Args: XX
|
|
case 'R': return CommandUnknown(packet);
|
|
// Command: $s
|
|
// - Desc: Single step
|
|
// - Args: [addr]
|
|
case 's': return CommandSingleStep(packet);
|
|
// Command: $S
|
|
// - Desc: Step with Signal
|
|
// - Args: sig[;addr]
|
|
// - Deprecated: See $vCont for multi-threaded support.
|
|
case 'S': return CommandUnknown(packet);
|
|
// Command: $t
|
|
// - Desc: Search backwards started at address with pattern and mask.
|
|
// - Args: addr:PP,MM
|
|
case 't': return CommandUnknown(packet);
|
|
// Command: $T
|
|
// - Desc: Find out if the thread is alive
|
|
// - Args: thread-id
|
|
case 'T': return CommandQueryThreadAlive(packet);
|
|
// Command: $v<Operation>
|
|
// - Desc: Multi-letter command
|
|
case 'v': return CommandMultiLetterV(packet);
|
|
// Command: $X
|
|
// - Desc: Write data to memory
|
|
// - Args: addr,length:XX...
|
|
case 'X': return CommandUnknown(packet);
|
|
// Command: $z
|
|
// - Desc: Insert a type of breakpoint or watchpoint
|
|
// - Args: type,addr,kind
|
|
case 'z':
|
|
// Command: $Z
|
|
// - Desc: Remove a type of breakpoint or watchpoint
|
|
// - Args: type,addr,kind
|
|
case 'Z': return CommandBreakpoint(packet);
|
|
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(*CommsSocket, false);
|
|
} else if (response.TypeResponse == HandledPacketType::TYPE_NACK || response.TypeResponse == HandledPacketType::TYPE_ONLYNACK) {
|
|
SendACK(*CommsSocket, true);
|
|
}
|
|
|
|
if (response.TypeResponse == HandledPacketType::TYPE_UNKNOWN) {
|
|
SendPacket(*CommsSocket, "");
|
|
} else if (response.TypeResponse != HandledPacketType::TYPE_ONLYNACK && response.TypeResponse != HandledPacketType::TYPE_ONLYACK &&
|
|
response.TypeResponse != HandledPacketType::TYPE_NONE) {
|
|
SendPacket(*CommsSocket, response.Response);
|
|
}
|
|
}
|
|
|
|
std::pair<fextl::vector<std::byte>::iterator, bool>
|
|
GdbServer::MatchPacket(fextl::vector<std::byte>::iterator begin, fextl::vector<std::byte>::iterator end) {
|
|
if (CommsBuffer.empty()) {
|
|
return std::make_pair(begin, false);
|
|
}
|
|
switch ((char)CommsBuffer[0]) {
|
|
case '+':
|
|
case '-':
|
|
case '\x03':
|
|
// No further data
|
|
return std::make_pair(std::next(begin), true);
|
|
|
|
case '$': {
|
|
// Message format: $packet-data#checksum, where checksum is a single byte.
|
|
auto match = std::find(begin, end, (std::byte)'#');
|
|
if (match == end) {
|
|
// No match; fetch more data
|
|
return std::make_pair(end, false);
|
|
} else if (end - match <= 2) {
|
|
// Found '#' but missing the checksum bytes
|
|
return std::make_pair(match, false);
|
|
} else {
|
|
return std::make_pair(std::next(match, 3), true);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default: ERROR_AND_DIE_FMT("Unexpected character at beginning of GDB packet: {}", CommsBuffer[0]);
|
|
}
|
|
}
|
|
|
|
void GdbServer::HandlePacket(fasio::error ec, size_t BytesInMessage) {
|
|
if (ec != fasio::error::success || BytesInMessage == 0) {
|
|
ERROR_AND_DIE_FMT("Failed");
|
|
}
|
|
|
|
char c = (char)CommsBuffer[0];
|
|
switch (c) {
|
|
case '$': {
|
|
auto packet = ReadPacket(std::span {CommsBuffer}.subspan(0, BytesInMessage));
|
|
auto 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(*CommsSocket, {});
|
|
}
|
|
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})", c, c);
|
|
}
|
|
|
|
CommsBuffer.erase(CommsBuffer.begin(), CommsBuffer.begin() + BytesInMessage);
|
|
|
|
async_read_until(*CommsSocket, fasio::dynamic_vector_buffer {CommsBuffer}, std::bind_front(&GdbServer::MatchPacket, this),
|
|
std::bind_front(&GdbServer::HandlePacket, this));
|
|
}
|
|
|
|
|
|
void GdbServer::GdbServerLoop() {
|
|
OpenListenSocket();
|
|
if (!Acceptor) {
|
|
// Couldn't open socket, just exit.
|
|
return;
|
|
}
|
|
|
|
Acceptor->async_accept([this](fasio::error ec, std::optional<fasio::tcp_socket> Socket) {
|
|
if (ec != fasio::error::success) {
|
|
// Listen socket error or shutting down
|
|
if (CommsSocket) {
|
|
LogMan::Msg::EFmt("[GdbServer] gdbserver shutting down: {}", CommsSocket->FD);
|
|
close(CommsSocket->FD);
|
|
CommsSocket.reset();
|
|
}
|
|
// Repeat to wait for another connection
|
|
return fasio::post_callback::repeat;
|
|
}
|
|
|
|
CommsSocket.emplace(*std::move(Socket));
|
|
|
|
// Receive packet data
|
|
async_read_until(*CommsSocket, fasio::dynamic_vector_buffer {CommsBuffer}, std::bind_front(&GdbServer::MatchPacket, this),
|
|
std::bind_front(&GdbServer::HandlePacket, this));
|
|
|
|
// Repeat to catch disconnect events
|
|
return fasio::post_callback::repeat;
|
|
});
|
|
|
|
CommsBuffer.reserve(1000);
|
|
|
|
// Enter event loop
|
|
Reactor.run();
|
|
|
|
// Shut down
|
|
std::lock_guard lk(sendMutex);
|
|
if (CommsSocket) {
|
|
close(CommsSocket->FD);
|
|
CommsSocket.reset();
|
|
}
|
|
|
|
CloseListenSocket();
|
|
}
|
|
static void* ThreadHandler(void* Arg) {
|
|
HLE::ThreadManager::SetThreadName("FEX:gdbserver");
|
|
auto This = reinterpret_cast<FEX::GdbServer*>(Arg);
|
|
This->GdbServerLoop();
|
|
return nullptr;
|
|
}
|
|
|
|
void GdbServer::StartThread() {
|
|
gdbServerThread = FEXCore::Threads::Thread::Create(ThreadHandler, this, {.Internal = true});
|
|
}
|
|
|
|
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());
|
|
|
|
for (int attempt = 0; attempt < 2; ++attempt) {
|
|
Acceptor = fasio::tcp_acceptor::create(Reactor, false, GdbUnixSocketPath, 1);
|
|
if (Acceptor) {
|
|
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 (!Acceptor) {
|
|
LogMan::Msg::EFmt("[GdbServer] Couldn't bind AF_UNIX socket '{}': {} {}\n", GdbUnixSocketPath, errno, strerror(errno));
|
|
return;
|
|
}
|
|
|
|
LogMan::Msg::IFmt("[GdbServer] Waiting for connection on {}", GdbUnixSocketPath);
|
|
LogMan::Msg::IFmt("[GdbServer] gdb-multiarch -ex \"set debug remote 1\" -ex \"target extended-remote {}\"", GdbUnixSocketPath);
|
|
}
|
|
|
|
void GdbServer::CloseListenSocket() {
|
|
Acceptor.reset();
|
|
unlink(GdbUnixSocketPath.c_str());
|
|
}
|
|
|
|
#endif
|
|
} // namespace FEX
|