diff --git a/CMakeModules/GenerateSettingKeys.cmake b/CMakeModules/GenerateSettingKeys.cmake index 988c51575..96a131f72 100644 --- a/CMakeModules/GenerateSettingKeys.cmake +++ b/CMakeModules/GenerateSettingKeys.cmake @@ -116,6 +116,7 @@ foreach(KEY IN ITEMS "log_filter" "log_regex_filter" "toggle_unique_data_console_type" + "break_on_unmapped_memory_access" "use_integer_scaling" "layouts_to_cycle" "camera_inner_flip" diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index 62fb24b9b..aa2c90228 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -3880,6 +3880,18 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det .c_str()); error_severity_icon = QMessageBox::Icon::Critical; can_continue = false; + } else if (result == Core::System::ResultStatus::ErrorCoreExceptionRaised) { + title = tr("An exception occurred"); + message = tr("An exception occurred while executing the emulated application.\n\n"); + message += QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Critical; + can_continue = false; + } else if (result == Core::System::ResultStatus::ErrorMemoryExceptionRaised) { + title = tr("An invalid memory access occurred"); + message = + tr("An invalid memory access occurred while executing the emulated application.\n\n"); + message += QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Critical; } else { title = tr("Fatal Error"); message = tr("A fatal error occurred. " diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index c797f61f5..89e6e8b51 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -510,6 +510,7 @@ void QtConfig::ReadDebuggingValues() { ReadBasicSetting(Settings::values.instant_debug_log); ReadBasicSetting(Settings::values.enable_rpc_server); ReadBasicSetting(Settings::values.toggle_unique_data_console_type); + ReadBasicSetting(Settings::values.break_on_unmapped_memory_access); qt_config->beginGroup(QStringLiteral("LLE")); for (const auto& service_module : Service::service_module_map) { @@ -1096,6 +1097,7 @@ void QtConfig::SaveDebuggingValues() { WriteBasicSetting(Settings::values.instant_debug_log); WriteBasicSetting(Settings::values.enable_rpc_server); WriteBasicSetting(Settings::values.toggle_unique_data_console_type); + WriteBasicSetting(Settings::values.break_on_unmapped_memory_access); qt_config->beginGroup(QStringLiteral("LLE")); for (const auto& service_module : Settings::values.lle_modules) { diff --git a/src/citra_qt/configuration/configure_debug.cpp b/src/citra_qt/configuration/configure_debug.cpp index 0fe4f49cf..db09c7b93 100644 --- a/src/citra_qt/configuration/configure_debug.cpp +++ b/src/citra_qt/configuration/configure_debug.cpp @@ -112,6 +112,8 @@ void ConfigureDebug::SetConfiguration() { #endif // !ENABLE_SCRIPTING ui->toggle_unique_data_console_type->setChecked( Settings::values.toggle_unique_data_console_type.GetValue()); + ui->break_on_unmapped_memory_access->setChecked( + Settings::values.break_on_unmapped_memory_access.GetValue()); ui->toggle_renderer_debug->setChecked(Settings::values.renderer_debug.GetValue()); ui->toggle_dump_command_buffers->setChecked(Settings::values.dump_command_buffers.GetValue()); @@ -153,6 +155,8 @@ void ConfigureDebug::ApplyConfiguration() { Settings::values.enable_rpc_server = ui->enable_rpc_server->isChecked(); Settings::values.toggle_unique_data_console_type = ui->toggle_unique_data_console_type->isChecked(); + Settings::values.break_on_unmapped_memory_access = + ui->break_on_unmapped_memory_access->isChecked(); Settings::values.renderer_debug = ui->toggle_renderer_debug->isChecked(); Settings::values.dump_command_buffers = ui->toggle_dump_command_buffers->isChecked(); Settings::values.instant_debug_log = ui->instant_debug_log->isChecked(); @@ -178,6 +182,7 @@ void ConfigureDebug::SetupPerGameUI() { ui->groupBox_2->setVisible(false); ui->enable_rpc_server->setVisible(false); ui->toggle_unique_data_console_type->setVisible(false); + ui->break_on_unmapped_memory_access->setVisible(false); ui->toggle_cpu_jit->setVisible(false); } diff --git a/src/citra_qt/configuration/configure_debug.ui b/src/citra_qt/configuration/configure_debug.ui index 990835a80..d9f66de38 100644 --- a/src/citra_qt/configuration/configure_debug.ui +++ b/src/citra_qt/configuration/configure_debug.ui @@ -299,6 +299,16 @@ + + + + <html><head/><body><p>Pauses emulation and shows an error message if an unmapped memory access is detected.</p></body></html> + + + Break on unmapped memory access + + + diff --git a/src/common/memory_ref.h b/src/common/memory_ref.h index f5935bf4c..3e4ddc67f 100644 --- a/src/common/memory_ref.h +++ b/src/common/memory_ref.h @@ -1,4 +1,4 @@ -// Copyright 2020 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -153,7 +153,9 @@ private: void serialize(Archive& ar, const unsigned int) { ar & backing_mem; ar & offset; - Init(); + if (Archive::is_loading::value) { + Init(); + } } friend class boost::serialization::access; }; diff --git a/src/common/settings.cpp b/src/common/settings.cpp index db40cc951..df8451e28 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -166,6 +166,8 @@ void LogSettings() { log_setting("Debugging_InstantDebugLog", values.instant_debug_log.GetValue()); log_setting("Debugging_ToggleUniqueDataConsoleType", values.toggle_unique_data_console_type.GetValue()); + log_setting("Debugging_BreakOnUnmappedMemoryAccess", + values.break_on_unmapped_memory_access.GetValue()); } bool IsConfiguringGlobal() { diff --git a/src/common/settings.h b/src/common/settings.h index 730128885..c5c924201 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -643,6 +643,7 @@ struct Values { Setting instant_debug_log{false, Keys::instant_debug_log}; Setting enable_rpc_server{false, Keys::enable_rpc_server}; Setting toggle_unique_data_console_type{false, Keys::toggle_unique_data_console_type}; + Setting break_on_unmapped_memory_access{false, Keys::break_on_unmapped_memory_access}; // Miscellaneous Setting log_filter{"*:Info", Keys::log_filter}; diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index 117c19df2..64effc9e2 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -122,8 +122,7 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P _CompleteFilename += _Filename; } -std::vector SplitString(const std::string& str, const char delim) { - std::istringstream iss(str); +static std::vector SplitString(std::istringstream& iss, const char delim) { std::vector output(1); while (std::getline(iss, *output.rbegin(), delim)) { @@ -134,6 +133,16 @@ std::vector SplitString(const std::string& str, const char delim) { return output; } +std::vector SplitString(std::string_view str, const char delim) { + std::istringstream iss{std::string(str)}; + return SplitString(iss, delim); +} + +std::vector SplitString(const std::string& str, const char delim) { + std::istringstream iss(str); + return SplitString(iss, delim); +} + std::string TabsToSpaces(int tab_size, std::string in) { std::size_t i = 0; diff --git a/src/common/string_util.h b/src/common/string_util.h index 033c3ddb1..5c6d0bdac 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -36,6 +36,7 @@ namespace Common { [[nodiscard]] bool EndsWith(const std::string& value, const std::string& ending); +[[nodiscard]] std::vector SplitString(std::string_view str, const char delim); [[nodiscard]] std::vector SplitString(const std::string& str, const char delim); // "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe" diff --git a/src/core/arm/arm_interface.h b/src/core/arm/arm_interface.h index 1f6ddc9e9..0aeb10de2 100644 --- a/src/core/arm/arm_interface.h +++ b/src/core/arm/arm_interface.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -198,12 +198,28 @@ public: return id; } + /** + * Sets the core to not being runnable until its break condition is handled. + */ + void SetBreakFlag() { + break_flag = true; + } + + /* + * Sets the core being runnable after its break condition was handled. + */ + void ClearBreakFlag() { + break_flag = false; + } + protected: // This us used for serialization. Returning nullptr is valid if page tables are not used. virtual std::shared_ptr GetPageTable() const = 0; std::shared_ptr timer; + bool break_flag{}; + private: u32 id; @@ -213,6 +229,7 @@ private: void save(Archive& ar, const unsigned int file_version) const { ar << timer; ar << id; + ar << break_flag; const auto page_table = GetPageTable(); ar << page_table; for (int i = 0; i < 15; i++) { @@ -258,6 +275,7 @@ private: ClearInstructionCache(); ar >> timer; ar >> id; + ar >> break_flag; std::shared_ptr page_table{}; ar >> page_table; SetPageTable(page_table); diff --git a/src/core/arm/dynarmic/arm_dynarmic.cpp b/src/core/arm/dynarmic/arm_dynarmic.cpp index 2cdc9c119..002c5488d 100644 --- a/src/core/arm/dynarmic/arm_dynarmic.cpp +++ b/src/core/arm/dynarmic/arm_dynarmic.cpp @@ -1,7 +1,8 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include @@ -17,6 +18,14 @@ #include "core/hle/kernel/svc.h" #include "core/memory.h" +#ifndef SIGILL +constexpr u32 SIGILL = 4; +#endif + +#ifndef SIGTRAP +constexpr u32 SIGTRAP = 5; +#endif + namespace Core { class DynarmicUserCallbacks final : public Dynarmic::A32::UserCallbacks { @@ -25,6 +34,21 @@ public: : parent(parent), svc_context(parent.system), memory(parent.memory) {} ~DynarmicUserCallbacks() = default; + std::optional MemoryReadCode(VAddr vaddr) override { + constexpr VAddr low_page_limit = 0x1000; + + // This check prevents a common cascading error that results from the + // memory system allowing unmapped memory accesses (in some situations, + // a vtable is read from an invalid address and execution jumps to the + // zero page). On real hw, it's not normally possible to map the zero page. + if (vaddr < low_page_limit) [[unlikely]] { + LOG_CRITICAL(Debug, "Tried to read code from low address 0x{:08x}", vaddr); + return {}; + } + + return MemoryRead32(vaddr); + } + std::uint8_t MemoryRead8(VAddr vaddr) override { return memory.Read8(vaddr); } @@ -83,9 +107,8 @@ public: break; case Dynarmic::A32::Exception::Breakpoint: if (GDBStub::IsConnected()) { - parent.jit->HaltExecution(); parent.SetPC(pc); - parent.ServeBreak(); + parent.ServeBreak(SIGTRAP); return; } break; @@ -99,11 +122,19 @@ public: case Dynarmic::A32::Exception::PreloadInstruction: return; } - for (int i = 0; i < 16; i++) { - LOG_CRITICAL(Debug, "r{:02d} = {:08X}", i, parent.GetReg(i)); + + parent.SetPC(pc); + if (GDBStub::IsConnected()) { + parent.ServeBreak(SIGILL); + } else { + std::string error; + for (int i = 0; i < 16; i++) { + error += fmt::format("r{:02d} = {:08X}\n", i, parent.GetReg(i)); + } + error += fmt::format("ExceptionRaised(exception = {}, pc = {:08X})", exception, pc); + parent.system.SetStatus(Core::System::ResultStatus::ErrorCoreExceptionRaised, + error.c_str()); } - ASSERT_MSG(false, "ExceptionRaised(exception = {}, pc = {:08X}, code = {:08X})", exception, - pc, MemoryReadCode(pc).value()); } void AddTicks(std::uint64_t ticks) override { @@ -138,16 +169,19 @@ MICROPROFILE_DEFINE(ARM_Jit, "ARM JIT", "ARM JIT", MP_RGB(255, 64, 64)); void ARM_Dynarmic::Run() { ASSERT(memory.GetCurrentPageTable() == current_page_table); MICROPROFILE_SCOPE(ARM_Jit); + if (break_flag) [[unlikely]] { + return; + } jit->Run(); } void ARM_Dynarmic::Step() { - jit->Step(); - - if (GDBStub::IsConnected()) { - ServeBreak(); + if (break_flag) [[unlikely]] { + return; } + + jit->Step(); } void ARM_Dynarmic::SetPC(u32 pc) { @@ -294,11 +328,8 @@ void ARM_Dynarmic::SetPageTable(const std::shared_ptr& page_t jits.emplace(current_page_table, std::move(new_jit)); } -void ARM_Dynarmic::ServeBreak() { - Kernel::Thread* thread = system.Kernel().GetCurrentThreadManager().GetCurrentThread(); - SaveContext(thread->context); - GDBStub::Break(); - GDBStub::SendTrap(thread, 5); +void ARM_Dynarmic::ServeBreak(int signal) { + GDBStub::Break(signal); } std::unique_ptr ARM_Dynarmic::MakeJit() { diff --git a/src/core/arm/dynarmic/arm_dynarmic.h b/src/core/arm/dynarmic/arm_dynarmic.h index ec0c13390..e854df376 100644 --- a/src/core/arm/dynarmic/arm_dynarmic.h +++ b/src/core/arm/dynarmic/arm_dynarmic.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -60,7 +60,7 @@ protected: std::shared_ptr GetPageTable() const override; private: - void ServeBreak(); + void ServeBreak(int signal); friend class DynarmicUserCallbacks; Core::System& system; diff --git a/src/core/arm/dyncom/arm_dyncom.cpp b/src/core/arm/dyncom/arm_dyncom.cpp index c9bb66e38..63db2f9c4 100644 --- a/src/core/arm/dyncom/arm_dyncom.cpp +++ b/src/core/arm/dyncom/arm_dyncom.cpp @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -24,10 +24,16 @@ ARM_DynCom::ARM_DynCom(Core::System& system_, Memory::MemorySystem& memory, ARM_DynCom::~ARM_DynCom() {} void ARM_DynCom::Run() { + if (break_flag) [[unlikely]] { + return; + } ExecuteInstructions(std::max(timer->GetDowncount(), 0)); } void ARM_DynCom::Step() { + if (break_flag) [[unlikely]] { + return; + } ExecuteInstructions(1); } diff --git a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp index 862c422bc..794197590 100644 --- a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp +++ b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2012 Michael Kang, 2014 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -954,11 +958,9 @@ unsigned InterpreterMainLoop(ARMul_State* cpu) { #define GDB_BP_CHECK \ cpu->Cpsr &= ~(1 << 5); \ cpu->Cpsr |= cpu->TFlag << 5; \ - if (GDBStub::IsServerEnabled()) { \ - if (GDBStub::IsMemoryBreak()) { \ - goto END; \ - } else if (breakpoint_data.type != GDBStub::BreakpointType::None && \ - PC == breakpoint_data.address) { \ + if (GDBStub::IsServerEnabled()) [[unlikely]] { \ + if (breakpoint_data.type != GDBStub::BreakpointType::None && \ + PC == breakpoint_data.address) { \ cpu->RecordBreak(breakpoint_data); \ goto END; \ } \ diff --git a/src/core/arm/skyeye_common/armstate.cpp b/src/core/arm/skyeye_common/armstate.cpp index 8ea916dbf..d7349b59a 100644 --- a/src/core/arm/skyeye_common/armstate.cpp +++ b/src/core/arm/skyeye_common/armstate.cpp @@ -1,8 +1,9 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include +#include #include "common/logging/log.h" #include "common/swap.h" #include "core/arm/skyeye_common/armstate.h" @@ -10,6 +11,10 @@ #include "core/core.h" #include "core/memory.h" +#ifndef SIGTRAP +constexpr u32 SIGTRAP = 5; +#endif + ARMul_State::ARMul_State(Core::System& system_, Memory::MemorySystem& memory_, PrivilegeMode initial_mode) : system{system_}, memory{memory_} { @@ -182,26 +187,12 @@ void ARMul_State::ResetMPCoreCP15Registers() { CP15[CP15_MAIN_TLB_LOCKDOWN_ATTRIBUTE] = 0x00000000; CP15[CP15_TLB_DEBUG_CONTROL] = 0x00000000; } -#ifdef ANDROID -static void CheckMemoryBreakpoint(u32 address, GDBStub::BreakpointType type) {} -#else -static void CheckMemoryBreakpoint(u32 address, GDBStub::BreakpointType type) { - if (GDBStub::IsServerEnabled() && GDBStub::CheckBreakpoint(address, type)) { - LOG_DEBUG(Debug, "Found memory breakpoint @ {:08x}", address); - GDBStub::Break(true); - } -} -#endif u8 ARMul_State::ReadMemory8(u32 address) const { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read); - return memory.Read8(address); } u16 ARMul_State::ReadMemory16(u32 address) const { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read); - u16 data = memory.Read16(address); if (InBigEndianMode()) @@ -211,8 +202,6 @@ u16 ARMul_State::ReadMemory16(u32 address) const { } u32 ARMul_State::ReadMemory32(u32 address) const { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read); - u32 data = memory.Read32(address); if (InBigEndianMode()) @@ -222,8 +211,6 @@ u32 ARMul_State::ReadMemory32(u32 address) const { } u64 ARMul_State::ReadMemory64(u32 address) const { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read); - u64 data = memory.Read64(address); if (InBigEndianMode()) @@ -233,14 +220,10 @@ u64 ARMul_State::ReadMemory64(u32 address) const { } void ARMul_State::WriteMemory8(u32 address, u8 data) { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write); - memory.Write8(address, data); } void ARMul_State::WriteMemory16(u32 address, u16 data) { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write); - if (InBigEndianMode()) data = Common::swap16(data); @@ -248,8 +231,6 @@ void ARMul_State::WriteMemory16(u32 address, u16 data) { } void ARMul_State::WriteMemory32(u32 address, u32 data) { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write); - if (InBigEndianMode()) data = Common::swap32(data); @@ -257,8 +238,6 @@ void ARMul_State::WriteMemory32(u32 address, u32 data) { } void ARMul_State::WriteMemory64(u32 address, u64 data) { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write); - if (InBigEndianMode()) data = Common::swap64(data); @@ -609,12 +588,8 @@ void ARMul_State::ServeBreak() { DEBUG_ASSERT(Reg[15] == last_bkpt.address); } - Kernel::Thread* thread = system.Kernel().GetCurrentThreadManager().GetCurrentThread(); - system.GetRunningCore().SaveContext(thread->context); - - if (last_bkpt_hit || GDBStub::IsMemoryBreak()) { + if (last_bkpt_hit) { last_bkpt_hit = false; - GDBStub::Break(); - GDBStub::SendTrap(thread, 5); + GDBStub::Break(SIGTRAP); } } diff --git a/src/core/core.cpp b/src/core/core.cpp index eb53b247e..9feac9216 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -88,6 +88,12 @@ System::ResultStatus System::RunLoop(bool tight_loop) { if (thread && running_core) { running_core->SaveContext(thread->context); } + // The break flag is only set if GDB is connected, + // we can do clearing here safely. If it is ever + // used outside, move the clearing outside the if. + for (auto& cpu_core : cpu_cores) { + cpu_core->ClearBreakFlag(); + } GDBStub::HandlePacket(*this); } @@ -249,6 +255,8 @@ System::ResultStatus System::RunLoop(bool tight_loop) { cpu_core->GetTimer().Idle(); PrepareReschedule(); } else { + // In the rare case the break flag is set (due to exception thrown) + // there is probably no need to adjust the timer accordingly. if (tight_loop) { cpu_core->Run(); } else { diff --git a/src/core/core.h b/src/core/core.h index 05620da32..89df9b00e 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -103,8 +103,10 @@ public: ErrorSavestate, ///< Error saving or loading ErrorArticDisconnected, ///< Error when artic base disconnects ErrorN3DSApplication, ///< Error launching New 3DS application in Old 3DS mode - ShutdownRequested, ///< Emulated program requested a system shutdown - ErrorUnknown ///< Any other error + ErrorCoreExceptionRaised, ///< The CPU emulation raised an exception + ErrorMemoryExceptionRaised, ///< Unmmaped memory was accessed + ShutdownRequested, ///< Emulated program requested a system shutdown + ErrorUnknown ///< Any other error }; explicit System(); diff --git a/src/core/gdbstub/gdbstub.cpp b/src/core/gdbstub/gdbstub.cpp index 114ac9bab..a1d0f8e73 100644 --- a/src/core/gdbstub/gdbstub.cpp +++ b/src/core/gdbstub/gdbstub.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. @@ -36,10 +40,13 @@ #include "core/gdbstub/gdbstub.h" #include "core/gdbstub/hio.h" #include "core/hle/kernel/process.h" +#include "core/loader/loader.h" #include "core/memory.h" +#pragma optimize("", off) + // Uncomment to log all GDB traffic -// #define PRINT_GDB_TRAFFIC +#define PRINT_GDB_TRAFFIC namespace GDBStub { namespace { @@ -62,6 +69,10 @@ constexpr u32 SIGTERM = 15; constexpr u32 MSG_WAITALL = 8; #endif +#ifndef SIGSEGV +constexpr u32 SIGSEGV = 11; +#endif + constexpr u32 SP_REGISTER = 13; constexpr u32 LR_REGISTER = 14; constexpr u32 PC_REGISTER = 15; @@ -76,6 +87,8 @@ constexpr char target_xml[] = R"(l + arm + 3DS @@ -129,19 +142,25 @@ u8 command_buffer[GDB_BUFFER_SIZE]; u32 command_length; u32 latest_signal = 0; -bool memory_break = false; static Kernel::Thread* current_thread = nullptr; +static Kernel::Process* current_process = nullptr; // Binding to a port within the reserved ports range (0-1023) requires root permissions, // so default to a port outside of that range. u16 gdbstub_port = 24689; -bool send_trap = false; +constexpr bool supports_extended_mode = true; +bool is_extended_mode = false; // If set to false, the server will never be started and no // gdbstub-related functions will be executed. std::atomic server_enabled(false); +int accept_socket = -1; +int continue_thread = -1; + +static Kernel::Thread* break_thread = nullptr; +static int break_signal = 0; #ifdef _WIN32 WSADATA InitData; @@ -161,16 +180,17 @@ BreakpointMap breakpoints_write; } // Anonymous namespace static Kernel::Thread* FindThreadById(int id) { - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (auto& thread : threads) { - if (thread->GetThreadId() == static_cast(id)) { - return thread.get(); - } + if (!current_process) { + return nullptr; + } + + auto thread_list = current_process->GetThreadList(); + for (auto& thread : thread_list) { + if (thread->GetThreadId() == static_cast(id)) { + return thread.get(); } } + return nullptr; } @@ -364,12 +384,40 @@ static u64 GdbHexToLong(const u8* src) { return output; } +static int GetErrno() { +#ifdef _WIN32 + return WSAGetLastError(); +#else + return errno; +#endif +} + +static bool SetNonBlock(int socket, bool nonblock) { +#ifdef _WIN32 + unsigned long nonblocking = nonblock ? 1 : 0; + int ret = ioctlsocket(socket, FIONBIO, &nonblocking); + if (ret < 0) { + LOG_ERROR(Debug_GDBStub, "Failed to set non-blocking gdb socket"); + return false; + } +#else + int flags = nonblock ? O_NONBLOCK : 0; + + const int ret = ::fcntl(socket, F_SETFL, flags); + if (ret < 0) { + LOG_ERROR(Debug_GDBStub, "Failed to set non-blocking gdb socket"); + return false; + } +#endif + return true; +} + /// Read a byte from the gdb client. static u8 ReadByte() { u8 c; std::size_t received_size = recv(gdbserver_socket, reinterpret_cast(&c), 1, MSG_WAITALL); if (received_size != 1) { - LOG_ERROR(Debug_GDBStub, "recv failed : {}", received_size); + LOG_ERROR(Debug_GDBStub, "recv failed : {}", GetErrno()); Shutdown(); } @@ -417,13 +465,15 @@ static void RemoveBreakpoint(BreakpointType type, VAddr addr) { bp->second.len, bp->second.addr, type); if (type == BreakpointType::Execute) { - Core::System::GetInstance().Memory().WriteBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), bp->second.addr, - bp->second.inst.data(), bp->second.inst.size()); + Core::System::GetInstance().Memory().WriteBlock(*current_process, bp->second.addr, + bp->second.inst.data(), bp->second.len); u32 num_cores = Core::GetNumCores(); for (u32 i = 0; i < num_cores; ++i) { Core::GetCore(i).ClearInstructionCache(); } + } else { + Core::System::GetInstance().Memory().UnregisterWatchpoint(*current_process, bp->second.addr, + bp->second.len); } p.erase(addr); } @@ -444,38 +494,58 @@ BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, BreakpointType type) return breakpoint; } -bool CheckBreakpoint(VAddr addr, BreakpointType type) { +bool CheckBreakpoint(VAddr addr, u32 access_len, BreakpointType type) { if (!IsConnected()) { return false; } const BreakpointMap& p = GetBreakpointMap(type); - const auto bp = p.find(addr); - if (bp == p.end()) { - return false; - } + // Access range: [addr, access_end) + const VAddr access_end = addr + access_len; - u32 len = bp->second.len; + for (const auto& [base_addr, bp] : p) { + if (!bp.active) { + continue; + } - // IDA Pro defaults to 4-byte breakpoints for all non-hardware breakpoints - // no matter if it's a 4-byte or 2-byte instruction. When you execute a - // Thumb instruction with a 4-byte breakpoint set, it will set a breakpoint on - // two instructions instead of the single instruction you placed the breakpoint - // on. So, as a way to make sure that execution breakpoints are only breaking - // on the instruction that was specified, set the length of an execution - // breakpoint to 1. This should be fine since the CPU should never begin executing - // an instruction anywhere except the beginning of the instruction. - if (type == BreakpointType::Execute) { - len = 1; - } + u32 bp_len = bp.len; - if (bp->second.active && (addr >= bp->second.addr && addr < bp->second.addr + len)) { - LOG_DEBUG(Debug_GDBStub, - "Found breakpoint type {} @ {:08x}, range: {:08x}" - " - {:08x} ({:x} bytes)", - type, addr, bp->second.addr, bp->second.addr + len, len); - return true; + // IDA Pro defaults to 4-byte breakpoints for all non-hardware breakpoints + // no matter if it's a 4-byte or 2-byte instruction. When you execute a + // Thumb instruction with a 4-byte breakpoint set, it will set a breakpoint on + // two instructions instead of the single instruction you placed the breakpoint + // on. So, as a way to make sure that execution breakpoints are only breaking + // on the instruction that was specified, set the length of an execution + // breakpoint to 1. This should be fine since the CPU should never begin executing + // an instruction anywhere except the beginning of the instruction. + if (type == BreakpointType::Execute) { + bp_len = 1; + } + + // Breakpoint/watchpoint range: [bp.addr, bp_end) + const VAddr bp_end = bp.addr + bp_len; + + bool hit = false; + + if (type == BreakpointType::Execute) { + // Execute breakpoints should only trigger on exact PC match. + hit = (addr == bp.addr); + } else { + // Range overlap test: + // [addr, access_end) overlaps [bp.addr, bp_end) + hit = (addr < bp_end) && (bp.addr < access_end); + } + + if (hit) { + LOG_DEBUG(Debug_GDBStub, + "Found breakpoint type {}, " + "access range: {:08x} - {:08x}, " + "breakpoint range: {:08x} - {:08x}", + type, addr, addr, access_end, bp.addr, bp_end); + + return true; + } } return false; @@ -503,11 +573,7 @@ void SendReply(const char* reply) { command_length = static_cast(strlen(reply)); #ifdef PRINT_GDB_TRAFFIC - if (command_length > 0) { - LOG_INFO(Debug_GDBStub, "Req: {} {}", *reply, reply + 1); - } else { - LOG_INFO(Debug_GDBStub, "Req:"); - } + LOG_INFO(Debug_GDBStub, "Res: {}", reply); #endif if (command_length + 4 > sizeof(command_buffer)) { @@ -545,60 +611,103 @@ static void HandleQuery() { if (strcmp(query, "TStatus") == 0) { SendReply("T0"); + } else if (strncmp(query, "Attached", strlen("Attached")) == 0) { + SendReply("1"); } else if (strncmp(query, "Supported", strlen("Supported")) == 0) { // PacketSize needs to be large enough for target xml - SendReply("PacketSize=2000;qXfer:features:read+;qXfer:threads:read+"); + SendReply("PacketSize=2000;qXfer:features:read+;qXfer:osdata:read+;qXfer:threads:read+;" + "vContSupported+"); } else if (strncmp(query, "Xfer:features:read:target.xml:", strlen("Xfer:features:read:target.xml:")) == 0) { SendReply(target_xml); } else if (strncmp(query, "fThreadInfo", strlen("fThreadInfo")) == 0) { - std::string val = "m"; - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (const auto& thread : threads) { - val += fmt::format("{:x},", thread->GetThreadId()); - } + if (!current_process) { + SendReply("E01"); + return; } + + auto thread_list = current_process->GetThreadList(); + std::string val = "m"; + for (const auto& thread : thread_list) { + val += fmt::format("{:x},", thread->GetThreadId()); + } + val.pop_back(); SendReply(val.c_str()); } else if (strncmp(query, "sThreadInfo", strlen("sThreadInfo")) == 0) { SendReply("l"); + } else if (strncmp(query, "Xfer:osdata:read:processes", strlen("Xfer:osdata:read:processes")) == + 0) { + std::string buffer; + buffer += "l"; + auto process_list = Core::System::GetInstance().Kernel().GetProcessList(); + for (const auto& p : process_list) { + buffer += fmt::format("" + "{}" + "{}" + "", + p->process_id, p->codeset->name); + } + buffer += ""; + SendReply(buffer.c_str()); } else if (strncmp(query, "Xfer:threads:read", strlen("Xfer:threads:read")) == 0) { + if (!current_process) { + SendReply("E01"); + return; + } + std::string buffer; buffer += "l"; buffer += ""; - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (const auto& thread : threads) { - buffer += fmt::format(R"*()*", - thread->GetThreadId(), thread->GetThreadId()); - } + auto thread_list = current_process->GetThreadList(); + for (const auto& thread : thread_list) { + buffer += fmt::format(R"*()*", + thread->GetThreadId(), thread->GetThreadId()); } buffer += ""; + SendReply(buffer.c_str()); } else { SendReply(""); } } +static bool SetThread(int thread_id) { + if (!current_process) { + // The process has not been selected yet + return false; + } + if (thread_id >= 1) { + current_thread = FindThreadById(thread_id); + } + if (!current_thread) { + auto thread_list = current_process->GetThreadList(); + if (thread_list.size() > 0) { + // Select the lowest thread ID, which is the main thread + std::sort(thread_list.begin(), thread_list.end(), + [](const std::shared_ptr& a, + const std::shared_ptr& b) { + return a->thread_id < b->thread_id; + }); + current_thread = thread_list[0].get(); + } + } + return current_thread != nullptr; +} /// Handle set thread command from gdb client. static void HandleSetThread() { int thread_id = -1; if (command_buffer[2] != '-') { thread_id = static_cast(HexToInt(command_buffer + 2, command_length - 2)); } - if (thread_id >= 1) { - current_thread = FindThreadById(thread_id); + + if (command_buffer[1] == 'c') { + continue_thread = thread_id; + SendReply("OK"); + return; } - if (!current_thread) { - thread_id = 1; - current_thread = FindThreadById(thread_id); - } - if (current_thread) { + + if (SetThread(thread_id)) { SendReply("OK"); return; } @@ -607,6 +716,11 @@ static void HandleSetThread() { /// Handle thread alive command from gdb client. static void HandleThreadAlive() { + if (!current_process) { + SendReply("E01"); + return; + } + int thread_id = static_cast(HexToInt(command_buffer + 1, command_length - 1)); if (thread_id == 0) { thread_id = 1; @@ -618,6 +732,15 @@ static void HandleThreadAlive() { SendReply("E01"); } +static void HandleExtendedMode() { + if (supports_extended_mode) { + is_extended_mode = true; + SendReply("OK"); + } else { + SendReply(""); + } +} + /** * Send signal packet to client. * @@ -636,11 +759,16 @@ static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) { std::string buffer; if (full) { - + Core::ARM_Interface::ThreadContext ctx{}; + if (thread) { + ctx = thread->context; + } else { + Core::GetRunningCore().SaveContext(ctx); + } buffer = fmt::format("T{:02x}{:02x}:{:08x};{:02x}:{:08x};{:02x}:{:08x}", latest_signal, - PC_REGISTER, htonl(Core::GetRunningCore().GetPC()), SP_REGISTER, - htonl(Core::GetRunningCore().GetReg(SP_REGISTER)), LR_REGISTER, - htonl(Core::GetRunningCore().GetReg(LR_REGISTER))); + PC_REGISTER, htonl(ctx.cpu_registers[PC_REGISTER]), SP_REGISTER, + htonl(ctx.cpu_registers[SP_REGISTER]), LR_REGISTER, + htonl(ctx.cpu_registers[LR_REGISTER])); } else { buffer = fmt::format("T{:02x}", latest_signal); } @@ -653,6 +781,53 @@ static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) { SendReply(buffer.c_str()); } +static void HandleGetStopReason() { + if (is_extended_mode) { + // In extended mode, tell the debugger that there is no selected process yet. + // That way the debugger can ask for the process list and attack to the right PID. + if (!current_process) { + // The process has not been selected yet. + SendReply("W00"); + } else { + SendSignal(current_thread, latest_signal); + } + } else { + // In non extended mode, select the process corresponding to the "main" + // launched application. + u64 program_id = 0; + Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); + auto process_list = Core::System::GetInstance().Kernel().GetProcessList(); + for (const auto& process : process_list) { + if (process->codeset->program_id == program_id) { + current_process = process.get(); + current_process->SetDebugBreak(true); + if (SetThread(0)) { + SendSignal(current_thread, 0); + } else { + // Should never happen + SendReply("W00"); + } + return; + } + } + + // No process, should never happen + SendReply("W00"); + } +} + +static void BreakImpl(int signal) { + if (signal == SIGSEGV) { + LOG_WARNING(Debug_GDBStub, "Segmentation fault signals may not be fully accurate"); + } + + current_process->SetDebugBreak(true); + + latest_signal = signal; + + SendSignal(current_thread, signal); +} + /// Read command from gdb client. static void ReadCommand() { command_length = 0; @@ -664,7 +839,7 @@ static void ReadCommand() { return; } else if (c == 0x03) { LOG_INFO(Debug_GDBStub, "gdb: found break command\n"); - SendSignal(current_thread, SIGTRAP); + BreakImpl(SIGTRAP); return; } else if (c != GDB_STUB_START) { LOG_DEBUG(Debug_GDBStub, "gdb: read invalid byte {:02x}\n", c); @@ -726,6 +901,11 @@ static bool IsDataAvailable() { /// Send requested register to gdb client. static void ReadRegister() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + static u8 reply[64]; std::memset(reply, 0, sizeof(reply)); @@ -752,6 +932,11 @@ static void ReadRegister() { /// Send all registers to the gdb client. static void ReadRegisters() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + static u8 buffer[GDB_BUFFER_SIZE - 4]; std::memset(buffer, 0, sizeof(buffer)); @@ -791,6 +976,11 @@ static void UpdateCPUThreadContext() { /// Modify data of register specified by gdb client. static void WriteRegister() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + const u8* buffer_ptr = command_buffer + 3; u32 id = HexCharToValue(command_buffer[1]); @@ -819,6 +1009,11 @@ static void WriteRegister() { /// Modify all registers with data received from the client. static void WriteRegisters() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + const u8* buffer_ptr = command_buffer + 1; if (command_buffer[0] != 'G') @@ -849,6 +1044,11 @@ static void WriteRegisters() { /// Read location in memory specified by gdb client. static void ReadMemory() { + if (!current_process) { + SendReply("E01"); + return; + } + static u8 reply[GDB_BUFFER_SIZE - 4]; auto start_offset = command_buffer + 1; @@ -866,13 +1066,12 @@ static void ReadMemory() { } auto& memory = Core::System::GetInstance().Memory(); - if (!memory.IsValidVirtualAddress(*Core::System::GetInstance().Kernel().GetCurrentProcess(), - addr)) { - return SendReply("E00"); + if (!memory.IsValidVirtualAddress(*current_process, addr)) { + return SendReply("E14"); } std::vector data(len); - memory.ReadBlock(addr, data.data(), len); + memory.ReadBlock(*current_process, addr, data.data(), len); MemToGdbHex(reply, data.data(), len); reply[len * 2] = '\0'; @@ -885,6 +1084,11 @@ static void ReadMemory() { /// Modify location in memory with data received from the gdb client. static void WriteMemory() { + if (!current_process) { + SendReply("E01"); + return; + } + auto start_offset = command_buffer + 1; auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); @@ -894,36 +1098,67 @@ static void WriteMemory() { u32 len = HexToInt(start_offset, static_cast(len_pos - start_offset)); auto& memory = Core::System::GetInstance().Memory(); - if (!memory.IsValidVirtualAddress(*Core::System::GetInstance().Kernel().GetCurrentProcess(), - addr)) { + if (!memory.IsValidVirtualAddress(*current_process, addr)) { return SendReply("E00"); } std::vector data(len); GdbHexToMem(data.data(), len_pos + 1, len); - memory.WriteBlock(addr, data.data(), len); + memory.WriteBlock(*current_process, addr, data.data(), len); ClearAllInstructionCache(); SendReply("OK"); } -void Break(bool is_memory_break) { - send_trap = true; - - memory_break = is_memory_break; -} - -bool IsMemoryBreak() { - if (!IsConnected()) { - return false; +void Break(int signal) { + if (!IsConnected() || !current_process || + Core::System::GetInstance().Kernel().GetCurrentProcess().get() != current_process) { + LOG_ERROR(Debug_GDBStub, "Got signal for un-attached process, ignoring..."); + return; } - return memory_break; + if (break_thread) { + LOG_ERROR(Debug_GDBStub, + "Got multiple break signals in quick succession, latest may be lost"); + return; + } + + break_thread = + Core::System::GetInstance().Kernel().GetCurrentThreadManager().GetCurrentThread(); + if (break_thread) { + break_signal = signal; + } + + // Try to break CPU asap + Core::GetRunningCore().SetBreakFlag(); + Core::GetRunningCore().ClearInstructionCache(); + Core::GetRunningCore().PrepareReschedule(); } /// Tell the CPU to continue executing. static void Continue() { - memory_break = false; + if (!current_process) { + return; + } + + u32 thread_id = -1; + if (continue_thread == 0) { + thread_id = current_process->GetThreadList()[0]->thread_id; + } else { + thread_id = continue_thread; + } + + // There is no documentation anywhere if continue should + // reset the continue thread value. Luma3DS implementation + // does this, and looks like IDA Pro expects it that way too. + continue_thread = -1; + + std::vector continue_list{}; + if (thread_id != -1) { + continue_list.push_back(thread_id); + } + + current_process->SetDebugBreak(false, continue_list); ClearAllInstructionCache(); } @@ -937,20 +1172,26 @@ static void Continue() { static bool CommitBreakpoint(BreakpointType type, VAddr addr, u32 len) { BreakpointMap& p = GetBreakpointMap(type); + if (type == BreakpointType::Execute && len != 2 && len != 4) { + return false; + } + Breakpoint breakpoint; breakpoint.active = true; breakpoint.addr = addr; breakpoint.len = len; - Core::System::GetInstance().Memory().ReadBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), addr, breakpoint.inst.data(), - breakpoint.inst.size()); + Core::System::GetInstance().Memory().ReadBlock(*current_process, addr, breakpoint.inst.data(), + len); static constexpr std::array btrap{0x70, 0x00, 0x20, 0xe1}; + static constexpr std::array btrap_thumb{0x00, 0xBE}; + if (type == BreakpointType::Execute) { Core::System::GetInstance().Memory().WriteBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), addr, btrap.data(), - btrap.size()); + *current_process, addr, (len == 2) ? btrap_thumb.data() : btrap.data(), len); ClearAllInstructionCache(); + } else { + Core::System::GetInstance().Memory().RegisterWatchpoint(*current_process, addr, len); } p.insert({addr, breakpoint}); @@ -962,6 +1203,11 @@ static bool CommitBreakpoint(BreakpointType type, VAddr addr, u32 len) { /// Handle add breakpoint command from gdb client. static void AddBreakpoint() { + if (!current_process) { + SendReply("E01"); + return; + } + BreakpointType type; u8 type_id = HexCharToValue(command_buffer[1]); @@ -1011,6 +1257,11 @@ static void AddBreakpoint() { /// Handle remove breakpoint command from gdb client. static void RemoveBreakpoint() { + if (!current_process) { + SendReply("E01"); + return; + } + BreakpointType type; u8 type_id = HexCharToValue(command_buffer[1]); @@ -1048,11 +1299,122 @@ static void RemoveBreakpoint() { SendReply("OK"); } +void HandleVCommand() { + std::string_view cmd_view((const char*)command_buffer, command_length); + + if (cmd_view.size() <= 1) { + SendReply("E01"); + return; + } + size_t delimiter_pos = cmd_view.find(';'); + std::string_view command = + cmd_view.substr(1, delimiter_pos == cmd_view.npos ? cmd_view.npos : delimiter_pos); + if (command == "Attach;") { + if (!is_extended_mode) { + SendReply("E01"); + return; + } + + std::string_view arg = cmd_view.substr(delimiter_pos + 1); + u32 pid = HexToInt(reinterpret_cast(arg.data()), arg.size()); + auto process = Core::System::GetInstance().Kernel().GetProcessById(pid); + if (!process) { + SendReply("E02"); + } else { + current_process = process.get(); + current_process->SetDebugBreak(true); + if (SetThread(0)) { + SendSignal(current_thread, 0); + } else { + // Should never happen + SendReply("W00"); + } + } + return; + } else if (command == "Cont?") { + SendReply("vCont;c;C"); + } else if (command == "Cont;") { + if (!current_process) { + SendReply("E01"); + return; + } + + std::string_view arg = cmd_view.substr(delimiter_pos + 1); + auto actions = Common::SplitString(arg, ';'); + + if (actions.empty()) { + SendReply("E01"); + return; + } + for (auto& action : actions) { + auto threads = Common::SplitString(action, ':'); + if (threads.empty()) { + SendReply("E01"); + return; + } + char action_type = threads[0][0]; + if (action_type != 'c') { + SendReply("E01"); + return; + } + std::vector thread_ids; + for (size_t i = 1; i < threads.size(); i++) { + thread_ids.push_back( + HexToInt(reinterpret_cast(threads[i].c_str()), threads[i].size())); + } + + current_process->SetDebugBreak(false, thread_ids); + } + } else { + SendReply(""); + } +} + void HandlePacket(Core::System& system) { + if (!IsConnected()) { if (defer_start) { ToggleServer(true); + defer_start = false; } + + // Handle accept new GDB connection + if (accept_socket != -1) { + sockaddr_in saddr_client; + sockaddr* client_addr = reinterpret_cast(&saddr_client); + socklen_t client_addrlen = sizeof(saddr_client); + gdbserver_socket = + static_cast(accept(accept_socket, client_addr, &client_addrlen)); + if (gdbserver_socket < 0) { +#ifdef _WIN32 + if (GetErrno() == WSAEWOULDBLOCK) { + // Nothing connected yet + return; + } +#else + if (GetErrno() == EAGAIN || GetErrno() == EWOULDBLOCK) { + // Nothing connected yet + return; + } +#endif + LOG_ERROR(Debug_GDBStub, "Failed to accept gdb client"); + } else { + LOG_INFO(Debug_GDBStub, "Client connected.\n"); + SetNonBlock(gdbserver_socket, false); + } + + shutdown(accept_socket, SHUT_RDWR); + accept_socket = -1; + } + return; + } + + if (break_thread) { + current_thread = break_thread; + break_thread = nullptr; + int signal = break_signal; + break_signal = 0; + BreakImpl(signal); return; } @@ -1072,7 +1434,7 @@ void HandlePacket(Core::System& system) { #ifdef PRINT_GDB_TRAFFIC std::string cmd_str(command_buffer + 1, command_buffer + command_length); - LOG_INFO(Debug_GDBStub, "Res: {:c} {}", command_buffer[0], cmd_str); + LOG_INFO(Debug_GDBStub, "Req: {:c} {}", command_buffer[0], cmd_str); #endif LOG_DEBUG(Debug_GDBStub, "Packet: {0:d} ('{0:c}')", command_buffer[0]); @@ -1085,16 +1447,28 @@ void HandlePacket(Core::System& system) { HandleSetThread(); break; case '?': - SendSignal(current_thread, latest_signal); + HandleGetStopReason(); + break; + case '!': + HandleExtendedMode(); + break; + case 'D': + SendReply("OK"); + ToggleServer(false); + // Continue execution + continue_thread = -1; + Continue(); break; case 'k': LOG_INFO(Debug_GDBStub, "killed by gdb"); ToggleServer(false); - // Continue execution so we don't hang forever after shutting down the server + // Continue execution and stop emulation + continue_thread = -1; Continue(); + system.RequestShutdown(); return; case 'F': - HandleHioReply(system, command_buffer, command_length); + HandleHioReply(system, current_process, command_buffer, command_length); break; case 'g': ReadRegisters(); @@ -1115,7 +1489,7 @@ void HandlePacket(Core::System& system) { WriteMemory(); break; case 's': - // Single step, return ENOTSUP + // Single step not supported, return ENOTSUP SendReply("E5F"); return; case 'C': @@ -1131,6 +1505,9 @@ void HandlePacket(Core::System& system) { case 'T': HandleThreadAlive(); break; + case 'v': + HandleVCommand(); + break; default: SendReply(""); break; @@ -1146,12 +1523,12 @@ void ToggleServer(bool status) { server_enabled = status; // Start server - if (!IsConnected() && Core::System::GetInstance().IsPoweredOn()) { + if (!IsInitialized() && Core::System::GetInstance().IsPoweredOn()) { Init(); } } else { // Stop server - if (IsConnected()) { + if (IsInitialized()) { Shutdown(); } @@ -1184,47 +1561,32 @@ static void Init(u16 port) { WSAStartup(MAKEWORD(2, 2), &InitData); #endif - int tmpsock = static_cast(socket(PF_INET, SOCK_STREAM, 0)); - if (tmpsock == -1) { + accept_socket = static_cast(socket(PF_INET, SOCK_STREAM, 0)); + if (accept_socket == -1) { LOG_ERROR(Debug_GDBStub, "Failed to create gdb socket"); } // Set socket to SO_REUSEADDR so it can always bind on the same port int reuse_enabled = 1; - if (setsockopt(tmpsock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse_enabled, + if (setsockopt(accept_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse_enabled, sizeof(reuse_enabled)) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to set gdb socket option"); } const sockaddr* server_addr = reinterpret_cast(&saddr_server); socklen_t server_addrlen = sizeof(saddr_server); - if (bind(tmpsock, server_addr, server_addrlen) < 0) { + if (bind(accept_socket, server_addr, server_addrlen) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to bind gdb socket"); } - if (listen(tmpsock, 1) < 0) { + if (listen(accept_socket, 1) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to listen to gdb socket"); } + SetNonBlock(accept_socket, true); + // Wait for gdb to connect LOG_INFO(Debug_GDBStub, "Waiting for gdb to connect...\n"); - sockaddr_in saddr_client; - sockaddr* client_addr = reinterpret_cast(&saddr_client); - socklen_t client_addrlen = sizeof(saddr_client); - gdbserver_socket = static_cast(accept(tmpsock, client_addr, &client_addrlen)); - if (gdbserver_socket < 0) { - // In the case that we couldn't start the server for whatever reason, just start CPU - // execution like normal. - LOG_ERROR(Debug_GDBStub, "Failed to accept gdb client"); - } else { - LOG_INFO(Debug_GDBStub, "Client connected.\n"); - saddr_client.sin_addr.s_addr = ntohl(saddr_client.sin_addr.s_addr); - } - - // Clean up temporary socket if it's still alive at this point. - if (tmpsock != -1) { - shutdown(tmpsock, SHUT_RDWR); - } } void Init() { @@ -1236,6 +1598,7 @@ void Shutdown() { return; } defer_start = false; + is_extended_mode = false; LOG_INFO(Debug_GDBStub, "Stopping GDB ..."); if (gdbserver_socket != -1) { @@ -1243,6 +1606,11 @@ void Shutdown() { gdbserver_socket = -1; } + if (accept_socket != -1) { + shutdown(accept_socket, SHUT_RDWR); + accept_socket = -1; + } + #ifdef _WIN32 WSACleanup(); #endif @@ -1254,19 +1622,11 @@ bool IsServerEnabled() { return server_enabled; } +bool IsInitialized() { + return IsServerEnabled() && (accept_socket != -1 || gdbserver_socket != -1); +} + bool IsConnected() { return IsServerEnabled() && gdbserver_socket != -1; } - -void SendTrap(Kernel::Thread* thread, int trap) { - if (!send_trap) { - return; - } - - current_thread = thread; - - SendSignal(thread, trap); - - send_trap = false; -} }; // namespace GDBStub diff --git a/src/core/gdbstub/gdbstub.h b/src/core/gdbstub/gdbstub.h index 8a021580b..14345aa77 100644 --- a/src/core/gdbstub/gdbstub.h +++ b/src/core/gdbstub/gdbstub.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. @@ -60,18 +64,18 @@ void Shutdown(); /// Checks if the gdbstub server is enabled. bool IsServerEnabled(); +/// Returns true if the GDB server is initialized +bool IsInitialized(); + /// Returns true if there is an active socket connection. bool IsConnected(); /** * Signal to the gdbstub server that it should halt CPU execution. * - * @param is_memory_break If true, the break resulted from a memory breakpoint. + * @param signal Signal that produced the break (SIGTRAP by default) */ -void Break(bool is_memory_break = false); - -/// Determine if there was a memory breakpoint. -bool IsMemoryBreak(); +void Break(int signal); /// Read and handle packet from gdb client. void HandlePacket(Core::System& system); @@ -88,17 +92,10 @@ BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, GDBStub::BreakpointTy * Check if a breakpoint of the specified type exists at the given address. * * @param addr Address of breakpoint. + * @param access_len Access size in bytes. * @param type Type of breakpoint. */ -bool CheckBreakpoint(VAddr addr, GDBStub::BreakpointType type); - -/** - * Send trap signal from thread back to the gdbstub server. - * - * @param thread Sending thread. - * @param trap Trap no. - */ -void SendTrap(Kernel::Thread* thread, int trap); +bool CheckBreakpoint(VAddr addr, u32 access_len, BreakpointType type); /** * Send reply to gdb client. diff --git a/src/core/gdbstub/hio.cpp b/src/core/gdbstub/hio.cpp index cbc528672..2d6c8058a 100644 --- a/src/core/gdbstub/hio.cpp +++ b/src/core/gdbstub/hio.cpp @@ -1,13 +1,18 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include "common/string_util.h" #include "core/core.h" #include "core/gdbstub/gdbstub.h" #include "core/gdbstub/hio.h" +#ifndef SIGTRAP +constexpr u32 SIGTRAP = 5; +#endif + namespace GDBStub { namespace { @@ -51,7 +56,7 @@ static void SendErrorReply(int error_code, int retval = -1) { SendReply(packet.data()); } -void SetHioRequest(Core::System& system, const VAddr addr) { +void SetHioRequest(Core::System& system, Kernel::Process* process, const VAddr addr) { if (!IsServerEnabled()) { LOG_WARNING(Debug_GDBStub, "HIO requested but GDB stub is not running"); return; @@ -67,14 +72,13 @@ void SetHioRequest(Core::System& system, const VAddr addr) { } auto& memory = system.Memory(); - const auto process = system.Kernel().GetCurrentProcess(); if (!memory.IsValidVirtualAddress(*process, addr)) { LOG_WARNING(Debug_GDBStub, "Invalid address for HIO request"); return; } - memory.ReadBlock(addr, ¤t_hio_request, sizeof(PackedGdbHioRequest)); + memory.ReadBlock(*process, addr, ¤t_hio_request, sizeof(PackedGdbHioRequest)); if (current_hio_request.magic != std::array{'G', 'D', 'B', '\0'}) { std::string_view bad_magic{ @@ -96,11 +100,11 @@ void SetHioRequest(Core::System& system, const VAddr addr) { // Now halt, so that no further instructions are executed until the request // is processed by the client. We will continue after the reply comes back - Break(); + Break(SIGTRAP); system.GetRunningCore().ClearInstructionCache(); } -void HandleHioReply(Core::System& system, const u8* const command_buffer, +void HandleHioReply(Core::System& system, Kernel::Process* process, const u8* const command_buffer, const u32 command_length) { if (!IsWaitingForHioReply()) { LOG_WARNING(Debug_GDBStub, "Got HIO reply but never sent a request"); @@ -169,7 +173,6 @@ void HandleHioReply(Core::System& system, const u8* const command_buffer, current_hio_request.retval, current_hio_request.gdb_errno, current_hio_request.ctrl_c); - const auto process = system.Kernel().GetCurrentProcess(); auto& memory = system.Memory(); // should have been checked when we first initialized the request, @@ -180,7 +183,8 @@ void HandleHioReply(Core::System& system, const u8* const command_buffer, return; } - memory.WriteBlock(current_hio_request_addr, ¤t_hio_request, sizeof(PackedGdbHioRequest)); + memory.WriteBlock(*process, current_hio_request_addr, ¤t_hio_request, + sizeof(PackedGdbHioRequest)); current_hio_request = {}; current_hio_request_addr = 0; diff --git a/src/core/gdbstub/hio.h b/src/core/gdbstub/hio.h index 827521841..5fe366bb4 100644 --- a/src/core/gdbstub/hio.h +++ b/src/core/gdbstub/hio.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -10,6 +10,10 @@ namespace Core { class System; } +namespace Kernel { +class Process; +} + namespace GDBStub { /** @@ -51,7 +55,7 @@ static_assert(sizeof(PackedGdbHioRequest) == 152, * * @param address The memory address of the \ref PackedGdbHioRequest. */ -void SetHioRequest(Core::System& system, const VAddr address); +void SetHioRequest(Core::System& system, Kernel::Process* process, const VAddr address); /** * If there is a pending HIO request, send it to the client. @@ -63,6 +67,7 @@ bool HandlePendingHioRequestPacket(); /** * Process an HIO reply from the client. */ -void HandleHioReply(Core::System& system, const u8* const command_buffer, const u32 command_length); +void HandleHioReply(Core::System& system, Kernel::Process* process, const u8* const command_buffer, + const u32 command_length); } // namespace GDBStub diff --git a/src/core/hle/kernel/process.cpp b/src/core/hle/kernel/process.cpp index 5e0d3810f..a808bb2ab 100644 --- a/src/core/hle/kernel/process.cpp +++ b/src/core/hle/kernel/process.cpp @@ -592,6 +592,42 @@ Result Process::Unmap(VAddr target, VAddr source, u32 size, VMAPermission perms, return ResultSuccess; } +std::vector> Kernel::Process::GetThreadList() { + std::vector> ret; + for (u32 core = 0; core < Core::GetNumCores(); core++) { + auto thread_list = kernel.GetThreadManager(core).GetThreadList(); + for (auto& thread : thread_list) { + if (thread->owner_process.lock().get() == this) { + ret.push_back(thread); + } + } + } + return ret; +} + +void Kernel::Process::SetDebugBreak(bool debug_break, std::vector thread_ids) { + auto thread_list = GetThreadList(); + bool needs_reschedule = false; + for (auto& t : thread_list) { + + if (!thread_ids.empty()) { + u32 thread_id = t->thread_id; + if (std::find(thread_ids.begin(), thread_ids.end(), thread_id) == thread_ids.end()) { + continue; + } + } + + needs_reschedule |= t->SetDebugBreak(debug_break); + } + + if (needs_reschedule) { + for (u32 i = 0; i < Core::GetNumCores(); i++) { + Core::GetCore(i).PrepareReschedule(); + kernel.GetThreadManager(i).Reschedule(); + } + } +} + void Process::FreeAllMemory() { if (memory_region == nullptr || resource_limit == nullptr) { return; diff --git a/src/core/hle/kernel/process.h b/src/core/hle/kernel/process.h index 7ec1adfbb..4067839cd 100644 --- a/src/core/hle/kernel/process.h +++ b/src/core/hle/kernel/process.h @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -226,6 +226,10 @@ public: Result Unmap(VAddr target, VAddr source, u32 size, VMAPermission perms, bool privileged = false); + std::vector> GetThreadList(); + + void SetDebugBreak(bool debug_break, std::vector thread_ids = {}); + private: void FreeAllMemory(); diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 833543d9a..a02e91427 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -1171,7 +1171,7 @@ void SVC::OutputDebugString(VAddr address, s32 len) { } if (len == 0) { - GDBStub::SetHioRequest(system, address); + GDBStub::SetHioRequest(system, kernel.GetCurrentProcess().get(), address); return; } diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index 41a706fdd..a3cf78ec6 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -77,6 +77,7 @@ void Thread::serialize(Archive& ar, const unsigned int file_version) { } } ar & wakeup_callback; + ar & debug_break; } SERIALIZE_IMPL(Thread) @@ -191,7 +192,7 @@ Thread* ThreadManager::PopNextReadyThread() { u32 next_priority{}; next = nullptr; - if (thread && thread->status == ThreadStatus::Running) { + if (thread && thread->status == ThreadStatus::Running && thread->CanSchedule()) { do { // We have to do better than the current thread. // This call returns null when that's not possible. @@ -201,18 +202,18 @@ Thread* ThreadManager::PopNextReadyThread() { // Otherwise just keep going with the current thread next = thread; break; - } else if (!next->can_schedule) { + } else if (!next->CanSchedule()) { skipped.push_back({next_priority, next}); } - } while (!next->can_schedule); + } while (!next->CanSchedule()); } else { do { std::tie(next_priority, next) = ready_queue.pop_first(); - if (next && !next->can_schedule) { + if (next && !next->CanSchedule()) { skipped.push_back({next_priority, next}); } - } while (next && !next->can_schedule); + } while (next && !next->CanSchedule()); } for (auto it = skipped.rbegin(); it != skipped.rend(); it++) { @@ -537,6 +538,14 @@ VAddr Thread::GetCommandBufferAddress() const { return GetTLSAddress() + command_header_offset; } +bool Thread::SetDebugBreak(bool _debug_break) { + if (debug_break == _debug_break) { + return false; + } + debug_break = _debug_break; + return true; +} + CpuLimiter::~CpuLimiter() {} CpuLimiterMulti::CpuLimiterMulti(Kernel::KernelSystem& _kernel) : kernel(_kernel) {} diff --git a/src/core/hle/kernel/thread.h b/src/core/hle/kernel/thread.h index be515319b..49891b31a 100644 --- a/src/core/hle/kernel/thread.h +++ b/src/core/hle/kernel/thread.h @@ -365,6 +365,15 @@ public: return status == ThreadStatus::WaitSynchAll; } + bool CanSchedule() { + // TODO(PabloMK7): This may not be the proper way + // threads are marked as non-schedulable when they + // are in debug break. Figure out and fix. + return can_schedule && !debug_break; + } + + bool SetDebugBreak(bool debug_break); + Core::ARM_Interface::ThreadContext context{}; u32 thread_id; @@ -410,6 +419,7 @@ public: private: ThreadManager& thread_manager; + bool debug_break{}; friend class boost::serialization::access; template diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 41ad049e5..47416ab87 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -16,6 +17,7 @@ #include "common/swap.h" #include "core/arm/arm_interface.h" #include "core/core.h" +#include "core/gdbstub/gdbstub.h" #include "core/global.h" #include "core/hle/kernel/process.h" #include "core/hle/service/plgldr/plgldr.h" @@ -28,6 +30,14 @@ SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl) SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl) +#ifndef SIGTRAP +constexpr u32 SIGTRAP = 5; +#endif + +#ifndef SIGSEGV +constexpr u32 SIGSEGV = 11; +#endif + namespace Memory { void PageTable::Clear() { @@ -187,7 +197,17 @@ public: std::memcpy(dest_buffer, src_ptr, copy_amount); break; } - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table.watchpoint_pages_map.find(page_index); + ASSERT_MSG(it != page_table.watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + const u8* src_ptr = it->second.memory.GetPtr() + page_offset; + std::memcpy(dest_buffer, src_ptr, copy_amount); + break; + } + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { if constexpr (!UNSAFE) { RasterizerFlushVirtualRegion(current_vaddr, static_cast(copy_amount), FlushMode::Flush); @@ -235,7 +255,17 @@ public: std::memcpy(dest_ptr, src_buffer, copy_amount); break; } - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table.watchpoint_pages_map.find(page_index); + ASSERT_MSG(it != page_table.watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + u8* dest_ptr = it->second.memory.GetPtr() + page_offset; + std::memcpy(dest_ptr, src_buffer, copy_amount); + break; + } + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { if constexpr (!UNSAFE) { RasterizerFlushVirtualRegion(current_vaddr, static_cast(copy_amount), FlushMode::Invalidate); @@ -392,6 +422,90 @@ PAddr& Memory::MemorySystem::Plugin3GXFramebufferAddress() { return impl->plugin_fb_address; } +#pragma optimize("", off) + +void MemorySystem::RegisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size) { + auto& page_table = *process.vm_manager.page_table; + + VAddr current = addr; + VAddr end = addr + size; + + while (current < end) { + const VAddr page_base = (current & ~CITRA_PAGE_MASK); + const VAddr page_index = page_base >> CITRA_PAGE_BITS; + + auto it = page_table.watchpoint_pages_map.find(page_index); + if (it != page_table.watchpoint_pages_map.end()) { + // Nothing to do, only increment count. + it->second.watchpoint_count++; + } else { + MemoryRef mem; + PageType& type = page_table.attributes[page_index]; + + switch (type) { + case PageType::Memory: + mem = page_table.pointers.Ref(page_index); + type = PageType::MemoryWatchpoint; + page_table.pointers[page_index] = nullptr; + break; + case PageType::RasterizerCachedMemory: + mem = GetPointerForRasterizerCache(page_base); + type = PageType::RasterizerCachedMemoryWatchpoint; + break; + default: + LOG_ERROR(HW_Memory, "Cannot get pointer to register watchpoint for page 0x{:08X}", + page_base); + continue; + } + + page_table.watchpoint_pages_map.insert( + {page_index, + PageTable::WatchpointPageInfo{.watchpoint_count = 1, .memory = std::move(mem)}}); + } + + current = page_base + CITRA_PAGE_SIZE; + } +} + +void MemorySystem::UnregisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size) { + auto& page_table = *process.vm_manager.page_table; + + VAddr current = addr; + VAddr end = addr + size; + + while (current < end) { + const VAddr page_base = (current & ~CITRA_PAGE_MASK); + const VAddr page_index = page_base >> CITRA_PAGE_BITS; + + auto it = page_table.watchpoint_pages_map.find(page_index); + if (it != page_table.watchpoint_pages_map.end()) { + if (--it->second.watchpoint_count == 0) { + + PageType& type = page_table.attributes[page_index]; + + switch (type) { + case PageType::MemoryWatchpoint: + type = PageType::Memory; + page_table.pointers[page_index] = it->second.memory; + break; + case PageType::RasterizerCachedMemoryWatchpoint: + type = PageType::RasterizerCachedMemory; + break; + default: + LOG_ERROR(HW_Memory, "Invalid watchpoint page type for page 0x{:08X}: {}", + page_base, static_cast(type)); + } + + page_table.watchpoint_pages_map.erase(page_index); + } + } else { + LOG_ERROR(HW_Memory, "No watchpoint found on page 0x{:08X}", page_base); + } + + current = page_base + CITRA_PAGE_SIZE; + } +} + void MemorySystem::MapPages(PageTable& page_table, u32 base, u32 size, MemoryRef memory, PageType type) { LOG_DEBUG(HW_Memory, "Mapping {} onto {:08X}-{:08X}", (void*)memory.GetPtr(), @@ -449,6 +563,23 @@ void MemorySystem::UnregisterPageTable(std::shared_ptr page_table) { } } +template +T MemorySystem::UnmappedAccess(const VAddr vaddr, const T value, bool read) { + const std::string mode = (read ? "Read" : "Write"); + const std::string value_str = read ? std::string("") : fmt::format(" 0x{:08X}", value); + const std::string message = fmt::format("unmapped {}{}{} @ 0x{:08X} at PC 0x{:08X}", mode, + sizeof(T) * 8, value_str, vaddr, impl->GetPC()); + if (GDBStub::IsConnected()) { + GDBStub::Break(SIGSEGV); + } else if (Settings::values.break_on_unmapped_memory_access) { + impl->system.SetStatus(Core::System::ResultStatus::ErrorMemoryExceptionRaised, + message.c_str()); + } + + LOG_ERROR(HW_Memory, "{}", message); + return {}; +} + template T MemorySystem::Read(const std::shared_ptr& page_table, const VAddr vaddr) { const u8* page_pointer = page_table->pointers[vaddr >> CITRA_PAGE_BITS]; @@ -476,20 +607,45 @@ T MemorySystem::Read(const std::shared_ptr& page_table, const VAddr v PageType type = page_table->attributes[vaddr >> CITRA_PAGE_BITS]; switch (type) { - case PageType::Unmapped: - LOG_ERROR(HW_Memory, "unmapped Read{} @ 0x{:08X} at PC 0x{:08X}", sizeof(T) * 8, vaddr, - impl->GetPC()); - return 0; + case PageType::Unmapped: { + return UnmappedAccess(vaddr, 0, true); + } case PageType::Memory: ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr); break; - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS); + ASSERT_MSG(it != page_table->watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + T value; + std::memcpy(&value, it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK), sizeof(T)); + + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Read)) { + GDBStub::Break(SIGTRAP); + } + + return value; + } + [[likely]] case PageType::RasterizerCachedMemory: { RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Flush); T value; std::memcpy(&value, GetPointerForRasterizerCache(vaddr), sizeof(T)); return value; } + case PageType::RasterizerCachedMemoryWatchpoint: { + RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Flush); + + T value; + std::memcpy(&value, GetPointerForRasterizerCache(vaddr), sizeof(T)); + + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Read)) { + GDBStub::Break(SIGTRAP); + } + + return value; + } default: UNREACHABLE(); } @@ -527,17 +683,39 @@ void MemorySystem::Write(const std::shared_ptr& page_table, const VAd PageType type = page_table->attributes[vaddr >> CITRA_PAGE_BITS]; switch (type) { case PageType::Unmapped: - LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}", - sizeof(data) * 8, (u32)data, vaddr, impl->GetPC()); + (void)UnmappedAccess(vaddr, data, false); return; case PageType::Memory: ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr); break; - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS); + ASSERT_MSG(it != page_table->watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + std::memcpy(it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK), &data, sizeof(T)); + + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) { + GDBStub::Break(SIGTRAP); + } + + break; + } + [[likely]] case PageType::RasterizerCachedMemory: { RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate); std::memcpy(GetPointerForRasterizerCache(vaddr), &data, sizeof(T)); break; } + case PageType::RasterizerCachedMemoryWatchpoint: { + RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate); + std::memcpy(GetPointerForRasterizerCache(vaddr), &data, sizeof(T)); + + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) { + GDBStub::Break(SIGTRAP); + } + + break; + } default: UNREACHABLE(); } @@ -556,18 +734,44 @@ bool MemorySystem::WriteExclusive(const VAddr vaddr, const T data, const T expec PageType type = impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS]; switch (type) { case PageType::Unmapped: - LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}", - sizeof(data) * 8, static_cast(data), vaddr, impl->GetPC()); + (void)UnmappedAccess(vaddr, data, false); return true; case PageType::Memory: ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr); return true; - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = impl->current_page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS); + ASSERT_MSG(it != impl->current_page_table->watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + const auto volatile_pointer = + reinterpret_cast(it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK)); + + bool ret = Common::AtomicCompareAndSwap(volatile_pointer, data, expected); + + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) { + GDBStub::Break(SIGTRAP); + } + + return ret; + } + [[likely]] case PageType::RasterizerCachedMemory: { RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate); const auto volatile_pointer = reinterpret_cast(GetPointerForRasterizerCache(vaddr).GetPtr()); return Common::AtomicCompareAndSwap(volatile_pointer, data, expected); } + case PageType::RasterizerCachedMemoryWatchpoint: { + RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate); + const auto volatile_pointer = + reinterpret_cast(GetPointerForRasterizerCache(vaddr).GetPtr()); + + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) { + GDBStub::Break(SIGTRAP); + } + + return Common::AtomicCompareAndSwap(volatile_pointer, data, expected); + } default: UNREACHABLE(); } @@ -582,7 +786,7 @@ bool MemorySystem::IsValidVirtualAddress(const Kernel::Process& process, const V return true; } - if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] == PageType::RasterizerCachedMemory) { + if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] != PageType::Unmapped) { return true; } @@ -600,7 +804,9 @@ u8* MemorySystem::GetPointer(const VAddr vaddr) { } if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == - PageType::RasterizerCachedMemory) { + PageType::RasterizerCachedMemory || + impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == + PageType::RasterizerCachedMemoryWatchpoint) { return GetPointerForRasterizerCache(vaddr); } @@ -615,7 +821,9 @@ const u8* MemorySystem::GetPointer(const VAddr vaddr) const { } if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == - PageType::RasterizerCachedMemory) { + PageType::RasterizerCachedMemory || + impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == + PageType::RasterizerCachedMemoryWatchpoint) { return GetPointerForRasterizerCache(vaddr); } @@ -755,7 +963,10 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached // address space, for example, a system module need not have a VRAM mapping. break; case PageType::Memory: - page_type = PageType::RasterizerCachedMemory; + case PageType::MemoryWatchpoint: + page_type = (page_type == PageType::Memory) + ? PageType::RasterizerCachedMemory + : PageType::RasterizerCachedMemoryWatchpoint; page_table->pointers[vaddr >> CITRA_PAGE_BITS] = nullptr; break; default: @@ -768,10 +979,16 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached // It is not necessary for a process to have this region mapped into its // address space, for example, a system module need not have a VRAM mapping. break; - case PageType::RasterizerCachedMemory: { - page_type = PageType::Memory; - page_table->pointers[vaddr >> CITRA_PAGE_BITS] = - GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK); + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { + page_type = (page_type == PageType::RasterizerCachedMemory) + ? PageType::Memory + : PageType::MemoryWatchpoint; + + if (page_type == PageType::Memory) { + page_table->pointers[vaddr >> CITRA_PAGE_BITS] = + GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK); + } break; } default: @@ -911,7 +1128,17 @@ void MemorySystem::ZeroBlock(const Kernel::Process& process, const VAddr dest_ad std::memset(dest_ptr, 0, copy_amount); break; } - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table.watchpoint_pages_map.find(page_index); + ASSERT_MSG(it != page_table.watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + u8* dest_ptr = it->second.memory.GetPtr() + page_offset; + std::memset(dest_ptr, 0, copy_amount); + break; + } + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { RasterizerFlushVirtualRegion(current_vaddr, static_cast(copy_amount), FlushMode::Invalidate); std::memset(GetPointerForRasterizerCache(current_vaddr), 0, copy_amount); @@ -960,7 +1187,17 @@ void MemorySystem::CopyBlock(const Kernel::Process& dest_process, WriteBlock(dest_process, dest_addr, src_ptr, copy_amount); break; } - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table.watchpoint_pages_map.find(page_index); + ASSERT_MSG(it != page_table.watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + const u8* src_ptr = it->second.memory.GetPtr() + page_offset; + WriteBlock(dest_process, dest_addr, src_ptr, copy_amount); + break; + } + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { RasterizerFlushVirtualRegion(current_vaddr, static_cast(copy_amount), FlushMode::Flush); WriteBlock(dest_process, dest_addr, GetPointerForRasterizerCache(current_vaddr), diff --git a/src/core/memory.h b/src/core/memory.h index 5c215b3f2..1433ad7d8 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -34,7 +34,7 @@ constexpr u32 CITRA_PAGE_MASK = CITRA_PAGE_SIZE - 1; constexpr int CITRA_PAGE_BITS = 12; constexpr std::size_t PAGE_TABLE_NUM_ENTRIES = 1 << (32 - CITRA_PAGE_BITS); -enum class PageType { +enum class PageType : u8 { /// Page is unmapped and should cause an access error. Unmapped, /// Page is mapped to regular memory. This is the only type you can get pointers to. @@ -42,6 +42,12 @@ enum class PageType { /// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and /// invalidation RasterizerCachedMemory, + /// Page is mapped to regular memory. Furthermore a debug watchpoint is set to an address within + /// the page. + MemoryWatchpoint, + /// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and + /// invalidation. Furthermore a debug watchpoint is set to an address within the page. + RasterizerCachedMemoryWatchpoint, }; /** @@ -82,6 +88,10 @@ struct PageTable { return Entry(*this, static_cast(idx)); } + const MemoryRef& Ref(std::size_t idx) { + return refs[idx]; + } + private: std::array raw; std::array refs; @@ -100,6 +110,22 @@ struct PageTable { return pointers.raw; } + struct WatchpointPageInfo { + u32 watchpoint_count{}; + MemoryRef memory; + + template + void serialize(Archive& ar, const unsigned int) { + ar & watchpoint_count; + ar & memory; + } + }; + + // Map holding pages that are marked to contain watchpoints. We don't need + // any fancy performance tricks here, as watchpoints are only used rarely + // while debugging and performance is not a priority in such cases. + std::unordered_map watchpoint_pages_map{}; + void Clear(); private: @@ -107,6 +133,7 @@ private: void serialize(Archive& ar, const unsigned int) { ar & pointers.refs; ar & attributes; + ar & watchpoint_pages_map; for (std::size_t i = 0; i < PAGE_TABLE_NUM_ENTRIES; i++) { pointers.raw[i] = pointers.refs[i].GetPtr(); } @@ -649,7 +676,14 @@ public: /// Returns a reference to the framebuffer address of the currently loaded 3GX plugin. PAddr& Plugin3GXFramebufferAddress(); + void RegisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size); + + void UnregisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size); + private: + template + T UnmappedAccess(const VAddr vaddr, const T value, bool read); + template T Read(const std::shared_ptr& page_table, const VAddr vaddr);