Merge pull request #9408 from rursache/feat/macos-trace-imports

feat(relinker): trace function imports on macOS
This commit is contained in:
Adrià authored and GitHub committed 2026-10-26 11:27:51 +01:00
commit cbf76e7a6e
14 files changed
+303 -23

No files matched your search

@@ -1,6 +1,17 @@
#if defined(_WIN32) || defined(__linux__)
#if defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__))
#ifdef _WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <cerrno>
#include <csignal>
#include <ctime>
#include <mach-o/dyld.h>
#include <mach-o/loader.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <pthread.h>
#include <sys/mman.h>
#include <unistd.h>
#else
#include <cerrno>
#include <csignal>
@@ -113,6 +124,10 @@ void StartCallCountWriter() {
std::uint64_t CurrentThread() {
#ifdef _WIN32
return GetCurrentThreadId();
#elif defined(__APPLE__)
std::uint64_t thread = 0;
pthread_threadid_np(nullptr, &thread);
return thread;
#else
return static_cast<std::uint64_t>(gettid());
#endif
@@ -128,6 +143,22 @@ void MapThunkArea() {
CloseHandle(section);
if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast<int>(code), std::system_category(), "call trace: MapViewOfFile");
}
#elif defined(__APPLE__)
void MapThunkArea() {
void* writable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
if (writable == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "call trace: mmap");
mach_vm_address_t executable = 0;
vm_prot_t current = VM_PROT_NONE;
vm_prot_t maximum = VM_PROT_NONE;
const kern_return_t remapped = mach_vm_remap(mach_task_self(), &executable, ThunkAreaBytes, 0, VM_FLAGS_ANYWHERE, mach_task_self(), reinterpret_cast<mach_vm_address_t>(writable), false, &current, &maximum, VM_INHERIT_NONE);
if (remapped != KERN_SUCCESS || mach_vm_protect(mach_task_self(), executable, ThunkAreaBytes, false, VM_PROT_READ | VM_PROT_EXECUTE) != KERN_SUCCESS) {
if (remapped == KERN_SUCCESS) mach_vm_deallocate(mach_task_self(), executable, ThunkAreaBytes);
munmap(writable, ThunkAreaBytes);
throw std::system_error(remapped == KERN_SUCCESS ? KERN_PROTECTION_FAILURE : remapped, std::system_category(), "call trace: mach_vm_remap");
}
g_writable = static_cast<std::uint8_t*>(writable);
g_executable = reinterpret_cast<std::uint8_t*>(executable);
}
#else
void MapThunkArea() {
const int file = memfd_create("aps5-call-trace", MFD_CLOEXEC);
@@ -147,7 +178,9 @@ void MapThunkArea() {
g_writable = static_cast<std::uint8_t*>(writable);
g_executable = static_cast<std::uint8_t*>(executable);
}
#endif
#ifndef _WIN32
struct ModuleSegment {
std::uint64_t start;
std::uint64_t end;
@@ -167,6 +200,42 @@ std::atomic<bool> g_reporting{false};
std::atomic<bool> g_reported{false};
unsigned long long g_moduleChanges = ~0ull;
#if defined(__APPLE__)
void SnapshotModules() {
const auto images = _dyld_image_count();
unsigned long long changes = images;
for (std::uint32_t index = 0; index < images; ++index)
changes = changes * 131u + reinterpret_cast<unsigned long long>(_dyld_get_image_header(index));
if (changes == g_moduleChanges) return;
g_moduleChanges = changes;
auto* map = new ModuleMap{};
for (std::uint32_t index = 0; index < images && map->count < std::size(map->segments); ++index) {
const auto* header = reinterpret_cast<const mach_header_64*>(_dyld_get_image_header(index));
if (header == nullptr || header->magic != MH_MAGIC_64) continue;
const char* path = _dyld_get_image_name(index);
if (path == nullptr || *path == '\0') path = getprogname();
const char* slash = std::strrchr(path, '/');
const char* name = slash == nullptr ? path : slash + 1;
const auto slide = static_cast<std::uint64_t>(_dyld_get_image_vmaddr_slide(index));
const auto base = reinterpret_cast<std::uint64_t>(header);
auto* command = reinterpret_cast<const load_command*>(header + 1);
for (std::uint32_t commandIndex = 0; commandIndex < header->ncmds && map->count < std::size(map->segments); ++commandIndex) {
if (command->cmd == LC_SEGMENT_64) {
const auto* segment = reinterpret_cast<const segment_command_64*>(command);
if (segment->vmsize != 0 && (segment->initprot & VM_PROT_EXECUTE) != 0) {
auto& recorded = map->segments[map->count++];
recorded.base = base;
recorded.start = segment->vmaddr + slide;
recorded.end = recorded.start + segment->vmsize;
std::strncpy(recorded.name, name, sizeof(recorded.name) - 1);
}
}
command = reinterpret_cast<const load_command*>(reinterpret_cast<const std::uint8_t*>(command) + command->cmdsize);
}
}
g_modules.store(map, std::memory_order_release);
}
#else
void SnapshotModules() {
unsigned long long changes = 0;
dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) {
@@ -198,6 +267,7 @@ void SnapshotModules() {
}, map);
g_modules.store(map, std::memory_order_release);
}
#endif
class ReportLine {
public:
@@ -370,12 +440,12 @@ void* TraceImport_nid_no_patch(void* target, const char* name, const char* libra
MapThunkArea();
g_records.store(new ImportRecord[RecordCapacity], std::memory_order_release);
g_ring.store(new RingSlot[RingCapacity], std::memory_order_release);
#ifdef __linux__
#if defined(__linux__) || defined(__APPLE__)
InstallAbortReport();
#endif
StartCallCountWriter();
}
#ifdef __linux__
#if defined(__linux__) || defined(__APPLE__)
SnapshotModules();
#endif
if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second;
@@ -475,7 +545,7 @@ asm(".text\n"
" movdqu %xmm7, 112(%rsp)\n"
" mov %r11, %rdi\n"
" mov 200(%rsp), %rsi\n"
" call CallTraceRecord_nid_no_patch\n"
" call " APS5_ASM_SYMBOL("CallTraceRecord_nid_no_patch") "\n"
" movdqu 0(%rsp), %xmm0\n"
" movdqu 16(%rsp), %xmm1\n"
" movdqu 32(%rsp), %xmm2\n"
+2
View File
@@ -612,6 +612,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $<TARGET_FILE:relinker>)
add_test(NAME linux_trace_imports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_trace_imports.py $<TARGET_FILE:relinker>)
add_test(NAME macos_trace_imports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_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}
-2
View File
@@ -199,8 +199,6 @@ Args ParseArgs(int argc, char* argv[]) {
if (args.windowsFixedBase && !args.toWindows)
throw std::runtime_error("--windows-fixed-base requires --windows");
if (args.traceImports && args.toMacos)
throw std::runtime_error("--trace-imports cannot be used with --macos");
if (args.traceImports && args.lazyBinding)
throw std::runtime_error("--trace-imports conflicts with --lazy-binding");
if (args.linuxFixedBase && (args.toWindows || args.toMacos))
@@ -41,6 +41,12 @@ struct MacOsTlsExport {
std::uint64_t Flags = 0;
};
struct MacOsTraceImport {
std::uint32_t Rva;
std::string Name;
std::string Library;
};
struct MacOsImageInput {
bool Executable = true;
std::string InstallName;
@@ -61,6 +67,8 @@ struct MacOsImageInput {
std::vector<MacOsTlsImport> TlsImports;
std::vector<MacOsTlsExport> TlsExports;
const std::vector<Codegen::TrampolineSite>* Trampolines = nullptr;
bool TraceImports = false;
std::vector<MacOsTraceImport> Traced;
std::function<void(std::string_view)> Message;
};
@@ -11,7 +11,7 @@ namespace Elfpatcher::MacOs {
class MacOsMachOPatcher final : public IElfPatcher {
public:
explicit MacOsMachOPatcher(std::optional<std::uint64_t> fixedBase = std::nullopt, std::function<void(std::string_view)> message = {});
explicit MacOsMachOPatcher(std::optional<std::uint64_t> fixedBase = std::nullopt, std::function<void(std::string_view)> message = {}, bool traceImports = false);
std::vector<std::uint8_t> Patch(
std::vector<std::uint8_t> sourceElf,
@@ -27,6 +27,7 @@ public:
private:
std::optional<std::uint64_t> fixedBase;
std::function<void(std::string_view)> message;
bool traceImports;
};
}
@@ -8,6 +8,7 @@
#include <algorithm>
#include <bit>
#include <iomanip>
#include <initializer_list>
#include <limits>
#include <map>
#include <memory>
@@ -95,6 +96,40 @@ constexpr std::uint8_t EntryStub[] = {
constexpr std::size_t EntryStubEntryDisplacement = 0x69;
constexpr std::size_t EntryStubStartDisplacement = 0x84;
struct MacOsTracePrologue {
std::vector<std::uint8_t> Bytes;
std::size_t EntryDisplacement;
};
MacOsTracePrologue BuildTracePrologue(const std::uint32_t prologueRva, const std::uint32_t tableRva, const std::uint32_t tableEndRva, const std::uint32_t functionSlotRva) {
std::vector<std::uint8_t> code = {0x57, 0x56, 0x52, 0x51, 0x53, 0x41, 0x54, 0x48, 0x83, 0xec, 0x08};
const auto rip = [&](const std::initializer_list<std::uint8_t> opcode, const std::uint32_t target) {
code.insert(code.end(), opcode);
const auto displacement = static_cast<std::int64_t>(target) - static_cast<std::int64_t>(prologueRva + code.size() + 4);
if (displacement < std::numeric_limits<std::int32_t>::min() || displacement > std::numeric_limits<std::int32_t>::max())
throw Domain::RelinkerException("Import trace stub exceeds rel32 range");
Io::AppendU32(code, static_cast<std::uint32_t>(displacement));
};
rip({0x48, 0x8d, 0x1d}, tableRva);
rip({0x4c, 0x8d, 0x25}, tableEndRva);
const auto loop = code.size();
code.insert(code.end(), {0x4c, 0x39, 0xe3, 0x73, 0x00});
const auto exit = code.size();
code.insert(code.end(), {0x48, 0x8b, 0x03, 0x48, 0x8b, 0x3c, 0x03, 0x48, 0x8b, 0x73, 0x08, 0x48, 0x01, 0xde, 0x48, 0x8b, 0x53, 0x10, 0x48, 0x01, 0xda});
rip({0xff, 0x15}, functionSlotRva);
code.insert(code.end(), {0x48, 0x8b, 0x0b, 0x48, 0x89, 0x04, 0x0b, 0x48, 0x83, 0xc3, 0x18, 0xeb});
const auto back = static_cast<std::int64_t>(loop) - static_cast<std::int64_t>(code.size() + 1);
if (back < -128 || back > 127)
throw Domain::RelinkerException("Import trace loop exceeds rel8 range");
code.push_back(static_cast<std::uint8_t>(static_cast<std::int8_t>(back)));
if (code.size() - exit > 127)
throw Domain::RelinkerException("Import trace loop exceeds rel8 range");
code[exit - 1] = static_cast<std::uint8_t>(code.size() - exit);
code.insert(code.end(), {0x48, 0x83, 0xc4, 0x08, 0x41, 0x5c, 0x5b, 0x59, 0x5a, 0x5e, 0x5f, 0xe9, 0x00, 0x00, 0x00, 0x00});
const auto entry = code.size() - 4;
return {std::move(code), entry};
}
struct Section {
std::string Name;
std::uint64_t Offset;
@@ -491,7 +526,8 @@ std::vector<std::uint8_t> WriteMacOsImage(MacOsImageInput& input) {
const auto descriptor = metaRva + static_cast<std::uint32_t>(MetaTlsDescriptor);
const auto initPointer = AlignUp(metaSize, 8);
const auto startSlot = initPointer + 8;
const auto tlsExports = startSlot + 8;
const auto traceFunctionSlot = input.TraceImports ? startSlot + 8 : startSlot;
const auto tlsExports = traceFunctionSlot + 8;
std::map<std::string, std::uint64_t> tlsImportSlots;
for (const auto& import : input.TlsImports) tlsImportSlots.emplace(import.Symbol, 0);
auto metaEnd = tlsExports + input.TlsExports.size() * 16;
@@ -666,6 +702,44 @@ std::vector<std::uint8_t> WriteMacOsImage(MacOsImageInput& input) {
}
if (input.Executable) {
input.Binds.push_back({"_Aps5StartGuest_nid_no_patch", metaRva + static_cast<std::uint32_t>(startSlot)});
if (input.TraceImports)
input.Binds.push_back({"_TraceImport_nid_no_patch", metaRva + static_cast<std::uint32_t>(traceFunctionSlot)});
}
std::size_t traceEntryPatch = 0;
std::uint32_t tracePrologueRva = 0;
if (input.Executable && input.TraceImports) {
const auto tableRva = code.GetRva();
const auto tableEnd = tableRva + static_cast<std::uint32_t>(input.Traced.size() * 24);
std::map<std::string, std::uint32_t> stringRva;
std::vector<std::string> stringOrder;
auto cursor = tableEnd;
const auto intern = [&](const std::string& text) {
const auto existing = stringRva.find(text);
if (existing != stringRva.end()) return existing->second;
stringRva.emplace(text, cursor);
stringOrder.push_back(text);
cursor += static_cast<std::uint32_t>(text.size() + 1);
return stringRva.at(text);
};
std::vector<std::uint8_t> table;
for (std::size_t index = 0; index < input.Traced.size(); ++index) {
const auto entry = tableRva + static_cast<std::uint32_t>(index * 24);
const auto relative = [entry](const std::uint32_t target) { return static_cast<std::uint64_t>(static_cast<std::int64_t>(target) - static_cast<std::int64_t>(entry)); };
Io::AppendU64(table, relative(input.Traced[index].Rva));
Io::AppendU64(table, relative(intern(input.Traced[index].Name)));
Io::AppendU64(table, relative(intern(input.Traced[index].Library)));
}
for (const auto byte : table) code.Emit({byte});
for (const auto& text : stringOrder) {
if (code.GetRva() != stringRva.at(text)) throw Domain::RelinkerException("Import trace string was placed at the wrong address");
for (const auto character : text) code.Emit({static_cast<std::uint8_t>(character)});
code.Emit({0});
}
if (code.GetRva() != cursor) throw Domain::RelinkerException("Import trace table size changed while it was written");
tracePrologueRva = cursor;
const auto prologue = BuildTracePrologue(cursor, tableRva, tableEnd, metaRva + static_cast<std::uint32_t>(traceFunctionSlot));
traceEntryPatch = static_cast<std::size_t>(code.GetRva() - codeRva) + prologue.EntryDisplacement;
for (const auto byte : prologue.Bytes) code.Emit({byte});
}
const auto metaBytes = meta.size();
segments.push_back({"__APS5DATA", metaRva, metaBytes, VmProtRead | VmProtWrite, std::move(meta), std::move(dataSections)});
@@ -680,6 +754,8 @@ std::vector<std::uint8_t> WriteMacOsImage(MacOsImageInput& input) {
while (used != 0 && data[used - 1] == 0) --used;
data.resize(used);
}
if (segments[index].Name == "__APS5TEXT" && traceEntryPatch != 0 && data.size() < traceEntryPatch + 4)
data.resize(traceEntryPatch + 4, 0);
data.resize(AlignUp(data.size(), PageSize), 0);
}
@@ -817,16 +893,32 @@ std::vector<std::uint8_t> WriteMacOsImage(MacOsImageInput& input) {
std::vector<std::uint8_t> file;
constexpr std::size_t HeaderSize = 32;
const auto stubOffset = AlignUp(HeaderSize + commands.size(), 16);
if (stubOffset + (input.Executable ? sizeof(EntryStub) : 0) > LoadRva)
const auto traceJump = input.Executable && input.TraceImports;
const auto entryOffset = stubOffset + (traceJump ? 5 : 0);
if (entryOffset + (input.Executable ? sizeof(EntryStub) : 0) > LoadRva)
throw Domain::RelinkerException("Mach-O load commands do not fit before the guest image");
file.resize(LoadRva, 0);
if (input.Executable) {
std::copy(std::begin(EntryStub), std::end(EntryStub), file.begin() + static_cast<std::ptrdiff_t>(stubOffset));
const auto writeDisplacement = [&](const std::size_t field, const std::uint64_t targetRva) {
const auto displacement = static_cast<std::int64_t>(targetRva) - static_cast<std::int64_t>(stubOffset + field + 4);
if (traceJump) {
const auto text = std::find_if(segments.begin(), segments.end(), [](const Segment& segment) { return segment.Name == "__APS5TEXT"; });
if (text == segments.end() || traceEntryPatch + 4 > text->Data.size())
throw Domain::RelinkerException("Import trace prologue is missing from the Mach-O image");
const auto displacement = static_cast<std::int64_t>(entryOffset) - static_cast<std::int64_t>(codeRva + traceEntryPatch + 4);
if (displacement < std::numeric_limits<std::int32_t>::min() || displacement > std::numeric_limits<std::int32_t>::max())
throw Domain::RelinkerException("macOS entry stub target is out of rel32 range");
Io::WriteU32(file, stubOffset + field, static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement)));
Io::WriteU32(text->Data, traceEntryPatch, static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement)));
const auto jump = static_cast<std::int64_t>(tracePrologueRva) - static_cast<std::int64_t>(stubOffset + 5);
if (jump < std::numeric_limits<std::int32_t>::min() || jump > std::numeric_limits<std::int32_t>::max())
throw Domain::RelinkerException("Import trace prologue is out of rel32 range");
file[stubOffset] = 0xe9;
Io::WriteU32(file, stubOffset + 1, static_cast<std::uint32_t>(static_cast<std::int32_t>(jump)));
}
std::copy(std::begin(EntryStub), std::end(EntryStub), file.begin() + static_cast<std::ptrdiff_t>(entryOffset));
const auto writeDisplacement = [&](const std::size_t field, const std::uint64_t targetRva) {
const auto displacement = static_cast<std::int64_t>(targetRva) - static_cast<std::int64_t>(entryOffset + field + 4);
if (displacement < std::numeric_limits<std::int32_t>::min() || displacement > std::numeric_limits<std::int32_t>::max())
throw Domain::RelinkerException("macOS entry stub target is out of rel32 range");
Io::WriteU32(file, entryOffset + field, static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement)));
};
writeDisplacement(EntryStubEntryDisplacement, input.EntryRva);
writeDisplacement(EntryStubStartDisplacement, metaRva + startSlot);
@@ -51,7 +51,7 @@ std::string MachODependency(const std::string& name, const std::string& origin)
return "@rpath/" + name;
}
MacOsMachOPatcher::MacOsMachOPatcher(const std::optional<std::uint64_t> fixedBase, std::function<void(std::string_view)> message) : fixedBase(fixedBase), message(std::move(message)) {
MacOsMachOPatcher::MacOsMachOPatcher(const std::optional<std::uint64_t> fixedBase, std::function<void(std::string_view)> message, const bool traceImports) : fixedBase(fixedBase), message(std::move(message)), traceImports(traceImports) {
if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0))
throw Domain::RelinkerException("Invalid fixed macOS guest image base");
}
@@ -65,6 +65,9 @@ std::vector<std::uint8_t> MacOsMachOPatcher::Patch(std::vector<std::uint8_t> sou
if (originalPltGotVaddr != 0)
image.GetRva(originalPltGotVaddr, 8);
const auto relocations = Windows::WindowsRelocationBuilder().Apply(image, dynamicSection, originalHeaders);
const auto needed = ReadNeededLibraries(dynamicSection);
if (traceImports && std::find(needed.begin(), needed.end(), "libc.prx") == needed.end())
throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries");
MacOsImageInput input;
input.Executable = true;
@@ -98,9 +101,13 @@ std::vector<std::uint8_t> MacOsMachOPatcher::Patch(std::vector<std::uint8_t> sou
}
}
input.Rebases = relocations.BaseRelocations;
for (const auto& import : relocations.Imports)
for (const auto& import : relocations.Imports) {
input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast<std::int64_t>(import.Addend), import.Weak});
for (const auto& library : ReadNeededLibraries(dynamicSection))
if (traceImports && import.Function && !import.Weak && import.Addend == 0 && !import.Name.empty())
input.Traced.push_back({import.TargetRva, import.Name, import.Library});
}
input.TraceImports = traceImports;
for (const auto& library : needed)
input.Dylibs.push_back(MachODependency(library, "@executable_path"));
for (const auto* library : {"@rpath/libkernel.prx", "@rpath/libc.prx"})
if (std::find(input.Dylibs.begin(), input.Dylibs.end(), library) == input.Dylibs.end())
@@ -155,7 +155,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
inputProgress.Message("Warning: with --lazy-binding the Linux executable stays position independent and is not placed at 0x400000; a title whose own sceLibcMspaceCreate checks the 40-bit address window stops at start-up\n");
std::shared_ptr<Elfpatcher::IElfPatcher> patcher;
if (macos) {
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); });
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }, options.TraceImports);
} 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, options.TraceImports, [&](std::string_view text) { inputProgress.Message(text); });
} else {
@@ -80,7 +80,7 @@ def main():
result = relink(relinker, source, case / "output.elf", "--trace-imports", "--lazy-binding")
assert result.returncode != 0 and "--trace-imports conflicts with --lazy-binding" in result.stderr, (result.returncode, result.stderr)
result = relink(relinker, source, case / "output", "--trace-imports", "--macos")
assert result.returncode != 0 and "--trace-imports cannot be used with --macos" in result.stderr, (result.returncode, result.stderr)
assert result.returncode == 2 and "--trace-imports requires libc.prx" in result.stderr, (result.returncode, result.stderr)
print("Linux import tracing tests passed")
@@ -0,0 +1,102 @@
"""--trace-imports passes each macOS function import through libc.prx's TraceImport_nid_no_patch before the guest entry."""
import os
from pathlib import Path
import platform
import subprocess
import sys
import tempfile
from guest_import_fixtures import executable
PROVIDER = "#include <unistd.h>\nint shared(void) { _exit(11); }\n"
HOOK = ("#include <string.h>\n"
"#include <unistd.h>\n"
"static int replacement(void) { _exit(77); }\n"
"void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) {\n"
" if (((unsigned long)__builtin_frame_address(0) & 15) != 0) _exit(13);\n"
" return strcmp(name, \"shared\") == 0 && strcmp(library, \"a.prx\") == 0 ? (void*)replacement : target;\n"
"}\n")
PLAIN = "int Unrelated(void) { return 0; }\n"
START = "void Aps5StartGuest_nid_no_patch(void (*entry)(void)) { entry(); }\n"
def relink(relinker, source, output, *options):
return subprocess.run([str(relinker), "--macos", "--skip-sce-module", *options, str(source), str(output)],
capture_output=True, text=True, timeout=30)
def library(work, name, code):
source = work / (name + ".c")
source.write_text(code)
output = work / (name + ".dylib")
subprocess.run(["xcrun", "clang", "-arch", "x86_64", "-dynamiclib", str(source),
"-Wl,-install_name,@rpath/" + name + ".prx", "-o", str(output)],
check=True, capture_output=True, timeout=20)
return output.read_bytes()
def run_binary(output):
command = (["/usr/bin/arch", "-x86_64"] if platform.machine() == "arm64" else []) + [str(output)]
return subprocess.run(command, capture_output=True, text=True, timeout=30)
def main():
relinker = Path(sys.argv[1]).resolve()
runnable = sys.platform == "darwin" and platform.machine() in ("x86_64", "arm64")
with tempfile.TemporaryDirectory(prefix="anyps5-macos-trace-imports-") as directory:
work = Path(directory)
libraries = {}
if runnable:
libraries = {name: library(work, name, code) for name, code in (
("a", PROVIDER), ("hook", HOOK), ("plain", PLAIN), ("libkernel", START))}
for name, options, libc, symbol_info, expected in (
("untraced", (), "hook", 0x12, 11),
("traced", ("--trace-imports",), "hook", 0x12, 77),
("fixed-base", ("--trace-imports", "--macos-fixed-base", "0x400000"), "hook", 0x12, 77),
("data", ("--trace-imports",), "hook", 0x11, 11),
("weak", ("--trace-imports",), "hook", 0x22, 11),
("unsupported", ("--trace-imports",), "plain", 0x12, None)):
case = work / name
case.mkdir()
image = executable("a.prx", extra_dependencies=("libc.prx",))
image[0x4898 + 4] = symbol_info
source = case / "input.elf"
source.write_bytes(image)
output = case / "output"
result = relink(relinker, source, output, *options)
assert result.returncode == 0, (name, result.stdout, result.stderr)
if not runnable:
continue
(case / "libs").mkdir()
(case / "libs" / "a.prx").write_bytes(libraries["a"])
(case / "libs" / "libc.prx").write_bytes(libraries[libc])
(case / "libs" / "libkernel.prx").write_bytes(libraries["libkernel"])
output.chmod(0o755)
run = run_binary(output)
if run.returncode != expected and "Bad CPU type in executable" in run.stderr:
assert os.environ.get("GITHUB_ACTIONS") != "true", "macOS CI must execute the x86_64 import trace"
runnable = False
continue
if expected is None:
assert run.returncode != 0 and "TraceImport_nid_no_patch" 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.mkdir()
source = case / "input.elf"
source.write_bytes(executable("a.prx"))
result = relink(relinker, source, case / "output", "--trace-imports")
assert result.returncode == 2 and "--trace-imports requires libc.prx" in result.stderr, (result.returncode, result.stderr)
assert not (case / "output").exists()
result = relink(relinker, source, case / "output", "--trace-imports", "--lazy-binding")
assert result.returncode != 0 and "--trace-imports conflicts with --lazy-binding" in result.stderr, (result.returncode, result.stderr)
print("macOS import tracing tests passed")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -11,7 +11,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M
| `APS5_TRACE_APR` | APR file reads, including path, offset, size and destination. |
| `APS5_TRACE_AUDIOOUT` | Audio output port parameters when a port is opened. |
| `APS5_TRACE_AUDIOOUT2` | AudioOut2 context and port parameters, attributes, pushes, queue polls and periodic queue summaries. |
| `APS5_TRACE_CALLS` | On Windows and Linux, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). |
| `APS5_TRACE_CALLS` | On Windows, Linux and macOS, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux and macOS) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). |
| `APS5_TRACE_CALLS_FILE` | With `APS5_TRACE_CALLS`, a file rewritten every 10 seconds with every traced import called so far, most called first: number of calls, library and import name. |
| `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. |
| `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. |
+1 -1
View File
@@ -172,7 +172,7 @@
- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../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 at end of input returns the number of assignments, including zero, rather than `EOF` as FreeBSD does, as reported from the console scanner in #8479; `%%` at end of input still returns `EOF`. A literal mismatch is a matching failure, where FreeBSD's `vfwscanf` reports an input failure. An incomplete floating exponent such as `1e+` converts its mantissa, as reported from the console scanner in #8479; other incomplete exponent forms are not separately verified. A short `%c` field is currently a matching failure without storing its available prefix, while #9321 reports that the console stores a short `%3lc` field; its return count, partial-output extent and precedence are unverified. An integer field that is only a prefix, such as `0x`, does not convert; FreeBSD's `vfwscanf` pushes back the `x` and converts the `0`. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. `%S` and `%C`, with any length modifier or `*`, end the scan as a matching failure, as the console's Dinkumware scanner in PPSA21564's `libc.prx` treats them as invalid conversions; this was read from the module, not run on a console, and whether input ending before them is an input failure instead is unverified. It throws for unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title
- [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 lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. 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) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp)
- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; 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`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds
- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; 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`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux and macOS have no crash report, so they print the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds. An arm64 libc build omits the recorder
- [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2.
- [_Exp and _FExp](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow PRAGMATA PPSA02530's bundled `libc.prx` (static disassembly): both operands are classified first and only finite/denormal pairs take the finite path. The strict x-only cutoffs at ±229/±1842 store `0.0*y` without an error below and `y*inf` with overflow above, so an offset does not compensate them. A zero multiplier keeps its sign, a NaN multiplier leaves `*px` unchanged, `+inf` with zero or `-inf` with infinity is invalid, and finite multipliers with `+inf`/`-inf` return signed infinity/zero without flags. Inside the cutoffs subnormals are `Finite` without flags, a zero from scaling raises underflow, and the negative cutoff itself raises nothing. `_FExp` uses the measured `c1`, `c2`, `invln2`, `Q` and `P`; `_Exp` finite constants, exact finite bits, signaling NaNs, directed rounding and flush-to-zero modes remain unverified.
- [exp2f](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_exp2f.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_exp2f.c), with `huge` and `twom100` read at run time and `x + redux` rounded to float, as msun's `volatile` reads and `STRICT_ASSIGN` do. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` only in msun's overflow (`x >= 128`) and underflow (`x <= -150`) branches, so a subnormal result leaves it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `exp2f` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host.
+1 -1
View File
@@ -41,6 +41,6 @@
- `--to-rosetta` output faults on an M2 running macOS 14.8.9 when it reaches AVX, AVX2, FMA, F16C, RDRAND or a BMI1/BMI2 instruction other than ANDN, TZCNT and LZCNT: Rosetta on that host raises SIGILL for them and for RDSEED, RDPID, CLWB, ADCX and ADOX ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Only TZCNT, LZCNT, POPCNT, MOVBE, CLFLUSHOPT, PREFETCHW and RDTSCP execute, and RDTSCP sets ECX to 0xAAAAAAAB. CPUID leaf 1 reports POPCNT and none of MOVBE, AVX, FMA, F16C or RDRAND, leaf 7 reads zero, and leaf 0x80000001 reports RDTSCP but neither LZCNT nor PREFETCHW. An M2 Max on macOS 27.2 reports the same CPUID but executes everything except RDSEED, RDPID, CLWB, ADCX and ADOX, so the faults follow the macOS version, not the chip.
- `--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).
- XSAVEC raises an illegal instruction under Rosetta and is not converted, because it writes the compacted format. XSAVEOPT, which also faults there, is rewritten to XSAVE in place by the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) under `--to-rosetta` (`0F AE /6` becomes `/4`; prefixes and operand are kept). The two differ only in the init and modified optimizations, which let XSAVEOPT skip writing unchanged or initial state ([Intel SDM volume 1](https://cdrdv2-public.intel.com/812382/253665-sdm-vol-1.pdf), chapter 13), so XRSTOR restores the same state from either area. Measured on one M5 with macOS 27.0.1: XSAVE and XRSTOR handle x87, SSE and AVX state in the standard format, although CPUID leaf 1 reports neither XSAVE nor OSXSAVE and leaf 0xD sub-leaf 1 reports neither XSAVEOPT nor XSAVEC. The converter tests' x86-64 build executes the replacement XSAVE under Rosetta on that host.
- `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp)) exists for Windows and Linux output, not macOS. 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. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on
- `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp), [macOS entry](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)) leaves every import that is not a PLT slot or a function GOT slot untraced, and the same for a function reached through any other relocation, `sceKernelDlsym`, a weak import, or a module loaded at run time. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on. On macOS the stub writes the thunk into the bound slot and does not change its protection, because that slot stays in a writable segment
- [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified.
+2 -2
View File
@@ -67,7 +67,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 instead of the preferred `0x400000`, 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). Windows and Linux output only; conflicts with `--lazy-binding`. |
| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows, Linux and macOS output; conflicts with `--lazy-binding`. |
| `--linux-fixed-base <address>` | Load the Linux executable at this fixed address instead of the default `0x400000`, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR, as it is at the default address. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. |
| `--macos-fixed-base <address>` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. |
| `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. |
@@ -348,7 +348,7 @@ To measure how a title runs on a machine, start it with `APS5_TRACE_SUMMARY_EVER
### Tracing system library calls
Windows and Linux. 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 (on Linux, which has no crash report, only an abort prints them): sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. The last 32 of these calls made by the reporting thread follow, taken from the 65536 most recent calls, so a thread whose calls were pushed out of the last 64 by a busier one still shows them. Then come the 32 most called imports since start-up, with their number of calls.
Windows, Linux and macOS. 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 (on Linux and macOS, which have no crash report, only an abort prints them): sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. The last 32 of these calls made by the reporting thread follow, taken from the 65536 most recent calls, so a thread whose calls were pushed out of the last 64 by a busier one still shows them. Then come the 32 most called imports since start-up, with their number of calls.
```
last traced system library calls (APS5_TRACE_CALLS):