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:
parent
9206c3e621
commit
92b20608e8
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public:
|
|||
) const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAmd64OnlyConverter> MakeAmd64OnlyConverter();
|
||||
std::unique_ptr<IAmd64OnlyConverter> MakeAmd64OnlyConverter(Amd64OnlyTarget target = Amd64OnlyTarget::Intel);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@
|
|||
|
||||
namespace Codegen {
|
||||
|
||||
enum class Amd64OnlyTarget : std::uint8_t {
|
||||
Intel,
|
||||
Rosetta
|
||||
};
|
||||
|
||||
enum class Amd64OnlyLowering : std::uint8_t {
|
||||
InPlace,
|
||||
Trampoline,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public:
|
|||
) const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAmd64OnlyInstructionMatcher> MakeAmd64OnlyInstructionMatcher();
|
||||
std::unique_ptr<IAmd64OnlyInstructionMatcher> MakeAmd64OnlyInstructionMatcher(Amd64OnlyTarget target = Amd64OnlyTarget::Intel);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
Loading…
Reference in New Issue