gdb: Revamp the entire GDB implementation and add debug features

This commit is contained in:
PabloMK7 2026-05-03 00:11:03 +02:00
parent 9ce755cd07
commit 19fbe53856
29 changed files with 1039 additions and 256 deletions

View file

@ -116,6 +116,7 @@ foreach(KEY IN ITEMS
"log_filter"
"log_regex_filter"
"toggle_unique_data_console_type"
"break_on_unmapped_memory_access"
"use_integer_scaling"
"layouts_to_cycle"
"camera_inner_flip"

View file

@ -3880,6 +3880,18 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
.c_str());
error_severity_icon = QMessageBox::Icon::Critical;
can_continue = false;
} else if (result == Core::System::ResultStatus::ErrorCoreExceptionRaised) {
title = tr("An exception occurred");
message = tr("An exception occurred while executing the emulated application.\n\n");
message += QString::fromStdString(details);
error_severity_icon = QMessageBox::Icon::Critical;
can_continue = false;
} else if (result == Core::System::ResultStatus::ErrorMemoryExceptionRaised) {
title = tr("An invalid memory access occurred");
message =
tr("An invalid memory access occurred while executing the emulated application.\n\n");
message += QString::fromStdString(details);
error_severity_icon = QMessageBox::Icon::Critical;
} else {
title = tr("Fatal Error");
message = tr("A fatal error occurred. "

View file

@ -510,6 +510,7 @@ void QtConfig::ReadDebuggingValues() {
ReadBasicSetting(Settings::values.instant_debug_log);
ReadBasicSetting(Settings::values.enable_rpc_server);
ReadBasicSetting(Settings::values.toggle_unique_data_console_type);
ReadBasicSetting(Settings::values.break_on_unmapped_memory_access);
qt_config->beginGroup(QStringLiteral("LLE"));
for (const auto& service_module : Service::service_module_map) {
@ -1096,6 +1097,7 @@ void QtConfig::SaveDebuggingValues() {
WriteBasicSetting(Settings::values.instant_debug_log);
WriteBasicSetting(Settings::values.enable_rpc_server);
WriteBasicSetting(Settings::values.toggle_unique_data_console_type);
WriteBasicSetting(Settings::values.break_on_unmapped_memory_access);
qt_config->beginGroup(QStringLiteral("LLE"));
for (const auto& service_module : Settings::values.lle_modules) {

View file

@ -112,6 +112,8 @@ void ConfigureDebug::SetConfiguration() {
#endif // !ENABLE_SCRIPTING
ui->toggle_unique_data_console_type->setChecked(
Settings::values.toggle_unique_data_console_type.GetValue());
ui->break_on_unmapped_memory_access->setChecked(
Settings::values.break_on_unmapped_memory_access.GetValue());
ui->toggle_renderer_debug->setChecked(Settings::values.renderer_debug.GetValue());
ui->toggle_dump_command_buffers->setChecked(Settings::values.dump_command_buffers.GetValue());
@ -153,6 +155,8 @@ void ConfigureDebug::ApplyConfiguration() {
Settings::values.enable_rpc_server = ui->enable_rpc_server->isChecked();
Settings::values.toggle_unique_data_console_type =
ui->toggle_unique_data_console_type->isChecked();
Settings::values.break_on_unmapped_memory_access =
ui->break_on_unmapped_memory_access->isChecked();
Settings::values.renderer_debug = ui->toggle_renderer_debug->isChecked();
Settings::values.dump_command_buffers = ui->toggle_dump_command_buffers->isChecked();
Settings::values.instant_debug_log = ui->instant_debug_log->isChecked();
@ -178,6 +182,7 @@ void ConfigureDebug::SetupPerGameUI() {
ui->groupBox_2->setVisible(false);
ui->enable_rpc_server->setVisible(false);
ui->toggle_unique_data_console_type->setVisible(false);
ui->break_on_unmapped_memory_access->setVisible(false);
ui->toggle_cpu_jit->setVisible(false);
}

View file

@ -299,6 +299,16 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="break_on_unmapped_memory_access">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Pauses emulation and shows an error message if an unmapped memory access is detected.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Break on unmapped memory access</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -1,4 +1,4 @@
// Copyright 2020 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -153,7 +153,9 @@ private:
void serialize(Archive& ar, const unsigned int) {
ar & backing_mem;
ar & offset;
Init();
if (Archive::is_loading::value) {
Init();
}
}
friend class boost::serialization::access;
};

View file

@ -166,6 +166,8 @@ void LogSettings() {
log_setting("Debugging_InstantDebugLog", values.instant_debug_log.GetValue());
log_setting("Debugging_ToggleUniqueDataConsoleType",
values.toggle_unique_data_console_type.GetValue());
log_setting("Debugging_BreakOnUnmappedMemoryAccess",
values.break_on_unmapped_memory_access.GetValue());
}
bool IsConfiguringGlobal() {

View file

@ -643,6 +643,7 @@ struct Values {
Setting<bool> instant_debug_log{false, Keys::instant_debug_log};
Setting<bool> enable_rpc_server{false, Keys::enable_rpc_server};
Setting<bool> toggle_unique_data_console_type{false, Keys::toggle_unique_data_console_type};
Setting<bool> break_on_unmapped_memory_access{false, Keys::break_on_unmapped_memory_access};
// Miscellaneous
Setting<std::string> log_filter{"*:Info", Keys::log_filter};

View file

@ -122,8 +122,7 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P
_CompleteFilename += _Filename;
}
std::vector<std::string> SplitString(const std::string& str, const char delim) {
std::istringstream iss(str);
static std::vector<std::string> SplitString(std::istringstream& iss, const char delim) {
std::vector<std::string> output(1);
while (std::getline(iss, *output.rbegin(), delim)) {
@ -134,6 +133,16 @@ std::vector<std::string> SplitString(const std::string& str, const char delim) {
return output;
}
std::vector<std::string> SplitString(std::string_view str, const char delim) {
std::istringstream iss{std::string(str)};
return SplitString(iss, delim);
}
std::vector<std::string> SplitString(const std::string& str, const char delim) {
std::istringstream iss(str);
return SplitString(iss, delim);
}
std::string TabsToSpaces(int tab_size, std::string in) {
std::size_t i = 0;

View file

@ -36,6 +36,7 @@ namespace Common {
[[nodiscard]] bool EndsWith(const std::string& value, const std::string& ending);
[[nodiscard]] std::vector<std::string> SplitString(std::string_view str, const char delim);
[[nodiscard]] std::vector<std::string> SplitString(const std::string& str, const char delim);
// "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe"

View file

@ -1,4 +1,4 @@
// Copyright 2014 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -198,12 +198,28 @@ public:
return id;
}
/**
* Sets the core to not being runnable until its break condition is handled.
*/
void SetBreakFlag() {
break_flag = true;
}
/*
* Sets the core being runnable after its break condition was handled.
*/
void ClearBreakFlag() {
break_flag = false;
}
protected:
// This us used for serialization. Returning nullptr is valid if page tables are not used.
virtual std::shared_ptr<Memory::PageTable> GetPageTable() const = 0;
std::shared_ptr<Core::Timing::Timer> timer;
bool break_flag{};
private:
u32 id;
@ -213,6 +229,7 @@ private:
void save(Archive& ar, const unsigned int file_version) const {
ar << timer;
ar << id;
ar << break_flag;
const auto page_table = GetPageTable();
ar << page_table;
for (int i = 0; i < 15; i++) {
@ -258,6 +275,7 @@ private:
ClearInstructionCache();
ar >> timer;
ar >> id;
ar >> break_flag;
std::shared_ptr<Memory::PageTable> page_table{};
ar >> page_table;
SetPageTable(page_table);

View file

@ -1,7 +1,8 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <csignal>
#include <cstring>
#include <dynarmic/interface/A32/a32.h>
#include <dynarmic/interface/optimization_flags.h>
@ -17,6 +18,14 @@
#include "core/hle/kernel/svc.h"
#include "core/memory.h"
#ifndef SIGILL
constexpr u32 SIGILL = 4;
#endif
#ifndef SIGTRAP
constexpr u32 SIGTRAP = 5;
#endif
namespace Core {
class DynarmicUserCallbacks final : public Dynarmic::A32::UserCallbacks {
@ -25,6 +34,21 @@ public:
: parent(parent), svc_context(parent.system), memory(parent.memory) {}
~DynarmicUserCallbacks() = default;
std::optional<std::uint32_t> MemoryReadCode(VAddr vaddr) override {
constexpr VAddr low_page_limit = 0x1000;
// This check prevents a common cascading error that results from the
// memory system allowing unmapped memory accesses (in some situations,
// a vtable is read from an invalid address and execution jumps to the
// zero page). On real hw, it's not normally possible to map the zero page.
if (vaddr < low_page_limit) [[unlikely]] {
LOG_CRITICAL(Debug, "Tried to read code from low address 0x{:08x}", vaddr);
return {};
}
return MemoryRead32(vaddr);
}
std::uint8_t MemoryRead8(VAddr vaddr) override {
return memory.Read8(vaddr);
}
@ -83,9 +107,8 @@ public:
break;
case Dynarmic::A32::Exception::Breakpoint:
if (GDBStub::IsConnected()) {
parent.jit->HaltExecution();
parent.SetPC(pc);
parent.ServeBreak();
parent.ServeBreak(SIGTRAP);
return;
}
break;
@ -99,11 +122,19 @@ public:
case Dynarmic::A32::Exception::PreloadInstruction:
return;
}
for (int i = 0; i < 16; i++) {
LOG_CRITICAL(Debug, "r{:02d} = {:08X}", i, parent.GetReg(i));
parent.SetPC(pc);
if (GDBStub::IsConnected()) {
parent.ServeBreak(SIGILL);
} else {
std::string error;
for (int i = 0; i < 16; i++) {
error += fmt::format("r{:02d} = {:08X}\n", i, parent.GetReg(i));
}
error += fmt::format("ExceptionRaised(exception = {}, pc = {:08X})", exception, pc);
parent.system.SetStatus(Core::System::ResultStatus::ErrorCoreExceptionRaised,
error.c_str());
}
ASSERT_MSG(false, "ExceptionRaised(exception = {}, pc = {:08X}, code = {:08X})", exception,
pc, MemoryReadCode(pc).value());
}
void AddTicks(std::uint64_t ticks) override {
@ -138,16 +169,19 @@ MICROPROFILE_DEFINE(ARM_Jit, "ARM JIT", "ARM JIT", MP_RGB(255, 64, 64));
void ARM_Dynarmic::Run() {
ASSERT(memory.GetCurrentPageTable() == current_page_table);
MICROPROFILE_SCOPE(ARM_Jit);
if (break_flag) [[unlikely]] {
return;
}
jit->Run();
}
void ARM_Dynarmic::Step() {
jit->Step();
if (GDBStub::IsConnected()) {
ServeBreak();
if (break_flag) [[unlikely]] {
return;
}
jit->Step();
}
void ARM_Dynarmic::SetPC(u32 pc) {
@ -294,11 +328,8 @@ void ARM_Dynarmic::SetPageTable(const std::shared_ptr<Memory::PageTable>& page_t
jits.emplace(current_page_table, std::move(new_jit));
}
void ARM_Dynarmic::ServeBreak() {
Kernel::Thread* thread = system.Kernel().GetCurrentThreadManager().GetCurrentThread();
SaveContext(thread->context);
GDBStub::Break();
GDBStub::SendTrap(thread, 5);
void ARM_Dynarmic::ServeBreak(int signal) {
GDBStub::Break(signal);
}
std::unique_ptr<Dynarmic::A32::Jit> ARM_Dynarmic::MakeJit() {

View file

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -60,7 +60,7 @@ protected:
std::shared_ptr<Memory::PageTable> GetPageTable() const override;
private:
void ServeBreak();
void ServeBreak(int signal);
friend class DynarmicUserCallbacks;
Core::System& system;

View file

@ -1,4 +1,4 @@
// Copyright 2014 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -24,10 +24,16 @@ ARM_DynCom::ARM_DynCom(Core::System& system_, Memory::MemorySystem& memory,
ARM_DynCom::~ARM_DynCom() {}
void ARM_DynCom::Run() {
if (break_flag) [[unlikely]] {
return;
}
ExecuteInstructions(std::max<s64>(timer->GetDowncount(), 0));
}
void ARM_DynCom::Step() {
if (break_flag) [[unlikely]] {
return;
}
ExecuteInstructions(1);
}

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2012 Michael Kang, 2014 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -954,11 +958,9 @@ unsigned InterpreterMainLoop(ARMul_State* cpu) {
#define GDB_BP_CHECK \
cpu->Cpsr &= ~(1 << 5); \
cpu->Cpsr |= cpu->TFlag << 5; \
if (GDBStub::IsServerEnabled()) { \
if (GDBStub::IsMemoryBreak()) { \
goto END; \
} else if (breakpoint_data.type != GDBStub::BreakpointType::None && \
PC == breakpoint_data.address) { \
if (GDBStub::IsServerEnabled()) [[unlikely]] { \
if (breakpoint_data.type != GDBStub::BreakpointType::None && \
PC == breakpoint_data.address) { \
cpu->RecordBreak(breakpoint_data); \
goto END; \
} \

View file

@ -1,8 +1,9 @@
// Copyright 2015 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <csignal>
#include "common/logging/log.h"
#include "common/swap.h"
#include "core/arm/skyeye_common/armstate.h"
@ -10,6 +11,10 @@
#include "core/core.h"
#include "core/memory.h"
#ifndef SIGTRAP
constexpr u32 SIGTRAP = 5;
#endif
ARMul_State::ARMul_State(Core::System& system_, Memory::MemorySystem& memory_,
PrivilegeMode initial_mode)
: system{system_}, memory{memory_} {
@ -182,26 +187,12 @@ void ARMul_State::ResetMPCoreCP15Registers() {
CP15[CP15_MAIN_TLB_LOCKDOWN_ATTRIBUTE] = 0x00000000;
CP15[CP15_TLB_DEBUG_CONTROL] = 0x00000000;
}
#ifdef ANDROID
static void CheckMemoryBreakpoint(u32 address, GDBStub::BreakpointType type) {}
#else
static void CheckMemoryBreakpoint(u32 address, GDBStub::BreakpointType type) {
if (GDBStub::IsServerEnabled() && GDBStub::CheckBreakpoint(address, type)) {
LOG_DEBUG(Debug, "Found memory breakpoint @ {:08x}", address);
GDBStub::Break(true);
}
}
#endif
u8 ARMul_State::ReadMemory8(u32 address) const {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read);
return memory.Read8(address);
}
u16 ARMul_State::ReadMemory16(u32 address) const {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read);
u16 data = memory.Read16(address);
if (InBigEndianMode())
@ -211,8 +202,6 @@ u16 ARMul_State::ReadMemory16(u32 address) const {
}
u32 ARMul_State::ReadMemory32(u32 address) const {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read);
u32 data = memory.Read32(address);
if (InBigEndianMode())
@ -222,8 +211,6 @@ u32 ARMul_State::ReadMemory32(u32 address) const {
}
u64 ARMul_State::ReadMemory64(u32 address) const {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read);
u64 data = memory.Read64(address);
if (InBigEndianMode())
@ -233,14 +220,10 @@ u64 ARMul_State::ReadMemory64(u32 address) const {
}
void ARMul_State::WriteMemory8(u32 address, u8 data) {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write);
memory.Write8(address, data);
}
void ARMul_State::WriteMemory16(u32 address, u16 data) {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write);
if (InBigEndianMode())
data = Common::swap16(data);
@ -248,8 +231,6 @@ void ARMul_State::WriteMemory16(u32 address, u16 data) {
}
void ARMul_State::WriteMemory32(u32 address, u32 data) {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write);
if (InBigEndianMode())
data = Common::swap32(data);
@ -257,8 +238,6 @@ void ARMul_State::WriteMemory32(u32 address, u32 data) {
}
void ARMul_State::WriteMemory64(u32 address, u64 data) {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write);
if (InBigEndianMode())
data = Common::swap64(data);
@ -609,12 +588,8 @@ void ARMul_State::ServeBreak() {
DEBUG_ASSERT(Reg[15] == last_bkpt.address);
}
Kernel::Thread* thread = system.Kernel().GetCurrentThreadManager().GetCurrentThread();
system.GetRunningCore().SaveContext(thread->context);
if (last_bkpt_hit || GDBStub::IsMemoryBreak()) {
if (last_bkpt_hit) {
last_bkpt_hit = false;
GDBStub::Break();
GDBStub::SendTrap(thread, 5);
GDBStub::Break(SIGTRAP);
}
}

View file

@ -88,6 +88,12 @@ System::ResultStatus System::RunLoop(bool tight_loop) {
if (thread && running_core) {
running_core->SaveContext(thread->context);
}
// The break flag is only set if GDB is connected,
// we can do clearing here safely. If it is ever
// used outside, move the clearing outside the if.
for (auto& cpu_core : cpu_cores) {
cpu_core->ClearBreakFlag();
}
GDBStub::HandlePacket(*this);
}
@ -249,6 +255,8 @@ System::ResultStatus System::RunLoop(bool tight_loop) {
cpu_core->GetTimer().Idle();
PrepareReschedule();
} else {
// In the rare case the break flag is set (due to exception thrown)
// there is probably no need to adjust the timer accordingly.
if (tight_loop) {
cpu_core->Run();
} else {

View file

@ -103,8 +103,10 @@ public:
ErrorSavestate, ///< Error saving or loading
ErrorArticDisconnected, ///< Error when artic base disconnects
ErrorN3DSApplication, ///< Error launching New 3DS application in Old 3DS mode
ShutdownRequested, ///< Emulated program requested a system shutdown
ErrorUnknown ///< Any other error
ErrorCoreExceptionRaised, ///< The CPU emulation raised an exception
ErrorMemoryExceptionRaised, ///< Unmmaped memory was accessed
ShutdownRequested, ///< Emulated program requested a system shutdown
ErrorUnknown ///< Any other error
};
explicit System();

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2013 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
@ -36,10 +40,13 @@
#include "core/gdbstub/gdbstub.h"
#include "core/gdbstub/hio.h"
#include "core/hle/kernel/process.h"
#include "core/loader/loader.h"
#include "core/memory.h"
#pragma optimize("", off)
// Uncomment to log all GDB traffic
// #define PRINT_GDB_TRAFFIC
#define PRINT_GDB_TRAFFIC
namespace GDBStub {
namespace {
@ -62,6 +69,10 @@ constexpr u32 SIGTERM = 15;
constexpr u32 MSG_WAITALL = 8;
#endif
#ifndef SIGSEGV
constexpr u32 SIGSEGV = 11;
#endif
constexpr u32 SP_REGISTER = 13;
constexpr u32 LR_REGISTER = 14;
constexpr u32 PC_REGISTER = 15;
@ -76,6 +87,8 @@ constexpr char target_xml[] =
R"(l<?xml version="1.0"?>
<!DOCTYPE target SYSTEM "gdb-target.dtd">
<target version="1.0">
<architecture>arm</architecture>
<osabi>3DS</osabi>
<feature name="org.gnu.gdb.arm.core">
<reg name="r0" bitsize="32"/>
<reg name="r1" bitsize="32"/>
@ -129,19 +142,25 @@ u8 command_buffer[GDB_BUFFER_SIZE];
u32 command_length;
u32 latest_signal = 0;
bool memory_break = false;
static Kernel::Thread* current_thread = nullptr;
static Kernel::Process* current_process = nullptr;
// Binding to a port within the reserved ports range (0-1023) requires root permissions,
// so default to a port outside of that range.
u16 gdbstub_port = 24689;
bool send_trap = false;
constexpr bool supports_extended_mode = true;
bool is_extended_mode = false;
// If set to false, the server will never be started and no
// gdbstub-related functions will be executed.
std::atomic<bool> server_enabled(false);
int accept_socket = -1;
int continue_thread = -1;
static Kernel::Thread* break_thread = nullptr;
static int break_signal = 0;
#ifdef _WIN32
WSADATA InitData;
@ -161,16 +180,17 @@ BreakpointMap breakpoints_write;
} // Anonymous namespace
static Kernel::Thread* FindThreadById(int id) {
u32 num_cores = Core::GetNumCores();
for (u32 i = 0; i < num_cores; ++i) {
const auto& threads =
Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList();
for (auto& thread : threads) {
if (thread->GetThreadId() == static_cast<u32>(id)) {
return thread.get();
}
if (!current_process) {
return nullptr;
}
auto thread_list = current_process->GetThreadList();
for (auto& thread : thread_list) {
if (thread->GetThreadId() == static_cast<u32>(id)) {
return thread.get();
}
}
return nullptr;
}
@ -364,12 +384,40 @@ static u64 GdbHexToLong(const u8* src) {
return output;
}
static int GetErrno() {
#ifdef _WIN32
return WSAGetLastError();
#else
return errno;
#endif
}
static bool SetNonBlock(int socket, bool nonblock) {
#ifdef _WIN32
unsigned long nonblocking = nonblock ? 1 : 0;
int ret = ioctlsocket(socket, FIONBIO, &nonblocking);
if (ret < 0) {
LOG_ERROR(Debug_GDBStub, "Failed to set non-blocking gdb socket");
return false;
}
#else
int flags = nonblock ? O_NONBLOCK : 0;
const int ret = ::fcntl(socket, F_SETFL, flags);
if (ret < 0) {
LOG_ERROR(Debug_GDBStub, "Failed to set non-blocking gdb socket");
return false;
}
#endif
return true;
}
/// Read a byte from the gdb client.
static u8 ReadByte() {
u8 c;
std::size_t received_size = recv(gdbserver_socket, reinterpret_cast<char*>(&c), 1, MSG_WAITALL);
if (received_size != 1) {
LOG_ERROR(Debug_GDBStub, "recv failed : {}", received_size);
LOG_ERROR(Debug_GDBStub, "recv failed : {}", GetErrno());
Shutdown();
}
@ -417,13 +465,15 @@ static void RemoveBreakpoint(BreakpointType type, VAddr addr) {
bp->second.len, bp->second.addr, type);
if (type == BreakpointType::Execute) {
Core::System::GetInstance().Memory().WriteBlock(
*Core::System::GetInstance().Kernel().GetCurrentProcess(), bp->second.addr,
bp->second.inst.data(), bp->second.inst.size());
Core::System::GetInstance().Memory().WriteBlock(*current_process, bp->second.addr,
bp->second.inst.data(), bp->second.len);
u32 num_cores = Core::GetNumCores();
for (u32 i = 0; i < num_cores; ++i) {
Core::GetCore(i).ClearInstructionCache();
}
} else {
Core::System::GetInstance().Memory().UnregisterWatchpoint(*current_process, bp->second.addr,
bp->second.len);
}
p.erase(addr);
}
@ -444,38 +494,58 @@ BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, BreakpointType type)
return breakpoint;
}
bool CheckBreakpoint(VAddr addr, BreakpointType type) {
bool CheckBreakpoint(VAddr addr, u32 access_len, BreakpointType type) {
if (!IsConnected()) {
return false;
}
const BreakpointMap& p = GetBreakpointMap(type);
const auto bp = p.find(addr);
if (bp == p.end()) {
return false;
}
// Access range: [addr, access_end)
const VAddr access_end = addr + access_len;
u32 len = bp->second.len;
for (const auto& [base_addr, bp] : p) {
if (!bp.active) {
continue;
}
// IDA Pro defaults to 4-byte breakpoints for all non-hardware breakpoints
// no matter if it's a 4-byte or 2-byte instruction. When you execute a
// Thumb instruction with a 4-byte breakpoint set, it will set a breakpoint on
// two instructions instead of the single instruction you placed the breakpoint
// on. So, as a way to make sure that execution breakpoints are only breaking
// on the instruction that was specified, set the length of an execution
// breakpoint to 1. This should be fine since the CPU should never begin executing
// an instruction anywhere except the beginning of the instruction.
if (type == BreakpointType::Execute) {
len = 1;
}
u32 bp_len = bp.len;
if (bp->second.active && (addr >= bp->second.addr && addr < bp->second.addr + len)) {
LOG_DEBUG(Debug_GDBStub,
"Found breakpoint type {} @ {:08x}, range: {:08x}"
" - {:08x} ({:x} bytes)",
type, addr, bp->second.addr, bp->second.addr + len, len);
return true;
// IDA Pro defaults to 4-byte breakpoints for all non-hardware breakpoints
// no matter if it's a 4-byte or 2-byte instruction. When you execute a
// Thumb instruction with a 4-byte breakpoint set, it will set a breakpoint on
// two instructions instead of the single instruction you placed the breakpoint
// on. So, as a way to make sure that execution breakpoints are only breaking
// on the instruction that was specified, set the length of an execution
// breakpoint to 1. This should be fine since the CPU should never begin executing
// an instruction anywhere except the beginning of the instruction.
if (type == BreakpointType::Execute) {
bp_len = 1;
}
// Breakpoint/watchpoint range: [bp.addr, bp_end)
const VAddr bp_end = bp.addr + bp_len;
bool hit = false;
if (type == BreakpointType::Execute) {
// Execute breakpoints should only trigger on exact PC match.
hit = (addr == bp.addr);
} else {
// Range overlap test:
// [addr, access_end) overlaps [bp.addr, bp_end)
hit = (addr < bp_end) && (bp.addr < access_end);
}
if (hit) {
LOG_DEBUG(Debug_GDBStub,
"Found breakpoint type {}, "
"access range: {:08x} - {:08x}, "
"breakpoint range: {:08x} - {:08x}",
type, addr, addr, access_end, bp.addr, bp_end);
return true;
}
}
return false;
@ -503,11 +573,7 @@ void SendReply(const char* reply) {
command_length = static_cast<u32>(strlen(reply));
#ifdef PRINT_GDB_TRAFFIC
if (command_length > 0) {
LOG_INFO(Debug_GDBStub, "Req: {} {}", *reply, reply + 1);
} else {
LOG_INFO(Debug_GDBStub, "Req:");
}
LOG_INFO(Debug_GDBStub, "Res: {}", reply);
#endif
if (command_length + 4 > sizeof(command_buffer)) {
@ -545,60 +611,103 @@ static void HandleQuery() {
if (strcmp(query, "TStatus") == 0) {
SendReply("T0");
} else if (strncmp(query, "Attached", strlen("Attached")) == 0) {
SendReply("1");
} else if (strncmp(query, "Supported", strlen("Supported")) == 0) {
// PacketSize needs to be large enough for target xml
SendReply("PacketSize=2000;qXfer:features:read+;qXfer:threads:read+");
SendReply("PacketSize=2000;qXfer:features:read+;qXfer:osdata:read+;qXfer:threads:read+;"
"vContSupported+");
} else if (strncmp(query, "Xfer:features:read:target.xml:",
strlen("Xfer:features:read:target.xml:")) == 0) {
SendReply(target_xml);
} else if (strncmp(query, "fThreadInfo", strlen("fThreadInfo")) == 0) {
std::string val = "m";
u32 num_cores = Core::GetNumCores();
for (u32 i = 0; i < num_cores; ++i) {
const auto& threads =
Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList();
for (const auto& thread : threads) {
val += fmt::format("{:x},", thread->GetThreadId());
}
if (!current_process) {
SendReply("E01");
return;
}
auto thread_list = current_process->GetThreadList();
std::string val = "m";
for (const auto& thread : thread_list) {
val += fmt::format("{:x},", thread->GetThreadId());
}
val.pop_back();
SendReply(val.c_str());
} else if (strncmp(query, "sThreadInfo", strlen("sThreadInfo")) == 0) {
SendReply("l");
} else if (strncmp(query, "Xfer:osdata:read:processes", strlen("Xfer:osdata:read:processes")) ==
0) {
std::string buffer;
buffer += "l<osdata type=\"processes\">";
auto process_list = Core::System::GetInstance().Kernel().GetProcessList();
for (const auto& p : process_list) {
buffer += fmt::format("<item>"
"<column name=\"pid\">{}</column>"
"<column name=\"command\">{}</column>"
"</item>",
p->process_id, p->codeset->name);
}
buffer += "</osdata>";
SendReply(buffer.c_str());
} else if (strncmp(query, "Xfer:threads:read", strlen("Xfer:threads:read")) == 0) {
if (!current_process) {
SendReply("E01");
return;
}
std::string buffer;
buffer += "l<?xml version=\"1.0\"?>";
buffer += "<threads>";
u32 num_cores = Core::GetNumCores();
for (u32 i = 0; i < num_cores; ++i) {
const auto& threads =
Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList();
for (const auto& thread : threads) {
buffer += fmt::format(R"*(<thread id="{:x}" name="Thread {:x}"></thread>)*",
thread->GetThreadId(), thread->GetThreadId());
}
auto thread_list = current_process->GetThreadList();
for (const auto& thread : thread_list) {
buffer += fmt::format(R"*(<thread id="{:x}" name="Thread {:x}"></thread>)*",
thread->GetThreadId(), thread->GetThreadId());
}
buffer += "</threads>";
SendReply(buffer.c_str());
} else {
SendReply("");
}
}
static bool SetThread(int thread_id) {
if (!current_process) {
// The process has not been selected yet
return false;
}
if (thread_id >= 1) {
current_thread = FindThreadById(thread_id);
}
if (!current_thread) {
auto thread_list = current_process->GetThreadList();
if (thread_list.size() > 0) {
// Select the lowest thread ID, which is the main thread
std::sort(thread_list.begin(), thread_list.end(),
[](const std::shared_ptr<Kernel::Thread>& a,
const std::shared_ptr<Kernel::Thread>& b) {
return a->thread_id < b->thread_id;
});
current_thread = thread_list[0].get();
}
}
return current_thread != nullptr;
}
/// Handle set thread command from gdb client.
static void HandleSetThread() {
int thread_id = -1;
if (command_buffer[2] != '-') {
thread_id = static_cast<int>(HexToInt(command_buffer + 2, command_length - 2));
}
if (thread_id >= 1) {
current_thread = FindThreadById(thread_id);
if (command_buffer[1] == 'c') {
continue_thread = thread_id;
SendReply("OK");
return;
}
if (!current_thread) {
thread_id = 1;
current_thread = FindThreadById(thread_id);
}
if (current_thread) {
if (SetThread(thread_id)) {
SendReply("OK");
return;
}
@ -607,6 +716,11 @@ static void HandleSetThread() {
/// Handle thread alive command from gdb client.
static void HandleThreadAlive() {
if (!current_process) {
SendReply("E01");
return;
}
int thread_id = static_cast<int>(HexToInt(command_buffer + 1, command_length - 1));
if (thread_id == 0) {
thread_id = 1;
@ -618,6 +732,15 @@ static void HandleThreadAlive() {
SendReply("E01");
}
static void HandleExtendedMode() {
if (supports_extended_mode) {
is_extended_mode = true;
SendReply("OK");
} else {
SendReply("");
}
}
/**
* Send signal packet to client.
*
@ -636,11 +759,16 @@ static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) {
std::string buffer;
if (full) {
Core::ARM_Interface::ThreadContext ctx{};
if (thread) {
ctx = thread->context;
} else {
Core::GetRunningCore().SaveContext(ctx);
}
buffer = fmt::format("T{:02x}{:02x}:{:08x};{:02x}:{:08x};{:02x}:{:08x}", latest_signal,
PC_REGISTER, htonl(Core::GetRunningCore().GetPC()), SP_REGISTER,
htonl(Core::GetRunningCore().GetReg(SP_REGISTER)), LR_REGISTER,
htonl(Core::GetRunningCore().GetReg(LR_REGISTER)));
PC_REGISTER, htonl(ctx.cpu_registers[PC_REGISTER]), SP_REGISTER,
htonl(ctx.cpu_registers[SP_REGISTER]), LR_REGISTER,
htonl(ctx.cpu_registers[LR_REGISTER]));
} else {
buffer = fmt::format("T{:02x}", latest_signal);
}
@ -653,6 +781,53 @@ static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) {
SendReply(buffer.c_str());
}
static void HandleGetStopReason() {
if (is_extended_mode) {
// In extended mode, tell the debugger that there is no selected process yet.
// That way the debugger can ask for the process list and attack to the right PID.
if (!current_process) {
// The process has not been selected yet.
SendReply("W00");
} else {
SendSignal(current_thread, latest_signal);
}
} else {
// In non extended mode, select the process corresponding to the "main"
// launched application.
u64 program_id = 0;
Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id);
auto process_list = Core::System::GetInstance().Kernel().GetProcessList();
for (const auto& process : process_list) {
if (process->codeset->program_id == program_id) {
current_process = process.get();
current_process->SetDebugBreak(true);
if (SetThread(0)) {
SendSignal(current_thread, 0);
} else {
// Should never happen
SendReply("W00");
}
return;
}
}
// No process, should never happen
SendReply("W00");
}
}
static void BreakImpl(int signal) {
if (signal == SIGSEGV) {
LOG_WARNING(Debug_GDBStub, "Segmentation fault signals may not be fully accurate");
}
current_process->SetDebugBreak(true);
latest_signal = signal;
SendSignal(current_thread, signal);
}
/// Read command from gdb client.
static void ReadCommand() {
command_length = 0;
@ -664,7 +839,7 @@ static void ReadCommand() {
return;
} else if (c == 0x03) {
LOG_INFO(Debug_GDBStub, "gdb: found break command\n");
SendSignal(current_thread, SIGTRAP);
BreakImpl(SIGTRAP);
return;
} else if (c != GDB_STUB_START) {
LOG_DEBUG(Debug_GDBStub, "gdb: read invalid byte {:02x}\n", c);
@ -726,6 +901,11 @@ static bool IsDataAvailable() {
/// Send requested register to gdb client.
static void ReadRegister() {
if (!current_process || !current_thread) {
SendReply("E01");
return;
}
static u8 reply[64];
std::memset(reply, 0, sizeof(reply));
@ -752,6 +932,11 @@ static void ReadRegister() {
/// Send all registers to the gdb client.
static void ReadRegisters() {
if (!current_process || !current_thread) {
SendReply("E01");
return;
}
static u8 buffer[GDB_BUFFER_SIZE - 4];
std::memset(buffer, 0, sizeof(buffer));
@ -791,6 +976,11 @@ static void UpdateCPUThreadContext() {
/// Modify data of register specified by gdb client.
static void WriteRegister() {
if (!current_process || !current_thread) {
SendReply("E01");
return;
}
const u8* buffer_ptr = command_buffer + 3;
u32 id = HexCharToValue(command_buffer[1]);
@ -819,6 +1009,11 @@ static void WriteRegister() {
/// Modify all registers with data received from the client.
static void WriteRegisters() {
if (!current_process || !current_thread) {
SendReply("E01");
return;
}
const u8* buffer_ptr = command_buffer + 1;
if (command_buffer[0] != 'G')
@ -849,6 +1044,11 @@ static void WriteRegisters() {
/// Read location in memory specified by gdb client.
static void ReadMemory() {
if (!current_process) {
SendReply("E01");
return;
}
static u8 reply[GDB_BUFFER_SIZE - 4];
auto start_offset = command_buffer + 1;
@ -866,13 +1066,12 @@ static void ReadMemory() {
}
auto& memory = Core::System::GetInstance().Memory();
if (!memory.IsValidVirtualAddress(*Core::System::GetInstance().Kernel().GetCurrentProcess(),
addr)) {
return SendReply("E00");
if (!memory.IsValidVirtualAddress(*current_process, addr)) {
return SendReply("E14");
}
std::vector<u8> data(len);
memory.ReadBlock(addr, data.data(), len);
memory.ReadBlock(*current_process, addr, data.data(), len);
MemToGdbHex(reply, data.data(), len);
reply[len * 2] = '\0';
@ -885,6 +1084,11 @@ static void ReadMemory() {
/// Modify location in memory with data received from the gdb client.
static void WriteMemory() {
if (!current_process) {
SendReply("E01");
return;
}
auto start_offset = command_buffer + 1;
auto addr_pos = std::find(start_offset, command_buffer + command_length, ',');
VAddr addr = HexToInt(start_offset, static_cast<u32>(addr_pos - start_offset));
@ -894,36 +1098,67 @@ static void WriteMemory() {
u32 len = HexToInt(start_offset, static_cast<u32>(len_pos - start_offset));
auto& memory = Core::System::GetInstance().Memory();
if (!memory.IsValidVirtualAddress(*Core::System::GetInstance().Kernel().GetCurrentProcess(),
addr)) {
if (!memory.IsValidVirtualAddress(*current_process, addr)) {
return SendReply("E00");
}
std::vector<u8> data(len);
GdbHexToMem(data.data(), len_pos + 1, len);
memory.WriteBlock(addr, data.data(), len);
memory.WriteBlock(*current_process, addr, data.data(), len);
ClearAllInstructionCache();
SendReply("OK");
}
void Break(bool is_memory_break) {
send_trap = true;
memory_break = is_memory_break;
}
bool IsMemoryBreak() {
if (!IsConnected()) {
return false;
void Break(int signal) {
if (!IsConnected() || !current_process ||
Core::System::GetInstance().Kernel().GetCurrentProcess().get() != current_process) {
LOG_ERROR(Debug_GDBStub, "Got signal for un-attached process, ignoring...");
return;
}
return memory_break;
if (break_thread) {
LOG_ERROR(Debug_GDBStub,
"Got multiple break signals in quick succession, latest may be lost");
return;
}
break_thread =
Core::System::GetInstance().Kernel().GetCurrentThreadManager().GetCurrentThread();
if (break_thread) {
break_signal = signal;
}
// Try to break CPU asap
Core::GetRunningCore().SetBreakFlag();
Core::GetRunningCore().ClearInstructionCache();
Core::GetRunningCore().PrepareReschedule();
}
/// Tell the CPU to continue executing.
static void Continue() {
memory_break = false;
if (!current_process) {
return;
}
u32 thread_id = -1;
if (continue_thread == 0) {
thread_id = current_process->GetThreadList()[0]->thread_id;
} else {
thread_id = continue_thread;
}
// There is no documentation anywhere if continue should
// reset the continue thread value. Luma3DS implementation
// does this, and looks like IDA Pro expects it that way too.
continue_thread = -1;
std::vector<u32> continue_list{};
if (thread_id != -1) {
continue_list.push_back(thread_id);
}
current_process->SetDebugBreak(false, continue_list);
ClearAllInstructionCache();
}
@ -937,20 +1172,26 @@ static void Continue() {
static bool CommitBreakpoint(BreakpointType type, VAddr addr, u32 len) {
BreakpointMap& p = GetBreakpointMap(type);
if (type == BreakpointType::Execute && len != 2 && len != 4) {
return false;
}
Breakpoint breakpoint;
breakpoint.active = true;
breakpoint.addr = addr;
breakpoint.len = len;
Core::System::GetInstance().Memory().ReadBlock(
*Core::System::GetInstance().Kernel().GetCurrentProcess(), addr, breakpoint.inst.data(),
breakpoint.inst.size());
Core::System::GetInstance().Memory().ReadBlock(*current_process, addr, breakpoint.inst.data(),
len);
static constexpr std::array<u8, 4> btrap{0x70, 0x00, 0x20, 0xe1};
static constexpr std::array<u8, 2> btrap_thumb{0x00, 0xBE};
if (type == BreakpointType::Execute) {
Core::System::GetInstance().Memory().WriteBlock(
*Core::System::GetInstance().Kernel().GetCurrentProcess(), addr, btrap.data(),
btrap.size());
*current_process, addr, (len == 2) ? btrap_thumb.data() : btrap.data(), len);
ClearAllInstructionCache();
} else {
Core::System::GetInstance().Memory().RegisterWatchpoint(*current_process, addr, len);
}
p.insert({addr, breakpoint});
@ -962,6 +1203,11 @@ static bool CommitBreakpoint(BreakpointType type, VAddr addr, u32 len) {
/// Handle add breakpoint command from gdb client.
static void AddBreakpoint() {
if (!current_process) {
SendReply("E01");
return;
}
BreakpointType type;
u8 type_id = HexCharToValue(command_buffer[1]);
@ -1011,6 +1257,11 @@ static void AddBreakpoint() {
/// Handle remove breakpoint command from gdb client.
static void RemoveBreakpoint() {
if (!current_process) {
SendReply("E01");
return;
}
BreakpointType type;
u8 type_id = HexCharToValue(command_buffer[1]);
@ -1048,11 +1299,122 @@ static void RemoveBreakpoint() {
SendReply("OK");
}
void HandleVCommand() {
std::string_view cmd_view((const char*)command_buffer, command_length);
if (cmd_view.size() <= 1) {
SendReply("E01");
return;
}
size_t delimiter_pos = cmd_view.find(';');
std::string_view command =
cmd_view.substr(1, delimiter_pos == cmd_view.npos ? cmd_view.npos : delimiter_pos);
if (command == "Attach;") {
if (!is_extended_mode) {
SendReply("E01");
return;
}
std::string_view arg = cmd_view.substr(delimiter_pos + 1);
u32 pid = HexToInt(reinterpret_cast<const u8*>(arg.data()), arg.size());
auto process = Core::System::GetInstance().Kernel().GetProcessById(pid);
if (!process) {
SendReply("E02");
} else {
current_process = process.get();
current_process->SetDebugBreak(true);
if (SetThread(0)) {
SendSignal(current_thread, 0);
} else {
// Should never happen
SendReply("W00");
}
}
return;
} else if (command == "Cont?") {
SendReply("vCont;c;C");
} else if (command == "Cont;") {
if (!current_process) {
SendReply("E01");
return;
}
std::string_view arg = cmd_view.substr(delimiter_pos + 1);
auto actions = Common::SplitString(arg, ';');
if (actions.empty()) {
SendReply("E01");
return;
}
for (auto& action : actions) {
auto threads = Common::SplitString(action, ':');
if (threads.empty()) {
SendReply("E01");
return;
}
char action_type = threads[0][0];
if (action_type != 'c') {
SendReply("E01");
return;
}
std::vector<u32> thread_ids;
for (size_t i = 1; i < threads.size(); i++) {
thread_ids.push_back(
HexToInt(reinterpret_cast<const u8*>(threads[i].c_str()), threads[i].size()));
}
current_process->SetDebugBreak(false, thread_ids);
}
} else {
SendReply("");
}
}
void HandlePacket(Core::System& system) {
if (!IsConnected()) {
if (defer_start) {
ToggleServer(true);
defer_start = false;
}
// Handle accept new GDB connection
if (accept_socket != -1) {
sockaddr_in saddr_client;
sockaddr* client_addr = reinterpret_cast<sockaddr*>(&saddr_client);
socklen_t client_addrlen = sizeof(saddr_client);
gdbserver_socket =
static_cast<int>(accept(accept_socket, client_addr, &client_addrlen));
if (gdbserver_socket < 0) {
#ifdef _WIN32
if (GetErrno() == WSAEWOULDBLOCK) {
// Nothing connected yet
return;
}
#else
if (GetErrno() == EAGAIN || GetErrno() == EWOULDBLOCK) {
// Nothing connected yet
return;
}
#endif
LOG_ERROR(Debug_GDBStub, "Failed to accept gdb client");
} else {
LOG_INFO(Debug_GDBStub, "Client connected.\n");
SetNonBlock(gdbserver_socket, false);
}
shutdown(accept_socket, SHUT_RDWR);
accept_socket = -1;
}
return;
}
if (break_thread) {
current_thread = break_thread;
break_thread = nullptr;
int signal = break_signal;
break_signal = 0;
BreakImpl(signal);
return;
}
@ -1072,7 +1434,7 @@ void HandlePacket(Core::System& system) {
#ifdef PRINT_GDB_TRAFFIC
std::string cmd_str(command_buffer + 1, command_buffer + command_length);
LOG_INFO(Debug_GDBStub, "Res: {:c} {}", command_buffer[0], cmd_str);
LOG_INFO(Debug_GDBStub, "Req: {:c} {}", command_buffer[0], cmd_str);
#endif
LOG_DEBUG(Debug_GDBStub, "Packet: {0:d} ('{0:c}')", command_buffer[0]);
@ -1085,16 +1447,28 @@ void HandlePacket(Core::System& system) {
HandleSetThread();
break;
case '?':
SendSignal(current_thread, latest_signal);
HandleGetStopReason();
break;
case '!':
HandleExtendedMode();
break;
case 'D':
SendReply("OK");
ToggleServer(false);
// Continue execution
continue_thread = -1;
Continue();
break;
case 'k':
LOG_INFO(Debug_GDBStub, "killed by gdb");
ToggleServer(false);
// Continue execution so we don't hang forever after shutting down the server
// Continue execution and stop emulation
continue_thread = -1;
Continue();
system.RequestShutdown();
return;
case 'F':
HandleHioReply(system, command_buffer, command_length);
HandleHioReply(system, current_process, command_buffer, command_length);
break;
case 'g':
ReadRegisters();
@ -1115,7 +1489,7 @@ void HandlePacket(Core::System& system) {
WriteMemory();
break;
case 's':
// Single step, return ENOTSUP
// Single step not supported, return ENOTSUP
SendReply("E5F");
return;
case 'C':
@ -1131,6 +1505,9 @@ void HandlePacket(Core::System& system) {
case 'T':
HandleThreadAlive();
break;
case 'v':
HandleVCommand();
break;
default:
SendReply("");
break;
@ -1146,12 +1523,12 @@ void ToggleServer(bool status) {
server_enabled = status;
// Start server
if (!IsConnected() && Core::System::GetInstance().IsPoweredOn()) {
if (!IsInitialized() && Core::System::GetInstance().IsPoweredOn()) {
Init();
}
} else {
// Stop server
if (IsConnected()) {
if (IsInitialized()) {
Shutdown();
}
@ -1184,47 +1561,32 @@ static void Init(u16 port) {
WSAStartup(MAKEWORD(2, 2), &InitData);
#endif
int tmpsock = static_cast<int>(socket(PF_INET, SOCK_STREAM, 0));
if (tmpsock == -1) {
accept_socket = static_cast<int>(socket(PF_INET, SOCK_STREAM, 0));
if (accept_socket == -1) {
LOG_ERROR(Debug_GDBStub, "Failed to create gdb socket");
}
// Set socket to SO_REUSEADDR so it can always bind on the same port
int reuse_enabled = 1;
if (setsockopt(tmpsock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse_enabled,
if (setsockopt(accept_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse_enabled,
sizeof(reuse_enabled)) < 0) {
LOG_ERROR(Debug_GDBStub, "Failed to set gdb socket option");
}
const sockaddr* server_addr = reinterpret_cast<const sockaddr*>(&saddr_server);
socklen_t server_addrlen = sizeof(saddr_server);
if (bind(tmpsock, server_addr, server_addrlen) < 0) {
if (bind(accept_socket, server_addr, server_addrlen) < 0) {
LOG_ERROR(Debug_GDBStub, "Failed to bind gdb socket");
}
if (listen(tmpsock, 1) < 0) {
if (listen(accept_socket, 1) < 0) {
LOG_ERROR(Debug_GDBStub, "Failed to listen to gdb socket");
}
SetNonBlock(accept_socket, true);
// Wait for gdb to connect
LOG_INFO(Debug_GDBStub, "Waiting for gdb to connect...\n");
sockaddr_in saddr_client;
sockaddr* client_addr = reinterpret_cast<sockaddr*>(&saddr_client);
socklen_t client_addrlen = sizeof(saddr_client);
gdbserver_socket = static_cast<int>(accept(tmpsock, client_addr, &client_addrlen));
if (gdbserver_socket < 0) {
// In the case that we couldn't start the server for whatever reason, just start CPU
// execution like normal.
LOG_ERROR(Debug_GDBStub, "Failed to accept gdb client");
} else {
LOG_INFO(Debug_GDBStub, "Client connected.\n");
saddr_client.sin_addr.s_addr = ntohl(saddr_client.sin_addr.s_addr);
}
// Clean up temporary socket if it's still alive at this point.
if (tmpsock != -1) {
shutdown(tmpsock, SHUT_RDWR);
}
}
void Init() {
@ -1236,6 +1598,7 @@ void Shutdown() {
return;
}
defer_start = false;
is_extended_mode = false;
LOG_INFO(Debug_GDBStub, "Stopping GDB ...");
if (gdbserver_socket != -1) {
@ -1243,6 +1606,11 @@ void Shutdown() {
gdbserver_socket = -1;
}
if (accept_socket != -1) {
shutdown(accept_socket, SHUT_RDWR);
accept_socket = -1;
}
#ifdef _WIN32
WSACleanup();
#endif
@ -1254,19 +1622,11 @@ bool IsServerEnabled() {
return server_enabled;
}
bool IsInitialized() {
return IsServerEnabled() && (accept_socket != -1 || gdbserver_socket != -1);
}
bool IsConnected() {
return IsServerEnabled() && gdbserver_socket != -1;
}
void SendTrap(Kernel::Thread* thread, int trap) {
if (!send_trap) {
return;
}
current_thread = thread;
SendSignal(thread, trap);
send_trap = false;
}
}; // namespace GDBStub

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2013 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
@ -60,18 +64,18 @@ void Shutdown();
/// Checks if the gdbstub server is enabled.
bool IsServerEnabled();
/// Returns true if the GDB server is initialized
bool IsInitialized();
/// Returns true if there is an active socket connection.
bool IsConnected();
/**
* Signal to the gdbstub server that it should halt CPU execution.
*
* @param is_memory_break If true, the break resulted from a memory breakpoint.
* @param signal Signal that produced the break (SIGTRAP by default)
*/
void Break(bool is_memory_break = false);
/// Determine if there was a memory breakpoint.
bool IsMemoryBreak();
void Break(int signal);
/// Read and handle packet from gdb client.
void HandlePacket(Core::System& system);
@ -88,17 +92,10 @@ BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, GDBStub::BreakpointTy
* Check if a breakpoint of the specified type exists at the given address.
*
* @param addr Address of breakpoint.
* @param access_len Access size in bytes.
* @param type Type of breakpoint.
*/
bool CheckBreakpoint(VAddr addr, GDBStub::BreakpointType type);
/**
* Send trap signal from thread back to the gdbstub server.
*
* @param thread Sending thread.
* @param trap Trap no.
*/
void SendTrap(Kernel::Thread* thread, int trap);
bool CheckBreakpoint(VAddr addr, u32 access_len, BreakpointType type);
/**
* Send reply to gdb client.

View file

@ -1,13 +1,18 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <csignal>
#include <fmt/ranges.h>
#include "common/string_util.h"
#include "core/core.h"
#include "core/gdbstub/gdbstub.h"
#include "core/gdbstub/hio.h"
#ifndef SIGTRAP
constexpr u32 SIGTRAP = 5;
#endif
namespace GDBStub {
namespace {
@ -51,7 +56,7 @@ static void SendErrorReply(int error_code, int retval = -1) {
SendReply(packet.data());
}
void SetHioRequest(Core::System& system, const VAddr addr) {
void SetHioRequest(Core::System& system, Kernel::Process* process, const VAddr addr) {
if (!IsServerEnabled()) {
LOG_WARNING(Debug_GDBStub, "HIO requested but GDB stub is not running");
return;
@ -67,14 +72,13 @@ void SetHioRequest(Core::System& system, const VAddr addr) {
}
auto& memory = system.Memory();
const auto process = system.Kernel().GetCurrentProcess();
if (!memory.IsValidVirtualAddress(*process, addr)) {
LOG_WARNING(Debug_GDBStub, "Invalid address for HIO request");
return;
}
memory.ReadBlock(addr, &current_hio_request, sizeof(PackedGdbHioRequest));
memory.ReadBlock(*process, addr, &current_hio_request, sizeof(PackedGdbHioRequest));
if (current_hio_request.magic != std::array{'G', 'D', 'B', '\0'}) {
std::string_view bad_magic{
@ -96,11 +100,11 @@ void SetHioRequest(Core::System& system, const VAddr addr) {
// Now halt, so that no further instructions are executed until the request
// is processed by the client. We will continue after the reply comes back
Break();
Break(SIGTRAP);
system.GetRunningCore().ClearInstructionCache();
}
void HandleHioReply(Core::System& system, const u8* const command_buffer,
void HandleHioReply(Core::System& system, Kernel::Process* process, const u8* const command_buffer,
const u32 command_length) {
if (!IsWaitingForHioReply()) {
LOG_WARNING(Debug_GDBStub, "Got HIO reply but never sent a request");
@ -169,7 +173,6 @@ void HandleHioReply(Core::System& system, const u8* const command_buffer,
current_hio_request.retval, current_hio_request.gdb_errno,
current_hio_request.ctrl_c);
const auto process = system.Kernel().GetCurrentProcess();
auto& memory = system.Memory();
// should have been checked when we first initialized the request,
@ -180,7 +183,8 @@ void HandleHioReply(Core::System& system, const u8* const command_buffer,
return;
}
memory.WriteBlock(current_hio_request_addr, &current_hio_request, sizeof(PackedGdbHioRequest));
memory.WriteBlock(*process, current_hio_request_addr, &current_hio_request,
sizeof(PackedGdbHioRequest));
current_hio_request = {};
current_hio_request_addr = 0;

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -10,6 +10,10 @@ namespace Core {
class System;
}
namespace Kernel {
class Process;
}
namespace GDBStub {
/**
@ -51,7 +55,7 @@ static_assert(sizeof(PackedGdbHioRequest) == 152,
*
* @param address The memory address of the \ref PackedGdbHioRequest.
*/
void SetHioRequest(Core::System& system, const VAddr address);
void SetHioRequest(Core::System& system, Kernel::Process* process, const VAddr address);
/**
* If there is a pending HIO request, send it to the client.
@ -63,6 +67,7 @@ bool HandlePendingHioRequestPacket();
/**
* Process an HIO reply from the client.
*/
void HandleHioReply(Core::System& system, const u8* const command_buffer, const u32 command_length);
void HandleHioReply(Core::System& system, Kernel::Process* process, const u8* const command_buffer,
const u32 command_length);
} // namespace GDBStub

View file

@ -592,6 +592,42 @@ Result Process::Unmap(VAddr target, VAddr source, u32 size, VMAPermission perms,
return ResultSuccess;
}
std::vector<std::shared_ptr<Kernel::Thread>> Kernel::Process::GetThreadList() {
std::vector<std::shared_ptr<Kernel::Thread>> ret;
for (u32 core = 0; core < Core::GetNumCores(); core++) {
auto thread_list = kernel.GetThreadManager(core).GetThreadList();
for (auto& thread : thread_list) {
if (thread->owner_process.lock().get() == this) {
ret.push_back(thread);
}
}
}
return ret;
}
void Kernel::Process::SetDebugBreak(bool debug_break, std::vector<u32> thread_ids) {
auto thread_list = GetThreadList();
bool needs_reschedule = false;
for (auto& t : thread_list) {
if (!thread_ids.empty()) {
u32 thread_id = t->thread_id;
if (std::find(thread_ids.begin(), thread_ids.end(), thread_id) == thread_ids.end()) {
continue;
}
}
needs_reschedule |= t->SetDebugBreak(debug_break);
}
if (needs_reschedule) {
for (u32 i = 0; i < Core::GetNumCores(); i++) {
Core::GetCore(i).PrepareReschedule();
kernel.GetThreadManager(i).Reschedule();
}
}
}
void Process::FreeAllMemory() {
if (memory_region == nullptr || resource_limit == nullptr) {
return;

View file

@ -1,4 +1,4 @@
// Copyright 2015 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -226,6 +226,10 @@ public:
Result Unmap(VAddr target, VAddr source, u32 size, VMAPermission perms,
bool privileged = false);
std::vector<std::shared_ptr<Kernel::Thread>> GetThreadList();
void SetDebugBreak(bool debug_break, std::vector<u32> thread_ids = {});
private:
void FreeAllMemory();

View file

@ -1171,7 +1171,7 @@ void SVC::OutputDebugString(VAddr address, s32 len) {
}
if (len == 0) {
GDBStub::SetHioRequest(system, address);
GDBStub::SetHioRequest(system, kernel.GetCurrentProcess().get(), address);
return;
}

View file

@ -77,6 +77,7 @@ void Thread::serialize(Archive& ar, const unsigned int file_version) {
}
}
ar & wakeup_callback;
ar & debug_break;
}
SERIALIZE_IMPL(Thread)
@ -191,7 +192,7 @@ Thread* ThreadManager::PopNextReadyThread() {
u32 next_priority{};
next = nullptr;
if (thread && thread->status == ThreadStatus::Running) {
if (thread && thread->status == ThreadStatus::Running && thread->CanSchedule()) {
do {
// We have to do better than the current thread.
// This call returns null when that's not possible.
@ -201,18 +202,18 @@ Thread* ThreadManager::PopNextReadyThread() {
// Otherwise just keep going with the current thread
next = thread;
break;
} else if (!next->can_schedule) {
} else if (!next->CanSchedule()) {
skipped.push_back({next_priority, next});
}
} while (!next->can_schedule);
} while (!next->CanSchedule());
} else {
do {
std::tie(next_priority, next) = ready_queue.pop_first();
if (next && !next->can_schedule) {
if (next && !next->CanSchedule()) {
skipped.push_back({next_priority, next});
}
} while (next && !next->can_schedule);
} while (next && !next->CanSchedule());
}
for (auto it = skipped.rbegin(); it != skipped.rend(); it++) {
@ -537,6 +538,14 @@ VAddr Thread::GetCommandBufferAddress() const {
return GetTLSAddress() + command_header_offset;
}
bool Thread::SetDebugBreak(bool _debug_break) {
if (debug_break == _debug_break) {
return false;
}
debug_break = _debug_break;
return true;
}
CpuLimiter::~CpuLimiter() {}
CpuLimiterMulti::CpuLimiterMulti(Kernel::KernelSystem& _kernel) : kernel(_kernel) {}

View file

@ -365,6 +365,15 @@ public:
return status == ThreadStatus::WaitSynchAll;
}
bool CanSchedule() {
// TODO(PabloMK7): This may not be the proper way
// threads are marked as non-schedulable when they
// are in debug break. Figure out and fix.
return can_schedule && !debug_break;
}
bool SetDebugBreak(bool debug_break);
Core::ARM_Interface::ThreadContext context{};
u32 thread_id;
@ -410,6 +419,7 @@ public:
private:
ThreadManager& thread_manager;
bool debug_break{};
friend class boost::serialization::access;
template <class Archive>

View file

@ -3,6 +3,7 @@
// Refer to the license.txt file included.
#include <array>
#include <csignal>
#include <cstring>
#include <boost/serialization/array.hpp>
#include <boost/serialization/binary_object.hpp>
@ -16,6 +17,7 @@
#include "common/swap.h"
#include "core/arm/arm_interface.h"
#include "core/core.h"
#include "core/gdbstub/gdbstub.h"
#include "core/global.h"
#include "core/hle/kernel/process.h"
#include "core/hle/service/plgldr/plgldr.h"
@ -28,6 +30,14 @@ SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl<Memory::Region::VRAM>
SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl<Memory::Region::DSP>)
SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl<Memory::Region::N3DS>)
#ifndef SIGTRAP
constexpr u32 SIGTRAP = 5;
#endif
#ifndef SIGSEGV
constexpr u32 SIGSEGV = 11;
#endif
namespace Memory {
void PageTable::Clear() {
@ -187,7 +197,17 @@ public:
std::memcpy(dest_buffer, src_ptr, copy_amount);
break;
}
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table.watchpoint_pages_map.find(page_index);
ASSERT_MSG(it != page_table.watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
const u8* src_ptr = it->second.memory.GetPtr() + page_offset;
std::memcpy(dest_buffer, src_ptr, copy_amount);
break;
}
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
if constexpr (!UNSAFE) {
RasterizerFlushVirtualRegion(current_vaddr, static_cast<u32>(copy_amount),
FlushMode::Flush);
@ -235,7 +255,17 @@ public:
std::memcpy(dest_ptr, src_buffer, copy_amount);
break;
}
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table.watchpoint_pages_map.find(page_index);
ASSERT_MSG(it != page_table.watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
u8* dest_ptr = it->second.memory.GetPtr() + page_offset;
std::memcpy(dest_ptr, src_buffer, copy_amount);
break;
}
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
if constexpr (!UNSAFE) {
RasterizerFlushVirtualRegion(current_vaddr, static_cast<u32>(copy_amount),
FlushMode::Invalidate);
@ -392,6 +422,90 @@ PAddr& Memory::MemorySystem::Plugin3GXFramebufferAddress() {
return impl->plugin_fb_address;
}
#pragma optimize("", off)
void MemorySystem::RegisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size) {
auto& page_table = *process.vm_manager.page_table;
VAddr current = addr;
VAddr end = addr + size;
while (current < end) {
const VAddr page_base = (current & ~CITRA_PAGE_MASK);
const VAddr page_index = page_base >> CITRA_PAGE_BITS;
auto it = page_table.watchpoint_pages_map.find(page_index);
if (it != page_table.watchpoint_pages_map.end()) {
// Nothing to do, only increment count.
it->second.watchpoint_count++;
} else {
MemoryRef mem;
PageType& type = page_table.attributes[page_index];
switch (type) {
case PageType::Memory:
mem = page_table.pointers.Ref(page_index);
type = PageType::MemoryWatchpoint;
page_table.pointers[page_index] = nullptr;
break;
case PageType::RasterizerCachedMemory:
mem = GetPointerForRasterizerCache(page_base);
type = PageType::RasterizerCachedMemoryWatchpoint;
break;
default:
LOG_ERROR(HW_Memory, "Cannot get pointer to register watchpoint for page 0x{:08X}",
page_base);
continue;
}
page_table.watchpoint_pages_map.insert(
{page_index,
PageTable::WatchpointPageInfo{.watchpoint_count = 1, .memory = std::move(mem)}});
}
current = page_base + CITRA_PAGE_SIZE;
}
}
void MemorySystem::UnregisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size) {
auto& page_table = *process.vm_manager.page_table;
VAddr current = addr;
VAddr end = addr + size;
while (current < end) {
const VAddr page_base = (current & ~CITRA_PAGE_MASK);
const VAddr page_index = page_base >> CITRA_PAGE_BITS;
auto it = page_table.watchpoint_pages_map.find(page_index);
if (it != page_table.watchpoint_pages_map.end()) {
if (--it->second.watchpoint_count == 0) {
PageType& type = page_table.attributes[page_index];
switch (type) {
case PageType::MemoryWatchpoint:
type = PageType::Memory;
page_table.pointers[page_index] = it->second.memory;
break;
case PageType::RasterizerCachedMemoryWatchpoint:
type = PageType::RasterizerCachedMemory;
break;
default:
LOG_ERROR(HW_Memory, "Invalid watchpoint page type for page 0x{:08X}: {}",
page_base, static_cast<u8>(type));
}
page_table.watchpoint_pages_map.erase(page_index);
}
} else {
LOG_ERROR(HW_Memory, "No watchpoint found on page 0x{:08X}", page_base);
}
current = page_base + CITRA_PAGE_SIZE;
}
}
void MemorySystem::MapPages(PageTable& page_table, u32 base, u32 size, MemoryRef memory,
PageType type) {
LOG_DEBUG(HW_Memory, "Mapping {} onto {:08X}-{:08X}", (void*)memory.GetPtr(),
@ -449,6 +563,23 @@ void MemorySystem::UnregisterPageTable(std::shared_ptr<PageTable> page_table) {
}
}
template <typename T>
T MemorySystem::UnmappedAccess(const VAddr vaddr, const T value, bool read) {
const std::string mode = (read ? "Read" : "Write");
const std::string value_str = read ? std::string("") : fmt::format(" 0x{:08X}", value);
const std::string message = fmt::format("unmapped {}{}{} @ 0x{:08X} at PC 0x{:08X}", mode,
sizeof(T) * 8, value_str, vaddr, impl->GetPC());
if (GDBStub::IsConnected()) {
GDBStub::Break(SIGSEGV);
} else if (Settings::values.break_on_unmapped_memory_access) {
impl->system.SetStatus(Core::System::ResultStatus::ErrorMemoryExceptionRaised,
message.c_str());
}
LOG_ERROR(HW_Memory, "{}", message);
return {};
}
template <typename T>
T MemorySystem::Read(const std::shared_ptr<PageTable>& page_table, const VAddr vaddr) {
const u8* page_pointer = page_table->pointers[vaddr >> CITRA_PAGE_BITS];
@ -476,20 +607,45 @@ T MemorySystem::Read(const std::shared_ptr<PageTable>& page_table, const VAddr v
PageType type = page_table->attributes[vaddr >> CITRA_PAGE_BITS];
switch (type) {
case PageType::Unmapped:
LOG_ERROR(HW_Memory, "unmapped Read{} @ 0x{:08X} at PC 0x{:08X}", sizeof(T) * 8, vaddr,
impl->GetPC());
return 0;
case PageType::Unmapped: {
return UnmappedAccess<T>(vaddr, 0, true);
}
case PageType::Memory:
ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr);
break;
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS);
ASSERT_MSG(it != page_table->watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
T value;
std::memcpy(&value, it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK), sizeof(T));
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Read)) {
GDBStub::Break(SIGTRAP);
}
return value;
}
[[likely]] case PageType::RasterizerCachedMemory: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Flush);
T value;
std::memcpy(&value, GetPointerForRasterizerCache(vaddr), sizeof(T));
return value;
}
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Flush);
T value;
std::memcpy(&value, GetPointerForRasterizerCache(vaddr), sizeof(T));
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Read)) {
GDBStub::Break(SIGTRAP);
}
return value;
}
default:
UNREACHABLE();
}
@ -527,17 +683,39 @@ void MemorySystem::Write(const std::shared_ptr<PageTable>& page_table, const VAd
PageType type = page_table->attributes[vaddr >> CITRA_PAGE_BITS];
switch (type) {
case PageType::Unmapped:
LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}",
sizeof(data) * 8, (u32)data, vaddr, impl->GetPC());
(void)UnmappedAccess<T>(vaddr, data, false);
return;
case PageType::Memory:
ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr);
break;
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS);
ASSERT_MSG(it != page_table->watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
std::memcpy(it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK), &data, sizeof(T));
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) {
GDBStub::Break(SIGTRAP);
}
break;
}
[[likely]] case PageType::RasterizerCachedMemory: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate);
std::memcpy(GetPointerForRasterizerCache(vaddr), &data, sizeof(T));
break;
}
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate);
std::memcpy(GetPointerForRasterizerCache(vaddr), &data, sizeof(T));
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) {
GDBStub::Break(SIGTRAP);
}
break;
}
default:
UNREACHABLE();
}
@ -556,18 +734,44 @@ bool MemorySystem::WriteExclusive(const VAddr vaddr, const T data, const T expec
PageType type = impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS];
switch (type) {
case PageType::Unmapped:
LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}",
sizeof(data) * 8, static_cast<u32>(data), vaddr, impl->GetPC());
(void)UnmappedAccess<T>(vaddr, data, false);
return true;
case PageType::Memory:
ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr);
return true;
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = impl->current_page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS);
ASSERT_MSG(it != impl->current_page_table->watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
const auto volatile_pointer =
reinterpret_cast<volatile T*>(it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK));
bool ret = Common::AtomicCompareAndSwap(volatile_pointer, data, expected);
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) {
GDBStub::Break(SIGTRAP);
}
return ret;
}
[[likely]] case PageType::RasterizerCachedMemory: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate);
const auto volatile_pointer =
reinterpret_cast<volatile T*>(GetPointerForRasterizerCache(vaddr).GetPtr());
return Common::AtomicCompareAndSwap(volatile_pointer, data, expected);
}
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate);
const auto volatile_pointer =
reinterpret_cast<volatile T*>(GetPointerForRasterizerCache(vaddr).GetPtr());
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) {
GDBStub::Break(SIGTRAP);
}
return Common::AtomicCompareAndSwap(volatile_pointer, data, expected);
}
default:
UNREACHABLE();
}
@ -582,7 +786,7 @@ bool MemorySystem::IsValidVirtualAddress(const Kernel::Process& process, const V
return true;
}
if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] == PageType::RasterizerCachedMemory) {
if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] != PageType::Unmapped) {
return true;
}
@ -600,7 +804,9 @@ u8* MemorySystem::GetPointer(const VAddr vaddr) {
}
if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] ==
PageType::RasterizerCachedMemory) {
PageType::RasterizerCachedMemory ||
impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] ==
PageType::RasterizerCachedMemoryWatchpoint) {
return GetPointerForRasterizerCache(vaddr);
}
@ -615,7 +821,9 @@ const u8* MemorySystem::GetPointer(const VAddr vaddr) const {
}
if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] ==
PageType::RasterizerCachedMemory) {
PageType::RasterizerCachedMemory ||
impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] ==
PageType::RasterizerCachedMemoryWatchpoint) {
return GetPointerForRasterizerCache(vaddr);
}
@ -755,7 +963,10 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached
// address space, for example, a system module need not have a VRAM mapping.
break;
case PageType::Memory:
page_type = PageType::RasterizerCachedMemory;
case PageType::MemoryWatchpoint:
page_type = (page_type == PageType::Memory)
? PageType::RasterizerCachedMemory
: PageType::RasterizerCachedMemoryWatchpoint;
page_table->pointers[vaddr >> CITRA_PAGE_BITS] = nullptr;
break;
default:
@ -768,10 +979,16 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached
// It is not necessary for a process to have this region mapped into its
// address space, for example, a system module need not have a VRAM mapping.
break;
case PageType::RasterizerCachedMemory: {
page_type = PageType::Memory;
page_table->pointers[vaddr >> CITRA_PAGE_BITS] =
GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK);
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
page_type = (page_type == PageType::RasterizerCachedMemory)
? PageType::Memory
: PageType::MemoryWatchpoint;
if (page_type == PageType::Memory) {
page_table->pointers[vaddr >> CITRA_PAGE_BITS] =
GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK);
}
break;
}
default:
@ -911,7 +1128,17 @@ void MemorySystem::ZeroBlock(const Kernel::Process& process, const VAddr dest_ad
std::memset(dest_ptr, 0, copy_amount);
break;
}
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table.watchpoint_pages_map.find(page_index);
ASSERT_MSG(it != page_table.watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
u8* dest_ptr = it->second.memory.GetPtr() + page_offset;
std::memset(dest_ptr, 0, copy_amount);
break;
}
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(current_vaddr, static_cast<u32>(copy_amount),
FlushMode::Invalidate);
std::memset(GetPointerForRasterizerCache(current_vaddr), 0, copy_amount);
@ -960,7 +1187,17 @@ void MemorySystem::CopyBlock(const Kernel::Process& dest_process,
WriteBlock(dest_process, dest_addr, src_ptr, copy_amount);
break;
}
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table.watchpoint_pages_map.find(page_index);
ASSERT_MSG(it != page_table.watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
const u8* src_ptr = it->second.memory.GetPtr() + page_offset;
WriteBlock(dest_process, dest_addr, src_ptr, copy_amount);
break;
}
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(current_vaddr, static_cast<u32>(copy_amount),
FlushMode::Flush);
WriteBlock(dest_process, dest_addr, GetPointerForRasterizerCache(current_vaddr),

View file

@ -34,7 +34,7 @@ constexpr u32 CITRA_PAGE_MASK = CITRA_PAGE_SIZE - 1;
constexpr int CITRA_PAGE_BITS = 12;
constexpr std::size_t PAGE_TABLE_NUM_ENTRIES = 1 << (32 - CITRA_PAGE_BITS);
enum class PageType {
enum class PageType : u8 {
/// Page is unmapped and should cause an access error.
Unmapped,
/// Page is mapped to regular memory. This is the only type you can get pointers to.
@ -42,6 +42,12 @@ enum class PageType {
/// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and
/// invalidation
RasterizerCachedMemory,
/// Page is mapped to regular memory. Furthermore a debug watchpoint is set to an address within
/// the page.
MemoryWatchpoint,
/// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and
/// invalidation. Furthermore a debug watchpoint is set to an address within the page.
RasterizerCachedMemoryWatchpoint,
};
/**
@ -82,6 +88,10 @@ struct PageTable {
return Entry(*this, static_cast<VAddr>(idx));
}
const MemoryRef& Ref(std::size_t idx) {
return refs[idx];
}
private:
std::array<u8*, PAGE_TABLE_NUM_ENTRIES> raw;
std::array<MemoryRef, PAGE_TABLE_NUM_ENTRIES> refs;
@ -100,6 +110,22 @@ struct PageTable {
return pointers.raw;
}
struct WatchpointPageInfo {
u32 watchpoint_count{};
MemoryRef memory;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar & watchpoint_count;
ar & memory;
}
};
// Map holding pages that are marked to contain watchpoints. We don't need
// any fancy performance tricks here, as watchpoints are only used rarely
// while debugging and performance is not a priority in such cases.
std::unordered_map<VAddr, WatchpointPageInfo> watchpoint_pages_map{};
void Clear();
private:
@ -107,6 +133,7 @@ private:
void serialize(Archive& ar, const unsigned int) {
ar & pointers.refs;
ar & attributes;
ar & watchpoint_pages_map;
for (std::size_t i = 0; i < PAGE_TABLE_NUM_ENTRIES; i++) {
pointers.raw[i] = pointers.refs[i].GetPtr();
}
@ -649,7 +676,14 @@ public:
/// Returns a reference to the framebuffer address of the currently loaded 3GX plugin.
PAddr& Plugin3GXFramebufferAddress();
void RegisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size);
void UnregisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size);
private:
template <typename T>
T UnmappedAccess(const VAddr vaddr, const T value, bool read);
template <typename T>
T Read(const std::shared_ptr<PageTable>& page_table, const VAddr vaddr);