553 lines
17 KiB
C++
553 lines
17 KiB
C++
#include <nall/gdb/server.hpp>
|
|
|
|
#include <inttypes.h>
|
|
|
|
using string = ::nall::string;
|
|
using string_view = ::nall::string_view;
|
|
|
|
namespace {
|
|
constexpr bool GDB_LOG_MESSAGES = false;
|
|
|
|
constexpr u32 MAX_REQUESTS_PER_UPDATE = 10;
|
|
constexpr u32 MAX_PACKET_SIZE = 0x4096;
|
|
constexpr u32 DEF_BREAKPOINT_SIZE = 64;
|
|
constexpr bool NON_STOP_MODE = false; // broken for now, mainly useful for multi-thread debugging, which we can't really support
|
|
|
|
auto gdbCalcChecksum(const string &payload) -> u8 {
|
|
u8 checksum = 0;
|
|
for(char c : payload)checksum += c;
|
|
return checksum;
|
|
}
|
|
|
|
template<typename T>
|
|
inline auto addOrRemoveEntry(vector<T> &data, T value, bool shouldAdd) {
|
|
if(shouldAdd) {
|
|
data.append(value);
|
|
} else {
|
|
data.removeByValue(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace nall::GDB {
|
|
Server server{};
|
|
|
|
auto Server::reportSignal(Signal sig, u64 originPC) -> bool {
|
|
if(!hasActiveClient || !handshakeDone)return true; // no client -> no error
|
|
if(forceHalt)return false; // Signals can only happen while the game is running, ignore others
|
|
|
|
pcOverride = originPC;
|
|
|
|
forceHalt = true;
|
|
haltSignalSent = true;
|
|
sendSignal(sig);
|
|
|
|
return true;
|
|
}
|
|
|
|
auto Server::reportWatchpoint(const Watchpoint &wp, u64 address) -> void {
|
|
auto orgAddress = wp.addressStartOrg + (address - wp.addressStart);
|
|
forceHalt = true;
|
|
haltSignalSent = true;
|
|
sendSignal(Signal::TRAP, {wp.getTypePrefix(), hex(orgAddress), ";"});
|
|
}
|
|
|
|
auto Server::reportMemRead(u64 address, u32 size) -> void {
|
|
if(!watchpointRead)return;
|
|
|
|
if(hooks.normalizeAddress) {
|
|
address = hooks.normalizeAddress(address);
|
|
}
|
|
|
|
u64 addressEnd = address + size - 1;
|
|
for(const auto& wp : watchpointRead) {
|
|
if(wp.hasOverlap(address, addressEnd)) {
|
|
return reportWatchpoint(wp, address);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto Server::reportMemWrite(u64 address, u32 size) -> void {
|
|
if(!watchpointWrite)return;
|
|
|
|
if(hooks.normalizeAddress) {
|
|
address = hooks.normalizeAddress(address);
|
|
}
|
|
|
|
u64 addressEnd = address + size - 1;
|
|
for(const auto& wp : watchpointWrite) {
|
|
if(wp.hasOverlap(address, addressEnd)) {
|
|
return reportWatchpoint(wp, address);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto Server::reportPC(u64 pc) -> bool {
|
|
if(!hasActiveClient)return true;
|
|
|
|
currentPC = pc;
|
|
bool needHalts = forceHalt || breakpoints.contains(pc);
|
|
|
|
if(needHalts) {
|
|
forceHalt = true; // breakpoints may get deleted after a signal, but we have to stay stopped
|
|
|
|
if(!haltSignalSent) {
|
|
haltSignalSent = true;
|
|
sendSignal(Signal::TRAP);
|
|
}
|
|
}
|
|
|
|
if(singleStepActive) {
|
|
singleStepActive = false;
|
|
forceHalt = true;
|
|
}
|
|
|
|
return !needHalts;
|
|
}
|
|
|
|
/**
|
|
* NOTE: please read the comment in the header server.hpp file before making any changes here!
|
|
*/
|
|
auto Server::processCommand(const string& cmd, bool &shouldReply) -> string
|
|
{
|
|
auto cmdParts = cmd.split(":");
|
|
auto cmdName = cmdParts[0];
|
|
char cmdPrefix = cmdName.size() > 0 ? cmdName(0) : ' ';
|
|
|
|
if constexpr(GDB_LOG_MESSAGES) {
|
|
print("GDB <: %s\n", cmdBuffer.data());
|
|
}
|
|
|
|
switch(cmdPrefix)
|
|
{
|
|
case '!': return "OK"; // informs us that "extended remote-debugging" is used
|
|
|
|
case '?': // handshake: why did we halt?
|
|
haltProgram();
|
|
haltSignalSent = true;
|
|
return "T05"; // needs to be faked, otherwise the GDB-client hangs up and eats 100% CPU
|
|
|
|
case 'c': // continue
|
|
case 'C': // continue (with signal, signal itself can be ignored)
|
|
// normal stop-mode is only allowed to respond once a signal was raised, non-stop must return OK immediately
|
|
handshakeDone = true; // good indicator that GDB is done, also enables exception sending
|
|
shouldReply = NON_STOP_MODE;
|
|
resumeProgram();
|
|
return "OK";
|
|
|
|
case 'D': // client wants to detach (Note: VScode doesn't seem to use this, uses vKill instead)
|
|
requestDisconnect = true;
|
|
return "OK";
|
|
break;
|
|
|
|
case 'g': // dump all general registers
|
|
if(hooks.regReadGeneral) {
|
|
return hooks.regReadGeneral();
|
|
} else {
|
|
return "0000000000000000000000000000000000000000";
|
|
}
|
|
break;
|
|
|
|
case 'G': // set all general registers
|
|
if(hooks.regWriteGeneral) {
|
|
hooks.regWriteGeneral(cmd.slice(1));
|
|
return "OK";
|
|
}
|
|
break;
|
|
|
|
case 'H': // set which thread a 'c' command that may follow belongs to (can be ignored in stop-mode)
|
|
if(cmdName == "Hc0")currentThreadC = 0;
|
|
if(cmdName == "Hc-1")currentThreadC = -1;
|
|
return "OK";
|
|
|
|
case 'k': // old version of vKill
|
|
if(handshakeDone) { // sometimes this gets send during handshake (to reset the program?) -> ignore
|
|
requestDisconnect = true;
|
|
}
|
|
return "OK";
|
|
break;
|
|
|
|
case 'm': // read memory (e.g.: "m80005A00,4")
|
|
{
|
|
if(!hooks.read) {
|
|
return "";
|
|
}
|
|
|
|
auto sepIdxMaybe = cmdName.find(",");
|
|
u32 sepIdx = sepIdxMaybe ? sepIdxMaybe.get() : 1;
|
|
|
|
u64 address = cmdName.slice(1, sepIdx-1).hex();
|
|
u64 count = cmdName.slice(sepIdx+1, cmdName.size()-sepIdx).hex();
|
|
return hooks.read(address, count);
|
|
}
|
|
break;
|
|
|
|
case 'M': // write memory (e.g.: "M801ef90a,4:01000000")
|
|
{
|
|
if(!hooks.write) {
|
|
return "";
|
|
}
|
|
|
|
auto sepIdxMaybe = cmdName.find(",");
|
|
u32 sepIdx = sepIdxMaybe ? sepIdxMaybe.get() : 1;
|
|
|
|
u64 address = cmdName.slice(1, sepIdx-1).hex();
|
|
u64 unitSize = cmdName.slice(sepIdx+1, 1).hex();
|
|
u64 value = cmdParts.size() > 1 ? cmdParts[1].hex() : 0;
|
|
|
|
hooks.write(address, unitSize, value);
|
|
return "OK";
|
|
}
|
|
|
|
break;
|
|
|
|
case 'p': // read specific register (e.g.: "p15")
|
|
if(hooks.regRead) {
|
|
u32 regIdx = cmdName.slice(1).integer();
|
|
return hooks.regRead(regIdx);
|
|
} else {
|
|
return "00000000";
|
|
}
|
|
break;
|
|
|
|
case 'P': // write specific register (e.g.: "P15=FFFFFFFF80001234")
|
|
if(hooks.regWrite) {
|
|
auto sepIdxMaybe = cmdName.find("=");
|
|
u32 sepIdx = sepIdxMaybe ? sepIdxMaybe.get() : 1;
|
|
|
|
u32 regIdx = static_cast<u32>(cmdName.slice(1, sepIdx-1).hex());
|
|
u64 regValue = cmdName.slice(sepIdx+1).hex();
|
|
|
|
return hooks.regWrite(regIdx, regValue) ? "OK" : "E00";
|
|
}
|
|
break;
|
|
|
|
case 'q':
|
|
// This tells the client what we can and can't do
|
|
if(cmdName == "qSupported"){ return {
|
|
"PacketSize=", hex(MAX_PACKET_SIZE),
|
|
";fork-events-;swbreak+;hwbreak-",
|
|
";vContSupported-", // prevent vCont commands (reduces potential GDB variations: some prefer using it, others don't)
|
|
NON_STOP_MODE ? ";QNonStop+" : "",
|
|
"QStartNoAckMode+",
|
|
hooks.targetXML ? ";xmlRegisters+;qXfer:features:read+" : "" // (see: https://marc.info/?l=gdb&m=149901965961257&w=2)
|
|
};}
|
|
|
|
// handshake-command, most return dummy values to convince gdb to connect
|
|
if(cmdName == "qTStatus")return forceHalt ? "T1" : "";
|
|
if(cmdName == "qAttached")return "1"; // we are always attached, since a game is running
|
|
if(cmdName == "qOffsets")return "Text=0;Data=0;Bss=0";
|
|
|
|
if(cmdName == "qSymbol")return "OK"; // client offers us symbol-names -> we don't care
|
|
|
|
// client asks us about existing breakpoints (may happen after a re-connect) -> ignore since we clear them on connect
|
|
if(cmdName == "qTfP")return "";
|
|
if(cmdName == "qTsP")return "";
|
|
|
|
// extended target features (gdb extension), most return XML data
|
|
if(cmdName == "qXfer" && cmdParts.size() > 4)
|
|
{
|
|
if(cmdParts[1] == "features" && cmdParts[2] == "read") {
|
|
// informs the client about arch/registers (https://sourceware.org/gdb/onlinedocs/gdb/Target-Description-Format.html#Target-Description-Format)
|
|
if(cmdParts[3] == "target.xml") {
|
|
return hooks.targetXML ? string{"l", hooks.targetXML()} : string{""};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Thread-related queries
|
|
if(cmdName == "qfThreadInfo")return {"m1"};
|
|
if(cmdName == "qsThreadInfo")return {"l"};
|
|
if(cmdName == "qThreadExtraInfo,1")return ""; // ignoring this command fixes support for CLion (and VSCode?), otherwise gdb hangs
|
|
if(cmdName == "qC")return {"QC1"};
|
|
// there will also be a "qP0000001f0000000000000001" command depending on the IDE, this is ignored to prevent GDB from hanging up
|
|
break;
|
|
|
|
case 'Q':
|
|
if(cmdName == "QNonStop") { // 0=stop, 1=non-stop-mode (this allows for async GDB-communication)
|
|
if(cmdParts.size() <= 1)return "E00";
|
|
nonStopMode = cmdParts[1] == "1";
|
|
|
|
if(nonStopMode) {
|
|
haltProgram();
|
|
} else {
|
|
resumeProgram();
|
|
}
|
|
return "OK";
|
|
}
|
|
|
|
if(cmdName == "QStartNoAckMode") {
|
|
if (noAckMode) {
|
|
return "OK";
|
|
}
|
|
// The final OK has to be sent in ack mode.
|
|
sendPayload("OK");
|
|
shouldReply = false;
|
|
noAckMode = true;
|
|
return "";
|
|
}
|
|
break;
|
|
|
|
case 's': {
|
|
if(cmdName.size() > 1) {
|
|
u64 address = cmdName.slice(1).integer();
|
|
printf("stepping at address unsupported, ignore (%016" PRIX64 ")\n", address);
|
|
}
|
|
|
|
shouldReply = false;
|
|
singleStepActive = true;
|
|
resumeProgram();
|
|
return "";
|
|
} break;
|
|
|
|
case 'v': {
|
|
// normalize (e.g. "vAttach;1" -> "vAttach")
|
|
auto sepIdxMaybe = cmdName.find(";");
|
|
auto vName = sepIdxMaybe ? cmdName.slice(0, sepIdxMaybe.get()) : cmdName;
|
|
|
|
if(vName == "vMustReplyEmpty")return ""; // handshake-command / keep-alive (must return the same as an unknown command would)
|
|
if(vName == "vAttach")return NON_STOP_MODE ? "OK" : "S05"; // attaches to the process, we must return a fake trap-exception to make gdb happy
|
|
if(vName == "vCont?")return ""; // even though "vContSupported-" is set, gdb may still ask for it -> ignore to force e.g. `s` instead of `vCont;s:1;c`
|
|
if(vName == "vStopped")return "";
|
|
if(vName == "vCtrlC") {
|
|
haltProgram();
|
|
return "OK";
|
|
}
|
|
|
|
if(vName == "vKill") {
|
|
if(handshakeDone) { // sometimes this gets send during handshake (to reset the program?) -> ignore
|
|
requestDisconnect = true;
|
|
}
|
|
return "OK";
|
|
}
|
|
|
|
if(vName == "vCont") return "E00"; // if GDB completely ignores both "vCont is unsupported" responses, throw an error here
|
|
|
|
} break;
|
|
|
|
case 'Z': // insert breakpoint (e.g. "Z0,801a0ef4,4")
|
|
case 'z': // remove breakpoint (e.g. "z0,801a0ef4,4")
|
|
{
|
|
bool isInsert = cmdPrefix == 'Z';
|
|
bool isHardware = cmdName(1) == '1'; // 0=software, 1=hardware
|
|
auto sepIdxMaybe = cmdName.findFrom(3, ",");
|
|
u32 sepIdx = sepIdxMaybe ? (sepIdxMaybe.get()+3) : 0;
|
|
|
|
u64 address = cmdName.slice(3, sepIdx-1).hex();
|
|
u64 addressStart = address;
|
|
u64 addressEnd = address + cmdName.slice(sepIdx+1).hex() - 1;
|
|
|
|
if(hooks.normalizeAddress) {
|
|
addressStart = hooks.normalizeAddress(addressStart);
|
|
addressEnd = hooks.normalizeAddress(addressEnd);
|
|
}
|
|
Watchpoint wp{addressStart, addressEnd, address};
|
|
|
|
switch(cmdName(1)) {
|
|
case '0': // (hardware/software breakpoints are the same for us)
|
|
case '1': addOrRemoveEntry(breakpoints, address, isInsert); break;
|
|
|
|
case '2':
|
|
wp.type = WatchpointType::WRITE;
|
|
addOrRemoveEntry(watchpointWrite, wp, isInsert);
|
|
break;
|
|
|
|
case '3':
|
|
wp.type = WatchpointType::READ;
|
|
addOrRemoveEntry(watchpointRead, wp, isInsert);
|
|
break;
|
|
|
|
case '4':
|
|
wp.type = WatchpointType::ACCESS;
|
|
addOrRemoveEntry(watchpointRead, wp, isInsert);
|
|
addOrRemoveEntry(watchpointWrite, wp, isInsert);
|
|
break;
|
|
default: return "E00";
|
|
}
|
|
|
|
if(hooks.emuCacheInvalidate) { // for re-compiler, otherwise breaks might be skipped
|
|
hooks.emuCacheInvalidate(address);
|
|
}
|
|
return "OK";
|
|
}
|
|
}
|
|
|
|
printf("Unknown-Command: %s (data: %s)\n", cmdName.data(), cmdBuffer.data());
|
|
return "";
|
|
}
|
|
|
|
auto Server::onText(string_view text) -> void {
|
|
|
|
if(cmdBuffer.size() == 0) {
|
|
cmdBuffer.reserve(text.size());
|
|
}
|
|
|
|
for(char c : text)
|
|
{
|
|
switch(c)
|
|
{
|
|
case '$':
|
|
insideCommand = true;
|
|
break;
|
|
|
|
case '#': { // end of message + 2-char checksum after that
|
|
insideCommand = false;
|
|
|
|
++messageCount;
|
|
bool shouldReply = true;
|
|
auto cmdRes = processCommand(cmdBuffer, shouldReply);
|
|
if(shouldReply) {
|
|
sendPayload(cmdRes);
|
|
} else if(!noAckMode) {
|
|
sendText("+");
|
|
}
|
|
|
|
cmdBuffer = "";
|
|
} break;
|
|
|
|
case '+': break; // "OK" response -> ignore
|
|
|
|
case '\x03': // CTRL+C (same as "vCtrlC" packet) -> force halt
|
|
if constexpr(GDB_LOG_MESSAGES) {
|
|
printf("GDB <: CTRL+C [0x03]\n");
|
|
}
|
|
haltProgram();
|
|
break;
|
|
|
|
default:
|
|
if(insideCommand) {
|
|
cmdBuffer.append(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto Server::updateLoop() -> void {
|
|
if(!isStarted())return;
|
|
|
|
if(requestDisconnect) {
|
|
requestDisconnect = false;
|
|
if(!noAckMode) {
|
|
sendText("+");
|
|
}
|
|
disconnectClient();
|
|
resumeProgram();
|
|
return;
|
|
}
|
|
|
|
// The following code manages the message processing which gets exchanged from the server thread.
|
|
// It was carefully build to balance latency, throughput and CPU usage to let the game still run at full speed
|
|
// while allowing for fast processing once the debugger is halted.
|
|
|
|
u32 loopFrames = isHalted() ? 20 : 1; // "frames" to check (loops with sleep in-between)
|
|
u32 loopCount = isHalted() ? 500 : 100; // loops inside a frame, the more the less latency, but CPU usage goes up
|
|
u32 maxLoopResets = 10000; // how many times can a new message reset the counter (prevents infinite loops with misbehaving clients)
|
|
bool wasHalted = isHalted();
|
|
|
|
for(u32 frame=0; frame<loopFrames; ++frame) {
|
|
for(u32 i=0; i<loopCount; ++i) {
|
|
messageCount = 0;
|
|
update();
|
|
|
|
// if the last message resumed the program, abort (no more messages will be send until the next stop)
|
|
if(wasHalted && !isHalted())return;
|
|
|
|
if(messageCount > 0 && maxLoopResets > 0) {
|
|
i = loopCount; // reset loop here to keep a fast chain of messages going (reduces latency)
|
|
--maxLoopResets;
|
|
}
|
|
}
|
|
|
|
if(wasHalted)usleep(1);
|
|
}
|
|
}
|
|
|
|
auto Server::getStatusText(u32 port, bool useIPv4) -> string {
|
|
auto url = getURL(port, useIPv4);
|
|
string prefix = isHalted() ? "⬛" : "▶";
|
|
|
|
if(hasClient())return {prefix, " GDB connected ", url};
|
|
if(isStarted())return {"GDB listening ", url};
|
|
return {"GDB pending (", url, ")"};
|
|
}
|
|
|
|
auto Server::sendSignal(Signal code) -> void {
|
|
sendPayload({"S", hex(static_cast<u8>(code), 2)});
|
|
}
|
|
|
|
auto Server::sendSignal(Signal code, const string& reason) -> void {
|
|
sendPayload({"T", hex(static_cast<u8>(code), 2), reason});
|
|
}
|
|
|
|
auto Server::sendPayload(const string& payload) -> void {
|
|
string msg{noAckMode ? "$" : "+$", payload, '#', hex(gdbCalcChecksum(payload), 2, '0')};
|
|
if constexpr(GDB_LOG_MESSAGES) {
|
|
printf("GDB >: %.*s\n", msg.size() > 100 ? 100 : msg.size(), msg.data());
|
|
}
|
|
sendText(msg);
|
|
}
|
|
|
|
auto Server::haltProgram() -> void {
|
|
forceHalt = true;
|
|
haltSignalSent = false;
|
|
}
|
|
|
|
auto Server::resumeProgram() -> void {
|
|
pcOverride.reset();
|
|
forceHalt = false;
|
|
haltSignalSent = false;
|
|
}
|
|
|
|
auto Server::onConnect() -> void {
|
|
printf("GDB client connected\n");
|
|
resetClientData();
|
|
hasActiveClient = true;
|
|
}
|
|
|
|
auto Server::onDisconnect() -> void {
|
|
printf("GDB client disconnected\n");
|
|
hadHandshake = false;
|
|
resetClientData();
|
|
}
|
|
|
|
auto Server::reset() -> void {
|
|
hooks.read.reset();
|
|
hooks.write.reset();
|
|
hooks.normalizeAddress.reset();
|
|
hooks.regReadGeneral.reset();
|
|
hooks.regWriteGeneral.reset();
|
|
hooks.regRead.reset();
|
|
hooks.regWrite.reset();
|
|
hooks.emuCacheInvalidate.reset();
|
|
hooks.targetXML.reset();
|
|
|
|
resetClientData();
|
|
}
|
|
|
|
auto Server::resetClientData() -> void {
|
|
breakpoints.reset();
|
|
breakpoints.reserve(DEF_BREAKPOINT_SIZE);
|
|
|
|
watchpointRead.reset();
|
|
watchpointRead.reserve(DEF_BREAKPOINT_SIZE);
|
|
|
|
watchpointWrite.reset();
|
|
watchpointWrite.reserve(DEF_BREAKPOINT_SIZE);
|
|
|
|
pcOverride.reset();
|
|
insideCommand = false;
|
|
cmdBuffer = "";
|
|
haltSignalSent = false;
|
|
forceHalt = false;
|
|
singleStepActive = false;
|
|
nonStopMode = false;
|
|
noAckMode = false;
|
|
|
|
currentThreadC = -1;
|
|
hasActiveClient = false;
|
|
handshakeDone = false;
|
|
requestDisconnect = false;
|
|
}
|
|
|
|
};
|