mirror of
https://github.com/stenzek/duckstation.git
synced 2026-07-11 01:24:11 +02:00
Cheats: Support branch labels in assembly codes
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -34,6 +35,30 @@ struct AssemblyCase
|
||||
u32 bits;
|
||||
};
|
||||
|
||||
struct LabelAssemblyCase
|
||||
{
|
||||
std::string_view label_text;
|
||||
std::string_view forward_text;
|
||||
std::string_view backward_text;
|
||||
};
|
||||
|
||||
struct LabelAssemblyInstructions
|
||||
{
|
||||
u32* instructions;
|
||||
size_t count;
|
||||
u32 base_pc;
|
||||
};
|
||||
|
||||
static u32* GetLabelAssemblyInstruction(const u32 pc, void* userdata)
|
||||
{
|
||||
const LabelAssemblyInstructions* const code = static_cast<LabelAssemblyInstructions*>(userdata);
|
||||
if (pc < code->base_pc || ((pc - code->base_pc) % CPU::INSTRUCTION_SIZE) != 0)
|
||||
return nullptr;
|
||||
|
||||
const size_t index = (pc - code->base_pc) / CPU::INSTRUCTION_SIZE;
|
||||
return (index < code->count) ? &code->instructions[index] : nullptr;
|
||||
}
|
||||
|
||||
TEST(CPUDisasm, Disassemble)
|
||||
{
|
||||
EXPECT_EQ(Disassemble(0x1000, 0x00000000), "sll zero, zero, 0");
|
||||
@@ -140,6 +165,95 @@ TEST(CPUDisasm, Assemble)
|
||||
EXPECT_EQ(Assemble(0x1000, test.text), test.bits) << test.text;
|
||||
}
|
||||
|
||||
TEST(CPUDisasm, JumpTargets)
|
||||
{
|
||||
struct JumpType
|
||||
{
|
||||
std::string_view mnemonic;
|
||||
u32 opcode;
|
||||
};
|
||||
|
||||
static constexpr std::array jump_types = {
|
||||
JumpType{"j", 0x08000000},
|
||||
JumpType{"jal", 0x0C000000},
|
||||
};
|
||||
static constexpr std::array<u32, 5> regions = {0x00000000, 0x10000000, 0x80000000, 0xA0000000, 0xF0000000};
|
||||
static constexpr std::array<u32, 4> target_fields = {0x0000000, 0x0000001, 0x0123456, 0x3FFFFFF};
|
||||
|
||||
for (const JumpType& jump : jump_types)
|
||||
{
|
||||
for (const u32 region : regions)
|
||||
{
|
||||
const u32 pc = region | 0x1000;
|
||||
for (const u32 target_field : target_fields)
|
||||
{
|
||||
const u32 target = region | (target_field << 2);
|
||||
const u32 bits = jump.opcode | target_field;
|
||||
const std::string text = fmt::format("{} 0x{:08x}", jump.mnemonic, target);
|
||||
SCOPED_TRACE(fmt::format("pc=0x{:08x}, instruction={}", pc, text));
|
||||
EXPECT_EQ(Assemble(pc, text), bits);
|
||||
EXPECT_EQ(Disassemble(pc, bits), text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr std::array boundary_cases = {
|
||||
AssemblyCase{"j 0x10000000", 0x08000000},
|
||||
AssemblyCase{"jal 0x10000000", 0x0C000000},
|
||||
};
|
||||
for (const AssemblyCase& test : boundary_cases)
|
||||
{
|
||||
EXPECT_EQ(Assemble(0x0FFFFFFC, test.text), test.bits);
|
||||
EXPECT_EQ(Disassemble(0x0FFFFFFC, test.bits), test.text);
|
||||
}
|
||||
|
||||
EXPECT_EQ(Assemble(0xFFFFFFFC, "j 0x00000000"), 0x08000000u);
|
||||
EXPECT_EQ(Disassemble(0xFFFFFFFC, 0x08000000), "j 0x00000000");
|
||||
}
|
||||
|
||||
TEST(CPUDisasm, BranchTargetBoundaries)
|
||||
{
|
||||
static constexpr std::array<s32, 5> word_offsets = {
|
||||
std::numeric_limits<s16>::min(), -1, 0, 1, std::numeric_limits<s16>::max(),
|
||||
};
|
||||
static constexpr u32 pc = 0x80010000;
|
||||
for (const s32 word_offset : word_offsets)
|
||||
{
|
||||
const u32 target = (pc + 4) + static_cast<u32>(word_offset * 4);
|
||||
const u32 bits = 0x11090000 | static_cast<u16>(word_offset);
|
||||
const std::string text = fmt::format("beq t0, t1, 0x{:08x}", target);
|
||||
SCOPED_TRACE(text);
|
||||
EXPECT_EQ(Assemble(pc, text), bits);
|
||||
EXPECT_EQ(Disassemble(pc, bits), text);
|
||||
}
|
||||
|
||||
EXPECT_EQ(Assemble(0xFFFFFFFC, "beq t0, t1, 0x00000000"), 0x11090000u);
|
||||
EXPECT_EQ(Disassemble(0xFFFFFFFC, 0x11090000), "beq t0, t1, 0x00000000");
|
||||
}
|
||||
|
||||
TEST(CPUDisasm, ControlFlowTargetErrorsDoNotModifyDestination)
|
||||
{
|
||||
struct ErrorCase
|
||||
{
|
||||
u32 pc;
|
||||
std::string_view text;
|
||||
};
|
||||
static constexpr std::array cases = {
|
||||
ErrorCase{0x80001000, "j 0x90001000"}, ErrorCase{0x80001000, "jal 0x80001002"},
|
||||
ErrorCase{0x0FFFFFF8, "j 0x10000000"}, ErrorCase{0x80010000, "beq t0, t1, 0x80030004"},
|
||||
ErrorCase{0x80010000, "beq t0, t1, 0x7fff0000"},
|
||||
};
|
||||
|
||||
for (const ErrorCase& test : cases)
|
||||
{
|
||||
u32 result = 0xDEADBEEF;
|
||||
Error error;
|
||||
EXPECT_FALSE(CPU::AssembleInstruction(&result, test.pc, test.text, &error)) << test.text;
|
||||
EXPECT_EQ(result, 0xDEADBEEFu) << test.text;
|
||||
EXPECT_TRUE(error.IsValid()) << test.text;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CPUDisasm, AssembleErrorsDoNotModifyDestination)
|
||||
{
|
||||
static constexpr std::array<std::string_view, 14> cases = {
|
||||
@@ -160,6 +274,54 @@ TEST(CPUDisasm, AssembleErrorsDoNotModifyDestination)
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CPUDisasm, AssembleBranchLabels)
|
||||
{
|
||||
static constexpr std::array cases = {
|
||||
LabelAssemblyCase{"j @target", "j 0x1040", "j 0x1000"},
|
||||
LabelAssemblyCase{"jal @target", "jal 0x1040", "jal 0x1000"},
|
||||
LabelAssemblyCase{"beq t1, t2, @target", "beq t1, t2, 0x1040", "beq t1, t2, 0x1000"},
|
||||
LabelAssemblyCase{"bne t1, t2, @target", "bne t1, t2, 0x1040", "bne t1, t2, 0x1000"},
|
||||
LabelAssemblyCase{"blez t1, @target", "blez t1, 0x1040", "blez t1, 0x1000"},
|
||||
LabelAssemblyCase{"bgtz t1, @target", "bgtz t1, 0x1040", "bgtz t1, 0x1000"},
|
||||
LabelAssemblyCase{"bltz t0, @target", "bltz t0, 0x1040", "bltz t0, 0x1000"},
|
||||
LabelAssemblyCase{"bgez t0, @target", "bgez t0, 0x1040", "bgez t0, 0x1000"},
|
||||
LabelAssemblyCase{"bltzal t0, @target", "bltzal t0, 0x1040", "bltzal t0, 0x1000"},
|
||||
LabelAssemblyCase{"bgezal t0, @target", "bgezal t0, 0x1040", "bgezal t0, 0x1000"},
|
||||
};
|
||||
|
||||
CPU::AssemblyLabelList forward_labels;
|
||||
std::array<u32, cases.size()> forward_results = {};
|
||||
for (size_t i = 0; i < cases.size(); i++)
|
||||
{
|
||||
Error error;
|
||||
ASSERT_TRUE(CPU::AssembleInstruction(&forward_results[i], 0x1000 + static_cast<u32>(i * 4), cases[i].label_text,
|
||||
&forward_labels, &error))
|
||||
<< error.GetDescription();
|
||||
}
|
||||
EXPECT_TRUE(CPU::ContainsUnresolvedLabels(forward_labels));
|
||||
|
||||
Error error;
|
||||
CPU::AssemblyLabel* label;
|
||||
ASSERT_TRUE((label = CPU::DefineAssemblyLabel(&forward_labels, "@target", 0x1040, &error))) << error.GetDescription();
|
||||
EXPECT_FALSE(CPU::ContainsUnresolvedLabels(forward_labels));
|
||||
LabelAssemblyInstructions forward_code = {forward_results.data(), forward_results.size(), 0x1000};
|
||||
ASSERT_TRUE(CPU::FixupAssemblyLabelBackreferences(label, &forward_code, &error, GetLabelAssemblyInstruction))
|
||||
<< error.GetDescription();
|
||||
for (size_t i = 0; i < cases.size(); i++)
|
||||
EXPECT_EQ(forward_results[i], Assemble(0x1000 + static_cast<u32>(i * 4), cases[i].forward_text));
|
||||
|
||||
CPU::AssemblyLabelList backward_labels;
|
||||
ASSERT_TRUE(CPU::DefineAssemblyLabel(&backward_labels, "@target", 0x1000, &error)) << error.GetDescription();
|
||||
for (size_t i = 0; i < cases.size(); i++)
|
||||
{
|
||||
const u32 pc = 0x1040 + static_cast<u32>(i * 4);
|
||||
u32 result;
|
||||
ASSERT_TRUE(CPU::AssembleInstruction(&result, pc, cases[i].label_text, &backward_labels, &error))
|
||||
<< error.GetDescription();
|
||||
EXPECT_EQ(result, Assemble(pc, cases[i].backward_text));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CPUDisasm, RoundTrip)
|
||||
{
|
||||
static constexpr std::array<u32, 54> instructions = {
|
||||
|
||||
+61
-29
@@ -4617,10 +4617,10 @@ std::unique_ptr<Cheats::AssemblyCheatCode> Cheats::AssemblyCheatCode::Parse(Meta
|
||||
}
|
||||
|
||||
std::unique_ptr<AssemblyCheatCode> code = std::make_unique<AssemblyCheatCode>(std::move(metadata));
|
||||
code->instructions.reserve(static_cast<size_t>(std::ranges::count(data, '\n')) + 1);
|
||||
CPU::AssemblyLabelList labels;
|
||||
CheatFileReader reader(data);
|
||||
std::optional<u32> pc;
|
||||
u32 section_line = 0;
|
||||
bool section_has_instruction = false;
|
||||
std::string_view line;
|
||||
while (reader.GetLine(&line))
|
||||
{
|
||||
@@ -4631,14 +4631,47 @@ std::unique_ptr<Cheats::AssemblyCheatCode> Cheats::AssemblyCheatCode::Parse(Meta
|
||||
if (linev.empty())
|
||||
continue;
|
||||
|
||||
if (linev.starts_with(".org "))
|
||||
const size_t colon_pos = linev.find(':');
|
||||
if (colon_pos != std::string_view::npos)
|
||||
{
|
||||
if (pc.has_value() && !section_has_instruction)
|
||||
const std::string_view label_name = StringUtil::StripWhitespace(linev.substr(0, colon_pos));
|
||||
if (label_name.empty() || label_name.front() == '+' || label_name.front() == '-' ||
|
||||
(label_name.front() >= '0' && label_name.front() <= '9') || label_name.find('?') != std::string_view::npos ||
|
||||
label_name.find_first_of(" \t\r\n") != std::string_view::npos)
|
||||
{
|
||||
Error::SetStringFmt(error, "No instructions after address at line {}.", section_line);
|
||||
Error::SetStringFmt(error, "Malformed label at line {}: {}", reader.GetCurrentLineNumber(), linev);
|
||||
return {};
|
||||
}
|
||||
if (!pc.has_value())
|
||||
{
|
||||
Error::SetStringFmt(error, "Label before starting address at line {}: {}", reader.GetCurrentLineNumber(),
|
||||
linev);
|
||||
return {};
|
||||
}
|
||||
|
||||
CPU::AssemblyLabel* label = CPU::DefineAssemblyLabel(&labels, label_name, pc.value(), error);
|
||||
if (!label)
|
||||
return {};
|
||||
|
||||
if (!CPU::FixupAssemblyLabelBackreferences(label, code.get(), error, [](u32 pc, void* userdata) {
|
||||
for (Instruction& inst : static_cast<AssemblyCheatCode*>(userdata)->instructions)
|
||||
{
|
||||
if (inst.pc == pc)
|
||||
return &inst.new_value;
|
||||
}
|
||||
return static_cast<u32*>(nullptr);
|
||||
}))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
linev = StringUtil::StripWhitespace(linev.substr(colon_pos + 1));
|
||||
if (linev.empty())
|
||||
continue;
|
||||
}
|
||||
|
||||
if (linev.starts_with(".org "))
|
||||
{
|
||||
std::string_view address_text = StringUtil::StripWhitespace(linev.substr(4));
|
||||
if (address_text.starts_with("0x") || address_text.starts_with("0X"))
|
||||
address_text.remove_prefix(2);
|
||||
@@ -4657,8 +4690,6 @@ std::unique_ptr<Cheats::AssemblyCheatCode> Cheats::AssemblyCheatCode::Parse(Meta
|
||||
}
|
||||
|
||||
pc = address.value();
|
||||
section_line = reader.GetCurrentLineNumber();
|
||||
section_has_instruction = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4669,12 +4700,22 @@ std::unique_ptr<Cheats::AssemblyCheatCode> Cheats::AssemblyCheatCode::Parse(Meta
|
||||
return {};
|
||||
}
|
||||
|
||||
u32 bits;
|
||||
const u32 instruction_pc = pc.value();
|
||||
if (std::ranges::any_of(code->instructions,
|
||||
[instruction_pc](const Instruction& inst) { return inst.pc == instruction_pc; }))
|
||||
{
|
||||
Error::SetStringFmt(error, "Duplicate instruction address 0x{:08X} at line {}.", instruction_pc,
|
||||
reader.GetCurrentLineNumber());
|
||||
return {};
|
||||
}
|
||||
|
||||
code->instructions.emplace_back(instruction_pc, 0, UNINITIALIZED_OLD_VALUE);
|
||||
u32& bits = code->instructions.back().new_value;
|
||||
Error assemble_error;
|
||||
const size_t wildcard_pos = linev.find('?');
|
||||
if (wildcard_pos == std::string_view::npos)
|
||||
{
|
||||
if (!CPU::AssembleInstruction(&bits, static_cast<u32>(pc.value()), linev, &assemble_error))
|
||||
if (!CPU::AssembleInstruction(&bits, instruction_pc, linev, &labels, &assemble_error))
|
||||
{
|
||||
Error::SetStringFmt(error, "Failed to assemble instruction at line {}: {}", reader.GetCurrentLineNumber(),
|
||||
assemble_error.GetDescription());
|
||||
@@ -4709,8 +4750,8 @@ std::unique_ptr<Cheats::AssemblyCheatCode> Cheats::AssemblyCheatCode::Parse(Meta
|
||||
}
|
||||
|
||||
u32 one_bits;
|
||||
if (!CPU::AssembleInstruction(&bits, static_cast<u32>(pc.value()), zero_instruction, &assemble_error) ||
|
||||
!CPU::AssembleInstruction(&one_bits, static_cast<u32>(pc.value()), one_instruction, &assemble_error))
|
||||
if (!CPU::AssembleInstruction(&bits, instruction_pc, zero_instruction, &labels, &assemble_error) ||
|
||||
!CPU::AssembleInstruction(&one_bits, instruction_pc, one_instruction, &labels, &assemble_error))
|
||||
{
|
||||
Error::SetStringFmt(error, "Failed to assemble wildcard instruction at line {}: {}",
|
||||
reader.GetCurrentLineNumber(), assemble_error.GetDescription());
|
||||
@@ -4730,35 +4771,26 @@ std::unique_ptr<Cheats::AssemblyCheatCode> Cheats::AssemblyCheatCode::Parse(Meta
|
||||
return {};
|
||||
}
|
||||
|
||||
code->option_instruction_values.emplace_back(static_cast<u32>(code->instructions.size()),
|
||||
code->option_instruction_values.emplace_back(static_cast<u32>(code->instructions.size() - 1),
|
||||
static_cast<u8>(option_bit_position),
|
||||
static_cast<u8>(option_bit_count));
|
||||
}
|
||||
|
||||
const u32 instruction_pc = static_cast<u32>(pc.value());
|
||||
if (std::ranges::any_of(code->instructions,
|
||||
[instruction_pc](const Instruction& inst) { return inst.pc == instruction_pc; }))
|
||||
{
|
||||
Error::SetStringFmt(error, "Duplicate instruction address 0x{:08X} at line {}.", instruction_pc,
|
||||
reader.GetCurrentLineNumber());
|
||||
return {};
|
||||
}
|
||||
|
||||
code->instructions.push_back({instruction_pc, bits, UNINITIALIZED_OLD_VALUE});
|
||||
section_has_instruction = true;
|
||||
pc = pc.value() + CPU::INSTRUCTION_SIZE;
|
||||
pc = instruction_pc + CPU::INSTRUCTION_SIZE;
|
||||
}
|
||||
|
||||
if (pc.has_value() && !section_has_instruction)
|
||||
{
|
||||
Error::SetStringFmt(error, "No instructions after address at line {}.", section_line);
|
||||
return {};
|
||||
}
|
||||
if (code->instructions.empty())
|
||||
{
|
||||
Error::SetStringView(error, "No instructions in code.");
|
||||
return {};
|
||||
}
|
||||
if (CPU::ContainsUnresolvedLabels(labels))
|
||||
{
|
||||
const auto iter =
|
||||
std::ranges::find_if(labels, [](const CPU::AssemblyLabel& label) { return !label.address.has_value(); });
|
||||
Error::SetStringFmt(error, "Undefined label '{}'.", iter->name);
|
||||
return {};
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
+146
-29
@@ -55,9 +55,13 @@ static bool ParseGPR(std::string_view text, u32* index);
|
||||
static bool ParseNumericRegister(std::string_view text, u32* index);
|
||||
static bool ParseCopRegister(std::string_view text, u32 cop_n, bool control, u32* index);
|
||||
static bool SetField(u32* bits, u32 shift, u32 width, u32 value);
|
||||
static bool AssembleFormat(u32* bits, u32 pc, const char* format, std::string_view text, Error* error);
|
||||
static bool EncodeAssemblyTarget(u32* bits, u32 pc, u32 target, bool jump, Error* error);
|
||||
static std::optional<u32> ResolveAssemblyTarget(AssemblyLabelList* labels, std::string_view operand, u32 placeholder,
|
||||
u32* instruction, u32 pc);
|
||||
static bool AssembleFormat(u32* bits, u32* instruction, u32 pc, const char* format, std::string_view text,
|
||||
AssemblyLabelList* labels, Error* error);
|
||||
static AssembleResult TryTableEntry(u32* dest, u32 pc, std::string_view text, const char* format, u32 base_bits,
|
||||
Error* error);
|
||||
AssemblyLabelList* labels, Error* error);
|
||||
static AssembleResult AssembleGTEInstruction(u32* dest, std::string_view text, Error* error);
|
||||
|
||||
static const std::array<const char*, 64> s_base_table = {{
|
||||
@@ -966,7 +970,60 @@ bool CPU::SetField(u32* bits, u32 shift, u32 width, u32 value)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CPU::AssembleFormat(u32* bits, u32 pc, const char* format, std::string_view text, Error* error)
|
||||
bool CPU::EncodeAssemblyTarget(u32* bits, const u32 pc, const u32 target, const bool jump, Error* error)
|
||||
{
|
||||
if (jump)
|
||||
{
|
||||
if ((target & 3) != 0 || (target & 0xF0000000u) != ((pc + 4) & 0xF0000000u))
|
||||
{
|
||||
Error::SetStringFmt(error, "Jump target 0x{:08X} is outside the current 256 MB region.", target);
|
||||
return false;
|
||||
}
|
||||
|
||||
SetField(bits, 0, 26, (target & ~0xF0000000u) >> 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
const u32 delta = target - (pc + 4);
|
||||
const s64 word_delta = static_cast<s64>(static_cast<s32>(delta)) / 4;
|
||||
if ((delta & 3) != 0 || word_delta < std::numeric_limits<s16>::min() || word_delta > std::numeric_limits<s16>::max())
|
||||
{
|
||||
Error::SetStringFmt(error, "Branch target 0x{:08X} is out of range.", target);
|
||||
return false;
|
||||
}
|
||||
|
||||
SetField(bits, 0, 16, static_cast<u16>(word_delta));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<u32> CPU::ResolveAssemblyTarget(AssemblyLabelList* labels, const std::string_view operand,
|
||||
const u32 placeholder, u32* instruction, const u32 pc)
|
||||
{
|
||||
if (!labels || operand.empty() || operand.front() == '+' || operand.front() == '-' ||
|
||||
(operand.front() >= '0' && operand.front() <= '9'))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto iter = std::ranges::find(*labels, operand, &AssemblyLabel::name);
|
||||
if (iter == labels->end())
|
||||
{
|
||||
labels->emplace_back(std::string(operand), std::nullopt, std::vector<u32>());
|
||||
labels->back().backreferences.push_back(pc);
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
if (!iter->address.has_value())
|
||||
{
|
||||
if (std::ranges::find(iter->backreferences, pc) == iter->backreferences.end())
|
||||
iter->backreferences.push_back(pc);
|
||||
}
|
||||
|
||||
return iter->address.value_or(placeholder);
|
||||
}
|
||||
|
||||
bool CPU::AssembleFormat(u32* bits, u32* instruction, u32 pc, const char* format, std::string_view text,
|
||||
AssemblyLabelList* labels, Error* error)
|
||||
{
|
||||
const std::string_view format_view(format);
|
||||
const size_t format_space = format_view.find_first_of(" \t\r\n");
|
||||
@@ -1047,40 +1104,46 @@ bool CPU::AssembleFormat(u32* bits, u32 pc, const char* format, std::string_view
|
||||
else if (type == "$rel")
|
||||
{
|
||||
u64 target_value;
|
||||
if (!ParseUnsignedValue(operand, &target_value) || target_value > std::numeric_limits<u32>::max() ||
|
||||
(target_value & 3) != 0)
|
||||
if (!ParseUnsignedValue(operand, &target_value))
|
||||
{
|
||||
const std::optional<u32> label_target = ResolveAssemblyTarget(labels, operand, pc + 4, instruction, pc);
|
||||
if (!label_target.has_value())
|
||||
{
|
||||
Error::SetStringFmt(error, "Branch target '{}' is not an aligned 32-bit address.", operand);
|
||||
return false;
|
||||
}
|
||||
target_value = label_target.value();
|
||||
}
|
||||
if (target_value > std::numeric_limits<u32>::max() || (target_value & 3) != 0)
|
||||
{
|
||||
Error::SetStringFmt(error, "Branch target '{}' is not an aligned 32-bit address.", operand);
|
||||
return false;
|
||||
}
|
||||
|
||||
const u32 delta = static_cast<u32>(target_value) - (pc + 4);
|
||||
const s64 word_delta = static_cast<s64>(static_cast<s32>(delta)) / 4;
|
||||
if ((delta & 3) != 0 || word_delta < std::numeric_limits<s16>::min() ||
|
||||
word_delta > std::numeric_limits<s16>::max())
|
||||
{
|
||||
Error::SetStringFmt(error, "Branch target '{}' is out of range.", operand);
|
||||
if (!EncodeAssemblyTarget(bits, pc, static_cast<u32>(target_value), false, error))
|
||||
return false;
|
||||
}
|
||||
SetField(bits, 0, 16, static_cast<u16>(word_delta));
|
||||
}
|
||||
else if (type == "$jt")
|
||||
{
|
||||
u64 target_value;
|
||||
if (!ParseUnsignedValue(operand, &target_value) || target_value > std::numeric_limits<u32>::max() ||
|
||||
(target_value & 3) != 0)
|
||||
if (!ParseUnsignedValue(operand, &target_value))
|
||||
{
|
||||
const std::optional<u32> label_target = ResolveAssemblyTarget(labels, operand, pc + 4, instruction, pc);
|
||||
if (!label_target.has_value())
|
||||
{
|
||||
Error::SetStringFmt(error, "Jump target '{}' is not an aligned 32-bit address.", operand);
|
||||
return false;
|
||||
}
|
||||
target_value = label_target.value();
|
||||
}
|
||||
if (target_value > std::numeric_limits<u32>::max() || (target_value & 3) != 0)
|
||||
{
|
||||
Error::SetStringFmt(error, "Jump target '{}' is not an aligned 32-bit address.", operand);
|
||||
return false;
|
||||
}
|
||||
|
||||
const u32 target = static_cast<u32>(target_value);
|
||||
if ((target & 0xF0000000u) != ((pc + 4) & 0xF0000000u))
|
||||
{
|
||||
Error::SetStringFmt(error, "Jump target '{}' is outside the current 256 MB region.", operand);
|
||||
if (!EncodeAssemblyTarget(bits, pc, static_cast<u32>(target_value), true, error))
|
||||
return false;
|
||||
}
|
||||
SetField(bits, 0, 26, target >> 2);
|
||||
}
|
||||
else if (type == "$offsetrs")
|
||||
{
|
||||
@@ -1118,7 +1181,7 @@ bool CPU::AssembleFormat(u32* bits, u32 pc, const char* format, std::string_view
|
||||
}
|
||||
|
||||
CPU::AssembleResult CPU::TryTableEntry(u32* dest, u32 pc, std::string_view text, const char* format, u32 base_bits,
|
||||
Error* error)
|
||||
AssemblyLabelList* labels, Error* error)
|
||||
{
|
||||
if (!format) // Unknown
|
||||
return AssembleResult::NoMatch;
|
||||
@@ -1128,7 +1191,7 @@ CPU::AssembleResult CPU::TryTableEntry(u32* dest, u32 pc, std::string_view text,
|
||||
return AssembleResult::NoMatch;
|
||||
|
||||
u32 bits = base_bits;
|
||||
if (!AssembleFormat(&bits, pc, format, text, error))
|
||||
if (!AssembleFormat(&bits, dest, pc, format, text, labels, error))
|
||||
return AssembleResult::Error;
|
||||
|
||||
*dest = bits;
|
||||
@@ -1225,6 +1288,11 @@ CPU::AssembleResult CPU::AssembleGTEInstruction(u32* dest, std::string_view text
|
||||
}
|
||||
|
||||
bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* error)
|
||||
{
|
||||
return AssembleInstruction(dest, pc, text, nullptr, error);
|
||||
}
|
||||
|
||||
bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, AssemblyLabelList* labels, Error* error)
|
||||
{
|
||||
if (!dest)
|
||||
{
|
||||
@@ -1242,7 +1310,7 @@ bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* e
|
||||
const std::string_view mnemonic = GetMnemonic(text);
|
||||
if (StringUtil::EqualNoCase(mnemonic, "nop"))
|
||||
{
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, "nop", 0, error);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, "nop", 0, labels, error);
|
||||
return (result == AssembleResult::Success);
|
||||
}
|
||||
|
||||
@@ -1254,14 +1322,14 @@ bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* e
|
||||
continue;
|
||||
}
|
||||
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, s_base_table[op], op << 26, error);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, s_base_table[op], op << 26, labels, error);
|
||||
if (result != AssembleResult::NoMatch)
|
||||
return (result == AssembleResult::Success);
|
||||
}
|
||||
|
||||
for (u32 funct = 0; funct < s_special_table.size(); funct++)
|
||||
{
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, s_special_table[funct], funct, error);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, s_special_table[funct], funct, labels, error);
|
||||
if (result != AssembleResult::NoMatch)
|
||||
return (result == AssembleResult::Success);
|
||||
}
|
||||
@@ -1275,7 +1343,7 @@ bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* e
|
||||
for (const auto& [rt, format] : branch_table)
|
||||
{
|
||||
const u32 base_bits = (static_cast<u32>(InstructionOp::b) << 26) | (rt << 16);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, error);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, labels, error);
|
||||
if (result != AssembleResult::NoMatch)
|
||||
return (result == AssembleResult::Success);
|
||||
}
|
||||
@@ -1286,7 +1354,7 @@ bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* e
|
||||
{
|
||||
const u32 base_bits =
|
||||
((static_cast<u32>(InstructionOp::cop0) + cop_n) << 26) | (static_cast<u32>(common_op) << 21);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, error);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, labels, error);
|
||||
if (result != AssembleResult::NoMatch)
|
||||
return (result == AssembleResult::Success);
|
||||
}
|
||||
@@ -1296,7 +1364,7 @@ bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* e
|
||||
{
|
||||
const u32 base_bits =
|
||||
(static_cast<u32>(InstructionOp::cop0) << 26) | (UINT32_C(1) << 25) | static_cast<u32>(cop0_op);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, error);
|
||||
const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, labels, error);
|
||||
if (result != AssembleResult::NoMatch)
|
||||
return (result == AssembleResult::Success);
|
||||
}
|
||||
@@ -1322,3 +1390,52 @@ bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* e
|
||||
Error::SetStringFmt(error, "Unknown instruction mnemonic '{}'.", mnemonic);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CPU::ContainsUnresolvedLabels(const AssemblyLabelList& labels)
|
||||
{
|
||||
return std::ranges::any_of(labels, [](const AssemblyLabel& label) { return !label.address.has_value(); });
|
||||
}
|
||||
|
||||
CPU::AssemblyLabel* CPU::DefineAssemblyLabel(AssemblyLabelList* labels, const std::string_view name, const u32 address,
|
||||
Error* error)
|
||||
{
|
||||
const auto iter = std::ranges::find(*labels, name, &AssemblyLabel::name);
|
||||
if (iter == labels->end())
|
||||
{
|
||||
labels->emplace_back(std::string(name), std::make_optional(address), std::vector<u32>());
|
||||
return &labels->back();
|
||||
}
|
||||
|
||||
if (iter->address.has_value())
|
||||
{
|
||||
Error::SetStringFmt(error, "Label '{}' is already defined.", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
iter->address = address;
|
||||
return &(*iter);
|
||||
}
|
||||
|
||||
bool CPU::FixupAssemblyLabelBackreferences(AssemblyLabel* label, void* userdata, Error* error,
|
||||
u32* (*instruction_reader)(u32 pc, void* userdata))
|
||||
{
|
||||
for (const u32 fixup_pc : label->backreferences)
|
||||
{
|
||||
u32* instruction = instruction_reader(fixup_pc, userdata);
|
||||
if (!instruction)
|
||||
{
|
||||
Error::SetStringFmt(error, "Failed to read instruction at 0x{:08X} for label backreference.", fixup_pc);
|
||||
return false;
|
||||
}
|
||||
|
||||
const InstructionOp op = Instruction{*instruction}.op;
|
||||
if (!EncodeAssemblyTarget(instruction, fixup_pc, label->address.value(),
|
||||
(op == InstructionOp::j || op == InstructionOp::jal), error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
label->backreferences.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,31 @@
|
||||
#pragma once
|
||||
#include "cpu_types.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class Error;
|
||||
class SmallStringBase;
|
||||
|
||||
namespace CPU {
|
||||
|
||||
struct AssemblyLabel
|
||||
{
|
||||
std::string name;
|
||||
std::optional<u32> address;
|
||||
std::vector<u32> backreferences; // pc list
|
||||
};
|
||||
using AssemblyLabelList = std::vector<AssemblyLabel>;
|
||||
|
||||
bool AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* error = nullptr);
|
||||
bool AssembleInstruction(u32* dest, u32 pc, std::string_view text, AssemblyLabelList* labels, Error* error = nullptr);
|
||||
AssemblyLabel* DefineAssemblyLabel(AssemblyLabelList* labels, std::string_view name, u32 address,
|
||||
Error* error = nullptr);
|
||||
bool FixupAssemblyLabelBackreferences(AssemblyLabel* label, void* userdata, Error* error,
|
||||
u32* (*instruction_reader)(u32 pc, void* userdata));
|
||||
bool ContainsUnresolvedLabels(const AssemblyLabelList& labels);
|
||||
void DisassembleInstruction(SmallStringBase* dest, u32 pc, u32 bits);
|
||||
void DisassembleInstructionComment(SmallStringBase* dest, u32 pc, u32 bits);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user