Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/relinker/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_symbol_names.py $<TARGET_FILE:relinker>)
add_test(NAME guest_registry COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_registry.py $<TARGET_FILE:relinker>)
add_test(NAME needed_libraries COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_needed_libraries.py $<TARGET_FILE:relinker>)
set_tests_properties(needed_libraries PROPERTIES TIMEOUT 30)
add_test(NAME macos_output COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_output.py $<TARGET_FILE:relinker>)
add_test(NAME guest_host_libc COMMAND ${Python3_EXECUTABLE}
Expand Down
1 change: 1 addition & 0 deletions core/relinker/domain/include/domain/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ struct SysVDynamicSection {
std::map<VirtualAddress, std::string> ImportModules;
std::set<VirtualAddress> FunctionImports;
std::set<std::string> UnmatchedImportModules;
std::vector<std::string> NeededLibraries;
};

struct CallRegistryEntry {
Expand Down
15 changes: 13 additions & 2 deletions core/relinker/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ int Run(const int argc, char* argv[]) {
checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics");

std::vector<std::filesystem::path> registryPaths;
std::vector<std::filesystem::path> neededPaths;
if (args.writeRegistry) {
for (const auto& executable : executables) {
const std::filesystem::path outFsPath = Io::NativePath(executable.OutputPath);
Expand All @@ -274,6 +275,12 @@ int Run(const int argc, char* argv[]) {
for (const auto& artifact : guestArtifacts)
registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json"));
for (const auto& path : registryPaths) checkOutputInputs(path, "Registry");
for (const auto& path : registryPaths) {
auto name = Io::Utf8Path(path.filename());
name.resize(name.size() - std::string(".registry.json").size());
neededPaths.push_back(path.parent_path() / Io::NativePath(name + ".needed.json"));
}
for (const auto& path : neededPaths) checkOutputInputs(path, "Needed libraries");
}

checkOutputInputs(Io::NativePath(absPath), "Executable");
Expand Down Expand Up @@ -312,10 +319,14 @@ int Run(const int argc, char* argv[]) {
}
if (args.writeRegistry) {
const auto registryWriter = std::make_shared<Relinker::CallRegistryWriter>();
for (std::size_t index = 0; index < executables.size(); ++index)
for (std::size_t index = 0; index < executables.size(); ++index) {
fileWriter.Write(Io::Utf8Path(registryPaths[index]), registryWriter->WriteCallRegistry(executables[index].Result.RegistryEntries));
for (std::size_t index = 0; index < guestArtifacts.size(); ++index)
fileWriter.Write(Io::Utf8Path(neededPaths[index]), registryWriter->WriteNeededLibraries(executables[index].Result.DynamicSection.NeededLibraries));
}
for (std::size_t index = 0; index < guestArtifacts.size(); ++index) {
fileWriter.Write(Io::Utf8Path(registryPaths[executables.size() + index]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports));
fileWriter.Write(Io::Utf8Path(neededPaths[executables.size() + index]), registryWriter->WriteNeededLibraries(guestArtifacts[index].NeededLibraries));
}
}
progress.Finish();
if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ICallRegistryWriter {

virtual std::string WriteCallRegistry(const std::vector<CallRegistryEntry>& entries) = 0;
virtual std::string WriteModuleImports(const std::vector<CallRegistryEntry>& entries) = 0;
virtual std::string WriteNeededLibraries(const std::vector<std::string>& libraries) = 0;
};

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ struct GuestArtifact {
std::vector<std::uint8_t> Bytes;
std::vector<Domain::CallRegistryEntry> Imports;
std::filesystem::path SourcePath;
std::vector<std::string> NeededLibraries;
};

struct GuestModuleInput {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class CallRegistryWriter : public ICallRegistryWriter {
public:
std::string WriteCallRegistry(const std::vector<CallRegistryEntry>& entries) override;
std::string WriteModuleImports(const std::vector<CallRegistryEntry>& entries) override;
std::string WriteNeededLibraries(const std::vector<std::string>& libraries) override;
};

}
Expand Down
6 changes: 5 additions & 1 deletion core/relinker/relinker/src/guest/GuestModuleBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
for (const auto& name : neededNames) addHost(name);
for (const auto& image : images) for (const auto& dependency : image.Dependencies) addHost(dependency);
if (uniqueHosts.contains("libSceLibcInternal.prx") && uniqueHosts.insert("libc.prx").second) hostLibraries.push_back("libc.prx");
dynamic.NeededLibraries = hostLibraries;
dynamic.DynamicSegmentData.clear();
const auto addNeeded = [&](const std::string& name) {
Io::AppendU64(dynamic.DynamicSegmentData, 1);
Expand Down Expand Up @@ -356,7 +357,10 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
}
}
dynamic.GuestModules.push_back(std::move(runtime));
artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath});
std::vector<std::string> neededLibraries;
for (const auto& name : image.Dependencies)
if (findGuest(name) == guestNames.end()) neededLibraries.push_back(name);
artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath, std::move(neededLibraries)});
progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()});
}
return artifacts;
Expand Down
12 changes: 12 additions & 0 deletions core/relinker/relinker/src/output/CallRegistryWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,16 @@ std::string CallRegistryWriter::WriteModuleImports(const std::vector<CallRegistr
return out.str();
}

std::string CallRegistryWriter::WriteNeededLibraries(const std::vector<std::string>& libraries) {
std::ostringstream out;
out << "[\n";
for (std::size_t index = 0; index < libraries.size(); ++index) {
out << " " << _jsonString(libraries[index]);
if (index + 1 < libraries.size()) out << ",";
out << "\n";
}
out << "]\n";
return out.str();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection(
std::uint32_t originalJmprelCount)
{
SysVDynamicSection result;
result.NeededLibraries = neededLibraries;
result.DynStrData.push_back(0);

std::vector<std::uint32_t> neededOffsets;
Expand Down
3 changes: 3 additions & 0 deletions core/relinker/relinker/tests/test_call_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def main():
"callSites": ["0x200"], "callSitesResolved": True, "weak": False}]
if entries != expected:
raise AssertionError((name, options, entries))
needed = json.loads(output.with_suffix(".needed.json").read_text(encoding="utf-8"))
if needed != ["lib.so"]:
raise AssertionError((name, options, needed))
strings = b"\0lib.so\0" + names[0].encode("utf-8") + b"\0"
image = fixture(str_size=len(strings), table_bytes=strings)
struct.pack_into("<H", image, 56, 6)
Expand Down
3 changes: 3 additions & 0 deletions core/relinker/relinker/tests/test_guest_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def main():
assert libc == [entry("CCCCCCCCCCC", "0x2388"), entry("CCCCCCCCCCC", "0x2390")], (name, libc)
other = read(output.parent / "output.other.prx.guest.prx.registry.json")
assert other == [], (name, other)
assert read(output.with_suffix(".needed.json")) == []
assert read(output.parent / "output.libc.prx.guest.prx.needed.json") == []
main_nids = [item["nid"] for item in read(output.parent / "output.registry.json")]
expected = [] if options else ["AAAAAAAAAAA#A#A", "DDDDDDDDDDD#D#D"]
assert main_nids == expected, (name, main_nids)
Expand All @@ -70,6 +72,7 @@ def main():
plain = convert(relinker, work / (name + "-plain"), options, registry=False)
written = sorted(path.name for path in plain.parent.glob("*.registry.json"))
assert written == [], (name, written)
assert list(plain.parent.glob("*.needed.json")) == [], name
print("Guest module registry tests passed")


Expand Down
14 changes: 9 additions & 5 deletions core/relinker/relinker/tests/test_input_preservation.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,22 @@ def check_diagnostic_inputs(relinker, work):
assert diagnostics.read_bytes() == (b"" if enabled else original)


def check_registry_inputs(relinker, work, windows):
def check_registry_inputs(relinker, work, windows, needed=False):
options = ["--windows"] if windows else []
for module_registry in (False, True):
for bundled in (False, True):
for alias in ("direct", "hardlink", "symlink"):
if bundled and alias == "direct":
continue
case = work / f"registry-{windows}-{module_registry}-{bundled}-{alias}"
case = work / f"registry-{windows}-{module_registry}-{bundled}-{alias}-{needed}"
modules = case / "sce_module"
modules.mkdir(parents=True)
module = modules / "sample.prx"
module_original = bytes(guest_fixture(PLAIN_SITE))
module.write_bytes(module_original)
output = case / ("output.exe" if windows else "output.elf")
registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"]
suffix = ".needed.json" if needed else ".registry.json"
registries = [case / ("output" + suffix), case / ("output.sample.prx.guest.prx" + suffix)]
registry = registries[int(module_registry)]
source = registry if alias == "direct" else case / "input.elf"
original = bytes(main_fixture())
Expand All @@ -111,7 +112,8 @@ def check_registry_inputs(relinker, work, windows):
output.write_bytes(previous_output)
result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)],
capture_output=True, text=True, timeout=20)
expected = "Registry output would overwrite an input module" if bundled else "Registry output would overwrite the input executable"
kind = "Needed libraries" if needed else "Registry"
expected = f"{kind} output would overwrite an input module" if bundled else f"{kind} output would overwrite the input executable"
assert result.returncode == 2 and expected in result.stderr, (result.returncode, result.stdout, result.stderr)
assert source.read_bytes() == original and module.read_bytes() == module_original
assert output.read_bytes() == previous_output and untouched.read_bytes() == previous_registry
Expand All @@ -130,7 +132,8 @@ def check_registry_after_failed_patch(relinker, work):
output = case / "output.exe"
previous_output = b"previous executable"
output.write_bytes(previous_output)
registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"]
registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json",
case / "output.needed.json", case / "output.sample.prx.guest.prx.needed.json"]
previous_registry = b"previous registry"
for registry in registries:
registry.write_bytes(previous_registry)
Expand Down Expand Up @@ -228,6 +231,7 @@ def main():
for windows in (False, True):
options = ["--windows"] if windows else []
check_registry_inputs(relinker, work, windows)
check_registry_inputs(relinker, work, windows, needed=True)
for bundled in (False, True):
for alias in ("direct", "normalized", "hardlink", "symlink"):
case = work / f"{windows}-{bundled}-{alias}"
Expand Down
92 changes: 92 additions & 0 deletions core/relinker/relinker/tests/test_needed_libraries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import json
from pathlib import Path
import struct
import subprocess
import sys
import tempfile

from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture
from test_guest_symbol_names import importing_executable

sys.path.insert(0, str(Path(__file__).resolve().parents[4] / "tools"))
import import_audit


def named_module(tag):
image = guest_fixture(PLAIN_SITE)
strings = b"\0Party.prx\0"
image[0x800:0x800 + len(strings)] = strings
struct.pack_into("<Q", image, 0x618, len(strings))
struct.pack_into("<qQqQ", image, 0x680, tag, 1, 0, 0)
struct.pack_into("<QQ", image, 208, 10 * 16, 10 * 16)
return image


def executable_with_dependencies(libraries):
image = importing_executable([], modules=libraries)
dynamic_size, = struct.unpack_from("<Q", image, 152)
struct.pack_into("<qQqQ", image, 0x4800 + dynamic_size - 16, 0x6100003F, 24, 0, 0)
struct.pack_into("<QQ", image, 152, dynamic_size + 16, dynamic_size + 16)
return image


def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-needed-libraries-") as directory:
work = Path(directory)
for windows in (False, True):
for bundled in (False, True):
case = work / f"{windows}-{bundled}"
modules = case / "sce_module"
modules.mkdir(parents=True)
source = case / "input.elf"
source.write_bytes(executable_with_dependencies([] if bundled else ["Party"]))
if bundled:
(modules / "engine.prx").write_bytes(named_module(1))
output = case / ("output.exe" if windows else "output.elf")
result = subprocess.run([str(relinker), "--registry", "unused-filter=2",
*(["--windows"] if windows else []), str(source), str(output)],
capture_output=True, text=True, timeout=20)
assert result.returncode == 0, (windows, bundled, result.stdout, result.stderr)
registry = case / ("output.engine.prx.guest.prx.registry.json" if bundled else "output.registry.json")
assert json.loads(registry.read_text()) == [], (windows, bundled)
needed = import_audit.read_needed_registry(import_audit.needed_registry_path(registry))
assert needed == ["Party.prx"], (windows, bundled, needed)
records, missing = import_audit.audit(import_audit.read_registry(registry), {}, {}, set(), set(), needed_libraries=needed)
assert records == [] and missing == {"Party.prx": 0}, (records, missing)
(modules / "provider.prx").write_bytes(named_module(14))
result = subprocess.run([str(relinker), "--registry", *(["--windows"] if windows else []),
str(source), str(output)], capture_output=True, text=True, timeout=20)
assert result.returncode == 0, (windows, bundled, result.stdout, result.stderr)
needed = import_audit.read_needed_registry(import_audit.needed_registry_path(registry))
assert needed == [], (windows, bundled, needed)
assert import_audit.audit([], {}, {}, set(), set(), needed_libraries=needed) == ([], {})
for windows in (False, True):
case = work / f"executables-{windows}"
picker = case / "picker" / "picker.bin"
picker.parent.mkdir(parents=True)
modules = case / "sce_module"
modules.mkdir()
picker.write_bytes(executable_with_dependencies(["Party"]))
source = case / "input.elf"
source.write_bytes(executable_with_dependencies([]))
output = case / ("output.exe" if windows else "output.elf")
command = [str(relinker), "--registry", "--executable", "picker/picker.bin",
*(["--windows"] if windows else []), str(source), str(output)]
result = subprocess.run(command, capture_output=True, text=True, timeout=20)
assert result.returncode == 0, (windows, result.stdout, result.stderr)
main_needed = case / "output.needed.json"
picker_needed = case / "picker.picker.bin.needed.json"
assert import_audit.read_needed_registry(main_needed) == []
assert import_audit.read_needed_registry(picker_needed) == ["Party.prx"]
(modules / "provider.prx").write_bytes(named_module(14))
result = subprocess.run(command, capture_output=True, text=True, timeout=20)
assert result.returncode == 0, (windows, result.stdout, result.stderr)
assert import_audit.read_needed_registry(main_needed) == []
assert import_audit.read_needed_registry(picker_needed) == []
assert import_audit.read_needed_registry(case / "output.provider.prx.guest.prx.needed.json") == []
print("Dependency-only library audit tests passed")


if __name__ == "__main__":
main()
4 changes: 3 additions & 1 deletion docs/user/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry.

Each `--modules` file is also audited itself: its undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries it needs must have a file in `--libs` or `--modules`. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table.

The report also lists needed libraries that have no file in `--libs` or `--modules`, and imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows.
Each registry has a companion with the same prefix and the suffix `.needed.json`, listing its host library dependencies, including declared `DT_NEEDED` entries with no imports. Dependencies supplied by converted bundled modules, including their aliases, are omitted. The executable's companion includes host dependencies of its bundled modules. The audit reads these companions automatically; keep them beside the registries. Older registries without companions still work, but cannot report dependencies that have no imports; regenerate them with `--registry` to include those dependencies.

The report also lists needed libraries that have no file in `--libs` or `--modules`, including dependencies with zero imports. These do not increase the import or reference counts. Windows can skip a missing dependency when no import needs it; Linux still needs the file. The audit also lists imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows.

| Option | Effect |
|--------|--------|
Expand Down
Loading
Loading