mirror of
https://github.com/hrydgard/ppsspp.git
synced 2024-11-22 21:09:52 +00:00
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
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:
commit
1c0f9d3d4f
@ -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
|
||||
@ -1523,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
|
||||
|
@ -591,6 +591,7 @@
|
||||
<ClInclude Include="Crypto\sha256.h" />
|
||||
<ClInclude Include="DbgNew.h" />
|
||||
<ClInclude Include="ExceptionHandlerSetup.h" />
|
||||
<ClInclude Include="GhidraClient.h" />
|
||||
<ClInclude Include="GraphicsContext.h" />
|
||||
<ClInclude Include="Log.h" />
|
||||
<ClInclude Include="Log\LogManager.h" />
|
||||
@ -1021,6 +1022,7 @@
|
||||
<ClCompile Include="GPU\Vulkan\VulkanRenderManager.cpp" />
|
||||
<ClCompile Include="Input\GestureDetector.cpp" />
|
||||
<ClCompile Include="Input\InputState.cpp" />
|
||||
<ClCompile Include="GhidraClient.cpp" />
|
||||
<ClCompile Include="Log.cpp" />
|
||||
<ClCompile Include="Math\curves.cpp" />
|
||||
<ClCompile Include="Math\expression_parser.cpp" />
|
||||
|
@ -6,6 +6,7 @@
|
||||
<ClInclude Include="CommonFuncs.h" />
|
||||
<ClInclude Include="CommonTypes.h" />
|
||||
<ClInclude Include="CPUDetect.h" />
|
||||
<ClInclude Include="GhidraClient.h" />
|
||||
<ClInclude Include="Log.h" />
|
||||
<ClInclude Include="MemArena.h" />
|
||||
<ClInclude Include="MemoryUtil.h" />
|
||||
@ -705,6 +706,7 @@
|
||||
<Filter>Serialize</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="TimeUtil.cpp" />
|
||||
<ClCompile Include="GhidraClient.cpp" />
|
||||
<ClCompile Include="Log.cpp" />
|
||||
<ClCompile Include="SysError.cpp" />
|
||||
<ClCompile Include="..\ext\libpng17\png.c">
|
||||
|
204
Common/GhidraClient.cpp
Normal file
204
Common/GhidraClient.cpp
Normal 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
142
Common/GhidraClient.h
Normal 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);
|
||||
};
|
@ -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) {
|
||||
|
@ -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_;
|
||||
|
813
UI/ImDebugger/ImStructViewer.cpp
Normal file
813
UI/ImDebugger/ImStructViewer.cpp
Normal 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();
|
||||
}
|
||||
}
|
84
UI/ImDebugger/ImStructViewer.h
Normal file
84
UI/ImDebugger/ImStructViewer.h
Normal 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);
|
||||
};
|
@ -53,6 +53,7 @@
|
||||
<ClCompile Include="GPUDriverTestScreen.cpp" />
|
||||
<ClCompile Include="ImDebugger\ImDebugger.cpp" />
|
||||
<ClCompile Include="ImDebugger\ImDisasmView.cpp" />
|
||||
<ClCompile Include="ImDebugger\ImStructViewer.cpp" />
|
||||
<ClCompile Include="JitCompareScreen.cpp" />
|
||||
<ClCompile Include="JoystickHistoryView.cpp" />
|
||||
<ClCompile Include="MainScreen.cpp" />
|
||||
@ -95,6 +96,7 @@
|
||||
<ClInclude Include="GPUDriverTestScreen.h" />
|
||||
<ClInclude Include="ImDebugger\ImDebugger.h" />
|
||||
<ClInclude Include="ImDebugger\ImDisasmView.h" />
|
||||
<ClInclude Include="ImDebugger\ImStructViewer.h" />
|
||||
<ClInclude Include="JitCompareScreen.h" />
|
||||
<ClInclude Include="JoystickHistoryView.h" />
|
||||
<ClInclude Include="MainScreen.h" />
|
||||
|
@ -104,6 +104,9 @@
|
||||
<ClCompile Include="ImDebugger\ImDisasmView.cpp">
|
||||
<Filter>ImDebugger</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ImDebugger\ImStructViewer.cpp">
|
||||
<Filter>ImDebugger</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="GameInfoCache.h" />
|
||||
@ -208,6 +211,9 @@
|
||||
<ClInclude Include="ImDebugger\ImDisasmView.h">
|
||||
<Filter>ImDebugger</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ImDebugger\ImStructViewer.h">
|
||||
<Filter>ImDebugger</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="Screens">
|
||||
|
@ -197,6 +197,7 @@
|
||||
<ClInclude Include="..\..\Common\Crypto\sha256.h" />
|
||||
<ClInclude Include="..\..\Common\DbgNew.h" />
|
||||
<ClInclude Include="..\..\Common\ExceptionHandlerSetup.h" />
|
||||
<ClInclude Include="..\..\Common\GhidraClient.h" />
|
||||
<ClInclude Include="..\..\Common\GraphicsContext.h" />
|
||||
<ClInclude Include="..\..\Common\Log.h" />
|
||||
<ClInclude Include="..\..\Common\Log\LogManager.h" />
|
||||
@ -360,6 +361,7 @@
|
||||
<ClCompile Include="..\..\Common\Crypto\sha1.cpp" />
|
||||
<ClCompile Include="..\..\Common\Crypto\sha256.cpp" />
|
||||
<ClCompile Include="..\..\Common\ExceptionHandlerSetup.cpp" />
|
||||
<ClCompile Include="..\..\Common\GhidraClient.cpp" />
|
||||
<ClCompile Include="..\..\Common\Log.cpp" />
|
||||
<ClCompile Include="..\..\Common\Log\LogManager.cpp" />
|
||||
<ClCompile Include="..\..\Common\LogReporting.cpp" />
|
||||
|
@ -119,6 +119,7 @@
|
||||
<ClCompile Include="..\..\Common\CPUDetect.cpp" />
|
||||
<ClCompile Include="..\..\Common\FakeCPUDetect.cpp" />
|
||||
<ClCompile Include="..\..\Common\ExceptionHandlerSetup.cpp" />
|
||||
<ClCompile Include="..\..\Common\GhidraClient.cpp" />
|
||||
<ClCompile Include="..\..\Common\Log.cpp" />
|
||||
<ClCompile Include="..\..\Common\Log\LogManager.cpp" />
|
||||
<ClCompile Include="..\..\Common\LogReporting.cpp" />
|
||||
@ -538,6 +539,7 @@
|
||||
<ClInclude Include="..\..\Common\CPUDetect.h" />
|
||||
<ClInclude Include="..\..\Common\DbgNew.h" />
|
||||
<ClInclude Include="..\..\Common\ExceptionHandlerSetup.h" />
|
||||
<ClInclude Include="..\..\Common\GhidraClient.h" />
|
||||
<ClInclude Include="..\..\Common\GraphicsContext.h" />
|
||||
<ClInclude Include="..\..\Common\Log.h" />
|
||||
<ClInclude Include="..\..\Common\Log\LogManager.h" />
|
||||
|
@ -128,6 +128,7 @@
|
||||
<ClInclude Include="..\..\UI\GPUDriverTestScreen.h" />
|
||||
<ClInclude Include="..\..\UI\ImDebugger\ImDebugger.h" />
|
||||
<ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h" />
|
||||
<ClInclude Include="..\..\UI\ImDebugger\ImStructViewer.h" />
|
||||
<ClInclude Include="..\..\UI\InstallZipScreen.h" />
|
||||
<ClInclude Include="..\..\UI\JitCompareScreen.h" />
|
||||
<ClInclude Include="..\..\UI\JoystickHistoryView.h" />
|
||||
@ -170,6 +171,7 @@
|
||||
<ClCompile Include="..\..\UI\GPUDriverTestScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\ImDebugger\ImDebugger.cpp" />
|
||||
<ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp" />
|
||||
<ClCompile Include="..\..\UI\ImDebugger\ImStructViewer.cpp" />
|
||||
<ClCompile Include="..\..\UI\InstallZipScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\JitCompareScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\JoystickHistoryView.cpp" />
|
||||
|
@ -45,6 +45,9 @@
|
||||
<ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp">
|
||||
<Filter>ImDebugger</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\UI\ImDebugger\ImStructViewer.cpp">
|
||||
<Filter>ImDebugger</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
@ -91,6 +94,9 @@
|
||||
<ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h">
|
||||
<Filter>ImDebugger</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\UI\ImDebugger\ImStructViewer.h">
|
||||
<Filter>ImDebugger</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="ImDebugger">
|
||||
|
@ -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 \
|
||||
@ -876,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 \
|
||||
|
@ -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 \
|
||||
|
Loading…
Reference in New Issue
Block a user