From fdf8ff7d94c31b03be42cc1c92aaebc439ef0fe2 Mon Sep 17 00:00:00 2001
From: kotcrab <4594081+kotcrab@users.noreply.github.com>
Date: Mon, 11 Nov 2024 21:31:26 +0100
Subject: [PATCH 1/5] Add GhidraClient
---
CMakeLists.txt | 2 +
Common/Common.vcxproj | 2 +
Common/Common.vcxproj.filters | 2 +
Common/GhidraClient.cpp | 205 ++++++++++++++++++++++++
Common/GhidraClient.h | 108 +++++++++++++
UWP/CommonUWP/CommonUWP.vcxproj | 2 +
UWP/CommonUWP/CommonUWP.vcxproj.filters | 2 +
android/jni/Android.mk | 1 +
libretro/Makefile.common | 1 +
9 files changed, 325 insertions(+)
create mode 100644 Common/GhidraClient.cpp
create mode 100644 Common/GhidraClient.h
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c8a8b914f3..8c22ed4a08 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -884,6 +884,8 @@ add_library(Common STATIC
Common/FakeCPUDetect.cpp
Common/ExceptionHandlerSetup.cpp
Common/ExceptionHandlerSetup.h
+ Common/GhidraClient.h
+ Common/GhidraClient.cpp
Common/Log.h
Common/Log.cpp
Common/Log/ConsoleListener.cpp
diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj
index d3c3b77978..f0870fdba6 100644
--- a/Common/Common.vcxproj
+++ b/Common/Common.vcxproj
@@ -591,6 +591,7 @@
+
@@ -1021,6 +1022,7 @@
+
diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters
index db662233d3..055a82c5d7 100644
--- a/Common/Common.vcxproj.filters
+++ b/Common/Common.vcxproj.filters
@@ -6,6 +6,7 @@
+
@@ -705,6 +706,7 @@
Serialize
+
diff --git a/Common/GhidraClient.cpp b/Common/GhidraClient.cpp
new file mode 100644
index 0000000000..b26e5a3742
--- /dev/null
+++ b/Common/GhidraClient.cpp
@@ -0,0 +1,205 @@
+#include "Common/Data/Format/JSONReader.h"
+#include "Common/Net/HTTPClient.h"
+#include "Common/Thread/ThreadUtil.h"
+
+#include "Common/GhidraClient.h"
+
+using namespace json;
+
+static GhidraTypeKind ResolveTypeKind(const std::string& kind) {
+ if (kind == "ENUM") return ENUM;
+ if (kind == "TYPEDEF") return TYPEDEF;
+ if (kind == "POINTER") return POINTER;
+ if (kind == "ARRAY") return ARRAY;
+ if (kind == "STRUCTURE") return STRUCTURE;
+ if (kind == "UNION") return UNION;
+ if (kind == "FUNCTION_DEFINITION") return FUNCTION_DEFINITION;
+ if (kind == "BUILT_IN") return BUILT_IN;
+ return UNKNOWN;
+}
+
+GhidraClient::~GhidraClient() {
+ if (thread_.joinable()) {
+ thread_.join();
+ }
+}
+
+void GhidraClient::FetchAll(const std::string& host, const int port) {
+ std::lock_guard lock(mutex_);
+ if (status_ != Status::Idle) {
+ return;
+ }
+ status_ = Status::Pending;
+ thread_ = std::thread([this, host, port] {
+ SetCurrentThreadName("GhidraClient");
+ FetchAllDo(host, port);
+ });
+}
+
+bool GhidraClient::FetchAllDo(const std::string& host, const int port) {
+ std::lock_guard lock(mutex_);
+ host_ = host;
+ port_ = port;
+ const bool result = FetchTypes() && FetchSymbols();
+ status_ = Status::Ready;
+ return result;
+}
+
+void GhidraClient::UpdateResult() {
+ std::lock_guard lock(mutex_);
+ if (status_ != Status::Ready) {
+ return;
+ }
+ if (thread_.joinable()) {
+ thread_.join();
+ }
+ result = std::move(pendingResult_);
+ pendingResult_ = Result();
+ status_ = Status::Idle;
+}
+
+bool GhidraClient::FetchSymbols() {
+ std::string json;
+ if (!FetchResource("/v1/symbols", json)) {
+ return false;
+ }
+ JsonReader reader(json.c_str(), json.size());
+ if (!reader.ok()) {
+ pendingResult_.error = "symbols parsing error";
+ return false;
+ }
+ const JsonValue entries = reader.root().getArray("symbols")->value;
+ if (entries.getTag() != JSON_ARRAY) {
+ pendingResult_.error = "symbols is not an array";
+ return false;
+ }
+
+ for (const auto pEntry: entries) {
+ JsonGet entry = pEntry->value;
+
+ GhidraSymbol symbol;
+ symbol.address = entry.getInt("address", 0);
+ symbol.name = entry.getStringOr("name", "");
+ symbol.label = strcmp(entry.getStringOr("type", ""), "Label") == 0;
+ symbol.userDefined = strcmp(entry.getStringOr("source", ""), "USER_DEFINED") == 0;
+ symbol.dataTypePathName = entry.getStringOr("dataTypePathName", "");
+ pendingResult_.symbols.emplace_back(symbol);
+ }
+ return true;
+}
+
+bool GhidraClient::FetchTypes() {
+ std::string json;
+ if (!FetchResource("/v1/types", json)) {
+ return false;
+ }
+ JsonReader reader(json.c_str(), json.size());
+ if (!reader.ok()) {
+ pendingResult_.error = "types parsing error";
+ return false;
+ }
+ const JsonValue entries = reader.root().getArray("types")->value;
+ if (entries.getTag() != JSON_ARRAY) {
+ pendingResult_.error = "types is not an array";
+ return false;
+ }
+
+ for (const auto pEntry: entries) {
+ const JsonGet entry = pEntry->value;
+
+ GhidraType type;
+ type.name = entry.getStringOr("name", "");
+ type.displayName = entry.getStringOr("displayName", "");
+ type.pathName = entry.getStringOr("pathName", "");
+ type.length = entry.getInt("length", 0);
+ type.alignedLength = entry.getInt("alignedLength", 0);
+ type.zeroLength = entry.getBool("zeroLength", false);
+ type.description = entry.getStringOr("description", "");
+ type.kind = ResolveTypeKind(entry.getStringOr("kind", ""));
+
+ switch (type.kind) {
+ case ENUM: {
+ const JsonNode* enumEntries = entry.getArray("members");
+ if (!enumEntries) {
+ pendingResult_.error = "missing enum members";
+ return false;
+ }
+ for (const JsonNode* pEnumEntry: enumEntries->value) {
+ JsonGet enumEntry = pEnumEntry->value;
+ GhidraEnumMember member;
+ member.name = enumEntry.getStringOr("name", "");
+ member.value = enumEntry.getInt("value", 0);
+ member.comment = enumEntry.getStringOr("comment", "");
+ type.enumMembers.push_back(member);
+ }
+ break;
+ }
+ case TYPEDEF:
+ type.typedefTypePathName = entry.getStringOr("typePathName", "");
+ type.typedefBaseTypePathName = entry.getStringOr("baseTypePathName", "");
+ break;
+ case POINTER:
+ type.pointerTypePathName = entry.getStringOr("typePathName", "");
+ break;
+ case ARRAY:
+ type.arrayTypePathName = entry.getStringOr("typePathName", "");
+ type.arrayElementLength = entry.getInt("elementLength", 0);
+ type.arrayElementCount = entry.getInt("elementCount", 0);
+ break;
+ case STRUCTURE:
+ case UNION: {
+ const JsonNode* compositeEntries = entry.getArray("members");
+ if (!compositeEntries) {
+ pendingResult_.error = "missing composite members";
+ return false;
+ }
+ for (const JsonNode* pCompositeEntry: compositeEntries->value) {
+ JsonGet compositeEntry = pCompositeEntry->value;
+ GhidraCompositeMember member;
+ member.fieldName = compositeEntry.getStringOr("fieldName", "");
+ member.ordinal = compositeEntry.getInt("ordinal", 0);
+ member.offset = compositeEntry.getInt("offset", 0);
+ member.length = compositeEntry.getInt("length", 0);
+ member.typePathName = compositeEntry.getStringOr("typePathName", "");
+ member.comment = compositeEntry.getStringOr("comment", "");
+ type.compositeMembers.push_back(member);
+ }
+ break;
+ }
+ case FUNCTION_DEFINITION:
+ type.functionPrototypeString = entry.getStringOr("prototypeString", "");
+ break;
+ case BUILT_IN:
+ type.builtInGroup = entry.getStringOr("group", "");
+ break;
+ default:
+ continue;
+ }
+
+ pendingResult_.types.emplace(type.pathName, type);
+ }
+ return true;
+}
+
+bool GhidraClient::FetchResource(const std::string& path, std::string& outResult) {
+ http::Client http;
+ if (!http.Resolve(host_.c_str(), port_)) {
+ pendingResult_.error = "can't resolve host";
+ return false;
+ }
+ bool cancelled = false;
+ if (!http.Connect(1, 5.0, &cancelled)) {
+ pendingResult_.error = "can't connect to host";
+ return false;
+ }
+ net::RequestProgress progress(&cancelled);
+ Buffer result;
+ const int code = http.GET(http::RequestParams(path.c_str()), &result, &progress);
+ http.Disconnect();
+ if (code != 200) {
+ pendingResult_.error = "unsuccessful response code";
+ return false;
+ }
+ result.TakeAll(&outResult);
+ return true;
+}
diff --git a/Common/GhidraClient.h b/Common/GhidraClient.h
new file mode 100644
index 0000000000..efaeb00c7c
--- /dev/null
+++ b/Common/GhidraClient.h
@@ -0,0 +1,108 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+struct GhidraSymbol {
+ u32 address = 0;
+ std::string name;
+ bool label;
+ bool userDefined;
+ std::string dataTypePathName;
+};
+
+enum GhidraTypeKind {
+ ENUM,
+ TYPEDEF,
+ POINTER,
+ ARRAY,
+ STRUCTURE,
+ UNION,
+ FUNCTION_DEFINITION,
+ BUILT_IN,
+ UNKNOWN,
+};
+
+struct GhidraEnumMember {
+ std::string name;
+ u64 value = 0;
+ std::string comment;
+};
+
+struct GhidraCompositeMember {
+ std::string fieldName;
+ u32 ordinal = 0;
+ u32 offset = 0;
+ int length = 0;
+ std::string typePathName;
+ std::string comment;
+};
+
+struct GhidraType {
+ GhidraTypeKind kind;
+ std::string name;
+ std::string displayName;
+ std::string pathName;
+ int length = 0;
+ int alignedLength = 0;
+ bool zeroLength = false;
+ std::string description;
+
+ std::vector compositeMembers;
+ std::vector enumMembers;
+ std::string pointerTypePathName;
+ std::string typedefTypePathName;
+ std::string typedefBaseTypePathName;
+ std::string arrayTypePathName;
+ int arrayElementLength = 0;
+ u32 arrayElementCount = 0;
+ std::string functionPrototypeString;
+ std::string builtInGroup;
+};
+
+class GhidraClient {
+public:
+ enum class Status {
+ Idle,
+ Pending,
+ Ready,
+ };
+
+ struct Result {
+ std::vector symbols;
+ std::unordered_map types;
+ std::string error;
+ };
+
+ Result result;
+
+ ~GhidraClient();
+
+ void FetchAll(const std::string& host, int port);
+
+ void UpdateResult();
+
+ bool Idle() const { return status_ == Status::Idle; }
+
+ bool Ready() const { return status_ == Status::Ready; }
+
+ bool Failed() const { return !result.error.empty(); }
+
+private:
+ std::thread thread_;
+ std::mutex mutex_;
+ std::atomic status_;
+ Result pendingResult_;
+ std::string host_;
+ int port_ = 0;
+
+ bool FetchAllDo(const std::string& host, int port);
+
+ bool FetchSymbols();
+
+ bool FetchTypes();
+
+ bool FetchResource(const std::string& path, std::string& outResult);
+};
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj b/UWP/CommonUWP/CommonUWP.vcxproj
index 1678bcce43..921c7dc006 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj
+++ b/UWP/CommonUWP/CommonUWP.vcxproj
@@ -197,6 +197,7 @@
+
@@ -360,6 +361,7 @@
+
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj.filters b/UWP/CommonUWP/CommonUWP.vcxproj.filters
index ec247f71b1..47d4699d33 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj.filters
+++ b/UWP/CommonUWP/CommonUWP.vcxproj.filters
@@ -119,6 +119,7 @@
+
@@ -538,6 +539,7 @@
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index f431e54666..6ef541a9c8 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -373,6 +373,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Common/CPUDetect.cpp \
$(SRC)/Common/ExceptionHandlerSetup.cpp \
$(SRC)/Common/FakeCPUDetect.cpp \
+ $(SRC)/Common/GhidraClient.cpp \
$(SRC)/Common/Log.cpp \
$(SRC)/Common/Log/LogManager.cpp \
$(SRC)/Common/LogReporting.cpp \
diff --git a/libretro/Makefile.common b/libretro/Makefile.common
index 1a3fa09324..cb1c6419a9 100644
--- a/libretro/Makefile.common
+++ b/libretro/Makefile.common
@@ -483,6 +483,7 @@ SOURCES_CXX += \
$(COMMONDIR)/Log/StdioListener.cpp \
$(COMMONDIR)/ExceptionHandlerSetup.cpp \
$(COMMONDIR)/FakeCPUDetect.cpp \
+ $(COMMONDIR)/GhidraClient.cpp \
$(COMMONDIR)/Log.cpp \
$(COMMONDIR)/Log/LogManager.cpp \
$(COMMONDIR)/OSVersion.cpp \
From 3182cc29e401b81f2b40e3280774f72eb38dbf30 Mon Sep 17 00:00:00 2001
From: kotcrab <4594081+kotcrab@users.noreply.github.com>
Date: Mon, 11 Nov 2024 21:34:19 +0100
Subject: [PATCH 2/5] Add struct viewer debugging tool
---
CMakeLists.txt | 2 +
UI/ImDebugger/ImDebugger.cpp | 5 +
UI/ImDebugger/ImDebugger.h | 3 +
UI/ImDebugger/ImStructViewer.cpp | 783 ++++++++++++++++++++++++++++++
UI/ImDebugger/ImStructViewer.h | 71 +++
UI/UI.vcxproj | 2 +
UI/UI.vcxproj.filters | 6 +
UWP/UI_UWP/UI_UWP.vcxproj | 2 +
UWP/UI_UWP/UI_UWP.vcxproj.filters | 6 +
android/jni/Android.mk | 1 +
10 files changed, 881 insertions(+)
create mode 100644 UI/ImDebugger/ImStructViewer.cpp
create mode 100644 UI/ImDebugger/ImStructViewer.h
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8c22ed4a08..69b2bf0c29 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1525,6 +1525,8 @@ list(APPEND NativeAppSource
UI/ImDebugger/ImDebugger.h
UI/ImDebugger/ImDisasmView.cpp
UI/ImDebugger/ImDisasmView.h
+ UI/ImDebugger/ImStructViewer.cpp
+ UI/ImDebugger/ImStructViewer.h
UI/DiscordIntegration.cpp
UI/NativeApp.cpp
UI/BackgroundAudio.h
diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp
index aa15f8fec6..15e5c12bf0 100644
--- a/UI/ImDebugger/ImDebugger.cpp
+++ b/UI/ImDebugger/ImDebugger.cpp
@@ -377,6 +377,7 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) {
ImGui::Checkbox("HLE Modules", &cfg_.modulesOpen);
ImGui::Checkbox("HLE Threads", &cfg_.threadsOpen);
ImGui::Checkbox("sceAtrac", &cfg_.atracOpen);
+ ImGui::Checkbox("Struct viewer", &cfg_.structViewerOpen);
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
@@ -411,6 +412,10 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) {
}
DrawHLEModules(cfg_);
+
+ if (&cfg_.structViewerOpen) {
+ structViewer_.Draw(mipsDebug, &cfg_.structViewerOpen);
+ }
}
void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, bool *open, CoreState coreState) {
diff --git a/UI/ImDebugger/ImDebugger.h b/UI/ImDebugger/ImDebugger.h
index 389088deb6..b084dcf492 100644
--- a/UI/ImDebugger/ImDebugger.h
+++ b/UI/ImDebugger/ImDebugger.h
@@ -14,6 +14,7 @@
#include "Core/Debugger/DebugInterface.h"
#include "UI/ImDebugger/ImDisasmView.h"
+#include "UI/ImDebugger/ImStructViewer.h"
// This is the main state container of the whole Dear ImGUI-based in-game cross-platform debugger.
//
@@ -57,6 +58,7 @@ struct ImConfig {
bool modulesOpen = true;
bool hleModulesOpen = false;
bool atracOpen = true;
+ bool structViewerOpen = false;
// HLE explorer settings
// bool filterByUsed = true;
@@ -71,6 +73,7 @@ struct ImDebugger {
ImDisasmWindow disasm_;
ImLuaConsole luaConsole_;
+ ImStructViewer structViewer_;
// Open variables.
ImConfig cfg_;
diff --git a/UI/ImDebugger/ImStructViewer.cpp b/UI/ImDebugger/ImStructViewer.cpp
new file mode 100644
index 0000000000..8604a0817f
--- /dev/null
+++ b/UI/ImDebugger/ImStructViewer.cpp
@@ -0,0 +1,783 @@
+#include
+#include
+
+#include "ext/imgui/imgui.h"
+
+#include "Core/MemMap.h"
+#include "Core/Debugger/Breakpoints.h"
+#include "Core/MIPS/MIPSDebugInterface.h"
+
+#include "UI/ImDebugger/ImStructViewer.h"
+
+static auto COLOR_GRAY = ImVec4(0.45f, 0.45f, 0.45f, 1);
+static auto COLOR_RED = ImVec4(1, 0, 0, 1);
+
+enum class BuiltInType {
+ Bool,
+ Char,
+ Int8,
+ Int16,
+ Int32,
+ Int64,
+ TerminatedString,
+ Float,
+ Void,
+};
+
+struct BuiltIn {
+ BuiltInType type;
+ ImGuiDataType imGuiType;
+ const char* hexFormat;
+};
+
+static const std::unordered_map knownBuiltIns = {
+ {"/bool", {BuiltInType::Bool, ImGuiDataType_U8, "%hhx"}},
+
+ {"/char", {BuiltInType::Char, ImGuiDataType_S8, "%hhx"}},
+ {"/uchar", {BuiltInType::Char, ImGuiDataType_U8, "%hhx"}},
+
+ {"/byte", {BuiltInType::Int8, ImGuiDataType_U8, "%hhx"}},
+ {"/sbyte", {BuiltInType::Int8, ImGuiDataType_S8, "%hhx"}},
+ {"/undefined1", {BuiltInType::Int8, ImGuiDataType_U8, "%hhx"}},
+
+ {"/word", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}},
+ {"/sword", {BuiltInType::Int16, ImGuiDataType_S16, "%hx"}},
+ {"/ushort", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}},
+ {"/short", {BuiltInType::Int16, ImGuiDataType_S16, "%hx"}},
+ {"/undefined2", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}},
+
+ {"/dword", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}},
+ {"/sdword", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}},
+ {"/uint", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}},
+ {"/int", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}},
+ {"/ulong", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}},
+ {"/long", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}},
+ {"/undefined4", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}},
+
+ {"/qword", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}},
+ {"/sqword", {BuiltInType::Int64, ImGuiDataType_S64, "%llx"}},
+ {"/ulonglong", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}},
+ {"/longlong", {BuiltInType::Int64, ImGuiDataType_S64, "%llx"}},
+ {"/undefined8", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}},
+
+ {"/TerminatedCString", {BuiltInType::TerminatedString, -1, nullptr}},
+
+ {"/float", {BuiltInType::Float, ImGuiDataType_Float, nullptr}},
+ {"/float4", {BuiltInType::Float, ImGuiDataType_Float, nullptr}},
+
+ {"/void", {BuiltInType::Void, -1, nullptr}},
+};
+
+static void DrawBuiltInEditPopup(const BuiltIn& builtIn, const u32 address) {
+ if (builtIn.imGuiType == -1) {
+ return;
+ }
+ ImGui::OpenPopupOnItemClick("edit", ImGuiPopupFlags_MouseButtonRight);
+ if (ImGui::BeginPopup("edit")) {
+ if (ImGui::Selectable("Set to zero")) {
+ switch (builtIn.type) {
+ case BuiltInType::Bool:
+ case BuiltInType::Char:
+ case BuiltInType::Int8:
+ Memory::Write_U8(0, address);
+ break;
+ case BuiltInType::Int16:
+ Memory::Write_U16(0, address);
+ break;
+ case BuiltInType::Int32:
+ Memory::Write_U32(0, address);
+ break;
+ case BuiltInType::Int64:
+ Memory::Write_U64(0, address);
+ break;
+ case BuiltInType::Float:
+ Memory::Write_Float(0, address);
+ break;
+ default:
+ break;
+ }
+ }
+ void* data = Memory::GetPointerWriteUnchecked(address);
+ if (builtIn.hexFormat) {
+ ImGui::DragScalar("Value (hex)", builtIn.imGuiType, data, 0.2f, nullptr, nullptr, builtIn.hexFormat);
+ }
+ ImGui::DragScalar("Value", builtIn.imGuiType, data, 0.2f);
+ ImGui::EndPopup();
+ }
+}
+
+static void DrawIntBuiltInEditPopup(const u32 address, const u32 length) {
+ switch (length) {
+ case 1:
+ DrawBuiltInEditPopup(knownBuiltIns.at("/byte"), address);
+ break;
+ case 2:
+ DrawBuiltInEditPopup(knownBuiltIns.at("/word"), address);
+ break;
+ case 4:
+ DrawBuiltInEditPopup(knownBuiltIns.at("/dword"), address);
+ break;
+ case 8:
+ DrawBuiltInEditPopup(knownBuiltIns.at("/qword"), address);
+ break;
+ default:
+ break;
+ }
+}
+
+static void DrawBuiltInContent(const BuiltIn& builtIn, const u32 address) {
+ switch (builtIn.type) {
+ case BuiltInType::Bool:
+ ImGui::Text("= %s", Memory::Read_U8(address) ? "true" : "false");
+ break;
+ case BuiltInType::Char: {
+ const u8 value = Memory::Read_U8(address);
+ if (std::isprint(value)) {
+ ImGui::Text("= %x '%c'", value, value);
+ } else {
+ ImGui::Text("= %x", value);
+ }
+ break;
+ }
+ case BuiltInType::Int8:
+ ImGui::Text("= %x", Memory::Read_U8(address));
+ break;
+ case BuiltInType::Int16:
+ ImGui::Text("= %x", Memory::Read_U16(address));
+ break;
+ case BuiltInType::Int32:
+ ImGui::Text("= %x", Memory::Read_U32(address));
+ break;
+ case BuiltInType::Int64:
+ ImGui::Text("= %llx", Memory::Read_U64(address));
+ break;
+ case BuiltInType::TerminatedString:
+ if (Memory::IsValidNullTerminatedString(address)) {
+ ImGui::Text("= \"%s\"", Memory::GetCharPointerUnchecked(address));
+ } else {
+ ImGui::Text("= %x ", Memory::Read_U8(address), address);
+ }
+ break;
+ case BuiltInType::Float:
+ ImGui::Text("= %f", Memory::Read_Float(address));
+ break;
+ case BuiltInType::Void:
+ ImGui::Text("");
+ default:
+ return;
+ }
+ DrawBuiltInEditPopup(builtIn, address);
+}
+
+static u64 ReadMemoryInt(const u32 address, const u32 length) {
+ switch (length) {
+ case 1:
+ return Memory::Read_U8(address);
+ case 2:
+ return Memory::Read_U16(address);
+ case 4:
+ return Memory::Read_U32(address);
+ case 8:
+ return Memory::Read_U64(address);
+ default:
+ return 0;
+ }
+}
+
+static constexpr int COLUMN_NAME = 0;
+static constexpr int COLUMN_TYPE = 1;
+static constexpr int COLUMN_CONTENT = 2;
+
+void ImStructViewer::Draw(MIPSDebugInterface* mipsDebug, bool* open) {
+ mipsDebug_ = mipsDebug;
+ ImGui::SetNextWindowSize(ImVec2(430, 450), ImGuiCond_FirstUseEver);
+ if (!ImGui::Begin("Struct viewer", open) || !mipsDebug->isAlive() || !Memory::IsActive()) {
+ ImGui::End();
+ return;
+ }
+ if (ghidraClient_.Ready()) {
+ ghidraClient_.UpdateResult();
+ if (!fetchedAtLeastOnce_ && !ghidraClient_.Failed()) {
+ fetchedAtLeastOnce_ = true;
+ }
+ }
+
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
+ if (fetchedAtLeastOnce_) {
+ DrawStructViewer();
+ } else {
+ DrawConnectionSetup();
+ }
+ ImGui::PopStyleVar();
+
+ ImGui::End();
+}
+
+void ImStructViewer::DrawConnectionSetup() {
+ ImGui::TextWrapped("Struct viewer visualizes data in memory using types from your Ghidra project.");
+ ImGui::TextWrapped("To get started install the ghidra-rest-api plugin and start the Rest API server.");
+ ImGui::TextWrapped("When ready press the connect button below.");
+
+ ImGui::BeginDisabled(!ghidraClient_.Idle());
+ ImGui::PushItemWidth(120);
+ ImGui::InputText("Host", ghidraHost_, IM_ARRAYSIZE(ghidraHost_));
+ ImGui::SameLine();
+ ImGui::InputInt("Port", &ghidraPort_, 0);
+ ImGui::SameLine();
+ if (ImGui::Button("Connect")) {
+ ghidraClient_.FetchAll(ghidraHost_, ghidraPort_);
+ }
+ ImGui::PopItemWidth();
+ ImGui::EndDisabled();
+
+ if (ghidraClient_.Idle() && ghidraClient_.Failed()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
+ ImGui::TextWrapped("Error: %s", ghidraClient_.result.error.c_str());
+ ImGui::PopStyleColor();
+ }
+}
+
+void ImStructViewer::DrawStructViewer() {
+ ImGui::BeginDisabled(!ghidraClient_.Idle());
+ if (ImGui::Button("Refresh data types")) {
+ ghidraClient_.FetchAll(ghidraHost_, ghidraPort_);
+ }
+ ImGui::EndDisabled();
+
+ if (ghidraClient_.Idle() && ghidraClient_.Failed()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
+ ImGui::SameLine();
+ ImGui::TextWrapped("Error: %s", ghidraClient_.result.error.c_str());
+ ImGui::PopStyleColor();
+ }
+
+ if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_Reorderable)) {
+ if (ImGui::BeginTabItem("Globals")) {
+ DrawGlobals();
+ ImGui::EndTabItem();
+ }
+ if (ImGui::BeginTabItem("Watch")) {
+ DrawWatch();
+ ImGui::EndTabItem();
+ }
+ ImGui::EndTabBar();
+ }
+}
+
+void ImStructViewer::DrawGlobals() {
+ globalFilter_.Draw();
+ if (ImGui::BeginTable("##globals", 3,
+ ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
+ ImGuiTableFlags_RowBg)) {
+ ImGui::TableSetupScrollFreeze(0, 1);
+ ImGui::TableSetupColumn("Field");
+ ImGui::TableSetupColumn("Type");
+ ImGui::TableSetupColumn("Content");
+ ImGui::TableHeadersRow();
+
+ for (const auto& symbol: ghidraClient_.result.symbols) {
+ if (!symbol.label || !symbol.userDefined || symbol.dataTypePathName.empty()) {
+ continue;
+ }
+ if (!globalFilter_.PassFilter(symbol.name.c_str())) {
+ continue;
+ }
+ DrawType(symbol.address, 0, symbol.dataTypePathName, nullptr, symbol.name.c_str(), -1);
+ }
+
+ ImGui::EndTable();
+ }
+}
+
+void ImStructViewer::DrawWatch() {
+ DrawNewWatchEntry();
+ ImGui::Dummy(ImVec2(1, 6));
+
+ watchFilter_.Draw();
+ if (ImGui::BeginTable("##watch", 3,
+ ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
+ ImGuiTableFlags_RowBg)) {
+ ImGui::TableSetupScrollFreeze(0, 1);
+ ImGui::TableSetupColumn("Field");
+ ImGui::TableSetupColumn("Type");
+ ImGui::TableSetupColumn("Content");
+ ImGui::TableHeadersRow();
+
+ int watchIndex = -1;
+ for (const auto& watch: watches_) {
+ watchIndex++;
+ if (!watchFilter_.PassFilter(watch.name.c_str())) {
+ continue;
+ }
+ u32 address = 0;
+ if (!watch.expression.empty()) {
+ u32 val;
+ PostfixExpression postfix;
+ if (mipsDebug_->initExpression(watch.expression.c_str(), postfix)) {
+ if (mipsDebug_->parseExpression(postfix, val)) {
+ address = val;
+ }
+ }
+ } else {
+ address = watch.address;
+ }
+ DrawType(address, 0, watch.typePathName, nullptr, watch.name.c_str(), watchIndex);
+ }
+ if (removeWatchIndex_ != -1) {
+ watches_.erase(watches_.begin() + removeWatchIndex_);
+ removeWatchIndex_ = -1;
+ }
+ if (addWatch_.address != 0) {
+ watches_.push_back(addWatch_);
+ addWatch_ = Watch();
+ }
+
+ ImGui::EndTable();
+ }
+}
+
+void ImStructViewer::DrawNewWatchEntry() {
+ ImGui::PushItemWidth(150);
+ ImGui::InputText("Name", newWatch_.name, IM_ARRAYSIZE(newWatch_.name));
+
+ ImGui::SameLine();
+ if (ImGui::BeginCombo("Type", newWatch_.typeDisplayName.c_str())) {
+ if (ImGui::IsWindowAppearing()) {
+ ImGui::SetKeyboardFocusHere(0);
+ }
+ newWatch_.typeFilter.Draw();
+ for (const auto& entry: ghidraClient_.result.types) {
+ const auto& type = entry.second;
+ if (newWatch_.typeFilter.PassFilter(type.displayName.c_str())) {
+ ImGui::PushID(type.pathName.c_str());
+ if (ImGui::Selectable(type.displayName.c_str(), newWatch_.typePathName == type.pathName)) {
+ newWatch_.typePathName = type.pathName;
+ newWatch_.typeDisplayName = type.displayName;
+ }
+ if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && ImGui::BeginTooltip()) {
+ ImGui::Text("%s (%s)", type.displayName.c_str(), type.pathName.c_str());
+ ImGui::Text("Length: %x (aligned: %x)", type.length, type.alignedLength);
+ ImGui::EndTooltip();
+ }
+ ImGui::PopID();
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ ImGui::SameLine();
+ ImGui::InputText("Expression", newWatch_.expression, IM_ARRAYSIZE(newWatch_.expression));
+ ImGui::SameLine();
+ ImGui::Checkbox("Dynamic", &newWatch_.dynamic);
+ if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal))
+ ImGui::SetTooltip("When checked the expression will be\nre-evaluated on each frame.");
+
+ ImGui::PopItemWidth();
+
+ ImGui::SameLine();
+ if (ImGui::Button("Add watch")) {
+ u32 val;
+ PostfixExpression postfix;
+ if (newWatch_.typePathName.empty()) {
+ newWatch_.error = "type can't be empty";
+ } else if (!mipsDebug_->initExpression(newWatch_.expression, postfix)
+ || !mipsDebug_->parseExpression(postfix, val)) {
+ newWatch_.error = "invalid expression";
+ } else {
+ std::string watchName = newWatch_.name;
+ if (watchName.empty()) {
+ watchName = "";
+ }
+ watches_.emplace_back(Watch{
+ newWatch_.dynamic ? newWatch_.expression : "",
+ newWatch_.dynamic ? 0 : val,
+ newWatch_.typePathName,
+ newWatch_.dynamic ? watchName + " (" + newWatch_.expression + ")" : watchName
+ });
+ memset(newWatch_.name, 0, sizeof(newWatch_.name));
+ memset(newWatch_.expression, 0, sizeof(newWatch_.name));
+ newWatch_.dynamic = false;
+ newWatch_.error = "";
+ newWatch_.typeFilter.Clear();
+ // Not clearing the actual selected type on purpose here, user will have to reselect one anyway and maybe
+ // there is a chance they will reuse the current one
+ }
+ }
+ if (!newWatch_.error.empty()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
+ ImGui::TextWrapped("Error: %s", newWatch_.error.c_str());
+ ImGui::PopStyleColor();
+ }
+}
+
+static void DrawTypeColumn(
+ const std::string& format,
+ const std::string& typeDisplayName,
+ const u32 base,
+ const u32 offset
+) {
+ ImGui::TableSetColumnIndex(COLUMN_TYPE);
+ ImGui::Text(format.c_str(), typeDisplayName.c_str());
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY);
+ ImGui::SameLine();
+ if (offset != 0) {
+ ImGui::Text("@ %x+%x", base, offset);
+ } else {
+ ImGui::Text("@ %x", base);
+ }
+ ImGui::PopStyleColor();
+}
+
+static void DrawArrayContent(
+ const std::unordered_map& types,
+ const GhidraType& type,
+ const u32 address
+) {
+ if (type.arrayElementLength != 1 || !types.count(type.arrayTypePathName)) {
+ return;
+ }
+ const auto& arrayType = types.at(type.arrayTypePathName);
+ bool charElement = false;
+ if (arrayType.kind == TYPEDEF && types.count(arrayType.typedefBaseTypePathName)) {
+ const auto& baseArrayType = types.at(arrayType.typedefBaseTypePathName);
+ charElement = baseArrayType.pathName == "/char";
+ } else {
+ charElement = arrayType.pathName == "/char";
+ }
+ if (!charElement) {
+ return;
+ }
+ const char* charPointer = Memory::GetCharPointerUnchecked(address);
+ std::string text(charPointer, charPointer + type.arrayElementCount);
+ text = std::regex_replace(text, std::regex("\n"), "\\n");
+ ImGui::Text("= \"%s\"", text.c_str());
+}
+
+static void DrawPointerText(const u32 value) {
+ if (Memory::IsValidAddress(value)) {
+ ImGui::Text("* %x", value);
+ return;
+ }
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY);
+ if (value == 0) {
+ ImGui::Text("* NULL");
+ } else {
+ ImGui::Text("* ", value);
+ }
+ ImGui::PopStyleColor();
+}
+
+static void DrawPointerContent(
+ const std::unordered_map& types,
+ const GhidraType& type,
+ const u32 value
+) {
+ if (!types.count(type.pointerTypePathName)) {
+ DrawPointerText(value);
+ return;
+ }
+ const auto& pointedType = types.at(type.pointerTypePathName);
+ bool charPointerElement = false;
+ if (pointedType.kind == TYPEDEF && types.count(pointedType.typedefBaseTypePathName)) {
+ const auto& basePointedType = types.at(pointedType.typedefBaseTypePathName);
+ charPointerElement = basePointedType.pathName == "/char";
+ } else {
+ charPointerElement = pointedType.pathName == "/char";
+ }
+ if (!charPointerElement || !Memory::IsValidNullTerminatedString(value)) {
+ DrawPointerText(value);
+ return;
+ }
+ const char* charPointer = Memory::GetCharPointerUnchecked(value);
+ std::string text(charPointer);
+ text = std::regex_replace(text, std::regex("\n"), "\\n");
+ ImGui::Text("= \"%s\"", text.c_str());
+}
+
+// Formatting enum value to a nice string as it would look in code with 'or' operator (e.g. "ALIGN_TOP | ALIGN_LEFT")
+static std::string FormatEnumValue(const std::vector& enumMembers, const u64 value) {
+ std::stringstream ss;
+ bool hasPrevious = false;
+ for (const auto& member: enumMembers) {
+ if (value & member.value) {
+ if (hasPrevious) {
+ ss << " | ";
+ }
+ ss << member.name;
+ hasPrevious = true;
+ }
+ }
+ return ss.str();
+}
+
+// This will be potentially called a lot of times in a frame so not using string here
+static void FormatIndexedMember(char* buffer, const size_t bufferSize, const std::string& name, const u32 index) {
+ snprintf(buffer, bufferSize, "%s[%x]", name.c_str(), index);
+}
+
+void ImStructViewer::DrawType(
+ const u32 base,
+ const u32 offset,
+ const std::string& typePathName,
+ const char* typeDisplayNameOverride,
+ const char* name,
+ const int watchIndex,
+ const ImGuiTreeNodeFlags extraTreeNodeFlags
+) {
+ const auto& types = ghidraClient_.result.types;
+ // Generic pointer is not included in the type listing, need to resolve it manually to void*
+ if (typePathName == "/pointer") {
+ DrawType(base, offset, "/void *", "pointer", name, watchIndex);
+ return;
+ }
+ // Undefined itself doesn't exist as a type, let's just display first byte in that case
+ if (typePathName == "/undefined") {
+ DrawType(base, offset, "/undefined1", "undefined", name, watchIndex);
+ return;
+ }
+
+ const bool hasType = types.count(typePathName) != 0;
+
+ // Resolve typedefs as early as possible
+ if (hasType) {
+ const auto& type = types.at(typePathName);
+ if (type.kind == TYPEDEF) {
+ DrawType(base, offset, type.typedefBaseTypePathName, type.displayName.c_str(), name,
+ watchIndex);
+ return;
+ }
+ }
+
+ const u32 address = base + offset;
+ ImGui::PushID(static_cast(address));
+ ImGui::PushID(watchIndex);
+
+ // Text and Tree nodes are less high than framed widgets, using AlignTextToFramePadding() we add vertical spacing
+ // to make the tree lines equal high.
+ ImGui::TableNextRow();
+ ImGui::TableSetColumnIndex(COLUMN_NAME);
+ ImGui::AlignTextToFramePadding();
+ // Flags used for nodes that can't be further opened
+ const ImGuiTreeNodeFlags leafFlags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen |
+ ImGuiTreeNodeFlags_Bullet | extraTreeNodeFlags;
+
+ // Type is missing in fetched types, this can happen e.g. if type used for watch is removed from Ghidra
+ if (!hasType) {
+ ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
+ DrawContextMenu(base, offset, 0, typePathName, name, watchIndex);
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
+ DrawTypeColumn("", typePathName, base, offset);
+ ImGui::PopStyleColor();
+ ImGui::PopID();
+ ImGui::PopID();
+ return;
+ }
+
+ const auto& type = types.at(typePathName);
+ const std::string typeDisplayName =
+ typeDisplayNameOverride == nullptr ? type.displayName : typeDisplayNameOverride;
+
+ // Handle cases where pointers or expressions point to invalid memory
+ if (!Memory::IsValidAddress(address)) {
+ ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawTypeColumn("%s", typeDisplayName, base, offset);
+ ImGui::TableSetColumnIndex(COLUMN_CONTENT);
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY);
+ ImGui::Text("", address);
+ ImGui::PopStyleColor();
+ ImGui::PopID();
+ ImGui::PopID();
+ return;
+ }
+
+ // For each type we create tree node with the field name and fill type column
+ // Content column and edit popup is only set for types where it makes sense
+ switch (type.kind) {
+ case ENUM: {
+ ImGui::TreeNodeEx("Enum", leafFlags, "%s", name);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawTypeColumn("%s", typeDisplayName, base, offset);
+
+ ImGui::TableSetColumnIndex(COLUMN_CONTENT);
+ const u64 value = ReadMemoryInt(address, type.length);
+ const std::string stringValue = FormatEnumValue(type.enumMembers, value);
+ ImGui::Text("= %llx (%s)", value, stringValue.c_str());
+ DrawIntBuiltInEditPopup(address, type.length);
+ break;
+ }
+ case POINTER: {
+ const bool nodeOpen = ImGui::TreeNodeEx("Pointer", extraTreeNodeFlags, "%s", name);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawTypeColumn("%s", typeDisplayName, base, offset);
+
+ ImGui::TableSetColumnIndex(COLUMN_CONTENT);
+ const u32 pointer = Memory::Read_U32(address);
+ DrawPointerContent(types, type, pointer);
+
+ if (nodeOpen) {
+ if (types.count(type.pointerTypePathName)) {
+ const auto& pointedType = types.at(type.pointerTypePathName);
+
+ const auto countStateId = ImGui::GetID("PointerElementCount");
+ const int pointerElementCount = ImGui::GetStateStorage()->GetInt(countStateId, 1);
+
+ // A pointer to unsized type (e.g. function or void) can't have more than one element
+ if (pointedType.alignedLength > 0) {
+ ImGui::TableNextRow();
+ ImGui::TableSetColumnIndex(COLUMN_NAME);
+ if (ImGui::Button("Show more")) {
+ ImGui::GetStateStorage()->SetInt(countStateId, pointerElementCount + 1);
+ }
+ if (pointerElementCount > 1) {
+ ImGui::SameLine();
+ ImGui::Text("(showing %x)", pointerElementCount);
+ }
+ }
+
+ for (int i = 0; i < pointerElementCount; i++) {
+ char nameBuffer[256];
+ FormatIndexedMember(nameBuffer, sizeof(nameBuffer), name, i);
+ // A pointer always creates extra node in the tree so using DefaultOpen to spare user one click
+ DrawType(pointer, i * pointedType.alignedLength, type.pointerTypePathName,
+ nullptr, nameBuffer, -1, ImGuiTreeNodeFlags_DefaultOpen);
+ }
+ }
+ ImGui::TreePop();
+ }
+ break;
+ }
+ case ARRAY: {
+ const bool nodeOpen = ImGui::TreeNodeEx("Array", extraTreeNodeFlags, "%s", name);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawTypeColumn("%s", typeDisplayName, base, offset);
+
+ ImGui::TableSetColumnIndex(COLUMN_CONTENT);
+ DrawArrayContent(types, type, address);
+
+ if (nodeOpen) {
+ for (int i = 0; i < type.arrayElementCount; i++) {
+ char nameBuffer[256];
+ FormatIndexedMember(nameBuffer, sizeof(nameBuffer), name, i);
+ DrawType(base, offset + i * type.arrayElementLength, type.arrayTypePathName,
+ nullptr, nameBuffer, -1);
+ }
+ ImGui::TreePop();
+ }
+ break;
+ }
+ case STRUCTURE:
+ case UNION: {
+ const bool nodeOpen = ImGui::TreeNodeEx("Composite", extraTreeNodeFlags, "%s", name);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawTypeColumn("%s", typeDisplayName, base, offset);
+
+ if (nodeOpen) {
+ for (const auto& member: type.compositeMembers) {
+ DrawType(base, offset + member.offset, member.typePathName, nullptr,
+ member.fieldName.c_str(), -1);
+ }
+ ImGui::TreePop();
+ }
+ break;
+ }
+ case FUNCTION_DEFINITION:
+ ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawTypeColumn("%s", typeDisplayName, base, offset);
+
+ ImGui::TableSetColumnIndex(COLUMN_CONTENT);
+ ImGui::Text(""); // TODO could be go to in disassembler here
+ break;
+ case BUILT_IN: {
+ ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+
+ if (knownBuiltIns.count(typePathName)) {
+ DrawTypeColumn("%s", typeDisplayName, base, offset);
+ ImGui::TableSetColumnIndex(COLUMN_CONTENT);
+ DrawBuiltInContent(knownBuiltIns.at(typePathName), address);
+ } else {
+ // Some built in types are rather obscure so we don't handle every possible one
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
+ DrawTypeColumn("", typePathName, base, offset);
+ ImGui::PopStyleColor();
+ }
+ break;
+ }
+ default: {
+ // At this point there is most likely some issue in the Ghidra plugin and the type wasn't
+ // classified to any category
+ ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
+ DrawTypeColumn("", typeDisplayName, base, offset);
+ ImGui::PopStyleColor();
+ break;
+ }
+ }
+
+ ImGui::PopID();
+ ImGui::PopID();
+}
+
+void ImStructViewer::DrawContextMenu(
+ const u32 base,
+ const u32 offset,
+ const int length,
+ const std::string& typePathName,
+ const char* name,
+ const int watchIndex
+) {
+ ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
+ if (ImGui::BeginPopup("context")) {
+ const u32 address = base + offset;
+
+ // This might be called when iterating over existing watches so can't modify the watch vector directly here
+ if (watchIndex < 0) {
+ if (ImGui::MenuItem("Add watch")) {
+ addWatch_.address = address;
+ addWatch_.typePathName = typePathName;
+ addWatch_.name = name;
+ }
+ } else if (watchIndex < watches_.size()) {
+ if (ImGui::MenuItem("Remove watch")) {
+ removeWatchIndex_ = watchIndex;
+ }
+ }
+
+ // if (ImGui::MenuItem("Go to in memory view")) {
+ // TODO imgui memory view not yet implemented
+ // }
+
+ // Memory breakpoints are only possible for sized types
+ if (length > 0) {
+ const u32 end = address + length;
+ MemCheck memCheck;
+ const bool hasMemCheck = CBreakPoints::GetMemCheck(address, end, &memCheck);
+ if (hasMemCheck) {
+ if (ImGui::MenuItem("Remove memory breakpoint")) {
+ CBreakPoints::RemoveMemCheck(address, end);
+ }
+ }
+ if (!hasMemCheck || !(memCheck.cond & MEMCHECK_READ)) {
+ if (ImGui::MenuItem("Add memory read breakpoint")) {
+ CBreakPoints::AddMemCheck(address, end, MEMCHECK_READ, BREAK_ACTION_PAUSE);
+ }
+ }
+ if (!hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE)) {
+ if (ImGui::MenuItem("Add memory write breakpoint")) {
+ CBreakPoints::AddMemCheck(address, end, MEMCHECK_WRITE, BREAK_ACTION_PAUSE);
+ }
+ }
+ if (!hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE_ONCHANGE)) {
+ if (ImGui::MenuItem("Add memory write on change breakpoint")) {
+ constexpr auto cond = static_cast(MEMCHECK_WRITE | MEMCHECK_WRITE_ONCHANGE);
+ CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE);
+ }
+ }
+ }
+
+ ImGui::EndPopup();
+ }
+}
diff --git a/UI/ImDebugger/ImStructViewer.h b/UI/ImDebugger/ImStructViewer.h
new file mode 100644
index 0000000000..473f4b4f5d
--- /dev/null
+++ b/UI/ImDebugger/ImStructViewer.h
@@ -0,0 +1,71 @@
+#pragma once
+
+#include "ext/imgui/imgui.h"
+
+#include "Common/GhidraClient.h"
+#include "Core/MIPS/MIPSDebugInterface.h"
+
+class ImStructViewer {
+ struct Watch {
+ std::string expression;
+ u32 address = 0;
+ std::string typePathName;
+ std::string name;
+ };
+
+ struct NewWatch {
+ char name[256] = {};
+ std::string typeDisplayName;
+ std::string typePathName;
+ char expression[256] = {};
+ bool dynamic = false;
+ std::string error;
+ ImGuiTextFilter typeFilter;
+ };
+
+public:
+ void Draw(MIPSDebugInterface* mipsDebug, bool* open);
+
+private:
+ MIPSDebugInterface* mipsDebug_ = nullptr;
+
+ ImGuiTextFilter globalFilter_;
+ ImGuiTextFilter watchFilter_;
+
+ GhidraClient ghidraClient_;
+ char ghidraHost_[128] = "localhost";
+ int ghidraPort_ = 18489;
+ bool fetchedAtLeastOnce_ = false;
+
+ std::vector watches_;
+ int removeWatchIndex_ = -1;
+ Watch addWatch_;
+ NewWatch newWatch_;
+
+ void DrawConnectionSetup();
+
+ void DrawStructViewer();
+
+ void DrawGlobals();
+
+ void DrawWatch();
+
+ void DrawNewWatchEntry();
+
+ void DrawType(
+ u32 base,
+ u32 offset,
+ const std::string& typePathName,
+ const char* typeDisplayNameOverride,
+ const char* name,
+ int watchIndex,
+ ImGuiTreeNodeFlags extraTreeNodeFlags = 0);
+
+ void DrawContextMenu(
+ u32 base,
+ u32 offset,
+ int length,
+ const std::string& typePathName,
+ const char* name,
+ int watchIndex);
+};
diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj
index f8fbc3f26f..b1b2ba0c90 100644
--- a/UI/UI.vcxproj
+++ b/UI/UI.vcxproj
@@ -53,6 +53,7 @@
+
@@ -95,6 +96,7 @@
+
diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters
index 288c8556a0..6c71cd8ab9 100644
--- a/UI/UI.vcxproj.filters
+++ b/UI/UI.vcxproj.filters
@@ -104,6 +104,9 @@
ImDebugger
+
+ ImDebugger
+
@@ -208,6 +211,9 @@
ImDebugger
+
+ ImDebugger
+
diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj
index 88f2bc539e..20fab0de8e 100644
--- a/UWP/UI_UWP/UI_UWP.vcxproj
+++ b/UWP/UI_UWP/UI_UWP.vcxproj
@@ -128,6 +128,7 @@
+
@@ -170,6 +171,7 @@
+
diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters
index 656c6c61e2..ba0db38ee8 100644
--- a/UWP/UI_UWP/UI_UWP.vcxproj.filters
+++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters
@@ -45,6 +45,9 @@
ImDebugger
+
+ ImDebugger
+
@@ -91,6 +94,9 @@
ImDebugger
+
+ ImDebugger
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index 6ef541a9c8..ebebbfe480 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -877,6 +877,7 @@ LOCAL_SRC_FILES := \
$(SRC)/android/jni/OpenSLContext.cpp \
$(SRC)/UI/ImDebugger/ImDebugger.cpp \
$(SRC)/UI/ImDebugger/ImDisasmView.cpp \
+ $(SRC)/UI/ImDebugger/ImStructViewer.cpp \
$(SRC)/UI/AudioCommon.cpp \
$(SRC)/UI/BackgroundAudio.cpp \
$(SRC)/UI/DiscordIntegration.cpp \
From f9d7e426f8e34be5b947c48b7ef7b19f15be5d35 Mon Sep 17 00:00:00 2001
From: kotcrab <4594081+kotcrab@users.noreply.github.com>
Date: Tue, 12 Nov 2024 21:05:11 +0100
Subject: [PATCH 3/5] Support copy address and value in Struct viewer
Reorganize add breakpoint menu
Style fixes
---
Common/GhidraClient.cpp | 8 +--
Common/GhidraClient.h | 2 +-
UI/ImDebugger/ImStructViewer.cpp | 88 ++++++++++++++++++++------------
UI/ImDebugger/ImStructViewer.h | 3 +-
4 files changed, 62 insertions(+), 39 deletions(-)
diff --git a/Common/GhidraClient.cpp b/Common/GhidraClient.cpp
index b26e5a3742..e0c5bbe3ca 100644
--- a/Common/GhidraClient.cpp
+++ b/Common/GhidraClient.cpp
@@ -74,7 +74,7 @@ bool GhidraClient::FetchSymbols() {
return false;
}
- for (const auto pEntry: entries) {
+ for (const auto pEntry : entries) {
JsonGet entry = pEntry->value;
GhidraSymbol symbol;
@@ -104,7 +104,7 @@ bool GhidraClient::FetchTypes() {
return false;
}
- for (const auto pEntry: entries) {
+ for (const auto pEntry : entries) {
const JsonGet entry = pEntry->value;
GhidraType type;
@@ -124,7 +124,7 @@ bool GhidraClient::FetchTypes() {
pendingResult_.error = "missing enum members";
return false;
}
- for (const JsonNode* pEnumEntry: enumEntries->value) {
+ for (const JsonNode* pEnumEntry : enumEntries->value) {
JsonGet enumEntry = pEnumEntry->value;
GhidraEnumMember member;
member.name = enumEntry.getStringOr("name", "");
@@ -153,7 +153,7 @@ bool GhidraClient::FetchTypes() {
pendingResult_.error = "missing composite members";
return false;
}
- for (const JsonNode* pCompositeEntry: compositeEntries->value) {
+ for (const JsonNode* pCompositeEntry : compositeEntries->value) {
JsonGet compositeEntry = pCompositeEntry->value;
GhidraCompositeMember member;
member.fieldName = compositeEntry.getStringOr("fieldName", "");
diff --git a/Common/GhidraClient.h b/Common/GhidraClient.h
index efaeb00c7c..1884e4a194 100644
--- a/Common/GhidraClient.h
+++ b/Common/GhidraClient.h
@@ -63,13 +63,13 @@ struct GhidraType {
};
class GhidraClient {
-public:
enum class Status {
Idle,
Pending,
Ready,
};
+public:
struct Result {
std::vector symbols;
std::unordered_map types;
diff --git a/UI/ImDebugger/ImStructViewer.cpp b/UI/ImDebugger/ImStructViewer.cpp
index 8604a0817f..84d1701a10 100644
--- a/UI/ImDebugger/ImStructViewer.cpp
+++ b/UI/ImDebugger/ImStructViewer.cpp
@@ -3,6 +3,7 @@
#include "ext/imgui/imgui.h"
+#include "Common/System/Request.h"
#include "Core/MemMap.h"
#include "Core/Debugger/Breakpoints.h"
#include "Core/MIPS/MIPSDebugInterface.h"
@@ -190,7 +191,7 @@ static constexpr int COLUMN_CONTENT = 2;
void ImStructViewer::Draw(MIPSDebugInterface* mipsDebug, bool* open) {
mipsDebug_ = mipsDebug;
- ImGui::SetNextWindowSize(ImVec2(430, 450), ImGuiCond_FirstUseEver);
+ ImGui::SetNextWindowSize(ImVec2(750, 550), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Struct viewer", open) || !mipsDebug->isAlive() || !Memory::IsActive()) {
ImGui::End();
return;
@@ -275,7 +276,7 @@ void ImStructViewer::DrawGlobals() {
ImGui::TableSetupColumn("Content");
ImGui::TableHeadersRow();
- for (const auto& symbol: ghidraClient_.result.symbols) {
+ for (const auto& symbol : ghidraClient_.result.symbols) {
if (!symbol.label || !symbol.userDefined || symbol.dataTypePathName.empty()) {
continue;
}
@@ -304,7 +305,7 @@ void ImStructViewer::DrawWatch() {
ImGui::TableHeadersRow();
int watchIndex = -1;
- for (const auto& watch: watches_) {
+ for (const auto& watch : watches_) {
watchIndex++;
if (!watchFilter_.PassFilter(watch.name.c_str())) {
continue;
@@ -313,10 +314,9 @@ void ImStructViewer::DrawWatch() {
if (!watch.expression.empty()) {
u32 val;
PostfixExpression postfix;
- if (mipsDebug_->initExpression(watch.expression.c_str(), postfix)) {
- if (mipsDebug_->parseExpression(postfix, val)) {
- address = val;
- }
+ if (mipsDebug_->initExpression(watch.expression.c_str(), postfix)
+ && mipsDebug_->parseExpression(postfix, val)) {
+ address = val;
}
} else {
address = watch.address;
@@ -346,7 +346,7 @@ void ImStructViewer::DrawNewWatchEntry() {
ImGui::SetKeyboardFocusHere(0);
}
newWatch_.typeFilter.Draw();
- for (const auto& entry: ghidraClient_.result.types) {
+ for (const auto& entry : ghidraClient_.result.types) {
const auto& type = entry.second;
if (newWatch_.typeFilter.PassFilter(type.displayName.c_str())) {
ImGui::PushID(type.pathName.c_str());
@@ -498,7 +498,7 @@ static void DrawPointerContent(
static std::string FormatEnumValue(const std::vector& enumMembers, const u64 value) {
std::stringstream ss;
bool hasPrevious = false;
- for (const auto& member: enumMembers) {
+ for (const auto& member : enumMembers) {
if (value & member.value) {
if (hasPrevious) {
ss << " | ";
@@ -559,12 +559,12 @@ void ImStructViewer::DrawType(
ImGui::AlignTextToFramePadding();
// Flags used for nodes that can't be further opened
const ImGuiTreeNodeFlags leafFlags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen |
- ImGuiTreeNodeFlags_Bullet | extraTreeNodeFlags;
+ extraTreeNodeFlags;
// Type is missing in fetched types, this can happen e.g. if type used for watch is removed from Ghidra
if (!hasType) {
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
- DrawContextMenu(base, offset, 0, typePathName, name, watchIndex);
+ DrawContextMenu(base, offset, 0, typePathName, name, watchIndex, nullptr);
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
DrawTypeColumn("", typePathName, base, offset);
ImGui::PopStyleColor();
@@ -580,7 +580,7 @@ void ImStructViewer::DrawType(
// Handle cases where pointers or expressions point to invalid memory
if (!Memory::IsValidAddress(address)) {
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
- DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
DrawTypeColumn("%s", typeDisplayName, base, offset);
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY);
@@ -596,23 +596,24 @@ void ImStructViewer::DrawType(
switch (type.kind) {
case ENUM: {
ImGui::TreeNodeEx("Enum", leafFlags, "%s", name);
- DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ const u64 enumValue = ReadMemoryInt(address, type.length);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, &enumValue);
DrawTypeColumn("%s", typeDisplayName, base, offset);
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
- const u64 value = ReadMemoryInt(address, type.length);
- const std::string stringValue = FormatEnumValue(type.enumMembers, value);
- ImGui::Text("= %llx (%s)", value, stringValue.c_str());
+ const std::string enumString = FormatEnumValue(type.enumMembers, enumValue);
+ ImGui::Text("= %llx (%s)", enumValue, enumString.c_str());
DrawIntBuiltInEditPopup(address, type.length);
break;
}
case POINTER: {
const bool nodeOpen = ImGui::TreeNodeEx("Pointer", extraTreeNodeFlags, "%s", name);
- DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ const u32 pointer = Memory::Read_U32(address);
+ const u64 pointer64 = pointer;
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, &pointer64);
DrawTypeColumn("%s", typeDisplayName, base, offset);
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
- const u32 pointer = Memory::Read_U32(address);
DrawPointerContent(types, type, pointer);
if (nodeOpen) {
@@ -649,7 +650,7 @@ void ImStructViewer::DrawType(
}
case ARRAY: {
const bool nodeOpen = ImGui::TreeNodeEx("Array", extraTreeNodeFlags, "%s", name);
- DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
DrawTypeColumn("%s", typeDisplayName, base, offset);
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
@@ -669,11 +670,11 @@ void ImStructViewer::DrawType(
case STRUCTURE:
case UNION: {
const bool nodeOpen = ImGui::TreeNodeEx("Composite", extraTreeNodeFlags, "%s", name);
- DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
DrawTypeColumn("%s", typeDisplayName, base, offset);
if (nodeOpen) {
- for (const auto& member: type.compositeMembers) {
+ for (const auto& member : type.compositeMembers) {
DrawType(base, offset + member.offset, member.typePathName, nullptr,
member.fieldName.c_str(), -1);
}
@@ -683,7 +684,7 @@ void ImStructViewer::DrawType(
}
case FUNCTION_DEFINITION:
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
- DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
DrawTypeColumn("%s", typeDisplayName, base, offset);
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
@@ -691,9 +692,11 @@ void ImStructViewer::DrawType(
break;
case BUILT_IN: {
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
- DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
if (knownBuiltIns.count(typePathName)) {
+ // This will copy float as int, but we can live with that for now
+ const u64 value = ReadMemoryInt(address, type.alignedLength);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, &value);
DrawTypeColumn("%s", typeDisplayName, base, offset);
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
DrawBuiltInContent(knownBuiltIns.at(typePathName), address);
@@ -709,7 +712,7 @@ void ImStructViewer::DrawType(
// At this point there is most likely some issue in the Ghidra plugin and the type wasn't
// classified to any category
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
- DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
+ DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
DrawTypeColumn("", typeDisplayName, base, offset);
ImGui::PopStyleColor();
@@ -721,18 +724,33 @@ void ImStructViewer::DrawType(
ImGui::PopID();
}
+static void CopyHexNumberToClipboard(u64 value) {
+ std::stringstream ss;
+ ss << std::hex << value;
+ const std::string valueString = ss.str();
+ System_CopyStringToClipboard(valueString);
+}
+
void ImStructViewer::DrawContextMenu(
const u32 base,
const u32 offset,
const int length,
const std::string& typePathName,
const char* name,
- const int watchIndex
+ const int watchIndex,
+ const u64* value
) {
ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
if (ImGui::BeginPopup("context")) {
const u32 address = base + offset;
+ if (ImGui::MenuItem("Copy address")) {
+ CopyHexNumberToClipboard(address);
+ }
+ if (value && ImGui::MenuItem("Copy value")) {
+ CopyHexNumberToClipboard(*value);
+ }
+
// This might be called when iterating over existing watches so can't modify the watch vector directly here
if (watchIndex < 0) {
if (ImGui::MenuItem("Add watch")) {
@@ -760,21 +778,25 @@ void ImStructViewer::DrawContextMenu(
CBreakPoints::RemoveMemCheck(address, end);
}
}
- if (!hasMemCheck || !(memCheck.cond & MEMCHECK_READ)) {
- if (ImGui::MenuItem("Add memory read breakpoint")) {
+ const bool canAddRead = !hasMemCheck || !(memCheck.cond & MEMCHECK_READ);
+ const bool canAddWrite = !hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE);
+ const bool canAddWriteOnChange = !hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE_ONCHANGE);
+ if ((canAddRead || canAddWrite || canAddWriteOnChange) && ImGui::BeginMenu("Add memory breakpoint")) {
+ if (canAddRead && canAddWrite && ImGui::MenuItem("Read/Write")) {
+ constexpr auto cond = static_cast(MEMCHECK_READ | MEMCHECK_WRITE);
+ CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE);
+ }
+ if (canAddRead && ImGui::MenuItem("Read")) {
CBreakPoints::AddMemCheck(address, end, MEMCHECK_READ, BREAK_ACTION_PAUSE);
}
- }
- if (!hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE)) {
- if (ImGui::MenuItem("Add memory write breakpoint")) {
+ if (canAddWrite && ImGui::MenuItem("Write")) {
CBreakPoints::AddMemCheck(address, end, MEMCHECK_WRITE, BREAK_ACTION_PAUSE);
}
- }
- if (!hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE_ONCHANGE)) {
- if (ImGui::MenuItem("Add memory write on change breakpoint")) {
+ if (canAddWriteOnChange && ImGui::MenuItem("Write Change")) {
constexpr auto cond = static_cast(MEMCHECK_WRITE | MEMCHECK_WRITE_ONCHANGE);
CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE);
}
+ ImGui::EndMenu();
}
}
diff --git a/UI/ImDebugger/ImStructViewer.h b/UI/ImDebugger/ImStructViewer.h
index 473f4b4f5d..808e92cbd2 100644
--- a/UI/ImDebugger/ImStructViewer.h
+++ b/UI/ImDebugger/ImStructViewer.h
@@ -67,5 +67,6 @@ private:
int length,
const std::string& typePathName,
const char* name,
- int watchIndex);
+ int watchIndex,
+ const u64* value);
};
From 2c49cae1e291806d1cb36b821f91f455637b32ef Mon Sep 17 00:00:00 2001
From: kotcrab <4594081+kotcrab@users.noreply.github.com>
Date: Fri, 15 Nov 2024 20:27:23 +0100
Subject: [PATCH 4/5] Struct viewer, fix build
Fix open check
---
Common/GhidraClient.h | 6 ++++--
UI/ImDebugger/ImDebugger.cpp | 2 +-
UI/ImDebugger/ImStructViewer.cpp | 8 +++++---
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/Common/GhidraClient.h b/Common/GhidraClient.h
index 1884e4a194..2746c99f6e 100644
--- a/Common/GhidraClient.h
+++ b/Common/GhidraClient.h
@@ -1,9 +1,11 @@
#pragma once
-#include
+#include
#include
-#include
+#include
+#include
#include
+#include
struct GhidraSymbol {
u32 address = 0;
diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp
index 15e5c12bf0..c5d606c6ef 100644
--- a/UI/ImDebugger/ImDebugger.cpp
+++ b/UI/ImDebugger/ImDebugger.cpp
@@ -413,7 +413,7 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) {
DrawHLEModules(cfg_);
- if (&cfg_.structViewerOpen) {
+ if (cfg_.structViewerOpen) {
structViewer_.Draw(mipsDebug, &cfg_.structViewerOpen);
}
}
diff --git a/UI/ImDebugger/ImStructViewer.cpp b/UI/ImDebugger/ImStructViewer.cpp
index 84d1701a10..03246831ec 100644
--- a/UI/ImDebugger/ImStructViewer.cpp
+++ b/UI/ImDebugger/ImStructViewer.cpp
@@ -1,5 +1,6 @@
#include
#include
+#include
#include "ext/imgui/imgui.h"
@@ -369,8 +370,9 @@ void ImStructViewer::DrawNewWatchEntry() {
ImGui::InputText("Expression", newWatch_.expression, IM_ARRAYSIZE(newWatch_.expression));
ImGui::SameLine();
ImGui::Checkbox("Dynamic", &newWatch_.dynamic);
- if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal))
+ if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal)) {
ImGui::SetTooltip("When checked the expression will be\nre-evaluated on each frame.");
+ }
ImGui::PopItemWidth();
@@ -399,8 +401,8 @@ void ImStructViewer::DrawNewWatchEntry() {
newWatch_.dynamic = false;
newWatch_.error = "";
newWatch_.typeFilter.Clear();
- // Not clearing the actual selected type on purpose here, user will have to reselect one anyway and maybe
- // there is a chance they will reuse the current one
+ // Not clearing the actual selected type on purpose here, user will have to reselect one anyway and
+ // maybe there is a chance they will reuse the current one
}
}
if (!newWatch_.error.empty()) {
From e3e831851b680c8ddd898391188f7fad3dec52a8 Mon Sep 17 00:00:00 2001
From: kotcrab <4594081+kotcrab@users.noreply.github.com>
Date: Tue, 19 Nov 2024 00:15:14 +0100
Subject: [PATCH 5/5] Add GhidraClient and ImStructViewer docs
Few more code comments and misc clean up
---
Common/GhidraClient.cpp | 3 +--
Common/GhidraClient.h | 34 +++++++++++++++++++++++++++++++-
UI/ImDebugger/ImStructViewer.cpp | 18 +++++++++++------
UI/ImDebugger/ImStructViewer.h | 20 +++++++++++++++----
4 files changed, 62 insertions(+), 13 deletions(-)
diff --git a/Common/GhidraClient.cpp b/Common/GhidraClient.cpp
index e0c5bbe3ca..a7e21c6df3 100644
--- a/Common/GhidraClient.cpp
+++ b/Common/GhidraClient.cpp
@@ -108,7 +108,6 @@ bool GhidraClient::FetchTypes() {
const JsonGet entry = pEntry->value;
GhidraType type;
- type.name = entry.getStringOr("name", "");
type.displayName = entry.getStringOr("displayName", "");
type.pathName = entry.getStringOr("pathName", "");
type.length = entry.getInt("length", 0);
@@ -197,7 +196,7 @@ bool GhidraClient::FetchResource(const std::string& path, std::string& outResult
const int code = http.GET(http::RequestParams(path.c_str()), &result, &progress);
http.Disconnect();
if (code != 200) {
- pendingResult_.error = "unsuccessful response code";
+ pendingResult_.error = "unsuccessful response code from API endpoint";
return false;
}
result.TakeAll(&outResult);
diff --git a/Common/GhidraClient.h b/Common/GhidraClient.h
index 2746c99f6e..334c46fd99 100644
--- a/Common/GhidraClient.h
+++ b/Common/GhidraClient.h
@@ -7,6 +7,10 @@
#include
#include
+/**
+ * Represents symbol from a Ghidra's program.
+ * A symbol can be for example a data label, instruction label or a function.
+ */
struct GhidraSymbol {
u32 address = 0;
std::string name;
@@ -15,6 +19,7 @@ struct GhidraSymbol {
std::string dataTypePathName;
};
+/** Possible kinds of data types, such as enum, structures or built-ins (Ghidra's primitive types). */
enum GhidraTypeKind {
ENUM,
TYPEDEF,
@@ -27,12 +32,14 @@ enum GhidraTypeKind {
UNKNOWN,
};
+/** Describes single member of an enum type. */
struct GhidraEnumMember {
std::string name;
u64 value = 0;
std::string comment;
};
+/** Describes single member of a composite (structure or union) type. */
struct GhidraCompositeMember {
std::string fieldName;
u32 ordinal = 0;
@@ -42,9 +49,13 @@ struct GhidraCompositeMember {
std::string comment;
};
+/**
+ * Describes data type from Ghidra. Note that some fields of this structure will only be populated depending on the
+ * type's kind. Each type has a display name that is suitable for displaying to the user and a path name that
+ * unambiguously identifies this type.
+ */
struct GhidraType {
GhidraTypeKind kind;
- std::string name;
std::string displayName;
std::string pathName;
int length = 0;
@@ -64,6 +75,23 @@ struct GhidraType {
std::string builtInGroup;
};
+/**
+ * GhidraClient implements fetching data (such as symbols or types) from a remote Ghidra project.
+ *
+ * This client uses unofficial API provided by the third party "ghidra-rest-api" extension. The extension is
+ * available at https://github.com/kotcrab/ghidra-rest-api.
+ *
+ * This class doesn't fetch data from every possible endpoint, only those that are actually used by PPSSPP.
+ *
+ * How to use:
+ * 1. The client is created in the Idle status.
+ * 2. To start fetching data call the FetchAll() method. The client goes to Pending status and the data is fetched
+ * in a background thread so your code remains unblocked.
+ * 3. Periodically check with the Ready() method is the operation has completed. (i.e. check if the client
+ is in the Ready status)
+ * 4. If the client is ready call UpdateResult() to update result field with new data.
+ * 5. The client is now back to Idle status, and you can call FetchAll() again later if needed.
+ */
class GhidraClient {
enum class Status {
Idle,
@@ -78,12 +106,16 @@ public:
std::string error;
};
+ /** Current result of the client. Your thread is safe to access this regardless of client status. */
Result result;
~GhidraClient();
+ /** If client is idle then asynchronously starts fetching data from Ghidra. */
void FetchAll(const std::string& host, int port);
+ /** If client is ready then updates the result field with newly fetched data. This must be called from the thread
+ * using the result. */
void UpdateResult();
bool Idle() const { return status_ == Status::Idle; }
diff --git a/UI/ImDebugger/ImStructViewer.cpp b/UI/ImDebugger/ImStructViewer.cpp
index 03246831ec..d277af8154 100644
--- a/UI/ImDebugger/ImStructViewer.cpp
+++ b/UI/ImDebugger/ImStructViewer.cpp
@@ -216,9 +216,15 @@ void ImStructViewer::Draw(MIPSDebugInterface* mipsDebug, bool* open) {
}
void ImStructViewer::DrawConnectionSetup() {
- ImGui::TextWrapped("Struct viewer visualizes data in memory using types from your Ghidra project.");
- ImGui::TextWrapped("To get started install the ghidra-rest-api plugin and start the Rest API server.");
- ImGui::TextWrapped("When ready press the connect button below.");
+ ImGui::TextWrapped(R"(Struct viewer visualizes data in game memory using types from your Ghidra project.
+It also allows to set memory breakpoints and edit field values which is helpful when reverse engineering unknown types.
+To get started:
+ 1. In Ghidra install the ghidra-rest-api extension by Kotcrab.
+ 2. After installing the extension enable the RestApiPlugin in the Miscellaneous plugins configuration window.
+ 3. Open your Ghidra project and click "Start Rest API Server" in the "Tools" menu bar.
+ 4. Press the connect button below.
+)");
+ ImGui::Dummy(ImVec2(1, 6));
ImGui::BeginDisabled(!ghidraClient_.Idle());
ImGui::PushItemWidth(120);
@@ -552,7 +558,7 @@ void ImStructViewer::DrawType(
const u32 address = base + offset;
ImGui::PushID(static_cast(address));
- ImGui::PushID(watchIndex);
+ ImGui::PushID(watchIndex); // We push watch index too as it's possible to have multiple watches on the same address
// Text and Tree nodes are less high than framed widgets, using AlignTextToFramePadding() we add vertical spacing
// to make the tree lines equal high.
@@ -711,7 +717,7 @@ void ImStructViewer::DrawType(
break;
}
default: {
- // At this point there is most likely some issue in the Ghidra plugin and the type wasn't
+ // At this point there is most likely some issue in the Ghidra extension and the type wasn't
// classified to any category
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
@@ -726,7 +732,7 @@ void ImStructViewer::DrawType(
ImGui::PopID();
}
-static void CopyHexNumberToClipboard(u64 value) {
+static void CopyHexNumberToClipboard(const u64 value) {
std::stringstream ss;
ss << std::hex << value;
const std::string valueString = ss.str();
diff --git a/UI/ImDebugger/ImStructViewer.h b/UI/ImDebugger/ImStructViewer.h
index 808e92cbd2..289025b64a 100644
--- a/UI/ImDebugger/ImStructViewer.h
+++ b/UI/ImDebugger/ImStructViewer.h
@@ -5,6 +5,18 @@
#include "Common/GhidraClient.h"
#include "Core/MIPS/MIPSDebugInterface.h"
+/**
+ * Struct viewer visualizes objects data in game memory using types and symbols fetched from a Ghidra project.
+ * It also allows to set memory breakpoints and edit field values which is helpful when reverse engineering unknown
+ * types.
+ *
+ * To use this you will need to install an unofficial Ghidra extension "ghidra-rest-api" by Kotcrab.
+ * (available at https://github.com/kotcrab/ghidra-rest-api). After installing the extension and starting the API
+ * server in Ghidra you can open the Struct viewer window and press the "Connect" button to start using it.
+ *
+ * See the original pull request https://github.com/hrydgard/ppsspp/pull/19629 for a screenshot and how to test this
+ * without the need to set up Ghidra.
+ */
class ImStructViewer {
struct Watch {
std::string expression;
@@ -35,12 +47,12 @@ private:
GhidraClient ghidraClient_;
char ghidraHost_[128] = "localhost";
int ghidraPort_ = 18489;
- bool fetchedAtLeastOnce_ = false;
+ bool fetchedAtLeastOnce_ = false; // True if fetched from Ghidra successfully at least once
std::vector watches_;
- int removeWatchIndex_ = -1;
- Watch addWatch_;
- NewWatch newWatch_;
+ int removeWatchIndex_ = -1; // Watch index entry to be removed on next draw
+ Watch addWatch_; // Temporary variable to store watch entry added from the Globals tab
+ NewWatch newWatch_; // State for the new watch entry UI
void DrawConnectionSetup();