Merge pull request #19629 from kotcrab/struct-viewer
Some checks are pending
Build / build-windows (ARM64) (push) Waiting to run
Build / build-windows (x64) (push) Waiting to run
Build / build-uwp (push) Waiting to run
Build / test-windows (push) Blocked by required conditions
Build / build (./b.sh --headless --unittest --fat --no-png --no-sdl2, clang, clang++, test, macos, macos-latest) (push) Waiting to run
Build / build (./b.sh --headless --unittest, clang, clang++, test, clang-normal, ubuntu-latest) (push) Waiting to run
Build / build (./b.sh --headless --unittest, gcc, g++, gcc-normal, ubuntu-latest) (push) Waiting to run
Build / build (./b.sh --ios, clang, clang++, ios, ios, macos-latest) (push) Waiting to run
Build / build (./b.sh --libretro_android ppsspp_libretro, clang, clang++, android, android-libretro, ubuntu-latest) (push) Waiting to run
Build / build (./b.sh --qt, gcc, g++, qt, qt, ubuntu-latest) (push) Waiting to run
Build / build (cd android && ./ab.sh -j2 APP_ABI=arm64-v8a OPENXR=1, clang, clang++, android, android-vr, ubuntu-latest) (push) Waiting to run
Build / build (cd android && ./ab.sh -j2 APP_ABI=arm64-v8a UNITTEST=1 HEADLESS=1, clang, clang++, android, android-arm64, ubuntu-latest) (push) Waiting to run
Build / build (cd android && ./ab.sh -j2 APP_ABI=armeabi-v7a UNITTEST=1 HEADLESS=1, clang, clang++, android, android-arm32, ubuntu-latest) (push) Waiting to run
Build / build (cd android && ./ab.sh -j2 APP_ABI=x86_64 UNITTEST=1 HEADLESS=1, clang, clang++, android, android-x86_64, ubuntu-latest) (push) Waiting to run
Build / build (make -C libretro -f Makefile -j2, clang, clang++, libretro, clang-libretro, ubuntu-latest) (push) Waiting to run
Build / build (make -C libretro -f Makefile -j2, gcc, g++, libretro, gcc-libretro, ubuntu-latest) (push) Waiting to run
Build / test (macos-latest) (push) Blocked by required conditions
Build / test (ubuntu-latest) (push) Blocked by required conditions
Build / build_test_headless_alpine (push) Waiting to run
Generate Docker Layer / build (push) Waiting to run

Struct viewer debugging tool
This commit is contained in:
Henrik Rydgård 2024-11-19 11:10:47 +01:00 committed by GitHub
commit 1c0f9d3d4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1282 additions and 0 deletions

View File

@ -884,6 +884,8 @@ add_library(Common STATIC
Common/FakeCPUDetect.cpp Common/FakeCPUDetect.cpp
Common/ExceptionHandlerSetup.cpp Common/ExceptionHandlerSetup.cpp
Common/ExceptionHandlerSetup.h Common/ExceptionHandlerSetup.h
Common/GhidraClient.h
Common/GhidraClient.cpp
Common/Log.h Common/Log.h
Common/Log.cpp Common/Log.cpp
Common/Log/ConsoleListener.cpp Common/Log/ConsoleListener.cpp
@ -1523,6 +1525,8 @@ list(APPEND NativeAppSource
UI/ImDebugger/ImDebugger.h UI/ImDebugger/ImDebugger.h
UI/ImDebugger/ImDisasmView.cpp UI/ImDebugger/ImDisasmView.cpp
UI/ImDebugger/ImDisasmView.h UI/ImDebugger/ImDisasmView.h
UI/ImDebugger/ImStructViewer.cpp
UI/ImDebugger/ImStructViewer.h
UI/DiscordIntegration.cpp UI/DiscordIntegration.cpp
UI/NativeApp.cpp UI/NativeApp.cpp
UI/BackgroundAudio.h UI/BackgroundAudio.h

View File

@ -591,6 +591,7 @@
<ClInclude Include="Crypto\sha256.h" /> <ClInclude Include="Crypto\sha256.h" />
<ClInclude Include="DbgNew.h" /> <ClInclude Include="DbgNew.h" />
<ClInclude Include="ExceptionHandlerSetup.h" /> <ClInclude Include="ExceptionHandlerSetup.h" />
<ClInclude Include="GhidraClient.h" />
<ClInclude Include="GraphicsContext.h" /> <ClInclude Include="GraphicsContext.h" />
<ClInclude Include="Log.h" /> <ClInclude Include="Log.h" />
<ClInclude Include="Log\LogManager.h" /> <ClInclude Include="Log\LogManager.h" />
@ -1021,6 +1022,7 @@
<ClCompile Include="GPU\Vulkan\VulkanRenderManager.cpp" /> <ClCompile Include="GPU\Vulkan\VulkanRenderManager.cpp" />
<ClCompile Include="Input\GestureDetector.cpp" /> <ClCompile Include="Input\GestureDetector.cpp" />
<ClCompile Include="Input\InputState.cpp" /> <ClCompile Include="Input\InputState.cpp" />
<ClCompile Include="GhidraClient.cpp" />
<ClCompile Include="Log.cpp" /> <ClCompile Include="Log.cpp" />
<ClCompile Include="Math\curves.cpp" /> <ClCompile Include="Math\curves.cpp" />
<ClCompile Include="Math\expression_parser.cpp" /> <ClCompile Include="Math\expression_parser.cpp" />

View File

@ -6,6 +6,7 @@
<ClInclude Include="CommonFuncs.h" /> <ClInclude Include="CommonFuncs.h" />
<ClInclude Include="CommonTypes.h" /> <ClInclude Include="CommonTypes.h" />
<ClInclude Include="CPUDetect.h" /> <ClInclude Include="CPUDetect.h" />
<ClInclude Include="GhidraClient.h" />
<ClInclude Include="Log.h" /> <ClInclude Include="Log.h" />
<ClInclude Include="MemArena.h" /> <ClInclude Include="MemArena.h" />
<ClInclude Include="MemoryUtil.h" /> <ClInclude Include="MemoryUtil.h" />
@ -705,6 +706,7 @@
<Filter>Serialize</Filter> <Filter>Serialize</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="TimeUtil.cpp" /> <ClCompile Include="TimeUtil.cpp" />
<ClCompile Include="GhidraClient.cpp" />
<ClCompile Include="Log.cpp" /> <ClCompile Include="Log.cpp" />
<ClCompile Include="SysError.cpp" /> <ClCompile Include="SysError.cpp" />
<ClCompile Include="..\ext\libpng17\png.c"> <ClCompile Include="..\ext\libpng17\png.c">

204
Common/GhidraClient.cpp Normal file
View File

@ -0,0 +1,204 @@
#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<std::mutex> 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<std::mutex> lock(mutex_);
host_ = host;
port_ = port;
const bool result = FetchTypes() && FetchSymbols();
status_ = Status::Ready;
return result;
}
void GhidraClient::UpdateResult() {
std::lock_guard<std::mutex> 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.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 from API endpoint";
return false;
}
result.TakeAll(&outResult);
return true;
}

142
Common/GhidraClient.h Normal file
View File

@ -0,0 +1,142 @@
#pragma once
#include <atomic>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
/**
* 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;
bool label;
bool userDefined;
std::string dataTypePathName;
};
/** Possible kinds of data types, such as enum, structures or built-ins (Ghidra's primitive types). */
enum GhidraTypeKind {
ENUM,
TYPEDEF,
POINTER,
ARRAY,
STRUCTURE,
UNION,
FUNCTION_DEFINITION,
BUILT_IN,
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;
u32 offset = 0;
int length = 0;
std::string typePathName;
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 displayName;
std::string pathName;
int length = 0;
int alignedLength = 0;
bool zeroLength = false;
std::string description;
std::vector<GhidraCompositeMember> compositeMembers;
std::vector<GhidraEnumMember> 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;
};
/**
* 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,
Pending,
Ready,
};
public:
struct Result {
std::vector<GhidraSymbol> symbols;
std::unordered_map<std::string, GhidraType> types;
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; }
bool Ready() const { return status_ == Status::Ready; }
bool Failed() const { return !result.error.empty(); }
private:
std::thread thread_;
std::mutex mutex_;
std::atomic<Status> 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);
};

View File

@ -377,6 +377,7 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) {
ImGui::Checkbox("HLE Modules", &cfg_.modulesOpen); ImGui::Checkbox("HLE Modules", &cfg_.modulesOpen);
ImGui::Checkbox("HLE Threads", &cfg_.threadsOpen); ImGui::Checkbox("HLE Threads", &cfg_.threadsOpen);
ImGui::Checkbox("sceAtrac", &cfg_.atracOpen); ImGui::Checkbox("sceAtrac", &cfg_.atracOpen);
ImGui::Checkbox("Struct viewer", &cfg_.structViewerOpen);
ImGui::EndMenu(); ImGui::EndMenu();
} }
ImGui::EndMainMenuBar(); ImGui::EndMainMenuBar();
@ -411,6 +412,10 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) {
} }
DrawHLEModules(cfg_); DrawHLEModules(cfg_);
if (cfg_.structViewerOpen) {
structViewer_.Draw(mipsDebug, &cfg_.structViewerOpen);
}
} }
void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, bool *open, CoreState coreState) { void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, bool *open, CoreState coreState) {

View File

@ -14,6 +14,7 @@
#include "Core/Debugger/DebugInterface.h" #include "Core/Debugger/DebugInterface.h"
#include "UI/ImDebugger/ImDisasmView.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. // 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 modulesOpen = true;
bool hleModulesOpen = false; bool hleModulesOpen = false;
bool atracOpen = true; bool atracOpen = true;
bool structViewerOpen = false;
// HLE explorer settings // HLE explorer settings
// bool filterByUsed = true; // bool filterByUsed = true;
@ -71,6 +73,7 @@ struct ImDebugger {
ImDisasmWindow disasm_; ImDisasmWindow disasm_;
ImLuaConsole luaConsole_; ImLuaConsole luaConsole_;
ImStructViewer structViewer_;
// Open variables. // Open variables.
ImConfig cfg_; ImConfig cfg_;

View File

@ -0,0 +1,813 @@
#include <regex>
#include <sstream>
#include <unordered_map>
#include "ext/imgui/imgui.h"
#include "Common/System/Request.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<std::string, BuiltIn> 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 <invalid string @ %x>", Memory::Read_U8(address), address);
}
break;
case BuiltInType::Float:
ImGui::Text("= %f", Memory::Read_Float(address));
break;
case BuiltInType::Void:
ImGui::Text("<void type>");
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(750, 550), 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(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);
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)
&& 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 = "<watch>";
}
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<std::string, GhidraType>& 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("* <invalid pointer: %x>", value);
}
ImGui::PopStyleColor();
}
static void DrawPointerContent(
const std::unordered_map<std::string, GhidraType>& 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<GhidraEnumMember>& 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<int>(address));
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.
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 |
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, nullptr);
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
DrawTypeColumn("<missing type: %s>", 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, nullptr);
DrawTypeColumn("%s", typeDisplayName, base, offset);
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY);
ImGui::Text("<invalid address: %x>", 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);
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 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);
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);
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, nullptr);
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, nullptr);
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, nullptr);
DrawTypeColumn("%s", typeDisplayName, base, offset);
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
ImGui::Text("<function definition>"); // TODO could be go to in disassembler here
break;
case BUILT_IN: {
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
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);
} else {
// Some built in types are rather obscure so we don't handle every possible one
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
DrawTypeColumn("<unsupported built in: %s>", typePathName, base, offset);
ImGui::PopStyleColor();
}
break;
}
default: {
// 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);
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
DrawTypeColumn("<not implemented type: %s>", typeDisplayName, base, offset);
ImGui::PopStyleColor();
break;
}
}
ImGui::PopID();
ImGui::PopID();
}
static void CopyHexNumberToClipboard(const 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 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")) {
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);
}
}
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<MemCheckCondition>(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 (canAddWrite && ImGui::MenuItem("Write")) {
CBreakPoints::AddMemCheck(address, end, MEMCHECK_WRITE, BREAK_ACTION_PAUSE);
}
if (canAddWriteOnChange && ImGui::MenuItem("Write Change")) {
constexpr auto cond = static_cast<MemCheckCondition>(MEMCHECK_WRITE | MEMCHECK_WRITE_ONCHANGE);
CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE);
}
ImGui::EndMenu();
}
}
ImGui::EndPopup();
}
}

View File

@ -0,0 +1,84 @@
#pragma once
#include "ext/imgui/imgui.h"
#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;
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; // True if fetched from Ghidra successfully at least once
std::vector<Watch> watches_;
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();
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,
const u64* value);
};

View File

@ -53,6 +53,7 @@
<ClCompile Include="GPUDriverTestScreen.cpp" /> <ClCompile Include="GPUDriverTestScreen.cpp" />
<ClCompile Include="ImDebugger\ImDebugger.cpp" /> <ClCompile Include="ImDebugger\ImDebugger.cpp" />
<ClCompile Include="ImDebugger\ImDisasmView.cpp" /> <ClCompile Include="ImDebugger\ImDisasmView.cpp" />
<ClCompile Include="ImDebugger\ImStructViewer.cpp" />
<ClCompile Include="JitCompareScreen.cpp" /> <ClCompile Include="JitCompareScreen.cpp" />
<ClCompile Include="JoystickHistoryView.cpp" /> <ClCompile Include="JoystickHistoryView.cpp" />
<ClCompile Include="MainScreen.cpp" /> <ClCompile Include="MainScreen.cpp" />
@ -95,6 +96,7 @@
<ClInclude Include="GPUDriverTestScreen.h" /> <ClInclude Include="GPUDriverTestScreen.h" />
<ClInclude Include="ImDebugger\ImDebugger.h" /> <ClInclude Include="ImDebugger\ImDebugger.h" />
<ClInclude Include="ImDebugger\ImDisasmView.h" /> <ClInclude Include="ImDebugger\ImDisasmView.h" />
<ClInclude Include="ImDebugger\ImStructViewer.h" />
<ClInclude Include="JitCompareScreen.h" /> <ClInclude Include="JitCompareScreen.h" />
<ClInclude Include="JoystickHistoryView.h" /> <ClInclude Include="JoystickHistoryView.h" />
<ClInclude Include="MainScreen.h" /> <ClInclude Include="MainScreen.h" />

View File

@ -104,6 +104,9 @@
<ClCompile Include="ImDebugger\ImDisasmView.cpp"> <ClCompile Include="ImDebugger\ImDisasmView.cpp">
<Filter>ImDebugger</Filter> <Filter>ImDebugger</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="ImDebugger\ImStructViewer.cpp">
<Filter>ImDebugger</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="GameInfoCache.h" /> <ClInclude Include="GameInfoCache.h" />
@ -208,6 +211,9 @@
<ClInclude Include="ImDebugger\ImDisasmView.h"> <ClInclude Include="ImDebugger\ImDisasmView.h">
<Filter>ImDebugger</Filter> <Filter>ImDebugger</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="ImDebugger\ImStructViewer.h">
<Filter>ImDebugger</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Filter Include="Screens"> <Filter Include="Screens">

View File

@ -197,6 +197,7 @@
<ClInclude Include="..\..\Common\Crypto\sha256.h" /> <ClInclude Include="..\..\Common\Crypto\sha256.h" />
<ClInclude Include="..\..\Common\DbgNew.h" /> <ClInclude Include="..\..\Common\DbgNew.h" />
<ClInclude Include="..\..\Common\ExceptionHandlerSetup.h" /> <ClInclude Include="..\..\Common\ExceptionHandlerSetup.h" />
<ClInclude Include="..\..\Common\GhidraClient.h" />
<ClInclude Include="..\..\Common\GraphicsContext.h" /> <ClInclude Include="..\..\Common\GraphicsContext.h" />
<ClInclude Include="..\..\Common\Log.h" /> <ClInclude Include="..\..\Common\Log.h" />
<ClInclude Include="..\..\Common\Log\LogManager.h" /> <ClInclude Include="..\..\Common\Log\LogManager.h" />
@ -360,6 +361,7 @@
<ClCompile Include="..\..\Common\Crypto\sha1.cpp" /> <ClCompile Include="..\..\Common\Crypto\sha1.cpp" />
<ClCompile Include="..\..\Common\Crypto\sha256.cpp" /> <ClCompile Include="..\..\Common\Crypto\sha256.cpp" />
<ClCompile Include="..\..\Common\ExceptionHandlerSetup.cpp" /> <ClCompile Include="..\..\Common\ExceptionHandlerSetup.cpp" />
<ClCompile Include="..\..\Common\GhidraClient.cpp" />
<ClCompile Include="..\..\Common\Log.cpp" /> <ClCompile Include="..\..\Common\Log.cpp" />
<ClCompile Include="..\..\Common\Log\LogManager.cpp" /> <ClCompile Include="..\..\Common\Log\LogManager.cpp" />
<ClCompile Include="..\..\Common\LogReporting.cpp" /> <ClCompile Include="..\..\Common\LogReporting.cpp" />

View File

@ -119,6 +119,7 @@
<ClCompile Include="..\..\Common\CPUDetect.cpp" /> <ClCompile Include="..\..\Common\CPUDetect.cpp" />
<ClCompile Include="..\..\Common\FakeCPUDetect.cpp" /> <ClCompile Include="..\..\Common\FakeCPUDetect.cpp" />
<ClCompile Include="..\..\Common\ExceptionHandlerSetup.cpp" /> <ClCompile Include="..\..\Common\ExceptionHandlerSetup.cpp" />
<ClCompile Include="..\..\Common\GhidraClient.cpp" />
<ClCompile Include="..\..\Common\Log.cpp" /> <ClCompile Include="..\..\Common\Log.cpp" />
<ClCompile Include="..\..\Common\Log\LogManager.cpp" /> <ClCompile Include="..\..\Common\Log\LogManager.cpp" />
<ClCompile Include="..\..\Common\LogReporting.cpp" /> <ClCompile Include="..\..\Common\LogReporting.cpp" />
@ -538,6 +539,7 @@
<ClInclude Include="..\..\Common\CPUDetect.h" /> <ClInclude Include="..\..\Common\CPUDetect.h" />
<ClInclude Include="..\..\Common\DbgNew.h" /> <ClInclude Include="..\..\Common\DbgNew.h" />
<ClInclude Include="..\..\Common\ExceptionHandlerSetup.h" /> <ClInclude Include="..\..\Common\ExceptionHandlerSetup.h" />
<ClInclude Include="..\..\Common\GhidraClient.h" />
<ClInclude Include="..\..\Common\GraphicsContext.h" /> <ClInclude Include="..\..\Common\GraphicsContext.h" />
<ClInclude Include="..\..\Common\Log.h" /> <ClInclude Include="..\..\Common\Log.h" />
<ClInclude Include="..\..\Common\Log\LogManager.h" /> <ClInclude Include="..\..\Common\Log\LogManager.h" />

View File

@ -128,6 +128,7 @@
<ClInclude Include="..\..\UI\GPUDriverTestScreen.h" /> <ClInclude Include="..\..\UI\GPUDriverTestScreen.h" />
<ClInclude Include="..\..\UI\ImDebugger\ImDebugger.h" /> <ClInclude Include="..\..\UI\ImDebugger\ImDebugger.h" />
<ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h" /> <ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h" />
<ClInclude Include="..\..\UI\ImDebugger\ImStructViewer.h" />
<ClInclude Include="..\..\UI\InstallZipScreen.h" /> <ClInclude Include="..\..\UI\InstallZipScreen.h" />
<ClInclude Include="..\..\UI\JitCompareScreen.h" /> <ClInclude Include="..\..\UI\JitCompareScreen.h" />
<ClInclude Include="..\..\UI\JoystickHistoryView.h" /> <ClInclude Include="..\..\UI\JoystickHistoryView.h" />
@ -170,6 +171,7 @@
<ClCompile Include="..\..\UI\GPUDriverTestScreen.cpp" /> <ClCompile Include="..\..\UI\GPUDriverTestScreen.cpp" />
<ClCompile Include="..\..\UI\ImDebugger\ImDebugger.cpp" /> <ClCompile Include="..\..\UI\ImDebugger\ImDebugger.cpp" />
<ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp" /> <ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp" />
<ClCompile Include="..\..\UI\ImDebugger\ImStructViewer.cpp" />
<ClCompile Include="..\..\UI\InstallZipScreen.cpp" /> <ClCompile Include="..\..\UI\InstallZipScreen.cpp" />
<ClCompile Include="..\..\UI\JitCompareScreen.cpp" /> <ClCompile Include="..\..\UI\JitCompareScreen.cpp" />
<ClCompile Include="..\..\UI\JoystickHistoryView.cpp" /> <ClCompile Include="..\..\UI\JoystickHistoryView.cpp" />

View File

@ -45,6 +45,9 @@
<ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp"> <ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp">
<Filter>ImDebugger</Filter> <Filter>ImDebugger</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\UI\ImDebugger\ImStructViewer.cpp">
<Filter>ImDebugger</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
@ -91,6 +94,9 @@
<ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h"> <ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h">
<Filter>ImDebugger</Filter> <Filter>ImDebugger</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\..\UI\ImDebugger\ImStructViewer.h">
<Filter>ImDebugger</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Filter Include="ImDebugger"> <Filter Include="ImDebugger">

View File

@ -373,6 +373,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Common/CPUDetect.cpp \ $(SRC)/Common/CPUDetect.cpp \
$(SRC)/Common/ExceptionHandlerSetup.cpp \ $(SRC)/Common/ExceptionHandlerSetup.cpp \
$(SRC)/Common/FakeCPUDetect.cpp \ $(SRC)/Common/FakeCPUDetect.cpp \
$(SRC)/Common/GhidraClient.cpp \
$(SRC)/Common/Log.cpp \ $(SRC)/Common/Log.cpp \
$(SRC)/Common/Log/LogManager.cpp \ $(SRC)/Common/Log/LogManager.cpp \
$(SRC)/Common/LogReporting.cpp \ $(SRC)/Common/LogReporting.cpp \
@ -876,6 +877,7 @@ LOCAL_SRC_FILES := \
$(SRC)/android/jni/OpenSLContext.cpp \ $(SRC)/android/jni/OpenSLContext.cpp \
$(SRC)/UI/ImDebugger/ImDebugger.cpp \ $(SRC)/UI/ImDebugger/ImDebugger.cpp \
$(SRC)/UI/ImDebugger/ImDisasmView.cpp \ $(SRC)/UI/ImDebugger/ImDisasmView.cpp \
$(SRC)/UI/ImDebugger/ImStructViewer.cpp \
$(SRC)/UI/AudioCommon.cpp \ $(SRC)/UI/AudioCommon.cpp \
$(SRC)/UI/BackgroundAudio.cpp \ $(SRC)/UI/BackgroundAudio.cpp \
$(SRC)/UI/DiscordIntegration.cpp \ $(SRC)/UI/DiscordIntegration.cpp \

View File

@ -483,6 +483,7 @@ SOURCES_CXX += \
$(COMMONDIR)/Log/StdioListener.cpp \ $(COMMONDIR)/Log/StdioListener.cpp \
$(COMMONDIR)/ExceptionHandlerSetup.cpp \ $(COMMONDIR)/ExceptionHandlerSetup.cpp \
$(COMMONDIR)/FakeCPUDetect.cpp \ $(COMMONDIR)/FakeCPUDetect.cpp \
$(COMMONDIR)/GhidraClient.cpp \
$(COMMONDIR)/Log.cpp \ $(COMMONDIR)/Log.cpp \
$(COMMONDIR)/Log/LogManager.cpp \ $(COMMONDIR)/Log/LogManager.cpp \
$(COMMONDIR)/OSVersion.cpp \ $(COMMONDIR)/OSVersion.cpp \