Browse Source
Merge pull request #8394 from liamwhite/debugger
Merge pull request #8394 from liamwhite/debugger
core/debugger: Implement new GDB stub debuggernce_cpp
committed by
GitHub
27 changed files with 1500 additions and 42 deletions
-
1src/common/settings.cpp
-
2src/common/settings.h
-
7src/core/CMakeLists.txt
-
5src/core/arm/arm_interface.cpp
-
5src/core/arm/arm_interface.h
-
35src/core/arm/dynarmic/arm_dynarmic_32.cpp
-
4src/core/arm/dynarmic/arm_dynarmic_32.h
-
32src/core/arm/dynarmic/arm_dynarmic_64.cpp
-
4src/core/arm/dynarmic/arm_dynarmic_64.h
-
29src/core/core.cpp
-
18src/core/core.h
-
259src/core/debugger/debugger.cpp
-
46src/core/debugger/debugger.h
-
74src/core/debugger/debugger_interface.h
-
382src/core/debugger/gdbstub.cpp
-
47src/core/debugger/gdbstub.h
-
406src/core/debugger/gdbstub_arch.cpp
-
67src/core/debugger/gdbstub_arch.h
-
4src/core/hle/kernel/k_process.cpp
-
13src/core/memory.cpp
-
11src/core/memory.h
-
11src/yuzu/bootmanager.cpp
-
10src/yuzu/bootmanager.h
-
5src/yuzu/configuration/config.cpp
-
9src/yuzu/configuration/configure_debug.cpp
-
54src/yuzu/configuration/configure_debug.ui
-
2src/yuzu/main.cpp
@ -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
|
||||
@ -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 |
||||
@ -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 |
||||
@ -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
|
||||
@ -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 |
||||
@ -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
|
||||
@ -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 |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue