From 493522a7698888f423e5595901b655059c5e3014 Mon Sep 17 00:00:00 2001 From: Antonio Guastella <65413476+Whoneon@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:14:41 +0100 Subject: [PATCH] recompiler: filter auto-generated map symbols to reduce oversized outputs (#95) --- ps2xRecomp/include/ps2recomp/elf_parser.h | 5 +- ps2xRecomp/src/lib/elf_parser.cpp | 50 +++++++++- ps2xTest/src/ps2_recompiler_tests.cpp | 107 ++++++++++++++++++++++ 3 files changed, 159 insertions(+), 3 deletions(-) diff --git a/ps2xRecomp/include/ps2recomp/elf_parser.h b/ps2xRecomp/include/ps2recomp/elf_parser.h index bb756d7..c03f43f 100644 --- a/ps2xRecomp/include/ps2recomp/elf_parser.h +++ b/ps2xRecomp/include/ps2recomp/elf_parser.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace ps2recomp { @@ -45,6 +46,8 @@ namespace ps2recomp std::vector m_symbols; std::vector m_relocations; std::vector m_extraFunctions; + bool m_hasLoadedGhidraMap = false; + std::unordered_set m_ghidraMapStarts; void loadSections(); void loadSymbols(); @@ -56,4 +59,4 @@ namespace ps2recomp } // namespace ps2recomp -#endif // PS2RECOMP_ELF_PARSER_H \ No newline at end of file +#endif // PS2RECOMP_ELF_PARSER_H diff --git a/ps2xRecomp/src/lib/elf_parser.cpp b/ps2xRecomp/src/lib/elf_parser.cpp index b96888e..bd91e03 100644 --- a/ps2xRecomp/src/lib/elf_parser.cpp +++ b/ps2xRecomp/src/lib/elf_parser.cpp @@ -26,7 +26,10 @@ namespace { bool IsAutoGeneratedName(const std::string &name) { - return name.rfind("sub_", 0) == 0; + return name.rfind("sub_", 0) == 0 || + name.rfind("FUN_", 0) == 0 || + name.rfind("LAB_", 0) == 0 || + name.rfind("DAT_", 0) == 0; } void AppendLoadSegmentsAsSections(const ELFIO::elfio &elf, std::vector §ions) @@ -566,6 +569,13 @@ namespace ps2recomp continue; } + if (m_hasLoadedGhidraMap && + IsAutoGeneratedName(symbol.name) && + !m_ghidraMapStarts.contains(symbol.address)) + { + continue; + } + const Section *functionSection = FindFunctionSectionByAddress(m_sections, symbol.address); if (!functionSection) { @@ -947,6 +957,9 @@ namespace ps2recomp return false; } + m_hasLoadedGhidraMap = false; + m_ghidraMapStarts.clear(); + std::ifstream file(mapPath); if (!file.is_open()) { @@ -963,6 +976,7 @@ namespace ps2recomp int count = 0; int skippedNonExecutable = 0; int skippedInvalidRange = 0; + std::unordered_set mapStarts; while (std::getline(file, line)) { if (line.empty()) @@ -1007,6 +1021,7 @@ namespace ps2recomp func.isSkipped = false; m_extraFunctions.push_back(std::move(func)); + mapStarts.insert(start); count++; } catch (...) @@ -1017,6 +1032,8 @@ namespace ps2recomp if (count > 0) { + m_hasLoadedGhidraMap = true; + m_ghidraMapStarts = mapStarts; std::cout << "Loaded " << count << " functions from Ghidra map" << std::endl; if (skippedNonExecutable > 0) { @@ -1029,9 +1046,36 @@ namespace ps2recomp << " Ghidra function(s) with invalid ranges after section clamping." << std::endl; } + m_extraFunctions.erase( + std::remove_if(m_extraFunctions.begin(), m_extraFunctions.end(), + [&](const Function &func) + { + return IsAutoGeneratedName(func.name) && !mapStarts.contains(func.start); + }), + m_extraFunctions.end()); + std::sort(m_extraFunctions.begin(), m_extraFunctions.end(), [](const Function &a, const Function &b) - { return a.start < b.start; }); + { + if (a.start != b.start) + { + return a.start < b.start; + } + + const bool aAuto = IsAutoGeneratedName(a.name); + const bool bAuto = IsAutoGeneratedName(b.name); + if (aAuto != bAuto) + { + return !aAuto; + } + + if (a.end != b.end) + { + return a.end > b.end; + } + + return a.name < b.name; + }); m_extraFunctions.erase( std::unique(m_extraFunctions.begin(), m_extraFunctions.end(), @@ -1267,6 +1311,8 @@ namespace ps2recomp void ElfParser::loadDebugFunctions() { m_extraFunctions.clear(); + m_hasLoadedGhidraMap = false; + m_ghidraMapStarts.clear(); if (HasDwarfSections(*m_elf)) { diff --git a/ps2xTest/src/ps2_recompiler_tests.cpp b/ps2xTest/src/ps2_recompiler_tests.cpp index d2fece9..a218622 100644 --- a/ps2xTest/src/ps2_recompiler_tests.cpp +++ b/ps2xTest/src/ps2_recompiler_tests.cpp @@ -6,6 +6,7 @@ #include "ps2recomp/types.h" #include #include +#include #include #include #include @@ -118,6 +119,41 @@ static bool writeMinimalMipsElfWithCodeAndDataFunctionSymbols(const std::filesys return writer.save(elfPath.string()); } +static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path &elfPath) +{ + ELFIO::elfio writer; + writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB); + writer.set_os_abi(ELFIO::ELFOSABI_NONE); + writer.set_type(ELFIO::ET_EXEC); + writer.set_machine(ELFIO::EM_MIPS); + writer.set_entry(0x00100000u); + + ELFIO::section *text = writer.sections.add(".text"); + text->set_type(ELFIO::SHT_PROGBITS); + text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR); + text->set_addr_align(4); + text->set_address(0x00100000u); + + const std::array textWords = { + 0x0C040004u, // jal 0x00100010 + 0x00000000u, // nop + 0x03E00008u, // jr $ra + 0x00000000u, // nop + 0x03E00008u, // jr $ra + 0x00000000u // nop + }; + text->set_data(reinterpret_cast(textWords.data()), + static_cast(textWords.size() * sizeof(uint32_t))); + + ELFIO::segment *textSegment = writer.segments.add(); + textSegment->set_type(ELFIO::PT_LOAD); + textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X); + textSegment->set_align(0x1000); + textSegment->add_section_index(text->get_index(), text->get_addr_align()); + + return writer.save(elfPath.string()); +} + void register_ps2_recompiler_tests() { MiniTest::Case("PS2Recompiler", [](TestCase &tc) @@ -515,6 +551,77 @@ void register_ps2_recompiler_tests() std::filesystem::remove(elfPath, removeError); }); + tc.Run("ghidra map replaces JAL fallback-only auto starts", [](TestCase &t) { + const auto uniqueSuffix = std::to_string( + static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + const std::filesystem::path elfPath = + std::filesystem::temp_directory_path() / ("ps2recomp-ghidra-merge-" + uniqueSuffix + ".elf"); + const std::filesystem::path mapPath = + std::filesystem::temp_directory_path() / ("ps2recomp-ghidra-merge-" + uniqueSuffix + ".csv"); + + const bool writeOk = writeMinimalMipsElfWithJalFallbackTarget(elfPath); + t.IsTrue(writeOk, "temporary ELF should be generated"); + if (!writeOk) + { + return; + } + + ElfParser parser(elfPath.string()); + const bool parseOk = parser.parse(); + t.IsTrue(parseOk, "generated ELF should parse"); + if (!parseOk) + { + std::error_code removeError; + std::filesystem::remove(elfPath, removeError); + return; + } + + const auto fallbackExtras = parser.extractExtraFunctions(); + const bool hasFallbackStart = std::any_of( + fallbackExtras.begin(), fallbackExtras.end(), + [](const Function &fn) + { return fn.start == 0x00100010u; }); + t.IsTrue(hasFallbackStart, "JAL fallback should discover secondary start before map load"); + + std::ofstream mapFile(mapPath); + t.IsTrue(static_cast(mapFile), "ghidra map file should be writable"); + if (!mapFile) + { + std::error_code removeError; + std::filesystem::remove(elfPath, removeError); + return; + } + mapFile << "name,start,end,size\n"; + mapFile << "FUN_00100000,0x00100000,0x00100010,0x10\n"; + mapFile.close(); + + const bool mapLoaded = parser.loadGhidraFunctionMap(mapPath.string()); + t.IsTrue(mapLoaded, "ghidra map should load"); + + const auto functions = parser.extractFunctions(); + const auto entryIt = std::find_if( + functions.begin(), functions.end(), + [](const Function &fn) + { return fn.start == 0x00100000u; }); + t.IsTrue(entryIt != functions.end(), "ghidra entry should exist"); + if (entryIt != functions.end()) + { + t.Equals(entryIt->name, std::string("FUN_00100000"), + "ghidra name should win over fallback auto-name"); + } + + const bool stillHasFallbackOnlyStart = std::any_of( + functions.begin(), functions.end(), + [](const Function &fn) + { return fn.start == 0x00100010u; }); + t.IsFalse(stillHasFallbackOnlyStart, + "fallback-only function starts should be removed once ghidra map is loaded"); + + std::error_code removeError; + std::filesystem::remove(elfPath, removeError); + std::filesystem::remove(mapPath, removeError); + }); + tc.Run("respect max length for .cpp filenames", [](TestCase& t) { t.IsTrue(PS2Recompiler::ClampFilenameLength("ReallyLongFunctionNameReallyLongFunctionNameReallyLongFunctionName_0x12345678",".cpp",50).length() <= 50,"Function name must be max 50 characters");