diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d227fc017d2c..eb485c46ccca 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -56,6 +56,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CodeInstructionCollector.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/InlineJumpTables.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SceCodeExtent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -588,6 +589,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_string_table_bounds.py $) add_test(NAME sce_code_extent COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_extent.py $) + add_test(NAME sce_inline_jump_tables COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_inline_jump_tables.py $) add_test(NAME sce_relro COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/include/relinker/analysis/InlineJumpTables.hpp b/core/relinker/relinker/include/relinker/analysis/InlineJumpTables.hpp new file mode 100644 index 000000000000..d185c86c7b77 --- /dev/null +++ b/core/relinker/relinker/include/relinker/analysis/InlineJumpTables.hpp @@ -0,0 +1,14 @@ +#ifndef RELINKER_ANALYSIS_INLINEJUMPTABLES_HPP +#define RELINKER_ANALYSIS_INLINEJUMPTABLES_HPP + +#include +#include +#include + +namespace Relinker { + +std::optional FindInlineJumpTables(std::span function, VirtualAddress begin); + +} + +#endif diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index c5ffda7b73f2..12bc453f2337 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -110,6 +111,11 @@ std::set CodeInstructionCollector::Collect(const std::ve } std::set roots; std::map functions; + std::map inlineData; + const auto isInlineData = [&](std::uint64_t address) { + const auto data = inlineData.upper_bound(address); + return data != inlineData.begin() && address < std::prev(data)->second; + }; const auto addRoot = [&](std::uint64_t address) { if (isCode(address)) roots.insert(address); }; const auto addAddressTakenRoot = [&](std::uint64_t address, const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { if (!info.HasRipRelativeDisp || code[info.OpcodeOffset] != 0x8d || !(info.RexPrefix & 8)) return; @@ -250,8 +256,13 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto& [begin, end] : functions) functionBytes += end - begin; std::uint64_t decodedBytes = 0; report(1000); - for (const auto& [begin, end] : functions) { - const auto offset = fileOffset(begin, end - begin); + for (const auto& [begin, functionEnd] : functions) { + const auto offset = fileOffset(begin, functionEnd - begin); + auto end = functionEnd; + if (const auto tables = FindInlineJumpTables(std::span(bytes.data() + offset, functionEnd - begin), begin)) { + end = *tables; + inlineData.emplace(end, functionEnd); + } for (auto address = begin; address < end;) { Codegen::DecodedInstructionInfo info; try { @@ -362,6 +373,12 @@ std::set CodeInstructionCollector::Collect(const std::ve std::size_t previousRoots = 0; std::unordered_set decodedInstructions; std::vector> sectionReachable(codeHeaders.size()); + for (std::size_t i = 0; i < codeHeaders.size(); ++i) { + for (const auto& [dataBegin, dataEnd] : inlineData) { + if (dataBegin < codeHeaders[i].MappedAddress || dataBegin - codeHeaders[i].MappedAddress >= codeHeaders[i].FileSize) continue; + for (auto slot = dataBegin; slot < dataEnd; slot += 4) sectionReachable[i].insert(slot); + } + } do { previousRoots = roots.size(); for (std::size_t i = 0; i < codeHeaders.size(); ++i) { @@ -374,7 +391,7 @@ std::set CodeInstructionCollector::Collect(const std::ve const std::span text(bytes.data() + header.Offset, header.FileSize); const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, true, sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }); for (const auto address : graph->ReachableVaddrs()) { - if (!decodedInstructions.insert(address).second) continue; + if (isInlineData(address) || !decodedInstructions.insert(address).second) continue; instructions.insert(address); const auto offset = address - header.MappedAddress; const auto info = decoder.DecodeInstruction(text.data() + offset, text.size() - offset); diff --git a/core/relinker/relinker/src/analysis/InlineJumpTables.cpp b/core/relinker/relinker/src/analysis/InlineJumpTables.cpp new file mode 100644 index 000000000000..f42aeebe8497 --- /dev/null +++ b/core/relinker/relinker/src/analysis/InlineJumpTables.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +namespace { + +std::int32_t readI32(std::span bytes, std::size_t offset) { + std::int32_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +bool holdsOnlyTables(std::span function, VirtualAddress begin, VirtualAddress first, const std::set& tables) { + const auto end = begin + function.size(); + if ((end - first) % 4 != 0) return false; + for (auto slot = first; slot < end; slot += 4) { + const auto base = *std::prev(tables.upper_bound(slot)); + const auto target = base + static_cast(static_cast(readI32(function, slot - begin))); + if (target < begin || target >= first) return false; + } + return true; +} + +} + +std::optional FindInlineJumpTables(std::span function, VirtualAddress begin) { + const Codegen::X64InstructionDecoder decoder; + const auto end = begin + function.size(); + std::map ripAddresses; + std::set tables; + for (auto address = begin; address < end;) { + while (!tables.empty() && *tables.begin() < address) tables.erase(tables.begin()); + if (!tables.empty() && address == *tables.begin() && holdsOnlyTables(function, begin, address, tables)) return address; + const auto* code = function.data() + (address - begin); + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(code, end - address); + } catch (const Codegen::CodegenException&) { + return std::nullopt; + } + if (info.Length == 0 || info.Length > end - address) return std::nullopt; + if (!info.IsTwoByteOpcode && info.HasModRm && info.Opcode == 0x8d && info.HasRipRelativeDisp && (info.RexPrefix & 8) != 0) { + const auto displacement = readI32(function, address - begin + info.RipRelativeDispOffset); + ripAddresses[static_cast(info.ModRmRegField | ((info.RexPrefix & 4) << 1))] = + address + info.Length + static_cast(static_cast(displacement)); + } else if (!info.IsTwoByteOpcode && info.HasModRm && (info.Opcode == 0x89 || info.Opcode == 0x8b) && (info.ModRmByte >> 6) == 3 && (info.RexPrefix & 8) != 0) { + const auto reg = static_cast(info.ModRmRegField | ((info.RexPrefix & 4) << 1)); + const auto rm = static_cast((info.ModRmByte & 7) | ((info.RexPrefix & 1) << 3)); + const auto destination = info.Opcode == 0x89 ? rm : reg; + const auto source = ripAddresses.find(info.Opcode == 0x89 ? reg : rm); + if (source != ripAddresses.end()) ripAddresses[destination] = source->second; + else ripAddresses.erase(destination); + } else if (!info.IsTwoByteOpcode && info.HasModRm && info.Opcode == 0x63 && (info.ModRmByte & 7) == 4 && (info.ModRmByte >> 6) <= 1 && info.OpcodeOffset + 2 < info.Length) { + const auto sib = code[info.OpcodeOffset + 2]; + const auto table = ripAddresses.find(static_cast((sib & 7) | ((info.RexPrefix & 1) << 3))); + if ((sib >> 6) == 2 && table != ripAddresses.end() && table->second > address && table->second < end) + tables.insert(table->second); + } + address += info.Length; + } + return std::nullopt; +} + +} diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 8e082aa3e90f..72321b9de533 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -361,6 +362,12 @@ RelinkResult RelinkerPipeline::Relink() { static_cast(segmentFileOffset); if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) segment.resize(*codeEnd - segmentVAddr); + for (const auto& function : functions) { + if (function.Begin < segmentVAddr || function.End <= function.Begin || function.End - segmentVAddr > segment.size()) continue; + const std::span code(segment.data() + (function.Begin - segmentVAddr), function.End - function.Begin); + if (const auto tables = FindInlineJumpTables(code, function.Begin)) + std::fill(code.begin() + static_cast(*tables - function.Begin), code.end(), std::uint8_t{0x90}); + } } } diff --git a/core/relinker/relinker/tests/test_sce_inline_jump_tables.py b/core/relinker/relinker/tests/test_sce_inline_jump_tables.py new file mode 100644 index 000000000000..f93448d0b382 --- /dev/null +++ b/core/relinker/relinker/tests/test_sce_inline_jump_tables.py @@ -0,0 +1,125 @@ +from pathlib import Path +import json +import struct +import subprocess +import sys +import tempfile + +PT_LOAD = 1 +PT_DYNAMIC = 2 +PT_GNU_EH_FRAME = 0x6474E550 +PT_SCE_DYNLIBDATA = 0x61000000 +FUNCTION = 0x1000 +FUNCTION_SIZE = 0x9000 +TABLE = FUNCTION + FUNCTION_SIZE - 8 +PLT = FUNCTION + FUNCTION_SIZE +GOT = 0xA00 +EH_FRAME_HEADER = 0x800 +EH_FRAME = 0x840 +UNSUPPORTED_TLS = bytes.fromhex("64 2d 65 6c 66 2e c3") +TABLE_ENTRIES = (-0x86F1, -0x9C) + + +def lea(opcode, address, target): + return bytes.fromhex(opcode) + struct.pack("