mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-05 04:05:27 +02:00
Add struct viewer debugging tool
This commit is contained in:
@@ -0,0 +1,783 @@
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
#include "ext/imgui/imgui.h"
|
||||
|
||||
#include "Core/MemMap.h"
|
||||
#include "Core/Debugger/Breakpoints.h"
|
||||
#include "Core/MIPS/MIPSDebugInterface.h"
|
||||
|
||||
#include "UI/ImDebugger/ImStructViewer.h"
|
||||
|
||||
static auto COLOR_GRAY = ImVec4(0.45f, 0.45f, 0.45f, 1);
|
||||
static auto COLOR_RED = ImVec4(1, 0, 0, 1);
|
||||
|
||||
enum class BuiltInType {
|
||||
Bool,
|
||||
Char,
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
TerminatedString,
|
||||
Float,
|
||||
Void,
|
||||
};
|
||||
|
||||
struct BuiltIn {
|
||||
BuiltInType type;
|
||||
ImGuiDataType imGuiType;
|
||||
const char* hexFormat;
|
||||
};
|
||||
|
||||
static const std::unordered_map<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(430, 450), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Struct viewer", open) || !mipsDebug->isAlive() || !Memory::IsActive()) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
if (ghidraClient_.Ready()) {
|
||||
ghidraClient_.UpdateResult();
|
||||
if (!fetchedAtLeastOnce_ && !ghidraClient_.Failed()) {
|
||||
fetchedAtLeastOnce_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
||||
if (fetchedAtLeastOnce_) {
|
||||
DrawStructViewer();
|
||||
} else {
|
||||
DrawConnectionSetup();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ImStructViewer::DrawConnectionSetup() {
|
||||
ImGui::TextWrapped("Struct viewer visualizes data in memory using types from your Ghidra project.");
|
||||
ImGui::TextWrapped("To get started install the ghidra-rest-api plugin and start the Rest API server.");
|
||||
ImGui::TextWrapped("When ready press the connect button below.");
|
||||
|
||||
ImGui::BeginDisabled(!ghidraClient_.Idle());
|
||||
ImGui::PushItemWidth(120);
|
||||
ImGui::InputText("Host", ghidraHost_, IM_ARRAYSIZE(ghidraHost_));
|
||||
ImGui::SameLine();
|
||||
ImGui::InputInt("Port", &ghidraPort_, 0);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Connect")) {
|
||||
ghidraClient_.FetchAll(ghidraHost_, ghidraPort_);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (ghidraClient_.Idle() && ghidraClient_.Failed()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
|
||||
ImGui::TextWrapped("Error: %s", ghidraClient_.result.error.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
void ImStructViewer::DrawStructViewer() {
|
||||
ImGui::BeginDisabled(!ghidraClient_.Idle());
|
||||
if (ImGui::Button("Refresh data types")) {
|
||||
ghidraClient_.FetchAll(ghidraHost_, ghidraPort_);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (ghidraClient_.Idle() && ghidraClient_.Failed()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextWrapped("Error: %s", ghidraClient_.result.error.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_Reorderable)) {
|
||||
if (ImGui::BeginTabItem("Globals")) {
|
||||
DrawGlobals();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Watch")) {
|
||||
DrawWatch();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
void ImStructViewer::DrawGlobals() {
|
||||
globalFilter_.Draw();
|
||||
if (ImGui::BeginTable("##globals", 3,
|
||||
ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_RowBg)) {
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("Field");
|
||||
ImGui::TableSetupColumn("Type");
|
||||
ImGui::TableSetupColumn("Content");
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (const auto& symbol: ghidraClient_.result.symbols) {
|
||||
if (!symbol.label || !symbol.userDefined || symbol.dataTypePathName.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (!globalFilter_.PassFilter(symbol.name.c_str())) {
|
||||
continue;
|
||||
}
|
||||
DrawType(symbol.address, 0, symbol.dataTypePathName, nullptr, symbol.name.c_str(), -1);
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
void ImStructViewer::DrawWatch() {
|
||||
DrawNewWatchEntry();
|
||||
ImGui::Dummy(ImVec2(1, 6));
|
||||
|
||||
watchFilter_.Draw();
|
||||
if (ImGui::BeginTable("##watch", 3,
|
||||
ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_RowBg)) {
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("Field");
|
||||
ImGui::TableSetupColumn("Type");
|
||||
ImGui::TableSetupColumn("Content");
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
int watchIndex = -1;
|
||||
for (const auto& watch: watches_) {
|
||||
watchIndex++;
|
||||
if (!watchFilter_.PassFilter(watch.name.c_str())) {
|
||||
continue;
|
||||
}
|
||||
u32 address = 0;
|
||||
if (!watch.expression.empty()) {
|
||||
u32 val;
|
||||
PostfixExpression postfix;
|
||||
if (mipsDebug_->initExpression(watch.expression.c_str(), postfix)) {
|
||||
if (mipsDebug_->parseExpression(postfix, val)) {
|
||||
address = val;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
address = watch.address;
|
||||
}
|
||||
DrawType(address, 0, watch.typePathName, nullptr, watch.name.c_str(), watchIndex);
|
||||
}
|
||||
if (removeWatchIndex_ != -1) {
|
||||
watches_.erase(watches_.begin() + removeWatchIndex_);
|
||||
removeWatchIndex_ = -1;
|
||||
}
|
||||
if (addWatch_.address != 0) {
|
||||
watches_.push_back(addWatch_);
|
||||
addWatch_ = Watch();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
void ImStructViewer::DrawNewWatchEntry() {
|
||||
ImGui::PushItemWidth(150);
|
||||
ImGui::InputText("Name", newWatch_.name, IM_ARRAYSIZE(newWatch_.name));
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::BeginCombo("Type", newWatch_.typeDisplayName.c_str())) {
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
ImGui::SetKeyboardFocusHere(0);
|
||||
}
|
||||
newWatch_.typeFilter.Draw();
|
||||
for (const auto& entry: ghidraClient_.result.types) {
|
||||
const auto& type = entry.second;
|
||||
if (newWatch_.typeFilter.PassFilter(type.displayName.c_str())) {
|
||||
ImGui::PushID(type.pathName.c_str());
|
||||
if (ImGui::Selectable(type.displayName.c_str(), newWatch_.typePathName == type.pathName)) {
|
||||
newWatch_.typePathName = type.pathName;
|
||||
newWatch_.typeDisplayName = type.displayName;
|
||||
}
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && ImGui::BeginTooltip()) {
|
||||
ImGui::Text("%s (%s)", type.displayName.c_str(), type.pathName.c_str());
|
||||
ImGui::Text("Length: %x (aligned: %x)", type.length, type.alignedLength);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::InputText("Expression", newWatch_.expression, IM_ARRAYSIZE(newWatch_.expression));
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Dynamic", &newWatch_.dynamic);
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal))
|
||||
ImGui::SetTooltip("When checked the expression will be\nre-evaluated on each frame.");
|
||||
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Add watch")) {
|
||||
u32 val;
|
||||
PostfixExpression postfix;
|
||||
if (newWatch_.typePathName.empty()) {
|
||||
newWatch_.error = "type can't be empty";
|
||||
} else if (!mipsDebug_->initExpression(newWatch_.expression, postfix)
|
||||
|| !mipsDebug_->parseExpression(postfix, val)) {
|
||||
newWatch_.error = "invalid expression";
|
||||
} else {
|
||||
std::string watchName = newWatch_.name;
|
||||
if (watchName.empty()) {
|
||||
watchName = "<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);
|
||||
|
||||
// Text and Tree nodes are less high than framed widgets, using AlignTextToFramePadding() we add vertical spacing
|
||||
// to make the tree lines equal high.
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(COLUMN_NAME);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
// Flags used for nodes that can't be further opened
|
||||
const ImGuiTreeNodeFlags leafFlags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen |
|
||||
ImGuiTreeNodeFlags_Bullet | extraTreeNodeFlags;
|
||||
|
||||
// Type is missing in fetched types, this can happen e.g. if type used for watch is removed from Ghidra
|
||||
if (!hasType) {
|
||||
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
|
||||
DrawContextMenu(base, offset, 0, typePathName, name, watchIndex);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
|
||||
DrawTypeColumn("<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);
|
||||
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);
|
||||
DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
|
||||
DrawTypeColumn("%s", typeDisplayName, base, offset);
|
||||
|
||||
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
|
||||
const u64 value = ReadMemoryInt(address, type.length);
|
||||
const std::string stringValue = FormatEnumValue(type.enumMembers, value);
|
||||
ImGui::Text("= %llx (%s)", value, stringValue.c_str());
|
||||
DrawIntBuiltInEditPopup(address, type.length);
|
||||
break;
|
||||
}
|
||||
case POINTER: {
|
||||
const bool nodeOpen = ImGui::TreeNodeEx("Pointer", extraTreeNodeFlags, "%s", name);
|
||||
DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
|
||||
DrawTypeColumn("%s", typeDisplayName, base, offset);
|
||||
|
||||
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
|
||||
const u32 pointer = Memory::Read_U32(address);
|
||||
DrawPointerContent(types, type, pointer);
|
||||
|
||||
if (nodeOpen) {
|
||||
if (types.count(type.pointerTypePathName)) {
|
||||
const auto& pointedType = types.at(type.pointerTypePathName);
|
||||
|
||||
const auto countStateId = ImGui::GetID("PointerElementCount");
|
||||
const int pointerElementCount = ImGui::GetStateStorage()->GetInt(countStateId, 1);
|
||||
|
||||
// A pointer to unsized type (e.g. function or void) can't have more than one element
|
||||
if (pointedType.alignedLength > 0) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(COLUMN_NAME);
|
||||
if (ImGui::Button("Show more")) {
|
||||
ImGui::GetStateStorage()->SetInt(countStateId, pointerElementCount + 1);
|
||||
}
|
||||
if (pointerElementCount > 1) {
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("(showing %x)", pointerElementCount);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < pointerElementCount; i++) {
|
||||
char nameBuffer[256];
|
||||
FormatIndexedMember(nameBuffer, sizeof(nameBuffer), name, i);
|
||||
// A pointer always creates extra node in the tree so using DefaultOpen to spare user one click
|
||||
DrawType(pointer, i * pointedType.alignedLength, type.pointerTypePathName,
|
||||
nullptr, nameBuffer, -1, ImGuiTreeNodeFlags_DefaultOpen);
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ARRAY: {
|
||||
const bool nodeOpen = ImGui::TreeNodeEx("Array", extraTreeNodeFlags, "%s", name);
|
||||
DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
|
||||
DrawTypeColumn("%s", typeDisplayName, base, offset);
|
||||
|
||||
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
|
||||
DrawArrayContent(types, type, address);
|
||||
|
||||
if (nodeOpen) {
|
||||
for (int i = 0; i < type.arrayElementCount; i++) {
|
||||
char nameBuffer[256];
|
||||
FormatIndexedMember(nameBuffer, sizeof(nameBuffer), name, i);
|
||||
DrawType(base, offset + i * type.arrayElementLength, type.arrayTypePathName,
|
||||
nullptr, nameBuffer, -1);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case STRUCTURE:
|
||||
case UNION: {
|
||||
const bool nodeOpen = ImGui::TreeNodeEx("Composite", extraTreeNodeFlags, "%s", name);
|
||||
DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
|
||||
DrawTypeColumn("%s", typeDisplayName, base, offset);
|
||||
|
||||
if (nodeOpen) {
|
||||
for (const auto& member: type.compositeMembers) {
|
||||
DrawType(base, offset + member.offset, member.typePathName, nullptr,
|
||||
member.fieldName.c_str(), -1);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FUNCTION_DEFINITION:
|
||||
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
|
||||
DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
|
||||
DrawTypeColumn("%s", typeDisplayName, base, offset);
|
||||
|
||||
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
|
||||
ImGui::Text("<function definition>"); // TODO could be go to in disassembler here
|
||||
break;
|
||||
case BUILT_IN: {
|
||||
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
|
||||
DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
|
||||
|
||||
if (knownBuiltIns.count(typePathName)) {
|
||||
DrawTypeColumn("%s", typeDisplayName, base, offset);
|
||||
ImGui::TableSetColumnIndex(COLUMN_CONTENT);
|
||||
DrawBuiltInContent(knownBuiltIns.at(typePathName), address);
|
||||
} else {
|
||||
// Some built in types are rather obscure so we don't handle every possible one
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
|
||||
DrawTypeColumn("<unsupported built in: %s>", typePathName, base, offset);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// At this point there is most likely some issue in the Ghidra plugin and the type wasn't
|
||||
// classified to any category
|
||||
ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
|
||||
DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
|
||||
DrawTypeColumn("<not implemented type: %s>", typeDisplayName, base, offset);
|
||||
ImGui::PopStyleColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void ImStructViewer::DrawContextMenu(
|
||||
const u32 base,
|
||||
const u32 offset,
|
||||
const int length,
|
||||
const std::string& typePathName,
|
||||
const char* name,
|
||||
const int watchIndex
|
||||
) {
|
||||
ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
|
||||
if (ImGui::BeginPopup("context")) {
|
||||
const u32 address = base + offset;
|
||||
|
||||
// This might be called when iterating over existing watches so can't modify the watch vector directly here
|
||||
if (watchIndex < 0) {
|
||||
if (ImGui::MenuItem("Add watch")) {
|
||||
addWatch_.address = address;
|
||||
addWatch_.typePathName = typePathName;
|
||||
addWatch_.name = name;
|
||||
}
|
||||
} else if (watchIndex < watches_.size()) {
|
||||
if (ImGui::MenuItem("Remove watch")) {
|
||||
removeWatchIndex_ = watchIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// if (ImGui::MenuItem("Go to in memory view")) {
|
||||
// TODO imgui memory view not yet implemented
|
||||
// }
|
||||
|
||||
// Memory breakpoints are only possible for sized types
|
||||
if (length > 0) {
|
||||
const u32 end = address + length;
|
||||
MemCheck memCheck;
|
||||
const bool hasMemCheck = CBreakPoints::GetMemCheck(address, end, &memCheck);
|
||||
if (hasMemCheck) {
|
||||
if (ImGui::MenuItem("Remove memory breakpoint")) {
|
||||
CBreakPoints::RemoveMemCheck(address, end);
|
||||
}
|
||||
}
|
||||
if (!hasMemCheck || !(memCheck.cond & MEMCHECK_READ)) {
|
||||
if (ImGui::MenuItem("Add memory read breakpoint")) {
|
||||
CBreakPoints::AddMemCheck(address, end, MEMCHECK_READ, BREAK_ACTION_PAUSE);
|
||||
}
|
||||
}
|
||||
if (!hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE)) {
|
||||
if (ImGui::MenuItem("Add memory write breakpoint")) {
|
||||
CBreakPoints::AddMemCheck(address, end, MEMCHECK_WRITE, BREAK_ACTION_PAUSE);
|
||||
}
|
||||
}
|
||||
if (!hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE_ONCHANGE)) {
|
||||
if (ImGui::MenuItem("Add memory write on change breakpoint")) {
|
||||
constexpr auto cond = static_cast<MemCheckCondition>(MEMCHECK_WRITE | MEMCHECK_WRITE_ONCHANGE);
|
||||
CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user