diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index b28654a3c..e670c9980 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -441,6 +441,8 @@ if(BUILD_TESTING) add_test(NAME windows_runpath COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_runpath.py $) set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) + add_test(NAME windows_trace_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 91586ff4e..0038cd80b 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -24,6 +24,7 @@ struct Args { bool windowsDiagnostics = false; bool windowsGui = false; std::optional windowsFixedBase; + bool traceImports = false; std::uint32_t unusedFilterLevel = 0; std::string inputPath; std::string outputPath; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 267cf0ce5..fadcbcb36 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -10,7 +10,7 @@ namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--windows-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf"; } @@ -86,6 +86,8 @@ Args ParseArgs(int argc, char* argv[]) { args.windowsDiagnostics = true; } else if (arg == "--windows-gui") { args.windowsGui = true; + } else if (arg == "--trace-imports") { + args.traceImports = true; } else if (arg.rfind("--", 0) == 0 || arg == "unused-filter") { throw std::runtime_error("unknown option: " + arg); } else if (arg.empty()) { @@ -124,6 +126,10 @@ Args ParseArgs(int argc, char* argv[]) { if (args.windowsFixedBase && !args.toWindows) throw std::runtime_error("--windows-fixed-base requires --windows"); + if (args.traceImports && !args.toWindows) + throw std::runtime_error("--trace-imports requires --windows"); + if (args.traceImports && args.lazyBinding) + throw std::runtime_error("--trace-imports conflicts with --lazy-binding"); if (!args.showHelp && (args.inputPath.empty() || args.outputPath.empty())) throw std::runtime_error(Usage()); diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index b7e81a307..5f1e25c73 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -14,6 +14,7 @@ struct GuestImport { std::uint64_t Addend; std::uint32_t RelocationType = 1; std::string Library; + bool Function = false; }; struct GuestRuntime { diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index 4e4cad6e5..31405091c 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -62,6 +62,7 @@ struct NidReference { VirtualAddress RelocationAddress; std::int64_t Addend; bool Weak = false; + bool Function = false; }; struct CallSiteInfo { @@ -98,6 +99,7 @@ struct SysVDynamicSection { std::vector RelaPltData; std::vector GuestModules; std::map ImportModules; + std::set FunctionImports; std::set UnmatchedImportModules; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index 8544e8e85..c9918ebb5 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -11,7 +11,7 @@ namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false); const std::string& GetDiagnostics() const; std::vector Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; @@ -23,6 +23,7 @@ private: std::string diagnostics; std::filesystem::path iconName; std::optional fixedBase; + bool traceImports; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp index 94f47c6a3..7a1968e9e 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp @@ -20,7 +20,7 @@ struct WindowsEntryStub { class WindowsEntryStubBuilder { public: - WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& guestModules = {}, const std::set& unmatchedImportModules = {}) const; + WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& guestModules = {}, const std::set& unmatchedImportModules = {}, bool traceImports = false) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index e20f23b1d..237b762c6 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -36,6 +36,7 @@ struct PeImport { std::uint32_t RelocationType = 1; std::string Library; bool Weak = false; + bool Function = false; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 058f64d14..e7b753b72 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -66,7 +67,7 @@ std::u16string toUtf16(const std::string& text) { } -WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& guestModules, const std::set& unmatchedImportModules) const { +WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& guestModules, const std::set& unmatchedImportModules, const bool traceImports) const { if (!imports.empty() && libraries.empty()) throw Domain::RelinkerException("ELF imports have no DT_NEEDED libraries"); const auto path = normalizeRunPath(runPath); @@ -187,6 +188,24 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con if (unmatched.contains(stem(libraries[index]))) imported[index] = true; } + std::optional traceProvider; + std::map traceLibraries; + std::uint32_t traceSymbol = 0; + std::uint32_t traceFunction = 0; + if (traceImports) { + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (libraries[index] == "libc.prx") traceProvider = index; + } + if (!traceProvider.has_value()) + throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); + imported[*traceProvider] = true; + traceSymbol = addString("TraceImport_nid_no_patch"); + traceFunction = reserve(8); + for (const auto& import : imports) { + if (!traceLibraries.contains(import.Library)) traceLibraries.emplace(import.Library, addString(import.Library)); + } + } + const auto lastError = reserve(4); const auto errorDigits = reserve(11); const auto errorMessage = reserve(ErrorMessageCapacity); @@ -215,6 +234,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con errors.push_back("FAIL: cannot prepare command-line arguments\n"); const auto directoryError = errors.size(); errors.push_back("FAIL: cannot make the executable directory the current directory\n"); + const auto traceError = errors.size(); + errors.push_back("FAIL: libc.prx does not export TraceImport_nid_no_patch\n"); std::vector errorRvas; for (const auto& error : errors) errorRvas.push_back(addString(error)); @@ -485,6 +506,21 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } } + if (traceImports) { + code.Rip({0x48, 0x8b, 0x0d}, CheckedRva(handles + *traceProvider * 8)); + code.Rip({0x48, 0x8d, 0x15}, traceSymbol); + call("GetProcAddress"); + requireNonzero(traceError, 0xc0000139u); + code.Rip({0x48, 0x89, 0x05}, traceFunction); + } + const auto trace = [&](const PeImport& import, const std::uint32_t symbolName) { + if (!traceImports || !import.Function || import.Addend != 0) return; + code.Emit({0x48, 0x89, 0xc1}); + code.Rip({0x48, 0x8d, 0x15}, symbolName); + code.Rip({0x4c, 0x8d, 0x05}, traceLibraries.at(import.Library)); + code.Rip({0xff, 0x15}, traceFunction); + }; + std::vector unresolvedBranches; std::vector lazyUnresolvedImports; std::vector tlsResolverAddresses; @@ -546,6 +582,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.U64(imports[index].Addend); code.Emit({0x48, 0x01, 0xd0}); } + trace(imports[index], symbolNames[index]); guestStartup.WriteImport(code, imports[index], handles); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index bcc3469dd..2e621c7cb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -70,7 +70,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } else { if (symbol.Name.empty()) throw Domain::RelinkerException("Empty guest import", target); image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, addend, 1, symbol.Library}); + runtime.Imports.push_back({symbol.Name, rva, addend, 1, symbol.Library, type == 7 || (type == 6 && (symbol.Info & 15) == 2)}); } } else throw Domain::RelinkerException("Unsupported Windows guest relocation " + std::to_string(type), target); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 562343c94..2b4faa6bf 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -28,7 +28,7 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed Windows guest image base"); } @@ -81,13 +81,16 @@ std::vector WindowsPePatcher::Patch(const std::vector(index), import.RelocationType, import.Library}); + for (const auto& import : module.Imports) { + relocations.Imports.push_back({import.Name, import.TargetRva, import.Addend, static_cast(index), import.RelocationType, import.Library}); + relocations.Imports.back().Function = import.Function; + } } libraries.insert(libraries.begin(), guestPaths.begin(), guestPaths.end()); if (dependencyDiagnostics) for (const auto& import : relocations.Imports) diagnostics += import.Name + '\n'; report(8500); - auto entry = WindowsEntryStubBuilder().Build(nextRva, image.GetEntryRva(), nativeImports, libraries, relocations.Imports, runPath, lazyBinding, dependencyDiagnostics, dynamicSection.GuestModules, dynamicSection.UnmatchedImportModules); + auto entry = WindowsEntryStubBuilder().Build(nextRva, image.GetEntryRva(), nativeImports, libraries, relocations.Imports, runPath, lazyBinding, dependencyDiagnostics, dynamicSection.GuestModules, dynamicSection.UnmatchedImportModules, traceImports); report(9000); directories[3] = entry.ExceptionDirectory; const auto entryRva = entry.Code.Rva; diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index 0d4efd25b..f1b5036de 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -51,6 +51,7 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); result.Imports.back().Weak = (dynamicSection.DynSymData[symbolOffset + 4] >> 4) == STB_WEAK; + result.Imports.back().Function = type == 7 || (type == 6 && dynamicSection.FunctionImports.contains(target)); if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) result.Imports.back().Library = module->second; } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index c8c7adb8f..95652550c 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -57,6 +57,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, options.LazyBinding = args.lazyBinding; options.RunPath = args.runPath; options.WindowsGui = args.windowsGui; + options.TraceImports = args.traceImports; options.WindowsFixedBase = mainExecutable ? args.windowsFixedBase : std::nullopt; options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable; options.ExcludedModules = args.excludedSceModules; diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index 4097ed184..d6b82a226 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -18,6 +18,7 @@ struct ConversionOptions { bool TrapSdkSyscallStubs = false; bool LazyBinding = false; bool WindowsGui = false; + bool TraceImports = false; std::optional WindowsFixedBase; bool WindowsDiagnostics = false; std::string RunPath = "$ORIGIN/libs"; diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index c65cfeac1..65441ef8c 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -94,6 +94,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const auto& ref : nidReferences) { if (!ref.Library.empty()) result.ImportModules.emplace(ref.RelocationAddress, ref.Library); + if (ref.Function) result.FunctionImports.insert(ref.RelocationAddress); std::uint32_t relType = ref.RelocationTypeValue; if (relType == 0) relType = R_X86_64_JUMP_SLOT; diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 5d6b03824..39582c496 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -102,7 +102,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (macos) { patcher = std::make_shared(); } else if (windows) { - patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase); + patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports); } else { patcher = std::make_shared( std::make_shared(), diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2baf1b713..bd9808804 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -264,7 +264,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf || std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), [](const std::uint8_t byte) { return byte != 0; }))) throw RelinkerException("Unnamed import symbol is not a null symbol entry", symOff); - nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend, (raw[symOff + 4] >> 4) == STB_WEAK}); + const bool weak = (raw[symOff + 4] >> 4) == STB_WEAK; + const bool function = (raw[symOff + 4] & 15) == STT_FUNC; + nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend, weak, function}); } }; diff --git a/core/relinker/relinker/tests/test_windows_trace_imports.py b/core/relinker/relinker/tests/test_windows_trace_imports.py new file mode 100644 index 000000000..41a3e42c6 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_trace_imports.py @@ -0,0 +1,93 @@ +"""--trace-imports passes each resolved function import through libc.prx's TraceImport_nid_no_patch.""" + +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_windows_import_modules import dynamic, executable, provider + + +HOOK = (b'\x80\x3a\x73\x75\x0e' + b'\x41\x80\x38\x61\x75\x08' + b'\x48\x8d\x05\x05\x00\x00\x00\xc3' + b'\x48\x89\xc8\xc3' + b'\xb8\x4d\x00\x00\x00\xc3') + + +def named_provider(symbol, soname, code): + image = provider(0, code=code) + strings = b'\0' + symbol.encode() + b'#A#B\0' + soname.encode() + b'\0' + image[0x800:0x800 + len(strings)] = strings + tags = [(5, 0x2200), (10, len(strings)), (6, 0x2280), (11, 24), + (4, 0x2240), (7, 0x2300), (8, 0), (9, 24), (14, len(symbol) + 6)] + dynamic(image, 0x600, 176, tags) + return image + + +def relink(relinker, source, output, *options): + return subprocess.run([str(relinker), '--windows', *options, str(source), str(output)], + capture_output=True, text=True, timeout=30) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-trace-imports-') as directory: + work = Path(directory) + build = work / 'providers' + (build / 'prx').mkdir(parents=True) + (build / 'prx' / 'a.prx').write_bytes(provider(11)) + (build / 'prx' / 'hook.prx').write_bytes(named_provider('TraceImport_nid_no_patch', 'hook.prx', HOOK)) + (build / 'prx' / 'plain.prx').write_bytes(named_provider('Unrelated', 'plain.prx', b'\x31\xc0\xc3')) + source = build / 'input.elf' + source.write_bytes(executable('a.prx')) + result = relink(relinker, source, build / 'output.exe') + assert result.returncode == 0, (result.stdout, result.stderr) + converted = {name: (build / 'app0' / 'prx' / (name + '.prx.guest.prx')).read_bytes() for name in ('a', 'hook', 'plain')} + + for name, options, libc, symbol_info, expected, message in ( + ('untraced', (), 'hook', 0x12, 11, None), + ('traced', ('--trace-imports',), 'hook', 0x12, 77, None), + ('data', ('--trace-imports',), 'hook', 0x11, 11, None), + ('weak', ('--trace-imports',), 'hook', 0x22, 11, None), + ('unsupported', ('--trace-imports',), 'plain', 0x12, None, 'libc.prx does not export TraceImport_nid_no_patch')): + case = work / name + (case / 'libs').mkdir(parents=True) + (case / 'prx').mkdir() + (case / 'libs' / 'a.prx').write_bytes(converted['a']) + (case / 'libs' / 'libc.prx').write_bytes(converted[libc]) + source = case / 'input.elf' + image = executable('a.prx', extra_dependencies=('libc.prx',)) + image[0x4898 + 4] = symbol_info + source.write_bytes(image) + output = case / 'output.exe' + result = relink(relinker, source, output, *options) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if os.name != 'nt': + continue + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + if expected is None: + assert run.returncode != 0 and message in run.stderr, (name, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + case = work / 'without-libc' + (case / 'prx').mkdir(parents=True) + source = case / 'input.elf' + source.write_bytes(executable('a.prx')) + result = relink(relinker, source, case / 'output.exe', '--trace-imports') + assert result.returncode == 2 and '--trace-imports requires libc.prx' in result.stderr, (result.returncode, result.stderr) + assert not (case / 'output.exe').exists() + + result = subprocess.run([str(relinker), '--trace-imports', str(source), str(case / 'output.elf')], + capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and '--trace-imports requires --windows' in result.stderr, (result.returncode, result.stderr) + result = relink(relinker, source, case / 'output.exe', '--trace-imports', '--lazy-binding') + assert result.returncode != 0 and '--trace-imports conflicts with --lazy-binding' in result.stderr, (result.returncode, result.stderr) + + print('Windows import tracing tests passed') + + +if __name__ == '__main__': + main() diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 166ff18ab..124436d2a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -65,3 +65,4 @@ - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The host's [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) locks also restrict ordinary reads and writes through other handles, unlike FreeBSD's advisory locks, and do not restrict mapped views. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred +- [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 8a8ca0a1b..a77abb414 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -13,3 +13,4 @@ - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE but reports none of them through CPUID, so code that checks CPUID takes its baseline path. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests; that the converted code runs under Rosetta was checked on an M5 Max only, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). +- `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 527742a69..4e75ad555 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -51,6 +51,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | | `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | +| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--lazy-binding`. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | | `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | | `unused-filter=0` | Keep all imported NID references. | @@ -157,6 +158,19 @@ Do not disable Dozen if it is the only Vulkan driver available; the application GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. +### Tracing system library calls + +Windows only. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls: sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. + +``` +last traced system library calls (APS5_TRACE_CALLS): + #1 thread 9384 libkernel.prx k+AXqu2-eBc from 0x0000000140010006 app.exe+0x10006 +most called traced imports (1 calls in total): + 1 libkernel.prx k+AXqu2-eBc +``` + +Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. + ## Exit codes `0`: conversion succeeded or help was displayed. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code.