mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-05 13:20:11 +00:00
The AMD-only converter moves the instructions next to a short AMD-only instruction into its stub, and refuses to move one that a branch enters. It knew only direct jump and call targets, so a switch case entered through a jump table could land inside the jump written over the moved instructions. The converter now also reads relative jump tables: a RIP-relative LEA whose address lies in a loaded segment starts a table of 32-bit offsets from that address, read while each target starts an instruction of the segment being converted. To read them, Convert takes the program headers and converts their executable load segments; the relinker passes all headers, and ElfReader::ReadCodeSegments, now unused, is removed. The converter test builds a dispatch through such a table whose second case enters the instruction after a 4-byte 256-bit store, and checks that the store takes the instruction before it instead. It also checks that reading stops at an entry that does not start an instruction, that only a LEA forms a table address, that tables are read only inside loaded segments, that only executable load segments are converted, and that a segment past the end of the file fails the conversion. PPSA12544: one 256-bit store in Il2cppUserAssemblies moved the instruction after it, which is case 1 of a switch; it now goes through an island. The other relinked files are unchanged. AI-assisted: yes
450 lines
21 KiB
C++
450 lines
21 KiB
C++
#include <codegen/IAmd64OnlyConverter.hpp>
|
|
#include <codegen/IInstructionScanner.hpp>
|
|
#include <codegen/CodegenException.hpp>
|
|
#include <codegen/x86/Amd64OnlySubstitutionTable.hpp>
|
|
#include <codegen/x86/X64InstructionDecoder.hpp>
|
|
#include <codegen/x86/X64OpcodeConstants.hpp>
|
|
#include <codegen/x86/IAmd64OnlyInstructionMatcher.hpp>
|
|
#include <codegen/x86/StubBodyBuilder.hpp>
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <span>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace Codegen {
|
|
|
|
namespace {
|
|
|
|
template<typename TOperation>
|
|
auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operation) {
|
|
try {
|
|
return operation();
|
|
} catch (const CodegenException& e) {
|
|
throw CodegenException(e.what(), base + e.FailureOffset);
|
|
}
|
|
}
|
|
|
|
constexpr std::int64_t kShortJumpSize = 2;
|
|
constexpr std::uint32_t kLoadSegment = 1;
|
|
constexpr std::uint32_t kExecutableSegment = 1;
|
|
constexpr std::uint8_t kLea = 0x8D;
|
|
constexpr std::uint64_t kIslandSearchBytes = 0x100;
|
|
|
|
struct Placement {
|
|
std::vector<TrampolineSite> Sites;
|
|
std::vector<std::size_t> Claimed;
|
|
std::vector<std::size_t> Consumed;
|
|
std::size_t ReplacementLength = 0;
|
|
};
|
|
|
|
class WindowPlacer {
|
|
public:
|
|
WindowPlacer(
|
|
std::vector<std::uint8_t>& seg,
|
|
const std::vector<InstructionMatch>& matches,
|
|
const Domain::ProgramHeader& ph,
|
|
const std::set<std::uint64_t>& branchTargets,
|
|
const std::set<std::size_t>& pending,
|
|
const std::set<std::size_t>& claimed,
|
|
const IAmd64OnlyInstructionMatcher& matcher
|
|
) : _seg(seg), _matches(matches), _ph(ph), _branchTargets(branchTargets), _pending(pending), _claimed(claimed), _matcher(matcher) {}
|
|
|
|
[[nodiscard]] std::optional<Placement> Window(const std::size_t index) const {
|
|
if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true))) {
|
|
if (auto placement = _relocate(index, index, index + 1))
|
|
return placement;
|
|
}
|
|
if (index > 0 && _movable(index - 1, false))
|
|
return _relocate(index, index - 1, index);
|
|
return std::nullopt;
|
|
}
|
|
|
|
[[nodiscard]] std::optional<Placement> Island(const std::size_t index) {
|
|
const auto store = _address(index);
|
|
for (std::size_t distance = 1;; ++distance) {
|
|
bool inReach = false;
|
|
for (const bool before : {true, false}) {
|
|
if (before ? distance > index : index + distance >= _matches.size())
|
|
continue;
|
|
const auto start = before ? index - distance : index + distance;
|
|
if (_address(start) + kIslandSearchBytes < store || _address(start) > store + kIslandSearchBytes)
|
|
continue;
|
|
inReach = true;
|
|
if (auto placement = _islandAt(index, start))
|
|
return placement;
|
|
}
|
|
if (!inReach)
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::vector<std::uint8_t>& _seg;
|
|
const std::vector<InstructionMatch>& _matches;
|
|
const Domain::ProgramHeader& _ph;
|
|
const std::set<std::uint64_t>& _branchTargets;
|
|
const std::set<std::size_t>& _pending;
|
|
const std::set<std::size_t>& _claimed;
|
|
const IAmd64OnlyInstructionMatcher& _matcher;
|
|
X64InstructionDecoder _decoder;
|
|
|
|
[[nodiscard]] std::uint64_t _address(const std::size_t index) const { return _ph.MappedAddress + _matches[index].Offset; }
|
|
[[nodiscard]] std::uint64_t _end(const std::size_t index) const { return _address(index) + _matches[index].Length; }
|
|
[[nodiscard]] std::span<const std::uint8_t> _bytes(const std::size_t index) const { return {_seg.data() + _matches[index].Offset, _matches[index].Length}; }
|
|
[[nodiscard]] DecodedInstructionInfo _info(const std::size_t index) const { return _decoder.DecodeInstruction(_seg.data() + _matches[index].Offset, _matches[index].Length); }
|
|
|
|
[[nodiscard]] bool _entered(const std::uint64_t start, const std::uint64_t end) const {
|
|
const auto hit = _branchTargets.upper_bound(start);
|
|
return hit != _branchTargets.end() && *hit < end;
|
|
}
|
|
|
|
[[nodiscard]] bool _blocksFollowing(const std::size_t index) const {
|
|
const auto kind = _info(index).FlowKind;
|
|
return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch;
|
|
}
|
|
|
|
[[nodiscard]] bool _movable(const std::size_t index, const bool last) const {
|
|
return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index));
|
|
}
|
|
|
|
[[nodiscard]] TrampolineSite _site(const std::uint64_t start, const std::size_t length, LoweredBody body) const {
|
|
const auto offset = static_cast<std::size_t>(start - _ph.MappedAddress);
|
|
return {
|
|
static_cast<Domain::FileByteOffset>(_ph.Offset + offset),
|
|
static_cast<Domain::VirtualAddress>(start),
|
|
length,
|
|
std::vector<std::uint8_t>(_seg.begin() + static_cast<std::ptrdiff_t>(offset), _seg.begin() + static_cast<std::ptrdiff_t>(offset + length)),
|
|
std::move(body.Bytes),
|
|
body.ReturnBranchOffset,
|
|
std::move(body.Relocations)
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] std::optional<Placement> _relocate(const std::size_t index, const std::size_t first, const std::size_t last) const {
|
|
if (_entered(_address(first), _end(last)))
|
|
return std::nullopt;
|
|
StubBodyBuilder body;
|
|
Placement placement;
|
|
try {
|
|
for (auto k = first; k <= last; ++k) {
|
|
if (_pending.contains(k)) {
|
|
_matcher.EmitOutOfLine(body, _bytes(k));
|
|
if (k != index)
|
|
placement.Consumed.push_back(k);
|
|
} else {
|
|
body.MoveControlFlow(_bytes(k), _info(k));
|
|
}
|
|
placement.Claimed.push_back(k);
|
|
}
|
|
} catch (const CodegenException&) {
|
|
return std::nullopt;
|
|
}
|
|
placement.Sites.push_back(_site(_address(first), static_cast<std::size_t>(_end(last) - _address(first)), body.Finish()));
|
|
placement.ReplacementLength = placement.Sites.back().Body.size();
|
|
return placement;
|
|
}
|
|
|
|
[[nodiscard]] std::optional<Placement> _islandAt(const std::size_t index, const std::size_t start) {
|
|
using Amd64OnlySubstitutionTable::kJmpRel32;
|
|
std::size_t head = 0;
|
|
for (auto last = start; last < _matches.size(); ++last) {
|
|
if (!_movable(last, true) || _entered(_address(start), _end(last)))
|
|
return std::nullopt;
|
|
if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size)
|
|
head = static_cast<std::size_t>(_end(last) - _address(start));
|
|
const auto island = _address(start) + head;
|
|
const auto reach = static_cast<std::int64_t>(island) - static_cast<std::int64_t>(_address(index) + kShortJumpSize);
|
|
if (head != 0 && (reach < std::numeric_limits<std::int8_t>::min() || reach > std::numeric_limits<std::int8_t>::max()))
|
|
return std::nullopt;
|
|
if (head != 0 && _end(last) >= island + kJmpRel32.Size) {
|
|
try {
|
|
return _buildIsland(index, start, last, head);
|
|
} catch (const CodegenException&) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
if (_blocksFollowing(last))
|
|
return std::nullopt;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
[[nodiscard]] Placement _buildIsland(const std::size_t index, const std::size_t start, const std::size_t last, const std::size_t head) {
|
|
using Amd64OnlySubstitutionTable::kJmpRel32;
|
|
const auto windowStart = _address(start);
|
|
const auto windowEnd = _end(last);
|
|
const auto island = windowStart + head;
|
|
const auto store = _address(index);
|
|
Placement placement;
|
|
|
|
StubBodyBuilder moved;
|
|
for (auto k = start; k <= last; ++k) {
|
|
moved.MoveControlFlow(_bytes(k), _info(k));
|
|
placement.Claimed.push_back(k);
|
|
}
|
|
if (!_blocksFollowing(last))
|
|
moved.JumpTo(static_cast<std::int64_t>(windowEnd - windowStart));
|
|
placement.Sites.push_back(_site(windowStart, head, moved.Finish()));
|
|
|
|
StubBodyBuilder lowered;
|
|
lowered.Seek(static_cast<std::int64_t>(store) - static_cast<std::int64_t>(island));
|
|
_matcher.EmitOutOfLine(lowered, _bytes(index));
|
|
lowered.JumpTo(static_cast<std::int64_t>(_end(index)) - static_cast<std::int64_t>(island));
|
|
placement.Sites.push_back(_site(island, kJmpRel32.Size, lowered.Finish()));
|
|
placement.ReplacementLength = placement.Sites.back().Body.size();
|
|
placement.Claimed.push_back(index);
|
|
|
|
const auto offset = _matches[index].Offset;
|
|
std::fill_n(_seg.begin() + static_cast<std::ptrdiff_t>(offset), _matches[index].Length, Amd64OnlySubstitutionTable::kTrapFill);
|
|
_seg[offset] = X64OpcodeConstants::OneByteJmpRel8;
|
|
_seg[offset + 1] = static_cast<std::uint8_t>(static_cast<std::int8_t>(static_cast<std::int64_t>(island) - static_cast<std::int64_t>(store + kShortJumpSize)));
|
|
return placement;
|
|
}
|
|
};
|
|
|
|
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>& segments
|
|
) const override;
|
|
|
|
private:
|
|
struct Pending {
|
|
std::size_t Index;
|
|
InstructionMatch Instruction;
|
|
Amd64OnlyMatch Substitution;
|
|
};
|
|
|
|
std::unique_ptr<IAmd64OnlyInstructionMatcher> _matcher;
|
|
std::unique_ptr<IInstructionScanner> _scanner = MakeInstructionScanner();
|
|
|
|
void _convertSegment(
|
|
std::vector<std::uint8_t>& fileBytes,
|
|
const Domain::ProgramHeader& ph,
|
|
const std::vector<Domain::ProgramHeader>& segments,
|
|
ConvertResult& result
|
|
) const;
|
|
|
|
[[nodiscard]] static std::set<std::uint64_t> _collectBranchTargets(
|
|
const std::vector<std::uint8_t>& seg,
|
|
const std::vector<InstructionMatch>& matches,
|
|
const Domain::ProgramHeader& ph,
|
|
const std::vector<std::uint8_t>& fileBytes,
|
|
const std::vector<Domain::ProgramHeader>& segments
|
|
);
|
|
};
|
|
|
|
std::set<std::uint64_t> Amd64OnlyConverter::_collectBranchTargets(
|
|
const std::vector<std::uint8_t>& seg,
|
|
const std::vector<InstructionMatch>& matches,
|
|
const Domain::ProgramHeader& ph,
|
|
const std::vector<std::uint8_t>& fileBytes,
|
|
const std::vector<Domain::ProgramHeader>& segments
|
|
) {
|
|
const auto startsInstruction = [&](const std::uint64_t address) {
|
|
const auto offset = address - ph.MappedAddress;
|
|
const auto match = std::lower_bound(matches.begin(), matches.end(), offset, [](const InstructionMatch& item, const std::uint64_t value) { return item.Offset < value; });
|
|
return match != matches.end() && match->Offset == offset;
|
|
};
|
|
const X64InstructionDecoder decoder;
|
|
std::set<std::uint64_t> targets;
|
|
for (const auto& match : matches) {
|
|
const auto info = decoder.DecodeInstruction(seg.data() + match.Offset, match.Length);
|
|
const auto next = static_cast<std::int64_t>(ph.MappedAddress + match.Offset + info.Length);
|
|
if (info.HasBranchTarget && !info.HasRipRelativeDisp) {
|
|
const auto target = next + info.BranchDisp;
|
|
if (target >= 0)
|
|
targets.insert(static_cast<std::uint64_t>(target));
|
|
} else if (info.HasRipRelativeDisp && info.Opcode == kLea) {
|
|
std::int32_t displacement;
|
|
std::memcpy(&displacement, seg.data() + match.Offset + info.RipRelativeDispOffset, sizeof(displacement));
|
|
const auto table = static_cast<std::uint64_t>(next + displacement);
|
|
for (const auto& data : segments) {
|
|
if (data.Type != kLoadSegment || table < data.MappedAddress || table - data.MappedAddress >= data.FileSize)
|
|
continue;
|
|
if (data.Offset > fileBytes.size() || data.FileSize > fileBytes.size() - data.Offset)
|
|
throw CodegenException("Segment exceeds the file", data.Offset);
|
|
for (auto entry = table; entry - data.MappedAddress + sizeof(std::int32_t) <= data.FileSize; entry += sizeof(std::int32_t)) {
|
|
std::int32_t relative;
|
|
std::memcpy(&relative, fileBytes.data() + data.Offset + (entry - data.MappedAddress), sizeof(relative));
|
|
const auto target = table + static_cast<std::uint64_t>(static_cast<std::int64_t>(relative));
|
|
if (!startsInstruction(target))
|
|
break;
|
|
targets.insert(target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
void Amd64OnlyConverter::_convertSegment(
|
|
std::vector<std::uint8_t>& fileBytes,
|
|
const Domain::ProgramHeader& ph,
|
|
const std::vector<Domain::ProgramHeader>& segments,
|
|
ConvertResult& result
|
|
) const {
|
|
const auto segOffset = static_cast<std::size_t>(ph.Offset);
|
|
const auto segSize = static_cast<std::size_t>(ph.FileSize);
|
|
if (segOffset > fileBytes.size() || segSize > fileBytes.size() - segOffset)
|
|
throw CodegenException("Code segment exceeds the file", ph.Offset);
|
|
|
|
std::vector<std::uint8_t> seg(
|
|
fileBytes.begin() + static_cast<std::ptrdiff_t>(segOffset),
|
|
fileBytes.begin() + static_cast<std::ptrdiff_t>(segOffset + segSize)
|
|
);
|
|
|
|
const auto matches = _atFileOffset(ph.Offset, [&] { return _scanner->ScanCodeSection(seg, 0, seg.size()); });
|
|
|
|
std::vector<Pending> pending;
|
|
for (std::size_t index = 0; index < matches.size(); ++index) {
|
|
const auto& match = matches[index];
|
|
auto substitution = _atFileOffset(ph.Offset + match.Offset, [&] { return _matcher->Match(seg.data() + match.Offset, match.Length); });
|
|
if (substitution.has_value())
|
|
pending.push_back({index, match, std::move(*substitution)});
|
|
}
|
|
if (pending.empty())
|
|
return;
|
|
|
|
const bool needsBranchTargets = std::any_of(pending.begin(), pending.end(), [](const Pending& item) {
|
|
return item.Substitution.Lowering == Amd64OnlyLowering::Trampoline;
|
|
});
|
|
const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph, fileBytes, segments) : std::set<std::uint64_t>{};
|
|
|
|
std::set<std::size_t> pendingIndices;
|
|
for (const auto& item : pending)
|
|
pendingIndices.insert(item.Index);
|
|
std::set<std::size_t> claimed;
|
|
std::set<std::size_t> consumed;
|
|
for (const auto& item : pending) {
|
|
if (consumed.contains(item.Index))
|
|
continue;
|
|
const auto& match = item.Instruction;
|
|
const auto& substitution = item.Substitution;
|
|
const auto fileOffset = static_cast<Domain::FileByteOffset>(ph.Offset + match.Offset);
|
|
const auto address = static_cast<Domain::VirtualAddress>(ph.MappedAddress + match.Offset);
|
|
std::size_t replacementLength = 0;
|
|
|
|
switch (substitution.Lowering) {
|
|
case Amd64OnlyLowering::InPlace: {
|
|
if (substitution.ReplacementBytes.size() != match.Length)
|
|
throw CodegenException("Intel substitution changes the instruction length", fileOffset);
|
|
std::copy(substitution.ReplacementBytes.begin(), substitution.ReplacementBytes.end(), seg.begin() + static_cast<std::ptrdiff_t>(match.Offset));
|
|
replacementLength = substitution.ReplacementBytes.size();
|
|
++result.ReplacedCount;
|
|
break;
|
|
}
|
|
case Amd64OnlyLowering::Trampoline: {
|
|
if (substitution.RelocatableWindow && match.Length < Amd64OnlySubstitutionTable::kJmpRel32.Size) {
|
|
WindowPlacer placer(seg, matches, ph, branchTargets, pendingIndices, claimed, *_matcher);
|
|
auto placement = placer.Window(item.Index);
|
|
if (!placement)
|
|
placement = placer.Island(item.Index);
|
|
if (!placement)
|
|
throw CodegenException("No room for a jump to the stub of " + substitution.InstructionName + " within its neighbours or a nearby island", fileOffset);
|
|
for (auto& site : placement->Sites)
|
|
result.Trampolines.push_back(std::move(site));
|
|
claimed.insert(placement->Claimed.begin(), placement->Claimed.end());
|
|
consumed.insert(placement->Consumed.begin(), placement->Consumed.end());
|
|
replacementLength = placement->ReplacementLength;
|
|
break;
|
|
}
|
|
std::vector<std::size_t> taken;
|
|
try {
|
|
std::size_t siteLength = match.Length;
|
|
auto stub = substitution;
|
|
if (siteLength < Amd64OnlySubstitutionTable::kJmpRel32.Size) {
|
|
const X64InstructionDecoder decoder;
|
|
std::vector<std::span<const std::uint8_t>> sequence{std::span<const std::uint8_t>(seg.data() + match.Offset, match.Length)};
|
|
std::size_t trailingBytes = 0;
|
|
for (auto next = item.Index + 1; siteLength < Amd64OnlySubstitutionTable::kJmpRel32.Size; ++next) {
|
|
if (next >= matches.size() || matches[next].Offset != match.Offset + siteLength)
|
|
throw CodegenException("AMD-only instruction too short for a jump and not followed by an instruction", fileOffset);
|
|
const auto& following = matches[next];
|
|
const std::span<const std::uint8_t> bytes(seg.data() + following.Offset, following.Length);
|
|
const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size());
|
|
const bool amdOnly = _matcher->Match(bytes.data(), bytes.size()).has_value();
|
|
if (amdOnly && trailingBytes == 0) {
|
|
sequence.push_back(bytes);
|
|
taken.push_back(next);
|
|
} else if (amdOnly || info.FlowKind != ControlFlowKind::Sequential || info.HasBranchTarget) {
|
|
throw CodegenException("AMD-only instruction too short for a jump is followed by an instruction that cannot move", ph.Offset + following.Offset);
|
|
} else {
|
|
trailingBytes += following.Length;
|
|
}
|
|
siteLength += following.Length;
|
|
}
|
|
const std::span<const std::uint8_t> trailing(seg.data() + match.Offset + siteLength - trailingBytes, trailingBytes);
|
|
auto relocated = _atFileOffset(fileOffset, [&] { return _matcher->MatchSequence(sequence, trailing); });
|
|
if (!relocated.has_value() || relocated->Lowering != Amd64OnlyLowering::Trampoline)
|
|
throw CodegenException("AMD-only instruction sequence has no out-of-line lowering", fileOffset);
|
|
stub = std::move(*relocated);
|
|
}
|
|
const auto hit = branchTargets.upper_bound(address);
|
|
if (hit != branchTargets.end() && *hit < address + siteLength)
|
|
throw CodegenException("Branch enters an AMD-only instruction", ph.Offset + (*hit - ph.MappedAddress));
|
|
const auto begin = seg.begin() + static_cast<std::ptrdiff_t>(match.Offset);
|
|
result.Trampolines.push_back({
|
|
fileOffset,
|
|
address,
|
|
siteLength,
|
|
std::vector<std::uint8_t>(begin, begin + static_cast<std::ptrdiff_t>(siteLength)),
|
|
stub.StubBody,
|
|
stub.ReturnBranchOffset,
|
|
stub.Relocations
|
|
});
|
|
replacementLength = stub.StubBody.size();
|
|
consumed.insert(taken.begin(), taken.end());
|
|
for (auto k = item.Index; k < matches.size() && matches[k].Offset < match.Offset + siteLength; ++k)
|
|
claimed.insert(k);
|
|
} catch (const CodegenException&) {
|
|
if (!substitution.Optional)
|
|
throw;
|
|
++result.KeptCount;
|
|
result.Reports.push_back({substitution.InstructionName, fileOffset, match.Length, 0, Amd64OnlyLowering::Kept});
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
case Amd64OnlyLowering::Kept:
|
|
case Amd64OnlyLowering::Unsupported:
|
|
throw CodegenException("AMD-only instruction without Intel lowering: " + substitution.InstructionName, fileOffset);
|
|
}
|
|
|
|
result.Reports.push_back({substitution.InstructionName, fileOffset, match.Length, replacementLength, substitution.Lowering});
|
|
}
|
|
|
|
if (seg.size() != segSize)
|
|
throw CodegenException("Code segment size changed during Intel conversion", ph.Offset);
|
|
std::copy(seg.begin(), seg.end(), fileBytes.begin() + static_cast<std::ptrdiff_t>(segOffset));
|
|
}
|
|
|
|
ConvertResult Amd64OnlyConverter::Convert(
|
|
std::vector<std::uint8_t> fileBytes,
|
|
const std::vector<Domain::ProgramHeader>& segments
|
|
) const {
|
|
ConvertResult result{};
|
|
for (const auto& ph : segments) {
|
|
if (ph.Type == kLoadSegment && (ph.Flags & kExecutableSegment) != 0)
|
|
_convertSegment(fileBytes, ph, segments, result);
|
|
}
|
|
result.Bytes = std::move(fileBytes);
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
std::unique_ptr<IAmd64OnlyConverter> MakeAmd64OnlyConverter(const Amd64OnlyTarget target) {
|
|
return std::make_unique<Amd64OnlyConverter>(target);
|
|
}
|
|
|
|
}
|