Merge pull request #4547 from codingreapermaps/feat/relinker-trace-imports

feat(relinker): add --trace-imports for Windows executables
This commit is contained in:
Adrià 2026-10-13 17:48:21 +02:00 committed by GitHub
commit 9dd3c2d1a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 179 additions and 10 deletions

View File

@ -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 $<TARGET_FILE:relinker>)
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 $<TARGET_FILE:relinker>)
add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $<TARGET_FILE:relinker>)
add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE}

View File

@ -24,6 +24,7 @@ struct Args {
bool windowsDiagnostics = false;
bool windowsGui = false;
std::optional<std::uint64_t> windowsFixedBase;
bool traceImports = false;
std::uint32_t unusedFilterLevel = 0;
std::string inputPath;
std::string outputPath;

View File

@ -10,7 +10,7 @@
namespace Cli {
const char* Usage() {
return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--windows-fixed-base <address>] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path <path>] [--exclude-sce-module <file>]... [--module-dir <dir>]... [--executable <path>]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath <path>] [--lazy-binding] [--autorun] <input.elf> <output.elf>\n"
return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base <address>] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path <path>] [--exclude-sce-module <file>]... [--module-dir <dir>]... [--executable <path>]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath <path>] [--lazy-binding] [--autorun] <input.elf> <output.elf>\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());

View File

@ -14,6 +14,7 @@ struct GuestImport {
std::uint64_t Addend;
std::uint32_t RelocationType = 1;
std::string Library;
bool Function = false;
};
struct GuestRuntime {

View File

@ -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<std::uint8_t> RelaPltData;
std::vector<GuestRuntime> GuestModules;
std::map<VirtualAddress, std::string> ImportModules;
std::set<VirtualAddress> FunctionImports;
std::set<std::string> UnmatchedImportModules;
};

View File

@ -11,7 +11,7 @@ namespace Elfpatcher::Windows {
class WindowsPePatcher : public IElfPatcher {
public:
explicit WindowsPePatcher(bool windowsGui = false, std::optional<std::vector<std::uint8_t>> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function<void(std::uint64_t, std::uint64_t)> progress = {}, std::optional<std::uint64_t> fixedBase = std::nullopt);
explicit WindowsPePatcher(bool windowsGui = false, std::optional<std::vector<std::uint8_t>> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function<void(std::uint64_t, std::uint64_t)> progress = {}, std::optional<std::uint64_t> fixedBase = std::nullopt, bool traceImports = false);
const std::string& GetDiagnostics() const;
std::vector<std::uint8_t> Patch(const std::vector<std::uint8_t>& sourceElf, const std::vector<Domain::ProgramHeader>& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector<Codegen::TrampolineSite>& trampolines) override;
@ -23,6 +23,7 @@ private:
std::string diagnostics;
std::filesystem::path iconName;
std::optional<std::uint64_t> fixedBase;
bool traceImports;
};
}

View File

@ -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<std::string>& libraries, const std::vector<PeImport>& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector<Domain::GuestRuntime>& guestModules = {}, const std::set<std::string>& unmatchedImportModules = {}) const;
WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector<std::string>& libraries, const std::vector<PeImport>& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector<Domain::GuestRuntime>& guestModules = {}, const std::set<std::string>& unmatchedImportModules = {}, bool traceImports = false) const;
};
}

View File

@ -36,6 +36,7 @@ struct PeImport {
std::uint32_t RelocationType = 1;
std::string Library;
bool Weak = false;
bool Function = false;
};
struct PeRelocations {

View File

@ -5,6 +5,7 @@
#include <io/BufferUtils.hpp>
#include <algorithm>
#include <cctype>
#include <map>
#include <set>
#include <optional>
#include <filesystem>
@ -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<std::string>& libraries, const std::vector<PeImport>& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector<Domain::GuestRuntime>& guestModules, const std::set<std::string>& unmatchedImportModules) const {
WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector<std::string>& libraries, const std::vector<PeImport>& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector<Domain::GuestRuntime>& guestModules, const std::set<std::string>& 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<std::size_t> traceProvider;
std::map<std::string, std::uint32_t> 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<std::uint32_t> 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<std::size_t> unresolvedBranches;
std::vector<std::size_t> lazyUnresolvedImports;
std::vector<std::size_t> 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);
}

View File

@ -70,7 +70,7 @@ std::vector<std::uint8_t> 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);
}

View File

@ -28,7 +28,7 @@ void writeGotStub(std::vector<PeSection>& sections, const std::uint32_t targetRv
}
WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional<std::vector<std::uint8_t>> icon, std::filesystem::path iconName, std::function<void(std::uint64_t, std::uint64_t)> progress, const std::optional<std::uint64_t> fixedBase) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase) {
WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional<std::vector<std::uint8_t>> icon, std::filesystem::path iconName, std::function<void(std::uint64_t, std::uint64_t)> progress, const std::optional<std::uint64_t> 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<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t
for (std::size_t index = 0; index < dynamicSection.GuestModules.size(); ++index) {
const auto& module = dynamicSection.GuestModules[index];
guestPaths.push_back(module.Path);
for (const auto& import : module.Imports) relocations.Imports.push_back({import.Name, import.TargetRva, import.Addend, static_cast<std::int32_t>(index), import.RelocationType, import.Library});
for (const auto& import : module.Imports) {
relocations.Imports.push_back({import.Name, import.TargetRva, import.Addend, static_cast<std::int32_t>(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;

View File

@ -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;
}

View File

@ -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;

View File

@ -18,6 +18,7 @@ struct ConversionOptions {
bool TrapSdkSyscallStubs = false;
bool LazyBinding = false;
bool WindowsGui = false;
bool TraceImports = false;
std::optional<std::uint64_t> WindowsFixedBase;
bool WindowsDiagnostics = false;
std::string RunPath = "$ORIGIN/libs";

View File

@ -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;

View File

@ -102,7 +102,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
if (macos) {
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>();
} else if (windows) {
patcher = std::make_shared<Elfpatcher::Windows::WindowsPePatcher>(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<Elfpatcher::Windows::WindowsPePatcher>(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<Elfpatcher::Linux::LinuxElfPatcher>(
std::make_shared<Elfpatcher::EntryStubBuilder>(),

View File

@ -264,7 +264,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
|| std::any_of(raw.begin() + static_cast<std::ptrdiff_t>(symOff), raw.begin() + static_cast<std::ptrdiff_t>(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});
}
};

View File

@ -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()

View File

@ -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<int>::_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

View File

@ -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

View File

@ -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 <address>` | 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.