summaryrefslogtreecommitdiff
path: root/Source/Core/Common/Debug/CodeTrace.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/Core/Common/Debug/CodeTrace.cpp')
-rw-r--r--Source/Core/Common/Debug/CodeTrace.cpp389
1 files changed, 389 insertions, 0 deletions
diff --git a/Source/Core/Common/Debug/CodeTrace.cpp b/Source/Core/Common/Debug/CodeTrace.cpp
new file mode 100644
index 0000000000..3eea7a72fc
--- /dev/null
+++ b/Source/Core/Common/Debug/CodeTrace.cpp
@@ -0,0 +1,389 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Common/Debug/CodeTrace.h"
+
+#include <chrono>
+#include <regex>
+
+#include "Common/Event.h"
+#include "Common/StringUtil.h"
+#include "Core/Debugger/PPCDebugInterface.h"
+#include "Core/HW/CPU.h"
+#include "Core/PowerPC/PowerPC.h"
+
+namespace
+{
+bool IsInstructionLoadStore(std::string_view ins)
+{
+ return (StringBeginsWith(ins, "l") && !StringBeginsWith(ins, "li")) ||
+ StringBeginsWith(ins, "st") || StringBeginsWith(ins, "psq_l") ||
+ StringBeginsWith(ins, "psq_s");
+}
+
+u32 GetMemoryTargetSize(std::string_view instr)
+{
+ // Word-size operations are taken as the default, check the others.
+ auto op = instr.substr(0, 4);
+
+ constexpr char BYTE_TAG = 'b';
+ constexpr char HALF_TAG = 'h';
+ constexpr char DOUBLE_WORD_TAG = 'd';
+ constexpr char PAIRED_TAG = 'p';
+
+ // Actual range is 0 to size - 1;
+ if (op.find(BYTE_TAG) != std::string::npos)
+ {
+ return 1;
+ }
+ else if (op.find(HALF_TAG) != std::string::npos)
+ {
+ return 2;
+ }
+ else if (op.find(DOUBLE_WORD_TAG) != std::string::npos ||
+ op.find(PAIRED_TAG) != std::string::npos)
+ {
+ return 8;
+ }
+
+ return 4;
+}
+} // namespace
+
+void CodeTrace::SetRegTracked(const std::string& reg)
+{
+ m_reg_autotrack.push_back(reg);
+}
+
+InstructionAttributes CodeTrace::GetInstructionAttributes(const TraceOutput& instruction) const
+{
+ // Slower process of breaking down saved instruction. Only used when stepping through code if a
+ // decision has to be made, otherwise used afterwards on a log file.
+ InstructionAttributes tmp_attributes;
+ tmp_attributes.instruction = instruction.instruction;
+ tmp_attributes.address = PC;
+ std::string instr = instruction.instruction;
+ std::smatch match;
+
+ // Convert sp, rtoc, and ps to r1, r2, and F#. ps is handled like a float operation.
+ static const std::regex replace_sp("(\\W)sp");
+ instr = std::regex_replace(instr, replace_sp, "$1r1");
+ static const std::regex replace_rtoc("rtoc");
+ instr = std::regex_replace(instr, replace_rtoc, "r2");
+ static const std::regex replace_ps("(\\W)p(\\d+)");
+ instr = std::regex_replace(instr, replace_ps, "$1f$2");
+
+ // Pull all register numbers out and store them. Limited to Reg0 if ps operation, as ps get
+ // too complicated to track easily.
+ // ex: add r4, r5, r6 -> r4 = Reg0, r5 = Reg1, r6 = Reg2. Reg0 is always the target register.
+ static const std::regex regis(
+ "\\W([rfp]\\d+)[^r^f]*(?:([rf]\\d+))?[^r^f\\d]*(?:([rf]\\d+))?[^r^f\\d]*(?:([rf]\\d+))?",
+ std::regex::optimize);
+
+ if (std::regex_search(instr, match, regis))
+ {
+ tmp_attributes.reg0 = match.str(1);
+ if (match[2].matched)
+ tmp_attributes.reg1 = match.str(2);
+ if (match[3].matched)
+ tmp_attributes.reg2 = match.str(3);
+ if (match[4].matched)
+ tmp_attributes.reg3 = match.str(4);
+
+ if (instruction.memory_target)
+ {
+ tmp_attributes.memory_target = instruction.memory_target;
+ tmp_attributes.memory_target_size = GetMemoryTargetSize(instr);
+
+ if (StringBeginsWith(instr, "st") || StringBeginsWith(instr, "psq_s"))
+ tmp_attributes.is_store = true;
+ else
+ tmp_attributes.is_load = true;
+ }
+ }
+
+ return tmp_attributes;
+}
+
+TraceOutput CodeTrace::SaveCurrentInstruction() const
+{
+ // Quickly save instruction and memory target for fast logging.
+ TraceOutput output;
+ const std::string instr = PowerPC::debug_interface.Disassemble(PC);
+ output.instruction = instr;
+ output.address = PC;
+
+ if (IsInstructionLoadStore(output.instruction))
+ output.memory_target = PowerPC::debug_interface.GetMemoryAddressFromInstruction(instr);
+
+ return output;
+}
+
+bool CompareMemoryTargetToTracked(const std::string& instr, const u32 mem_target,
+ const std::set<u32>& mem_tracked)
+{
+ // This function is hit often and should be optimized.
+ auto it_lower = std::lower_bound(mem_tracked.begin(), mem_tracked.end(), mem_target);
+
+ if (it_lower == mem_tracked.end())
+ return false;
+ else if (*it_lower == mem_target)
+ return true;
+
+ // If the base value doesn't hit, still need to check if longer values overlap.
+ return *it_lower < mem_target + GetMemoryTargetSize(instr);
+}
+
+AutoStepResults CodeTrace::AutoStepping(bool continue_previous, AutoStop stop_on)
+{
+ AutoStepResults results;
+
+ if (!CPU::IsStepping() || m_recording)
+ return results;
+
+ TraceOutput pc_instr = SaveCurrentInstruction();
+ const InstructionAttributes instr = GetInstructionAttributes(pc_instr);
+
+ // Not an instruction we should start autostepping from (ie branches).
+ if (instr.reg0.empty() && !continue_previous)
+ return results;
+
+ m_recording = true;
+
+ // Once autostep stops, it can be told to continue running without resetting the tracked
+ // registers and memory.
+ if (!continue_previous)
+ {
+ m_reg_autotrack.clear();
+ m_mem_autotrack.clear();
+ m_reg_autotrack.push_back(instr.reg0);
+
+ // It wouldn't necessarily be wrong to also record the memory of a load operation, as the
+ // value exists there too. May or may not be desirable depending on task. Leaving it out.
+ if (instr.is_store)
+ {
+ const u32 size = GetMemoryTargetSize(instr.instruction);
+ for (u32 i = 0; i < size; i++)
+ m_mem_autotrack.insert(instr.memory_target.value() + i);
+ }
+ }
+
+ // Count is important for feedback on how much work was done.
+
+ HitType hit = HitType::SKIP;
+ HitType stop_condition = HitType::SAVELOAD;
+
+ // Could use bit flags, but I organized it to have decreasing levels of verbosity, so the
+ // less-than comparison ignores what is needed for the current usage.
+ if (stop_on == AutoStop::Always)
+ stop_condition = HitType::SAVELOAD;
+ else if (stop_on == AutoStop::Used)
+ stop_condition = HitType::PASSIVE;
+ else if (stop_on == AutoStop::Changed)
+ stop_condition = HitType::ACTIVE;
+
+ CPU::PauseAndLock(true, false);
+ PowerPC::breakpoints.ClearAllTemporary();
+ using clock = std::chrono::steady_clock;
+ clock::time_point timeout = clock::now() + std::chrono::seconds(4);
+
+ PowerPC::CoreMode old_mode = PowerPC::GetMode();
+ PowerPC::SetMode(PowerPC::CoreMode::Interpreter);
+
+ do
+ {
+ PowerPC::SingleStep();
+
+ pc_instr = SaveCurrentInstruction();
+ hit = TraceLogic(pc_instr);
+ results.count += 1;
+ } while (clock::now() < timeout && hit < stop_condition &&
+ !(m_reg_autotrack.empty() && m_mem_autotrack.empty()));
+
+ // Report the timeout to the caller.
+ if (clock::now() >= timeout)
+ results.timed_out = true;
+
+ PowerPC::SetMode(old_mode);
+ CPU::PauseAndLock(false, false);
+ m_recording = false;
+
+ results.reg_tracked = m_reg_autotrack;
+ results.mem_tracked = m_mem_autotrack;
+
+ // Doesn't currently need to report the hit type to the caller. Denoting when the reg and mem
+ // trackers are both empty is important, as it means our target was overwritten and can no longer
+ // be tracked. Different actions can be taken on a timeout vs empty trackers, so they are reported
+ // individually.
+ return results;
+}
+
+HitType CodeTrace::TraceLogic(const TraceOutput& current_instr, bool first_hit)
+{
+ // Tracks the original value that is in the targeted register or memory through loads, stores,
+ // register moves, and value changes. Also finds when it is used. ps operations are not fully
+ // supported. -ux memory instructions may need special cases.
+ // Should not be called if reg and mem tracked are empty.
+
+ // Using a std::set because it can easily insert the memory range being accessed without
+ // causing duplicates, and quickly erases all members of the memory range without caring if the
+ // element actually exists.
+
+ bool mem_hit = false;
+ if (current_instr.memory_target && !m_mem_autotrack.empty())
+ {
+ mem_hit = CompareMemoryTargetToTracked(current_instr.instruction, *current_instr.memory_target,
+ m_mem_autotrack);
+ }
+
+ // Optimization for tracking a memory target when no registers are being tracked.
+ if (m_reg_autotrack.empty() && !mem_hit)
+ return HitType::SKIP;
+
+ // Break instruction down into parts to be analyzed.
+ const InstructionAttributes instr = GetInstructionAttributes(current_instr);
+
+ // Not an instruction we care about (branches).
+ if (instr.reg0.empty())
+ return HitType::SKIP;
+
+ // The reg_itr will be used later for erasing.
+ auto reg_itr = std::find(m_reg_autotrack.begin(), m_reg_autotrack.end(), instr.reg0);
+ const bool match_reg123 =
+ (!instr.reg1.empty() && std::find(m_reg_autotrack.begin(), m_reg_autotrack.end(),
+ instr.reg1) != m_reg_autotrack.end()) ||
+ (!instr.reg2.empty() && std::find(m_reg_autotrack.begin(), m_reg_autotrack.end(),
+ instr.reg2) != m_reg_autotrack.end()) ||
+ (!instr.reg3.empty() && std::find(m_reg_autotrack.begin(), m_reg_autotrack.end(),
+ instr.reg3) != m_reg_autotrack.end());
+ const bool match_reg0 = reg_itr != m_reg_autotrack.end();
+
+ if (!match_reg0 && !match_reg123 && !mem_hit)
+ return HitType::SKIP;
+
+ // Checks if the intstruction is a type that needs special handling.
+ const auto CompareInstruction = [](std::string_view instruction, const auto& type_compare) {
+ return std::any_of(
+ type_compare.begin(), type_compare.end(),
+ [&instruction](std::string_view s) { return StringBeginsWith(instruction, s); });
+ };
+
+ // Exclusions from updating tracking logic. mt operations are too complex and specialized.
+ // Combiner used later.
+ static const std::array<std::string_view, 3> exclude{"dc", "ic", "mt"};
+ static const std::array<std::string_view, 2> compare{"c", "fc"};
+
+ // rlwimi, at least, can preserve parts of the target register. Not sure if rldimi can too or if
+ // there are any others like this.
+ static const std::array<std::string_view, 1> combiner{"rlwimi"};
+
+ static const std::array<std::string_view, 2> mover{"mr", "fmr"};
+
+ // Link register for when r0 gets overwritten
+ if (StringBeginsWith(instr.instruction, "mflr") && match_reg0)
+ {
+ m_reg_autotrack.erase(reg_itr);
+ return HitType::OVERWRITE;
+ }
+ else if (StringBeginsWith(instr.instruction, "mtlr") && match_reg0)
+ {
+ // LR is not something tracked
+ return HitType::MOVED;
+ }
+
+ if (CompareInstruction(instr.instruction, exclude))
+ return HitType::SKIP;
+ else if (CompareInstruction(instr.instruction, compare))
+ return HitType::PASSIVE;
+ else if (match_reg123 && !match_reg0 && (instr.is_store || instr.is_load))
+ return HitType::POINTER;
+
+ // Update tracking logic. At this point a memory or register hit happened.
+ // Save/Load
+ if (instr.memory_target)
+ {
+ if (mem_hit)
+ {
+ // If hit a tracked memory. Load -> Add register to tracked. Store -> Remove tracked memory
+ // if overwritten.
+
+ if (instr.is_load && !match_reg0)
+ {
+ m_reg_autotrack.push_back(instr.reg0);
+ return HitType::SAVELOAD;
+ }
+ else if (instr.is_store && !match_reg0)
+ {
+ // On First Hit it wouldn't necessarily be wrong to track the register, which contains the
+ // same value. A matter of preference.
+ if (first_hit)
+ return HitType::SAVELOAD;
+
+ for (u32 i = 0; i < instr.memory_target_size; i++)
+ m_mem_autotrack.erase(*instr.memory_target + i);
+
+ return HitType::OVERWRITE;
+ }
+ else
+ {
+ // If reg0 and store/load are both already tracked, do nothing.
+ return HitType::SAVELOAD;
+ }
+ }
+ else if (instr.is_store && match_reg0)
+ {
+ // If store to untracked memory, then track memory.
+ for (u32 i = 0; i < instr.memory_target_size; i++)
+ m_mem_autotrack.insert(*instr.memory_target + i);
+
+ return HitType::SAVELOAD;
+ }
+ else if (instr.is_load && match_reg0)
+ {
+ // Not wrong to track load memory_target here. Preference.
+ if (first_hit)
+ return HitType::SAVELOAD;
+
+ // If untracked load is overwriting tracked register, then remove register
+ m_reg_autotrack.erase(reg_itr);
+ return HitType::OVERWRITE;
+ }
+ }
+ else if (!match_reg0 && !match_reg123)
+ {
+ // Skip if no matches. Happens most often.
+ return HitType::SKIP;
+ }
+ else
+ {
+ // If tracked register data is being stored in a new register, save new register.
+ if (match_reg123 && !match_reg0)
+ {
+ m_reg_autotrack.push_back(instr.reg0);
+
+ // This should include any instruction that can reach this point and is not ACTIVE. Can only
+ // think of mr at this time.
+ if (CompareInstruction(instr.instruction, mover))
+ return HitType::MOVED;
+
+ return HitType::ACTIVE;
+ }
+ // If tracked register is overwritten, stop tracking.
+ else if (match_reg0 && !match_reg123)
+ {
+ if (CompareInstruction(instr.instruction, combiner) || first_hit)
+ return HitType::UPDATED;
+
+ m_reg_autotrack.erase(reg_itr);
+ return HitType::OVERWRITE;
+ }
+ else if (match_reg0 && match_reg123)
+ {
+ // Or moved
+ return HitType::UPDATED;
+ }
+ }
+
+ // Should not reach this
+ return HitType::SKIP;
+}