1329 lines
42 KiB
C++
1329 lines
42 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2020 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "xenia/cpu/processor.h"
|
|
|
|
#include "xenia/base/assert.h"
|
|
#include "xenia/base/atomic.h"
|
|
#include "xenia/base/byte_order.h"
|
|
#include "xenia/base/byte_stream.h"
|
|
#include "xenia/base/cvar.h"
|
|
#include "xenia/base/debugging.h"
|
|
#include "xenia/base/exception_handler.h"
|
|
#include "xenia/base/literals.h"
|
|
#include "xenia/base/logging.h"
|
|
#include "xenia/base/memory.h"
|
|
#include "xenia/base/platform.h"
|
|
#include "xenia/base/profiling.h"
|
|
#include "xenia/base/threading.h"
|
|
#include "xenia/cpu/breakpoint.h"
|
|
#include "xenia/cpu/cpu_flags.h"
|
|
#include "xenia/cpu/export_resolver.h"
|
|
#include "xenia/cpu/module.h"
|
|
#include "xenia/cpu/ppc/ppc_decode_data.h"
|
|
#include "xenia/cpu/ppc/ppc_frontend.h"
|
|
#include "xenia/cpu/stack_walker.h"
|
|
#include "xenia/cpu/thread.h"
|
|
#include "xenia/cpu/thread_state.h"
|
|
#include "xenia/cpu/xex_module.h"
|
|
|
|
// TODO(benvanik): based on compiler support
|
|
#include "xenia/cpu/backend/x64/x64_backend.h"
|
|
|
|
#if 0 && DEBUG
|
|
#define DEFAULT_DEBUG_FLAG true
|
|
#else
|
|
#define DEFAULT_DEBUG_FLAG false
|
|
#endif
|
|
|
|
DEFINE_bool(debug, DEFAULT_DEBUG_FLAG,
|
|
"Allow debugging and retain debug information.", "General");
|
|
DEFINE_path(trace_function_data_path, "", "File to write trace data to.",
|
|
"CPU");
|
|
DEFINE_bool(break_on_start, false, "Break into the debugger on startup.",
|
|
"CPU");
|
|
|
|
namespace xe {
|
|
namespace kernel {
|
|
class XThread;
|
|
} // namespace kernel
|
|
|
|
namespace cpu {
|
|
|
|
using xe::cpu::ppc::PPCOpcode;
|
|
using xe::kernel::XThread;
|
|
|
|
using namespace xe::literals;
|
|
|
|
class BuiltinModule : public Module {
|
|
public:
|
|
explicit BuiltinModule(Processor* processor)
|
|
: Module(processor), name_("builtin") {}
|
|
|
|
const std::string& name() const override { return name_; }
|
|
bool is_executable() const override { return false; }
|
|
|
|
bool ContainsAddress(uint32_t address) override {
|
|
return (address & 0xFFFFFFF0) == 0xFFFFFFF0;
|
|
}
|
|
|
|
protected:
|
|
std::unique_ptr<Function> CreateFunction(uint32_t address) override {
|
|
return std::unique_ptr<Function>(new BuiltinFunction(this, address));
|
|
}
|
|
|
|
private:
|
|
std::string name_;
|
|
};
|
|
|
|
Processor::Processor(xe::Memory* memory, ExportResolver* export_resolver)
|
|
: memory_(memory), export_resolver_(export_resolver) {}
|
|
|
|
Processor::~Processor() {
|
|
{
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
modules_.clear();
|
|
}
|
|
|
|
frontend_.reset();
|
|
backend_.reset();
|
|
|
|
if (functions_trace_file_) {
|
|
functions_trace_file_->Flush();
|
|
functions_trace_file_.reset();
|
|
}
|
|
}
|
|
|
|
bool Processor::Setup(std::unique_ptr<backend::Backend> backend) {
|
|
// TODO(benvanik): query mode from debugger?
|
|
debug_info_flags_ = 0;
|
|
|
|
auto frontend = std::make_unique<ppc::PPCFrontend>(this);
|
|
// TODO(benvanik): set options/etc.
|
|
|
|
// Must be initialized by subclass before calling into this.
|
|
assert_not_null(memory_);
|
|
|
|
std::unique_ptr<Module> builtin_module(new BuiltinModule(this));
|
|
builtin_module_ = builtin_module.get();
|
|
modules_.push_back(std::move(builtin_module));
|
|
|
|
if (frontend_ || backend_) {
|
|
return false;
|
|
}
|
|
|
|
if (!backend) {
|
|
return false;
|
|
}
|
|
if (!backend->Initialize(this)) {
|
|
return false;
|
|
}
|
|
if (!frontend->Initialize()) {
|
|
return false;
|
|
}
|
|
|
|
backend_ = std::move(backend);
|
|
frontend_ = std::move(frontend);
|
|
|
|
// Stack walker is used when profiling, debugging, and dumping.
|
|
// Note that creation may fail, in which case we'll have to disable those
|
|
// features.
|
|
// The code cache may be unavailable in case of a "null" backend.
|
|
cpu::backend::CodeCache* code_cache = backend_->code_cache();
|
|
if (code_cache) {
|
|
stack_walker_ = StackWalker::Create(code_cache);
|
|
}
|
|
if (!stack_walker_) {
|
|
// TODO(benvanik): disable features.
|
|
if (cvars::debug) {
|
|
XELOGW("Disabling --debug due to lack of stack walker");
|
|
cvars::debug = false;
|
|
}
|
|
}
|
|
|
|
// Open the trace data path, if requested.
|
|
functions_trace_path_ = cvars::trace_function_data_path;
|
|
if (!functions_trace_path_.empty()) {
|
|
functions_trace_file_ =
|
|
ChunkedMappedMemoryWriter::Open(functions_trace_path_, 32_MiB, true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Processor::PreLaunch() {
|
|
if (cvars::break_on_start) {
|
|
// Start paused.
|
|
XELOGI("Breaking into debugger because of --break_on_start...");
|
|
execution_state_ = ExecutionState::kRunning;
|
|
Pause();
|
|
} else {
|
|
// Start running.
|
|
execution_state_ = ExecutionState::kRunning;
|
|
}
|
|
}
|
|
|
|
bool Processor::AddModule(std::unique_ptr<Module> module) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
modules_.push_back(std::move(module));
|
|
return true;
|
|
}
|
|
|
|
void Processor::RemoveModule(const std::string_view name) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
|
|
auto itr =
|
|
std::find_if(modules_.cbegin(), modules_.cend(),
|
|
[name](std::unique_ptr<xe::cpu::Module> const& module) {
|
|
return module->name() == name;
|
|
});
|
|
|
|
if (itr != modules_.cend()) {
|
|
const std::vector<uint32_t> addressed_functions =
|
|
(*itr)->GetAddressedFunctions();
|
|
|
|
modules_.erase(itr);
|
|
|
|
for (const uint32_t entry : addressed_functions) {
|
|
RemoveFunctionByAddress(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
Module* Processor::GetModule(const std::string_view name) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
for (const auto& module : modules_) {
|
|
if (module->name() == name) {
|
|
return module.get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<Module*> Processor::GetModules() {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
std::vector<Module*> clone(modules_.size());
|
|
for (const auto& module : modules_) {
|
|
clone.push_back(module.get());
|
|
}
|
|
return clone;
|
|
}
|
|
|
|
Function* Processor::DefineBuiltin(const std::string_view name,
|
|
BuiltinFunction::Handler handler, void* arg0,
|
|
void* arg1) {
|
|
uint32_t address = next_builtin_address_;
|
|
next_builtin_address_ += 4;
|
|
|
|
Function* function;
|
|
builtin_module_->DeclareFunction(address, &function);
|
|
function->set_end_address(address + 4);
|
|
function->set_name(name);
|
|
|
|
auto builtin_function = static_cast<BuiltinFunction*>(function);
|
|
builtin_function->SetupBuiltin(handler, arg0, arg1);
|
|
|
|
function->set_status(Symbol::Status::kDeclared);
|
|
return function;
|
|
}
|
|
|
|
Function* Processor::QueryFunction(uint32_t address) {
|
|
auto entry = entry_table_.Get(address);
|
|
if (!entry) {
|
|
return nullptr;
|
|
}
|
|
return entry->function;
|
|
}
|
|
|
|
std::vector<Function*> Processor::FindFunctionsWithAddress(uint32_t address) {
|
|
return entry_table_.FindWithAddress(address);
|
|
}
|
|
|
|
void Processor::RemoveFunctionByAddress(uint32_t address) {
|
|
entry_table_.Delete(address);
|
|
}
|
|
|
|
Function* Processor::ResolveFunction(uint32_t address) {
|
|
Entry* entry;
|
|
Entry::Status status = entry_table_.GetOrCreate(address, &entry);
|
|
if (status == Entry::STATUS_NEW) {
|
|
// Needs to be generated. We have the 'lock' on it and must do so now.
|
|
|
|
// Grab symbol declaration.
|
|
auto function = LookupFunction(address);
|
|
if (!function) {
|
|
entry->status = Entry::STATUS_FAILED;
|
|
return nullptr;
|
|
}
|
|
|
|
if (!DemandFunction(function)) {
|
|
entry->status = Entry::STATUS_FAILED;
|
|
return nullptr;
|
|
}
|
|
entry->function = function;
|
|
entry->end_address = function->end_address();
|
|
status = entry->status = Entry::STATUS_READY;
|
|
}
|
|
if (status == Entry::STATUS_READY) {
|
|
// Ready to use.
|
|
return entry->function;
|
|
} else {
|
|
// Failed or bad state.
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
Function* Processor::LookupFunction(uint32_t address) {
|
|
// TODO(benvanik): fast reject invalid addresses/log errors.
|
|
|
|
// Find the module that contains the address.
|
|
Module* code_module = nullptr;
|
|
{
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
// TODO(benvanik): sort by code address (if contiguous) so can bsearch.
|
|
// TODO(benvanik): cache last module low/high, as likely to be in there.
|
|
for (const auto& module : modules_) {
|
|
if (module->ContainsAddress(address)) {
|
|
code_module = module.get();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!code_module) {
|
|
// No module found that could contain the address.
|
|
return nullptr;
|
|
}
|
|
|
|
return LookupFunction(code_module, address);
|
|
}
|
|
|
|
Function* Processor::LookupFunction(Module* module, uint32_t address) {
|
|
// Atomic create/lookup symbol in module.
|
|
// If we get back the NEW flag we must declare it now.
|
|
Function* function = nullptr;
|
|
auto symbol_status = module->DeclareFunction(address, &function);
|
|
if (symbol_status == Symbol::Status::kNew) {
|
|
// Symbol is undeclared, so declare now.
|
|
assert_true(function->is_guest());
|
|
if (!frontend_->DeclareFunction(static_cast<GuestFunction*>(function))) {
|
|
function->set_status(Symbol::Status::kFailed);
|
|
return nullptr;
|
|
}
|
|
function->set_status(Symbol::Status::kDeclared);
|
|
}
|
|
return function;
|
|
}
|
|
|
|
bool Processor::DemandFunction(Function* function) {
|
|
// Lock function for generation. If it's already being generated
|
|
// by another thread this will block and return DECLARED.
|
|
auto module = function->module();
|
|
auto symbol_status = module->DefineFunction(function);
|
|
if (symbol_status == Symbol::Status::kNew) {
|
|
// Symbol is undefined, so define now.
|
|
assert_true(function->is_guest());
|
|
if (!frontend_->DefineFunction(static_cast<GuestFunction*>(function),
|
|
debug_info_flags_)) {
|
|
function->set_status(Symbol::Status::kFailed);
|
|
return false;
|
|
}
|
|
|
|
// Before we give the symbol back to the rest, let the debugger know.
|
|
OnFunctionDefined(function);
|
|
|
|
function->set_status(Symbol::Status::kDefined);
|
|
symbol_status = function->status();
|
|
}
|
|
|
|
if (symbol_status == Symbol::Status::kFailed) {
|
|
// Symbol likely failed.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Processor::Execute(ThreadState* thread_state, uint32_t address) {
|
|
SCOPE_profile_cpu_f("cpu");
|
|
|
|
// Attempt to get the function.
|
|
auto function = ResolveFunction(address);
|
|
if (!function) {
|
|
// Symbol not found in any module.
|
|
XELOGCPU("Execute({:08X}): failed to find function", address);
|
|
return false;
|
|
}
|
|
|
|
auto context = thread_state->context();
|
|
|
|
// Pad out stack a bit, as some games seem to overwrite the caller by about
|
|
// 16 to 32b.
|
|
context->r[1] -= 64 + 112;
|
|
|
|
// This could be set to anything to give us a unique identifier to track
|
|
// re-entrancy/etc.
|
|
uint64_t previous_lr = context->lr;
|
|
context->lr = 0xBCBCBCBC;
|
|
|
|
// Execute the function.
|
|
auto result = function->Call(thread_state, uint32_t(context->lr));
|
|
|
|
context->lr = previous_lr;
|
|
context->r[1] += 64 + 112;
|
|
|
|
return result;
|
|
}
|
|
|
|
bool Processor::ExecuteRaw(ThreadState* thread_state, uint32_t address) {
|
|
SCOPE_profile_cpu_f("cpu");
|
|
|
|
// Attempt to get the function.
|
|
auto function = ResolveFunction(address);
|
|
if (!function) {
|
|
// Symbol not found in any module.
|
|
XELOGCPU("Execute({:08X}): failed to find function", address);
|
|
return false;
|
|
}
|
|
|
|
return function->Call(thread_state, 0xBCBCBCBC);
|
|
}
|
|
|
|
uint64_t Processor::Execute(ThreadState* thread_state, uint32_t address,
|
|
uint64_t args[], size_t arg_count) {
|
|
SCOPE_profile_cpu_f("cpu");
|
|
|
|
auto context = thread_state->context();
|
|
for (size_t i = 0; i < std::min(arg_count, static_cast<size_t>(8)); ++i) {
|
|
context->r[3 + i] = args[i];
|
|
}
|
|
|
|
if (arg_count > 7) {
|
|
// Rest of the arguments go on the stack.
|
|
// FIXME: This assumes arguments are 32 bits!
|
|
auto stack_arg_base =
|
|
memory()->TranslateVirtual((uint32_t)context->r[1] + 0x54 - (64 + 112));
|
|
for (size_t i = 0; i < arg_count - 8; i++) {
|
|
xe::store_and_swap<uint32_t>(stack_arg_base + (i * 8),
|
|
(uint32_t)args[i + 8]);
|
|
}
|
|
}
|
|
|
|
if (!Execute(thread_state, address)) {
|
|
return 0xDEADBABE;
|
|
}
|
|
return context->r[3];
|
|
}
|
|
|
|
uint64_t Processor::ExecuteInterrupt(ThreadState* thread_state,
|
|
uint32_t address, uint64_t args[],
|
|
size_t arg_count) {
|
|
SCOPE_profile_cpu_f("cpu");
|
|
|
|
// Hold the global lock during interrupt dispatch.
|
|
// This will block if any code is in a critical region (has interrupts
|
|
// disabled) or if any other interrupt is executing.
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
|
|
auto context = thread_state->context();
|
|
assert_true(arg_count <= 5);
|
|
for (size_t i = 0; i < arg_count; ++i) {
|
|
context->r[3 + i] = args[i];
|
|
}
|
|
|
|
// TLS ptr must be zero during interrupts. Some games check this and
|
|
// early-exit routines when under interrupts.
|
|
auto pcr_address =
|
|
memory_->TranslateVirtual(static_cast<uint32_t>(context->r[13]));
|
|
uint32_t old_tls_ptr = xe::load_and_swap<uint32_t>(pcr_address);
|
|
xe::store_and_swap<uint32_t>(pcr_address, 0);
|
|
|
|
if (!Execute(thread_state, address)) {
|
|
return 0xDEADBABE;
|
|
}
|
|
|
|
// Restores TLS ptr.
|
|
xe::store_and_swap<uint32_t>(pcr_address, old_tls_ptr);
|
|
|
|
return context->r[3];
|
|
}
|
|
|
|
Irql Processor::RaiseIrql(Irql new_value) {
|
|
return static_cast<Irql>(
|
|
xe::atomic_exchange(static_cast<uint32_t>(new_value),
|
|
reinterpret_cast<volatile uint32_t*>(&irql_)));
|
|
}
|
|
|
|
void Processor::LowerIrql(Irql old_value) {
|
|
xe::atomic_exchange(static_cast<uint32_t>(old_value),
|
|
reinterpret_cast<volatile uint32_t*>(&irql_));
|
|
}
|
|
|
|
bool Processor::Save(ByteStream* stream) {
|
|
stream->Write(kProcessorSaveSignature);
|
|
return true;
|
|
}
|
|
|
|
bool Processor::Restore(ByteStream* stream) {
|
|
if (stream->Read<uint32_t>() != kProcessorSaveSignature) {
|
|
XELOGE("Processor::Restore - Invalid magic value!");
|
|
return false;
|
|
}
|
|
|
|
// Clear cached thread data for zombie threads.
|
|
std::vector<uint32_t> to_delete;
|
|
for (auto& it : thread_debug_infos_) {
|
|
if (it.second->state == ThreadDebugInfo::State::kZombie) {
|
|
to_delete.push_back(it.first);
|
|
}
|
|
}
|
|
for (uint32_t thread_id : to_delete) {
|
|
thread_debug_infos_.erase(thread_id);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint8_t* Processor::AllocateFunctionTraceData(size_t size) {
|
|
if (!functions_trace_file_) {
|
|
return nullptr;
|
|
}
|
|
return functions_trace_file_->Allocate(size);
|
|
}
|
|
|
|
void Processor::OnFunctionDefined(Function* function) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
for (auto breakpoint : breakpoints_) {
|
|
if (breakpoint->address_type() == Breakpoint::AddressType::kGuest) {
|
|
if (function->ContainsAddress(breakpoint->guest_address())) {
|
|
if (breakpoint->is_installed()) {
|
|
backend_->InstallBreakpoint(breakpoint, function);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Processor::OnThreadCreated(uint32_t thread_handle,
|
|
ThreadState* thread_state, Thread* thread) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
auto thread_info = std::make_unique<ThreadDebugInfo>();
|
|
thread_info->thread_handle = thread_handle;
|
|
thread_info->thread_id = thread_state->thread_id();
|
|
thread_info->thread = thread;
|
|
thread_info->state = ThreadDebugInfo::State::kAlive;
|
|
thread_info->suspended = false;
|
|
thread_debug_infos_.emplace(thread_info->thread_id, std::move(thread_info));
|
|
}
|
|
|
|
void Processor::OnThreadExit(uint32_t thread_id) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
auto it = thread_debug_infos_.find(thread_id);
|
|
assert_true(it != thread_debug_infos_.end());
|
|
auto thread_info = it->second.get();
|
|
thread_info->state = ThreadDebugInfo::State::kExited;
|
|
}
|
|
|
|
void Processor::OnThreadDestroyed(uint32_t thread_id) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
auto it = thread_debug_infos_.find(thread_id);
|
|
assert_true(it != thread_debug_infos_.end());
|
|
thread_debug_infos_.erase(it);
|
|
}
|
|
|
|
void Processor::OnThreadEnteringWait(uint32_t thread_id) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
auto it = thread_debug_infos_.find(thread_id);
|
|
assert_true(it != thread_debug_infos_.end());
|
|
auto thread_info = it->second.get();
|
|
thread_info->state = ThreadDebugInfo::State::kWaiting;
|
|
}
|
|
|
|
void Processor::OnThreadLeavingWait(uint32_t thread_id) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
auto it = thread_debug_infos_.find(thread_id);
|
|
assert_true(it != thread_debug_infos_.end());
|
|
auto thread_info = it->second.get();
|
|
if (thread_info->state == ThreadDebugInfo::State::kWaiting) {
|
|
thread_info->state = ThreadDebugInfo::State::kAlive;
|
|
}
|
|
}
|
|
|
|
std::vector<ThreadDebugInfo*> Processor::QueryThreadDebugInfos() {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
std::vector<ThreadDebugInfo*> result;
|
|
for (auto& it : thread_debug_infos_) {
|
|
result.push_back(it.second.get());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ThreadDebugInfo* Processor::QueryThreadDebugInfo(uint32_t thread_id) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
const auto& it = thread_debug_infos_.find(thread_id);
|
|
if (it == thread_debug_infos_.end()) {
|
|
return nullptr;
|
|
}
|
|
return it->second.get();
|
|
}
|
|
|
|
void Processor::AddBreakpoint(Breakpoint* breakpoint) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
|
|
// Add to breakpoints map.
|
|
breakpoints_.push_back(breakpoint);
|
|
|
|
if (execution_state_ == ExecutionState::kRunning) {
|
|
breakpoint->Resume();
|
|
}
|
|
}
|
|
|
|
void Processor::RemoveBreakpoint(Breakpoint* breakpoint) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
|
|
// Uninstall (if needed).
|
|
if (execution_state_ == ExecutionState::kRunning) {
|
|
breakpoint->Suspend();
|
|
}
|
|
|
|
// Remove from breakpoint map.
|
|
auto it = std::find(breakpoints_.begin(), breakpoints_.end(), breakpoint);
|
|
breakpoints_.erase(it);
|
|
}
|
|
|
|
Breakpoint* Processor::FindBreakpoint(uint32_t address) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
for (auto breakpoint : breakpoints_) {
|
|
if (breakpoint->address() == address) {
|
|
return breakpoint;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Processor::set_debug_listener(DebugListener* debug_listener) {
|
|
if (debug_listener == debug_listener_) {
|
|
return;
|
|
}
|
|
if (debug_listener_) {
|
|
// Detach old debug listener.
|
|
debug_listener_->OnDetached();
|
|
debug_listener_ = nullptr;
|
|
}
|
|
if (debug_listener) {
|
|
debug_listener_ = debug_listener;
|
|
} else {
|
|
if (execution_state_ == ExecutionState::kPaused) {
|
|
XELOGI("Debugger detaching while execution is paused; continuing...");
|
|
Continue();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Processor::DemandDebugListener() {
|
|
if (debug_listener_) {
|
|
// Already present.
|
|
debug_listener_->OnFocus();
|
|
return;
|
|
}
|
|
if (!debug_listener_handler_) {
|
|
XELOGE("Debugger demanded a listener but no handler was registered.");
|
|
xe::debugging::Break();
|
|
return;
|
|
}
|
|
set_debug_listener(debug_listener_handler_(this));
|
|
}
|
|
|
|
bool Processor::OnThreadBreakpointHit(Exception* ex) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
|
|
// Suspend all threads (but ourselves).
|
|
SuspendAllThreads();
|
|
|
|
// Lookup thread info block.
|
|
auto it = thread_debug_infos_.find(ThreadState::GetThreadID());
|
|
if (it == thread_debug_infos_.end()) {
|
|
// Not found - exception on a thread we don't know about?
|
|
assert_always("UD2 on a thread we don't track");
|
|
return false;
|
|
}
|
|
auto thread_info = it->second.get();
|
|
|
|
// Run through and uninstall all breakpoint UD2s to get us back to a clean
|
|
// state.
|
|
if (execution_state_ != ExecutionState::kStepping) {
|
|
SuspendAllBreakpoints();
|
|
}
|
|
|
|
// Update all thread states with their latest values, using the context we
|
|
// got from the exception instead of a sampled value (as it would just show
|
|
// the exception handler).
|
|
UpdateThreadExecutionStates(thread_info->thread_id, ex->thread_context());
|
|
|
|
// Walk the captured thread stack and look for breakpoints at any address in
|
|
// the stack. We just look for the first one.
|
|
Breakpoint* breakpoint = nullptr;
|
|
for (size_t i = 0; i < thread_info->frames.size(); ++i) {
|
|
auto& frame = thread_info->frames[i];
|
|
for (auto scan_breakpoint : breakpoints_) {
|
|
if ((scan_breakpoint->address_type() == Breakpoint::AddressType::kGuest &&
|
|
scan_breakpoint->guest_address() == frame.guest_pc) ||
|
|
(scan_breakpoint->address_type() == Breakpoint::AddressType::kHost &&
|
|
scan_breakpoint->host_address() == frame.host_pc)) {
|
|
breakpoint = scan_breakpoint;
|
|
break;
|
|
}
|
|
}
|
|
if (breakpoint) {
|
|
breakpoint->OnHit(thread_info, frame.host_pc);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We are waiting on the debugger now. Either wait for it to continue, add a
|
|
// new step, or direct us somewhere else.
|
|
// The debugger will ResumeAllThreads or just resume us (depending on what
|
|
// it wants to do).
|
|
execution_state_ = ExecutionState::kPaused;
|
|
thread_info->suspended = true;
|
|
|
|
// Must unlock, or we will deadlock.
|
|
global_lock.unlock();
|
|
|
|
if (debug_listener_) {
|
|
debug_listener_->OnExecutionPaused();
|
|
}
|
|
|
|
thread_info->thread->thread()->Suspend();
|
|
|
|
// Apply thread context changes.
|
|
// TODO(benvanik): apply to all threads?
|
|
#if XE_ARCH_AMD64
|
|
ex->set_resume_pc(thread_info->host_context.rip);
|
|
#elif XE_ARCH_ARM64
|
|
ex->set_resume_pc(thread_info->host_context.pc);
|
|
#else
|
|
#error Instruction pointer not specified for the target CPU architecture.
|
|
#endif // XE_ARCH
|
|
|
|
// Resume execution.
|
|
return true;
|
|
}
|
|
|
|
void Processor::OnStepCompleted(ThreadDebugInfo* thread_info) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
execution_state_ = ExecutionState::kPaused;
|
|
if (debug_listener_) {
|
|
debug_listener_->OnExecutionPaused();
|
|
}
|
|
|
|
// Note that we stay suspended.
|
|
}
|
|
|
|
bool Processor::OnUnhandledException(Exception* ex) {
|
|
// If we have no listener return right away.
|
|
// TODO(benvanik): DemandDebugListener()?
|
|
if (!debug_listener_) {
|
|
return false;
|
|
}
|
|
|
|
// If this isn't a managed thread, fail - let VS handle it for now.
|
|
if (!Thread::IsInThread()) {
|
|
return false;
|
|
}
|
|
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
|
|
// Suspend all guest threads (but this one).
|
|
SuspendAllThreads();
|
|
|
|
UpdateThreadExecutionStates(Thread::GetCurrentThreadId(),
|
|
ex->thread_context());
|
|
|
|
// Stop and notify the listener.
|
|
// This will take control.
|
|
assert_true(execution_state_ == ExecutionState::kRunning);
|
|
execution_state_ = ExecutionState::kPaused;
|
|
|
|
// Notify debugger that exceution stopped.
|
|
// debug_listener_->OnException(info);
|
|
debug_listener_->OnExecutionPaused();
|
|
|
|
// Suspend self.
|
|
Thread::GetCurrentThread()->thread()->Suspend();
|
|
|
|
return true;
|
|
}
|
|
|
|
void Processor::ShowDebugger() {
|
|
if (debug_listener_) {
|
|
debug_listener_->OnFocus();
|
|
} else {
|
|
DemandDebugListener();
|
|
}
|
|
}
|
|
|
|
bool Processor::SuspendAllThreads() {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
for (auto& it : thread_debug_infos_) {
|
|
auto thread_info = it.second.get();
|
|
if (thread_info->suspended) {
|
|
// Already suspended - ignore.
|
|
continue;
|
|
} else if (thread_info->state == ThreadDebugInfo::State::kZombie ||
|
|
thread_info->state == ThreadDebugInfo::State::kExited) {
|
|
// Thread is dead and cannot be suspended - ignore.
|
|
continue;
|
|
} else if (Thread::IsInThread() &&
|
|
thread_info->thread_id == Thread::GetCurrentThreadId()) {
|
|
// Can't suspend ourselves.
|
|
continue;
|
|
}
|
|
auto thread = thread_info->thread;
|
|
if (!thread->can_debugger_suspend()) {
|
|
// Thread is a host thread, and we aren't suspending those (for now).
|
|
continue;
|
|
}
|
|
bool did_suspend = thread->thread()->Suspend(nullptr);
|
|
assert_true(did_suspend);
|
|
thread_info->suspended = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Processor::ResumeThread(uint32_t thread_id) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
auto it = thread_debug_infos_.find(thread_id);
|
|
if (it == thread_debug_infos_.end()) {
|
|
return false;
|
|
}
|
|
auto thread_info = it->second.get();
|
|
assert_true(thread_info->suspended);
|
|
assert_false(thread_info->state == ThreadDebugInfo::State::kExited ||
|
|
thread_info->state == ThreadDebugInfo::State::kZombie);
|
|
thread_info->suspended = false;
|
|
auto thread = thread_info->thread;
|
|
return thread->thread()->Resume();
|
|
}
|
|
|
|
bool Processor::ResumeAllThreads() {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
for (auto& it : thread_debug_infos_) {
|
|
auto thread_info = it.second.get();
|
|
if (!thread_info->suspended) {
|
|
// Not suspended by us - ignore.
|
|
continue;
|
|
} else if (thread_info->state == ThreadDebugInfo::State::kZombie ||
|
|
thread_info->state == ThreadDebugInfo::State::kExited) {
|
|
// Thread is dead and cannot be resumed - ignore.
|
|
continue;
|
|
} else if (Thread::IsInThread() &&
|
|
thread_info->thread_id == Thread::GetCurrentThreadId()) {
|
|
// Can't resume ourselves.
|
|
continue;
|
|
}
|
|
auto thread = thread_info->thread;
|
|
if (!thread->can_debugger_suspend()) {
|
|
// Thread is a host thread, and we aren't suspending those (for now).
|
|
continue;
|
|
}
|
|
thread_info->suspended = false;
|
|
bool did_resume = thread->thread()->Resume();
|
|
assert_true(did_resume);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Processor::UpdateThreadExecutionStates(
|
|
uint32_t override_thread_id, HostThreadContext* override_context) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
uint64_t frame_host_pcs[64];
|
|
xe::cpu::StackFrame cpu_frames[64];
|
|
for (auto& it : thread_debug_infos_) {
|
|
auto thread_info = it.second.get();
|
|
auto thread = thread_info->thread;
|
|
if (!thread) {
|
|
continue;
|
|
}
|
|
|
|
// Grab PPC context.
|
|
// Note that this is only up to date if --store_all_context_values is
|
|
// enabled (or --debug).
|
|
if (thread->can_debugger_suspend()) {
|
|
std::memcpy(&thread_info->guest_context,
|
|
thread->thread_state()->context(),
|
|
sizeof(thread_info->guest_context));
|
|
}
|
|
|
|
// Grab stack trace and X64 context then resolve all symbols.
|
|
uint64_t hash;
|
|
HostThreadContext* in_host_context = nullptr;
|
|
if (override_thread_id == thread_info->thread_id) {
|
|
// If we were passed an override context we use that. Otherwise, ask the
|
|
// stack walker for a new context.
|
|
in_host_context = override_context;
|
|
}
|
|
size_t count = stack_walker_->CaptureStackTrace(
|
|
thread->thread()->native_handle(), frame_host_pcs, 0,
|
|
xe::countof(frame_host_pcs), in_host_context,
|
|
&thread_info->host_context, &hash);
|
|
stack_walker_->ResolveStack(frame_host_pcs, cpu_frames, count);
|
|
thread_info->frames.resize(count);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
auto& cpu_frame = cpu_frames[i];
|
|
auto& frame = thread_info->frames[i];
|
|
frame.host_pc = cpu_frame.host_pc;
|
|
frame.host_function_address = cpu_frame.host_symbol.address;
|
|
frame.guest_pc = cpu_frame.guest_pc;
|
|
frame.guest_function_address = 0;
|
|
frame.guest_function = nullptr;
|
|
auto function = cpu_frame.guest_symbol.function;
|
|
if (cpu_frame.type == cpu::StackFrame::Type::kGuest && function) {
|
|
frame.guest_function_address = function->address();
|
|
frame.guest_function = function;
|
|
} else {
|
|
std::strncpy(frame.name, cpu_frame.host_symbol.name,
|
|
xe::countof(frame.name));
|
|
frame.name[xe::countof(frame.name) - 1] = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Processor::SuspendAllBreakpoints() {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
for (auto breakpoint : breakpoints_) {
|
|
breakpoint->Suspend();
|
|
}
|
|
}
|
|
|
|
void Processor::ResumeAllBreakpoints() {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
for (auto breakpoint : breakpoints_) {
|
|
breakpoint->Resume();
|
|
}
|
|
}
|
|
|
|
void Processor::Pause() {
|
|
{
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
assert_true(execution_state_ == ExecutionState::kRunning);
|
|
SuspendAllThreads();
|
|
SuspendAllBreakpoints();
|
|
UpdateThreadExecutionStates();
|
|
execution_state_ = ExecutionState::kPaused;
|
|
if (debug_listener_) {
|
|
debug_listener_->OnExecutionPaused();
|
|
}
|
|
}
|
|
DemandDebugListener();
|
|
}
|
|
|
|
void Processor::Continue() {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
if (execution_state_ == ExecutionState::kRunning) {
|
|
return;
|
|
} else if (execution_state_ == ExecutionState::kStepping) {
|
|
assert_always("cancel stepping not done yet");
|
|
}
|
|
execution_state_ = ExecutionState::kRunning;
|
|
ResumeAllBreakpoints();
|
|
ResumeAllThreads();
|
|
if (debug_listener_) {
|
|
debug_listener_->OnExecutionContinued();
|
|
}
|
|
}
|
|
|
|
void Processor::StepHostInstruction(uint32_t thread_id) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
assert_true(execution_state_ == ExecutionState::kPaused);
|
|
execution_state_ = ExecutionState::kStepping;
|
|
|
|
auto thread_info = QueryThreadDebugInfo(thread_id);
|
|
uint64_t new_host_pc = backend_->CalculateNextHostInstruction(
|
|
thread_info, thread_info->frames[0].host_pc);
|
|
|
|
assert_null(thread_info->step_breakpoint.get());
|
|
thread_info->step_breakpoint.reset(
|
|
new Breakpoint(this, Breakpoint::AddressType::kHost, new_host_pc,
|
|
[this, thread_info](Breakpoint* breakpoint,
|
|
ThreadDebugInfo* breaking_thread_info,
|
|
uint64_t host_address) {
|
|
if (thread_info != breaking_thread_info) {
|
|
assert_always("Step in another thread?");
|
|
}
|
|
// Our step request has completed. Remove the breakpoint
|
|
// and fire event.
|
|
breakpoint->Suspend();
|
|
RemoveBreakpoint(breakpoint);
|
|
thread_info->step_breakpoint.reset();
|
|
OnStepCompleted(thread_info);
|
|
}));
|
|
AddBreakpoint(thread_info->step_breakpoint.get());
|
|
thread_info->step_breakpoint->Resume();
|
|
|
|
// ResumeAllBreakpoints();
|
|
ResumeThread(thread_id);
|
|
}
|
|
|
|
void Processor::StepGuestInstruction(uint32_t thread_id) {
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
assert_true(execution_state_ == ExecutionState::kPaused);
|
|
execution_state_ = ExecutionState::kStepping;
|
|
|
|
auto thread_info = QueryThreadDebugInfo(thread_id);
|
|
|
|
uint32_t next_pc = CalculateNextGuestInstruction(
|
|
thread_info, thread_info->frames[0].guest_pc);
|
|
|
|
assert_null(thread_info->step_breakpoint.get());
|
|
thread_info->step_breakpoint.reset(
|
|
new Breakpoint(this, Breakpoint::AddressType::kGuest, next_pc,
|
|
[this, thread_info](Breakpoint* breakpoint,
|
|
ThreadDebugInfo* breaking_thread_info,
|
|
uint64_t host_address) {
|
|
if (thread_info != breaking_thread_info) {
|
|
assert_always("Step in another thread?");
|
|
}
|
|
// Our step request has completed. Remove the breakpoint
|
|
// and fire event.
|
|
breakpoint->Suspend();
|
|
RemoveBreakpoint(breakpoint);
|
|
thread_info->step_breakpoint.reset();
|
|
OnStepCompleted(thread_info);
|
|
}));
|
|
AddBreakpoint(thread_info->step_breakpoint.get());
|
|
thread_info->step_breakpoint->Resume();
|
|
|
|
// ResumeAllBreakpoints();
|
|
ResumeThread(thread_id);
|
|
}
|
|
|
|
bool Processor::StepToGuestAddress(uint32_t thread_id, uint32_t pc) {
|
|
auto functions = FindFunctionsWithAddress(pc);
|
|
if (functions.empty()) {
|
|
// Function hasn't been generated yet. Generate it.
|
|
if (!ResolveFunction(pc)) {
|
|
XELOGE(
|
|
"Processor::StepToAddress({:08X}) - Function could not be resolved",
|
|
pc);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Instruct the thread to step forwards.
|
|
threading::Fence fence;
|
|
cpu::Breakpoint bp(
|
|
this, Breakpoint::AddressType::kGuest, pc,
|
|
[&fence](Breakpoint* breakpoint, ThreadDebugInfo* thread_info,
|
|
uint64_t host_address) { fence.Signal(); });
|
|
bp.Resume();
|
|
|
|
// HACK
|
|
auto thread_info = QueryThreadDebugInfo(thread_id);
|
|
uint32_t suspend_count = 1;
|
|
while (suspend_count) {
|
|
thread_info->thread->thread()->Resume(&suspend_count);
|
|
}
|
|
|
|
fence.Wait();
|
|
bp.Suspend();
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32_t Processor::StepIntoGuestBranchTarget(uint32_t thread_id, uint32_t pc) {
|
|
xe::cpu::ppc::PPCDecodeData d;
|
|
d.address = pc;
|
|
d.code = xe::load_and_swap<uint32_t>(memory()->TranslateVirtual(d.address));
|
|
auto opcode = xe::cpu::ppc::LookupOpcode(d.code);
|
|
|
|
// Must be on a branch.
|
|
assert_true(xe::cpu::ppc::GetOpcodeInfo(opcode).group ==
|
|
xe::cpu::ppc::PPCOpcodeGroup::kB);
|
|
|
|
auto thread_info = QueryThreadDebugInfo(thread_id);
|
|
auto thread = thread_info->thread;
|
|
auto context = thread->thread_state()->context();
|
|
|
|
if (d.code == 0x4E800020) {
|
|
// blr
|
|
uint32_t nia = uint32_t(context->lr);
|
|
StepToGuestAddress(thread_id, nia);
|
|
pc = nia;
|
|
} else if (d.code == 0x4E800420) {
|
|
// bctr
|
|
uint32_t nia = uint32_t(context->ctr);
|
|
StepToGuestAddress(thread_id, nia);
|
|
pc = nia;
|
|
} else if (opcode == PPCOpcode::bx) {
|
|
// bx
|
|
uint32_t nia = d.I.ADDR();
|
|
StepToGuestAddress(thread_id, nia);
|
|
pc = nia;
|
|
} else if (opcode == PPCOpcode::bcx || opcode == PPCOpcode::bcctrx ||
|
|
opcode == PPCOpcode::bclrx) {
|
|
threading::Fence fence;
|
|
auto callback = [&fence, &pc](Breakpoint* breakpoint,
|
|
ThreadDebugInfo* thread_info,
|
|
uint64_t host_address) {
|
|
pc = breakpoint->guest_address();
|
|
fence.Signal();
|
|
};
|
|
|
|
cpu::Breakpoint bpf(this, Breakpoint::AddressType::kGuest, pc + 4,
|
|
callback);
|
|
bpf.Resume();
|
|
|
|
uint32_t nia = 0;
|
|
if (opcode == PPCOpcode::bcx) {
|
|
// bcx
|
|
nia = d.B.ADDR();
|
|
} else if (opcode == PPCOpcode::bcctrx) {
|
|
// bcctrx
|
|
nia = uint32_t(context->ctr);
|
|
} else if (opcode == PPCOpcode::bclrx) {
|
|
// bclrx
|
|
nia = uint32_t(context->lr);
|
|
}
|
|
|
|
cpu::Breakpoint bpt(this, Breakpoint::AddressType::kGuest, nia, callback);
|
|
bpt.Resume();
|
|
|
|
// HACK
|
|
uint32_t suspend_count = 1;
|
|
while (suspend_count) {
|
|
thread->thread()->Resume(&suspend_count);
|
|
}
|
|
|
|
fence.Wait();
|
|
bpt.Suspend();
|
|
bpf.Suspend();
|
|
}
|
|
|
|
return pc;
|
|
}
|
|
|
|
uint32_t Processor::StepToGuestSafePoint(uint32_t thread_id, bool ignore_host) {
|
|
// This cannot be done if we're the calling thread!
|
|
if (thread_id == ThreadState::GetThreadID()) {
|
|
assert_always(
|
|
"Processor::StepToSafePoint(): target thread is the calling thread!");
|
|
return 0;
|
|
}
|
|
auto thread_info = QueryThreadDebugInfo(thread_id);
|
|
auto thread = thread_info->thread;
|
|
|
|
// Now the fun part begins: Registers are only guaranteed to be synchronized
|
|
// with the PPC context at a basic block boundary. Unfortunately, we most
|
|
// likely stopped the thread at some point other than a boundary. We need to
|
|
// step forward until we reach a boundary, and then perform the save.
|
|
uint64_t frame_host_pcs[64];
|
|
cpu::StackFrame cpu_frames[64];
|
|
size_t count = stack_walker_->CaptureStackTrace(
|
|
thread->thread()->native_handle(), frame_host_pcs, 0,
|
|
xe::countof(frame_host_pcs), nullptr, nullptr);
|
|
stack_walker_->ResolveStack(frame_host_pcs, cpu_frames, count);
|
|
if (count == 0) {
|
|
return 0;
|
|
}
|
|
|
|
auto& first_frame = cpu_frames[0];
|
|
if (ignore_host) {
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (cpu_frames[i].type == cpu::StackFrame::Type::kGuest &&
|
|
cpu_frames[i].guest_pc) {
|
|
first_frame = cpu_frames[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we're in guest code or host code.
|
|
uint32_t pc = 0;
|
|
if (first_frame.type == cpu::StackFrame::Type::kGuest) {
|
|
auto& frame = first_frame;
|
|
if (!frame.guest_pc) {
|
|
// Lame. The guest->host thunk is a "guest" function.
|
|
frame = cpu_frames[1];
|
|
}
|
|
|
|
pc = frame.guest_pc;
|
|
|
|
// We're in guest code.
|
|
// First: Find a synchronizing instruction and go to it.
|
|
xe::cpu::ppc::PPCDecodeData d;
|
|
const xe::cpu::ppc::PPCOpcodeInfo* sync_info = nullptr;
|
|
d.address = cpu_frames[0].guest_pc - 4;
|
|
do {
|
|
d.address += 4;
|
|
d.code =
|
|
xe::load_and_swap<uint32_t>(memory()->TranslateVirtual(d.address));
|
|
auto& opcode_info = xe::cpu::ppc::LookupOpcodeInfo(d.code);
|
|
if (opcode_info.type == cpu::ppc::PPCOpcodeType::kSync) {
|
|
sync_info = &opcode_info;
|
|
break;
|
|
}
|
|
} while (true);
|
|
|
|
if (d.address != pc) {
|
|
StepToGuestAddress(thread_id, d.address);
|
|
pc = d.address;
|
|
}
|
|
|
|
// Okay. Now we're on a synchronizing instruction but we need to step
|
|
// past it in order to get a synchronized context.
|
|
// If we're on a branching instruction, it's guaranteed only going to have
|
|
// two possible targets. For non-branching instructions, we can just step
|
|
// over them.
|
|
if (sync_info->group == xe::cpu::ppc::PPCOpcodeGroup::kB) {
|
|
pc = StepIntoGuestBranchTarget(thread_id, d.address);
|
|
}
|
|
} else {
|
|
// We're in host code. Search backwards til we can get an idea of where
|
|
// we are.
|
|
cpu::GuestFunction* thunk_func = nullptr;
|
|
cpu::Export* export_data = nullptr;
|
|
uint32_t first_pc = 0;
|
|
for (int i = 0; i < count; i++) {
|
|
auto& frame = cpu_frames[i];
|
|
if (frame.type == cpu::StackFrame::Type::kGuest && frame.guest_pc) {
|
|
auto func = frame.guest_symbol.function;
|
|
assert_true(func->is_guest());
|
|
|
|
if (!first_pc) {
|
|
first_pc = frame.guest_pc;
|
|
}
|
|
|
|
thunk_func = reinterpret_cast<cpu::GuestFunction*>(func);
|
|
export_data = thunk_func->export_data();
|
|
if (export_data) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the export is blocking, we wrap up and save inside the export thunk.
|
|
// When we're restored, we'll call the blocking export again.
|
|
// Otherwise, we return from the thunk and save.
|
|
if (export_data && export_data->tags & cpu::ExportTag::kBlocking) {
|
|
pc = thunk_func->address();
|
|
} else if (export_data) {
|
|
// Non-blocking. Run until we return from the thunk.
|
|
pc = static_cast<uint32_t>(thread->thread_state()->context()->lr);
|
|
StepToGuestAddress(thread_id, pc);
|
|
} else if (first_pc) {
|
|
// We're in the MMIO handler/mfmsr/something calling out of the guest
|
|
// that doesn't use an export. If the current instruction is
|
|
// synchronizing, we can just save here. Otherwise, step forward
|
|
// (and call ourselves again so we run the correct logic).
|
|
uint32_t code =
|
|
xe::load_and_swap<uint32_t>(memory()->TranslateVirtual(first_pc));
|
|
auto& opcode_info = xe::cpu::ppc::LookupOpcodeInfo(code);
|
|
if (opcode_info.type == xe::cpu::ppc::PPCOpcodeType::kSync) {
|
|
// Good to go.
|
|
pc = first_pc;
|
|
} else {
|
|
// Step forward and run this logic again.
|
|
StepToGuestAddress(thread_id, first_pc + 4);
|
|
return StepToGuestSafePoint(thread_id, true);
|
|
}
|
|
} else {
|
|
// We've managed to catch a thread before it called into the guest.
|
|
// Set a breakpoint on its startup procedure and capture it there.
|
|
// TODO(DrChat): Reimplement
|
|
assert_always("Unimplemented");
|
|
/*
|
|
auto creation_params = thread->creation_params();
|
|
pc = creation_params->xapi_thread_startup
|
|
? creation_params->xapi_thread_startup
|
|
: creation_params->start_address;
|
|
StepToGuestAddress(thread_id, pc);
|
|
*/
|
|
}
|
|
}
|
|
|
|
return pc;
|
|
}
|
|
|
|
bool TestPpcCondition(const xe::cpu::ppc::PPCContext* context, uint32_t bo,
|
|
uint32_t bi, bool check_ctr, bool check_cond) {
|
|
bool ctr_ok = true;
|
|
if (check_ctr) {
|
|
if (select_bits(bo, 2, 2)) {
|
|
ctr_ok = true;
|
|
} else {
|
|
uint32_t new_ctr_value = static_cast<uint32_t>(context->ctr - 1);
|
|
if (select_bits(bo, 1, 1)) {
|
|
ctr_ok = new_ctr_value == 0;
|
|
} else {
|
|
ctr_ok = new_ctr_value != 0;
|
|
}
|
|
}
|
|
}
|
|
bool cond_ok = true;
|
|
if (check_cond) {
|
|
if (select_bits(bo, 4, 4)) {
|
|
cond_ok = true;
|
|
} else {
|
|
uint8_t cr = *(reinterpret_cast<const uint8_t*>(&context->cr0) +
|
|
(4 * (bi >> 2)) + (bi & 3));
|
|
if (select_bits(bo, 3, 3)) {
|
|
cond_ok = cr != 0;
|
|
} else {
|
|
cond_ok = cr == 0;
|
|
}
|
|
}
|
|
}
|
|
return ctr_ok && cond_ok;
|
|
}
|
|
|
|
uint32_t Processor::CalculateNextGuestInstruction(ThreadDebugInfo* thread_info,
|
|
uint32_t current_pc) {
|
|
xe::cpu::ppc::PPCDecodeData d;
|
|
d.address = current_pc;
|
|
d.code = xe::load_and_swap<uint32_t>(memory_->TranslateVirtual(d.address));
|
|
auto opcode = xe::cpu::ppc::LookupOpcode(d.code);
|
|
if (d.code == 0x4E800020) {
|
|
// blr -- unconditional branch to LR.
|
|
uint32_t target_pc = static_cast<uint32_t>(thread_info->guest_context.lr);
|
|
return target_pc;
|
|
} else if (d.code == 0x4E800420) {
|
|
// bctr -- unconditional branch to CTR.
|
|
uint32_t target_pc = static_cast<uint32_t>(thread_info->guest_context.ctr);
|
|
return target_pc;
|
|
} else if (opcode == PPCOpcode::bx) {
|
|
// b/ba/bl/bla
|
|
uint32_t target_pc = d.I.ADDR();
|
|
return target_pc;
|
|
} else if (opcode == PPCOpcode::bcx) {
|
|
// bc/bca/bcl/bcla
|
|
uint32_t target_pc = d.B.ADDR();
|
|
bool test_passed = TestPpcCondition(&thread_info->guest_context, d.B.BO(),
|
|
d.B.BI(), true, true);
|
|
return test_passed ? target_pc : current_pc + 4;
|
|
} else if (opcode == PPCOpcode::bclrx) {
|
|
// bclr/bclrl
|
|
uint32_t target_pc = static_cast<uint32_t>(thread_info->guest_context.lr);
|
|
bool test_passed = TestPpcCondition(&thread_info->guest_context, d.XL.BO(),
|
|
d.XL.BI(), true, true);
|
|
return test_passed ? target_pc : current_pc + 4;
|
|
} else if (opcode == PPCOpcode::bcctrx) {
|
|
// bcctr/bcctrl
|
|
uint32_t target_pc = static_cast<uint32_t>(thread_info->guest_context.ctr);
|
|
bool test_passed = TestPpcCondition(&thread_info->guest_context, d.XL.BO(),
|
|
d.XL.BI(), false, true);
|
|
return test_passed ? target_pc : current_pc + 4;
|
|
} else {
|
|
return current_pc + 4;
|
|
}
|
|
}
|
|
|
|
} // namespace cpu
|
|
} // namespace xe
|