Files
Xenia-Canary/src/xenia/cpu/processor.cc
chss95cs@gmail.com 90c771526d "Fix" debug console, we were checking the cvar before any cvars were loaded, and the condition it checks in AttachConsole is somehow always false
Remove dead #if 0'd code in math.h

On amd64, page_size == 4096 constant, on amd64 w/ win32, allocation_granularity == 65536. These values for x86 windows havent changed over the last 20 years so this is probably safe
and gives a modest code size reduction

Enable XE_USE_KUSER_SHARED. This sources host time from KUSER_SHARED instead of from QueryPerformanceCounter, which is far faster, but only has a granularity of 100 nanoseconds.

In some games seemingly random crashes were happening that were hard to trace because
the faulting thread was actually not the one that was misbehaving, another threads stack was underflowing into the faulting thread.

Added a bunch of code to synchronize the guest stack and host stack so that if a guest longjmps the host's stack will be adjusted.
Changes were also made to allow the guest to call into a piece of an existing x64 function.

This synchronization might have a slight performance impact on lower end cpus, to disable it set enable_host_guest_stack_synchronization to false.
It is possible it may have introduced regressions, but i dont know of any yet

So far, i know the synchronization change fixes the "hub crash" in super sonic and allows the game "london 2012" to go ingame.

Removed emit_useless_fpscr_updates, not emitting these updates breaks the raiden game

MapGuestAddressToMachineCode now returns nullptr if no address was found, instead of the start of the function

add Processor::LookupModule

Add Backend::DeinitializeBackendContext
Use WriteRegisterRangeFromRing_WithKnownBound<0, 0xFFFF> in WriteRegisterRangeFromRing for inlining (previously regressed on performance of ExecutePacketType0)

add notes about flags that trap in XamInputGetCapabilities

0 == 3 in XamInputGetCapabilities

Name arg 2 of XamInputSetState

PrefetchW in critical section kernel funcs if available & doing cmpxchg

Add terminated field to X_KTHREAD, set it on termination

Expanded the logic of NtResumeThread/NtSuspendThread to include checking the type of the handle (in release, LookupObject doesnt seem to do anything with the type)
and returning X_STATUS_OBJECT_TYPE_MISMATCH if invalid. Do termination check in NtSuspendThread.

Add basic host exception messagebox, need to flesh it out more (maybe use the new stack tracking stuff if on guest thrd?)

Add rdrand patching hack, mostly affects users with nvidia cards who have many threads on zen

Use page_size_shift in more places

Once again disable precompilation! Raiden is mostly weird ppc asm which probably breaks the precompilation. The code is still useful for running the compiler over the whole of an xex in debug to test for issues
"Fix" debug console, we were checking the cvar before any cvars were loaded, and the condition it checks in AttachConsole is somehow always false

Remove dead #if 0'd code in math.h

On amd64, page_size == 4096 constant, on amd64 w/ win32, allocation_granularity == 65536. These values for x86 windows havent changed over the last 20 years so this is probably safe
and gives a modest code size reduction

Enable XE_USE_KUSER_SHARED. This sources host time from KUSER_SHARED instead of from QueryPerformanceCounter, which is far faster, but only has a granularity of 100 nanoseconds.

In some games seemingly random crashes were happening that were hard to trace because
the faulting thread was actually not the one that was misbehaving, another threads stack was underflowing into the faulting thread.

Added a bunch of code to synchronize the guest stack and host stack so that if a guest longjmps the host's stack will be adjusted.
Changes were also made to allow the guest to call into a piece of an existing x64 function.

This synchronization might have a slight performance impact on lower end cpus, to disable it set enable_host_guest_stack_synchronization to false.
It is possible it may have introduced regressions, but i dont know of any yet

So far, i know the synchronization change fixes the "hub crash" in super sonic and allows the game "london 2012" to go ingame.

Removed emit_useless_fpscr_updates, not emitting these updates breaks the raiden game

MapGuestAddressToMachineCode now returns nullptr if no address was found, instead of the start of the function

add Processor::LookupModule

Add Backend::DeinitializeBackendContext
Use WriteRegisterRangeFromRing_WithKnownBound<0, 0xFFFF> in WriteRegisterRangeFromRing for inlining (previously regressed on performance of ExecutePacketType0)

add notes about flags that trap in XamInputGetCapabilities

0 == 3 in XamInputGetCapabilities

Name arg 2 of XamInputSetState

PrefetchW in critical section kernel funcs if available & doing cmpxchg

Add terminated field to X_KTHREAD, set it on termination

Expanded the logic of NtResumeThread/NtSuspendThread to include checking the type of the handle (in release, LookupObject doesnt seem to do anything with the type)
and returning X_STATUS_OBJECT_TYPE_MISMATCH if invalid. Do termination check in NtSuspendThread.

Add basic host exception messagebox, need to flesh it out more (maybe use the new stack tracking stuff if on guest thrd?)

Add rdrand patching hack, mostly affects users with nvidia cards who have many threads on zen

Use page_size_shift in more places

Once again disable precompilation! Raiden is mostly weird ppc asm which probably breaks the precompilation. The code is still useful for running the compiler over the whole of an xex in debug to test for issues
2022-11-27 09:39:33 -08:00

1341 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;
}
//only add it to the list of resolved functions if resolving succeeded
auto module_for = function->module();
auto xexmod = dynamic_cast<XexModule*>(module_for);
if (xexmod) {
auto addr_flags = xexmod->GetInstructionAddressFlags(address);
if (addr_flags) {
addr_flags->was_resolved = 1;
}
}
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;
}
}
Module* Processor::LookupModule(uint32_t address) {
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)) {
return module.get();
}
}
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 = LookupModule(address);
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