diff --git a/src/core-tests/cpu_disasm_tests.cpp b/src/core-tests/cpu_disasm_tests.cpp index 5858b66ab..1119a724e 100644 --- a/src/core-tests/cpu_disasm_tests.cpp +++ b/src/core-tests/cpu_disasm_tests.cpp @@ -10,6 +10,7 @@ #include #include +#include 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(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 regions = {0x00000000, 0x10000000, 0x80000000, 0xA0000000, 0xF0000000}; + static constexpr std::array 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 word_offsets = { + std::numeric_limits::min(), -1, 0, 1, std::numeric_limits::max(), + }; + static constexpr u32 pc = 0x80010000; + for (const s32 word_offset : word_offsets) + { + const u32 target = (pc + 4) + static_cast(word_offset * 4); + const u32 bits = 0x11090000 | static_cast(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 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 forward_results = {}; + for (size_t i = 0; i < cases.size(); i++) + { + Error error; + ASSERT_TRUE(CPU::AssembleInstruction(&forward_results[i], 0x1000 + static_cast(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(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(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 instructions = { diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp index be7cb3b02..9e601366f 100644 --- a/src/core/cheats.cpp +++ b/src/core/cheats.cpp @@ -4617,10 +4617,10 @@ std::unique_ptr Cheats::AssemblyCheatCode::Parse(Meta } std::unique_ptr code = std::make_unique(std::move(metadata)); + code->instructions.reserve(static_cast(std::ranges::count(data, '\n')) + 1); + CPU::AssemblyLabelList labels; CheatFileReader reader(data); std::optional 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::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(userdata)->instructions) + { + if (inst.pc == pc) + return &inst.new_value; + } + return static_cast(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::Parse(Meta } pc = address.value(); - section_line = reader.GetCurrentLineNumber(); - section_has_instruction = false; continue; } @@ -4669,12 +4700,22 @@ std::unique_ptr 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(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::Parse(Meta } u32 one_bits; - if (!CPU::AssembleInstruction(&bits, static_cast(pc.value()), zero_instruction, &assemble_error) || - !CPU::AssembleInstruction(&one_bits, static_cast(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::Parse(Meta return {}; } - code->option_instruction_values.emplace_back(static_cast(code->instructions.size()), + code->option_instruction_values.emplace_back(static_cast(code->instructions.size() - 1), static_cast(option_bit_position), static_cast(option_bit_count)); } - const u32 instruction_pc = static_cast(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; } diff --git a/src/core/cpu_disasm.cpp b/src/core/cpu_disasm.cpp index d8696524c..1d1256fe8 100644 --- a/src/core/cpu_disasm.cpp +++ b/src/core/cpu_disasm.cpp @@ -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 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 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(static_cast(delta)) / 4; + if ((delta & 3) != 0 || word_delta < std::numeric_limits::min() || word_delta > std::numeric_limits::max()) + { + Error::SetStringFmt(error, "Branch target 0x{:08X} is out of range.", target); + return false; + } + + SetField(bits, 0, 16, static_cast(word_delta)); + return true; +} + +std::optional 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()); + 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::max() || - (target_value & 3) != 0) + if (!ParseUnsignedValue(operand, &target_value)) + { + const std::optional 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::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(target_value) - (pc + 4); - const s64 word_delta = static_cast(static_cast(delta)) / 4; - if ((delta & 3) != 0 || word_delta < std::numeric_limits::min() || - word_delta > std::numeric_limits::max()) - { - Error::SetStringFmt(error, "Branch target '{}' is out of range.", operand); + if (!EncodeAssemblyTarget(bits, pc, static_cast(target_value), false, error)) return false; - } - SetField(bits, 0, 16, static_cast(word_delta)); } else if (type == "$jt") { u64 target_value; - if (!ParseUnsignedValue(operand, &target_value) || target_value > std::numeric_limits::max() || - (target_value & 3) != 0) + if (!ParseUnsignedValue(operand, &target_value)) + { + const std::optional 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::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(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(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(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(InstructionOp::cop0) + cop_n) << 26) | (static_cast(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(InstructionOp::cop0) << 26) | (UINT32_C(1) << 25) | static_cast(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()); + 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; +} diff --git a/src/core/cpu_disasm.h b/src/core/cpu_disasm.h index 20faea011..bffc98509 100644 --- a/src/core/cpu_disasm.h +++ b/src/core/cpu_disasm.h @@ -4,14 +4,31 @@ #pragma once #include "cpu_types.h" +#include +#include #include +#include class Error; class SmallStringBase; namespace CPU { +struct AssemblyLabel +{ + std::string name; + std::optional address; + std::vector backreferences; // pc list +}; +using AssemblyLabelList = std::vector; + 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);