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