mirror of
https://github.com/RPCS3/rpcs3.git
synced 2025-04-21 20:15:27 +00:00
Debugger/PPU: Superior Callstack Detection
This commit is contained in:
parent
78f2d44a0e
commit
17302a9422
5 changed files with 362 additions and 34 deletions
|
@ -262,6 +262,8 @@ struct ppu_pattern_matrix
|
|||
// PPU Instruction Type
|
||||
struct ppu_itype
|
||||
{
|
||||
static constexpr struct branch_tag{} branch{}; // Branch Instructions
|
||||
|
||||
enum type
|
||||
{
|
||||
UNK = 0,
|
||||
|
@ -432,11 +434,8 @@ struct ppu_itype
|
|||
ADDIC,
|
||||
ADDI,
|
||||
ADDIS,
|
||||
BC,
|
||||
SC,
|
||||
B,
|
||||
MCRF,
|
||||
BCLR,
|
||||
CRNOR,
|
||||
CRANDC,
|
||||
ISYNC,
|
||||
|
@ -446,7 +445,6 @@ struct ppu_itype
|
|||
CREQV,
|
||||
CRORC,
|
||||
CROR,
|
||||
BCCTR,
|
||||
RLWIMI,
|
||||
RLWINM,
|
||||
RLWNM,
|
||||
|
@ -781,6 +779,11 @@ struct ppu_itype
|
|||
FCTID_,
|
||||
FCTIDZ_,
|
||||
FCFID_,
|
||||
|
||||
B, // branch_tag first
|
||||
BC,
|
||||
BCLR,
|
||||
BCCTR, // branch_tag last
|
||||
};
|
||||
|
||||
// Enable address-of operator for ppu_decoder<>
|
||||
|
@ -788,6 +791,11 @@ struct ppu_itype
|
|||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
friend constexpr bool operator &(type value, branch_tag)
|
||||
{
|
||||
return value >= B && value <= BCCTR;
|
||||
}
|
||||
};
|
||||
|
||||
struct ppu_iname
|
||||
|
|
|
@ -106,12 +106,7 @@ std::pair<PPUDisAsm::const_op, u64> PPUDisAsm::try_get_const_op_gpr_value(u32 re
|
|||
|
||||
const auto type = s_ppu_itype.decode(opcode);
|
||||
|
||||
auto is_branch = [](enum ppu_itype::type itype)
|
||||
{
|
||||
return itype == ppu_itype::BC || itype == ppu_itype::B || itype == ppu_itype::BCLR || itype == ppu_itype::BCCTR;
|
||||
};
|
||||
|
||||
if (is_branch(type) || type == ppu_itype::UNK)
|
||||
if (type & ppu_itype::branch || type == ppu_itype::UNK)
|
||||
{
|
||||
// TODO: Detect calls, ignore them if reg is a non-volatile register
|
||||
return {};
|
||||
|
|
|
@ -1563,16 +1563,19 @@ std::vector<std::pair<u32, u32>> ppu_thread::dump_callstack_list() const
|
|||
|
||||
std::vector<std::pair<u32, u32>> call_stack_list;
|
||||
|
||||
bool first = true;
|
||||
bool is_first = true;
|
||||
bool skip_single_frame = false;
|
||||
|
||||
const u64 _lr = this->lr;
|
||||
const u32 _cia = this->cia;
|
||||
const u64 gpr0 = this->gpr[0];
|
||||
|
||||
for (
|
||||
u64 sp = r1;
|
||||
sp % 0x10 == 0u && sp >= stack_min && sp <= stack_max - ppu_stack_start_offset;
|
||||
first = false
|
||||
is_first = false
|
||||
)
|
||||
{
|
||||
u64 addr = *vm::get_super_ptr<u64>(static_cast<u32>(sp + 16));
|
||||
|
||||
auto is_invalid = [](u64 addr)
|
||||
{
|
||||
if (addr > u32{umax} || addr % 4 || !vm::check_addr(static_cast<u32>(addr), vm::page_executable))
|
||||
|
@ -1584,28 +1587,330 @@ std::vector<std::pair<u32, u32>> ppu_thread::dump_callstack_list() const
|
|||
return addr == g_fxo->get<ppu_function_manager>().func_addr(1, true);
|
||||
};
|
||||
|
||||
if (is_invalid(addr))
|
||||
if (is_first && !is_invalid(_lr))
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
// Function hasn't saved LR, could be because it's a leaf function
|
||||
// Use LR directly instead
|
||||
addr = lr;
|
||||
// Detect functions with no stack or before LR has been stored
|
||||
|
||||
if (is_invalid(addr))
|
||||
// Tracking if instruction has already been passed through
|
||||
// Instead of using map or set, use two vectors relative to CIA and resize as needed
|
||||
std::vector<be_t<u32>> inst_neg;
|
||||
std::vector<be_t<u32>> inst_pos;
|
||||
|
||||
auto get_inst = [&](u32 pos) -> be_t<u32>&
|
||||
{
|
||||
static be_t<u32> s_inst_empty{};
|
||||
|
||||
if (pos < _cia)
|
||||
{
|
||||
// Skip it, workaround
|
||||
continue;
|
||||
const u32 neg_dist = (_cia - pos - 4) / 4;
|
||||
|
||||
if (neg_dist >= inst_neg.size())
|
||||
{
|
||||
const u32 inst_bound = pos & -256;
|
||||
|
||||
const usz old_size = inst_neg.size();
|
||||
const usz new_size = neg_dist + (pos - inst_bound) / 4 + 1;
|
||||
|
||||
if (new_size >= 0x8000)
|
||||
{
|
||||
// Gross lower limit for the function (if it is that size it is unlikely that it is even a leaf function)
|
||||
return s_inst_empty;
|
||||
}
|
||||
|
||||
inst_neg.resize(new_size);
|
||||
|
||||
if (!vm::try_access(inst_bound, &inst_neg[old_size], (new_size - old_size) * sizeof(be_t<u32>), false))
|
||||
{
|
||||
// Failure (this would be detected as failure by zeroes)
|
||||
}
|
||||
|
||||
// Reverse the array (because this buffer directs backwards in address)
|
||||
|
||||
for (usz start = old_size, end = new_size - 1; start < end; start++, end--)
|
||||
{
|
||||
std::swap(inst_neg[start], inst_neg[end]);
|
||||
}
|
||||
}
|
||||
|
||||
return inst_neg[neg_dist];
|
||||
}
|
||||
|
||||
const u32 pos_dist = (pos - _cia) / 4;
|
||||
|
||||
if (pos_dist >= inst_pos.size())
|
||||
{
|
||||
const u32 inst_bound = utils::align<u32>(pos, 256);
|
||||
|
||||
const usz old_size = inst_pos.size();
|
||||
const usz new_size = pos_dist + (inst_bound - pos) / 4 + 1;
|
||||
|
||||
if (new_size >= 0x8000)
|
||||
{
|
||||
// Gross upper limit for the function (if it is that size it is unlikely that it is even a leaf function)
|
||||
return s_inst_empty;
|
||||
}
|
||||
|
||||
inst_pos.resize(new_size);
|
||||
|
||||
if (!vm::try_access(pos, &inst_pos[old_size], (new_size - old_size) * sizeof(be_t<u32>), false))
|
||||
{
|
||||
// Failure (this would be detected as failure by zeroes)
|
||||
}
|
||||
}
|
||||
|
||||
return inst_pos[pos_dist];
|
||||
};
|
||||
|
||||
bool upper_abort = false;
|
||||
|
||||
struct context_t
|
||||
{
|
||||
u32 start_point;
|
||||
bool maybe_leaf = false; // True if the function is leaf or at the very end/start of non-leaf
|
||||
bool non_leaf = false; // Absolutely not a leaf
|
||||
bool about_to_push_frame = false; // STDU incoming
|
||||
bool about_to_store_lr = false; // Link is about to be stored on stack
|
||||
bool about_to_pop_frame = false; // ADDI R1 is about to be issued
|
||||
bool about_to_load_link = false; // MTLR is about to be issued
|
||||
bool maybe_use_reg0_instead_of_lr = false; // Use R0 at the end of a non-leaf function if ADDI has been issued before MTLR
|
||||
};
|
||||
|
||||
// Start with CIA
|
||||
std::deque<context_t> workload{context_t{_cia}};
|
||||
|
||||
usz start = 0;
|
||||
|
||||
for (; start < workload.size(); start++)
|
||||
{
|
||||
for (u32 wa = workload[start].start_point; vm::check_addr(wa, vm::page_executable);)
|
||||
{
|
||||
be_t<u32>& opcode = get_inst(wa);
|
||||
|
||||
auto& [_, maybe_leaf, non_leaf, about_to_push_frame, about_to_store_lr,
|
||||
about_to_pop_frame, about_to_load_link, maybe_use_reg0_instead_of_lr] = workload[start];
|
||||
|
||||
if (!opcode)
|
||||
{
|
||||
// Already passed or failure of reading
|
||||
break;
|
||||
}
|
||||
|
||||
const ppu_opcode_t op{opcode};
|
||||
|
||||
// Mark as passed through
|
||||
opcode = 0;
|
||||
|
||||
const auto type = g_ppu_itype.decode(op.opcode);
|
||||
|
||||
if (workload.size() == 1 && type == ppu_itype::STDU && op.rs == 1u && op.ra == 1u)
|
||||
{
|
||||
if (op.simm16 >= 0)
|
||||
{
|
||||
// Against ABI
|
||||
non_leaf = true;
|
||||
upper_abort = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Saving LR to register: this is indeed a new function (ok because LR has not been saved yet)
|
||||
maybe_leaf = true;
|
||||
about_to_push_frame = true;
|
||||
about_to_pop_frame = false;
|
||||
upper_abort = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (workload.size() == 1 && type == ppu_itype::STD && op.ra == 1u && op.rs == 0u)
|
||||
{
|
||||
bool found_matching_stdu = false;
|
||||
|
||||
for (u32 back = 1; back < 20; back++)
|
||||
{
|
||||
be_t<u32>& opcode = get_inst(utils::sub_saturate<u32>(_cia, back * 4));
|
||||
|
||||
if (!opcode)
|
||||
{
|
||||
// Already passed or failure of reading
|
||||
break;
|
||||
}
|
||||
|
||||
const ppu_opcode_t test_op{opcode};
|
||||
|
||||
const auto type = g_ppu_itype.decode(test_op.opcode);
|
||||
|
||||
const u32 spr = ((test_op.spr >> 5) | ((test_op.spr & 0x1f) << 5));
|
||||
|
||||
if (type == ppu_itype::BCLR)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (type == ppu_itype::STDU && test_op.rs == 1u && test_op.ra == 1u)
|
||||
{
|
||||
if (0 - (test_op.ds << 2) == (op.ds << 2) - 0x10)
|
||||
{
|
||||
found_matching_stdu = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found_matching_stdu)
|
||||
{
|
||||
// Saving LR to stack: this is indeed a new function (ok because LR has not been saved yet)
|
||||
maybe_leaf = true;
|
||||
about_to_store_lr = true;
|
||||
about_to_pop_frame = true;
|
||||
upper_abort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const u32 spr = ((op.spr >> 5) | ((op.spr & 0x1f) << 5));
|
||||
|
||||
// It can be placed before or after STDU, ignore for now
|
||||
// if (workload.size() == 1 && type == ppu_itype::MFSPR && op.rs == 0u && spr == 0x8)
|
||||
// {
|
||||
// // Saving LR to register: this is indeed a new function (ok because LR has not been saved yet)
|
||||
// maybe_leaf = true;
|
||||
// about_to_store_lr = true;
|
||||
// about_to_pop_frame = true;
|
||||
// }
|
||||
|
||||
if (type == ppu_itype::MTSPR && spr == 0x8 && op.rs == 0u)
|
||||
{
|
||||
// Test for special case: if ADDI R1 is not found later in code, it means that LR is not restored and R0 should be used instead
|
||||
// Can also search for ADDI R1 backwards and pull the value from stack (needs more research if it is more reliable)
|
||||
maybe_use_reg0_instead_of_lr = true;
|
||||
}
|
||||
|
||||
if (type == ppu_itype::UNK)
|
||||
{
|
||||
// Ignore for now
|
||||
break;
|
||||
}
|
||||
|
||||
if ((type & ppu_itype::branch) && op.lk)
|
||||
{
|
||||
// Gave up on LR before saving
|
||||
non_leaf = true;
|
||||
about_to_pop_frame = true;
|
||||
upper_abort = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Even if BCLR is conditional, it still counts because LR value is ready for return
|
||||
if (type == ppu_itype::BCLR)
|
||||
{
|
||||
// Returned
|
||||
maybe_leaf = true;
|
||||
upper_abort = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (type == ppu_itype::ADDI && op.ra == 1u && op.rd == 1u)
|
||||
{
|
||||
if (op.simm16 < 0)
|
||||
{
|
||||
// Against ABI
|
||||
non_leaf = true;
|
||||
upper_abort = true;
|
||||
break;
|
||||
}
|
||||
else if (op.simm16 > 0)
|
||||
{
|
||||
// Remember that SP is about to be restored
|
||||
about_to_pop_frame = true;
|
||||
non_leaf = true;
|
||||
upper_abort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto results = op_branch_targets(wa, op);
|
||||
|
||||
bool proceeded = false;
|
||||
|
||||
for (usz res_i = 0; res_i < results.size(); res_i++)
|
||||
{
|
||||
const u32 route_pc = results[res_i];
|
||||
|
||||
if (route_pc == umax)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (vm::check_addr(route_pc, vm::page_executable) && get_inst(route_pc))
|
||||
{
|
||||
if (proceeded)
|
||||
{
|
||||
// Remember next route start point
|
||||
workload.push_back(context_t{route_pc});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Next PC
|
||||
wa = route_pc;
|
||||
proceeded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (upper_abort)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
const context_t& res = workload[start];
|
||||
|
||||
if (res.maybe_leaf && !res.non_leaf)
|
||||
{
|
||||
break;
|
||||
const u32 result = res.maybe_use_reg0_instead_of_lr ? static_cast<u32>(gpr0) : static_cast<u32>(_lr);
|
||||
|
||||
// Same stack as far as we know
|
||||
call_stack_list.emplace_back(result, static_cast<u32>(sp));
|
||||
|
||||
if (res.about_to_store_lr)
|
||||
{
|
||||
// LR has yet to be stored on stack, ignore the stack value
|
||||
skip_single_frame = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.about_to_pop_frame || (res.maybe_leaf && !res.non_leaf))
|
||||
{
|
||||
const u64 temp_sp = *vm::get_super_ptr<u64>(static_cast<u32>(sp));
|
||||
|
||||
if (temp_sp <= sp)
|
||||
{
|
||||
// Ensure inequality and that the old stack pointer is higher than current
|
||||
break;
|
||||
}
|
||||
|
||||
// Read the first stack frame so caller addresses can be obtained
|
||||
sp = temp_sp;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: function addresses too
|
||||
call_stack_list.emplace_back(static_cast<u32>(addr), static_cast<u32>(sp));
|
||||
u64 addr = *vm::get_super_ptr<u64>(static_cast<u32>(sp + 16));
|
||||
|
||||
if (skip_single_frame)
|
||||
{
|
||||
skip_single_frame = false;
|
||||
}
|
||||
else if (!is_invalid(addr))
|
||||
{
|
||||
// TODO: function addresses too
|
||||
call_stack_list.emplace_back(static_cast<u32>(addr), static_cast<u32>(sp));
|
||||
}
|
||||
else if (!is_first)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const u64 temp_sp = *vm::get_super_ptr<u64>(static_cast<u32>(sp));
|
||||
|
||||
|
@ -1616,6 +1921,8 @@ std::vector<std::pair<u32, u32>> ppu_thread::dump_callstack_list() const
|
|||
}
|
||||
|
||||
sp = temp_sp;
|
||||
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
return call_stack_list;
|
||||
|
|
|
@ -1276,6 +1276,7 @@ std::vector<std::pair<u32, u32>> spu_thread::dump_callstack_list() const
|
|||
bool first = true;
|
||||
|
||||
const v128 gpr0 = gpr[0];
|
||||
const u32 _pc = pc;
|
||||
|
||||
// Declare first 128-bytes as invalid for stack (common values such as 0 do not make sense here)
|
||||
for (u32 sp = gpr[1]._u32[3]; (sp & 0xF) == 0u && sp >= 0x80u && sp <= 0x3FFE0u; first = false)
|
||||
|
@ -1299,8 +1300,10 @@ std::vector<std::pair<u32, u32>> spu_thread::dump_callstack_list() const
|
|||
if (first && lr._u32[3] != gpr0._u32[3] && !is_invalid(gpr0))
|
||||
{
|
||||
// Detect functions with no stack or before LR has been stored
|
||||
std::vector<bool> passed(SPU_LS_SIZE / 4);
|
||||
std::vector<u32> start_points{pc};
|
||||
std::vector<bool> passed(_pc / 4);
|
||||
|
||||
// Start with PC
|
||||
std::basic_string<u32> start_points{_pc};
|
||||
|
||||
bool is_ok = false;
|
||||
bool all_failed = false;
|
||||
|
@ -1309,7 +1312,11 @@ std::vector<std::pair<u32, u32>> spu_thread::dump_callstack_list() const
|
|||
{
|
||||
for (u32 i = start_points[start]; i < SPU_LS_SIZE;)
|
||||
{
|
||||
if (passed[i / 4])
|
||||
if (i / 4 >= passed.size())
|
||||
{
|
||||
passed.resize(i / 4 + 1);
|
||||
}
|
||||
else if (passed[i / 4])
|
||||
{
|
||||
// Already passed
|
||||
break;
|
||||
|
@ -1322,7 +1329,7 @@ std::vector<std::pair<u32, u32>> spu_thread::dump_callstack_list() const
|
|||
|
||||
if (start == 0 && type == spu_itype::STQD && op.ra == 1u && op.rt == 0u)
|
||||
{
|
||||
// Saving LR to stack: this is indeed a new function
|
||||
// Saving LR to stack: this is indeed a new function (ok because LR has not been saved yet)
|
||||
is_ok = true;
|
||||
break;
|
||||
}
|
||||
|
@ -1360,12 +1367,23 @@ std::vector<std::pair<u32, u32>> spu_thread::dump_callstack_list() const
|
|||
for (usz res_i = 0; res_i < results.size(); res_i++)
|
||||
{
|
||||
const u32 route_pc = results[res_i];
|
||||
if (route_pc < SPU_LS_SIZE && !passed[route_pc / 4])
|
||||
|
||||
if (route_pc >= SPU_LS_SIZE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (route_pc / 4 >= passed.size())
|
||||
{
|
||||
passed.resize(route_pc / 4 + 1);
|
||||
}
|
||||
|
||||
if (!passed[route_pc / 4])
|
||||
{
|
||||
if (proceeded)
|
||||
{
|
||||
// Remember next route start point
|
||||
start_points.emplace_back(route_pc);
|
||||
start_points.push_back(route_pc);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -1330,7 +1330,7 @@ void debugger_frame::DoStep(bool step_over)
|
|||
ppu_opcode_t ppu_op{result};
|
||||
const ppu_itype::type itype = g_ppu_itype.decode(ppu_op.opcode);
|
||||
|
||||
should_step_over = (itype == ppu_itype::BC || itype == ppu_itype::B || itype == ppu_itype::BCCTR || itype == ppu_itype::BCLR) && ppu_op.lk;
|
||||
should_step_over = (itype & ppu_itype::branch && ppu_op.lk);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue