Reconcile debugger and save state stuff into a single implementation.
Fixes #497 and fixes #496. Still rough edges, but at least less duplication.
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
class DebugInfo;
|
||||
class FunctionDebugInfo;
|
||||
class GuestFunction;
|
||||
namespace hir {
|
||||
class HIRBuilder;
|
||||
@@ -39,7 +39,7 @@ class Assembler {
|
||||
|
||||
virtual bool Assemble(GuestFunction* function, hir::HIRBuilder* builder,
|
||||
uint32_t debug_info_flags,
|
||||
std::unique_ptr<DebugInfo> debug_info) = 0;
|
||||
std::unique_ptr<FunctionDebugInfo> debug_info) = 0;
|
||||
|
||||
protected:
|
||||
Backend* backend_;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "xenia/cpu/backend/machine_info.h"
|
||||
#include "xenia/cpu/thread_debug_info.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
@@ -53,11 +54,15 @@ class Backend {
|
||||
virtual std::unique_ptr<GuestFunction> CreateGuestFunction(
|
||||
Module* module, uint32_t address) = 0;
|
||||
|
||||
virtual bool InstallBreakpoint(Breakpoint* bp) { return false; }
|
||||
virtual bool InstallBreakpoint(Breakpoint* bp, Function* func) {
|
||||
return false;
|
||||
}
|
||||
virtual bool UninstallBreakpoint(Breakpoint* bp) { return false; }
|
||||
// Calculates the next host instruction based on the current thread state and
|
||||
// current PC. This will look for branches and other control flow
|
||||
// instructions.
|
||||
virtual uint64_t CalculateNextHostInstruction(ThreadDebugInfo* thread_info,
|
||||
uint64_t current_pc) = 0;
|
||||
|
||||
virtual void InstallBreakpoint(Breakpoint* breakpoint) {}
|
||||
virtual void InstallBreakpoint(Breakpoint* breakpoint, Function* fn) {}
|
||||
virtual void UninstallBreakpoint(Breakpoint* breakpoint) {}
|
||||
|
||||
protected:
|
||||
Processor* processor_;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "xenia/base/profiling.h"
|
||||
#include "xenia/base/reset_scope.h"
|
||||
#include "xenia/cpu/backend/x64/x64_backend.h"
|
||||
#include "xenia/cpu/backend/x64/x64_code_cache.h"
|
||||
#include "xenia/cpu/backend/x64/x64_emitter.h"
|
||||
#include "xenia/cpu/backend/x64/x64_function.h"
|
||||
#include "xenia/cpu/cpu_flags.h"
|
||||
@@ -67,7 +68,7 @@ void X64Assembler::Reset() {
|
||||
|
||||
bool X64Assembler::Assemble(GuestFunction* function, HIRBuilder* builder,
|
||||
uint32_t debug_info_flags,
|
||||
std::unique_ptr<DebugInfo> debug_info) {
|
||||
std::unique_ptr<FunctionDebugInfo> debug_info) {
|
||||
SCOPE_profile_cpu_f("cpu");
|
||||
|
||||
// Reset when we leave.
|
||||
@@ -89,18 +90,17 @@ bool X64Assembler::Assemble(GuestFunction* function, HIRBuilder* builder,
|
||||
string_buffer_.Reset();
|
||||
}
|
||||
|
||||
// Dump debug data.
|
||||
if (FLAGS_disassemble_functions) {
|
||||
if (debug_info_flags & DebugInfoFlags::kDebugInfoDisasmSource) {
|
||||
// auto fn_data = backend_->processor()->debugger()->AllocateFunctionData(
|
||||
// xe::debug::FunctionDisasmData::SizeOfHeader());
|
||||
}
|
||||
}
|
||||
|
||||
function->set_debug_info(std::move(debug_info));
|
||||
static_cast<X64Function*>(function)->Setup(
|
||||
reinterpret_cast<uint8_t*>(machine_code), code_size);
|
||||
|
||||
// Install into indirection table.
|
||||
uint64_t host_address = reinterpret_cast<uint64_t>(machine_code);
|
||||
assert_true((host_address >> 32) == 0);
|
||||
reinterpret_cast<X64CodeCache*>(backend_->code_cache())
|
||||
->AddIndirection(function->address(),
|
||||
static_cast<uint32_t>(host_address));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class X64Assembler : public Assembler {
|
||||
|
||||
bool Assemble(GuestFunction* function, hir::HIRBuilder* builder,
|
||||
uint32_t debug_info_flags,
|
||||
std::unique_ptr<DebugInfo> debug_info) override;
|
||||
std::unique_ptr<FunctionDebugInfo> debug_info) override;
|
||||
|
||||
private:
|
||||
void DumpMachineCode(void* machine_code, size_t code_size,
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#include "xenia/cpu/backend/x64/x64_backend.h"
|
||||
|
||||
#include "third_party/capstone/include/capstone.h"
|
||||
#include "third_party/capstone/include/x86.h"
|
||||
#include "xenia/base/exception_handler.h"
|
||||
#include "xenia/cpu/backend/x64/x64_assembler.h"
|
||||
#include "xenia/cpu/backend/x64/x64_code_cache.h"
|
||||
@@ -39,13 +41,23 @@ class X64ThunkEmitter : public X64Emitter {
|
||||
};
|
||||
|
||||
X64Backend::X64Backend(Processor* processor)
|
||||
: Backend(processor), code_cache_(nullptr), emitter_data_(0) {}
|
||||
: Backend(processor), code_cache_(nullptr), emitter_data_(0) {
|
||||
if (cs_open(CS_ARCH_X86, CS_MODE_64, &capstone_handle_) != CS_ERR_OK) {
|
||||
assert_always("Failed to initialize capstone");
|
||||
}
|
||||
cs_option(capstone_handle_, CS_OPT_SYNTAX, CS_OPT_SYNTAX_INTEL);
|
||||
cs_option(capstone_handle_, CS_OPT_DETAIL, CS_OPT_ON);
|
||||
cs_option(capstone_handle_, CS_OPT_SKIPDATA, CS_OPT_OFF);
|
||||
}
|
||||
|
||||
X64Backend::~X64Backend() {
|
||||
if (emitter_data_) {
|
||||
processor()->memory()->SystemHeapFree(emitter_data_);
|
||||
emitter_data_ = 0;
|
||||
}
|
||||
if (capstone_handle_) {
|
||||
cs_close(&capstone_handle_);
|
||||
}
|
||||
|
||||
ExceptionHandler::Uninstall(&ExceptionCallbackThunk, this);
|
||||
}
|
||||
@@ -124,59 +136,227 @@ std::unique_ptr<GuestFunction> X64Backend::CreateGuestFunction(
|
||||
return std::make_unique<X64Function>(module, address);
|
||||
}
|
||||
|
||||
bool X64Backend::InstallBreakpoint(Breakpoint* bp) {
|
||||
auto functions = processor()->FindFunctionsWithAddress(bp->address());
|
||||
if (functions.empty()) {
|
||||
// Go ahead and fail - let the caller handle this.
|
||||
return false;
|
||||
uint64_t ReadCapstoneReg(X64Context* context, x86_reg reg) {
|
||||
switch (reg) {
|
||||
case X86_REG_RAX:
|
||||
return context->rax;
|
||||
case X86_REG_RCX:
|
||||
return context->rcx;
|
||||
case X86_REG_RDX:
|
||||
return context->rdx;
|
||||
case X86_REG_RBX:
|
||||
return context->rbx;
|
||||
case X86_REG_RSP:
|
||||
return context->rsp;
|
||||
case X86_REG_RBP:
|
||||
return context->rbp;
|
||||
case X86_REG_RSI:
|
||||
return context->rsi;
|
||||
case X86_REG_RDI:
|
||||
return context->rdi;
|
||||
case X86_REG_R8:
|
||||
return context->r8;
|
||||
case X86_REG_R9:
|
||||
return context->r9;
|
||||
case X86_REG_R10:
|
||||
return context->r10;
|
||||
case X86_REG_R11:
|
||||
return context->r11;
|
||||
case X86_REG_R12:
|
||||
return context->r12;
|
||||
case X86_REG_R13:
|
||||
return context->r13;
|
||||
case X86_REG_R14:
|
||||
return context->r14;
|
||||
case X86_REG_R15:
|
||||
return context->r15;
|
||||
default:
|
||||
assert_unhandled_case(reg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (auto function : functions) {
|
||||
assert_true(function->is_guest());
|
||||
auto guest_function = reinterpret_cast<cpu::GuestFunction*>(function);
|
||||
auto code = guest_function->MapGuestAddressToMachineCode(bp->address());
|
||||
if (!code) {
|
||||
// This should not happen.
|
||||
assert_always();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto orig_bytes =
|
||||
xe::load_and_swap<uint16_t>(reinterpret_cast<void*>(code + 0x0));
|
||||
bp->backend_data().push_back({code, orig_bytes});
|
||||
|
||||
xe::store_and_swap<uint16_t>(reinterpret_cast<void*>(code + 0x0), 0x0F0C);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool X64Backend::InstallBreakpoint(Breakpoint* bp, Function* func) {
|
||||
assert_true(func->is_guest());
|
||||
auto guest_function = reinterpret_cast<cpu::GuestFunction*>(func);
|
||||
auto code = guest_function->MapGuestAddressToMachineCode(bp->address());
|
||||
if (!code) {
|
||||
#define X86_EFLAGS_CF 0x00000001 // Carry Flag
|
||||
#define X86_EFLAGS_PF 0x00000004 // Parity Flag
|
||||
#define X86_EFLAGS_ZF 0x00000040 // Zero Flag
|
||||
#define X86_EFLAGS_SF 0x00000080 // Sign Flag
|
||||
#define X86_EFLAGS_OF 0x00000800 // Overflow Flag
|
||||
bool TestCapstoneEflags(uint32_t eflags, uint32_t insn) {
|
||||
// http://www.felixcloutier.com/x86/Jcc.html
|
||||
switch (insn) {
|
||||
case X86_INS_JAE:
|
||||
// CF=0 && ZF=0
|
||||
return ((eflags & X86_EFLAGS_CF) == 0) && ((eflags & X86_EFLAGS_ZF) == 0);
|
||||
case X86_INS_JA:
|
||||
// CF=0
|
||||
return (eflags & X86_EFLAGS_CF) == 0;
|
||||
case X86_INS_JBE:
|
||||
// CF=1 || ZF=1
|
||||
return ((eflags & X86_EFLAGS_CF) == X86_EFLAGS_CF) ||
|
||||
((eflags & X86_EFLAGS_ZF) == X86_EFLAGS_ZF);
|
||||
case X86_INS_JB:
|
||||
// CF=1
|
||||
return (eflags & X86_EFLAGS_CF) == X86_EFLAGS_CF;
|
||||
case X86_INS_JE:
|
||||
// ZF=1
|
||||
return (eflags & X86_EFLAGS_ZF) == X86_EFLAGS_ZF;
|
||||
case X86_INS_JGE:
|
||||
// SF=OF
|
||||
return (eflags & X86_EFLAGS_SF) == (eflags & X86_EFLAGS_OF);
|
||||
case X86_INS_JG:
|
||||
// ZF=0 && SF=OF
|
||||
return ((eflags & X86_EFLAGS_ZF) == 0) &&
|
||||
((eflags & X86_EFLAGS_SF) == (eflags & X86_EFLAGS_OF));
|
||||
case X86_INS_JLE:
|
||||
// ZF=1 || SF!=OF
|
||||
return ((eflags & X86_EFLAGS_ZF) == X86_EFLAGS_ZF) ||
|
||||
((eflags & X86_EFLAGS_SF) != X86_EFLAGS_OF);
|
||||
case X86_INS_JL:
|
||||
// SF!=OF
|
||||
return (eflags & X86_EFLAGS_SF) != (eflags & X86_EFLAGS_OF);
|
||||
case X86_INS_JNE:
|
||||
// ZF=0
|
||||
return (eflags & X86_EFLAGS_ZF) == 0;
|
||||
case X86_INS_JNO:
|
||||
// OF=0
|
||||
return (eflags & X86_EFLAGS_OF) == 0;
|
||||
case X86_INS_JNP:
|
||||
// PF=0
|
||||
return (eflags & X86_EFLAGS_PF) == 0;
|
||||
case X86_INS_JNS:
|
||||
// SF=0
|
||||
return (eflags & X86_EFLAGS_SF) == 0;
|
||||
case X86_INS_JO:
|
||||
// OF=1
|
||||
return (eflags & X86_EFLAGS_OF) == X86_EFLAGS_OF;
|
||||
case X86_INS_JP:
|
||||
// PF=1
|
||||
return (eflags & X86_EFLAGS_PF) == X86_EFLAGS_PF;
|
||||
case X86_INS_JS:
|
||||
// SF=1
|
||||
return (eflags & X86_EFLAGS_SF) == X86_EFLAGS_SF;
|
||||
default:
|
||||
assert_unhandled_case(insn);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t X64Backend::CalculateNextHostInstruction(ThreadDebugInfo* thread_info,
|
||||
uint64_t current_pc) {
|
||||
auto machine_code_ptr = reinterpret_cast<const uint8_t*>(current_pc);
|
||||
size_t remaining_machine_code_size = 64;
|
||||
uint64_t host_address = current_pc;
|
||||
cs_insn insn = {0};
|
||||
cs_detail all_detail = {0};
|
||||
insn.detail = &all_detail;
|
||||
cs_disasm_iter(capstone_handle_, &machine_code_ptr,
|
||||
&remaining_machine_code_size, &host_address, &insn);
|
||||
auto& detail = all_detail.x86;
|
||||
switch (insn.id) {
|
||||
default:
|
||||
// Not a branching instruction - just move over it.
|
||||
return current_pc + insn.size;
|
||||
case X86_INS_CALL: {
|
||||
assert_true(detail.op_count == 1);
|
||||
assert_true(detail.operands[0].type == X86_OP_REG);
|
||||
uint64_t target_pc =
|
||||
ReadCapstoneReg(&thread_info->host_context, detail.operands[0].reg);
|
||||
return target_pc;
|
||||
} break;
|
||||
case X86_INS_RET: {
|
||||
assert_zero(detail.op_count);
|
||||
auto stack_ptr =
|
||||
reinterpret_cast<uint64_t*>(thread_info->host_context.rsp);
|
||||
uint64_t target_pc = stack_ptr[0];
|
||||
return target_pc;
|
||||
} break;
|
||||
case X86_INS_JMP: {
|
||||
assert_true(detail.op_count == 1);
|
||||
if (detail.operands[0].type == X86_OP_IMM) {
|
||||
uint64_t target_pc = static_cast<uint64_t>(detail.operands[0].imm);
|
||||
return target_pc;
|
||||
} else if (detail.operands[0].type == X86_OP_REG) {
|
||||
uint64_t target_pc =
|
||||
ReadCapstoneReg(&thread_info->host_context, detail.operands[0].reg);
|
||||
return target_pc;
|
||||
} else {
|
||||
// TODO(benvanik): find some more uses of this.
|
||||
assert_always("jmp branch emulation not yet implemented");
|
||||
return current_pc + insn.size;
|
||||
}
|
||||
} break;
|
||||
case X86_INS_JCXZ:
|
||||
case X86_INS_JECXZ:
|
||||
case X86_INS_JRCXZ:
|
||||
assert_always("j*cxz branch emulation not yet implemented");
|
||||
return current_pc + insn.size;
|
||||
case X86_INS_JAE:
|
||||
case X86_INS_JA:
|
||||
case X86_INS_JBE:
|
||||
case X86_INS_JB:
|
||||
case X86_INS_JE:
|
||||
case X86_INS_JGE:
|
||||
case X86_INS_JG:
|
||||
case X86_INS_JLE:
|
||||
case X86_INS_JL:
|
||||
case X86_INS_JNE:
|
||||
case X86_INS_JNO:
|
||||
case X86_INS_JNP:
|
||||
case X86_INS_JNS:
|
||||
case X86_INS_JO:
|
||||
case X86_INS_JP:
|
||||
case X86_INS_JS: {
|
||||
assert_true(detail.op_count == 1);
|
||||
assert_true(detail.operands[0].type == X86_OP_IMM);
|
||||
uint64_t target_pc = static_cast<uint64_t>(detail.operands[0].imm);
|
||||
bool test_passed =
|
||||
TestCapstoneEflags(thread_info->host_context.eflags, insn.id);
|
||||
if (test_passed) {
|
||||
return target_pc;
|
||||
} else {
|
||||
return current_pc + insn.size;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void X64Backend::InstallBreakpoint(Breakpoint* breakpoint) {
|
||||
breakpoint->ForEachHostAddress([breakpoint](uint64_t host_address) {
|
||||
auto ptr = reinterpret_cast<void*>(host_address);
|
||||
auto original_bytes = xe::load_and_swap<uint16_t>(ptr);
|
||||
assert_true(original_bytes != 0x0F0B);
|
||||
xe::store_and_swap<uint16_t>(ptr, 0x0F0B);
|
||||
breakpoint->backend_data().emplace_back(host_address, original_bytes);
|
||||
});
|
||||
}
|
||||
|
||||
void X64Backend::InstallBreakpoint(Breakpoint* breakpoint, Function* fn) {
|
||||
assert_true(breakpoint->address_type() == Breakpoint::AddressType::kGuest);
|
||||
assert_true(fn->is_guest());
|
||||
auto guest_function = reinterpret_cast<cpu::GuestFunction*>(fn);
|
||||
auto host_address =
|
||||
guest_function->MapGuestAddressToMachineCode(breakpoint->guest_address());
|
||||
if (!host_address) {
|
||||
assert_always();
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Assume we haven't already installed a breakpoint in this spot.
|
||||
auto orig_bytes =
|
||||
xe::load_and_swap<uint16_t>(reinterpret_cast<void*>(code + 0x0));
|
||||
bp->backend_data().push_back({code, orig_bytes});
|
||||
|
||||
xe::store_and_swap<uint16_t>(reinterpret_cast<void*>(code + 0x0), 0x0F0C);
|
||||
return true;
|
||||
auto ptr = reinterpret_cast<void*>(host_address);
|
||||
auto original_bytes = xe::load_and_swap<uint16_t>(ptr);
|
||||
assert_true(original_bytes != 0x0F0B);
|
||||
xe::store_and_swap<uint16_t>(ptr, 0x0F0B);
|
||||
breakpoint->backend_data().emplace_back(host_address, original_bytes);
|
||||
}
|
||||
|
||||
bool X64Backend::UninstallBreakpoint(Breakpoint* bp) {
|
||||
for (auto& pair : bp->backend_data()) {
|
||||
xe::store_and_swap<uint16_t>(reinterpret_cast<void*>(pair.first),
|
||||
uint16_t(pair.second));
|
||||
void X64Backend::UninstallBreakpoint(Breakpoint* breakpoint) {
|
||||
for (auto& pair : breakpoint->backend_data()) {
|
||||
auto ptr = reinterpret_cast<uint8_t*>(pair.first);
|
||||
auto instruction_bytes = xe::load_and_swap<uint16_t>(ptr);
|
||||
assert_true(instruction_bytes == 0x0F0B);
|
||||
xe::store_and_swap<uint16_t>(ptr, static_cast<uint16_t>(pair.second));
|
||||
}
|
||||
|
||||
bp->backend_data().clear();
|
||||
return true;
|
||||
breakpoint->backend_data().clear();
|
||||
}
|
||||
|
||||
bool X64Backend::ExceptionCallbackThunk(Exception* ex, void* data) {
|
||||
@@ -186,39 +366,22 @@ bool X64Backend::ExceptionCallbackThunk(Exception* ex, void* data) {
|
||||
|
||||
bool X64Backend::ExceptionCallback(Exception* ex) {
|
||||
if (ex->code() != Exception::Code::kIllegalInstruction) {
|
||||
// Has nothing to do with breakpoints. Not ours.
|
||||
// We only care about illegal instructions. Other things will be handled by
|
||||
// other handlers (probably). If nothing else picks it up we'll be called
|
||||
// with OnUnhandledException to do real crash handling.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify an expected illegal instruction.
|
||||
auto instruction_bytes =
|
||||
xe::load_and_swap<uint16_t>(reinterpret_cast<void*>(ex->pc()));
|
||||
if (instruction_bytes != 0x0F0C) {
|
||||
// Not a BP instruction - not ours.
|
||||
if (instruction_bytes != 0x0F0B) {
|
||||
// Not our ud2 - not us.
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t host_pcs[64];
|
||||
cpu::StackFrame frames[64];
|
||||
size_t count = processor()->stack_walker()->CaptureStackTrace(
|
||||
host_pcs, 0, xe::countof(host_pcs));
|
||||
processor()->stack_walker()->ResolveStack(host_pcs, frames, count);
|
||||
if (count == 0) {
|
||||
// Stack resolve failed.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (frames[i].type != cpu::StackFrame::Type::kGuest) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processor()->BreakpointHit(frames[i].guest_pc, frames[i].host_pc)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No breakpoints found at this address.
|
||||
return false;
|
||||
// Let the processor handle things.
|
||||
return processor()->OnThreadBreakpointHit(ex);
|
||||
}
|
||||
|
||||
X64ThunkEmitter::X64ThunkEmitter(X64Backend* backend, XbyakAllocator* allocator)
|
||||
|
||||
@@ -62,14 +62,19 @@ class X64Backend : public Backend {
|
||||
std::unique_ptr<GuestFunction> CreateGuestFunction(Module* module,
|
||||
uint32_t address) override;
|
||||
|
||||
bool InstallBreakpoint(Breakpoint* bp) override;
|
||||
bool InstallBreakpoint(Breakpoint* bp, Function* func) override;
|
||||
bool UninstallBreakpoint(Breakpoint* bp) override;
|
||||
uint64_t CalculateNextHostInstruction(ThreadDebugInfo* thread_info,
|
||||
uint64_t current_pc) override;
|
||||
|
||||
void InstallBreakpoint(Breakpoint* breakpoint) override;
|
||||
void InstallBreakpoint(Breakpoint* breakpoint, Function* fn) override;
|
||||
void UninstallBreakpoint(Breakpoint* breakpoint) override;
|
||||
|
||||
private:
|
||||
static bool ExceptionCallbackThunk(Exception* ex, void* data);
|
||||
bool ExceptionCallback(Exception* ex);
|
||||
|
||||
uintptr_t capstone_handle_ = 0;
|
||||
|
||||
std::unique_ptr<X64CodeCache> code_cache_;
|
||||
|
||||
uint32_t emitter_data_;
|
||||
|
||||
@@ -27,12 +27,11 @@
|
||||
#include "xenia/cpu/backend/x64/x64_sequences.h"
|
||||
#include "xenia/cpu/backend/x64/x64_stack_layout.h"
|
||||
#include "xenia/cpu/cpu_flags.h"
|
||||
#include "xenia/cpu/debug_info.h"
|
||||
#include "xenia/cpu/function.h"
|
||||
#include "xenia/cpu/function_debug_info.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/cpu/symbol.h"
|
||||
#include "xenia/cpu/thread_state.h"
|
||||
#include "xenia/debug/debugger.h"
|
||||
|
||||
DEFINE_bool(enable_debugprint_log, false,
|
||||
"Log debugprint traps to the active debugger");
|
||||
@@ -89,7 +88,7 @@ X64Emitter::X64Emitter(X64Backend* backend, XbyakAllocator* allocator)
|
||||
X64Emitter::~X64Emitter() = default;
|
||||
|
||||
bool X64Emitter::Emit(GuestFunction* function, HIRBuilder* builder,
|
||||
uint32_t debug_info_flags, DebugInfo* debug_info,
|
||||
uint32_t debug_info_flags, FunctionDebugInfo* debug_info,
|
||||
void** out_code_address, size_t* out_code_size,
|
||||
std::vector<SourceMapEntry>* out_source_map) {
|
||||
SCOPE_profile_cpu_f("cpu");
|
||||
@@ -187,7 +186,7 @@ bool X64Emitter::Emit(HIRBuilder* builder, size_t* out_stack_size) {
|
||||
inc(qword[low_address(&trace_header->function_call_count)]);
|
||||
|
||||
// Get call history slot.
|
||||
static_assert(debug::FunctionTraceData::kFunctionCallerHistoryCount == 4,
|
||||
static_assert(FunctionTraceData::kFunctionCallerHistoryCount == 4,
|
||||
"bitmask depends on count");
|
||||
mov(rax, qword[low_address(&trace_header->function_call_count)]);
|
||||
and_(rax, 0b00000011);
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
|
||||
#include "xenia/base/arena.h"
|
||||
#include "xenia/cpu/function.h"
|
||||
#include "xenia/cpu/function_trace_data.h"
|
||||
#include "xenia/cpu/hir/hir_builder.h"
|
||||
#include "xenia/cpu/hir/instr.h"
|
||||
#include "xenia/cpu/hir/value.h"
|
||||
#include "xenia/debug/function_trace_data.h"
|
||||
#include "xenia/memory.h"
|
||||
|
||||
// NOTE: must be included last as it expects windows.h to already be included.
|
||||
@@ -115,7 +115,7 @@ class X64Emitter : public Xbyak::CodeGenerator {
|
||||
X64Backend* backend() const { return backend_; }
|
||||
|
||||
bool Emit(GuestFunction* function, hir::HIRBuilder* builder,
|
||||
uint32_t debug_info_flags, DebugInfo* debug_info,
|
||||
uint32_t debug_info_flags, FunctionDebugInfo* debug_info,
|
||||
void** out_code_address, size_t* out_code_size,
|
||||
std::vector<SourceMapEntry>* out_source_map);
|
||||
|
||||
@@ -190,7 +190,7 @@ class X64Emitter : public Xbyak::CodeGenerator {
|
||||
return (feature_flags_ & feature_flag) != 0;
|
||||
}
|
||||
|
||||
DebugInfo* debug_info() const { return debug_info_; }
|
||||
FunctionDebugInfo* debug_info() const { return debug_info_; }
|
||||
|
||||
size_t stack_size() const { return stack_size_; }
|
||||
|
||||
@@ -212,9 +212,9 @@ class X64Emitter : public Xbyak::CodeGenerator {
|
||||
|
||||
hir::Instr* current_instr_ = nullptr;
|
||||
|
||||
DebugInfo* debug_info_ = nullptr;
|
||||
FunctionDebugInfo* debug_info_ = nullptr;
|
||||
uint32_t debug_info_flags_ = 0;
|
||||
debug::FunctionTraceData* trace_data_ = nullptr;
|
||||
FunctionTraceData* trace_data_ = nullptr;
|
||||
Arena source_map_arena_;
|
||||
|
||||
size_t stack_size_ = 0;
|
||||
|
||||
@@ -9,31 +9,108 @@
|
||||
|
||||
#include "xenia/cpu/breakpoint.h"
|
||||
|
||||
#include "xenia/base/string_util.h"
|
||||
#include "xenia/cpu/backend/backend.h"
|
||||
#include "xenia/cpu/backend/code_cache.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
|
||||
Breakpoint::Breakpoint(Processor* processor,
|
||||
std::function<void(uint32_t, uint64_t)> hit_callback)
|
||||
: processor_(processor), hit_callback_(hit_callback) {}
|
||||
Breakpoint::Breakpoint(Processor* processor, uint32_t address,
|
||||
std::function<void(uint32_t, uint64_t)> hit_callback)
|
||||
: processor_(processor), address_(address), hit_callback_(hit_callback) {}
|
||||
Breakpoint::Breakpoint(Processor* processor, AddressType address_type,
|
||||
uint64_t address, HitCallback hit_callback)
|
||||
: processor_(processor),
|
||||
address_type_(address_type),
|
||||
address_(address),
|
||||
hit_callback_(hit_callback) {}
|
||||
|
||||
Breakpoint::~Breakpoint() { assert_false(installed_); }
|
||||
|
||||
bool Breakpoint::Install() {
|
||||
void Breakpoint::Install() {
|
||||
assert_false(installed_);
|
||||
|
||||
installed_ = processor_->InstallBreakpoint(this);
|
||||
return installed_;
|
||||
processor_->backend()->InstallBreakpoint(this);
|
||||
installed_ = true;
|
||||
}
|
||||
|
||||
bool Breakpoint::Uninstall() {
|
||||
void Breakpoint::Uninstall() {
|
||||
assert_true(installed_);
|
||||
processor_->backend()->UninstallBreakpoint(this);
|
||||
installed_ = false;
|
||||
}
|
||||
|
||||
installed_ = !processor_->UninstallBreakpoint(this);
|
||||
return !installed_;
|
||||
std::string Breakpoint::to_string() const {
|
||||
if (address_type_ == AddressType::kGuest) {
|
||||
auto str =
|
||||
std::string("PPC ") + xe::string_util::to_hex_string(guest_address());
|
||||
auto functions = processor_->FindFunctionsWithAddress(guest_address());
|
||||
if (functions.empty()) {
|
||||
return str;
|
||||
}
|
||||
str += " " + functions[0]->name();
|
||||
return str;
|
||||
} else {
|
||||
return std::string("x64 ") + xe::string_util::to_hex_string(host_address());
|
||||
}
|
||||
}
|
||||
|
||||
GuestFunction* Breakpoint::guest_function() const {
|
||||
if (address_type_ == AddressType::kGuest) {
|
||||
auto functions = processor_->FindFunctionsWithAddress(guest_address());
|
||||
if (functions.empty()) {
|
||||
return nullptr;
|
||||
} else if (functions[0]->is_guest()) {
|
||||
return static_cast<xe::cpu::GuestFunction*>(functions[0]);
|
||||
}
|
||||
return nullptr;
|
||||
} else {
|
||||
return processor_->backend()->code_cache()->LookupFunction(host_address());
|
||||
}
|
||||
}
|
||||
|
||||
bool Breakpoint::ContainsHostAddress(uintptr_t search_address) const {
|
||||
bool contains = false;
|
||||
ForEachHostAddress([&contains, search_address](uintptr_t host_address) {
|
||||
if (host_address == search_address) {
|
||||
contains = true;
|
||||
}
|
||||
});
|
||||
return contains;
|
||||
}
|
||||
|
||||
void Breakpoint::ForEachHostAddress(
|
||||
std::function<void(uintptr_t)> callback) const {
|
||||
if (address_type_ == AddressType::kGuest) {
|
||||
auto guest_address = this->guest_address();
|
||||
|
||||
// Lookup all functions that contain this guest address and patch them.
|
||||
auto functions = processor_->FindFunctionsWithAddress(guest_address);
|
||||
|
||||
if (functions.empty()) {
|
||||
// If function does not exist demand it, as we need someplace to put our
|
||||
// breakpoint. Note that this follows the same resolution rules as the
|
||||
// JIT, so what's returned is the function the JIT would have jumped to.
|
||||
auto fn = processor_->ResolveFunction(guest_address);
|
||||
if (!fn) {
|
||||
// TODO(benvanik): error out better with 'invalid breakpoint'?
|
||||
assert_not_null(fn);
|
||||
return;
|
||||
}
|
||||
functions.push_back(fn);
|
||||
}
|
||||
assert_false(functions.empty());
|
||||
|
||||
for (auto function : functions) {
|
||||
// TODO(benvanik): other function types.
|
||||
assert_true(function->is_guest());
|
||||
auto guest_function = reinterpret_cast<GuestFunction*>(function);
|
||||
uintptr_t host_address =
|
||||
guest_function->MapGuestAddressToMachineCode(guest_address);
|
||||
assert_not_zero(host_address);
|
||||
callback(host_address);
|
||||
}
|
||||
} else {
|
||||
// Direct host address patching.
|
||||
callback(host_address());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace cpu
|
||||
|
||||
@@ -17,22 +17,61 @@ namespace cpu {
|
||||
|
||||
class Breakpoint {
|
||||
public:
|
||||
Breakpoint(Processor* processor,
|
||||
std::function<void(uint32_t, uint64_t)> hit_callback);
|
||||
Breakpoint(Processor* processor, uint32_t address,
|
||||
std::function<void(uint32_t, uint64_t)> hit_callback);
|
||||
enum class AddressType {
|
||||
kGuest,
|
||||
kHost,
|
||||
};
|
||||
typedef std::function<void(Breakpoint*, ThreadDebugInfo*, uint64_t)>
|
||||
HitCallback;
|
||||
|
||||
Breakpoint(Processor* processor, AddressType address_type, uint64_t address,
|
||||
HitCallback hit_callback);
|
||||
~Breakpoint();
|
||||
|
||||
uint32_t address() const { return address_; }
|
||||
void set_address(uint32_t address) {
|
||||
assert_false(installed_);
|
||||
address_ = address;
|
||||
AddressType address_type() const { return address_type_; }
|
||||
uint64_t address() const { return address_; }
|
||||
uint32_t guest_address() const {
|
||||
assert_true(address_type_ == AddressType::kGuest);
|
||||
return static_cast<uint32_t>(address_);
|
||||
}
|
||||
uintptr_t host_address() const {
|
||||
assert_true(address_type_ == AddressType::kHost);
|
||||
return static_cast<uintptr_t>(address_);
|
||||
}
|
||||
bool installed() const { return installed_; }
|
||||
|
||||
bool Install();
|
||||
bool Uninstall();
|
||||
void Hit(uint64_t host_pc) { hit_callback_(address_, host_pc); }
|
||||
// Whether the breakpoint has been enabled by the user.
|
||||
bool is_enabled() const { return enabled_; }
|
||||
|
||||
// Toggles the breakpoint state.
|
||||
// Assumes the caller holds the global lock.
|
||||
void set_enabled(bool is_enabled) {
|
||||
enabled_ = is_enabled;
|
||||
if (!enabled_ && installed_) {
|
||||
Uninstall();
|
||||
} else if (enabled_ && !installed_ && !suspend_count_) {
|
||||
Install();
|
||||
}
|
||||
}
|
||||
|
||||
// Whether the breakpoint is currently installed and active.
|
||||
bool is_installed() const { return installed_; }
|
||||
|
||||
std::string to_string() const;
|
||||
|
||||
// Returns a guest function that contains the guest address, if any.
|
||||
// If there are multiple functions that contain the address a random one will
|
||||
// be returned. If this is a host-address code breakpoint this will attempt to
|
||||
// find a function with that address and otherwise return nullptr.
|
||||
GuestFunction* guest_function() const;
|
||||
|
||||
// Returns true if this breakpoint, when installed, contains the given host
|
||||
// address.
|
||||
bool ContainsHostAddress(uintptr_t search_address) const;
|
||||
|
||||
// Enumerates all host addresses that correspond to this breakpoint.
|
||||
// If this is a host type it will return the single host address, if it is a
|
||||
// guest type it may return multiple host addresses.
|
||||
void ForEachHostAddress(std::function<void(uintptr_t)> callback) const;
|
||||
|
||||
// CPU backend data. Implementation specific - DO NOT TOUCH THIS!
|
||||
std::vector<std::pair<uint64_t, uint64_t>> backend_data() const {
|
||||
@@ -42,12 +81,50 @@ class Breakpoint {
|
||||
return backend_data_;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Processor;
|
||||
|
||||
void OnHit(ThreadDebugInfo* thread_info, uint64_t host_pc) {
|
||||
hit_callback_(this, thread_info, host_pc);
|
||||
}
|
||||
|
||||
// Suspends the breakpoint until it is resumed with Resume.
|
||||
// This preserves the user enabled state.
|
||||
// Assumes the caller holds the global lock.
|
||||
void Suspend() {
|
||||
++suspend_count_;
|
||||
if (installed_) {
|
||||
Uninstall();
|
||||
}
|
||||
}
|
||||
|
||||
// Resumes a previously-suspended breakpoint, reinstalling it if required.
|
||||
// Assumes the caller holds the global lock.
|
||||
void Resume() {
|
||||
--suspend_count_;
|
||||
if (!suspend_count_ && enabled_) {
|
||||
Install();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Processor* processor_ = nullptr;
|
||||
|
||||
void Install();
|
||||
void Uninstall();
|
||||
|
||||
// True if patched into code.
|
||||
bool installed_ = false;
|
||||
uint32_t address_ = 0;
|
||||
std::function<void(uint32_t, uint64_t)> hit_callback_;
|
||||
// True if user enabled.
|
||||
bool enabled_ = true;
|
||||
// Depth of suspends (must be 0 to be installed). Defaults to suspended so
|
||||
// that it's never installed unless the debugger knows about it.
|
||||
int suspend_count_ = 1;
|
||||
|
||||
AddressType address_type_;
|
||||
uint64_t address_ = 0;
|
||||
|
||||
HitCallback hit_callback_;
|
||||
|
||||
// Opaque backend data. Don't touch this.
|
||||
std::vector<std::pair<uint64_t, uint64_t>> backend_data_;
|
||||
|
||||
57
src/xenia/cpu/debug_listener.h
Normal file
57
src/xenia/cpu/debug_listener.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2016 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_CPU_DEBUG_LISTENER_H_
|
||||
#define XENIA_CPU_DEBUG_LISTENER_H_
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
|
||||
class Breakpoint;
|
||||
struct ThreadDebugInfo;
|
||||
|
||||
// Debug event listener interface.
|
||||
// Implementations will receive calls from arbitrary threads and must marshal
|
||||
// them as required.
|
||||
class DebugListener {
|
||||
public:
|
||||
// Handles request for debugger focus (such as when the user requests the
|
||||
// debugger be brought to the front).
|
||||
virtual void OnFocus() = 0;
|
||||
|
||||
// Handles the debugger detaching from the target.
|
||||
// This will be called on shutdown or when the user requests the debug session
|
||||
// end.
|
||||
virtual void OnDetached() = 0;
|
||||
|
||||
// Handles execution being interrupted and transitioning to
|
||||
// ExceutionState::kPaused.
|
||||
virtual void OnExecutionPaused() = 0;
|
||||
// Handles execution continuing when transitioning to
|
||||
// ExecutionState::kRunning or ExecutionState::kStepping.
|
||||
virtual void OnExecutionContinued() = 0;
|
||||
// Handles execution ending when transitioning to ExecutionState::kEnded.
|
||||
virtual void OnExecutionEnded() = 0;
|
||||
|
||||
// Handles step completion events when a requested step operation completes.
|
||||
// The thread is the one that hit its step target. Note that because multiple
|
||||
// threads could be stepping simultaneously (such as a run-to-cursor) use the
|
||||
// thread passed instead of keeping any other state.
|
||||
virtual void OnStepCompleted(ThreadDebugInfo* thread_info) = 0;
|
||||
|
||||
// Handles breakpoint events as they are hit per-thread.
|
||||
// Breakpoints may be hit during stepping.
|
||||
virtual void OnBreakpointHit(Breakpoint* breakpoint,
|
||||
ThreadDebugInfo* thread_info) = 0;
|
||||
};
|
||||
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_CPU_DEBUG_LISTENER_H_
|
||||
@@ -13,11 +13,11 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/cpu/debug_info.h"
|
||||
#include "xenia/cpu/function_debug_info.h"
|
||||
#include "xenia/cpu/function_trace_data.h"
|
||||
#include "xenia/cpu/ppc/ppc_context.h"
|
||||
#include "xenia/cpu/symbol.h"
|
||||
#include "xenia/cpu/thread_state.h"
|
||||
#include "xenia/debug/function_trace_data.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
@@ -111,11 +111,11 @@ class GuestFunction : public Function {
|
||||
virtual uint8_t* machine_code() const = 0;
|
||||
virtual size_t machine_code_length() const = 0;
|
||||
|
||||
DebugInfo* debug_info() const { return debug_info_.get(); }
|
||||
void set_debug_info(std::unique_ptr<DebugInfo> debug_info) {
|
||||
FunctionDebugInfo* debug_info() const { return debug_info_.get(); }
|
||||
void set_debug_info(std::unique_ptr<FunctionDebugInfo> debug_info) {
|
||||
debug_info_ = std::move(debug_info);
|
||||
}
|
||||
debug::FunctionTraceData& trace_data() { return trace_data_; }
|
||||
FunctionTraceData& trace_data() { return trace_data_; }
|
||||
std::vector<SourceMapEntry>& source_map() { return source_map_; }
|
||||
|
||||
ExternHandler extern_handler() const { return extern_handler_; }
|
||||
@@ -136,8 +136,8 @@ class GuestFunction : public Function {
|
||||
virtual bool CallImpl(ThreadState* thread_state, uint32_t return_address) = 0;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<DebugInfo> debug_info_;
|
||||
debug::FunctionTraceData trace_data_;
|
||||
std::unique_ptr<FunctionDebugInfo> debug_info_;
|
||||
FunctionTraceData trace_data_;
|
||||
std::vector<SourceMapEntry> source_map_;
|
||||
ExternHandler extern_handler_ = nullptr;
|
||||
Export* export_data_ = nullptr;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/cpu/debug_info.h"
|
||||
#include "xenia/cpu/function_debug_info.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -17,20 +17,20 @@
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
|
||||
DebugInfo::DebugInfo()
|
||||
FunctionDebugInfo::FunctionDebugInfo()
|
||||
: source_disasm_(nullptr),
|
||||
raw_hir_disasm_(nullptr),
|
||||
hir_disasm_(nullptr),
|
||||
machine_code_disasm_(nullptr) {}
|
||||
|
||||
DebugInfo::~DebugInfo() {
|
||||
FunctionDebugInfo::~FunctionDebugInfo() {
|
||||
free(source_disasm_);
|
||||
free(raw_hir_disasm_);
|
||||
free(hir_disasm_);
|
||||
free(machine_code_disasm_);
|
||||
}
|
||||
|
||||
void DebugInfo::Dump() {
|
||||
void FunctionDebugInfo::Dump() {
|
||||
if (source_disasm_) {
|
||||
XELOGD("PPC:\n%s\n", source_disasm_);
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_CPU_DEBUG_INFO_H_
|
||||
#define XENIA_CPU_DEBUG_INFO_H_
|
||||
#ifndef XENIA_CPU_FUNCTION_DEBUG_INFO_H_
|
||||
#define XENIA_CPU_FUNCTION_DEBUG_INFO_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -38,10 +38,10 @@ enum DebugInfoFlags : uint32_t {
|
||||
// DEPRECATED
|
||||
// This will be getting removed or refactored to contain only on-demand
|
||||
// disassembly data.
|
||||
class DebugInfo {
|
||||
class FunctionDebugInfo {
|
||||
public:
|
||||
DebugInfo();
|
||||
~DebugInfo();
|
||||
FunctionDebugInfo();
|
||||
~FunctionDebugInfo();
|
||||
|
||||
uint32_t address_reference_count() const { return address_reference_count_; }
|
||||
void set_address_reference_count(uint32_t value) {
|
||||
@@ -78,4 +78,4 @@ class DebugInfo {
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_CPU_DEBUG_INFO_H_
|
||||
#endif // XENIA_CPU_FUNCTION_DEBUG_INFO_H_
|
||||
93
src/xenia/cpu/function_trace_data.h
Normal file
93
src/xenia/cpu/function_trace_data.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2015 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_CPU_FUNCTION_TRACE_DATA_H_
|
||||
#define XENIA_CPU_FUNCTION_TRACE_DATA_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "xenia/base/memory.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
|
||||
class FunctionTraceData {
|
||||
public:
|
||||
static const int kFunctionCallerHistoryCount = 4;
|
||||
|
||||
struct Header {
|
||||
// Format is used by tooling, changes must be made across all targets.
|
||||
// + 0 4b (data size)
|
||||
// + 4 4b start_address
|
||||
// + 8 4b end_address
|
||||
// +12 4b type (user, external, etc)
|
||||
// +16 8b function_thread_use // bitmask of thread id
|
||||
// +24 8b function_call_count
|
||||
// +32 4b+ function_caller_history[4]
|
||||
// +48 8b+ instruction_execute_count[instruction count]
|
||||
uint32_t data_size;
|
||||
uint32_t start_address;
|
||||
uint32_t end_address;
|
||||
uint32_t type;
|
||||
uint64_t function_thread_use;
|
||||
uint64_t function_call_count;
|
||||
uint32_t function_caller_history[kFunctionCallerHistoryCount];
|
||||
// uint64_t instruction_execute_count[];
|
||||
};
|
||||
|
||||
FunctionTraceData() : header_(nullptr) {}
|
||||
|
||||
void Reset(uint8_t* trace_data, size_t trace_data_size,
|
||||
uint32_t start_address, uint32_t end_address) {
|
||||
header_ = reinterpret_cast<Header*>(trace_data);
|
||||
header_->data_size = uint32_t(trace_data_size);
|
||||
header_->start_address = start_address;
|
||||
header_->end_address = end_address;
|
||||
header_->type = 0;
|
||||
header_->function_thread_use = 0;
|
||||
header_->function_call_count = 0;
|
||||
for (int i = 0; i < kFunctionCallerHistoryCount; ++i) {
|
||||
header_->function_caller_history[i] = 0;
|
||||
}
|
||||
// Clear any remaining.
|
||||
std::memset(trace_data + sizeof(Header), 0,
|
||||
trace_data_size - sizeof(Header));
|
||||
}
|
||||
|
||||
bool is_valid() const { return header_ != nullptr; }
|
||||
|
||||
uint32_t start_address() const { return header_->start_address; }
|
||||
uint32_t end_address() const { return header_->end_address; }
|
||||
uint32_t instruction_count() const {
|
||||
return (header_->end_address - header_->start_address) / 4 + 1;
|
||||
}
|
||||
|
||||
Header* header() const { return header_; }
|
||||
|
||||
uint8_t* instruction_execute_counts() const {
|
||||
return reinterpret_cast<uint8_t*>(header_) + sizeof(Header);
|
||||
}
|
||||
|
||||
static size_t SizeOfHeader() { return sizeof(Header); }
|
||||
|
||||
static size_t SizeOfInstructionCounts(uint32_t start_address,
|
||||
uint32_t end_address) {
|
||||
uint32_t instruction_count = (end_address - start_address) / 4 + 1;
|
||||
return instruction_count * 8;
|
||||
}
|
||||
|
||||
private:
|
||||
Header* header_;
|
||||
};
|
||||
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_CPU_FUNCTION_TRACE_DATA_H_
|
||||
@@ -41,7 +41,7 @@ bool PPCScanner::IsRestGprLr(uint32_t address) {
|
||||
return function && function->behavior() == Function::Behavior::kEpilogReturn;
|
||||
}
|
||||
|
||||
bool PPCScanner::Scan(GuestFunction* function, DebugInfo* debug_info) {
|
||||
bool PPCScanner::Scan(GuestFunction* function, FunctionDebugInfo* debug_info) {
|
||||
// This is a simple basic block analyizer. It walks the start address to the
|
||||
// end address looking for branches. Each span of instructions between
|
||||
// branches is considered a basic block. When the last blr (that has no
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/cpu/debug_info.h"
|
||||
#include "xenia/cpu/function.h"
|
||||
#include "xenia/cpu/function_debug_info.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
@@ -31,7 +31,7 @@ class PPCScanner {
|
||||
explicit PPCScanner(PPCFrontend* frontend);
|
||||
~PPCScanner();
|
||||
|
||||
bool Scan(GuestFunction* function, DebugInfo* debug_info);
|
||||
bool Scan(GuestFunction* function, FunctionDebugInfo* debug_info);
|
||||
|
||||
std::vector<BlockInfo> FindBlocks(GuestFunction* function);
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "xenia/cpu/ppc/ppc_opcode_info.h"
|
||||
#include "xenia/cpu/ppc/ppc_scanner.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/debug/debugger.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
@@ -118,9 +117,9 @@ bool PPCTranslator::Translate(GuestFunction* function,
|
||||
if (FLAGS_trace_function_data) {
|
||||
debug_info_flags |= DebugInfoFlags::kDebugInfoTraceFunctionData;
|
||||
}
|
||||
std::unique_ptr<DebugInfo> debug_info;
|
||||
std::unique_ptr<FunctionDebugInfo> debug_info;
|
||||
if (debug_info_flags) {
|
||||
debug_info.reset(new DebugInfo());
|
||||
debug_info.reset(new FunctionDebugInfo());
|
||||
}
|
||||
|
||||
// Scan the function to find its extents and gather debug data.
|
||||
@@ -128,25 +127,24 @@ bool PPCTranslator::Translate(GuestFunction* function,
|
||||
return false;
|
||||
}
|
||||
|
||||
auto debugger = frontend_->processor()->debugger();
|
||||
if (!debugger) {
|
||||
debug_info_flags &= ~DebugInfoFlags::kDebugInfoAllTracing;
|
||||
}
|
||||
|
||||
// Setup trace data, if needed.
|
||||
if (debug_info_flags & DebugInfoFlags::kDebugInfoTraceFunctions) {
|
||||
// Base trace data.
|
||||
size_t trace_data_size = debug::FunctionTraceData::SizeOfHeader();
|
||||
size_t trace_data_size = FunctionTraceData::SizeOfHeader();
|
||||
if (debug_info_flags & DebugInfoFlags::kDebugInfoTraceFunctionCoverage) {
|
||||
// Additional space for instruction coverage counts.
|
||||
trace_data_size += debug::FunctionTraceData::SizeOfInstructionCounts(
|
||||
trace_data_size += FunctionTraceData::SizeOfInstructionCounts(
|
||||
function->address(), function->end_address());
|
||||
}
|
||||
uint8_t* trace_data = debugger->AllocateFunctionTraceData(trace_data_size);
|
||||
uint8_t* trace_data =
|
||||
frontend_->processor()->AllocateFunctionTraceData(trace_data_size);
|
||||
if (trace_data) {
|
||||
function->trace_data().Reset(trace_data, trace_data_size,
|
||||
function->address(),
|
||||
function->end_address());
|
||||
} else {
|
||||
debug_info_flags &= ~(DebugInfoFlags::kDebugInfoTraceFunctions |
|
||||
DebugInfoFlags::kDebugInfoTraceFunctionCoverage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ class TestRunner {
|
||||
memory->Reset();
|
||||
|
||||
// Setup a fresh processor.
|
||||
processor.reset(new Processor(nullptr, memory.get(), nullptr, nullptr));
|
||||
processor.reset(new Processor(memory.get(), nullptr));
|
||||
processor->Setup();
|
||||
processor->set_debug_info_flags(DebugInfoFlags::kDebugInfoAll);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ project("xenia-cpu-ppc-tests")
|
||||
"xenia-cpu-backend-x64",
|
||||
|
||||
-- TODO(benvanik): remove these dependencies.
|
||||
"xenia-debug",
|
||||
"xenia-kernel"
|
||||
})
|
||||
files({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,34 +10,33 @@
|
||||
#ifndef XENIA_CPU_PROCESSOR_H_
|
||||
#define XENIA_CPU_PROCESSOR_H_
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/mapped_memory.h"
|
||||
#include "xenia/base/mutex.h"
|
||||
#include "xenia/cpu/backend/backend.h"
|
||||
#include "xenia/cpu/debug_listener.h"
|
||||
#include "xenia/cpu/entry_table.h"
|
||||
#include "xenia/cpu/export_resolver.h"
|
||||
#include "xenia/cpu/function.h"
|
||||
#include "xenia/cpu/module.h"
|
||||
#include "xenia/cpu/ppc/ppc_frontend.h"
|
||||
#include "xenia/cpu/thread_debug_info.h"
|
||||
#include "xenia/cpu/thread_state.h"
|
||||
#include "xenia/memory.h"
|
||||
|
||||
namespace xe {
|
||||
class Emulator;
|
||||
|
||||
namespace debug {
|
||||
class Debugger;
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
DECLARE_bool(debug);
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
|
||||
class Breakpoint;
|
||||
class StackWalker;
|
||||
class ThreadState;
|
||||
class XexModule;
|
||||
|
||||
enum class Irql : uint32_t {
|
||||
@@ -47,15 +46,26 @@ enum class Irql : uint32_t {
|
||||
DPC = 3,
|
||||
};
|
||||
|
||||
// Describes the current state of the emulator as known to the debugger.
|
||||
// This determines which state the debugger is in and what operations are
|
||||
// allowed.
|
||||
enum class ExecutionState {
|
||||
// Target is running; the debugger is not waiting for any events.
|
||||
kRunning,
|
||||
// Target is running in stepping mode with the debugger waiting for feedback.
|
||||
kStepping,
|
||||
// Target is paused for debugging.
|
||||
kPaused,
|
||||
// Target has been stopped and cannot be restarted (crash, etc).
|
||||
kEnded,
|
||||
};
|
||||
|
||||
class Processor {
|
||||
public:
|
||||
Processor(Emulator* emulator, Memory* memory, ExportResolver* export_resolver,
|
||||
debug::Debugger* debugger);
|
||||
Processor(Memory* memory, ExportResolver* export_resolver);
|
||||
~Processor();
|
||||
|
||||
Emulator* emulator() const { return emulator_; }
|
||||
Memory* memory() const { return memory_; }
|
||||
debug::Debugger* debugger() const { return debugger_; }
|
||||
StackWalker* stack_walker() const { return stack_walker_.get(); }
|
||||
ppc::PPCFrontend* frontend() const { return frontend_.get(); }
|
||||
backend::Backend* backend() const { return backend_.get(); }
|
||||
@@ -63,6 +73,28 @@ class Processor {
|
||||
|
||||
bool Setup();
|
||||
|
||||
// Runs any pre-launch logic once the module and thread have been setup.
|
||||
void PreLaunch();
|
||||
|
||||
// The current execution state of the emulator.
|
||||
ExecutionState execution_state() const { return execution_state_; }
|
||||
|
||||
// True if a debug listener is attached and the debugger is active.
|
||||
bool is_debugger_attached() const { return !!debug_listener_; }
|
||||
// Gets the active debug listener, if any.
|
||||
DebugListener* debug_listener() const { return debug_listener_; }
|
||||
// Sets the active debug listener, if any.
|
||||
// This can be used to detach the listener.
|
||||
void set_debug_listener(DebugListener* debug_listener);
|
||||
// Sets a handler that will be called from a random thread when a debugger
|
||||
// listener is required (such as on a breakpoint hit/etc).
|
||||
// Will only be called if the debug listener has not already been specified
|
||||
// with set_debug_listener.
|
||||
void set_debug_listener_request_handler(
|
||||
std::function<DebugListener*(Processor*)> handler) {
|
||||
debug_listener_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void set_debug_info_flags(uint32_t debug_info_flags) {
|
||||
debug_info_flags_ = debug_info_flags;
|
||||
}
|
||||
@@ -94,23 +126,128 @@ class Processor {
|
||||
Irql RaiseIrql(Irql new_value);
|
||||
void LowerIrql(Irql old_value);
|
||||
|
||||
bool InstallBreakpoint(Breakpoint* bp);
|
||||
bool UninstallBreakpoint(Breakpoint* bp);
|
||||
bool BreakpointHit(uint32_t address, uint64_t host_pc);
|
||||
bool Save(ByteStream* stream);
|
||||
bool Restore(ByteStream* stream);
|
||||
|
||||
// Returns a list of debugger info for all threads that have ever existed.
|
||||
// This is the preferred way to sample thread state vs. attempting to ask
|
||||
// the kernel.
|
||||
std::vector<ThreadDebugInfo*> QueryThreadDebugInfos();
|
||||
|
||||
// Returns the debugger info for the given thread.
|
||||
ThreadDebugInfo* QueryThreadDebugInfo(uint32_t thread_id);
|
||||
|
||||
// Adds a breakpoint to the debugger and activates it (if enabled).
|
||||
// The given breakpoint will not be owned by the debugger and must remain
|
||||
// allocated so long as it is added.
|
||||
void AddBreakpoint(Breakpoint* breakpoint);
|
||||
|
||||
// Removes a breakpoint from the debugger and deactivates it.
|
||||
void RemoveBreakpoint(Breakpoint* breakpoint);
|
||||
|
||||
// Finds a breakpoint that may be registered at the given address.
|
||||
Breakpoint* FindBreakpoint(uint32_t address);
|
||||
std::vector<Breakpoint*> breakpoints() const { return breakpoints_; }
|
||||
|
||||
// Returns all currently registered breakpoints.
|
||||
std::vector<Breakpoint*> breakpoints() const;
|
||||
|
||||
// Shows the debug listener, focusing it if it already exists.
|
||||
void ShowDebugger();
|
||||
|
||||
// Pauses target execution by suspending all threads.
|
||||
// The debug listener will be requested if it has not been attached.
|
||||
void Pause();
|
||||
|
||||
// Continues target execution from wherever it is.
|
||||
// This will cancel any active step operations and resume all threads.
|
||||
void Continue();
|
||||
|
||||
// Steps the given thread a single x64 host instruction.
|
||||
// If the step is over a branch the branch will be followed.
|
||||
void StepHostInstruction(uint32_t thread_id);
|
||||
|
||||
// Steps the given thread a single PPC guest instruction.
|
||||
// If the step is over a branch the branch will be followed.
|
||||
void StepGuestInstruction(uint32_t thread_id);
|
||||
|
||||
// Steps the given thread until the guest address is hit.
|
||||
// Returns false if the step could not be completed (invalid target address).
|
||||
bool StepToGuestAddress(uint32_t thread_id, uint32_t pc);
|
||||
|
||||
// Steps the given thread to the target of the branch at the specified guest
|
||||
// address. The address must specify a branch instruction.
|
||||
// Returns the new PC guest address.
|
||||
uint32_t StepIntoGuestBranchTarget(uint32_t thread_id, uint32_t pc);
|
||||
|
||||
// Steps the thread to a point where it's safe to terminate or read its
|
||||
// context. Returns the PC after we've finished stepping.
|
||||
// Pass true for ignore_host if you've stopped the thread yourself
|
||||
// in host code you want to ignore.
|
||||
// Returns the new PC guest address.
|
||||
uint32_t StepToGuestSafePoint(uint32_t thread_id, bool ignore_host = false);
|
||||
|
||||
public:
|
||||
// TODO(benvanik): hide.
|
||||
void OnThreadCreated(uint32_t handle, ThreadState* thread_state,
|
||||
kernel::XThread* thread);
|
||||
void OnThreadExit(uint32_t thread_id);
|
||||
void OnThreadDestroyed(uint32_t thread_id);
|
||||
void OnThreadEnteringWait(uint32_t thread_id);
|
||||
void OnThreadLeavingWait(uint32_t thread_id);
|
||||
|
||||
bool OnUnhandledException(Exception* ex);
|
||||
bool OnThreadBreakpointHit(Exception* ex);
|
||||
|
||||
uint8_t* AllocateFunctionTraceData(size_t size);
|
||||
|
||||
private:
|
||||
void BreakpointFunctionDefined(Function* function);
|
||||
// Synchronously demands a debug listener.
|
||||
void DemandDebugListener();
|
||||
|
||||
// Suspends all known threads (except the caller).
|
||||
bool SuspendAllThreads();
|
||||
// Resumes the given thread.
|
||||
bool ResumeThread(uint32_t thread_id);
|
||||
// Resumes all known threads (except the caller).
|
||||
bool ResumeAllThreads();
|
||||
// Updates all cached thread execution info (state, call stacks, etc).
|
||||
// The given override thread handle and context will be used in place of
|
||||
// sampled values for that thread.
|
||||
void UpdateThreadExecutionStates(uint32_t override_handle = 0,
|
||||
X64Context* override_context = nullptr);
|
||||
|
||||
// Suspends all breakpoints, uninstalling them as required.
|
||||
// No breakpoints will be triggered until they are resumed.
|
||||
void SuspendAllBreakpoints();
|
||||
// Resumes all breakpoints, re-installing them if required.
|
||||
void ResumeAllBreakpoints();
|
||||
|
||||
void OnFunctionDefined(Function* function);
|
||||
|
||||
static bool ExceptionCallbackThunk(Exception* ex, void* data);
|
||||
bool ExceptionCallback(Exception* ex);
|
||||
void OnStepCompleted(ThreadDebugInfo* thread_info);
|
||||
void OnBreakpointHit(ThreadDebugInfo* thread_info, Breakpoint* breakpoint);
|
||||
|
||||
// Calculates the next guest instruction based on the current thread state and
|
||||
// current PC. This will look for branches and other control flow
|
||||
// instructions.
|
||||
uint32_t CalculateNextGuestInstruction(ThreadDebugInfo* thread_info,
|
||||
uint32_t current_pc);
|
||||
|
||||
bool DemandFunction(Function* function);
|
||||
|
||||
Emulator* emulator_ = nullptr;
|
||||
Memory* memory_ = nullptr;
|
||||
debug::Debugger* debugger_ = nullptr;
|
||||
std::unique_ptr<StackWalker> stack_walker_;
|
||||
|
||||
std::function<DebugListener*(Processor*)> debug_listener_handler_;
|
||||
DebugListener* debug_listener_ = nullptr;
|
||||
|
||||
// Which debug features are enabled in generated code.
|
||||
uint32_t debug_info_flags_ = 0;
|
||||
// If specified, the file trace data gets written to when running.
|
||||
std::wstring functions_trace_path_;
|
||||
std::unique_ptr<ChunkedMappedMemoryWriter> functions_trace_file_;
|
||||
|
||||
std::unique_ptr<ppc::PPCFrontend> frontend_;
|
||||
std::unique_ptr<backend::Backend> backend_;
|
||||
@@ -118,11 +255,16 @@ class Processor {
|
||||
|
||||
EntryTable entry_table_;
|
||||
xe::global_critical_region global_critical_region_;
|
||||
ExecutionState execution_state_ = ExecutionState::kPaused;
|
||||
std::vector<std::unique_ptr<Module>> modules_;
|
||||
Module* builtin_module_ = nullptr;
|
||||
uint32_t next_builtin_address_ = 0xFFFF0000u;
|
||||
|
||||
std::recursive_mutex breakpoint_lock_;
|
||||
// Maps thread ID to state. Updated on thread create, and threads are never
|
||||
// removed. Must be guarded with the global lock.
|
||||
std::map<uint32_t, std::unique_ptr<ThreadDebugInfo>> thread_debug_infos_;
|
||||
|
||||
// TODO(benvanik): cleanup/change structures.
|
||||
std::vector<Breakpoint*> breakpoints_;
|
||||
|
||||
Irql irql_;
|
||||
|
||||
@@ -241,8 +241,10 @@ class Win32StackWalker : public StackWalker {
|
||||
// displacement in x64 from the JIT'ed code start to the PC.
|
||||
if (function->is_guest()) {
|
||||
auto guest_function = static_cast<GuestFunction*>(function);
|
||||
// Adjust the host PC by -1 so that we will go back into whatever
|
||||
// instruction was executing before the capture (like a call).
|
||||
frame.guest_pc =
|
||||
guest_function->MapMachineCodeToGuestAddress(frame.host_pc);
|
||||
guest_function->MapMachineCodeToGuestAddress(frame.host_pc - 1);
|
||||
}
|
||||
} else {
|
||||
frame.guest_symbol.function = nullptr;
|
||||
|
||||
@@ -12,7 +12,6 @@ test_suite("xenia-cpu-tests", project_root, ".", {
|
||||
"xenia-cpu-backend-x64",
|
||||
|
||||
-- TODO(benvanik): cut these dependencies?
|
||||
"xenia-debug",
|
||||
"xenia-kernel",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -39,8 +39,7 @@ class TestFunction {
|
||||
|
||||
#if XENIA_TEST_X64
|
||||
{
|
||||
auto processor =
|
||||
std::make_unique<Processor>(nullptr, memory.get(), nullptr, nullptr);
|
||||
auto processor = std::make_unique<Processor>(memory.get(), nullptr);
|
||||
processor->Setup();
|
||||
processors.emplace_back(std::move(processor));
|
||||
}
|
||||
|
||||
109
src/xenia/cpu/thread_debug_info.h
Normal file
109
src/xenia/cpu/thread_debug_info.h
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2016 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_CPU_THREAD_DEBUG_INFO_H_
|
||||
#define XENIA_CPU_THREAD_DEBUG_INFO_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/x64_context.h"
|
||||
#include "xenia/cpu/thread_state.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
class XThread;
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
|
||||
class Breakpoint;
|
||||
class Function;
|
||||
|
||||
// Per-thread structure holding debugger state and a cache of the sampled call
|
||||
// stack.
|
||||
//
|
||||
// In most cases debug consumers should rely only on data in this structure as
|
||||
// it is never removed (even when a thread is destroyed) and always available
|
||||
// even when running.
|
||||
struct ThreadDebugInfo {
|
||||
ThreadDebugInfo() = default;
|
||||
~ThreadDebugInfo() = default;
|
||||
|
||||
enum class State {
|
||||
// Thread is alive and running.
|
||||
kAlive,
|
||||
// Thread is in a wait state.
|
||||
kWaiting,
|
||||
// Thread has exited but not yet been killed.
|
||||
kExited,
|
||||
// Thread has been killed.
|
||||
kZombie,
|
||||
};
|
||||
|
||||
// XThread::thread_id(), unique to the thread for the run of the emulator.
|
||||
uint32_t thread_id = 0;
|
||||
// XThread::handle() of the thread.
|
||||
// This will be invalidated when the thread dies.
|
||||
uint32_t thread_handle = 0;
|
||||
// Kernel thread object. Only valid when the thread is alive.
|
||||
kernel::XThread* thread = nullptr;
|
||||
// Current state of the thread.
|
||||
State state = State::kAlive;
|
||||
// Whether the debugger has forcefully suspended this thread.
|
||||
bool suspended = false;
|
||||
|
||||
// A breakpoint managed by the stepping system, installed as required to
|
||||
// trigger a break at the next instruction.
|
||||
std::unique_ptr<Breakpoint> step_breakpoint;
|
||||
// A breakpoint managed by the stepping system, installed as required to
|
||||
// trigger after a step over a disabled breakpoint.
|
||||
// When this breakpoint is hit the breakpoint referenced in
|
||||
// restore_original_breakpoint will be reinstalled.
|
||||
// Breakpoint restore_step_breakpoint;
|
||||
// If the thread is stepping over a disabled breakpoint this will point to
|
||||
// that breakpoint so it can be restored.
|
||||
// Breakpoint* restore_original_breakpoint = nullptr;
|
||||
|
||||
// Last-sampled PPC context.
|
||||
// This is updated whenever the debugger stops.
|
||||
ppc::PPCContext guest_context;
|
||||
// Last-sampled host x64 context.
|
||||
// This is updated whenever the debugger stops and must be used instead of any
|
||||
// value taken from the StackWalker as it properly respects exception stacks.
|
||||
X64Context host_context;
|
||||
|
||||
// A single frame in a call stack.
|
||||
struct Frame {
|
||||
// PC of the current instruction in host code.
|
||||
uint64_t host_pc = 0;
|
||||
// Base of the function the current host_pc is located within.
|
||||
uint64_t host_function_address = 0;
|
||||
// PC of the current instruction in guest code.
|
||||
// 0 if not a guest address or not known.
|
||||
uint32_t guest_pc = 0;
|
||||
// Base of the function the current guest_pc is located within.
|
||||
uint32_t guest_function_address = 0;
|
||||
// Function the current guest_pc is located within.
|
||||
Function* guest_function = nullptr;
|
||||
// Name of the function, if known.
|
||||
// TODO(benvanik): string table?
|
||||
char name[256] = {0};
|
||||
};
|
||||
|
||||
// Last-sampled call stack.
|
||||
// This is updated whenever the debugger stops.
|
||||
std::vector<Frame> frames;
|
||||
};
|
||||
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_CPU_THREAD_DEBUG_INFO_H_
|
||||
@@ -16,7 +16,6 @@
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/debug/debugger.h"
|
||||
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
@@ -73,7 +72,9 @@ void ThreadState::Bind(ThreadState* thread_state) {
|
||||
|
||||
ThreadState* ThreadState::Get() { return thread_state_; }
|
||||
|
||||
uint32_t ThreadState::GetThreadID() { return thread_state_->thread_id_; }
|
||||
uint32_t ThreadState::GetThreadID() {
|
||||
return thread_state_ ? thread_state_->thread_id_ : 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
Reference in New Issue
Block a user