From de45c3e604ca7859c9c8b0f7fa5888e806aaa93a Mon Sep 17 00:00:00 2001 From: pineappleEA Date: Wed, 1 Jun 2022 20:08:41 +0200 Subject: [PATCH] early-access version 2750 --- CMakeLists.txt | 5 + README.md | 2 +- src/common/settings.cpp | 1 + src/common/settings.h | 2 +- src/core/CMakeLists.txt | 10 + src/core/arm/arm_interface.cpp | 46 ++ src/core/arm/arm_interface.h | 17 +- src/core/arm/dynarmic/arm_dynarmic_32.cpp | 34 +- src/core/arm/dynarmic/arm_dynarmic_32.h | 7 +- src/core/arm/dynarmic/arm_dynarmic_64.cpp | 33 +- src/core/arm/dynarmic/arm_dynarmic_64.h | 7 +- src/core/core.cpp | 29 +- src/core/core.h | 18 +- src/core/debugger/debugger.cpp | 268 ++++++++++++ src/core/debugger/debugger.h | 41 ++ src/core/debugger/debugger_interface.h | 79 ++++ src/core/debugger/gdbstub.cpp | 467 +++++++++++++++++++++ src/core/debugger/gdbstub.h | 49 +++ src/core/debugger/gdbstub_arch.cpp | 406 ++++++++++++++++++ src/core/debugger/gdbstub_arch.h | 67 +++ src/core/hle/kernel/k_process.cpp | 4 + src/core/hle/kernel/k_thread.h | 15 + src/core/memory.cpp | 13 + src/core/memory.h | 11 + src/video_core/CMakeLists.txt | 4 + src/yuzu/CMakeLists.txt | 4 + src/yuzu/bootmanager.cpp | 11 +- src/yuzu/bootmanager.h | 10 - src/yuzu/configuration/config.cpp | 5 + src/yuzu/configuration/configure_debug.cpp | 9 +- src/yuzu/configuration/configure_debug.ui | 54 +++ src/yuzu/main.cpp | 2 +- 32 files changed, 1661 insertions(+), 69 deletions(-) create mode 100755 src/core/debugger/debugger.cpp create mode 100755 src/core/debugger/debugger.h create mode 100755 src/core/debugger/debugger_interface.h create mode 100755 src/core/debugger/gdbstub.cpp create mode 100755 src/core/debugger/gdbstub.h create mode 100755 src/core/debugger/gdbstub_arch.cpp create mode 100755 src/core/debugger/gdbstub_arch.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e3de90ff..be70c04ae 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,6 +222,11 @@ else() list(APPEND CONAN_REQUIRED_LIBS "boost/1.79.0") endif() +# boost:asio has functions that require AcceptEx et al +if (MINGW) + find_library(MSWSOCK_LIBRARY mswsock REQUIRED) +endif() + # Attempt to locate any packages that are required and report the missing ones in CONAN_REQUIRED_LIBS yuzu_find_packages() diff --git a/README.md b/README.md index 50243ec22..269ae1af6 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ yuzu emulator early access ============= -This is the source code for early-access 2747. +This is the source code for early-access 2750. ## Legal Notice diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 9a9c74a70..6ffab63af 100755 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -70,6 +70,7 @@ void LogSettings() { log_path("DataStorage_NANDDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir)); log_path("DataStorage_SDMCDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir)); log_setting("Debugging_ProgramArgs", values.program_args.GetValue()); + log_setting("Debugging_GDBStub", values.use_gdbstub.GetValue()); log_setting("Input_EnableMotion", values.motion_enabled.GetValue()); log_setting("Input_EnableVibration", values.vibration_enabled.GetValue()); log_setting("Input_EnableRawInput", values.enable_raw_input.GetValue()); diff --git a/src/common/settings.h b/src/common/settings.h index 6d6e50425..a507744a2 100755 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -601,7 +601,7 @@ struct Values { // Debugging bool record_frame_times; BasicSetting use_gdbstub{false, "use_gdbstub"}; - BasicSetting gdbstub_port{0, "gdbstub_port"}; + BasicSetting gdbstub_port{6543, "gdbstub_port"}; BasicSetting program_args{std::string(), "program_args"}; BasicSetting dump_exefs{false, "dump_exefs"}; BasicSetting dump_nso{false, "dump_nso"}; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d12b3c7be..8c1040a98 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -36,6 +36,13 @@ add_library(core STATIC crypto/ctr_encryption_layer.h crypto/xts_encryption_layer.cpp crypto/xts_encryption_layer.h + debugger/debugger_interface.h + debugger/debugger.cpp + debugger/debugger.h + debugger/gdbstub_arch.cpp + debugger/gdbstub_arch.h + debugger/gdbstub.cpp + debugger/gdbstub.h device_memory.cpp device_memory.h file_sys/bis_factory.cpp @@ -736,6 +743,9 @@ create_target_directory_groups(core) target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls Opus::Opus) +if (MINGW) + target_link_libraries(core PRIVATE ${MSWSOCK_LIBRARY}) +endif() if (ENABLE_WEB_SERVICE) target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE) diff --git a/src/core/arm/arm_interface.cpp b/src/core/arm/arm_interface.cpp index c347e7ea7..9b5a5ca57 100755 --- a/src/core/arm/arm_interface.cpp +++ b/src/core/arm/arm_interface.cpp @@ -9,7 +9,9 @@ #include "core/arm/arm_interface.h" #include "core/arm/symbols.h" #include "core/core.h" +#include "core/debugger/debugger.h" #include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/svc.h" #include "core/loader/loader.h" #include "core/memory.h" @@ -88,4 +90,48 @@ void ARM_Interface::LogBacktrace() const { } } +void ARM_Interface::Run() { + using Kernel::StepState; + using Kernel::SuspendType; + + while (true) { + Kernel::KThread* current_thread{system.Kernel().CurrentScheduler()->GetCurrentThread()}; + Dynarmic::HaltReason hr{}; + + // Notify the debugger and go to sleep if a step was performed + // and this thread has been scheduled again. + if (current_thread->GetStepState() == StepState::StepPerformed) { + system.GetDebugger().NotifyThreadStopped(current_thread); + current_thread->RequestSuspend(SuspendType::Debug); + break; + } + + // Otherwise, run the thread. + if (current_thread->GetStepState() == StepState::StepPending) { + hr = StepJit(); + + if (Has(hr, step_thread)) { + current_thread->SetStepState(StepState::StepPerformed); + } + } else { + hr = RunJit(); + } + + // Notify the debugger and go to sleep if a breakpoint was hit. + if (Has(hr, breakpoint)) { + system.GetDebugger().NotifyThreadStopped(current_thread); + current_thread->RequestSuspend(Kernel::SuspendType::Debug); + break; + } + + // Handle syscalls and scheduling (this may change the current thread) + if (Has(hr, svc_call)) { + Kernel::Svc::Call(system, GetSvcNumber()); + } + if (Has(hr, break_loop) || !uses_wall_clock) { + break; + } + } +} + } // namespace Core diff --git a/src/core/arm/arm_interface.h b/src/core/arm/arm_interface.h index 8ce973a77..66f6107e9 100755 --- a/src/core/arm/arm_interface.h +++ b/src/core/arm/arm_interface.h @@ -6,6 +6,9 @@ #include #include + +#include + #include "common/common_funcs.h" #include "common/common_types.h" #include "core/hardware_properties.h" @@ -64,10 +67,7 @@ public: static_assert(sizeof(ThreadContext64) == 0x320); /// Runs the CPU until an event happens - virtual void Run() = 0; - - /// Step CPU by one instruction - virtual void Step() = 0; + void Run(); /// Clear all instruction cache virtual void ClearInstructionCache() = 0; @@ -194,6 +194,11 @@ public: void LogBacktrace() const; + static constexpr Dynarmic::HaltReason step_thread = Dynarmic::HaltReason::Step; + static constexpr Dynarmic::HaltReason break_loop = Dynarmic::HaltReason::UserDefined2; + static constexpr Dynarmic::HaltReason svc_call = Dynarmic::HaltReason::UserDefined3; + static constexpr Dynarmic::HaltReason breakpoint = Dynarmic::HaltReason::UserDefined4; + protected: /// System context that this ARM interface is running under. System& system; @@ -201,6 +206,10 @@ protected: bool uses_wall_clock; static void SymbolicateBacktrace(Core::System& system, std::vector& out); + + virtual Dynarmic::HaltReason RunJit() = 0; + virtual Dynarmic::HaltReason StepJit() = 0; + virtual u32 GetSvcNumber() const = 0; }; } // namespace Core diff --git a/src/core/arm/dynarmic/arm_dynarmic_32.cpp b/src/core/arm/dynarmic/arm_dynarmic_32.cpp index 8b5bfeead..199d2a93d 100755 --- a/src/core/arm/dynarmic/arm_dynarmic_32.cpp +++ b/src/core/arm/dynarmic/arm_dynarmic_32.cpp @@ -17,6 +17,8 @@ #include "core/arm/dynarmic/arm_exclusive_monitor.h" #include "core/core.h" #include "core/core_timing.h" +#include "core/debugger/debugger.h" +#include "core/hle/kernel/k_process.h" #include "core/hle/kernel/svc.h" #include "core/memory.h" @@ -24,9 +26,6 @@ namespace Core { using namespace Common::Literals; -constexpr Dynarmic::HaltReason break_loop = Dynarmic::HaltReason::UserDefined2; -constexpr Dynarmic::HaltReason svc_call = Dynarmic::HaltReason::UserDefined3; - class DynarmicCallbacks32 : public Dynarmic::A32::UserCallbacks { public: explicit DynarmicCallbacks32(ARM_Dynarmic_32& parent_) @@ -78,16 +77,21 @@ public: } void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override { + if (parent.system.DebuggerEnabled()) { + parent.jit.load()->Regs()[15] = pc; + parent.jit.load()->HaltExecution(ARM_Interface::breakpoint); + return; + } + parent.LogBacktrace(); LOG_CRITICAL(Core_ARM, "ExceptionRaised(exception = {}, pc = {:08X}, code = {:08X}, thumb = {})", exception, pc, MemoryReadCode(pc), parent.IsInThumbMode()); - UNIMPLEMENTED(); } void CallSVC(u32 swi) override { parent.svc_swi = swi; - parent.jit.load()->HaltExecution(svc_call); + parent.jit.load()->HaltExecution(ARM_Interface::svc_call); } void AddTicks(u64 ticks) override { @@ -239,20 +243,16 @@ std::shared_ptr ARM_Dynarmic_32::MakeJit(Common::PageTable* return std::make_unique(config); } -void ARM_Dynarmic_32::Run() { - while (true) { - const auto hr = jit.load()->Run(); - if (Has(hr, svc_call)) { - Kernel::Svc::Call(system, svc_swi); - } - if (Has(hr, break_loop) || !uses_wall_clock) { - break; - } - } +Dynarmic::HaltReason ARM_Dynarmic_32::RunJit() { + return jit.load()->Run(); } -void ARM_Dynarmic_32::Step() { - jit.load()->Step(); +Dynarmic::HaltReason ARM_Dynarmic_32::StepJit() { + return jit.load()->Step(); +} + +u32 ARM_Dynarmic_32::GetSvcNumber() const { + return svc_swi; } ARM_Dynarmic_32::ARM_Dynarmic_32(System& system_, CPUInterrupts& interrupt_handlers_, diff --git a/src/core/arm/dynarmic/arm_dynarmic_32.h b/src/core/arm/dynarmic/arm_dynarmic_32.h index abfe76644..5b1d60005 100755 --- a/src/core/arm/dynarmic/arm_dynarmic_32.h +++ b/src/core/arm/dynarmic/arm_dynarmic_32.h @@ -41,8 +41,6 @@ public: void SetVectorReg(int index, u128 value) override; u32 GetPSTATE() const override; void SetPSTATE(u32 pstate) override; - void Run() override; - void Step() override; VAddr GetTlsAddress() const override; void SetTlsAddress(VAddr address) override; void SetTPIDR_EL0(u64 value) override; @@ -70,6 +68,11 @@ public: std::vector GetBacktrace() const override; +protected: + Dynarmic::HaltReason RunJit() override; + Dynarmic::HaltReason StepJit() override; + u32 GetSvcNumber() const override; + private: std::shared_ptr MakeJit(Common::PageTable* page_table) const; diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.cpp b/src/core/arm/dynarmic/arm_dynarmic_64.cpp index ae5a4b76a..1f0f51c54 100755 --- a/src/core/arm/dynarmic/arm_dynarmic_64.cpp +++ b/src/core/arm/dynarmic/arm_dynarmic_64.cpp @@ -15,6 +15,7 @@ #include "core/arm/dynarmic/arm_exclusive_monitor.h" #include "core/core.h" #include "core/core_timing.h" +#include "core/debugger/debugger.h" #include "core/hardware_properties.h" #include "core/hle/kernel/k_process.h" #include "core/hle/kernel/svc.h" @@ -25,9 +26,6 @@ namespace Core { using Vector = Dynarmic::A64::Vector; using namespace Common::Literals; -constexpr Dynarmic::HaltReason break_loop = Dynarmic::HaltReason::UserDefined2; -constexpr Dynarmic::HaltReason svc_call = Dynarmic::HaltReason::UserDefined3; - class DynarmicCallbacks64 : public Dynarmic::A64::UserCallbacks { public: explicit DynarmicCallbacks64(ARM_Dynarmic_64& parent_) @@ -119,8 +117,13 @@ public: case Dynarmic::A64::Exception::SendEventLocal: case Dynarmic::A64::Exception::Yield: return; - case Dynarmic::A64::Exception::Breakpoint: default: + if (parent.system.DebuggerEnabled()) { + parent.jit.load()->SetPC(pc); + parent.jit.load()->HaltExecution(ARM_Interface::breakpoint); + return; + } + parent.LogBacktrace(); ASSERT_MSG(false, "ExceptionRaised(exception = {}, pc = {:08X}, code = {:08X})", static_cast(exception), pc, MemoryReadCode(pc)); @@ -129,7 +132,7 @@ public: void CallSVC(u32 swi) override { parent.svc_swi = swi; - parent.jit.load()->HaltExecution(svc_call); + parent.jit.load()->HaltExecution(ARM_Interface::svc_call); } void AddTicks(u64 ticks) override { @@ -300,20 +303,16 @@ std::shared_ptr ARM_Dynarmic_64::MakeJit(Common::PageTable* return std::make_shared(config); } -void ARM_Dynarmic_64::Run() { - while (true) { - const auto hr = jit.load()->Run(); - if (Has(hr, svc_call)) { - Kernel::Svc::Call(system, svc_swi); - } - if (Has(hr, break_loop) || !uses_wall_clock) { - break; - } - } +Dynarmic::HaltReason ARM_Dynarmic_64::RunJit() { + return jit.load()->Run(); } -void ARM_Dynarmic_64::Step() { - jit.load()->Step(); +Dynarmic::HaltReason ARM_Dynarmic_64::StepJit() { + return jit.load()->Step(); +} + +u32 ARM_Dynarmic_64::GetSvcNumber() const { + return svc_swi; } ARM_Dynarmic_64::ARM_Dynarmic_64(System& system_, CPUInterrupts& interrupt_handlers_, diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.h b/src/core/arm/dynarmic/arm_dynarmic_64.h index 01a7e4dad..abfbc3c3f 100755 --- a/src/core/arm/dynarmic/arm_dynarmic_64.h +++ b/src/core/arm/dynarmic/arm_dynarmic_64.h @@ -39,8 +39,6 @@ public: void SetVectorReg(int index, u128 value) override; u32 GetPSTATE() const override; void SetPSTATE(u32 pstate) override; - void Run() override; - void Step() override; VAddr GetTlsAddress() const override; void SetTlsAddress(VAddr address) override; void SetTPIDR_EL0(u64 value) override; @@ -64,6 +62,11 @@ public: std::vector GetBacktrace() const override; +protected: + Dynarmic::HaltReason RunJit() override; + Dynarmic::HaltReason StepJit() override; + u32 GetSvcNumber() const override; + private: std::shared_ptr MakeJit(Common::PageTable* page_table, std::size_t address_space_bits) const; diff --git a/src/core/core.cpp b/src/core/core.cpp index 8a887904d..7d974ba65 100755 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -17,6 +17,7 @@ #include "core/core.h" #include "core/core_timing.h" #include "core/cpu_manager.h" +#include "core/debugger/debugger.h" #include "core/device_memory.h" #include "core/file_sys/bis_factory.h" #include "core/file_sys/mode.h" @@ -171,6 +172,10 @@ struct System::Impl { } } + void InitializeDebugger(System& system, u16 port) { + debugger = std::make_unique(system, port); + } + SystemResultStatus Init(System& system, Frontend::EmuWindow& emu_window) { LOG_DEBUG(Core, "initialized OK"); @@ -329,6 +334,7 @@ struct System::Impl { gpu_core->NotifyShutdown(); } + debugger.reset(); services.reset(); service_manager.reset(); cheat_engine.reset(); @@ -436,6 +442,9 @@ struct System::Impl { /// Network instance Network::NetworkInstance network_instance; + /// Debugger + std::unique_ptr debugger; + SystemResultStatus status = SystemResultStatus::Success; std::string status_details = ""; @@ -472,10 +481,6 @@ SystemResultStatus System::Pause() { return impl->Pause(); } -SystemResultStatus System::SingleStep() { - return SystemResultStatus::Success; -} - void System::InvalidateCpuInstructionCaches() { impl->kernel.InvalidateAllInstructionCaches(); } @@ -496,6 +501,10 @@ void System::UnstallCPU() { impl->UnstallCPU(); } +void System::InitializeDebugger() { + impl->InitializeDebugger(*this, Settings::values.gdbstub_port.GetValue()); +} + SystemResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath, u64 program_id, std::size_t program_index) { return impl->Load(*this, emu_window, filepath, program_id, program_index); @@ -809,6 +818,18 @@ bool System::IsMulticore() const { return impl->is_multicore; } +bool System::DebuggerEnabled() const { + return Settings::values.use_gdbstub.GetValue(); +} + +Core::Debugger& System::GetDebugger() { + return *impl->debugger; +} + +const Core::Debugger& System::GetDebugger() const { + return *impl->debugger; +} + void System::RegisterExecuteProgramCallback(ExecuteProgramCallback&& callback) { impl->execute_program_callback = std::move(callback); } diff --git a/src/core/core.h b/src/core/core.h index 4a0c7dc84..94477206e 100755 --- a/src/core/core.h +++ b/src/core/core.h @@ -97,6 +97,7 @@ namespace Core { class ARM_Interface; class CpuManager; +class Debugger; class DeviceMemory; class ExclusiveMonitor; class SpeedLimiter; @@ -147,12 +148,6 @@ public: */ [[nodiscard]] SystemResultStatus Pause(); - /** - * Step the CPU one instruction - * @return Result status, indicating whether or not the operation succeeded. - */ - [[nodiscard]] SystemResultStatus SingleStep(); - /** * Invalidate the CPU instruction caches * This function should only be used by GDB Stub to support breakpoints, memory updates and @@ -168,6 +163,11 @@ public: std::unique_lock StallCPU(); void UnstallCPU(); + /** + * Initialize the debugger. + */ + void InitializeDebugger(); + /** * Load an executable application. * @param emu_window Reference to the host-system window used for video output and keyboard @@ -354,6 +354,9 @@ public: [[nodiscard]] Service::Time::TimeManager& GetTimeManager(); [[nodiscard]] const Service::Time::TimeManager& GetTimeManager() const; + [[nodiscard]] Core::Debugger& GetDebugger(); + [[nodiscard]] const Core::Debugger& GetDebugger() const; + void SetExitLock(bool locked); [[nodiscard]] bool GetExitLock() const; @@ -375,6 +378,9 @@ public: /// Tells if system is running on multicore. [[nodiscard]] bool IsMulticore() const; + /// Tells if the system debugger is enabled. + [[nodiscard]] bool DebuggerEnabled() const; + /// Type used for the frontend to designate a callback for System to re-launch the application /// using a specified program index. using ExecuteProgramCallback = std::function; diff --git a/src/core/debugger/debugger.cpp b/src/core/debugger/debugger.cpp new file mode 100755 index 000000000..68ab33e46 --- /dev/null +++ b/src/core/debugger/debugger.cpp @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include +#include + +#include "common/logging/log.h" +#include "common/thread.h" +#include "core/core.h" +#include "core/debugger/debugger.h" +#include "core/debugger/debugger_interface.h" +#include "core/debugger/gdbstub.h" +#include "core/hle/kernel/global_scheduler_context.h" + +template +static void AsyncReceiveInto(Readable& r, Buffer& buffer, Callback&& c) { + static_assert(std::is_trivial_v); + auto boost_buffer{boost::asio::buffer(&buffer, sizeof(Buffer))}; + r.async_read_some(boost_buffer, [&](const boost::system::error_code& error, size_t bytes_read) { + if (!error.failed()) { + const u8* buffer_start = reinterpret_cast(&buffer); + std::span received_data{buffer_start, buffer_start + bytes_read}; + c(received_data); + } + + AsyncReceiveInto(r, buffer, c); + }); +} + +template +static std::span ReceiveInto(Readable& r, Buffer& buffer) { + static_assert(std::is_trivial_v); + auto boost_buffer{boost::asio::buffer(&buffer, sizeof(Buffer))}; + size_t bytes_read = r.read_some(boost_buffer); + const u8* buffer_start = reinterpret_cast(&buffer); + std::span received_data{buffer_start, buffer_start + bytes_read}; + return received_data; +} + +namespace Core { + +class DebuggerImpl : public DebuggerBackend { +public: + explicit DebuggerImpl(Core::System& system_, u16 port) + : system{system_}, signal_pipe{io_context}, client_socket{io_context} { + frontend = std::make_unique(*this, system); + InitializeServer(port); + } + + ~DebuggerImpl() override { + ShutdownServer(); + } + + bool NotifyThreadStopped(Kernel::KThread* thread) { + std::scoped_lock lk{connection_lock}; + + if (stopped) { + // Do not notify the debugger about another event. + // It should be ignored. + return false; + } + stopped = true; + + signal_pipe.write_some(boost::asio::buffer(&thread, sizeof(thread))); + return true; + } + + std::span ReadFromClient() override { + return ReceiveInto(client_socket, client_data); + } + + void WriteToClient(std::span data) override { + client_socket.write_some(boost::asio::buffer(data.data(), data.size_bytes())); + } + + void SetActiveThread(Kernel::KThread* thread) override { + active_thread = thread; + } + + Kernel::KThread* GetActiveThread() override { + return active_thread; + } + +private: + void InitializeServer(u16 port) { + using boost::asio::ip::tcp; + + LOG_INFO(Debug_GDBStub, "Starting server on port {}...", port); + + // Run the connection thread. + connection_thread = std::jthread([&, port](std::stop_token stop_token) { + try { + // Initialize the listening socket and accept a new client. + tcp::endpoint endpoint{boost::asio::ip::address_v4::loopback(), port}; + tcp::acceptor acceptor{io_context, endpoint}; + + acceptor.async_accept(client_socket, [](const auto&) {}); + io_context.run_one(); + io_context.restart(); + + if (stop_token.stop_requested()) { + return; + } + + ThreadLoop(stop_token); + } catch (const std::exception& ex) { + LOG_CRITICAL(Debug_GDBStub, "Stopping server: {}", ex.what()); + } + }); + } + + void ShutdownServer() { + connection_thread.request_stop(); + io_context.stop(); + connection_thread.join(); + } + + void ThreadLoop(std::stop_token stop_token) { + Common::SetCurrentThreadName("yuzu:Debugger"); + + // Set up the client signals for new data. + AsyncReceiveInto(signal_pipe, active_thread, [&](auto d) { PipeData(d); }); + AsyncReceiveInto(client_socket, client_data, [&](auto d) { ClientData(d); }); + + // Stop the emulated CPU. + AllCoreStop(); + + // Set the active thread. + UpdateActiveThread(); + + // Set up the frontend. + frontend->Connected(); + + // Main event loop. + while (!stop_token.stop_requested() && io_context.run()) { + } + } + + void PipeData(std::span data) { + AllCoreStop(); + UpdateActiveThread(); + frontend->Stopped(active_thread); + } + + void ClientData(std::span data) { + const auto actions{frontend->ClientData(data)}; + for (const auto action : actions) { + switch (action) { + case DebuggerAction::Interrupt: { + { + std::scoped_lock lk{connection_lock}; + stopped = true; + } + AllCoreStop(); + UpdateActiveThread(); + frontend->Stopped(active_thread); + break; + } + case DebuggerAction::Continue: + active_thread->SetStepState(Kernel::StepState::NotStepping); + ResumeInactiveThreads(); + AllCoreResume(); + break; + case DebuggerAction::StepThreadUnlocked: + active_thread->SetStepState(Kernel::StepState::StepPending); + ResumeInactiveThreads(); + AllCoreResume(); + break; + case DebuggerAction::StepThreadLocked: + active_thread->SetStepState(Kernel::StepState::StepPending); + SuspendInactiveThreads(); + AllCoreResume(); + break; + case DebuggerAction::ShutdownEmulation: { + // Suspend all threads and release any locks held + active_thread->RequestSuspend(Kernel::SuspendType::Debug); + SuspendInactiveThreads(); + AllCoreResume(); + + // Spawn another thread that will exit after shutdown, + // to avoid a deadlock + Core::System* system_ref{&system}; + std::thread t([system_ref] { system_ref->Exit(); }); + t.detach(); + break; + } + } + } + } + + void AllCoreStop() { + if (!suspend) { + suspend = system.StallCPU(); + } + } + + void AllCoreResume() { + stopped = false; + system.UnstallCPU(); + suspend.reset(); + } + + void SuspendInactiveThreads() { + for (auto* thread : ThreadList()) { + if (thread != active_thread) { + thread->RequestSuspend(Kernel::SuspendType::Debug); + } + } + } + + void ResumeInactiveThreads() { + for (auto* thread : ThreadList()) { + if (thread != active_thread) { + thread->Resume(Kernel::SuspendType::Debug); + thread->SetStepState(Kernel::StepState::NotStepping); + } + } + } + + void UpdateActiveThread() { + const auto& threads{ThreadList()}; + if (std::find(threads.begin(), threads.end(), active_thread) == threads.end()) { + active_thread = threads[0]; + } + active_thread->Resume(Kernel::SuspendType::Debug); + active_thread->SetStepState(Kernel::StepState::NotStepping); + } + + const std::vector& ThreadList() { + return system.GlobalSchedulerContext().GetThreadList(); + } + +private: + System& system; + std::unique_ptr frontend; + + std::jthread connection_thread; + std::mutex connection_lock; + boost::asio::io_context io_context; + boost::process::async_pipe signal_pipe; + boost::asio::ip::tcp::socket client_socket; + std::optional> suspend; + + Kernel::KThread* active_thread; + bool stopped; + + std::array client_data; +}; + +Debugger::Debugger(Core::System& system, u16 port) { + try { + impl = std::make_unique(system, port); + } catch (const std::exception& ex) { + LOG_CRITICAL(Debug_GDBStub, "Failed to initialize debugger: {}", ex.what()); + } +} + +Debugger::~Debugger() = default; + +bool Debugger::NotifyThreadStopped(Kernel::KThread* thread) { + return impl && impl->NotifyThreadStopped(thread); +} + +} // namespace Core diff --git a/src/core/debugger/debugger.h b/src/core/debugger/debugger.h new file mode 100755 index 000000000..ea36c6ab2 --- /dev/null +++ b/src/core/debugger/debugger.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" + +namespace Kernel { +class KThread; +} + +namespace Core { +class System; + +class DebuggerImpl; + +class Debugger { +public: + /** + * Blocks and waits for a connection on localhost, port `server_port`. + * Does not create the debugger if the port is already in use. + */ + explicit Debugger(Core::System& system, u16 server_port); + ~Debugger(); + + /** + * Notify the debugger that the given thread is stopped + * (due to a breakpoint, or due to stopping after a successful step). + * + * The debugger will asynchronously halt emulation after the notification has + * occurred. If another thread attempts to notify before emulation has stopped, + * it is ignored and this method will return false. Otherwise it will return true. + */ + bool NotifyThreadStopped(Kernel::KThread* thread); + +private: + std::unique_ptr impl; +}; +} // namespace Core diff --git a/src/core/debugger/debugger_interface.h b/src/core/debugger/debugger_interface.h new file mode 100755 index 000000000..35ba0bc61 --- /dev/null +++ b/src/core/debugger/debugger_interface.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "common/common_types.h" + +namespace Kernel { +class KThread; +} + +namespace Core { + +enum class DebuggerAction { + Interrupt, ///< Stop emulation as soon as possible. + Continue, ///< Resume emulation. + StepThreadLocked, ///< Step the currently-active thread without resuming others. + StepThreadUnlocked, ///< Step the currently-active thread and resume others. + ShutdownEmulation, ///< Shut down the emulator. +}; + +class DebuggerBackend { +public: + virtual ~DebuggerBackend() = default; + + /** + * Can be invoked from a callback to synchronously wait for more data. + * Will return as soon as least one byte is received. Reads up to 4096 bytes. + */ + virtual std::span ReadFromClient() = 0; + + /** + * Can be invoked from a callback to write data to the client. + * Returns immediately after the data is sent. + */ + virtual void WriteToClient(std::span data) = 0; + + /** + * Gets the currently active thread when the debugger is stopped. + */ + virtual Kernel::KThread* GetActiveThread() = 0; + + /** + * Sets the currently active thread when the debugger is stopped. + */ + virtual void SetActiveThread(Kernel::KThread* thread) = 0; +}; + +class DebuggerFrontend { +public: + explicit DebuggerFrontend(DebuggerBackend& backend_) : backend{backend_} {} + + virtual ~DebuggerFrontend() = default; + + /** + * Called after the client has successfully connected to the port. + */ + virtual void Connected() = 0; + + /** + * Called when emulation has stopped. + */ + virtual void Stopped(Kernel::KThread* thread) = 0; + + /** + * Called when new data is asynchronously received on the client socket. + * A list of actions to perform is returned. + */ + [[nodiscard]] virtual std::vector ClientData(std::span data) = 0; + +protected: + DebuggerBackend& backend; +}; + +} // namespace Core diff --git a/src/core/debugger/gdbstub.cpp b/src/core/debugger/gdbstub.cpp new file mode 100755 index 000000000..0c36069a6 --- /dev/null +++ b/src/core/debugger/gdbstub.cpp @@ -0,0 +1,467 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include + +#include + +#include "common/hex_util.h" +#include "common/logging/log.h" +#include "common/scope_exit.h" +#include "core/arm/arm_interface.h" +#include "core/core.h" +#include "core/debugger/gdbstub.h" +#include "core/debugger/gdbstub_arch.h" +#include "core/hle/kernel/k_page_table.h" +#include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/k_thread.h" +#include "core/loader/loader.h" +#include "core/memory.h" + +namespace Core { + +constexpr char GDB_STUB_START = '$'; +constexpr char GDB_STUB_END = '#'; +constexpr char GDB_STUB_ACK = '+'; +constexpr char GDB_STUB_NACK = '-'; +constexpr char GDB_STUB_INT3 = 0x03; +constexpr int GDB_STUB_SIGTRAP = 5; + +constexpr char GDB_STUB_REPLY_ERR[] = "E01"; +constexpr char GDB_STUB_REPLY_OK[] = "OK"; +constexpr char GDB_STUB_REPLY_EMPTY[] = ""; + +GDBStub::GDBStub(DebuggerBackend& backend_, Core::System& system_) + : DebuggerFrontend(backend_), system{system_} { + if (system.CurrentProcess()->Is64BitProcess()) { + arch = std::make_unique(); + } else { + arch = std::make_unique(); + } +} + +GDBStub::~GDBStub() = default; + +void GDBStub::Connected() {} + +void GDBStub::Stopped(Kernel::KThread* thread) { + SendReply(arch->ThreadStatus(thread, GDB_STUB_SIGTRAP)); +} + +std::vector GDBStub::ClientData(std::span data) { + std::vector actions; + current_command.insert(current_command.end(), data.begin(), data.end()); + + while (current_command.size() != 0) { + ProcessData(actions); + } + + return actions; +} + +void GDBStub::ProcessData(std::vector& actions) { + const char c{current_command[0]}; + + // Acknowledgement + if (c == GDB_STUB_ACK || c == GDB_STUB_NACK) { + current_command.erase(current_command.begin()); + return; + } + + // Interrupt + if (c == GDB_STUB_INT3) { + LOG_INFO(Debug_GDBStub, "Received interrupt"); + current_command.erase(current_command.begin()); + actions.push_back(DebuggerAction::Interrupt); + SendStatus(GDB_STUB_ACK); + return; + } + + // Otherwise, require the data to be the start of a command + if (c != GDB_STUB_START) { + LOG_ERROR(Debug_GDBStub, "Invalid command buffer contents: {}", current_command.data()); + current_command.clear(); + SendStatus(GDB_STUB_NACK); + return; + } + + // Continue reading until command is complete + while (CommandEnd() == current_command.end()) { + const auto new_data{backend.ReadFromClient()}; + current_command.insert(current_command.end(), new_data.begin(), new_data.end()); + } + + // Execute and respond to GDB + const auto command{DetachCommand()}; + + if (command) { + SendStatus(GDB_STUB_ACK); + ExecuteCommand(*command, actions); + } else { + SendStatus(GDB_STUB_NACK); + } +} + +void GDBStub::ExecuteCommand(std::string_view packet, std::vector& actions) { + LOG_TRACE(Debug_GDBStub, "Executing command: {}", packet); + + if (packet.length() == 0) { + SendReply(GDB_STUB_REPLY_ERR); + return; + } + + if (packet.starts_with("vCont")) { + HandleVCont(packet.substr(5), actions); + return; + } + + std::string_view command{packet.substr(1, packet.size())}; + + switch (packet[0]) { + case 'H': { + Kernel::KThread* thread{nullptr}; + s64 thread_id{strtoll(command.data() + 1, nullptr, 16)}; + if (thread_id >= 1) { + thread = GetThreadByID(thread_id); + } else { + thread = backend.GetActiveThread(); + } + + if (thread) { + SendReply(GDB_STUB_REPLY_OK); + backend.SetActiveThread(thread); + } else { + SendReply(GDB_STUB_REPLY_ERR); + } + break; + } + case 'T': { + s64 thread_id{strtoll(command.data(), nullptr, 16)}; + if (GetThreadByID(thread_id)) { + SendReply(GDB_STUB_REPLY_OK); + } else { + SendReply(GDB_STUB_REPLY_ERR); + } + break; + } + case 'Q': + case 'q': + HandleQuery(command); + break; + case '?': + SendReply(arch->ThreadStatus(backend.GetActiveThread(), GDB_STUB_SIGTRAP)); + break; + case 'k': + LOG_INFO(Debug_GDBStub, "Shutting down emulation"); + actions.push_back(DebuggerAction::ShutdownEmulation); + break; + case 'g': + SendReply(arch->ReadRegisters(backend.GetActiveThread())); + break; + case 'G': + arch->WriteRegisters(backend.GetActiveThread(), command); + SendReply(GDB_STUB_REPLY_OK); + break; + case 'p': { + const size_t reg{static_cast(strtoll(command.data(), nullptr, 16))}; + SendReply(arch->RegRead(backend.GetActiveThread(), reg)); + break; + } + case 'P': { + const auto sep{std::find(command.begin(), command.end(), '=') - command.begin() + 1}; + const size_t reg{static_cast(strtoll(command.data(), nullptr, 16))}; + arch->RegWrite(backend.GetActiveThread(), reg, std::string_view(command).substr(sep)); + break; + } + case 'm': { + const auto sep{std::find(command.begin(), command.end(), ',') - command.begin() + 1}; + const size_t addr{static_cast(strtoll(command.data(), nullptr, 16))}; + const size_t size{static_cast(strtoll(command.data() + sep, nullptr, 16))}; + + if (system.Memory().IsValidVirtualAddressRange(addr, size)) { + std::vector mem(size); + system.Memory().ReadBlock(addr, mem.data(), size); + + SendReply(Common::HexToString(mem)); + } else { + SendReply(GDB_STUB_REPLY_ERR); + } + break; + } + case 'M': { + const auto size_sep{std::find(command.begin(), command.end(), ',') - command.begin() + 1}; + const auto mem_sep{std::find(command.begin(), command.end(), ':') - command.begin() + 1}; + + const size_t addr{static_cast(strtoll(command.data(), nullptr, 16))}; + const size_t size{static_cast(strtoll(command.data() + size_sep, nullptr, 16))}; + + const auto mem_substr{std::string_view(command).substr(mem_sep)}; + const auto mem{Common::HexStringToVector(mem_substr, false)}; + + if (system.Memory().IsValidVirtualAddressRange(addr, size)) { + system.Memory().WriteBlock(addr, mem.data(), size); + system.InvalidateCpuInstructionCacheRange(addr, size); + SendReply(GDB_STUB_REPLY_OK); + } else { + SendReply(GDB_STUB_REPLY_ERR); + } + break; + } + case 's': + actions.push_back(DebuggerAction::StepThreadLocked); + break; + case 'C': + case 'c': + actions.push_back(DebuggerAction::Continue); + break; + case 'Z': { + const auto addr_sep{std::find(command.begin(), command.end(), ',') - command.begin() + 1}; + const size_t addr{static_cast(strtoll(command.data() + addr_sep, nullptr, 16))}; + + if (system.Memory().IsValidVirtualAddress(addr)) { + replaced_instructions[addr] = system.Memory().Read32(addr); + system.Memory().Write32(addr, arch->BreakpointInstruction()); + system.InvalidateCpuInstructionCacheRange(addr, sizeof(u32)); + + SendReply(GDB_STUB_REPLY_OK); + } else { + SendReply(GDB_STUB_REPLY_ERR); + } + break; + } + case 'z': { + const auto addr_sep{std::find(command.begin(), command.end(), ',') - command.begin() + 1}; + const size_t addr{static_cast(strtoll(command.data() + addr_sep, nullptr, 16))}; + + const auto orig_insn{replaced_instructions.find(addr)}; + if (system.Memory().IsValidVirtualAddress(addr) && + orig_insn != replaced_instructions.end()) { + system.Memory().Write32(addr, orig_insn->second); + system.InvalidateCpuInstructionCacheRange(addr, sizeof(u32)); + replaced_instructions.erase(addr); + + SendReply(GDB_STUB_REPLY_OK); + } else { + SendReply(GDB_STUB_REPLY_ERR); + } + break; + } + default: + SendReply(GDB_STUB_REPLY_EMPTY); + break; + } +} + +static std::string_view GetThreadWaitReason(const Kernel::KThread* thread) { + switch (thread->GetWaitReasonForDebugging()) { + case Kernel::ThreadWaitReasonForDebugging::Sleep: + return "Sleep"; + case Kernel::ThreadWaitReasonForDebugging::IPC: + return "IPC"; + case Kernel::ThreadWaitReasonForDebugging::Synchronization: + return "Synchronization"; + case Kernel::ThreadWaitReasonForDebugging::ConditionVar: + return "ConditionVar"; + case Kernel::ThreadWaitReasonForDebugging::Arbitration: + return "Arbitration"; + case Kernel::ThreadWaitReasonForDebugging::Suspended: + return "Suspended"; + default: + return "Unknown"; + } +} + +static std::string GetThreadState(const Kernel::KThread* thread) { + switch (thread->GetState()) { + case Kernel::ThreadState::Initialized: + return "Initialized"; + case Kernel::ThreadState::Waiting: + return fmt::format("Waiting ({})", GetThreadWaitReason(thread)); + case Kernel::ThreadState::Runnable: + return "Runnable"; + case Kernel::ThreadState::Terminated: + return "Terminated"; + default: + return "Unknown"; + } +} + +void GDBStub::HandleQuery(std::string_view command) { + if (command.starts_with("TStatus")) { + // no tracepoint support + SendReply("T0"); + } else if (command.starts_with("Supported")) { + SendReply("PacketSize=4000;qXfer:features:read+;qXfer:threads:read+;qXfer:libraries:read+;" + "vContSupported+;QStartNoAckMode+"); + } else if (command.starts_with("Xfer:features:read:target.xml:")) { + const auto offset{command.substr(30)}; + const auto amount{command.substr(command.find(',') + 1)}; + + const auto offset_val{static_cast(strtoll(offset.data(), nullptr, 16))}; + const auto amount_val{static_cast(strtoll(amount.data(), nullptr, 16))}; + const auto target_xml{arch->GetTargetXML()}; + + if (offset_val + amount_val > target_xml.size()) { + SendReply("l" + target_xml.substr(offset_val)); + } else { + SendReply("m" + target_xml.substr(offset_val, amount_val)); + } + } else if (command.starts_with("Offsets")) { + Loader::AppLoader::Modules modules; + system.GetAppLoader().ReadNSOModules(modules); + + const auto main = std::find_if(modules.begin(), modules.end(), + [](const auto& key) { return key.second == "main"; }); + if (main != modules.end()) { + SendReply(fmt::format("TextSeg={:x}", main->first)); + } else { + SendReply(fmt::format("TextSeg={:x}", + system.CurrentProcess()->PageTable().GetCodeRegionStart())); + } + } else if (command.starts_with("fThreadInfo")) { + // beginning of list + const auto& threads = system.GlobalSchedulerContext().GetThreadList(); + std::vector thread_ids; + for (const auto& thread : threads) { + thread_ids.push_back(fmt::format("{:x}", thread->GetThreadID())); + } + SendReply(fmt::format("m{}", fmt::join(thread_ids, ","))); + } else if (command.starts_with("sThreadInfo")) { + // end of list + SendReply("l"); + } else if (command.starts_with("Xfer:threads:read")) { + std::string buffer; + buffer += R"(l)"; + buffer += ""; + + const auto& threads = system.GlobalSchedulerContext().GetThreadList(); + for (const auto& thread : threads) { + buffer += fmt::format(R"({})", + thread->GetThreadID(), thread->GetActiveCore(), + thread->GetThreadID(), GetThreadState(thread)); + } + + buffer += ""; + SendReply(buffer); + } else if (command.starts_with("Attached")) { + SendReply("0"); + } else if (command.starts_with("StartNoAckMode")) { + no_ack = true; + SendReply(GDB_STUB_REPLY_OK); + } else { + SendReply(GDB_STUB_REPLY_EMPTY); + } +} + +void GDBStub::HandleVCont(std::string_view command, std::vector& actions) { + if (command == "?") { + // Continuing and stepping are supported + // (signal is ignored, but required for GDB to use vCont) + SendReply("vCont;c;C;s;S"); + return; + } + + Kernel::KThread* stepped_thread{nullptr}; + bool lock_execution{true}; + + std::vector entries; + boost::split(entries, command.substr(1), boost::is_any_of(";")); + for (const auto& thread_action : entries) { + std::vector parts; + boost::split(parts, thread_action, boost::is_any_of(":")); + + if (parts.size() == 1 && (parts[0] == "c" || parts[0].starts_with("C"))) { + lock_execution = false; + } + if (parts.size() == 2 && (parts[0] == "s" || parts[0].starts_with("S"))) { + stepped_thread = GetThreadByID(strtoll(parts[1].data(), nullptr, 16)); + } + } + + if (stepped_thread) { + backend.SetActiveThread(stepped_thread); + actions.push_back(lock_execution ? DebuggerAction::StepThreadLocked + : DebuggerAction::StepThreadUnlocked); + } else { + actions.push_back(DebuggerAction::Continue); + } +} + +Kernel::KThread* GDBStub::GetThreadByID(u64 thread_id) { + const auto& threads{system.GlobalSchedulerContext().GetThreadList()}; + for (auto* thread : threads) { + if (thread->GetThreadID() == thread_id) { + return thread; + } + } + + return nullptr; +} + +std::vector::const_iterator GDBStub::CommandEnd() const { + // Find the end marker + const auto end{std::find(current_command.begin(), current_command.end(), GDB_STUB_END)}; + + // Require the checksum to be present + return std::min(end + 2, current_command.end()); +} + +std::optional GDBStub::DetachCommand() { + // Slice the string part from the beginning to the end marker + const auto end{CommandEnd()}; + + // Extract possible command data + std::string data(current_command.data(), end - current_command.begin() + 1); + + // Shift over the remaining contents + current_command.erase(current_command.begin(), end + 1); + + // Validate received command + if (data[0] != GDB_STUB_START) { + LOG_ERROR(Debug_GDBStub, "Invalid start data: {}", data[0]); + return std::nullopt; + } + + u8 calculated = CalculateChecksum(std::string_view(data).substr(1, data.size() - 4)); + u8 received = static_cast(strtoll(data.data() + data.size() - 2, nullptr, 16)); + + // Verify checksum + if (calculated != received) { + LOG_ERROR(Debug_GDBStub, "Checksum mismatch: calculated {:02x}, received {:02x}", + calculated, received); + return std::nullopt; + } + + return data.substr(1, data.size() - 4); +} + +u8 GDBStub::CalculateChecksum(std::string_view data) { + return std::accumulate(data.begin(), data.end(), u8{0}, + [](u8 lhs, u8 rhs) { return static_cast(lhs + rhs); }); +} + +void GDBStub::SendReply(std::string_view data) { + const auto output{ + fmt::format("{}{}{}{:02x}", GDB_STUB_START, data, GDB_STUB_END, CalculateChecksum(data))}; + LOG_TRACE(Debug_GDBStub, "Writing reply: {}", output); + + // C++ string support is complete rubbish + const u8* output_begin = reinterpret_cast(output.data()); + const u8* output_end = output_begin + output.size(); + backend.WriteToClient(std::span(output_begin, output_end)); +} + +void GDBStub::SendStatus(char status) { + if (no_ack) { + return; + } + + std::array buf = {static_cast(status)}; + LOG_TRACE(Debug_GDBStub, "Writing status: {}", status); + backend.WriteToClient(buf); +} + +} // namespace Core diff --git a/src/core/debugger/gdbstub.h b/src/core/debugger/gdbstub.h new file mode 100755 index 000000000..aa1f7de6c --- /dev/null +++ b/src/core/debugger/gdbstub.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "core/debugger/debugger_interface.h" +#include "core/debugger/gdbstub_arch.h" + +namespace Core { + +class System; + +class GDBStub : public DebuggerFrontend { +public: + explicit GDBStub(DebuggerBackend& backend, Core::System& system); + ~GDBStub() override; + + void Connected() override; + void Stopped(Kernel::KThread* thread) override; + std::vector ClientData(std::span data) override; + +private: + void ProcessData(std::vector& actions); + void ExecuteCommand(std::string_view packet, std::vector& actions); + void HandleVCont(std::string_view command, std::vector& actions); + void HandleQuery(std::string_view command); + std::vector::const_iterator CommandEnd() const; + std::optional DetachCommand(); + Kernel::KThread* GetThreadByID(u64 thread_id); + + static u8 CalculateChecksum(std::string_view data); + void SendReply(std::string_view data); + void SendStatus(char status); + +private: + Core::System& system; + std::unique_ptr arch; + std::vector current_command; + std::map replaced_instructions; + bool no_ack{}; +}; + +} // namespace Core diff --git a/src/core/debugger/gdbstub_arch.cpp b/src/core/debugger/gdbstub_arch.cpp new file mode 100755 index 000000000..99e3893a9 --- /dev/null +++ b/src/core/debugger/gdbstub_arch.cpp @@ -0,0 +1,406 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/hex_util.h" +#include "core/debugger/gdbstub_arch.h" +#include "core/hle/kernel/k_thread.h" + +namespace Core { + +template +static T HexToValue(std::string_view hex) { + static_assert(std::is_trivially_copyable_v); + T value{}; + const auto mem{Common::HexStringToVector(hex, false)}; + std::memcpy(&value, mem.data(), std::min(mem.size(), sizeof(T))); + return value; +} + +template +static std::string ValueToHex(const T value) { + static_assert(std::is_trivially_copyable_v); + std::array mem{}; + std::memcpy(mem.data(), &value, sizeof(T)); + return Common::HexToString(mem); +} + +template +static T GetSIMDRegister(const std::array& simd_regs, size_t offset) { + static_assert(std::is_trivially_copyable_v); + T value{}; + std::memcpy(&value, reinterpret_cast(simd_regs.data()) + sizeof(T) * offset, + sizeof(T)); + return value; +} + +template +static void PutSIMDRegister(std::array& simd_regs, size_t offset, const T value) { + static_assert(std::is_trivially_copyable_v); + std::memcpy(reinterpret_cast(simd_regs.data()) + sizeof(T) * offset, &value, sizeof(T)); +} + +// For sample XML files see the GDB source /gdb/features +// This XML defines what the registers are for this specific ARM device +std::string GDBStubA64::GetTargetXML() const { + constexpr const char* target_xml = + R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + + return target_xml; +} + +std::string GDBStubA64::RegRead(const Kernel::KThread* thread, size_t id) const { + if (!thread) { + return ""; + } + + const auto& context{thread->GetContext64()}; + const auto& gprs{context.cpu_registers}; + const auto& fprs{context.vector_registers}; + + if (id <= SP_REGISTER) { + return ValueToHex(gprs[id]); + } else if (id == PC_REGISTER) { + return ValueToHex(context.pc); + } else if (id == PSTATE_REGISTER) { + return ValueToHex(context.pstate); + } else if (id >= Q0_REGISTER && id < FPCR_REGISTER) { + return ValueToHex(fprs[id - Q0_REGISTER]); + } else if (id == FPCR_REGISTER) { + return ValueToHex(context.fpcr); + } else if (id == FPSR_REGISTER) { + return ValueToHex(context.fpsr); + } else { + return ""; + } +} + +void GDBStubA64::RegWrite(Kernel::KThread* thread, size_t id, std::string_view value) const { + if (!thread) { + return; + } + + auto& context{thread->GetContext64()}; + + if (id <= SP_REGISTER) { + context.cpu_registers[id] = HexToValue(value); + } else if (id == PC_REGISTER) { + context.pc = HexToValue(value); + } else if (id == PSTATE_REGISTER) { + context.pstate = HexToValue(value); + } else if (id >= Q0_REGISTER && id < FPCR_REGISTER) { + context.vector_registers[id - Q0_REGISTER] = HexToValue(value); + } else if (id == FPCR_REGISTER) { + context.fpcr = HexToValue(value); + } else if (id == FPSR_REGISTER) { + context.fpsr = HexToValue(value); + } +} + +std::string GDBStubA64::ReadRegisters(const Kernel::KThread* thread) const { + std::string output; + + for (size_t reg = 0; reg <= FPCR_REGISTER; reg++) { + output += RegRead(thread, reg); + } + + return output; +} + +void GDBStubA64::WriteRegisters(Kernel::KThread* thread, std::string_view register_data) const { + for (size_t i = 0, reg = 0; reg <= FPCR_REGISTER; reg++) { + if (reg <= SP_REGISTER || reg == PC_REGISTER) { + RegWrite(thread, reg, register_data.substr(i, 16)); + i += 16; + } else if (reg == PSTATE_REGISTER || reg == FPCR_REGISTER || reg == FPSR_REGISTER) { + RegWrite(thread, reg, register_data.substr(i, 8)); + i += 8; + } else if (reg >= Q0_REGISTER && reg < FPCR_REGISTER) { + RegWrite(thread, reg, register_data.substr(i, 32)); + i += 32; + } + } +} + +std::string GDBStubA64::ThreadStatus(const Kernel::KThread* thread, u8 signal) const { + return fmt::format("T{:02x}{:02x}:{};{:02x}:{};{:02x}:{};thread:{:x};", signal, PC_REGISTER, + RegRead(thread, PC_REGISTER), SP_REGISTER, RegRead(thread, SP_REGISTER), + LR_REGISTER, RegRead(thread, LR_REGISTER), thread->GetThreadID()); +} + +u32 GDBStubA64::BreakpointInstruction() const { + // A64: brk #0 + return 0xd4200000; +} + +std::string GDBStubA32::GetTargetXML() const { + constexpr const char* target_xml = + R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + + return target_xml; +} + +std::string GDBStubA32::RegRead(const Kernel::KThread* thread, size_t id) const { + if (!thread) { + return ""; + } + + const auto& context{thread->GetContext32()}; + const auto& gprs{context.cpu_registers}; + const auto& fprs{context.extension_registers}; + + if (id <= PC_REGISTER) { + return ValueToHex(gprs[id]); + } else if (id == CPSR_REGISTER) { + return ValueToHex(context.cpsr); + } else if (id >= D0_REGISTER && id < Q0_REGISTER) { + const u64 dN{GetSIMDRegister(fprs, id - D0_REGISTER)}; + return ValueToHex(dN); + } else if (id >= Q0_REGISTER && id < FPSCR_REGISTER) { + const u128 qN{GetSIMDRegister(fprs, id - Q0_REGISTER)}; + return ValueToHex(qN); + } else if (id == FPSCR_REGISTER) { + return ValueToHex(context.fpscr); + } else { + return ""; + } +} + +void GDBStubA32::RegWrite(Kernel::KThread* thread, size_t id, std::string_view value) const { + if (!thread) { + return; + } + + auto& context{thread->GetContext32()}; + auto& fprs{context.extension_registers}; + + if (id <= PC_REGISTER) { + context.cpu_registers[id] = HexToValue(value); + } else if (id == CPSR_REGISTER) { + context.cpsr = HexToValue(value); + } else if (id >= D0_REGISTER && id < Q0_REGISTER) { + PutSIMDRegister(fprs, id - D0_REGISTER, HexToValue(value)); + } else if (id >= Q0_REGISTER && id < FPSCR_REGISTER) { + PutSIMDRegister(fprs, id - Q0_REGISTER, HexToValue(value)); + } else if (id == FPSCR_REGISTER) { + context.fpscr = HexToValue(value); + } +} + +std::string GDBStubA32::ReadRegisters(const Kernel::KThread* thread) const { + std::string output; + + for (size_t reg = 0; reg <= FPSCR_REGISTER; reg++) { + const bool gpr{reg <= PC_REGISTER}; + const bool dfpr{reg >= D0_REGISTER && reg < Q0_REGISTER}; + const bool qfpr{reg >= Q0_REGISTER && reg < FPSCR_REGISTER}; + + if (!(gpr || dfpr || qfpr || reg == CPSR_REGISTER || reg == FPSCR_REGISTER)) { + continue; + } + + output += RegRead(thread, reg); + } + + return output; +} + +void GDBStubA32::WriteRegisters(Kernel::KThread* thread, std::string_view register_data) const { + for (size_t i = 0, reg = 0; reg <= FPSCR_REGISTER; reg++) { + const bool gpr{reg <= PC_REGISTER}; + const bool dfpr{reg >= D0_REGISTER && reg < Q0_REGISTER}; + const bool qfpr{reg >= Q0_REGISTER && reg < FPSCR_REGISTER}; + + if (gpr || reg == CPSR_REGISTER || reg == FPSCR_REGISTER) { + RegWrite(thread, reg, register_data.substr(i, 8)); + i += 8; + } else if (dfpr) { + RegWrite(thread, reg, register_data.substr(i, 16)); + i += 16; + } else if (qfpr) { + RegWrite(thread, reg, register_data.substr(i, 32)); + i += 32; + } + + if (reg == PC_REGISTER) { + reg = CPSR_REGISTER - 1; + } else if (reg == CPSR_REGISTER) { + reg = D0_REGISTER - 1; + } + } +} + +std::string GDBStubA32::ThreadStatus(const Kernel::KThread* thread, u8 signal) const { + return fmt::format("T{:02x}{:02x}:{};{:02x}:{};{:02x}:{};thread:{:x};", signal, PC_REGISTER, + RegRead(thread, PC_REGISTER), SP_REGISTER, RegRead(thread, SP_REGISTER), + LR_REGISTER, RegRead(thread, LR_REGISTER), thread->GetThreadID()); +} + +u32 GDBStubA32::BreakpointInstruction() const { + // A32: trap + // T32: trap + b #4 + return 0xe7ffdefe; +} + +} // namespace Core diff --git a/src/core/debugger/gdbstub_arch.h b/src/core/debugger/gdbstub_arch.h new file mode 100755 index 000000000..e943848e5 --- /dev/null +++ b/src/core/debugger/gdbstub_arch.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" + +namespace Kernel { +class KThread; +} + +namespace Core { + +class GDBStubArch { +public: + virtual std::string GetTargetXML() const = 0; + virtual std::string RegRead(const Kernel::KThread* thread, size_t id) const = 0; + virtual void RegWrite(Kernel::KThread* thread, size_t id, std::string_view value) const = 0; + virtual std::string ReadRegisters(const Kernel::KThread* thread) const = 0; + virtual void WriteRegisters(Kernel::KThread* thread, std::string_view register_data) const = 0; + virtual std::string ThreadStatus(const Kernel::KThread* thread, u8 signal) const = 0; + virtual u32 BreakpointInstruction() const = 0; +}; + +class GDBStubA64 final : public GDBStubArch { +public: + std::string GetTargetXML() const override; + std::string RegRead(const Kernel::KThread* thread, size_t id) const override; + void RegWrite(Kernel::KThread* thread, size_t id, std::string_view value) const override; + std::string ReadRegisters(const Kernel::KThread* thread) const override; + void WriteRegisters(Kernel::KThread* thread, std::string_view register_data) const override; + std::string ThreadStatus(const Kernel::KThread* thread, u8 signal) const override; + u32 BreakpointInstruction() const override; + +private: + static constexpr u32 LR_REGISTER = 30; + static constexpr u32 SP_REGISTER = 31; + static constexpr u32 PC_REGISTER = 32; + static constexpr u32 PSTATE_REGISTER = 33; + static constexpr u32 Q0_REGISTER = 34; + static constexpr u32 FPCR_REGISTER = 66; + static constexpr u32 FPSR_REGISTER = 67; +}; + +class GDBStubA32 final : public GDBStubArch { +public: + std::string GetTargetXML() const override; + std::string RegRead(const Kernel::KThread* thread, size_t id) const override; + void RegWrite(Kernel::KThread* thread, size_t id, std::string_view value) const override; + std::string ReadRegisters(const Kernel::KThread* thread) const override; + void WriteRegisters(Kernel::KThread* thread, std::string_view register_data) const override; + std::string ThreadStatus(const Kernel::KThread* thread, u8 signal) const override; + u32 BreakpointInstruction() const override; + +private: + static constexpr u32 SP_REGISTER = 13; + static constexpr u32 LR_REGISTER = 14; + static constexpr u32 PC_REGISTER = 15; + static constexpr u32 CPSR_REGISTER = 25; + static constexpr u32 D0_REGISTER = 32; + static constexpr u32 Q0_REGISTER = 64; + static constexpr u32 FPSCR_REGISTER = 80; +}; + +} // namespace Core diff --git a/src/core/hle/kernel/k_process.cpp b/src/core/hle/kernel/k_process.cpp index 490e31fc7..dcfeacccd 100755 --- a/src/core/hle/kernel/k_process.cpp +++ b/src/core/hle/kernel/k_process.cpp @@ -64,6 +64,10 @@ void SetupMainThread(Core::System& system, KProcess& owner_process, u32 priority { KScopedSchedulerLock lock{kernel}; thread->SetState(ThreadState::Runnable); + + if (system.DebuggerEnabled()) { + thread->RequestSuspend(SuspendType::Debug); + } } } } // Anonymous namespace diff --git a/src/core/hle/kernel/k_thread.h b/src/core/hle/kernel/k_thread.h index b55a922ab..60ae0da78 100755 --- a/src/core/hle/kernel/k_thread.h +++ b/src/core/hle/kernel/k_thread.h @@ -100,6 +100,12 @@ enum class ThreadWaitReasonForDebugging : u32 { Suspended, ///< Thread is waiting due to process suspension }; +enum class StepState : u32 { + NotStepping, ///< Thread is not currently stepping + StepPending, ///< Thread will step when next scheduled + StepPerformed, ///< Thread has stepped, waiting to be scheduled again +}; + [[nodiscard]] KThread* GetCurrentThreadPointer(KernelCore& kernel); [[nodiscard]] KThread& GetCurrentThread(KernelCore& kernel); [[nodiscard]] s32 GetCurrentCoreId(KernelCore& kernel); @@ -267,6 +273,14 @@ public: void SetState(ThreadState state); + [[nodiscard]] StepState GetStepState() const { + return step_state; + } + + void SetStepState(StepState state) { + step_state = state; + } + [[nodiscard]] s64 GetLastScheduledTick() const { return last_scheduled_tick; } @@ -769,6 +783,7 @@ private: std::shared_ptr host_context{}; bool is_single_core{}; ThreadType thread_type{}; + StepState step_state{}; std::mutex dummy_wait_lock; std::condition_variable dummy_wait_cv; diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 28d30eee2..7534de01e 100755 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -594,6 +594,19 @@ bool Memory::IsValidVirtualAddress(const VAddr vaddr) const { return pointer != nullptr || type == Common::PageType::RasterizerCachedMemory; } +bool Memory::IsValidVirtualAddressRange(VAddr base, u64 size) const { + VAddr end = base + size; + VAddr page = Common::AlignDown(base, PAGE_SIZE); + + for (; page < end; page += PAGE_SIZE) { + if (!IsValidVirtualAddress(page)) { + return false; + } + } + + return true; +} + u8* Memory::GetPointer(VAddr vaddr) { return impl->GetPointer(vaddr); } diff --git a/src/core/memory.h b/src/core/memory.h index b5721b740..58cc27b29 100755 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -95,6 +95,17 @@ public: */ [[nodiscard]] bool IsValidVirtualAddress(VAddr vaddr) const; + /** + * Checks whether or not the supplied range of addresses are all valid + * virtual addresses for the current process. + * + * @param base The address to begin checking. + * @param size The amount of bytes to check. + * + * @returns True if all bytes in the given range are valid, false otherwise. + */ + [[nodiscard]] bool IsValidVirtualAddressRange(VAddr base, u64 size) const; + /** * Gets a pointer to the given address. * diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 6a6325e38..256695804 100755 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -277,3 +277,7 @@ else() $<$:-Werror=unused-but-set-variable> ) endif() + +if (ARCHITECTURE_x86_64) + target_link_libraries(video_core PRIVATE dynarmic) +endif() diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 39989885d..4ca779d80 100755 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -321,3 +321,7 @@ endif() if (NOT APPLE) target_compile_definitions(yuzu PRIVATE HAS_OPENGL) endif() + +if (ARCHITECTURE_x86_64) + target_link_libraries(yuzu PRIVATE dynarmic) +endif() diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 8f0a6bbb8..aae2de2f8 100755 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -50,6 +50,7 @@ void EmuThread::run() { auto& gpu = system.GPU(); auto stop_token = stop_source.get_token(); + bool debugger_should_start = system.DebuggerEnabled(); system.RegisterHostThread(); @@ -89,6 +90,12 @@ void EmuThread::run() { this->SetRunning(false); emit ErrorThrown(result, system.GetStatusDetails()); } + + if (debugger_should_start) { + system.InitializeDebugger(); + debugger_should_start = false; + } + running_wait.Wait(); result = system.Pause(); if (result != Core::SystemResultStatus::Success) { @@ -102,11 +109,9 @@ void EmuThread::run() { was_active = true; emit DebugModeEntered(); } - } else if (exec_step) { - UNIMPLEMENTED(); } else { std::unique_lock lock{running_mutex}; - running_cv.wait(lock, stop_token, [this] { return IsRunning() || exec_step; }); + running_cv.wait(lock, stop_token, [this] { return IsRunning(); }); } } diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index 841816564..87c559e7a 100755 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -54,15 +54,6 @@ public: */ void run() override; - /** - * Steps the emulation thread by a single CPU instruction (if the CPU is not already running) - * @note This function is thread-safe - */ - void ExecStep() { - exec_step = true; - running_cv.notify_all(); - } - /** * Sets whether the emulation thread is running or not * @param running Boolean value, set the emulation thread to running if true @@ -99,7 +90,6 @@ public: } private: - bool exec_step = false; bool running = false; std::stop_source stop_source; std::mutex running_mutex; diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 8b95b677b..9a938d272 100755 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -525,6 +525,9 @@ void Config::ReadDebuggingValues() { // Intentionally not using the QT default setting as this is intended to be changed in the ini Settings::values.record_frame_times = qt_config->value(QStringLiteral("record_frame_times"), false).toBool(); + + ReadBasicSetting(Settings::values.use_gdbstub); + ReadBasicSetting(Settings::values.gdbstub_port); ReadBasicSetting(Settings::values.program_args); ReadBasicSetting(Settings::values.dump_exefs); ReadBasicSetting(Settings::values.dump_nso); @@ -1102,6 +1105,8 @@ void Config::SaveDebuggingValues() { // Intentionally not using the QT default setting as this is intended to be changed in the ini qt_config->setValue(QStringLiteral("record_frame_times"), Settings::values.record_frame_times); + WriteBasicSetting(Settings::values.use_gdbstub); + WriteBasicSetting(Settings::values.gdbstub_port); WriteBasicSetting(Settings::values.program_args); WriteBasicSetting(Settings::values.dump_exefs); WriteBasicSetting(Settings::values.dump_nso); diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp index d6e8b5ead..343d2aee1 100755 --- a/src/yuzu/configuration/configure_debug.cpp +++ b/src/yuzu/configuration/configure_debug.cpp @@ -24,13 +24,18 @@ ConfigureDebug::ConfigureDebug(const Core::System& system_, QWidget* parent) QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::LogDir)); QDesktopServices::openUrl(QUrl::fromLocalFile(path)); }); + + connect(ui->toggle_gdbstub, &QCheckBox::toggled, + [&]() { ui->gdbport_spinbox->setEnabled(ui->toggle_gdbstub->isChecked()); }); } ConfigureDebug::~ConfigureDebug() = default; void ConfigureDebug::SetConfiguration() { const bool runtime_lock = !system.IsPoweredOn(); - + ui->toggle_gdbstub->setChecked(Settings::values.use_gdbstub.GetValue()); + ui->gdbport_spinbox->setEnabled(Settings::values.use_gdbstub.GetValue()); + ui->gdbport_spinbox->setValue(Settings::values.gdbstub_port.GetValue()); ui->toggle_console->setEnabled(runtime_lock); ui->toggle_console->setChecked(UISettings::values.show_console.GetValue()); ui->log_filter_edit->setText(QString::fromStdString(Settings::values.log_filter.GetValue())); @@ -71,6 +76,8 @@ void ConfigureDebug::SetConfiguration() { } void ConfigureDebug::ApplyConfiguration() { + Settings::values.use_gdbstub = ui->toggle_gdbstub->isChecked(); + Settings::values.gdbstub_port = ui->gdbport_spinbox->value(); UISettings::values.show_console = ui->toggle_console->isChecked(); Settings::values.log_filter = ui->log_filter_edit->text().toStdString(); Settings::values.program_args = ui->homebrew_args_edit->text().toStdString(); diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui index 863a3fd57..1152fa6c6 100755 --- a/src/yuzu/configuration/configure_debug.ui +++ b/src/yuzu/configuration/configure_debug.ui @@ -3,6 +3,60 @@ ConfigureDebug + + + + + + Debugger + + + + + + + + Enable GDB Stub + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Port: + + + + + + + 1024 + + + 65535 + + + + + + + + + + diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index d55733932..57d3113cd 100755 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -3174,7 +3174,7 @@ void GMainWindow::OnTasStateChanged() { } void GMainWindow::UpdateStatusBar() { - if (emu_thread == nullptr) { + if (emu_thread == nullptr || !system->IsPoweredOn()) { status_bar_update_timer.stop(); return; }