feat(relinker): convert for Rosetta with --to-rosetta

Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE, but, unlike an Intel
host, it has neither RDSEED, CLWB nor RDPID. --to-rosetta converts
everything --to-intel does and lowers these three as well:

- RDSEED becomes RDRAND (the /6 form of the same opcode), which also
  returns a random value and sets CF.
- CLWB only writes a cache line back, so it becomes a NOP of the same
  length.
- RDPID becomes a load of 0 through an optional stub: Rosetta's TSC_AUX
  is a constant, not a processor index, so every thread reports
  processor 0. A site with no movable instruction after it for the jump
  is kept.

The flag implies --to-intel and conflicts with --windows. It is
independent of --macos: it only changes what the conversion produces, so
--macos alone leaves these instructions as they are, as on an Intel Mac,
and a title for Rosetta is relinked with --macos --to-rosetta. Intel
output is unchanged: the target is a parameter of the converter, Intel by
default.
This commit is contained in:
mugurc 2026-10-10 19:53:49 +03:00
parent 9206c3e621
commit 92b20608e8
18 changed files with 232 additions and 16 deletions

View File

@ -330,6 +330,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_entry_point_bounds.py $<TARGET_FILE:relinker>)
add_test(NAME init_array_roots COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_init_array_roots.py $<TARGET_FILE:relinker>)
add_test(NAME to_rosetta COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_to_rosetta.py $<TARGET_FILE:relinker>)
else()
message(STATUS "Python3 interpreter not found; skipping Python relinker tests")
endif()

View File

@ -12,6 +12,7 @@ struct Args {
bool skipSyscallCheck = false;
bool skipSceModule = false;
bool toIntel = false;
bool toRosetta = false;
bool writeRegistry = false;
bool toWindows = false;
bool toMacos = false;

View File

@ -8,7 +8,7 @@
namespace Cli {
const char* Usage() {
return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check] [--skip-sce-module | --sce-module-path <path>] [--exclude-sce-module <file>]... [--to-intel] [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] [--skip-syscall-check] [--skip-sce-module | --sce-module-path <path>] [--exclude-sce-module <file>]... [--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";
}
@ -36,6 +36,9 @@ Args ParseArgs(int argc, char* argv[]) {
args.excludedSceModules.insert(argv[++i]);
} else if (arg == "--to-intel") {
args.toIntel = true;
} else if (arg == "--to-rosetta") {
args.toRosetta = true;
args.toIntel = true;
} else if (arg.rfind("unused-filter=", 0) == 0) {
const std::string value = arg.substr(14);
if (unusedFilterSpecified || value.size() != 1 || value[0] < '0' || value[0] > '2')
@ -78,6 +81,8 @@ Args ParseArgs(int argc, char* argv[]) {
if (args.toWindows && args.toMacos)
throw std::runtime_error("--windows conflicts with --macos");
if (args.toRosetta && args.toWindows)
throw std::runtime_error("--to-rosetta conflicts with --windows");
if (args.windowsDiagnostics && !args.toWindows)
throw std::runtime_error("--windows-diagnostics requires --windows");

View File

@ -18,7 +18,7 @@ public:
) const = 0;
};
std::unique_ptr<IAmd64OnlyConverter> MakeAmd64OnlyConverter();
std::unique_ptr<IAmd64OnlyConverter> MakeAmd64OnlyConverter(Amd64OnlyTarget target = Amd64OnlyTarget::Intel);
}

View File

@ -38,6 +38,9 @@ inline constexpr Entry kMwaitx = {"MWAITX", nullptr, 0};
inline constexpr Entry kClzero = {"CLZERO", nullptr, 0};
inline constexpr Entry kRdpru = {"RDPRU", nullptr, 0};
inline constexpr Entry kMcommit = {"MCOMMIT", nullptr, 0};
inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0};
inline constexpr Entry kRdpid = {"RDPID", nullptr, 0};
inline constexpr Entry kClwb = {"CLWB", nullptr, 0};
inline constexpr Entry kExtrq = {"EXTRQ", nullptr, 0};
inline constexpr Entry kInsertq = {"INSERTQ", nullptr, 0};
inline constexpr Entry kExtrqRegisterForm = {"EXTRQ register form", nullptr, 0};

View File

@ -8,6 +8,11 @@
namespace Codegen {
enum class Amd64OnlyTarget : std::uint8_t {
Intel,
Rosetta
};
enum class Amd64OnlyLowering : std::uint8_t {
InPlace,
Trampoline,

View File

@ -23,6 +23,9 @@ struct DecodedInstruction {
[[nodiscard]] bool IsMcommit() const;
[[nodiscard]] bool IsMovntss() const;
[[nodiscard]] bool IsMovntsd() const;
[[nodiscard]] bool IsRdseed() const;
[[nodiscard]] bool IsRdpid() const;
[[nodiscard]] bool IsClwb() const;
private:
[[nodiscard]] std::size_t _skipPrefixesAndRex(bool* outHasOperandSizePrefix, bool* outHasRepnePrefix, bool* outHasRepPrefix) const;

View File

@ -25,7 +25,7 @@ public:
) const = 0;
};
std::unique_ptr<IAmd64OnlyInstructionMatcher> MakeAmd64OnlyInstructionMatcher();
std::unique_ptr<IAmd64OnlyInstructionMatcher> MakeAmd64OnlyInstructionMatcher(Amd64OnlyTarget target = Amd64OnlyTarget::Intel);
}

View File

@ -26,6 +26,8 @@ auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operatio
class Amd64OnlyConverter : public IAmd64OnlyConverter {
public:
explicit Amd64OnlyConverter(const Amd64OnlyTarget target) : _matcher(MakeAmd64OnlyInstructionMatcher(target)) {}
[[nodiscard]] ConvertResult Convert(
std::vector<std::uint8_t> fileBytes,
const std::vector<Domain::ProgramHeader>& codeSegments
@ -38,7 +40,7 @@ private:
Amd64OnlyMatch Substitution;
};
std::unique_ptr<IAmd64OnlyInstructionMatcher> _matcher = MakeAmd64OnlyInstructionMatcher();
std::unique_ptr<IAmd64OnlyInstructionMatcher> _matcher;
std::unique_ptr<IInstructionScanner> _scanner = MakeInstructionScanner();
void _convertSegment(
@ -205,8 +207,8 @@ ConvertResult Amd64OnlyConverter::Convert(
}
std::unique_ptr<IAmd64OnlyConverter> MakeAmd64OnlyConverter() {
return std::make_unique<Amd64OnlyConverter>();
std::unique_ptr<IAmd64OnlyConverter> MakeAmd64OnlyConverter(const Amd64OnlyTarget target) {
return std::make_unique<Amd64OnlyConverter>(target);
}
}

View File

@ -107,6 +107,19 @@ bool _isClzeroOpcode(const DecodedInstruction& instr) {
return pos + 2 < instr.Length && instr.Data[pos] == X64OpcodeConstants::TwoByteOpcodeEscape && instr.Data[pos + 1] == X64OpcodeConstants::TwoByteGrp7 && instr.Data[pos + 2] == 0xFC;
}
std::vector<std::uint8_t> _rdpidReplacement(const DecodedInstruction& instr) {
using namespace X64OpcodeConstants;
const auto opcode = instr.OpcodeOffset();
const auto rex = opcode > 0 && instr.Data[opcode - 1] >= RexMin && instr.Data[opcode - 1] <= RexMax ? instr.Data[opcode - 1] : 0;
const auto reg = static_cast<std::uint8_t>((instr.Data[opcode + 2] & ModRmRmMask) | ((rex & 1) << 3));
std::vector<std::uint8_t> bytes;
if (reg >= 8)
bytes.push_back(0x41);
bytes.push_back(static_cast<std::uint8_t>(0xB8 + (reg & 7)));
bytes.insert(bytes.end(), 4, 0);
return bytes;
}
bool _validWait(const DecodedInstruction& instr) {
const auto opcode = instr.Data + instr.OpcodeOffset();
return instr.Length <= kMaxInstructionLength && std::find(instr.Data, opcode, X64OpcodeConstants::PrefixLock) == opcode;
@ -114,6 +127,8 @@ bool _validWait(const DecodedInstruction& instr) {
class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher {
public:
explicit Amd64OnlyInstructionMatcher(const Amd64OnlyTarget target) : _target(target) {}
[[nodiscard]] std::optional<Amd64OnlyMatch> Match(
const std::uint8_t* data,
std::size_t length,
@ -126,6 +141,7 @@ public:
) const override;
private:
Amd64OnlyTarget _target;
Sse4aLowering _lowering;
Sha256Lowering _sha256Lowering;
Sha1Lowering _sha1Lowering;
@ -137,6 +153,7 @@ private:
[[nodiscard]] Amd64OnlyMatch _matchSha256(const DecodedInstruction& instr, std::span<const std::uint8_t> trailing) const;
[[nodiscard]] Amd64OnlyMatch _matchSha1(const DecodedInstruction& instr, std::span<const std::uint8_t> trailing) const;
[[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span<const std::uint8_t> trailing) const;
[[nodiscard]] std::optional<Amd64OnlyMatch> _matchRosetta(const DecodedInstruction& instr, std::span<const std::uint8_t> trailing) const;
};
Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const {
@ -177,6 +194,21 @@ Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchClzero(const DecodedInstructio
return _trampoline(kClzero.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _clzeroLowering.EmitOutOfLine(body, operands); }));
}
std::optional<Amd64OnlyMatch> Amd64OnlyInstructionMatcher::_matchRosetta(const DecodedInstruction& instr, std::span<const std::uint8_t> trailing) const {
if (_target != Amd64OnlyTarget::Rosetta)
return std::nullopt;
if (instr.IsRdseed()) {
std::vector<std::uint8_t> replacement(instr.Data, instr.Data + instr.Length);
replacement[instr.OpcodeOffset() + 2] &= static_cast<std::uint8_t>(~(1u << X64OpcodeConstants::ModRmRegShift));
return Amd64OnlyMatch{kRdseed.Name, instr.Length, Amd64OnlyLowering::InPlace, std::move(replacement), {}, 0};
}
if (instr.IsClwb())
return _inPlace(kClwb, instr.Length, {});
if (instr.IsRdpid())
return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true);
return std::nullopt;
}
std::optional<Amd64OnlyMatch> Amd64OnlyInstructionMatcher::MatchSequence(
std::span<const std::span<const std::uint8_t>> instructions,
std::span<const std::uint8_t> trailing
@ -211,13 +243,18 @@ std::optional<Amd64OnlyMatch> Amd64OnlyInstructionMatcher::MatchSequence(
if (name == nullptr)
name = _reciprocalEntry(*reciprocal).Name;
_reciprocalLowering.EmitOutOfLine(body, *reciprocal);
} else if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) {
if (name == nullptr)
name = kRdpid.Name;
body.Raw(_rdpidReplacement(instr));
} else {
return std::nullopt;
}
body.Advance(instr.Length);
}
_moveTrailing(body, trailing);
const bool optional = DecodeVexReciprocal(instructions.front().data(), instructions.front().size()).has_value();
const DecodedInstruction first{instructions.front().data(), instructions.front().size()};
const bool optional = DecodeVexReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid());
return _trampoline(name, instructions.front().size(), body.Finish(), optional);
}
@ -268,13 +305,13 @@ std::optional<Amd64OnlyMatch> Amd64OnlyInstructionMatcher::Match(
if (instr.IsMcommit())
return _unsupported(kMcommit, length);
return std::nullopt;
return _matchRosetta(instr, trailing);
}
}
std::unique_ptr<IAmd64OnlyInstructionMatcher> MakeAmd64OnlyInstructionMatcher() {
return std::make_unique<Amd64OnlyInstructionMatcher>();
std::unique_ptr<IAmd64OnlyInstructionMatcher> MakeAmd64OnlyInstructionMatcher(const Amd64OnlyTarget target) {
return std::make_unique<Amd64OnlyInstructionMatcher>(target);
}
}

View File

@ -262,6 +262,51 @@ bool DecodedInstruction::IsMcommit() const {
return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp7 && Data[pos + 2] == 0xFA;
}
bool DecodedInstruction::IsRdseed() const {
bool operandSizeOverride = false;
bool repnePrefix = false;
bool repPrefix = false;
const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix);
if (repnePrefix || repPrefix || pos + 2 >= Length) {
return false;
}
const auto modrm = Data[pos + 2];
return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp9 &&
((modrm >> ModRmModShift) & ModRmModMask) == ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 7;
}
bool DecodedInstruction::IsRdpid() const {
bool operandSizeOverride = false;
bool repnePrefix = false;
bool repPrefix = false;
const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix);
if (operandSizeOverride || repnePrefix || !repPrefix || pos + 2 >= Length) {
return false;
}
const auto modrm = Data[pos + 2];
return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp9 &&
((modrm >> ModRmModShift) & ModRmModMask) == ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 7;
}
bool DecodedInstruction::IsClwb() const {
bool operandSizeOverride = false;
bool repnePrefix = false;
bool repPrefix = false;
const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix);
if (!operandSizeOverride || repnePrefix || repPrefix || pos + 2 >= Length) {
return false;
}
const auto modrm = Data[pos + 2];
return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp15 &&
((modrm >> ModRmModShift) & ModRmModMask) != ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 6;
}
bool DecodedInstruction::IsMovntss() const {
bool operandSizeOverride = false;
bool repnePrefix = false;

View File

@ -649,6 +649,56 @@ void converterClzero() {
require(failureOffset([&] { (void)converter->Convert(segmentRelative, {segmentHeader(text.size())}); }, "FS-relative CLZERO was accepted") == 0x20A, "CLZERO operand failure does not carry the file offset");
}
void rosettaSubstitutions() {
using Codegen::Amd64OnlyLowering;
const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher();
const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta);
const auto match = [](const auto& matcher, const Bytes& bytes) { return matcher->Match(bytes.data(), bytes.size()); };
for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}})
require(!match(intel, bytes).has_value(), "RDSEED, RDPID or CLWB was converted for an Intel host");
const auto rdseed = match(rosetta, {0x48, 0x0F, 0xC7, 0xF9});
require(rdseed && rdseed->Lowering == Amd64OnlyLowering::InPlace && rdseed->ReplacementBytes == Bytes{0x48, 0x0F, 0xC7, 0xF1} && rdseed->InstructionName == "RDSEED", "RDSEED was not rewritten to RDRAND");
const auto rdseed16 = match(rosetta, {0x66, 0x41, 0x0F, 0xC7, 0xFA});
require(rdseed16 && rdseed16->ReplacementBytes == Bytes{0x66, 0x41, 0x0F, 0xC7, 0xF2}, "16-bit RDSEED was not rewritten to RDRAND");
require(!match(rosetta, {0x0F, 0xC7, 0x38}).has_value(), "VMPTRST (the memory form of 0F C7 /7) was taken for RDSEED");
const auto clwb = match(rosetta, {0x66, 0x0F, 0xAE, 0x74, 0x24, 0x08});
require(clwb && clwb->Lowering == Amd64OnlyLowering::InPlace && clwb->ReplacementBytes == Bytes{0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00} && clwb->InstructionName == "CLWB", "CLWB was not replaced by a NOP");
require(!match(rosetta, {0x66, 0x0F, 0xAE, 0xF1}).has_value(), "TPAUSE (the register form of 66 0F AE /6) was taken for CLWB");
require(!match(rosetta, {0x66, 0x0F, 0xAE, 0x38}).has_value(), "CLFLUSHOPT was taken for CLWB");
const auto rdpid = match(rosetta, {0xF3, 0x41, 0x0F, 0xC7, 0xFB});
require(rdpid && rdpid->Lowering == Amd64OnlyLowering::Trampoline && rdpid->Optional && rdpid->InstructionName == "RDPID", "RDPID was not lowered through an optional stub");
require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning");
}
void converterRosetta() {
const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta);
Bytes file(0x300, 0xCC);
const Bytes text = {
0x0F, 0xC7, 0xF8,
0xF3, 0x0F, 0xC7, 0xF9,
0x48, 0x83, 0xC1, 0x01,
0x66, 0x0F, 0xAE, 0x37,
0xC3};
std::copy(text.begin(), text.end(), file.begin() + 0x200);
const auto result = converter->Convert(file, {segmentHeader(text.size())});
require(result.ReplacedCount == 2 && result.Trampolines.size() == 1 && result.KeptCount == 0 && result.Reports.size() == 3, "RDSEED, RDPID and CLWB were not converted for Rosetta");
const auto& site = result.Trampolines[0];
require(site.Offset == 0x203 && site.Length == 8 && result.Reports[1].InstructionName == "RDPID", "RDPID did not absorb the following instruction");
const Bytes add = {0x48, 0x83, 0xC1, 0x01};
require(Bytes(site.Body.begin(), site.Body.begin() + 5) == Bytes{0xB9, 0, 0, 0, 0} && Bytes(site.Body.begin() + static_cast<std::ptrdiff_t>(site.ReturnBranchOffset - add.size()), site.Body.begin() + static_cast<std::ptrdiff_t>(site.ReturnBranchOffset)) == add, "RDPID stub does not load 0 and then run the absorbed instruction");
auto expected = file;
expected[0x202] = 0xF0;
const Bytes nop = {0x0F, 0x1F, 0x40, 0x00};
std::copy(nop.begin(), nop.end(), expected.begin() + 0x20B);
require(result.Bytes == expected, "RDSEED or CLWB was replaced with the wrong bytes");
auto beforeReturn = file;
beforeReturn[0x207] = 0xC3;
const auto kept = converter->Convert(beforeReturn, {segmentHeader(text.size())});
require(kept.KeptCount == 1 && kept.Trampolines.empty() && kept.ReplacedCount == 2, "RDPID without room for a jump was not kept");
const auto intel = Codegen::MakeAmd64OnlyConverter()->Convert(file, {segmentHeader(text.size())});
require(intel.Reports.empty() && intel.Bytes == file, "The Intel target converted RDSEED, RDPID or CLWB");
}
void reciprocalOperands() {
const auto vex2 = Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x52, 0xD5}.data(), 4);
require(vex2 && vex2->Operation == Codegen::ReciprocalOperation::ReciprocalSquareRoot && vex2->Destination == 2 && vex2->Source == 5, "VEX2 VRSQRTPS was not decoded");
@ -1340,6 +1390,8 @@ int main() {
converterSha1();
converterMonitorWait();
converterClzero();
rosettaSubstitutions();
converterRosetta();
converterReciprocal();
converterStrayRex();
rewriterStrayRex();

View File

@ -66,7 +66,7 @@ int Run(const int argc, char* argv[]) {
std::vector<Codegen::TrampolineSite> trampolines;
if (args.toIntel) {
const auto codeSegments = Relinker::ElfReader(sourceBytes).ReadCodeSegments();
auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(std::move(sourceBytes), codeSegments);
auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), codeSegments);
sourceBytes = std::move(converted.Bytes);
trampolines = std::move(converted.Trampolines);
std::map<std::string, std::size_t> stubsByName;
@ -108,7 +108,7 @@ int Run(const int argc, char* argv[]) {
std::vector<Relinker::GuestArtifact> guestArtifacts;
if (!args.skipSceModule) {
guestArtifacts = Relinker::GuestModuleBuilder().Build(Io::NativePath(args.inputPath), Io::NativePath(absPath), result.DynamicSection, args.toWindows, args.toMacos, args.toIntel, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules, Io::NativePath(args.sceModulePath));
guestArtifacts = Relinker::GuestModuleBuilder().Build(Io::NativePath(args.inputPath), Io::NativePath(absPath), result.DynamicSection, args.toWindows, args.toMacos, args.toIntel, args.toRosetta, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules, Io::NativePath(args.sceModulePath));
}
if (args.writeRegistry) {

View File

@ -58,7 +58,7 @@ struct GuestArtifact {
class GuestModuleBuilder {
public:
std::vector<GuestArtifact> Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set<std::string>& excludedModules, const std::filesystem::path& sceModulePath) const;
std::vector<GuestArtifact> Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, bool toRosetta, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set<std::string>& excludedModules, const std::filesystem::path& sceModulePath) const;
};
}

View File

@ -50,7 +50,7 @@ std::string ModuleStem(std::string name, const bool windows) {
}
std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set<std::string>& excludedModules, const std::filesystem::path& sceModulePath) const {
std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, const bool toRosetta, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set<std::string>& excludedModules, const std::filesystem::path& sceModulePath) const {
const auto root = std::filesystem::absolute(sceModulePath).lexically_normal();
if (!std::filesystem::exists(root)) throw Domain::RelinkerException("Guest module parent directory does not exist: " + Io::Utf8Path(root));
if (!std::filesystem::is_directory(root)) throw Domain::RelinkerException("Guest module parent path is not a directory: " + Io::Utf8Path(root));
@ -147,7 +147,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
std::vector<Domain::ProgramHeader> codeHeaders;
for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header);
if (toIntel) {
auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(std::move(image.Bytes), codeHeaders);
auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(image.Bytes), codeHeaders);
image.Trampolines = std::move(converted.Trampolines);
image.Bytes = std::move(converted.Bytes);
}

View File

@ -0,0 +1,59 @@
"""Check that --to-rosetta lowers what Rosetta lacks, and that --to-intel and a plain relink leave it alone."""
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_linux_load_alignment import fixture
RDSEED = bytes.fromhex("0fc7f8")
RDRAND = bytes.fromhex("0fc7f0")
CLWB = bytes.fromhex("660fae37")
NOP4 = bytes.fromhex("0f1f4000")
SITES = RDSEED + CLWB + b"\xc3"
LOWERED = RDRAND + NOP4 + b"\xc3"
def executable():
image = fixture()
struct.pack_into("<Q", image, 24, 0x10)
image[0x4010:0x4010 + len(SITES)] = SITES
return image
def relink(relinker, directory, name, *options):
source = Path(directory) / f"{name}.elf"
output = Path(directory) / f"{name}.out"
source.write_bytes(bytes(executable()))
result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], capture_output=True, text=True, timeout=30)
return result, output
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-rosetta-") as directory:
for options in ((), ("--to-intel",)):
result, output = relink(relinker, directory, "keep" + "".join(options), *options)
assert result.returncode == 0, (options, result.stdout, result.stderr)
data = output.read_bytes()
assert SITES in data and LOWERED not in data, f"{options or 'a plain relink'} changed RDSEED or CLWB"
for options in (("--to-rosetta",), ("--to-rosetta", "--to-intel")):
result, output = relink(relinker, directory, "lower" + "".join(options), *options)
assert result.returncode == 0, (options, result.stdout, result.stderr)
data = output.read_bytes()
assert LOWERED in data and SITES not in data, f"{options} did not lower RDSEED and CLWB"
for options, lowered in ((("--macos",), False), (("--macos", "--to-intel"), False), (("--macos", "--to-rosetta"), True)):
result, output = relink(relinker, directory, "macos" + "".join(options), *options)
assert result.returncode == 0, (options, result.stdout, result.stderr)
data = output.read_bytes()
assert data[:4] == bytes.fromhex("cffaedfe"), f"{options} did not write a Mach-O"
assert (LOWERED in data and SITES not in data) if lowered else (SITES in data and LOWERED not in data), f"{options} {'did not lower' if lowered else 'changed'} RDSEED and CLWB"
result, _ = relink(relinker, directory, "windows", "--to-rosetta", "--windows")
assert result.returncode != 0 and "--to-rosetta conflicts with --windows" in result.stdout + result.stderr, "--to-rosetta together with --windows was accepted"
print("--to-rosetta integration tests passed")
if __name__ == "__main__":
main()

View File

@ -418,6 +418,7 @@ Throughout the project, every function at every stage either **does exactly what
- [COPY_DATA from the GPU clock](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect.
- [UINT16_ABGR color exports](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws
- `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows.
- `--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 VEX.128 register forms of VRSQRTPS and 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). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site 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).
- [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates the 101-key (US) arrangement and the alphanumeric layer of the 106-key (Japanese) one, which follows the JIS layout of the HID usages: the Yen (0x89) and Ro (0x87) keys return `\` (0x5C) unshifted, as JIS X 0201 places the yen sign there; whether the console returns U+00A5 instead is unverified. With the Kana LED (bit 4, the HID position) set the 106-key arrangement throws, as the kana the console returns are unknown. Ctrl and Alt do not change the character.
- [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage.

View File

@ -33,7 +33,7 @@ macOS output (x86-64 Mach-O, which runs under Rosetta on Apple silicon):
relinker --macos source/input.elf eboot
```
Add `--to-intel` for Intel hosts. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows.
Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows.
The executable output must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. An existing output file can be replaced if it is separate from those inputs.
@ -48,6 +48,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`. |
| `--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. |
| `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis; preserve PLT imports. |
| `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. |