Add prototype decompiler GUI (#93)

* json output for basic ops

* add prototype gui

* add some more fields and layout the gui a little bit nicer
This commit is contained in:
water111
2020-10-23 17:20:04 -04:00
committed by GitHub
parent fc1a8f37c6
commit 0bc2466f86
5 changed files with 424 additions and 4 deletions
+2
View File
@@ -84,6 +84,8 @@ class Function {
DecompilerTypeSystem& dts,
LinkedObjectFile& file);
TypeSpec type;
std::shared_ptr<IR> ir = nullptr;
int segment = -1;
@@ -11,6 +11,7 @@
#include "LinkedObjectFile.h"
#include "decompiler/Disasm/InstructionDecode.h"
#include "decompiler/config.h"
#include "third-party/json.hpp"
/*!
* Set the number of segments in this object file.
@@ -504,6 +505,70 @@ void LinkedObjectFile::process_fp_relative_links() {
}
}
std::string LinkedObjectFile::to_asm_json() {
nlohmann::json data;
std::vector<nlohmann::json::object_t> functions;
std::unordered_map<std::string, int> functions_seen;
for (int seg = segments; seg-- > 0;) {
for (size_t fi = functions_by_seg.at(seg).size(); fi--;) {
auto& func = functions_by_seg.at(seg).at(fi);
auto fname = func.guessed_name.to_string();
if (functions_seen.find(fname) != functions_seen.end()) {
printf("duplicated %s\n", fname.c_str()); // todo - this needs fixing
functions_seen[fname]++;
fname += "-v" + std::to_string(functions_seen[fname]);
} else {
functions_seen[fname] = 0;
}
nlohmann::json::object_t f;
f["name"] = fname;
f["type"] = func.type.print();
f["segment"] = seg;
f["warnings"] = func.warnings;
std::vector<nlohmann::json::object_t> ops;
for (int i = 1; i < func.end_word - func.start_word; i++) {
nlohmann::json::object_t op;
auto label_id = get_label_at(seg, (func.start_word + i) * 4);
if (label_id != -1) {
op["label"] = labels.at(label_id).name;
}
auto& instr = func.instructions.at(i);
op["id"] = i;
op["asm_op"] = instr.to_string(*this);
if (func.has_basic_ops() && func.instr_starts_basic_op(i)) {
op["basic_op"] = func.get_basic_op_at_instr(i)->print(*this);
if (func.has_typemaps()) {
auto& tm = func.get_typemap_by_instr_idx(i);
auto& json_type_map = op["type_map"];
for (auto& kv : tm) {
json_type_map[kv.first.to_charp()] = kv.second.print();
}
}
}
for (int iidx = 0; iidx < instr.n_src; iidx++) {
if (instr.get_src(iidx).is_label()) {
auto lab = labels.at(instr.get_src(iidx).get_label());
if (is_string(lab.target_segment, lab.offset)) {
op["referenced_string"] = get_goal_string(lab.target_segment, lab.offset / 4 - 1);
}
}
}
ops.push_back(op);
}
f["asm"] = ops;
functions.push_back(f);
}
}
data["functions"] = functions;
return data.dump();
}
/*!
* Print disassembled functions and data segments.
*/
+1
View File
@@ -61,6 +61,7 @@ class LinkedObjectFile {
std::string print_disassembly();
bool has_any_functions();
void append_word_to_string(std::string& dest, const LinkedWord& word) const;
std::string to_asm_json();
struct Stats {
uint32_t total_code_bytes = 0;
+12 -4
View File
@@ -324,7 +324,7 @@ std::string pad_string(const std::string& in, size_t length) {
} // namespace
std::string ObjectFileDB::generate_obj_listing() {
std::string result;
std::string result = "[";
std::set<std::string> all_unique_names;
int unique_count = 0;
for (auto& obj_file : obj_file_order) {
@@ -348,7 +348,9 @@ std::string ObjectFileDB::generate_obj_listing() {
// this check is extremely important. It makes sure we don't have any repeat names. This could
// be caused by two files with the same name, in the same DGOs, but different data.
assert(int(all_unique_names.size()) == unique_count);
return result;
result.pop_back(); // kill last new line
result.pop_back(); // kill last comma
return result + "]";
}
/*!
@@ -447,9 +449,14 @@ void ObjectFileDB::write_disassembly(const std::string& output_dir,
if (obj.linked_data.has_any_functions() || disassemble_objects_without_functions) {
auto file_text = obj.linked_data.print_disassembly();
auto file_name = combine_path(output_dir, obj.record.to_unique_name() + ".func");
total_bytes += file_text.size();
auto json_asm_text = obj.linked_data.to_asm_json();
auto json_asm_file_name = combine_path(output_dir, obj.to_unique_name() + "_asm.json");
file_util::write_text_file(json_asm_file_name, json_asm_text);
total_bytes += file_text.size() + json_asm_text.size();
file_util::write_text_file(file_name, file_text);
total_files++;
total_files += 2;
}
});
@@ -655,6 +662,7 @@ void ObjectFileDB::analyze_functions() {
assert(false);
}
// GOOD!
func.type = kv->second;
spdlog::info("Type Analysis on {} {}", func.guessed_name.to_string(),
kv->second.print());
func.run_type_analysis(kv->second, dts, data.linked_data);
+344
View File
@@ -0,0 +1,344 @@
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QTreeView, QVBoxLayout, QWidget, QPlainTextEdit, QLineEdit, QListView, QDialog, QSplitter, QSizePolicy
from PyQt5.Qt import QStandardItemModel, QStandardItem, QFont, QModelIndex
import json
import re
import os
def get_monospaced_font():
"""
Get a monospaced font. Should work on both windows and linux.
"""
font = QFont("monospace")
font.setStyleHint(QFont.TypeWriter)
return font
def get_jak_path():
"""
Get a path to jak-project/
"""
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "..")
def segment_id_to_name(sid):
"""
GOAL segment ID to name string
"""
if sid == 0:
return "main"
elif sid == 1:
return "debug"
elif sid == 2:
return "top-level"
else:
return "INVALID-SEGMENT"
# Hold all the metadata for an object file
class ObjFile():
def __init__(self, obj):
"""
Convert the json data in obj.txt.
If the format changes or we add new fields, they should be added here.
"""
self.unique_name = obj[0] # The unique name that's used by the decompiler.
self.name_in_dgo = obj[1] # Name in the game.
self.version = obj[2] # GOAL object file format version
def get_description(self):
return "Name: {}\n Version: {}\n Name in game: {}".format(self.unique_name, self.version, self.name_in_dgo)
# Hold all of the object files in a dgo.
class DgoFile():
def __init__(self):
self.obj_files = dict()
def add_obj(self, obj):
self.obj_files[obj[0]] = ObjFile(obj)
# Hold all DGOs/Object files.
class FileMap():
def __init__(self):
self.dgo_files = dict()
self.all_objs = dict()
def add_obj_to_dgo(self, dgo, obj):
if not(dgo in self.dgo_files):
self.dgo_files[dgo] = DgoFile()
self.dgo_files[dgo].add_obj(obj)
self.all_objs[obj[0]] = ObjFile(obj)
def get_objs_matching_regex(self, regex):
"""
Get a list of object files with a name that matches the given regex.
"""
try:
r = re.compile(regex)
except:
return []
return list(filter(r.match, self.all_objs.keys()))
def load_obj_map_file(file_path):
"""
Load the obj.txt file generated by the decompiler.
Return a FileMap.
"""
file_map = FileMap()
with open(file_path) as f:
json_data = json.loads(f.read())
for obj_file in json_data:
for dgo in obj_file[3]:
file_map.add_obj_to_dgo(dgo, obj_file)
return file_map
class ObjectFileView(QDialog):
def __init__(self, name):
super().__init__()
self.setWindowTitle(name)
with open(os.path.join(get_jak_path(), "decompiler_out", "{}_asm.json".format(name))) as f:
self.asm_data = json.loads(f.read())
main_layout = QVBoxLayout()
monospaced_font = get_monospaced_font()
self.header_label = QLabel()
main_layout.addWidget(self.header_label)
function_splitter = QSplitter()
function_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding))
self.function_list = QTreeView()
self.function_list_model = QStandardItemModel()
self.functions_by_name = dict()
root = self.function_list_model.invisibleRootItem()
seg_roots = []
for i in range(3):
seg_entry = QStandardItem(segment_id_to_name(i))
seg_entry.setFont(monospaced_font)
seg_entry.setEditable(False)
root.appendRow(seg_entry)
seg_roots.append(seg_entry)
for f in self.asm_data["functions"]:
function_entry = QStandardItem(f["name"])
function_entry.setFont(monospaced_font)
function_entry.setEditable(False)
seg_roots[f["segment"]].appendRow(function_entry)
self.functions_by_name[f["name"]] = f
self.header_label.setText("Object File {} Functions ({} total):".format(name, len(self.asm_data["functions"])))
self.function_list.setModel(self.function_list_model)
self.function_list.clicked.connect(self.display_function)
function_splitter.addWidget(self.function_list)
layout = QVBoxLayout()
self.function_header_label = QLabel("No function selected")
self.function_header_label.setFont(monospaced_font)
self.header_label.setSizePolicy(QSizePolicy(QSizePolicy.Minimum,
QSizePolicy.Minimum))
layout.addWidget(self.function_header_label)
self.op_asm_split_view = QSplitter()
self.op_asm_split_view.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding))
self.basic_op_pane = QListView()
self.basic_op_pane.clicked.connect(self.basic_op_clicked)
#layout.addWidget(self.basic_op_pane)
self.op_asm_split_view.addWidget(self.basic_op_pane)
self.asm_pane = QListView()
self.op_asm_split_view.addWidget(self.asm_pane)
layout.addWidget(self.op_asm_split_view)
self.asm_display = QPlainTextEdit()
self.asm_display.setMaximumHeight(80)
layout.addWidget(self.asm_display)
self.warnings_label = QLabel()
layout.addWidget(self.warnings_label)
widget = QWidget()
widget.setLayout(layout)
function_splitter.addWidget(widget)
main_layout.addWidget(function_splitter)
# add it to the window!
self.setLayout(main_layout)
def display_function(self, item):
name = item.data()
monospaced_font = get_monospaced_font()
func = self.functions_by_name[name]
basic_op_model = QStandardItemModel()
basic_op_root = basic_op_model.invisibleRootItem()
asm_model = QStandardItemModel()
asm_root = asm_model.invisibleRootItem()
self.basic_id_to_asm = []
self.current_function = name
op_idx = 0
basic_idx = 0
for op in func["asm"]:
if "label" in op:
asm_item = QStandardItem(op["label"] + "\n " + op["asm_op"])
else:
asm_item = QStandardItem(" " + op["asm_op"])
asm_item.setFont(monospaced_font)
asm_item.setEditable(False)
asm_root.appendRow(asm_item)
if "basic_op" in op:
if "label" in op:
basic_item = QStandardItem(op["label"] + "\n " + op["basic_op"])
else:
basic_item = QStandardItem(" " + op["basic_op"])
basic_item.setFont(monospaced_font)
basic_item.setEditable(False)
basic_op_root.appendRow(basic_item)
self.basic_id_to_asm.append(op_idx)
basic_idx = basic_idx + 1
op_idx = op_idx + 1
self.basic_id_to_asm.append(op_idx)
self.basic_op_pane.setModel(basic_op_model)
self.asm_pane.setModel(asm_model)
self.warnings_label.setText(func["warnings"])
self.asm_display.setPlainText("")
self.function_header_label.setText("{}, type: {}".format(name, func["type"]))
def basic_op_clicked(self, item):
text = ""
added_reg = 0
asm_idx = self.basic_id_to_asm[item.row()]
asm_op = self.functions_by_name[self.current_function]["asm"][asm_idx]
if "type_map" in asm_op:
for reg, type_name in asm_op["type_map"].items():
text += "{}: {} ".format(reg, type_name)
added_reg += 1
if added_reg >= 4:
text += "\n"
added_reg = 0
text += "\n"
for i in range(asm_idx, self.basic_id_to_asm[item.row() + 1]):
text += self.functions_by_name[self.current_function]["asm"][i]["asm_op"] + "\n"
self.asm_display.setPlainText(text)
self.asm_display.setFont(get_monospaced_font())
self.asm_pane.setCurrentIndex(self.asm_pane.model().index(asm_idx, 0))
# A window for browsing all the object files.
# Doesn't actually know anything about what's in the files, it's just used to select a file.
class ObjectFileBrowser(QMainWindow):
def __init__(self, obj_map):
self.obj_map = obj_map
super().__init__()
self.setWindowTitle("Object File Browser")
self.childen_windows = []
layout = QVBoxLayout()
monospaced_font = get_monospaced_font()
layout.addWidget(QLabel("Browse object files by dgo..."))
# Set up the tree view
self.tree = QTreeView()
self.tree_model = QStandardItemModel()
self.tree_root = self.tree_model.invisibleRootItem()
for dgo_name, dgo in obj_map.dgo_files.items():
dgo_entry = QStandardItem(dgo_name)
dgo_entry.setFont(monospaced_font)
dgo_entry.setEditable(False)
for obj_name, obj in dgo.obj_files.items():
obj_entry = QStandardItem(obj_name)
obj_entry.setFont(monospaced_font)
obj_entry.setEditable(False)
dgo_entry.appendRow(obj_entry)
self.tree_root.appendRow(dgo_entry)
self.tree.setModel(self.tree_model)
self.tree.clicked.connect(self.handle_tree_click)
self.tree.doubleClicked.connect(self.handle_tree_double_click)
layout.addWidget(self.tree)
# Set up the Search Box
layout.addWidget(QLabel("Or search for object (regex):"))
self.search_box = QLineEdit()
self.search_box.textChanged.connect(self.handle_search_change)
layout.addWidget(self.search_box)
# Set up Search Results
self.search_result = QListView()
layout.addWidget(self.search_result)
self.search_result.clicked.connect(self.handle_search_result_click)
self.search_result.doubleClicked.connect(self.handle_search_result_double_click)
self.search_result.setMaximumHeight(200)
# Set up the info box at the bottom
self.text_box = QPlainTextEdit()
self.text_box.setReadOnly(True)
self.text_box.setFont(monospaced_font)
layout.addWidget(self.text_box)
self.text_box.setMaximumHeight(100)
self.text_box.setPlainText("Select an object file to see details. Double click to open.")
# add it to the window!
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
def handle_tree_click(self, val):
if not(val.parent().isValid()):
return
dgo = val.parent().data()
obj = val.data()
obj_info = self.obj_map.dgo_files[dgo].obj_files[obj]
self.text_box.setPlainText("{}\n DGO: {}".format(obj_info.get_description(), dgo))
def handle_search_change(self, text):
objs = self.obj_map.get_objs_matching_regex(text)
model = QStandardItemModel()
root = model.invisibleRootItem()
monospaced_font = get_monospaced_font()
for x in objs:
entry = QStandardItem(x)
entry.setFont(monospaced_font)
entry.setEditable(False)
root.appendRow(entry)
self.search_result.setModel(model)
def handle_search_result_click(self, val):
obj = val.data()
obj_info = self.obj_map.all_objs[obj]
self.text_box.setPlainText(obj_info.get_description())
def handle_search_result_double_click(self, val):
obj = val.data()
window = ObjectFileView(obj)
window.show()
# prevents window from being GC'd and closed.
self.childen_windows.append(window)
def handle_tree_double_click(self, val):
if not(val.parent().isValid()):
return
obj = val.data()
window = ObjectFileView(obj)
window.show()
# prevents window from being GC'd and closed.
self.childen_windows.append(window)
map_file = load_obj_map_file(os.path.join(get_jak_path(), "decompiler_out", "obj.txt"))
app = QApplication([])
app.setStyle('Windows')
window = ObjectFileBrowser(map_file)
window.show()
app.exec_()