mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2024-11-14 13:27:45 -07:00
Merge pull request #10771 from TryTwo/PR_AutoStep
Debugger: Implement base code tracing logic. and feature to auto-step through code.
This commit is contained in:
commit
431301add3
@ -33,6 +33,8 @@ add_library(common
|
||||
Crypto/ec.h
|
||||
Crypto/SHA1.cpp
|
||||
Crypto/SHA1.h
|
||||
Debug/CodeTrace.cpp
|
||||
Debug/CodeTrace.h
|
||||
Debug/MemoryPatches.cpp
|
||||
Debug/MemoryPatches.h
|
||||
Debug/Threads.h
|
||||
|
389
Source/Core/Common/Debug/CodeTrace.cpp
Normal file
389
Source/Core/Common/Debug/CodeTrace.cpp
Normal file
@ -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;
|
||||
}
|
76
Source/Core/Common/Debug/CodeTrace.h
Normal file
76
Source/Core/Common/Debug/CodeTrace.h
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright 2022 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
struct InstructionAttributes
|
||||
{
|
||||
u32 address = 0;
|
||||
std::string instruction = "";
|
||||
std::string reg0 = "";
|
||||
std::string reg1 = "";
|
||||
std::string reg2 = "";
|
||||
std::string reg3 = "";
|
||||
std::optional<u32> memory_target = std::nullopt;
|
||||
u32 memory_target_size = 4;
|
||||
bool is_store = false;
|
||||
bool is_load = false;
|
||||
};
|
||||
|
||||
struct TraceOutput
|
||||
{
|
||||
u32 address;
|
||||
std::optional<u32> memory_target = std::nullopt;
|
||||
std::string instruction;
|
||||
};
|
||||
|
||||
struct AutoStepResults
|
||||
{
|
||||
std::vector<std::string> reg_tracked;
|
||||
std::set<u32> mem_tracked;
|
||||
u32 count = 0;
|
||||
bool timed_out = false;
|
||||
bool trackers_empty = false;
|
||||
};
|
||||
|
||||
enum class HitType : u32
|
||||
{
|
||||
SKIP = (1 << 0), // Not a hit
|
||||
OVERWRITE = (1 << 1), // Tracked value gets overwritten by untracked. Typically skipped.
|
||||
MOVED = (1 << 2), // Target duplicated to another register, unchanged.
|
||||
SAVELOAD = (1 << 3), // Target saved or loaded. Priority over Pointer.
|
||||
POINTER = (1 << 4), // Target used as pointer/offset for save or load
|
||||
PASSIVE = (1 << 5), // Conditional, etc, but not pointer. Unchanged
|
||||
ACTIVE = (1 << 6), // Math, etc. Changed.
|
||||
UPDATED = (1 << 7), // Masked or math without changing register.
|
||||
};
|
||||
|
||||
class CodeTrace
|
||||
{
|
||||
public:
|
||||
enum class AutoStop
|
||||
{
|
||||
Always,
|
||||
Used,
|
||||
Changed
|
||||
};
|
||||
|
||||
void SetRegTracked(const std::string& reg);
|
||||
AutoStepResults AutoStepping(bool continue_previous = false, AutoStop stop_on = AutoStop::Always);
|
||||
|
||||
private:
|
||||
InstructionAttributes GetInstructionAttributes(const TraceOutput& line) const;
|
||||
TraceOutput SaveCurrentInstruction() const;
|
||||
HitType TraceLogic(const TraceOutput& current_instr, bool first_hit = false);
|
||||
|
||||
bool m_recording = false;
|
||||
std::vector<std::string> m_reg_autotrack;
|
||||
std::set<u32> m_mem_autotrack;
|
||||
};
|
@ -37,6 +37,7 @@
|
||||
<ClInclude Include="Common\Crypto\bn.h" />
|
||||
<ClInclude Include="Common\Crypto\ec.h" />
|
||||
<ClInclude Include="Common\Crypto\SHA1.h" />
|
||||
<ClInclude Include="Common\Debug\CodeTrace.h" />
|
||||
<ClInclude Include="Common\Debug\MemoryPatches.h" />
|
||||
<ClInclude Include="Common\Debug\Threads.h" />
|
||||
<ClInclude Include="Common\Debug\Watches.h" />
|
||||
@ -737,6 +738,7 @@
|
||||
-->
|
||||
<AdditionalOptions Condition="'$(Platform)'=='ARM64'">/d2ssa-peeps-post-color- %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Common\Debug\CodeTrace.cpp" />
|
||||
<ClCompile Include="Common\Debug\MemoryPatches.cpp" />
|
||||
<ClCompile Include="Common\Debug\Watches.cpp" />
|
||||
<ClCompile Include="Common\DynamicLibrary.cpp" />
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
@ -23,6 +24,7 @@
|
||||
#include <QWheelEvent>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/Debug/CodeTrace.h"
|
||||
#include "Common/GekkoDisassembler.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Core/Core.h"
|
||||
@ -565,6 +567,26 @@ void CodeViewWidget::OnContextMenu()
|
||||
auto* restore_action =
|
||||
menu->addAction(tr("Restore instruction"), this, &CodeViewWidget::OnRestoreInstruction);
|
||||
|
||||
QString target;
|
||||
if (addr == PC && running && Core::GetState() == Core::State::Paused)
|
||||
{
|
||||
const std::string line = PowerPC::debug_interface.Disassemble(PC);
|
||||
const auto target_it = std::find(line.begin(), line.end(), '\t');
|
||||
const auto target_end = std::find(target_it, line.end(), ',');
|
||||
|
||||
if (target_it != line.end() && target_end != line.end())
|
||||
target = QString::fromStdString(std::string{target_it + 1, target_end});
|
||||
}
|
||||
|
||||
auto* run_until_menu = menu->addMenu(tr("Run until (ignoring breakpoints)"));
|
||||
run_until_menu->addAction(tr("%1's value is hit").arg(target), this,
|
||||
[this] { AutoStep(CodeTrace::AutoStop::Always); });
|
||||
run_until_menu->addAction(tr("%1's value is used").arg(target), this,
|
||||
[this] { AutoStep(CodeTrace::AutoStop::Used); });
|
||||
run_until_menu->addAction(tr("%1's value is changed").arg(target),
|
||||
[this] { AutoStep(CodeTrace::AutoStop::Changed); });
|
||||
|
||||
run_until_menu->setEnabled(!target.isEmpty());
|
||||
follow_branch_action->setEnabled(running && GetBranchFromAddress(addr));
|
||||
|
||||
for (auto* action : {copy_address_action, copy_line_action, copy_hex_action, function_action,
|
||||
@ -588,6 +610,85 @@ void CodeViewWidget::OnContextMenu()
|
||||
Update();
|
||||
}
|
||||
|
||||
void CodeViewWidget::AutoStep(CodeTrace::AutoStop option)
|
||||
{
|
||||
// Autosteps and follows value in the target (left-most) register. The Used and Changed options
|
||||
// silently follows target through reshuffles in memory and registers and stops on use or update.
|
||||
|
||||
CodeTrace code_trace;
|
||||
bool repeat = false;
|
||||
|
||||
QMessageBox msgbox(QMessageBox::NoIcon, tr("Run until"), {}, QMessageBox::Cancel);
|
||||
QPushButton* run_button = msgbox.addButton(tr("Keep Running"), QMessageBox::AcceptRole);
|
||||
// Not sure if we want default to be cancel. Spacebar can let you quickly continue autostepping if
|
||||
// Yes.
|
||||
|
||||
do
|
||||
{
|
||||
// Run autostep then update codeview
|
||||
const AutoStepResults results = code_trace.AutoStepping(repeat, option);
|
||||
emit Host::GetInstance()->UpdateDisasmDialog();
|
||||
repeat = true;
|
||||
|
||||
// Invalid instruction, 0 means no step executed.
|
||||
if (results.count == 0)
|
||||
return;
|
||||
|
||||
// Status report
|
||||
if (results.reg_tracked.empty() && results.mem_tracked.empty())
|
||||
{
|
||||
QMessageBox::warning(
|
||||
this, tr("Overwritten"),
|
||||
tr("Target value was overwritten by current instruction.\nInstructions executed: %1")
|
||||
.arg(QString::number(results.count)),
|
||||
QMessageBox::Cancel);
|
||||
return;
|
||||
}
|
||||
else if (results.timed_out)
|
||||
{
|
||||
// Can keep running and try again after a time out.
|
||||
msgbox.setText(
|
||||
tr("<font color='#ff0000'>AutoStepping timed out. Current instruction is irrelevant."));
|
||||
}
|
||||
else
|
||||
{
|
||||
msgbox.setText(tr("Value tracked to current instruction."));
|
||||
}
|
||||
|
||||
// Mem_tracked needs to track each byte individually, so a tracked word-sized value would have
|
||||
// four entries. The displayed memory list needs to be shortened so it's not a huge list of
|
||||
// bytes. Assumes adjacent bytes represent a word or half-word and removes the redundant bytes.
|
||||
std::set<u32> mem_out;
|
||||
auto iter = results.mem_tracked.begin();
|
||||
|
||||
while (iter != results.mem_tracked.end())
|
||||
{
|
||||
const u32 address = *iter;
|
||||
mem_out.insert(address);
|
||||
|
||||
for (u32 i = 1; i <= 3; i++)
|
||||
{
|
||||
if (results.mem_tracked.count(address + i))
|
||||
iter++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
iter++;
|
||||
}
|
||||
|
||||
const QString msgtext =
|
||||
tr("Instructions executed: %1\nValue contained in:\nRegisters: %2\nMemory: %3")
|
||||
.arg(QString::number(results.count))
|
||||
.arg(QString::fromStdString(fmt::format("{}", fmt::join(results.reg_tracked, ", "))))
|
||||
.arg(QString::fromStdString(fmt::format("{:#x}", fmt::join(mem_out, ", "))));
|
||||
|
||||
msgbox.setInformativeText(msgtext);
|
||||
msgbox.exec();
|
||||
|
||||
} while (msgbox.clickedButton() == (QAbstractButton*)run_button);
|
||||
}
|
||||
|
||||
void CodeViewWidget::OnCopyAddress()
|
||||
{
|
||||
const u32 addr = GetContextAddress();
|
||||
|
@ -8,6 +8,7 @@
|
||||
#include <QTableWidget>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Debug/CodeTrace.h"
|
||||
|
||||
class QKeyEvent;
|
||||
class QMouseEvent;
|
||||
@ -68,6 +69,7 @@ private:
|
||||
|
||||
void OnContextMenu();
|
||||
|
||||
void AutoStep(CodeTrace::AutoStop option = CodeTrace::AutoStop::Always);
|
||||
void OnFollowBranch();
|
||||
void OnCopyAddress();
|
||||
void OnCopyTargetAddress();
|
||||
|
@ -8,9 +8,11 @@
|
||||
#include <QActionGroup>
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Common/Debug/CodeTrace.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/ProcessorInterface.h"
|
||||
#include "Core/PowerPC/PowerPC.h"
|
||||
@ -164,6 +166,16 @@ void RegisterWidget::ShowContextMenu()
|
||||
auto* view_double_column = menu->addAction(tr("All Double"));
|
||||
view_double_column->setData(static_cast<int>(RegisterDisplay::Double));
|
||||
|
||||
if (type == RegisterType::gpr || type == RegisterType::fpr)
|
||||
{
|
||||
menu->addSeparator();
|
||||
|
||||
const std::string type_string =
|
||||
fmt::format("{}{}", type == RegisterType::gpr ? "r" : "f", m_table->currentItem()->row());
|
||||
menu->addAction(tr("Run until hit (ignoring breakpoints)"),
|
||||
[this, type_string]() { AutoStep(type_string); });
|
||||
}
|
||||
|
||||
for (auto* action : {view_hex, view_int, view_uint, view_float, view_double})
|
||||
{
|
||||
action->setCheckable(true);
|
||||
@ -269,6 +281,32 @@ void RegisterWidget::ShowContextMenu()
|
||||
menu->exec(QCursor::pos());
|
||||
}
|
||||
|
||||
void RegisterWidget::AutoStep(const std::string& reg) const
|
||||
{
|
||||
CodeTrace trace;
|
||||
trace.SetRegTracked(reg);
|
||||
|
||||
QMessageBox msgbox(
|
||||
QMessageBox::NoIcon, tr("Timed Out"),
|
||||
tr("<font color='#ff0000'>AutoStepping timed out. Current instruction is irrelevant."),
|
||||
QMessageBox::Cancel);
|
||||
QPushButton* run_button = msgbox.addButton(tr("Keep Running"), QMessageBox::AcceptRole);
|
||||
|
||||
while (true)
|
||||
{
|
||||
const AutoStepResults results = trace.AutoStepping(true);
|
||||
emit Host::GetInstance()->UpdateDisasmDialog();
|
||||
|
||||
if (!results.timed_out)
|
||||
break;
|
||||
|
||||
// Can keep running and try again after a time out.
|
||||
msgbox.exec();
|
||||
if (msgbox.clickedButton() != (QAbstractButton*)run_button)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterWidget::PopulateTable()
|
||||
{
|
||||
for (int i = 0; i < 32; i++)
|
||||
|
@ -46,6 +46,7 @@ private:
|
||||
void AddRegister(int row, int column, RegisterType type, std::string register_name,
|
||||
std::function<u64()> get_reg, std::function<void(u64)> set_reg);
|
||||
|
||||
void AutoStep(const std::string& reg) const;
|
||||
void Update();
|
||||
|
||||
QTableWidget* m_table;
|
||||
|
Loading…
Reference in New Issue
Block a user