core/debugger: Implement new GDB stub debugger
This commit is contained in:
		
							
								
								
									
										259
									
								
								src/core/debugger/debugger.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								src/core/debugger/debugger.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,259 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| #include <mutex> | ||||
| #include <thread> | ||||
|  | ||||
| #include <boost/asio.hpp> | ||||
| #include <boost/process/async_pipe.hpp> | ||||
|  | ||||
| #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 <typename Readable, typename Buffer, typename Callback> | ||||
| static void AsyncReceiveInto(Readable& r, Buffer& buffer, Callback&& c) { | ||||
|     static_assert(std::is_trivial_v<Buffer>); | ||||
|     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<const u8*>(&buffer); | ||||
|             std::span<const u8> received_data{buffer_start, buffer_start + bytes_read}; | ||||
|             c(received_data); | ||||
|         } | ||||
|  | ||||
|         AsyncReceiveInto(r, buffer, c); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| template <typename Readable, typename Buffer> | ||||
| static std::span<const u8> ReceiveInto(Readable& r, Buffer& buffer) { | ||||
|     static_assert(std::is_trivial_v<Buffer>); | ||||
|     auto boost_buffer{boost::asio::buffer(&buffer, sizeof(Buffer))}; | ||||
|     size_t bytes_read = r.read_some(boost_buffer); | ||||
|     const u8* buffer_start = reinterpret_cast<const u8*>(&buffer); | ||||
|     std::span<const u8> 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<GDBStub>(*this, system); | ||||
|         InitializeServer(port); | ||||
|     } | ||||
|  | ||||
|     ~DebuggerImpl() { | ||||
|         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<const u8> ReadFromClient() override { | ||||
|         return ReceiveInto(client_socket, client_data); | ||||
|     } | ||||
|  | ||||
|     void WriteToClient(std::span<const u8> 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; | ||||
|     } | ||||
|  | ||||
|     bool IsStepping() const { | ||||
|         return stepping; | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     void InitializeServer(u16 port) { | ||||
|         using boost::asio::ip::tcp; | ||||
|  | ||||
|         LOG_INFO(Debug_GDBStub, "Starting server on port {}...", port); | ||||
|  | ||||
|         // 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}; | ||||
|         client_socket = acceptor.accept(); | ||||
|  | ||||
|         // Run the connection thread. | ||||
|         connection_thread = std::jthread([&](std::stop_token stop_token) { | ||||
|             try { | ||||
|                 ThreadLoop(stop_token); | ||||
|             } catch (const std::exception& ex) { | ||||
|                 LOG_CRITICAL(Debug_GDBStub, "Stopping server: {}", ex.what()); | ||||
|             } | ||||
|  | ||||
|             client_socket.shutdown(client_socket.shutdown_both); | ||||
|             client_socket.close(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     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. | ||||
|         active_thread = ThreadList()[0]; | ||||
|         active_thread->Resume(Kernel::SuspendType::Debug); | ||||
|  | ||||
|         // Set up the frontend. | ||||
|         frontend->Connected(); | ||||
|  | ||||
|         // Main event loop. | ||||
|         while (!stop_token.stop_requested() && io_context.run()) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void PipeData(std::span<const u8> data) { | ||||
|         AllCoreStop(); | ||||
|         active_thread->Resume(Kernel::SuspendType::Debug); | ||||
|         frontend->Stopped(active_thread); | ||||
|     } | ||||
|  | ||||
|     void ClientData(std::span<const u8> 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(); | ||||
|                 active_thread = ThreadList()[0]; | ||||
|                 active_thread->Resume(Kernel::SuspendType::Debug); | ||||
|                 frontend->Stopped(active_thread); | ||||
|                 break; | ||||
|             } | ||||
|             case DebuggerAction::Continue: | ||||
|                 stepping = false; | ||||
|                 ResumeInactiveThreads(); | ||||
|                 AllCoreResume(); | ||||
|                 break; | ||||
|             case DebuggerAction::StepThread: | ||||
|                 stepping = true; | ||||
|                 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); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const std::vector<Kernel::KThread*>& ThreadList() { | ||||
|         return system.GlobalSchedulerContext().GetThreadList(); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     System& system; | ||||
|     std::unique_ptr<DebuggerFrontend> 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<std::unique_lock<std::mutex>> suspend; | ||||
|  | ||||
|     Kernel::KThread* active_thread; | ||||
|     bool stopped; | ||||
|     bool stepping; | ||||
|  | ||||
|     std::array<u8, 4096> client_data; | ||||
| }; | ||||
|  | ||||
| Debugger::Debugger(Core::System& system, u16 port) { | ||||
|     try { | ||||
|         impl = std::make_unique<DebuggerImpl>(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); | ||||
| } | ||||
|  | ||||
| bool Debugger::IsStepping() const { | ||||
|     return impl && impl->IsStepping(); | ||||
| } | ||||
|  | ||||
| } // namespace Core | ||||
							
								
								
									
										46
									
								
								src/core/debugger/debugger.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/core/debugger/debugger.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <memory> | ||||
|  | ||||
| #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); | ||||
|  | ||||
|     /** | ||||
|      * Returns whether a step is in progress. | ||||
|      */ | ||||
|     bool IsStepping() const; | ||||
|  | ||||
| private: | ||||
|     std::unique_ptr<DebuggerImpl> impl; | ||||
| }; | ||||
| } // namespace Core | ||||
							
								
								
									
										74
									
								
								src/core/debugger/debugger_interface.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/core/debugger/debugger_interface.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <functional> | ||||
| #include <span> | ||||
| #include <vector> | ||||
|  | ||||
| #include "common/common_types.h" | ||||
|  | ||||
| namespace Kernel { | ||||
| class KThread; | ||||
| } | ||||
|  | ||||
| namespace Core { | ||||
|  | ||||
| enum class DebuggerAction { | ||||
|     Interrupt,         // Stop emulation as soon as possible. | ||||
|     Continue,          // Resume emulation. | ||||
|     StepThread,        // Step the currently-active thread. | ||||
|     ShutdownEmulation, // Shut down the emulator. | ||||
| }; | ||||
|  | ||||
| class DebuggerBackend { | ||||
| public: | ||||
|     /** | ||||
|      * 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<const u8> 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<const u8> 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_} {} | ||||
|  | ||||
|     /** | ||||
|      * 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<DebuggerAction> ClientData(std::span<const u8> data) = 0; | ||||
|  | ||||
| protected: | ||||
|     DebuggerBackend& backend; | ||||
| }; | ||||
|  | ||||
| } // namespace Core | ||||
							
								
								
									
										382
									
								
								src/core/debugger/gdbstub.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								src/core/debugger/gdbstub.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,382 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| #include <atomic> | ||||
| #include <numeric> | ||||
| #include <optional> | ||||
| #include <thread> | ||||
|  | ||||
| #include <boost/asio.hpp> | ||||
| #include <boost/process/async_pipe.hpp> | ||||
|  | ||||
| #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<GDBStubA64>(); | ||||
|     } else { | ||||
|         arch = std::make_unique<GDBStubA32>(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| GDBStub::~GDBStub() = default; | ||||
|  | ||||
| void GDBStub::Connected() {} | ||||
|  | ||||
| void GDBStub::Stopped(Kernel::KThread* thread) { | ||||
|     SendReply(arch->ThreadStatus(thread, GDB_STUB_SIGTRAP)); | ||||
| } | ||||
|  | ||||
| std::vector<DebuggerAction> GDBStub::ClientData(std::span<const u8> data) { | ||||
|     std::vector<DebuggerAction> 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<DebuggerAction>& 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<DebuggerAction>& actions) { | ||||
|     LOG_TRACE(Debug_GDBStub, "Executing command: {}", packet); | ||||
|  | ||||
|     if (packet.length() == 0) { | ||||
|         SendReply(GDB_STUB_REPLY_ERR); | ||||
|         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); | ||||
|         } | ||||
|  | ||||
|         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': | ||||
|         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<size_t>(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<size_t>(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<size_t>(strtoll(command.data(), nullptr, 16))}; | ||||
|         const size_t size{static_cast<size_t>(strtoll(command.data() + sep, nullptr, 16))}; | ||||
|  | ||||
|         if (system.Memory().IsValidVirtualAddressRange(addr, size)) { | ||||
|             std::vector<u8> 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<size_t>(strtoll(command.data(), nullptr, 16))}; | ||||
|         const size_t size{static_cast<size_t>(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::StepThread); | ||||
|         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<size_t>(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<size_t>(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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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+"); | ||||
|     } 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<u64>(strtoll(offset.data(), nullptr, 16))}; | ||||
|         const auto amount_val{static_cast<u64>(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<std::string> 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<?xml version="1.0"?>)"; | ||||
|         buffer += "<threads>"; | ||||
|  | ||||
|         const auto& threads = system.GlobalSchedulerContext().GetThreadList(); | ||||
|         for (const auto& thread : threads) { | ||||
|             buffer += | ||||
|                 fmt::format(R"(<thread id="{:x}" core="{:d}" name="Thread {:d}"/>)", | ||||
|                             thread->GetThreadID(), thread->GetActiveCore(), thread->GetThreadID()); | ||||
|         } | ||||
|  | ||||
|         buffer += "</threads>"; | ||||
|         SendReply(buffer); | ||||
|     } else { | ||||
|         SendReply(GDB_STUB_REPLY_EMPTY); | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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<char>::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<std::string> 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<u8>(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 static_cast<u8>( | ||||
|         std::accumulate(data.begin(), data.end(), u8{0}, [](u8 lhs, u8 rhs) { return 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<const u8*>(output.data()); | ||||
|     const u8* output_end = output_begin + output.size(); | ||||
|     backend.WriteToClient(std::span<const u8>(output_begin, output_end)); | ||||
| } | ||||
|  | ||||
| void GDBStub::SendStatus(char status) { | ||||
|     std::array<u8, 1> buf = {static_cast<u8>(status)}; | ||||
|     LOG_TRACE(Debug_GDBStub, "Writing status: {}", status); | ||||
|     backend.WriteToClient(buf); | ||||
| } | ||||
|  | ||||
| } // namespace Core | ||||
							
								
								
									
										47
									
								
								src/core/debugger/gdbstub.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/core/debugger/gdbstub.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <optional> | ||||
| #include <string_view> | ||||
| #include <vector> | ||||
|  | ||||
| #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(); | ||||
|  | ||||
|     void Connected() override; | ||||
|     void Stopped(Kernel::KThread* thread) override; | ||||
|     std::vector<DebuggerAction> ClientData(std::span<const u8> data) override; | ||||
|  | ||||
| private: | ||||
|     void ProcessData(std::vector<DebuggerAction>& actions); | ||||
|     void ExecuteCommand(std::string_view packet, std::vector<DebuggerAction>& actions); | ||||
|     void HandleQuery(std::string_view command); | ||||
|     std::vector<char>::const_iterator CommandEnd() const; | ||||
|     std::optional<std::string> 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<GDBStubArch> arch; | ||||
|     std::vector<char> current_command; | ||||
|     std::map<VAddr, u32> replaced_instructions; | ||||
| }; | ||||
|  | ||||
| } // namespace Core | ||||
							
								
								
									
										406
									
								
								src/core/debugger/gdbstub_arch.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										406
									
								
								src/core/debugger/gdbstub_arch.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <typename T> | ||||
| static T HexToValue(std::string_view hex) { | ||||
|     static_assert(std::is_trivially_copyable_v<T>); | ||||
|     T value{}; | ||||
|     const auto mem{Common::HexStringToVector(hex, false)}; | ||||
|     std::memcpy(&value, mem.data(), std::min(mem.size(), sizeof(T))); | ||||
|     return value; | ||||
| } | ||||
|  | ||||
| template <typename T> | ||||
| static std::string ValueToHex(const T value) { | ||||
|     static_assert(std::is_trivially_copyable_v<T>); | ||||
|     std::array<u8, sizeof(T)> mem{}; | ||||
|     std::memcpy(mem.data(), &value, sizeof(T)); | ||||
|     return Common::HexToString(mem); | ||||
| } | ||||
|  | ||||
| template <typename T> | ||||
| static T GetSIMDRegister(const std::array<u32, 64>& simd_regs, size_t offset) { | ||||
|     static_assert(std::is_trivially_copyable_v<T>); | ||||
|     T value{}; | ||||
|     std::memcpy(&value, reinterpret_cast<const u8*>(simd_regs.data()) + sizeof(T) * offset, | ||||
|                 sizeof(T)); | ||||
|     return value; | ||||
| } | ||||
|  | ||||
| template <typename T> | ||||
| static void PutSIMDRegister(std::array<u32, 64>& simd_regs, size_t offset, const T value) { | ||||
|     static_assert(std::is_trivially_copyable_v<T>); | ||||
|     std::memcpy(reinterpret_cast<u8*>(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"(<?xml version="1.0"?> | ||||
| <!DOCTYPE target SYSTEM "gdb-target.dtd"> | ||||
| <target version="1.0"> | ||||
|   <feature name="org.gnu.gdb.aarch64.core"> | ||||
|     <reg name="x0" bitsize="64"/> | ||||
|     <reg name="x1" bitsize="64"/> | ||||
|     <reg name="x2" bitsize="64"/> | ||||
|     <reg name="x3" bitsize="64"/> | ||||
|     <reg name="x4" bitsize="64"/> | ||||
|     <reg name="x5" bitsize="64"/> | ||||
|     <reg name="x6" bitsize="64"/> | ||||
|     <reg name="x7" bitsize="64"/> | ||||
|     <reg name="x8" bitsize="64"/> | ||||
|     <reg name="x9" bitsize="64"/> | ||||
|     <reg name="x10" bitsize="64"/> | ||||
|     <reg name="x11" bitsize="64"/> | ||||
|     <reg name="x12" bitsize="64"/> | ||||
|     <reg name="x13" bitsize="64"/> | ||||
|     <reg name="x14" bitsize="64"/> | ||||
|     <reg name="x15" bitsize="64"/> | ||||
|     <reg name="x16" bitsize="64"/> | ||||
|     <reg name="x17" bitsize="64"/> | ||||
|     <reg name="x18" bitsize="64"/> | ||||
|     <reg name="x19" bitsize="64"/> | ||||
|     <reg name="x20" bitsize="64"/> | ||||
|     <reg name="x21" bitsize="64"/> | ||||
|     <reg name="x22" bitsize="64"/> | ||||
|     <reg name="x23" bitsize="64"/> | ||||
|     <reg name="x24" bitsize="64"/> | ||||
|     <reg name="x25" bitsize="64"/> | ||||
|     <reg name="x26" bitsize="64"/> | ||||
|     <reg name="x27" bitsize="64"/> | ||||
|     <reg name="x28" bitsize="64"/> | ||||
|     <reg name="x29" bitsize="64"/> | ||||
|     <reg name="x30" bitsize="64"/> | ||||
|     <reg name="sp" bitsize="64" type="data_ptr"/> | ||||
|     <reg name="pc" bitsize="64" type="code_ptr"/> | ||||
|     <flags id="pstate_flags" size="4"> | ||||
|       <field name="SP" start="0" end="0"/> | ||||
|       <field name="" start="1" end="1"/> | ||||
|       <field name="EL" start="2" end="3"/> | ||||
|       <field name="nRW" start="4" end="4"/> | ||||
|       <field name="" start="5" end="5"/> | ||||
|       <field name="F" start="6" end="6"/> | ||||
|       <field name="I" start="7" end="7"/> | ||||
|       <field name="A" start="8" end="8"/> | ||||
|       <field name="D" start="9" end="9"/> | ||||
|       <field name="IL" start="20" end="20"/> | ||||
|       <field name="SS" start="21" end="21"/> | ||||
|       <field name="V" start="28" end="28"/> | ||||
|       <field name="C" start="29" end="29"/> | ||||
|       <field name="Z" start="30" end="30"/> | ||||
|       <field name="N" start="31" end="31"/> | ||||
|     </flags> | ||||
|     <reg name="pstate" bitsize="32" type="pstate_flags"/> | ||||
|   </feature> | ||||
|   <feature name="org.gnu.gdb.aarch64.fpu"> | ||||
|   </feature> | ||||
| </target>)"; | ||||
|  | ||||
|     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<u64>(value); | ||||
|     } else if (id == PC_REGISTER) { | ||||
|         context.pc = HexToValue<u64>(value); | ||||
|     } else if (id == PSTATE_REGISTER) { | ||||
|         context.pstate = HexToValue<u32>(value); | ||||
|     } else if (id >= Q0_REGISTER && id < FPCR_REGISTER) { | ||||
|         context.vector_registers[id - Q0_REGISTER] = HexToValue<u128>(value); | ||||
|     } else if (id == FPCR_REGISTER) { | ||||
|         context.fpcr = HexToValue<u32>(value); | ||||
|     } else if (id == FPSR_REGISTER) { | ||||
|         context.fpsr = HexToValue<u32>(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"(<?xml version="1.0"?> | ||||
| <!DOCTYPE target SYSTEM "gdb-target.dtd"> | ||||
| <target version="1.0"> | ||||
|   <feature name="org.gnu.gdb.arm.core"> | ||||
|     <reg name="r0" bitsize="32" type="uint32"/> | ||||
|     <reg name="r1" bitsize="32" type="uint32"/> | ||||
|     <reg name="r2" bitsize="32" type="uint32"/> | ||||
|     <reg name="r3" bitsize="32" type="uint32"/> | ||||
|     <reg name="r4" bitsize="32" type="uint32"/> | ||||
|     <reg name="r5" bitsize="32" type="uint32"/> | ||||
|     <reg name="r6" bitsize="32" type="uint32"/> | ||||
|     <reg name="r7" bitsize="32" type="uint32"/> | ||||
|     <reg name="r8" bitsize="32" type="uint32"/> | ||||
|     <reg name="r9" bitsize="32" type="uint32"/> | ||||
|     <reg name="r10" bitsize="32" type="uint32"/> | ||||
|     <reg name="r11" bitsize="32" type="uint32"/> | ||||
|     <reg name="r12" bitsize="32" type="uint32"/> | ||||
|     <reg name="sp" bitsize="32" type="data_ptr"/> | ||||
|     <reg name="lr" bitsize="32" type="code_ptr"/> | ||||
|     <reg name="pc" bitsize="32" type="code_ptr"/> | ||||
|     <!-- The CPSR is register 25, rather than register 16, because | ||||
|          the FPA registers historically were placed between the PC | ||||
|          and the CPSR in the "g" packet.  --> | ||||
|     <reg name="cpsr" bitsize="32" regnum="25"/> | ||||
|   </feature> | ||||
|   <feature name="org.gnu.gdb.arm.vfp"> | ||||
|     <vector id="neon_uint8x8" type="uint8" count="8"/> | ||||
|     <vector id="neon_uint16x4" type="uint16" count="4"/> | ||||
|     <vector id="neon_uint32x2" type="uint32" count="2"/> | ||||
|     <vector id="neon_float32x2" type="ieee_single" count="2"/> | ||||
|     <union id="neon_d"> | ||||
|       <field name="u8" type="neon_uint8x8"/> | ||||
|       <field name="u16" type="neon_uint16x4"/> | ||||
|       <field name="u32" type="neon_uint32x2"/> | ||||
|       <field name="u64" type="uint64"/> | ||||
|       <field name="f32" type="neon_float32x2"/> | ||||
|       <field name="f64" type="ieee_double"/> | ||||
|     </union> | ||||
|     <vector id="neon_uint8x16" type="uint8" count="16"/> | ||||
|     <vector id="neon_uint16x8" type="uint16" count="8"/> | ||||
|     <vector id="neon_uint32x4" type="uint32" count="4"/> | ||||
|     <vector id="neon_uint64x2" type="uint64" count="2"/> | ||||
|     <vector id="neon_float32x4" type="ieee_single" count="4"/> | ||||
|     <vector id="neon_float64x2" type="ieee_double" count="2"/> | ||||
|     <union id="neon_q"> | ||||
|       <field name="u8" type="neon_uint8x16"/> | ||||
|       <field name="u16" type="neon_uint16x8"/> | ||||
|       <field name="u32" type="neon_uint32x4"/> | ||||
|       <field name="u64" type="neon_uint64x2"/> | ||||
|       <field name="f32" type="neon_float32x4"/> | ||||
|       <field name="f64" type="neon_float64x2"/> | ||||
|     </union> | ||||
|     <reg name="d0" bitsize="64" type="neon_d" regnum="32"/> | ||||
|     <reg name="d1" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d2" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d3" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d4" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d5" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d6" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d7" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d8" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d9" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d10" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d11" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d12" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d13" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d14" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d15" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d16" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d17" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d18" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d19" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d20" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d21" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d22" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d23" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d24" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d25" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d26" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d27" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d28" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d29" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d30" bitsize="64" type="neon_d"/> | ||||
|     <reg name="d31" bitsize="64" type="neon_d"/> | ||||
|  | ||||
|     <reg name="q0" bitsize="128" type="neon_q" regnum="64"/> | ||||
|     <reg name="q1" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q2" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q3" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q4" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q5" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q6" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q7" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q8" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q9" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q10" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q10" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q12" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q13" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q14" bitsize="128" type="neon_q"/> | ||||
|     <reg name="q15" bitsize="128" type="neon_q"/> | ||||
|  | ||||
|     <reg name="fpscr" bitsize="32" type="int" group="float" regnum="80"/> | ||||
|   </feature> | ||||
| </target>)"; | ||||
|  | ||||
|     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<u64>(fprs, id - D0_REGISTER)}; | ||||
|         return ValueToHex(dN); | ||||
|     } else if (id >= Q0_REGISTER && id < FPSCR_REGISTER) { | ||||
|         const u128 qN{GetSIMDRegister<u128>(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<u32>(value); | ||||
|     } else if (id == CPSR_REGISTER) { | ||||
|         context.cpsr = HexToValue<u32>(value); | ||||
|     } else if (id >= D0_REGISTER && id < Q0_REGISTER) { | ||||
|         PutSIMDRegister(fprs, id - D0_REGISTER, HexToValue<u64>(value)); | ||||
|     } else if (id >= Q0_REGISTER && id < FPSCR_REGISTER) { | ||||
|         PutSIMDRegister(fprs, id - Q0_REGISTER, HexToValue<u128>(value)); | ||||
|     } else if (id == FPSCR_REGISTER) { | ||||
|         context.fpscr = HexToValue<u32>(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 | ||||
							
								
								
									
										67
									
								
								src/core/debugger/gdbstub_arch.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/core/debugger/gdbstub_arch.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <string> | ||||
|  | ||||
| #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 | ||||
		Reference in New Issue
	
	Block a user