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();