[Decompiler] Add offline tests (#303)

* add offline test for gcommon as an example.

* fix test

* unused var
This commit is contained in:
water111
2021-03-03 15:42:55 -05:00
committed by GitHub
parent 2eca9ab801
commit 413c1f5c74
17 changed files with 1743 additions and 43 deletions
+1
View File
@@ -168,6 +168,7 @@ class Function {
Form* top_form = nullptr;
std::string debug_form_string;
bool print_debug_forms = false;
bool expressions_succeeded = false;
} ir2;
private:
+10 -5
View File
@@ -2241,10 +2241,14 @@ void ArrayFieldAccess::update_with_val(Form* new_val,
auto sll_matcher =
Matcher::fixed_op(FixedOperatorKind::SHL, {reg1_matcher, Matcher::integer(power_of_two)});
sll_matcher = Matcher::match_or({Matcher::cast("uint", sll_matcher), sll_matcher});
auto matcher = Matcher::fixed_op(FixedOperatorKind::ADDITION, {reg0_matcher, sll_matcher});
auto matcher = Matcher::match_or(
{Matcher::fixed_op(FixedOperatorKind::ADDITION, {reg0_matcher, sll_matcher}),
Matcher::fixed_op(FixedOperatorKind::ADDITION_PTR, {reg0_matcher, sll_matcher})});
auto match_result = match(matcher, new_val);
if (!match_result.matched) {
matcher = Matcher::fixed_op(FixedOperatorKind::ADDITION, {sll_matcher, reg0_matcher});
matcher = Matcher::match_or(
{Matcher::fixed_op(FixedOperatorKind::ADDITION, {sll_matcher, reg0_matcher}),
Matcher::fixed_op(FixedOperatorKind::ADDITION_PTR, {sll_matcher, reg0_matcher})});
match_result = match(matcher, new_val);
if (!match_result.matched) {
fmt::print("power {}\n", power_of_two);
@@ -2405,7 +2409,6 @@ void TypeOfElement::update_from_stack(const Env& env,
value->update_children_from_stack(env, pool, stack, allow_side_effects);
result->push_back(this);
}
////////////////////////
// EmptyElement
////////////////////////
@@ -2434,8 +2437,10 @@ void ConditionalMoveFalseElement::push_to_stack(const Env& env, FormPool& pool,
// pop the value and the original
auto popped = pop_to_forms({old_value, source}, env, pool, stack, true);
if (!is_symbol_true(popped.at(0))) {
throw std::runtime_error("Got unrecognized ConditionalMoveFalseElement original: " +
popped.at(0)->to_string(env));
lg::warn("Failed to ConditionalMoveFalseElement::push_to_stack");
stack.push_value_to_reg(source, popped.at(1), true);
stack.push_form_element(this, true);
return;
}
stack.push_value_to_reg(dest,
pool.alloc_single_element_form<GenericElement>(
+2 -1
View File
@@ -78,7 +78,8 @@ class ObjectFileDB {
void ir2_write_results(const std::string& output_dir);
std::string ir2_to_file(ObjectFileData& data);
std::string ir2_function_to_string(ObjectFileData& data, Function& function, int seg);
std::string ir2_final_out(ObjectFileData& data);
std::string ir2_final_out(ObjectFileData& data,
const std::unordered_set<std::string>& skip_functions = {});
void process_tpages();
std::string process_game_count_file();
+9 -6
View File
@@ -46,8 +46,11 @@ void ObjectFileDB::analyze_functions_ir2(const std::string& output_dir) {
lg::info("Expression building...");
ir2_build_expressions();
}
lg::info("Writing results...");
ir2_write_results(output_dir);
if (!output_dir.empty()) {
lg::info("Writing results...");
ir2_write_results(output_dir);
}
}
/*!
@@ -411,8 +414,7 @@ void ObjectFileDB::ir2_build_expressions() {
if (convert_to_expressions(func.ir2.top_form, *func.ir2.form_pool, func, dts)) {
successful++;
func.ir2.print_debug_forms = true;
// auto end = final_defun_out(func, func.ir2.env, dts);
// fmt::print("{}\n\n", end);
func.ir2.expressions_succeeded = true;
}
}
});
@@ -782,14 +784,15 @@ bool ObjectFileDB::lookup_function_type(const FunctionName& name,
return false;
}
std::string ObjectFileDB::ir2_final_out(ObjectFileData& data) {
std::string ObjectFileDB::ir2_final_out(ObjectFileData& data,
const std::unordered_set<std::string>& skip_functions) {
if (data.obj_version == 3) {
std::string result;
result += ";;-*-Lisp-*-\n";
result += "(in-package goal)\n\n";
assert(data.linked_data.functions_by_seg.at(TOP_LEVEL_SEGMENT).size() == 1);
auto top_level = data.linked_data.functions_by_seg.at(TOP_LEVEL_SEGMENT).at(0);
result += write_from_top_level(top_level, dts, data.linked_data);
result += write_from_top_level(top_level, dts, data.linked_data, skip_functions);
result += "\n\n";
return result;
} else {
+19 -6
View File
@@ -153,7 +153,8 @@ std::string careful_function_to_string(
std::string write_from_top_level(const Function& top_level,
const DecompilerTypeSystem& dts,
const LinkedObjectFile& file) {
const LinkedObjectFile& file,
const std::unordered_set<std::string>& skip_functions) {
auto top_form = top_level.ir2.top_form;
if (!top_form) {
return ";; ERROR: top level function was not converted to expressions. Cannot decompile.\n\n";
@@ -212,7 +213,11 @@ std::string write_from_top_level(const Function& top_level,
something_matched = true;
result += fmt::format(";; definition for function {}\n",
global_match_result.maps.strings.at(func_name));
result += careful_function_to_string(func, dts);
if (skip_functions.find(func->guessed_name.to_string()) == skip_functions.end()) {
result += careful_function_to_string(func, dts);
} else {
result += ";; skipped.\n\n";
}
}
}
@@ -224,7 +229,11 @@ std::string write_from_top_level(const Function& top_level,
something_matched = true;
result += fmt::format(";; definition for method of type {}\n",
method_match_result.maps.strings.at(type_name));
result += careful_function_to_string(func, dts);
if (skip_functions.find(func->guessed_name.to_string()) == skip_functions.end()) {
result += careful_function_to_string(func, dts);
} else {
result += ";; skipped.\n\n";
}
}
}
}
@@ -238,8 +247,8 @@ std::string write_from_top_level(const Function& top_level,
result += dts.ts.generate_deftype(dts.ts.lookup_type(name));
result += "\n\n";
} else {
result +=
fmt::format(";; type {} defintion, but it is unknown to the decompiler\n\n", name);
result += fmt::format(
";; type {} is defined here, but it is unknown to the decompiler\n\n", name);
}
something_matched = true;
}
@@ -256,7 +265,11 @@ std::string write_from_top_level(const Function& top_level,
something_matched = true;
result += fmt::format(";; definition (debug) for function {}\n",
debug_match_result.maps.strings.at(0));
result += careful_function_to_string(func, dts, FunctionDefSpecials::DEFUN_DEBUG);
if (skip_functions.find(func->guessed_name.to_string()) == skip_functions.end()) {
result += careful_function_to_string(func, dts, FunctionDefSpecials::DEFUN_DEBUG);
} else {
result += ";; skipped.\n\n";
}
}
}
}
+2 -1
View File
@@ -12,5 +12,6 @@ std::string final_defun_out(const Function& func,
FunctionDefSpecials special_mode = FunctionDefSpecials::NONE);
std::string write_from_top_level(const Function& top_level,
const DecompilerTypeSystem& dts,
const LinkedObjectFile& file);
const LinkedObjectFile& file,
const std::unordered_set<std::string>& skip_functions = {});
} // namespace decompiler
@@ -1,4 +1,8 @@
{
"gcommon":[
["L345", "float", true]
],
"math":[
["L41", "float", true],
["L34", "float", true],
+15
View File
@@ -288,11 +288,26 @@ bool Compiler::connect_to_target() {
return true;
}
/*!
* Just run the front end on a string. Will not do register allocation or code generation.
* Useful for typechecking or running strings that invoke the compiler again.
*/
void Compiler::run_front_end_on_string(const std::string& src) {
auto code = m_goos.reader.read_from_string({src});
compile_object_file("run-on-string", code, true);
}
/*!
* Run the entire compilation process on the input source code. Will generate an object file, but
* won't save it anywhere.
*/
void Compiler::run_full_compiler_on_string_no_save(const std::string& src) {
auto code = m_goos.reader.read_from_string({src});
auto compiled = compile_object_file("run-on-string", code, true);
color_object_file(compiled);
codegen_object_file(compiled);
}
std::vector<std::string> Compiler::run_test_no_load(const std::string& source_code) {
auto code = m_goos.reader.read_from_file({source_code});
compile_object_file("test-code", code, true);
+1
View File
@@ -38,6 +38,7 @@ class Compiler {
std::vector<std::string> run_test_no_load(const std::string& source_code);
void compile_and_send_from_string(const std::string& source_code);
void run_front_end_on_string(const std::string& src);
void run_full_compiler_on_string_no_save(const std::string& src);
void shutdown_target();
void enable_throw_on_redefines() { m_throw_on_define_extern_redefinition = true; }
Debugger& get_debugger() { return m_debugger; }
@@ -197,7 +197,7 @@ Val* Compiler::compile_asm_file(const goos::Object& form, const goos::Object& re
/*!
* Simple help / documentation command
*/
Val* Compiler::compile_repl_help(const goos::Object& form, const goos::Object& rest, Env* env) {
Val* Compiler::compile_repl_help(const goos::Object&, const goos::Object&, Env*) {
fmt::print(fmt::emphasis::bold, "\nREPL Controls:\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(e)\n");
fmt::print(" - Exit the compiler once the current REPL command is finished\n");
+3 -3
View File
@@ -4,8 +4,8 @@
#include "common/util/FileUtil.h"
#include "common/log/log.h"
#include "third-party/fmt/core.h";
#include "third-party/fmt/color.h";
#include "third-party/fmt/core.h"
#include "third-party/fmt/color.h"
#include "common/goos/ReplHistory.h"
@@ -61,7 +61,7 @@ int main(int argc, char** argv) {
fmt::print(" for help with common commands and REPL usage.\n");
fmt::print("Run ");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(lt)");
fmt::print(" to connect to the local listener.\n");
fmt::print(" to connect to the local target.\n");
ReplHistory::repl_load_history();
if (argument.empty()) {
+1
View File
@@ -1,6 +1,7 @@
set(CMAKE_CXX_STANDARD 17)
include(${CMAKE_CURRENT_LIST_DIR}/goalc/CMakeLists.txt)
include(${CMAKE_CURRENT_LIST_DIR}/offline/CMakeLists.txt)
add_executable(goalc-test
${CMAKE_CURRENT_LIST_DIR}/test_main.cpp
File diff suppressed because it is too large Load Diff
@@ -2572,4 +2572,25 @@ TEST_F(FormRegressionTest, ExprTerminal2) {
" (- f0-4 (+ arg1 (* arg2 (* f0-4 f0-4))))\n"
" )";
test_with_expr(func, type, expected, false, "", {{"L17", "A ~A"}});
}
TEST_F(FormRegressionTest, MoveFalse) {
std::string func =
"sll r0, r0, 0\n"
"L29:\n"
" daddiu sp, sp, -16\n"
" sd fp, 8(sp)\n"
" or fp, t9, r0\n"
" daddiu v1, s7, 8\n"
" daddiu a0, a0, 12\n"
" andi a0, a0, 1\n"
" movz v1, s7, a0\n"
" or v0, v1, r0\n"
" ld fp, 8(sp)\n"
" jr ra\n"
" daddiu sp, sp, 16\n";
std::string type = "(function int symbol)";
std::string expected = "(nonzero? (logand (+ arg0 12) 1))";
test_with_expr(func, type, expected, false, "", {{"L17", "A ~A"}});
}
+5
View File
@@ -0,0 +1,5 @@
add_executable(offline-test
${CMAKE_CURRENT_LIST_DIR}/offline_test_main.cpp)
target_link_libraries(offline-test common gtest decomp compiler)
+303
View File
@@ -0,0 +1,303 @@
#include <common/link_types.h>
#include "common/util/FileUtil.h"
#include "gtest/gtest.h"
#include "common/log/log.h"
#include "decompiler/Disasm/OpcodeInfo.h"
#include "decompiler/config.h"
#include "decompiler/ObjectFile/ObjectFileDB.h"
#include "goalc/compiler/Compiler.h"
int main(int argc, char** argv) {
lg::initialize();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
namespace {
// the object files to test
const std::unordered_set<std::string> object_files_to_decompile = {"gcommon"};
// the object files to check against a reference in test/decompiler/reference
const std::unordered_set<std::string> object_files_to_check_against_reference = {
"gcommon" // NOTE: this file needs work, but adding it for now just to test the framework.
};
// the functions we expect the decompiler to skip
const std::unordered_set<std::string> expected_skip_in_decompiler = {
// gcommon
"quad-copy!", // asm mempcy
// gkernel
"set-to-run-bootstrap", // kernel context switch
"throw", // manually sets fp/t9.
"throw-dispatch", // restore context
"(method 0 catch-frame)", // save context
"(method 11 cpu-thread)", // kernel -> user context switch
"(method 10 cpu-thread)", // user -> kernel context switch
"reset-and-call", // kernel -> user
"return-from-thread-dead", // kernel -> user
"return-from-thread", // kernel -> user
"return-from-exception", // ps2 exception -> ps2 user
// pskernel
"kernel-check-hardwired-addresses", // ps2 ee kernel debug hook
"kernel-read-function", // ps2 ee kernel debug hook
"kernel-write-function", // ps2 ee kernel debug hook
"kernel-copy-function" // ps2 ee kernel debug hook
};
const std::unordered_set<std::string> skip_in_compiling = {
// these functions are not implemented by the compiler in OpenGOAL, but are in GOAL.
"abs", "ash", "min", "max", "lognor"};
} // namespace
class OfflineDecompilation : public ::testing::Test {
protected:
static std::unique_ptr<decompiler::ObjectFileDB> db;
static void SetUpTestCase() {
// global setup
file_util::init_crc();
decompiler::init_opcode_info();
decompiler::set_config(
file_util::get_file_path({"decompiler", "config", "jak1_ntsc_black_label.jsonc"}));
decompiler::get_config().allowed_objects = object_files_to_decompile;
db = std::make_unique<decompiler::ObjectFileDB>(
std::vector<std::string>{file_util::get_file_path({"iso_data", "CGO", "KERNEL.CGO"}),
file_util::get_file_path({"iso_data", "CGO", "ENGINE.CGO"})},
decompiler::get_config().obj_file_name_map_file, std::vector<std::string>{},
std::vector<std::string>{});
// basic processing to find functions/data/disassembly
db->process_link_data();
db->find_code();
db->process_labels();
// fancy decompilation.
db->analyze_functions_ir2({});
}
static void TearDownTestCase() { db.reset(); }
};
std::unique_ptr<decompiler::ObjectFileDB> OfflineDecompilation::db;
/*!
* Check that the most basic disassembly into files/functions/instructions has succeeded.
*/
TEST_F(OfflineDecompilation, CheckBasicDecode) {
int obj_count = 0;
db->for_each_obj([&](decompiler::ObjectFileData& obj) {
obj_count++;
auto& stats = obj.linked_data.stats;
// make sure we decoded all instructions
EXPECT_EQ(stats.code_bytes / 4, stats.decoded_ops);
// make sure all FP uses are properly recognized
EXPECT_EQ(stats.n_fp_reg_use, stats.n_fp_reg_use_resolved);
});
EXPECT_EQ(obj_count, decompiler::get_config().allowed_objects.size());
}
/*!
* Not a super great test, but check that we find functions, methods, and logins.
* This is a test of ir2_top_level_pass, which isn't tested as part of the normal decompiler tests.
*/
TEST_F(OfflineDecompilation, FunctionDetect) {
int function_count = 0; // global functions
int method_count = 0; // methods
int login_count = 0; // top-level logins
int unknown_count = 0; // unknown functions, like anonymous lambdas
db->for_each_function(
[&](decompiler::Function& func, int segment_id, decompiler::ObjectFileData&) {
if (segment_id == TOP_LEVEL_SEGMENT) {
EXPECT_EQ(func.guessed_name.kind, decompiler::FunctionName::FunctionKind::TOP_LEVEL_INIT);
} else {
EXPECT_NE(func.guessed_name.kind, decompiler::FunctionName::FunctionKind::TOP_LEVEL_INIT);
}
switch (func.guessed_name.kind) {
case decompiler::FunctionName::FunctionKind::GLOBAL:
function_count++;
break;
case decompiler::FunctionName::FunctionKind::METHOD:
method_count++;
break;
case decompiler::FunctionName::FunctionKind::TOP_LEVEL_INIT:
login_count++;
break;
case decompiler::FunctionName::FunctionKind::UNIDENTIFIED:
unknown_count++;
break;
default:
assert(false);
}
});
// one login per object file
EXPECT_EQ(decompiler::get_config().allowed_objects.size(), login_count);
// not many lambdas.
EXPECT_TRUE(unknown_count < 10);
}
TEST_F(OfflineDecompilation, AsmFunction) {
int failed_count = 0;
db->for_each_function([&](decompiler::Function& func, int, decompiler::ObjectFileData&) {
if (func.suspected_asm) {
if (expected_skip_in_decompiler.find(func.guessed_name.to_string()) ==
expected_skip_in_decompiler.end()) {
lg::error("Function {} was marked as asm, but wasn't expected.",
func.guessed_name.to_string());
failed_count++;
}
}
});
EXPECT_EQ(failed_count, 0);
}
/*!
* Test that all functions pass CFG build stage.
*/
TEST_F(OfflineDecompilation, CfgBuild) {
int failed_count = 0;
db->for_each_function([&](decompiler::Function& func, int, decompiler::ObjectFileData&) {
if (!func.suspected_asm) {
if (!func.cfg || !func.cfg->is_fully_resolved()) {
lg::error("Function {} failed cfg", func.guessed_name.to_string());
failed_count++;
}
}
});
EXPECT_EQ(failed_count, 0);
}
/*!
* Test that all functions pass the atomic op construction stage
*/
TEST_F(OfflineDecompilation, AtomicOp) {
int failed_count = 0;
db->for_each_function([&](decompiler::Function& func, int, decompiler::ObjectFileData&) {
if (!func.suspected_asm) {
if (!func.ir2.atomic_ops || !func.ir2.atomic_ops_succeeded) {
lg::error("Function {} failed atomic ops", func.guessed_name.to_string());
failed_count++;
}
}
});
EXPECT_EQ(failed_count, 0);
}
/*!
* Test that all functions pass the type analysis stage
*/
TEST_F(OfflineDecompilation, TypeAnalysis) {
int failed_count = 0;
db->for_each_function([&](decompiler::Function& func, int, decompiler::ObjectFileData&) {
if (!func.suspected_asm) {
if (!func.ir2.env.has_type_analysis()) {
lg::error("Function {} failed types", func.guessed_name.to_string());
failed_count++;
}
}
});
EXPECT_EQ(failed_count, 0);
}
TEST_F(OfflineDecompilation, RegisterUse) {
int failed_count = 0;
db->for_each_function([&](decompiler::Function& func, int, decompiler::ObjectFileData&) {
if (!func.suspected_asm) {
if (!func.ir2.env.has_reg_use()) {
lg::error("Function {} failed reg use", func.guessed_name.to_string());
failed_count++;
}
}
});
EXPECT_EQ(failed_count, 0);
}
TEST_F(OfflineDecompilation, VariableSSA) {
int failed_count = 0;
db->for_each_function([&](decompiler::Function& func, int, decompiler::ObjectFileData&) {
if (!func.suspected_asm) {
if (!func.ir2.env.has_local_vars()) {
lg::error("Function {} failed ssa", func.guessed_name.to_string());
failed_count++;
}
}
});
EXPECT_EQ(failed_count, 0);
}
TEST_F(OfflineDecompilation, Structuring) {
int failed_count = 0;
db->for_each_function([&](decompiler::Function& func, int, decompiler::ObjectFileData&) {
if (!func.suspected_asm) {
if (!func.ir2.top_form) {
lg::error("Function {} failed structuring", func.guessed_name.to_string());
failed_count++;
}
}
});
EXPECT_EQ(failed_count, 0);
}
TEST_F(OfflineDecompilation, Expressions) {
int failed_count = 0;
db->for_each_function([&](decompiler::Function& func, int, decompiler::ObjectFileData&) {
if (!func.suspected_asm) {
if (!func.ir2.expressions_succeeded) {
lg::error("Function {} failed expressions", func.guessed_name.to_string());
failed_count++;
}
}
});
EXPECT_EQ(failed_count, 0);
}
namespace {
void strip_trailing_newlines(std::string& in) {
while (!in.empty() && in.back() == '\n') {
in.pop_back();
}
}
} // namespace
TEST_F(OfflineDecompilation, Reference) {
for (auto& file : object_files_to_check_against_reference) {
auto& obj_l = db->obj_files_by_name.at(file);
ASSERT_EQ(obj_l.size(), 1);
std::string src = db->ir2_final_out(obj_l.at(0));
auto reference = file_util::read_text_file(file_util::get_file_path(
{"test", "decompiler", "reference", fmt::format("{}_REF.gc", file)}));
strip_trailing_newlines(reference);
strip_trailing_newlines(src);
EXPECT_EQ(reference, src);
}
}
TEST_F(OfflineDecompilation, Compile) {
Compiler compiler;
for (auto& file : object_files_to_check_against_reference) {
auto& obj_l = db->obj_files_by_name.at(file);
ASSERT_EQ(obj_l.size(), 1);
std::string src = db->ir2_final_out(obj_l.at(0), skip_in_compiling);
compiler.run_full_compiler_on_string_no_save(src);
}
}
+4 -20
View File
@@ -1,11 +1,9 @@
#include <filesystem>
#include "gtest/gtest.h"
#include "common/util/FileUtil.h"
#include <filesystem>
#ifdef _WIN32
#include <Windows.h>
#endif
#include "common/log/log.h"
// Running subsets of tests, see:
// -
@@ -19,22 +17,8 @@
// to make it easier to test a subset of tests
int main(int argc, char** argv) {
#ifdef _WIN32
// Always enable VIRTUAL_TERMINAL_PROCESSING, this console mode allows the console (stdout) to
// support ANSI colors in the outputted text, which are used by the logging tool.
// This mode may not be enabled by default, and changing that involves modifying the registry,
// so it seems like a better solution would be enabling it ourselves.
lg::initialize();
// Get handle to stdout
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
// get current stdout mode
DWORD modeStdOut;
GetConsoleMode(hStdOut, &modeStdOut);
// enable VIRTUAL_TERMINAL_PROCESSING. As a bitwise OR it will not do anything if it is
// already set
modeStdOut |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hStdOut, modeStdOut);
#endif
::testing::InitGoogleTest(&argc, argv);
// Re-init failed folder