perf(relinker): index guest exports for dependency resolution

This commit is contained in:
Evgeniy Kazarin 2026-10-12 16:15:06 +03:00
parent 33ae282e4e
commit 33c955a8e3
3 changed files with 152 additions and 15 deletions

View File

@ -447,6 +447,9 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $<TARGET_FILE:relinker>)
add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $<TARGET_FILE:relinker>)
add_test(NAME guest_export_lookup COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_export_lookup.py $<TARGET_FILE:relinker>)
set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30)
add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $<TARGET_FILE:relinker>)
add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE}

View File

@ -16,6 +16,7 @@
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
@ -23,6 +24,11 @@ namespace Relinker {
namespace {
struct GuestExport {
std::size_t Provider;
std::uint8_t Type;
};
std::vector<std::string> ReadNeededNames(const Domain::SysVDynamicSection& dynamic) {
if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table");
std::vector<std::string> names;
@ -136,7 +142,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
if (std::filesystem::equivalent(path, outputPath))
throw Domain::RelinkerException("Executable output would overwrite an input module: " + Io::Utf8Path(outputPath));
}
std::map<std::string, std::vector<std::size_t>> exports;
std::unordered_map<std::string, std::vector<GuestExport>> exports;
std::map<std::string, std::set<std::size_t>> sharedExports;
std::set<std::string> outputNames;
for (const auto& path : paths) {
@ -153,10 +159,11 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
for (const auto& symbol : image.Symbols) {
if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue;
auto& providers = exports[symbol.Name];
const bool repeated = std::find(providers.begin(), providers.end(), images.size()) != providers.end();
if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front()].SourcePath));
if (!repeated) providers.push_back(images.size());
if (!windows && providers.size() > 1) sharedExports[symbol.Name].insert(providers.begin(), providers.end());
const bool repeated = !providers.empty() && providers.back().Provider == images.size();
if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath));
if (!repeated) providers.push_back({images.size(), static_cast<std::uint8_t>(symbol.Info & 15)});
if (!windows && providers.size() > 1)
for (const auto& provider : providers) sharedExports[symbol.Name].insert(provider.Provider);
}
images.push_back(std::move(image));
}
@ -264,20 +271,19 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
if (shared == sharedExports.end() || declared == guestNames.end() || !shared->second.contains(declared->second))
rejectSharedImport(symbol.Name, Io::Utf8Path(images[index].SourcePath));
const auto found = exports.find(symbol.Name);
std::vector<std::size_t> providers;
const GuestExport* provider = nullptr;
if (found != exports.end()) {
for (const auto provider : found->second) {
if (symbol.Library.empty() || (declared != guestNames.end() && declared->second == provider)) providers.push_back(provider);
for (const auto& candidate : found->second) {
if (!symbol.Library.empty() && (declared == guestNames.end() || declared->second != candidate.Provider)) continue;
if (provider != nullptr) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name);
provider = &candidate;
}
}
if (providers.size() > 1) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name);
if (!providers.empty()) {
const auto& provider = images[providers.front()];
const auto exported = std::find_if(provider.Symbols.begin(), provider.Symbols.end(), [&](const auto& candidate) { return candidate.Section != 0 && candidate.Section != AbsoluteSection && candidate.Name == symbol.Name && (candidate.Info >> 4) != 0 && candidate.Visibility != 1 && candidate.Visibility != 2; });
if (exported == provider.Symbols.end() || ((symbol.Info & 15) != 0 && (symbol.Info & 15) != (exported->Info & 15))) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name);
if (providers.front() != index) dependencies[index].insert(providers.front());
if (provider != nullptr) {
if ((symbol.Info & 15) != 0 && (symbol.Info & 15) != provider->Type) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name);
if (provider->Provider != index) dependencies[index].insert(provider->Provider);
} else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name);
if (providers.empty()) systemImports[index].insert(symbolIndex);
if (provider == nullptr) systemImports[index].insert(symbolIndex);
}
}
std::vector<bool> required(images.size());

View File

@ -0,0 +1,128 @@
from pathlib import Path
import os
import struct
import subprocess
import sys
import tempfile
from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture
from test_guest_module_directories import needed_libraries
from test_guest_symbol_names import imported_symbols, module_symbols
from test_windows_import_modules import executable
def guest_symbols(symbols, dependency=None):
image = guest_fixture(PLAIN_SITE)
image[0x400:0x406] = b'\xb8\x2a\x00\x00\x00\xc3'
strings = bytearray(b'\0')
table = bytearray(24)
for name, info, visibility, section in symbols:
offset = len(strings)
strings.extend(name.encode() + b'\0')
table.extend(struct.pack('<IBBHQQ', offset, info, visibility, section,
0x1000 if section else 0, 6 if section else 0))
dependency_offset = len(strings)
if dependency:
strings.extend(dependency.encode() + b'\0')
string_offset = 0x800
symbol_offset = (string_offset + len(strings) + 7) & ~7
hash_offset = symbol_offset + len(table)
end = hash_offset + 8 + 4 * (1 + len(table) // 24)
image.extend(bytes(max(0, end - len(image))))
image[string_offset:string_offset + len(strings)] = strings
image[symbol_offset:symbol_offset + len(table)] = table
image[hash_offset:end] = bytes(end - hash_offset)
struct.pack_into('<II', image, hash_offset, 1, len(table) // 24)
address = lambda offset: offset + 0x1a00
tags = [(5, address(string_offset)), (10, len(strings)),
(6, address(symbol_offset)), (11, 24), (4, address(hash_offset))]
if dependency:
tags.append((1, dependency_offset))
tags.append((0, 0))
image[0x600:0x800] = bytes(0x200)
for index, tag in enumerate(tags):
struct.pack_into('<qQ', image, 0x600 + index * 16, *tag)
struct.pack_into('<QQ', image, 120 + 32, end - 0x600, end - 0x600)
struct.pack_into('<QQ', image, 176 + 32, len(tags) * 16, len(tags) * 16)
return image
def large_modules(count):
names = [f'entry{index:06d}' for index in range(count)]
return {
'provider.prx': guest_symbols((name, 0x12, 0, 1) for name in names),
'consumer.prx': guest_symbols(((name, 0x12, 0, 0) for name in reversed(names)),
'provider.prx'),
}
def convert(relinker, case, windows, modules, source=None):
directory = case / 'sce_module'
directory.mkdir(parents=True)
for name, image in modules.items():
(directory / name).write_bytes(image)
input_path = case / 'input.elf'
input_path.write_bytes(main_fixture() if source is None else source)
output = case / ('output.exe' if windows else 'output.elf')
result = subprocess.run([str(relinker), *(['--windows'] if windows else []),
str(input_path), str(output)], capture_output=True,
text=True, timeout=30)
return result, output
def main():
relinker = Path(sys.argv[1]).resolve()
cases = 0
with tempfile.TemporaryDirectory(prefix='anyps5-export-lookup-') as directory:
work = Path(directory)
for windows in (False, True):
modules = large_modules(4096)
result, output = convert(relinker, work / f'large-{windows}', windows, modules,
executable('provider.prx', symbol='entry004095'))
assert result.returncode == 0, (result.stdout, result.stderr)
provider = output.parent / 'app0/sce_module/provider.prx.guest.prx'
consumer = output.parent / 'app0/sce_module/consumer.prx.guest.prx'
assert provider.is_file() and consumer.is_file()
if not windows:
names = {f'entry{index:06d}#guest' for index in range(4096)}
assert set(module_symbols(provider.read_bytes())) == names
assert set(module_symbols(consumer.read_bytes())) == names
assert needed_libraries(consumer.read_bytes()) == ['$ORIGIN/provider.prx.guest.prx']
assert imported_symbols(output.read_bytes()) == {'entry004095#guest'}
elif os.name == 'nt':
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
assert run.returncode == 42, (run.returncode, run.stdout, run.stderr)
cases += 1
for label, exports, imports, error in (
('object', [('entry', 0x11, 0, 1)], [('entry', 0x11, 0, 0)], None),
('unspecified', [('entry', 0x12, 0, 1)], [('entry', 0x10, 0, 0)], None),
('mismatch', [('entry', 0x11, 0, 1)], [('entry', 0x12, 0, 0)],
'Guest import/export type mismatch: entry'),
('filtered-first', [('entry', 0x12, 2, 1), ('entry', 0x11, 0, 1)],
[('entry', 0x11, 0, 0)], None),
('filtered-mismatch', [('entry', 0x12, 2, 1), ('entry', 0x11, 0, 1)],
[('entry', 0x12, 0, 0)], 'Guest import/export type mismatch: entry'),
('internal-first', [('entry', 0x12, 1, 1), ('entry', 0x11, 0, 1)],
[('entry', 0x11, 0, 0)], None),
('local-first', [('entry', 0x02, 0, 1), ('entry', 0x11, 0, 1)],
[('entry', 0x11, 0, 0)], None),
('duplicate-first', [('entry#A#A', 0x12, 0, 1), ('entry#B#A', 0x12, 0, 1)],
[('entry', 0x12, 0, 0)], None if windows else 'Duplicate or empty export after stripping #: entry'),
('duplicate-mismatch', [('entry#A#A', 0x12, 0, 1), ('entry#B#A', 0x12, 0, 1)],
[('entry', 0x11, 0, 0)], 'Guest import/export type mismatch: entry' if windows else 'Duplicate or empty export after stripping #: entry'),
('weak', [('entry', 0x22, 0, 1)], [('entry', 0x12, 0, 0)], None),
('protected', [('entry', 0x12, 3, 1)], [('entry', 0x12, 0, 0)], None)):
result, _ = convert(relinker, work / f'{label}-{windows}', windows, {
'provider.prx': guest_symbols(exports),
'consumer.prx': guest_symbols(imports, 'provider.prx'),
})
if error:
assert result.returncode != 0 and error in result.stderr, (result.stdout, result.stderr)
else:
assert result.returncode == 0, (result.stdout, result.stderr)
cases += 1
print(f'{cases} guest export lookup cases passed')
if __name__ == '__main__':
main()