diff --git a/.gitignore b/.gitignore index 418b6a3e11..96f8b2347d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ *.moc *.user .vs/ +.vscode/ +.idea/ +cmake-*/ obj/ out/ obj-amd64/ obj-arm64/ out-amd64/ -out-arm64/ +out-arm64/ \ No newline at end of file diff --git a/ares/ares/ares.cpp b/ares/ares/ares.cpp index 46d6c7bbef..6b23035b19 100644 --- a/ares/ares/ares.cpp +++ b/ares/ares/ares.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include diff --git a/ares/n64/cpu/cpu.cpp b/ares/n64/cpu/cpu.cpp index 1eb21e01aa..57f36cf650 100644 --- a/ares/n64/cpu/cpu.cpp +++ b/ares/n64/cpu/cpu.cpp @@ -1,4 +1,5 @@ #include +#include namespace ares::Nintendo64 { @@ -30,7 +31,7 @@ auto CPU::unload() -> void { } auto CPU::main() -> void { - while(!vi.refreshed) { + while(!vi.refreshed && GDB::server.reportPC(ipu.pc & 0xFFFFFFFF)) { instruction(); synchronize(); } @@ -104,7 +105,7 @@ auto CPU::instruction() -> void { } if (auto address = devirtualize(ipu.pc)) { - auto block = recompiler.block(ipu.pc, *address); + auto block = recompiler.block(ipu.pc, *address, GDB::server.hasBreakpoints()); block->execute(*this); } } diff --git a/ares/n64/cpu/cpu.hpp b/ares/n64/cpu/cpu.hpp index 5be25805a5..c55664afcd 100644 --- a/ares/n64/cpu/cpu.hpp +++ b/ares/n64/cpu/cpu.hpp @@ -186,6 +186,8 @@ struct CPU : Thread { template auto write(u32 vaddr, u32 address, u64 data) -> void; auto power(bool reset) -> void; + auto readDebug(u32 vaddr, u32 address) -> u8; + //8KB struct Line { auto hit(u32 address) const -> bool; @@ -242,8 +244,9 @@ struct CPU : Thread { } entry[TLB::Entries]; //tlb.cpp - auto load(u64 vaddr) -> Match; - auto load(u64 vaddr, const Entry& entry) -> maybe; + auto load(u64 vaddr, bool noExceptions = false) -> Match; + auto load(u64 vaddr, const Entry& entry, bool noExceptions = false) -> maybe; + auto loadFast(u64 vaddr) -> Match; auto store(u64 vaddr) -> Match; auto store(u64 vaddr, const Entry& entry) -> maybe; @@ -292,6 +295,7 @@ struct CPU : Thread { auto segment(u64 vaddr) -> Context::Segment; auto devirtualize(u64 vaddr) -> maybe; alwaysinline auto devirtualizeFast(u64 vaddr) -> u64; + auto devirtualizeDebug(u64 vaddr) -> u64; auto fetch(u64 vaddr) -> maybe; template auto busWrite(u32 address, u64 data) -> void; @@ -301,6 +305,8 @@ struct CPU : Thread { template auto vaddrAlignedError(u64 vaddr, bool write) -> bool; auto addressException(u64 vaddr) -> void; + auto readDebug(u64 vaddr) -> u8; + //serialization.cpp auto serialize(serializer&) -> void; @@ -894,10 +900,10 @@ struct CPU : Thread { } auto pool(u32 address) -> Pool*; - auto block(u32 vaddr, u32 address) -> Block*; + auto block(u32 vaddr, u32 address, bool singleInstruction = false) -> Block*; auto fastFetchBlock(u32 address) -> Block*; - auto emit(u32 vaddr, u32 address) -> Block*; + auto emit(u32 vaddr, u32 address, bool singleInstruction = false) -> Block*; auto emitEXECUTE(u32 instruction) -> bool; auto emitSPECIAL(u32 instruction) -> bool; auto emitREGIMM(u32 instruction) -> bool; diff --git a/ares/n64/cpu/dcache.cpp b/ares/n64/cpu/dcache.cpp index 5fa69aa3d3..95df19e271 100644 --- a/ares/n64/cpu/dcache.cpp +++ b/ares/n64/cpu/dcache.cpp @@ -101,6 +101,15 @@ auto CPU::DataCache::read(u32 vaddr, u32 address) -> u64 { return line.read(address); } +auto CPU::DataCache::readDebug(u32 vaddr, u32 address) -> u8 { + auto& line = this->line(vaddr); + if(!line.hit(address)) { + Thread dummyThread{}; + return bus.read(address, dummyThread); + } + return line.read(address); +} + template auto CPU::DataCache::write(u32 vaddr, u32 address, u64 data) -> void { auto& line = this->line(vaddr); diff --git a/ares/n64/cpu/exceptions.cpp b/ares/n64/cpu/exceptions.cpp index 310662f496..00189db5c1 100644 --- a/ares/n64/cpu/exceptions.cpp +++ b/ares/n64/cpu/exceptions.cpp @@ -1,6 +1,11 @@ auto CPU::Exception::trigger(u32 code, u32 coprocessor, bool tlbMiss) -> void { self.debugger.exception(code); + if(code != 0) { + auto sig = (code == 2 || code == 3) ? GDB::Signal::SEGV : GDB::Signal::TRAP; + GDB::server.reportSignal(sig, self.ipu.pc); + } + u64 vectorBase = !self.scc.status.vectorLocation ? (s32)0x8000'0000 : (s32)0xbfc0'0200; u16 vectorOffset = 0x0180; diff --git a/ares/n64/cpu/memory.cpp b/ares/n64/cpu/memory.cpp index dc0d44ec3c..1ca871621d 100644 --- a/ares/n64/cpu/memory.cpp +++ b/ares/n64/cpu/memory.cpp @@ -140,6 +140,10 @@ auto CPU::devirtualizeFast(u64 vaddr) -> u64 { return devirtualizeCache.pbase = 0; } +auto CPU::devirtualizeDebug(u64 vaddr) -> u64 { + return devirtualizeFast(vaddr); // this wrapper preserves the inlining of 'devirtualizeFast' +} + template inline auto CPU::busWrite(u32 address, u64 data) -> void { bus.write(address, data, *this); @@ -185,6 +189,8 @@ auto CPU::fetch(u64 vaddr) -> maybe { template auto CPU::read(u64 vaddr) -> maybe { if(vaddrAlignedError(vaddr, false)) return nothing; + GDB::server.reportMemRead(vaddr, Size); + switch(segment(vaddr)) { case Context::Segment::Unused: step(1 * 2); @@ -215,10 +221,37 @@ auto CPU::read(u64 vaddr) -> maybe { unreachable; } +auto CPU::readDebug(u64 vaddr) -> u8 { + Thread dummyThread{}; + + switch(segment(vaddr)) { + case Context::Segment::Unused: return 0; + case Context::Segment::Mapped: + if(auto match = tlb.load(vaddr, true)) { + if(match.cache) return dcache.readDebug(vaddr, match.address & context.physMask); + return bus.read(match.address & context.physMask, dummyThread); + } + return 0; + case Context::Segment::Cached: + return dcache.readDebug(vaddr, vaddr & 0x1fff'ffff); + case Context::Segment::Cached32: + return dcache.readDebug(vaddr, vaddr & 0xffff'ffff); + case Context::Segment::Direct: + return bus.read(vaddr & 0x1fff'ffff, dummyThread); + case Context::Segment::Direct32: + return bus.read(vaddr & 0xffff'ffff, dummyThread); + } + + unreachable; +} + template auto CPU::write(u64 vaddr0, u64 data, bool alignedError) -> bool { if(alignedError && vaddrAlignedError(vaddr0, true)) return false; u64 vaddr = vaddr0 & ~((u64)Size - 1); + + GDB::server.reportMemWrite(vaddr0, Size); + switch(segment(vaddr)) { case Context::Segment::Unused: step(1 * 2); diff --git a/ares/n64/cpu/recompiler.cpp b/ares/n64/cpu/recompiler.cpp index 030a2f0957..1dc02f197f 100644 --- a/ares/n64/cpu/recompiler.cpp +++ b/ares/n64/cpu/recompiler.cpp @@ -4,9 +4,9 @@ auto CPU::Recompiler::pool(u32 address) -> Pool* { return pool; } -auto CPU::Recompiler::block(u32 vaddr, u32 address) -> Block* { +auto CPU::Recompiler::block(u32 vaddr, u32 address, bool singleInstruction) -> Block* { if(auto block = pool(address)->blocks[address >> 2 & 0x3f]) return block; - auto block = emit(vaddr, address); + auto block = emit(vaddr, address, singleInstruction); pool(address)->blocks[address >> 2 & 0x3f] = block; memory::jitprotect(true); return block; @@ -18,7 +18,7 @@ auto CPU::Recompiler::fastFetchBlock(u32 address) -> Block* { return nullptr; } -auto CPU::Recompiler::emit(u32 vaddr, u32 address) -> Block* { +auto CPU::Recompiler::emit(u32 vaddr, u32 address, bool singleInstruction) -> Block* { if(unlikely(allocator.available() < 1_MiB)) { print("CPU allocator flush\n"); memory::jitprotect(false); @@ -44,7 +44,7 @@ auto CPU::Recompiler::emit(u32 vaddr, u32 address) -> Block* { call(&CPU::instructionEpilogue); vaddr += 4; address += 4; - if(hasBranched || (address & 0xfc) == 0) break; //block boundary + if(hasBranched || (address & 0xfc) == 0 || singleInstruction) break; //block boundary hasBranched = branched; testJumpEpilog(); } diff --git a/ares/n64/cpu/tlb.cpp b/ares/n64/cpu/tlb.cpp index c66abe8887..d596179a09 100644 --- a/ares/n64/cpu/tlb.cpp +++ b/ares/n64/cpu/tlb.cpp @@ -1,10 +1,12 @@ -auto CPU::TLB::load(u64 vaddr, const Entry& entry) -> maybe { +auto CPU::TLB::load(u64 vaddr, const Entry& entry, bool noExceptions) -> maybe { if(!entry.globals && entry.addressSpaceID != self.scc.tlb.addressSpaceID) return nothing; if((vaddr & entry.addressMaskHi) != entry.virtualAddress) return nothing; if(vaddr >> 62 != entry.region) return nothing; bool lo = vaddr & entry.addressSelect; if(!entry.valid[lo]) { + if(noExceptions)return Match{false}; + self.addressException(vaddr); self.debugger.tlbLoadInvalid(vaddr); self.exception.tlbLoadInvalid(); @@ -15,22 +17,24 @@ auto CPU::TLB::load(u64 vaddr, const Entry& entry) -> maybe { return Match{true, entry.cacheAlgorithm[lo] != 2, physicalAddress}; } -auto CPU::TLB::load(u64 vaddr) -> Match { +auto CPU::TLB::load(u64 vaddr, bool noExceptions) -> Match { for(auto& entry : this->tlbCache.entry) { if(!entry.entry) continue; - if(auto match = load(vaddr, *entry.entry)) { + if(auto match = load(vaddr, *entry.entry, noExceptions)) { entry.frequency++; return *match; } } for(auto& entry : this->entry) { - if(auto match = load(vaddr, entry)) { + if(auto match = load(vaddr, entry, noExceptions)) { this->tlbCache.insert(entry); return *match; } } + if(noExceptions)return {false}; + self.addressException(vaddr); self.debugger.tlbLoadMiss(vaddr); self.exception.tlbLoadMiss(); diff --git a/ares/n64/system/system.cpp b/ares/n64/system/system.cpp index 1798dd94ac..1d379a8cd7 100644 --- a/ares/n64/system/system.cpp +++ b/ares/n64/system/system.cpp @@ -1,5 +1,7 @@ #include +#include + namespace ares::Nintendo64 { auto enumerate() -> vector { @@ -105,12 +107,141 @@ auto System::load(Node::System& root, string name) -> bool { #if defined(VULKAN) vulkan.load(node); #endif + + initDebugHooks(); + return true; } +auto System::initDebugHooks() -> void { + + // See: https://sourceware.org/gdb/onlinedocs/gdb/Target-Description-Format.html#Target-Description-Format + GDB::server.hooks.targetXML = []() -> string { + return "" + "mips:4000" + ""; + }; + + GDB::server.hooks.normalizeAddress = [](u64 address) -> u64 { + return cpu.devirtualizeDebug(address); + }; + + GDB::server.hooks.read = [](u64 address, u32 byteCount) -> string { + address |= 0xFFFFFFFF'00000000ull; + + string res{}; + res.resize(byteCount * 2); + char* resPtr = res.begin(); + + for(u32 i : range(byteCount)) { + auto val = cpu.readDebug(address++); + hexByte(resPtr, val); + resPtr += 2; + } + + return res; + }; + + GDB::server.hooks.write = [](u64 address, u32 unitSize, u64 value) { + address |= 0xFFFFFFFF'00000000ull; + switch(unitSize) { + case Byte: cpu.write(address, value, false); break; + case Half: cpu.write(address, value, false); break; + case Word: cpu.write(address, value, false); break; + case Dual: cpu.write(address, value, false); break; + } + }; + + GDB::server.hooks.regRead = [](u32 regIdx) { + if(regIdx < 32) { + return hex(cpu.ipu.r[regIdx].u64, 16, '0'); + } + + switch (regIdx) + { + case 32: return hex(cpu.getControlRegister(12), 16, '0'); // COP0 status + case 33: return hex(cpu.ipu.lo.u64, 16, '0'); + case 34: return hex(cpu.ipu.hi.u64, 16, '0'); + case 35: return hex(cpu.getControlRegister(8), 16, '0'); // COP0 badvaddr + case 36: return hex(cpu.getControlRegister(13), 16, '0'); // COP0 cause + case 37: { // PC + auto pcOverride = GDB::server.getPcOverride(); + return hex(pcOverride ? pcOverride.get() : cpu.ipu.pc, 16, '0'); + } + + // case 38-69: -> FPU + case 70: return hex(cpu.getControlRegisterFPU(31), 16, '0'); // FPU control + } + + if(regIdx < (38 + 32)) { + return hex(cpu.fpu.r[regIdx-38].u64, 16, '0'); + } + + return string{"0000000000000000"}; + }; + + GDB::server.hooks.regWrite = [](u32 regIdx, u64 regValue) -> bool { + if(regIdx == 0)return true; + + if(regIdx < 32) { + cpu.ipu.r[regIdx].u64 = regValue; + return true; + } + + switch (regIdx) + { + case 32: return true; // COP0 status (ignore write) + case 33: cpu.ipu.lo.u64 = regValue; return true; + case 34: cpu.ipu.hi.u64 = regValue; return true; + case 35: return true; // COP0 badvaddr (ignore write) + case 36: return true; // COP0 cause (ignore write) + case 37: { // PC + if(!GDB::server.getPcOverride()) { + cpu.ipu.pc = regValue; + } + return true; + } + + // case 38-69: -> FPU + case 70: return true; // FPU control (ignore) + } + + if(regIdx < (38 + 32)) { + cpu.fpu.r[regIdx-38].u64 = regValue; + return true; + } + + if(regIdx == 71)return true; // ignore, GDB wants this register even though it doesn't exist + return false; + }; + + GDB::server.hooks.regReadGeneral = []() { + string res{}; + for(auto i : range(71)) { + res.append(GDB::server.hooks.regRead(i)); + } + return res; + }; + + GDB::server.hooks.regWriteGeneral = [](const string ®Data) { + u32 regIdx{0}; + for(auto i=0; i void { if(!node) return; save(); + if(vi.screen) vi.screen->quit(); //stop video thread #if defined(VULKAN) vulkan.unload(); diff --git a/ares/n64/system/system.hpp b/ares/n64/system/system.hpp index 97c77fd97e..c9f2566cec 100644 --- a/ares/n64/system/system.hpp +++ b/ares/n64/system/system.hpp @@ -31,6 +31,8 @@ struct System { bool dd = false; } information; + auto initDebugHooks() -> void; + //serialization.cpp auto serialize(serializer&, bool synchronize) -> void; }; diff --git a/desktop-ui/desktop-ui.hpp b/desktop-ui/desktop-ui.hpp index e61d2c0505..3a09d6ceb2 100644 --- a/desktop-ui/desktop-ui.hpp +++ b/desktop-ui/desktop-ui.hpp @@ -5,6 +5,7 @@ using namespace hiro; #include +#include #include #include diff --git a/desktop-ui/presentation/presentation.cpp b/desktop-ui/presentation/presentation.cpp index 13049012c5..3e515838d7 100644 --- a/desktop-ui/presentation/presentation.cpp +++ b/desktop-ui/presentation/presentation.cpp @@ -131,6 +131,9 @@ Presentation::Presentation() { driverSettingsAction.setText("Drivers" ELLIPSIS).setIcon(Icon::Place::Settings).onActivate([&] { settingsWindow.show("Drivers"); }); + debugSettingsAction.setText("Debug" ELLIPSIS).setIcon(Icon::Device::Network).onActivate([&] { + settingsWindow.show("Debug"); + }); toolsMenu.setVisible(false).setText("Tools"); saveStateMenu.setText("Save State").setIcon(Icon::Media::Record); @@ -246,10 +249,12 @@ Presentation::Presentation() { spacerLeft .setBackgroundColor({32, 32, 32}); statusLeft .setBackgroundColor({32, 32, 32}).setForegroundColor({255, 255, 255}); + statusDebug.setBackgroundColor({32, 32, 32}).setForegroundColor({255, 255, 255}); statusRight.setBackgroundColor({32, 32, 32}).setForegroundColor({255, 255, 255}); spacerRight.setBackgroundColor({32, 32, 32}); statusLeft .setAlignment(0.0).setFont(Font().setBold()); + statusDebug.setAlignment(1.0).setFont(Font().setBold()); statusRight.setAlignment(1.0).setFont(Font().setBold()); onClose([&] { diff --git a/desktop-ui/presentation/presentation.hpp b/desktop-ui/presentation/presentation.hpp index 510a7aa3a2..50ff8566c5 100644 --- a/desktop-ui/presentation/presentation.hpp +++ b/desktop-ui/presentation/presentation.hpp @@ -50,6 +50,7 @@ struct Presentation : Window { MenuItem firmwareSettingsAction{&settingsMenu}; MenuItem pathSettingsAction{&settingsMenu}; MenuItem driverSettingsAction{&settingsMenu}; + MenuItem debugSettingsAction{&settingsMenu}; Menu toolsMenu{&menuBar}; Menu saveStateMenu{&toolsMenu}; Menu loadStateMenu{&toolsMenu}; @@ -85,7 +86,8 @@ struct Presentation : Window { HorizontalLayout statusLayout{&layout, Size{~0, StatusHeight}, 0}; Label spacerLeft{&statusLayout, Size{8, ~0}, 0}; Label statusLeft{&statusLayout, Size{~0, ~0}, 0}; - Label statusRight{&statusLayout, Size{100, ~0}, 0}; + Label statusDebug{&statusLayout, Size{200, ~0}, 0}; + Label statusRight{&statusLayout, Size{90, ~0}, 0}; Label spacerRight{&statusLayout, Size{8, ~0}, 0}; }; diff --git a/desktop-ui/program/load.cpp b/desktop-ui/program/load.cpp index b1b16e1df6..4b1973dc42 100644 --- a/desktop-ui/program/load.cpp +++ b/desktop-ui/program/load.cpp @@ -33,6 +33,10 @@ auto Program::load(shared_pointer emulator, string location) -> bool { } auto Program::load(string location) -> bool { + if(settings.debugServer.enabled) { + nall::GDB::server.reset(); + } + if(!emulator->load(location)) { emulator.reset(); if(settings.video.adaptiveSizing) presentation.resizeWindow(); @@ -71,8 +75,13 @@ auto Program::load(string location) -> bool { } else { pause(false); } + showMessage({"Loaded ", Location::prefix(location)}); + if(settings.debugServer.enabled) { + nall::GDB::server.open(settings.debugServer.port, settings.debugServer.useIPv4); + } + //update recent games list for(s32 index = 7; index >= 0; index--) { settings.recent.game[index + 1] = settings.recent.game[index]; @@ -86,6 +95,9 @@ auto Program::load(string location) -> bool { auto Program::unload() -> void { if(!emulator) return; + nall::GDB::server.close(); + nall::GDB::server.reset(); + settings.save(); clearUndoStates(); showMessage({"Unloaded ", Location::prefix(emulator->game->location)}); diff --git a/desktop-ui/program/program.cpp b/desktop-ui/program/program.cpp index 2c060bcb38..66c7a863f1 100644 --- a/desktop-ui/program/program.cpp +++ b/desktop-ui/program/program.cpp @@ -50,16 +50,33 @@ auto Program::main() -> void { updateMessage(); inputManager.poll(); inputManager.pollHotkeys(); + bool defocused = driverSettings.inputDefocusPause.checked() && !ruby::video.fullScreen() && !presentation.focused(); if(emulator && defocused) message.text = "Paused"; + + if(settings.debugServer.enabled) { + presentation.statusDebug.setText( + nall::GDB::server.getStatusText(settings.debugServer.port, settings.debugServer.useIPv4) + ); + } + + if(emulator && nall::GDB::server.isHalted()) { + ruby::audio.clear(); + nall::GDB::server.updateLoop(); // sleeps internally + return; + } + if(!emulator || (paused && !program.requestFrameAdvance) || defocused) { ruby::audio.clear(); + nall::GDB::server.updateLoop(); usleep(20 * 1000); return; } rewindRun(); + nall::GDB::server.updateLoop(); + program.requestFrameAdvance = false; if(!runAhead || fastForwarding || rewinding) { emulator->root->run(); @@ -73,6 +90,8 @@ auto Program::main() -> void { emulator->root->unserialize(state); } + nall::GDB::server.updateLoop(); + if(settings.general.autoSaveMemory) { static u64 previousTime = chrono::timestamp(); u64 currentTime = chrono::timestamp(); diff --git a/desktop-ui/settings/debug.cpp b/desktop-ui/settings/debug.cpp new file mode 100644 index 0000000000..c9d4e433d6 --- /dev/null +++ b/desktop-ui/settings/debug.cpp @@ -0,0 +1,70 @@ +auto DebugSettings::construct() -> void { + setCollapsible(); + setVisible(false); + + debugLabel.setText("GDB-Server").setFont(Font().setBold()); + portLayout.setAlignment(1); + portLabel.setText("Port"); + + port.setText(integer(settings.debugServer.port)); + port.setEditable(true); + port.onChange([&](){ + settings.debugServer.port = port.text().integer(); + string portStr = integer(settings.debugServer.port); + + if(portStr != port.text()) { + port.setText(settings.debugServer.port == 0 ? string{""} : portStr); + } + + infoRefresh(); + }); + + portHint.setText("Safe range: 1024 - 32767").setFont(Font().setSize(7.0)).setForegroundColor(SystemColor::Sublabel); + + ipv4Layout.setAlignment(1); + ipv4Label.setText("Use IPv4"); + + ipv4.setEnabled(true); + ipv4.setChecked(settings.debugServer.useIPv4); + ipv4.onToggle([&](){ + settings.debugServer.useIPv4 = ipv4.checked(); + serverRefresh(); + infoRefresh(); + }); + + enabledLayout.setAlignment(1); + enabledLabel.setText("Enabled"); + + enabled.setEnabled(true); + enabled.setChecked(settings.debugServer.enabled); + enabled.onToggle([&](){ + settings.debugServer.enabled = enabled.checked(); + serverRefresh(); + infoRefresh(); + }); + + infoRefresh(); +} + +auto DebugSettings::infoRefresh() -> void { + if(settings.debugServer.enabled) { + connectInfo.setText(settings.debugServer.useIPv4 + ? "Note: IPv4 mode binds to any device, enabling anyone in your network to access this server" + : "Note: localhost only (for Windows/WSL: please use IPv4 instead)" + ); + presentation.statusDebug.setText( + nall::GDB::server.getStatusText(settings.debugServer.port, settings.debugServer.useIPv4) + ); + } else { + connectInfo.setText(""); + presentation.statusDebug.setText(""); + } +} + +auto DebugSettings::serverRefresh() -> void { + nall::GDB::server.close(); + + if(settings.debugServer.enabled) { + nall::GDB::server.open(settings.debugServer.port, settings.debugServer.useIPv4); + } +} diff --git a/desktop-ui/settings/settings.cpp b/desktop-ui/settings/settings.cpp index cee08ed63e..60630e9098 100644 --- a/desktop-ui/settings/settings.cpp +++ b/desktop-ui/settings/settings.cpp @@ -8,6 +8,7 @@ #include "firmware.cpp" #include "paths.cpp" #include "drivers.cpp" +#include "debug.cpp" #include "home.cpp" Settings settings; @@ -21,6 +22,7 @@ EmulatorSettings& emulatorSettings = settingsWindow.emulatorSettings; OptionSettings& optionSettings = settingsWindow.optionSettings; FirmwareSettings& firmwareSettings = settingsWindow.firmwareSettings; PathSettings& pathSettings = settingsWindow.pathSettings; +DebugSettings& debugSettings = settingsWindow.debugSettings; DriverSettings& driverSettings = settingsWindow.driverSettings; auto Settings::load() -> void { @@ -109,6 +111,10 @@ auto Settings::process(bool load) -> void { bind(string, "Paths/SuperFamicom/BSMemory", paths.superFamicom.bsMemory); bind(string, "Paths/SuperFamicom/SufamiTurbo", paths.superFamicom.sufamiTurbo); + bind(natural, "DebugServer/Port", debugServer.port); + bind(boolean, "DebugServer/Enabled", debugServer.enabled); + bind(boolean, "DebugServer/UseIPv4", debugServer.useIPv4); + for(u32 index : range(9)) { string name = {"Recent/Game-", 1 + index}; bind(string, name, recent.game[index]); @@ -177,6 +183,7 @@ SettingsWindow::SettingsWindow() { panelList.append(ListViewItem().setText("Firmware").setIcon(Icon::Emblem::Binary)); panelList.append(ListViewItem().setText("Paths").setIcon(Icon::Emblem::Folder)); panelList.append(ListViewItem().setText("Drivers").setIcon(Icon::Place::Settings)); + panelList.append(ListViewItem().setText("Debug").setIcon(Icon::Device::Network)); panelList->setUsesSidebarStyle(); panelList.onChange([&] { eventChange(); }); @@ -189,6 +196,7 @@ SettingsWindow::SettingsWindow() { panelContainer.append(firmwareSettings, Size{~0, ~0}); panelContainer.append(pathSettings, Size{~0, ~0}); panelContainer.append(driverSettings, Size{~0, ~0}); + panelContainer.append(debugSettings, Size{~0, ~0}); panelContainer.append(homePanel, Size{~0, ~0}); videoSettings.construct(); @@ -200,6 +208,7 @@ SettingsWindow::SettingsWindow() { firmwareSettings.construct(); pathSettings.construct(); driverSettings.construct(); + debugSettings.construct(); homePanel.construct(); setDismissable(); @@ -232,6 +241,7 @@ auto SettingsWindow::eventChange() -> void { firmwareSettings.setVisible(false); pathSettings.setVisible(false); driverSettings.setVisible(false); + debugSettings.setVisible(false); homePanel.setVisible(false); bool found = false; @@ -245,6 +255,7 @@ auto SettingsWindow::eventChange() -> void { if(item.text() == "Firmware" ) found = true, firmwareSettings.setVisible(); if(item.text() == "Paths" ) found = true, pathSettings.setVisible(); if(item.text() == "Drivers" ) found = true, driverSettings.setVisible(); + if(item.text() == "Debug" ) found = true, debugSettings.setVisible(); } if(!found) homePanel.setVisible(); diff --git a/desktop-ui/settings/settings.hpp b/desktop-ui/settings/settings.hpp index 67b9db6415..9a5734f272 100644 --- a/desktop-ui/settings/settings.hpp +++ b/desktop-ui/settings/settings.hpp @@ -88,6 +88,12 @@ struct Settings : Markup::Node { struct Recent { string game[9]; } recent; + + struct DebugServer { + u32 port = 9123; + bool enabled = false; // if enabled, server starts with ares + bool useIPv4 = false; // forces IPv4 over IPv6 + } debugServer; }; struct VideoSettings : VerticalLayout { @@ -345,6 +351,29 @@ struct DriverSettings : VerticalLayout { Group inputDefocusGroup{&inputDefocusPause, &inputDefocusBlock, &inputDefocusAllow}; }; +struct DebugSettings : VerticalLayout { + auto construct() -> void; + auto infoRefresh() -> void; + auto serverRefresh() -> void; + + Label debugLabel{this, Size{~0, 0}, 5}; + + HorizontalLayout portLayout{this, Size{~0, 0}}; + Label portLabel{&portLayout, Size{48, 20}}; + LineEdit port{&portLayout, Size{~0, 0}}; + Label portHint{&portLayout, Size{~0, 0}}; + + HorizontalLayout ipv4Layout{this, Size{~0, 0}}; + Label ipv4Label{&ipv4Layout, Size{48, 20}}; + CheckLabel ipv4{&ipv4Layout, Size{~0, 0}}; + + HorizontalLayout enabledLayout{this, Size{~0, 0}}; + Label enabledLabel{&enabledLayout, Size{48, 20}}; + CheckLabel enabled{&enabledLayout, Size{~0, 0}}; + + Label connectInfo{this, Size{~0, 30}, 5}; +}; + struct HomePanel : VerticalLayout { auto construct() -> void; @@ -368,6 +397,7 @@ struct SettingsWindow : Window { FirmwareSettings firmwareSettings; PathSettings pathSettings; DriverSettings driverSettings; + DebugSettings debugSettings; HomePanel homePanel; }; @@ -383,3 +413,4 @@ extern OptionSettings& optionSettings; extern FirmwareSettings& firmwareSettings; extern PathSettings& pathSettings; extern DriverSettings& driverSettings; +extern DebugSettings& debugSettings; \ No newline at end of file diff --git a/nall/gdb/Readme.md b/nall/gdb/Readme.md new file mode 100644 index 0000000000..dd81ff811c --- /dev/null +++ b/nall/gdb/Readme.md @@ -0,0 +1,227 @@ +# Debug Server + +The file `server.cpp` adds a gdb-server compatible with several GDB versions and IDEs like VScode and CLion.
+It is implemented as a standalone server independent of any specific system, and even ares itself.
+This allows for easy integration with systems without having to worry about the details of GDB itself.
+ +Managing the server itself, including the underlying TCP connection, is done by ares.
+System specific logic is handled via (optional) call-backs that a can be registered,
+as well as methods to report events to GDB. + +The overall design of this server is to be as neutral as possible.
+Meaning that things like stopping, stepping and reading memory should not affect the game.
+This is done to make sure that games behave the same as if they were running without a debugger, down to the cycle.
+ +## Integration Guide +This section describes how to implement the debugger for a system in ares.
+It should not be necessary to modify the server itself, or to know much about the GDB protocol.
+Simply registering callbacks and reporting events are enough to get the full set of features working.
+ +For a minimal working debugging session, register/memory reads and a way to report the PC are required.
+Although implementing as much as possible is recommended to make GDB more stable. + +Interactions with the server can be split in three categories: +- **Hooks:** lets GDB call functions in your ares system (e.g.: memory read) +- **Report-functions:** notify GDB about events (e.g.: exceptions) +- **Status-functions:** helper to check the GDB status (e.g.: are breakpoints set or not) + +Hooks can be set via setting the callbacks in `GDB::server.hooks.XXX`.
+Report functions are prefixed `GDB::server.reportXXX()`, and status functions a documented here separately.
+All hooks/report/status functions can be safely set or called even if the server is not running.
+ +As an example of a fictional system, this is what a memory read could look like: +```cpp +GDB::server.hooks.regRead = [](u32 regIdx) { + return hex(cpu.readRegister(regIdx), 16, '0'); +}; +``` +Or the main execution loop: +```cpp +while(!endOfFrame && GDB::server.reportPC(cpu.getPC())) { + cpu.step(); +} +``` + +For a real reference implementation, you can take a look at the N64 system.
+ +## Hooks + +### Memory Read - `read = (u64 address, u32 byteCount) -> string` +Reads `byteCount` bytes from `address` and returns them as a hex-string.
+Both the hex-encoding / single-byte reads are dictated by the GDB protocol.
+ +It is important to implement this in a neutral way: no exceptions and status changes.
+The GDB-client may issue reads from any address at any point while halted.
+If not handled properly, this can cause game crashes or different emulation behavior.
+ +If your system emulates cache, make sure to also handle this here.
+A read must be able to see the cache, but never cause a flush.
+ +Example response (reading 3 bytes): `A1B200` + +### Memory Write - `write = (u64 address, u32 unitSize, u64 value) -> void` +Writes `value` of byte-size `unitSize` to `address`.
+For example, writing a 32-bit value would issue a call like this: `write(0x80001230, 4, 0x0000000012345678)`.
+Contrary to read, this is not required to be neutral, and is allowed to cause exceptions.
+ +If your system emulates cache, make sure to also handle this here.
+The write should behave the same as if it was done via a CPU instruction, incl. flushing the cache if needed.
+ +### Normalize Address - `normalizeAddress = (u64 address) -> u64` +Normalizes an address into something that makes it comparable.
+This is only used for memory-watchpoints, which needs to compare what GDB send to what ares has internally.
+If your system has virtual addresses or masks, this should de-virtualize it.
+ +It's OK to not set this function, or to simply return the input untouched.
+In case that memory-watchpoint are not working, this is probably the place to fix it.
+ +Example implementation: +```cpp +GDB::server.hooks.normalizeAddress = [](u64 address) { + return address & 0x0FFF'FFFF; +}; +``` + +### Register Read - `regRead = (u32 regIdx) -> string` +Reads a single register at `regIdx` and returns it as a hex-string.
+The size of the hex-string is dictated by the specific architecture.
+ +Same as for memory-read, this must be implemented in a neutral way.
+Any invalid register can be returned as zero.
+ +Example response: `00000000000123AB` + +### Register Write - `regWrite = (u32 regIdx, u64 regValue) -> bool` + +Writes the value `regValue` to the register at `regIdx`.
+This write is allowed to have side effects.
+ +If the specific register is not writable or doesn't exist, `false` must be returned.
+On success, `true` must be returned.
+ +### Register Read (General) - `regReadGeneral = () -> string` +Most common way for GDB to read registers, this fetches all registers at once.
+The amount and order of registers is dictated by the specific architecture and GDB.
+When implementing this, GDB will usually complain if the order/size is incorrect.
+ +Same as for single reads, this must be implemented in a neutral way.
+ +Due to some issues regarding exception handling, you are given the option to return a different PC.
+This PC-override can be accessed via `GDB::server.getPcOverride() -> maybe`.
+The reasons for that are explained later in `reportSignal()`. + +Other than that, this can be implemented by looping over `hooks.regRead` and returning a concatenated string.
+Example response: `0000000000000000ffffffff8001000000000000000000420000000000000000000000000000000100000`... + +### Register Write (General) - `regWriteGeneral = (const string ®Data) -> void` +Writes all registers at once, this happens very rarely.
+The format of `regData` is the same as the response of `hooks.regReadGeneral`.
+Any register that is not writable or doesn't exist can be ignored.
+ +### Emulator Cache - `emuCacheInvalidate = (u64 address) -> void` +Should invalidate the emulator's cache at `address`.
+This is only necessary if you have a re-compiler or some form of instruction cache.
+ +### Target XML - `targetXML = () -> string` +Provides an XML description of the target system.
+The XML must not contain any newlines, and should be as short as possible.
+If the client has access to an `.elf` file, this will be mostly ignored. + +Example implementation: +```cpp +GDB::server.hooks.targetXML = []() -> string { + return "" + "mips:4000" + ""; +}; +``` +Documentation: https://sourceware.org/gdb/onlinedocs/gdb/Target-Description-Format.html#Target-Description-Format +
+ +## Report-Functions + +### Signal `reportSignal(Signal sig, u64 originPC) -> bool` +Reports a signal/exception `sig` that occurred at `originPC`.
+The architecture specific exception must be mapped to the enum in `Signal`.
+As a default, `Signal::TRAP` can be used.
+ +It will return `false` if the exception occurred while the game was already paused.
+This can be safely ignored.
+ +Since you may not be able to stop the execution before an exception occurs,
+The `originPC` value will be saved until the next time the game is resumed.
+An `hooks.regReadGeneral` implementation may use this to temp. return a different PC.
+This is done to allow GDB to halt on the causing instruction instead of the exception handler.
+If you can halt before an exception occurs, you can ignore this.
+ +### PC `reportPC(u64 pc) -> bool` +Sets a new PC, this will internally check for break- and watch-points.
+For convenience, it will return `false` if you should halt execution.
+If no debugger is running, it will always return `true`.
+ +You must only call this once per step, before the instruction at the given address gets executed.
+This also means a return value of `false` should make it halt before the instruction too.
+Once halted, it's safe to call this with the same PC each iteration.
+ +If a re-compiler is used, you may not want to call this for every single instruction.
+In that case take a look at `hasBreakpoints()` on how to optimize this.
+ +In case you need the information if a halt is required multiple times, use `GDB::server.isHalted()` instead.
+ +### Memory Read `reportMemRead(u64 address, u32 size) -> void` +Reports that a memory read occurred at `address` with `size` bytes.
+The passed address must be the raw un-normalized address.
+ +This is exclusively used for memory-watchpoints.
+No PC override mechanism is provided here, since it's breaks GDB.
+ +### Memory Write `reportMemWrite(u64 address, u32 size) -> void` +Exactly the same as `reportMemRead`, but for writes instead.
+The new value of that location will be automatically fetched by the client via a memory read,
+and is therefore not needed here. + +## Status-Functions + +### Halted `isHalted() -> bool` +Returns if the game should be currently halted or not.
+For convenience, the same value gets directly returned from `reportPC`.
+ +### Breakpoints `hasBreakpoints() -> bool` +Return `true` if at least one break- or watch-point is set.
+ +If you use a block-based re-compiler, stopping at every instruction may not be possible.
+You may use this information to force single-instruction execution in that case.
+If it returns false, you can safely resume using the block-based execution again.
+ +### PC Override `getPcOverride() -> maybe` +Returns a value if a PC override is active.
+As mentioned in `reportSignal()`, this can be used to return a different PC letting GDB halt at the causing instruction.
+You can safely call this function multiple times.
+Once a single step is taken, or the game is resumed, the override is cleared.
+ +## API Usage + +This API can also be used without GDB, which allows for more use cases.
+For example, you can write automated tooling or custom debugging UIs.
+To make access easier, no strict checks are performed.
+This means that the handshake protocol is optional, and checksums are not verified. + +### TCP +TCP connections behave the same way as a GDB session.
+The connection is kept open the entire time, and commands are sent sequentially, each waiting for an response before sending the next command. + +However, it is possible to send commands even if the game is still running, +this allows for real-time data access. + +Keep in minds that the server uses the RDP-commands, which are different from what you would type into a GDB client.
+For a list of all commands, see: https://sourceware.org/gdb/onlinedocs/gdb/Packets.html#Packets + +As an example, reading from memory would look like this: +``` +$m8020a504,100#00 +``` +This reads 100 bytes from address `0x8020a504`, the `$` and `#` define the message start/end, and the `00` is the checksum (which is not checked). + +One detail, and security check, is that new connections must send `+` as the first byte in the first payload.
+It's also a good idea to send a proper disconnect-command before closing the socket.
+Otherwise, the debugger will not accept new connections until a reset or restart occurs. \ No newline at end of file diff --git a/nall/gdb/server.cpp b/nall/gdb/server.cpp new file mode 100644 index 0000000000..0137bd0b73 --- /dev/null +++ b/nall/gdb/server.cpp @@ -0,0 +1,536 @@ +#include + +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 + inline auto addOrRemoveEntry(vector &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) { + printf("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(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+" : "", + 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"; + } + break; + + case 's': { + if(cmdName.size() > 1) { + u64 address = cmdName.slice(1).integer(); + printf("stepping at address unsupported, ignore (%016lX)\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 { + sendText("+"); // acknowledge always needed + } + + 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; + printf("GDB ending session, disconnecting client\n"); + 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 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(code), 2)}); + } + + auto Server::sendSignal(Signal code, const string& reason) -> void { + sendPayload({"T", hex(static_cast(code), 2), reason}); + } + + auto Server::sendPayload(const string& payload) -> void { + string msg{"+$", 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: TCP connect\n"); + resetClientData(); + hasActiveClient = true; + } + + auto Server::onDisconnect() -> void { + printf("GDB: TCP 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; + + currentThreadC = -1; + hasActiveClient = false; + handshakeDone = false; + requestDisconnect = false; + } + +}; \ No newline at end of file diff --git a/nall/gdb/server.hpp b/nall/gdb/server.hpp new file mode 100644 index 0000000000..8ecc4f2ee6 --- /dev/null +++ b/nall/gdb/server.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include + +namespace nall::GDB { + +enum class Signal : u8 { + HANGUP = 1, + INT = 2, + QUIT = 3, + ILLEGAL = 4, + TRAP = 5, + ABORT = 6, + SEGV = 11, +}; + +/** + * This implements a GDB server to handle remote debugging via a GDB client. + * It is both independent of ares itself and any specific system. + * Functionality is added by providing system-specific callbacks, as well as using the API inside a system. + * (See the Readme.md file for more information.) + * + * NOTE: + * Command handling and the overall logic was carefully designed to support as many IDEs and GDB versions as possible. + * Things can break very easily (and the official documentation may lie), so be very sure of any changes made here. + * If changes are necessary, please verify that the following gdb-versions / IDEs still work properly: + * + * GDB: + * - gdb-multiarch (the plain vanilla version exists in most package managers, supports a lot of arches) + * - mips64-ultra-elf-gdb (special MIPS build of gdb-multiarch, i do NOT recommend it, behaves strangely) + * - mingw-w64-x86_64-gdb (vanilla build for Windows/MSYS) + * + * IDEs/Tools: + * - GDB's CLI + * - VSCode + * - CLion (with bundled gdb-multiarch) + * + * For testing, please also check both linux and windows (WSL2). + * With WSL2, windows-ares is started from within WSL, while the debugger runs in linux. + * This can be easily tested with VSCode and it's debugger. + */ +class Server : public nall::TCPText::Server { + public: + + auto reset() -> void; + + struct { + // Memory + function read{}; + function write{}; + function normalizeAddress{}; + + // Registers + function regReadGeneral{}; + function regWriteGeneral{}; + function regRead{}; + function regWrite{}; + + // Emulator + function emuCacheInvalidate{}; + function targetXML{}; + + + } hooks{}; + + // Exception + auto reportSignal(Signal sig, u64 originPC) -> bool; + + // PC / Memory State Updates + auto reportPC(u64 pc) -> bool; + auto reportMemRead(u64 address, u32 size) -> void; + auto reportMemWrite(u64 address, u32 size) -> void; + + // Breakpoints / Watchpoints + auto isHalted() const { return forceHalt && haltSignalSent; } + auto hasBreakpoints() const { + return breakpoints || singleStepActive || watchpointRead || watchpointWrite; + } + + auto getPcOverride() const { return pcOverride; }; + + auto updateLoop() -> void; + auto getStatusText(u32 port, bool useIPv4) -> string; + + protected: + auto onText(string_view text) -> void override; + auto onConnect() -> void override; + auto onDisconnect() -> void override; + + private: + bool insideCommand{false}; + string cmdBuffer{""}; + + bool haltSignalSent{false}; // marks if a signal as been sent for new halts (force-halt and breakpoints) + bool forceHalt{false}; // forces a halt despite no breakpoints being hit + bool singleStepActive{false}; + + bool nonStopMode{false}; // (NOTE: Not working for now), gets set if gdb wants to switch over to async-messaging + bool handshakeDone{false}; // set to true after a few handshake commands, used to prevent exception-reporting until client is ready + bool requestDisconnect{false}; // set to true if the client decides it wants to disconnect + + bool hasActiveClient{false}; + u32 messageCount{0}; // message count per update loop + s32 currentThreadC{-1}; // selected thread for the next 'c' command + + u64 currentPC{0}; + maybe pcOverride{0}; // temporary override to handle edge-cases for exceptions/watchpoints + + // client-state: + vector breakpoints{}; + vector watchpointRead{}; + vector watchpointWrite{}; + + auto processCommand(const string& cmd, bool &shouldReply) -> string; + auto resetClientData() -> void; + + auto reportWatchpoint(const Watchpoint &wp, u64 address) -> void; + + auto sendPayload(const string& payload) -> void; + auto sendSignal(Signal code) -> void; + auto sendSignal(Signal code, const string& reason) -> void; + + auto haltProgram() -> void; + auto resumeProgram() -> void; +}; + +extern Server server; + +} \ No newline at end of file diff --git a/nall/gdb/watchpoint.hpp b/nall/gdb/watchpoint.hpp new file mode 100644 index 0000000000..b60d32657d --- /dev/null +++ b/nall/gdb/watchpoint.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +namespace nall::GDB { + + enum class WatchpointType : u32 { + WRITE, READ, ACCESS + }; + + struct Watchpoint { + u64 addressStart{0}; + u64 addressEnd{0}; + u64 addressStartOrg{0}; // un-normalized address, GDB needs this + WatchpointType type{}; + + auto operator==(const Watchpoint& w) const { + return addressStart == w.addressStart && addressEnd == w.addressEnd + && addressStartOrg == w.addressStartOrg && type == w.type; + } + + auto hasOverlap(u64 start, u64 end) const { + return (end >= addressStart) && (start <= addressEnd); + } + + auto getTypePrefix() const -> string { + if(type == WatchpointType::WRITE)return "watch:"; + if(type == WatchpointType::READ)return "rwatch:"; + return "awatch:"; + } + }; +} \ No newline at end of file diff --git a/nall/nall.cpp b/nall/nall.cpp index 33e6ef9d3f..890c9882e6 100644 --- a/nall/nall.cpp +++ b/nall/nall.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include //currently unused by ares //#include //#include diff --git a/nall/string/format.hpp b/nall/string/format.hpp index 8400811951..5aeca7e1ee 100644 --- a/nall/string/format.hpp +++ b/nall/string/format.hpp @@ -108,6 +108,12 @@ template inline auto hex(T value, long precision, char padchar) -> s return buffer; } +// ~3x faster than method above with larger payloads of single individual bytes +inline auto hexByte(char* out, u8 value) -> void { + out[0] = "0123456789ABCDEF"[value >> 4]; + out[1] = "0123456789ABCDEF"[value & 0xF]; +} + template inline auto octal(T value, long precision, char padchar) -> string { string buffer; buffer.resize(sizeof(T) * 3); diff --git a/nall/tcptext/tcp-socket.cpp b/nall/tcptext/tcp-socket.cpp new file mode 100644 index 0000000000..dc744c0921 --- /dev/null +++ b/nall/tcptext/tcp-socket.cpp @@ -0,0 +1,298 @@ +#include +#include +#include + +#if defined(PLATFORM_WINDOWS) + #include +#else + #include +#endif + +struct sockaddr_in; +struct sockaddr_in6; + +namespace { + constexpr bool TCP_LOG_MESSAGES = false; + + constexpr u32 TCP_BUFFER_SIZE = 1024 * 16; + constexpr u32 CLIENT_SLEEP_MS = 10; // ms to sleep while checking for new clients + constexpr u32 CYCLES_BEFORE_SLEEP = 100; // how often to do a send/receive check before a sleep + constexpr u32 RECEIVE_TIMEOUT_SEC = 1; // only important for latency of disconnecting clients, reads are blocming anyways + + // A few platform specific socket functions: + // (In general, windows+linux share the same names, yet they behave differenly) + auto socketSetBlockingMode(s32 socket, bool isBlocking) -> bool + { + if(socket < 0)return false; + #if defined(O_NONBLOCK) // Linux + auto oldFlags = fcntl(socket, F_GETFL, 0); + auto newFlags = isBlocking ? (oldFlags ^ O_NONBLOCK) : (oldFlags | O_NONBLOCK); + printf("TCP: set socket (linux) to blocking=%d\n", isBlocking ? 1 : 0); + return fcntl(socket, F_SETFL, newFlags) == 0; + #elif defined(FIONBIO) // Windows + u_long state = isBlocking ? 0 : 1; + printf("TCP: set socket (windows) to blocking=%d\n", isBlocking ? 1 : 0); + return ioctlsocket(socket, FIONBIO, &state) == NO_ERROR; + #endif + } + + auto socketShutdown(s32 socket) { + if(socket < 0)return; + #if defined(SD_BOTH) // Windows + ::shutdown(socket, SD_BOTH); + #elif defined(SHUT_RDWR) // Linux, Mac + ::shutdown(socket, SHUT_RDWR); + #endif + } + + auto socketClose(s32 socket) { + if(socket < 0)return; + #if defined(PLATFORM_WINDOWS) + ::closesocket(socket); + #else + ::close(socket); + #endif + } +} + +namespace nall::TCP { + +NALL_HEADER_INLINE auto Socket::getURL(u32 port, bool useIPv4) const -> string { + return {useIPv4 ? "127.0.0.1:" : "[::1]:", port}; +} + +NALL_HEADER_INLINE auto Socket::open(u32 port, bool useIPv4) -> bool { + stopServer = false; + + auto url = getURL(port, useIPv4); + printf("Opening TCP-server on %s\n", url.data()); + + auto threadServer = std::thread([this, port, useIPv4]() { + serverRunning = true; + + fdServer = socket(useIPv4 ? AF_INET : AF_INET6, SOCK_STREAM, 0); + if(fdServer < 0) { + serverRunning = false; + return; + } + + { + s32 valueOn = 1; + #if defined(SO_NOSIGPIPE) //BSD, OSX + setsockopt(fdServer, SOL_SOCKET, SO_NOSIGPIPE, &valueOn, sizeof(s32)); + #endif + + #if defined(SO_REUSEADDR) //BSD, Linux, OSX + setsockopt(fdServer, SOL_SOCKET, SO_REUSEADDR, &valueOn, sizeof(s32)); + #endif + + #if defined(SO_REUSEPORT) //BSD, OSX + setsockopt(fdServer, SOL_SOCKET, SO_REUSEPORT, &valueOn, sizeof(s32)); + #endif + + #if defined(TCP_NODELAY) + printf("TCP: setting TCP_NODELAY\n"); + setsockopt(fdServer, IPPROTO_TCP, TCP_NODELAY, &valueOn, sizeof(s32)); + #endif + + if(!socketSetBlockingMode(fdServer, true)) { + printf("TCP: failed to set to blocking mode!\n"); + } + + #if defined(SO_RCVTIMEO) + #if defined(PLATFORM_WINDOWS) + DWORD rcvTimeMs = 1000 * RECEIVE_TIMEOUT_SEC; + printf("TCP: setting SO_RCVTIMEO to %ldms\n", rcvTimeMs); + setsockopt(fdServer, SOL_SOCKET, SO_RCVTIMEO, &rcvTimeMs, sizeof(rcvTimeMs)); + #else + struct timeval rcvtimeo; + rcvtimeo.tv_sec = RECEIVE_TIMEOUT_SEC; + rcvtimeo.tv_usec = 0; + printf("TCP: setting SO_RCVTIMEO to %lds %ldms\n", rcvtimeo.tv_sec, rcvtimeo.tv_usec); + setsockopt(fdServer, SOL_SOCKET, SO_RCVTIMEO, &rcvtimeo, sizeof(rcvtimeo)); + #endif + #endif + } + + s32 bindRes; + if(useIPv4) { + sockaddr_in serverAddrV4{}; + serverAddrV4.sin_family = AF_INET; + serverAddrV4.sin_addr.s_addr = htonl(INADDR_ANY); + serverAddrV4.sin_port = htons(port); + + bindRes = ::bind(fdServer, (sockaddr*)&serverAddrV4, sizeof(serverAddrV4)) < 0; + } else { + sockaddr_in6 serverAddrV6{}; + serverAddrV6.sin6_family = AF_INET6; + serverAddrV6.sin6_addr = in6addr_loopback; + serverAddrV6.sin6_port = htons(port); + + bindRes = ::bind(fdServer, (sockaddr*)&serverAddrV6, sizeof(serverAddrV6)) < 0; + } + + if(bindRes < 0 || listen(fdServer, 1) < 0) { + printf("error binding socket on port %d! (%s)\n", port, strerror(errno)); + stopServer = true; + } + + while(!stopServer) + { + // scan for new connections + if(fdClient < 0) { + fdClient = ::accept(fdServer, nullptr, nullptr); + if(fdClient >= 0) { + printf("TCP: Client connected!\n"); + } + } + + // Kick client if we need to + if(fdClient >= 0 && wantKickClient) { + socketClose(fdClient); + fdClient = -1; + wantKickClient = false; + onDisconnect(); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(CLIENT_SLEEP_MS)); + } + + printf("Stopping TCP-server...\n"); + + socketClose(fdClient); + socketClose(fdServer); + fdServer = -1; + fdClient = -1; + + wantKickClient = false; + + printf("TCP-server stopped\n"); + serverRunning = false; + }); + + auto threadSend = std::thread([this]() + { + vector localSendBuffer{}; + u32 cycles = 0; + + while(!stopServer) + { + if(fdClient < 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(CLIENT_SLEEP_MS)); + continue; + } + + { // copy send-data to minimize lock time + std::lock_guard guard{sendBufferMutex}; + if(sendBuffer.size() > 0) { + localSendBuffer = sendBuffer; + sendBuffer.resize(0); + } + } + + // send data + if(localSendBuffer.size() > 0) { + auto bytesWritten = send(fdClient, localSendBuffer.data(), localSendBuffer.size(), 0); + if(bytesWritten < localSendBuffer.size()) { + printf("Error sending data! (%s)\n", strerror(errno)); + } + + if constexpr(TCP_LOG_MESSAGES) { + printf("%.4f | TCP >: [%ld]: %.*s\n", (f64)chrono::millisecond() / 1000.0, localSendBuffer.size(), localSendBuffer.size() > 100 ? 100 : (int)localSendBuffer.size(), (char*)localSendBuffer.data()); + } + + localSendBuffer.resize(0); + cycles = 0; // sending once has a good chance of sending more -> reset sleep timer + } + + if(cycles++ >= CYCLES_BEFORE_SLEEP) { + std::this_thread::sleep_for(std::chrono::microseconds(1)); + cycles = 0; + } + } + }); + + auto threadReceive = std::thread([this]() + { + u8 packet[TCP_BUFFER_SIZE]{0}; + + while(!stopServer) + { + if(fdClient < 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(CLIENT_SLEEP_MS)); + continue; + } + + // receive data from connected clients + s32 length = recv(fdClient, packet, TCP_BUFFER_SIZE, MSG_NOSIGNAL); + if(length > 0) { + std::lock_guard guard{receiveBufferMutex}; + auto oldSize = receiveBuffer.size(); + receiveBuffer.resize(oldSize + length); + memcpy(receiveBuffer.data() + oldSize, packet, length); + + if constexpr(TCP_LOG_MESSAGES) { + printf("%.4f | TCP <: [%d]: %.*s ([%d]: %.*s)\n", (f64)chrono::millisecond() / 1000.0, length, length, (char*)receiveBuffer.data(), length, length, (char*)packet); + } + } + } + }); + + threadServer.detach(); + threadSend.detach(); + threadReceive.detach(); + + return true; +} + +NALL_HEADER_INLINE auto Socket::close(bool notifyHandler) -> void { + printf("TCP: schedule socket-close\n"); + stopServer = true; + + // we have to forcefully shut it down here, since otherwise accept() would hang causing a UI crash + socketShutdown(fdServer); + socketClose(fdClient); + socketClose(fdServer); + fdServer = -1; + fdClient = -1; + + while(serverRunning) { + printf("TCP: waiting for shutdown...\n"); + std::this_thread::sleep_for(std::chrono::milliseconds(250)); // wait for other threads to stop + } + + if(notifyHandler) { + onDisconnect(); // don't call this in destructor, it's virtual + } + printf("TCP: socket closed!\n"); +} + +NALL_HEADER_INLINE auto Socket::update() -> void { + vector data{}; + + { // local copy, minimize lock time + std::lock_guard guard{receiveBufferMutex}; + if(receiveBuffer.size() > 0) { + data = receiveBuffer; + receiveBuffer.resize(0); + } + } + + if(data.size() > 0) { + onData(data); + } +} + +NALL_HEADER_INLINE auto Socket::disconnectClient() -> void { + printf("TCP: schedule client diconnect\n"); + wantKickClient = true; +} + +NALL_HEADER_INLINE auto Socket::sendData(const u8* data, u32 size) -> void { + std::lock_guard guard{sendBufferMutex}; + u32 oldSize = sendBuffer.size(); + sendBuffer.resize(oldSize + size); + memcpy(sendBuffer.data() + oldSize, data, size); +} + +} diff --git a/nall/tcptext/tcp-socket.hpp b/nall/tcptext/tcp-socket.hpp new file mode 100644 index 0000000000..467b563421 --- /dev/null +++ b/nall/tcptext/tcp-socket.hpp @@ -0,0 +1,65 @@ +#pragma once + +/** + * Opens a TCP server with callbacks to send and receive data. + * + * This spawns 3 new threads: + * threadServer: listens for new connections, kicks connections + * threadSend: sends data to the client + * threadReceive: receives data from the client + * + * Each contains it's own loop including sleeps to not use too much CPU. + * The exception is threadReceive which relies on the blocking recv() call (kernel wakes it up again). + * + * Incoming and outgoing data is synchronized using mutexes, + * and put into buffers that are shared with the main thread. + * Meaning, the thread that calls 'update()' with also be the one that gets 'onData()' calls. + * No additional synchronization is needed. + * + * NOTE: if you work on the loop/sleeps, make sure to test CPU usage and package-latency. + */ +namespace nall::TCP { + +class Socket { + public: + auto open(u32 port, bool useIPv4) -> bool; + auto close(bool notifyHandler = true) -> void; + + auto disconnectClient() -> void; + + auto isStarted() const -> bool { return fdServer >= 0; } + auto hasClient() const -> bool { return fdClient >= 0; } + + auto getURL(u32 port, bool useIPv4) const -> string; + + ~Socket() { close(false); } + + protected: + auto update() -> void; + + auto sendData(const u8* data, u32 size) -> void; + virtual auto onData(const vector &data) -> void = 0; + + virtual auto onConnect() -> void = 0; + virtual auto onDisconnect() -> void = 0; + + private: + std::atomic stopServer{false}; // set to true to let the server-thread know to stop. + std::atomic serverRunning{false}; // signals the current state of the server-thread + std::atomic wantKickClient{false}; // set to true to let server know to disconnect the current client (if conn.) + + std::atomic fdServer{-1}; + std::atomic fdClient{-1}; + + vector receiveBuffer{}; + std::mutex receiveBufferMutex{}; + + vector sendBuffer{}; + std::mutex sendBufferMutex{}; +}; + +} + +#if defined(NALL_HEADER_ONLY) + #include +#endif diff --git a/nall/tcptext/tcptext-server.cpp b/nall/tcptext/tcptext-server.cpp new file mode 100644 index 0000000000..4d0581f9d6 --- /dev/null +++ b/nall/tcptext/tcptext-server.cpp @@ -0,0 +1,29 @@ +#include + +namespace nall::TCPText { + NALL_HEADER_INLINE auto Server::sendText(const string &text) -> void { + sendData((const u8*)text.data(), text.size()); + } + + NALL_HEADER_INLINE auto Server::onData(const vector &data) -> void { + string_view dataStr((const char*)data.data(), (u32)data.size()); + + if(!hadHandshake) { + hadHandshake = true; + + // This is a security check for browsers. + // Any website can request localhost via JS or HTML, while it can't see the result, + // GDB will receive the data and commands could be injected (true for all GDB-servers). + // Since all HTTP requests start with headers, we can simply block anything that doesn't start like a GDB client. + if(dataStr[0] != '+') { + printf("Non-GDB client detected (message: %s), disconnect client\n", dataStr.data()); + disconnectClient(); + return; + } + + onConnect(); + } + + onText(dataStr); + } +} diff --git a/nall/tcptext/tcptext-server.hpp b/nall/tcptext/tcptext-server.hpp new file mode 100644 index 0000000000..8299bcdea6 --- /dev/null +++ b/nall/tcptext/tcptext-server.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +/** + * Provides text-based TCP server on top of the Socket. + * This handles incoming messages and can send data back to the client. + */ +namespace nall::TCPText { + +class Server : public TCP::Socket { + public: + bool hadHandshake{false}; + + protected: + auto onData(const vector &data) -> void override; + + auto sendText(const string &text) -> void; + virtual auto onText(string_view text) -> void = 0; +}; + +} + +#if defined(NALL_HEADER_ONLY) + #include +#endif diff --git a/nall/vector.hpp b/nall/vector.hpp index ed52da7a5d..a8c129554e 100644 --- a/nall/vector.hpp +++ b/nall/vector.hpp @@ -124,6 +124,7 @@ struct vector_base { auto find(const function& comparator) -> maybe; auto find(const T& value) const -> maybe; auto findSorted(const T& value) const -> maybe; + auto contains(const T& value) const -> bool; auto foreach(const function& callback) -> void; auto foreach(const function& callback) -> void; diff --git a/nall/vector/utility.hpp b/nall/vector/utility.hpp index 14f70f9599..670ff97392 100644 --- a/nall/vector/utility.hpp +++ b/nall/vector/utility.hpp @@ -36,6 +36,13 @@ template auto vector::findSorted(const T& value) const -> maybe auto vector::contains(const T& value) const -> bool { + for(const auto &v : *this) { + if(v == value) return true; + } + return false; +} + template auto vector::foreach(const function& callback) -> void { for(u64 n : range(size())) callback(_pool[n]); }