Debugger stuff. Lots of wasted work :/
This commit is contained in:
parent
42ef3f224a
commit
7ecc6362de
|
@ -45,6 +45,33 @@ class Delegate {
|
|||
std::vector<Listener> listeners_;
|
||||
};
|
||||
|
||||
template <>
|
||||
class Delegate<void> {
|
||||
public:
|
||||
typedef std::function<void()> Listener;
|
||||
|
||||
void AddListener(Listener const& listener) {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
listeners_.push_back(listener);
|
||||
}
|
||||
|
||||
void RemoveAllListeners() {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
listeners_.clear();
|
||||
}
|
||||
|
||||
void operator()() {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
for (auto& listener : listeners_) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex lock_;
|
||||
std::vector<Listener> listeners_;
|
||||
};
|
||||
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_BASE_DELEGATE_H_
|
||||
|
|
|
@ -28,7 +28,8 @@ namespace xe {
|
|||
// wait handle should be waited on until new data arrives.
|
||||
class Socket {
|
||||
public:
|
||||
// TODO(benvanik): client socket static Connect method.
|
||||
// Synchronously connects to the given hostname:port.
|
||||
static std::unique_ptr<Socket> Connect(std::string hostname, uint16_t port);
|
||||
|
||||
virtual ~Socket() = default;
|
||||
|
||||
|
@ -84,6 +85,12 @@ class Socket {
|
|||
auto buffer_list = std::make_pair(buffer.data(), buffer.size());
|
||||
return Send(&buffer_list, 1);
|
||||
}
|
||||
|
||||
// Asynchronously sends a string buffer.
|
||||
// Returns false if the socket is disconnected or the data cannot be sent.
|
||||
bool Send(const std::string& value) {
|
||||
return Send(value.data(), value.size());
|
||||
}
|
||||
};
|
||||
|
||||
// Runs a socket server on the specified local port.
|
||||
|
|
|
@ -36,6 +36,51 @@ class Win32Socket : public Socket {
|
|||
Win32Socket() = default;
|
||||
~Win32Socket() override { Close(); }
|
||||
|
||||
bool Connect(std::string hostname, uint16_t port) {
|
||||
addrinfo hints = {0};
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_protocol = IPPROTO_TCP;
|
||||
|
||||
// Resolve the server address and port.
|
||||
// May return multiple results, so attempt to connect to an address until
|
||||
// one succeeds.
|
||||
addrinfo* result = nullptr;
|
||||
auto port_string = std::to_string(port);
|
||||
int ret =
|
||||
getaddrinfo(hostname.c_str(), port_string.c_str(), &hints, &result);
|
||||
if (ret != 0) {
|
||||
XELOGE("getaddrinfo failed with error: %d", ret);
|
||||
return false;
|
||||
}
|
||||
SOCKET try_socket = INVALID_SOCKET;
|
||||
for (addrinfo* ptr = result; ptr; ptr = ptr->ai_next) {
|
||||
// Create a SOCKET for connecting to server.
|
||||
try_socket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
|
||||
if (try_socket == INVALID_SOCKET) {
|
||||
XELOGE("socket failed with error: %ld", WSAGetLastError());
|
||||
freeaddrinfo(result);
|
||||
return false;
|
||||
}
|
||||
// Connect to server.
|
||||
ret = connect(try_socket, ptr->ai_addr, (int)ptr->ai_addrlen);
|
||||
if (ret == SOCKET_ERROR) {
|
||||
closesocket(try_socket);
|
||||
try_socket = INVALID_SOCKET;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
if (try_socket == INVALID_SOCKET) {
|
||||
XELOGE("Unable to connect to server");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Piggyback to setup the socket.
|
||||
return Accept(try_socket);
|
||||
}
|
||||
|
||||
bool Accept(SOCKET socket) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
|
||||
|
@ -142,6 +187,16 @@ class Win32Socket : public Socket {
|
|||
std::unique_ptr<xe::threading::Event> event_;
|
||||
};
|
||||
|
||||
std::unique_ptr<Socket> Socket::Connect(std::string hostname, uint16_t port) {
|
||||
InitializeWinsock();
|
||||
|
||||
auto socket = std::make_unique<Win32Socket>();
|
||||
if (!socket->Connect(std::move(hostname), port)) {
|
||||
return nullptr;
|
||||
}
|
||||
return std::unique_ptr<Socket>(socket.release());
|
||||
}
|
||||
|
||||
class Win32SocketServer : public SocketServer {
|
||||
public:
|
||||
Win32SocketServer(
|
||||
|
|
|
@ -104,17 +104,9 @@ ThreadState::ThreadState(Processor* processor, uint32_t thread_id,
|
|||
// Set initial registers.
|
||||
context_->r[1] = stack_base_;
|
||||
context_->r[13] = pcr_address_;
|
||||
|
||||
if (processor_->debugger()) {
|
||||
processor_->debugger()->OnThreadCreated(this);
|
||||
}
|
||||
}
|
||||
|
||||
ThreadState::~ThreadState() {
|
||||
if (processor_->debugger()) {
|
||||
processor_->debugger()->OnThreadDestroyed(this);
|
||||
}
|
||||
|
||||
if (backend_data_) {
|
||||
processor_->backend()->FreeThreadData(backend_data_);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/client/xdp/xdp_client.h"
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/debug/debugger.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace client {
|
||||
namespace xdp {
|
||||
|
||||
using namespace xe::debug::proto;
|
||||
|
||||
constexpr size_t kReceiveBufferSize = 1 * 1024 * 1024;
|
||||
constexpr size_t kTransmitBufferSize = 1 * 1024 * 1024;
|
||||
|
||||
XdpClient::XdpClient()
|
||||
: packet_reader_(kReceiveBufferSize), packet_writer_(kTransmitBufferSize) {
|
||||
receive_buffer_.resize(kReceiveBufferSize);
|
||||
}
|
||||
|
||||
XdpClient::~XdpClient() {
|
||||
socket_->Close();
|
||||
xe::threading::Wait(thread_.get(), true);
|
||||
thread_.reset();
|
||||
}
|
||||
|
||||
bool XdpClient::Connect(std::string hostname, uint16_t port) {
|
||||
socket_ = Socket::Connect(std::move(hostname), port);
|
||||
if (!socket_) {
|
||||
XELOGE("Unable to connect to remote host");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a thread to manage the connection.
|
||||
thread_ = xe::threading::Thread::Create({}, [this]() {
|
||||
// Main loop.
|
||||
bool running = true;
|
||||
while (running) {
|
||||
auto wait_result = xe::threading::Wait(socket_->wait_handle(), true);
|
||||
switch (wait_result) {
|
||||
case xe::threading::WaitResult::kSuccess:
|
||||
// Event (read or close).
|
||||
running = HandleSocketEvent();
|
||||
continue;
|
||||
case xe::threading::WaitResult::kAbandoned:
|
||||
case xe::threading::WaitResult::kFailed:
|
||||
// Error - kill the thread.
|
||||
running = false;
|
||||
continue;
|
||||
default:
|
||||
// Eh. Continue.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Kill socket (likely already disconnected).
|
||||
socket_.reset();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XdpClient::HandleSocketEvent() {
|
||||
if (!socket_->is_connected()) {
|
||||
// Known-disconnected.
|
||||
return false;
|
||||
}
|
||||
// Attempt to read into our buffer.
|
||||
size_t bytes_read =
|
||||
socket_->Receive(receive_buffer_.data(), receive_buffer_.capacity());
|
||||
if (bytes_read == -1) {
|
||||
// Disconnected.
|
||||
return false;
|
||||
} else if (bytes_read == 0) {
|
||||
// No data available. Wait again.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pass off the command processor to do with it what it wants.
|
||||
if (!ProcessBuffer(receive_buffer_.data(), bytes_read)) {
|
||||
// Error.
|
||||
XELOGE("Error processing incoming XDP command; forcing disconnect");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XdpClient::ProcessBuffer(const uint8_t* buffer, size_t buffer_length) {
|
||||
// Grow and append the bytes to the receive buffer.
|
||||
packet_reader_.AppendBuffer(buffer, buffer_length);
|
||||
|
||||
// Process all packets in the buffer.
|
||||
while (true) {
|
||||
auto packet = packet_reader_.Begin();
|
||||
if (!packet) {
|
||||
// Out of packets. Compact any unused space.
|
||||
packet_reader_.Compact();
|
||||
break;
|
||||
}
|
||||
|
||||
// Emit packet.
|
||||
if (!ProcessPacket(packet)) {
|
||||
// Failed to process packet.
|
||||
XELOGE("Failed to process incoming packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
packet_reader_.End();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XdpClient::ProcessPacket(const proto::Packet* packet) {
|
||||
// Hold lock during processing.
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
|
||||
switch (packet->packet_type) {
|
||||
case PacketType::kGenericResponse: {
|
||||
//
|
||||
} break;
|
||||
case PacketType::kExecutionNotification: {
|
||||
auto body = packet_reader_.Read<ExecutionNotification>();
|
||||
switch (body->current_state) {
|
||||
case ExecutionNotification::State::kStopped: {
|
||||
// body->stop_reason
|
||||
execution_state_ = ExecutionState::kStopped;
|
||||
listener_->OnExecutionStateChanged(execution_state_);
|
||||
BeginUpdateAllState();
|
||||
} break;
|
||||
case ExecutionNotification::State::kRunning: {
|
||||
execution_state_ = ExecutionState::kRunning;
|
||||
listener_->OnExecutionStateChanged(execution_state_);
|
||||
} break;
|
||||
case ExecutionNotification::State::kExited: {
|
||||
execution_state_ = ExecutionState::kExited;
|
||||
listener_->OnExecutionStateChanged(execution_state_);
|
||||
BeginUpdateAllState();
|
||||
} break;
|
||||
}
|
||||
} break;
|
||||
case PacketType::kModuleListResponse: {
|
||||
auto body = packet_reader_.Read<ModuleListResponse>();
|
||||
auto entries = packet_reader_.ReadArray<ModuleListEntry>(body->count);
|
||||
listener_->OnModulesUpdated(entries);
|
||||
} break;
|
||||
case PacketType::kThreadListResponse: {
|
||||
auto body = packet_reader_.Read<ThreadListResponse>();
|
||||
auto entries = packet_reader_.ReadArray<ThreadListEntry>(body->count);
|
||||
listener_->OnThreadsUpdated(entries);
|
||||
} break;
|
||||
default: {
|
||||
XELOGE("Unknown incoming packet type");
|
||||
return false;
|
||||
} break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void XdpClient::Flush() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
if (!packet_writer_.buffer_offset()) {
|
||||
return;
|
||||
}
|
||||
socket_->Send(packet_writer_.buffer(), packet_writer_.buffer_offset());
|
||||
packet_writer_.Reset();
|
||||
}
|
||||
|
||||
void XdpClient::Continue() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
packet_writer_.Begin(PacketType::kExecutionRequest);
|
||||
auto body = packet_writer_.Append<ExecutionRequest>();
|
||||
body->action = ExecutionRequest::Action::kContinue;
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpClient::StepOne(uint32_t thread_id) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
packet_writer_.Begin(PacketType::kExecutionRequest);
|
||||
auto body = packet_writer_.Append<ExecutionRequest>();
|
||||
body->action = ExecutionRequest::Action::kStep;
|
||||
body->step_mode = ExecutionRequest::StepMode::kStepOne;
|
||||
body->thread_id = thread_id;
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpClient::StepTo(uint32_t thread_id, uint32_t target_pc) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
packet_writer_.Begin(PacketType::kExecutionRequest);
|
||||
auto body = packet_writer_.Append<ExecutionRequest>();
|
||||
body->action = ExecutionRequest::Action::kStep;
|
||||
body->step_mode = ExecutionRequest::StepMode::kStepTo;
|
||||
body->thread_id = thread_id;
|
||||
body->target_pc = target_pc;
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpClient::Interrupt() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
packet_writer_.Begin(PacketType::kExecutionRequest);
|
||||
auto body = packet_writer_.Append<ExecutionRequest>();
|
||||
body->action = ExecutionRequest::Action::kInterrupt;
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpClient::Exit() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
packet_writer_.Begin(PacketType::kExecutionRequest);
|
||||
auto body = packet_writer_.Append<ExecutionRequest>();
|
||||
body->action = ExecutionRequest::Action::kExit;
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpClient::BeginUpdateAllState() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
|
||||
packet_writer_.Begin(PacketType::kModuleListRequest);
|
||||
packet_writer_.End();
|
||||
packet_writer_.Begin(PacketType::kThreadListRequest);
|
||||
packet_writer_.End();
|
||||
|
||||
Flush();
|
||||
}
|
||||
|
||||
} // namespace xdp
|
||||
} // namespace client
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_CLIENT_XDP_XDP_CLIENT_H_
|
||||
#define XENIA_DEBUG_CLIENT_XDP_XDP_CLIENT_H_
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "xenia/base/socket.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/debug/proto/packet_reader.h"
|
||||
#include "xenia/debug/proto/packet_writer.h"
|
||||
#include "xenia/debug/proto/xdp_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace client {
|
||||
namespace xdp {
|
||||
|
||||
using proto::ModuleListEntry;
|
||||
using proto::ThreadListEntry;
|
||||
|
||||
enum class ExecutionState {
|
||||
kRunning,
|
||||
kStopped,
|
||||
kExited,
|
||||
};
|
||||
|
||||
// NOTE: all callbacks are issued on the client thread and should be marshalled
|
||||
// to the UI thread. All other methods can be called from any thread.
|
||||
|
||||
class ClientListener {
|
||||
public:
|
||||
virtual void OnExecutionStateChanged(ExecutionState execution_state) = 0;
|
||||
virtual void OnModulesUpdated(
|
||||
std::vector<const ModuleListEntry*> entries) = 0;
|
||||
virtual void OnThreadsUpdated(
|
||||
std::vector<const ThreadListEntry*> entries) = 0;
|
||||
};
|
||||
|
||||
class XdpClient {
|
||||
public:
|
||||
XdpClient();
|
||||
~XdpClient();
|
||||
|
||||
Socket* socket() const { return socket_.get(); }
|
||||
void set_listener(ClientListener* listener) { listener_ = listener; }
|
||||
|
||||
ExecutionState execution_state() const { return execution_state_; }
|
||||
|
||||
bool Connect(std::string hostname, uint16_t port);
|
||||
|
||||
void Continue();
|
||||
void StepOne(uint32_t thread_id);
|
||||
void StepTo(uint32_t thread_id, uint32_t target_pc);
|
||||
void Interrupt();
|
||||
void Exit();
|
||||
|
||||
private:
|
||||
bool HandleSocketEvent();
|
||||
bool ProcessBuffer(const uint8_t* buffer, size_t buffer_length);
|
||||
bool ProcessPacket(const proto::Packet* packet);
|
||||
void Flush();
|
||||
|
||||
void BeginUpdateAllState();
|
||||
|
||||
std::recursive_mutex mutex_;
|
||||
std::unique_ptr<Socket> socket_;
|
||||
std::unique_ptr<xe::threading::Thread> thread_;
|
||||
std::vector<uint8_t> receive_buffer_;
|
||||
|
||||
proto::PacketReader packet_reader_;
|
||||
proto::PacketWriter packet_writer_;
|
||||
|
||||
ClientListener* listener_ = nullptr;
|
||||
|
||||
ExecutionState execution_state_ = ExecutionState::kStopped;
|
||||
};
|
||||
|
||||
} // namespace xdp
|
||||
} // namespace client
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_CLIENT_XDP_XDP_CLIENT_H_
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/client/xdp/xdp_command_processor.h"
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/string_buffer.h"
|
||||
#include "xenia/debug/client/xdp/xdp_client.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace client {
|
||||
namespace xdp {
|
||||
|
||||
using namespace xe::debug::proto;
|
||||
|
||||
constexpr size_t kReceiveBufferSize = 1 * 1024 * 1024;
|
||||
constexpr size_t kTransmitBufferSize = 1 * 1024 * 1024;
|
||||
|
||||
XdpCommandProcessor::XdpCommandProcessor(XdpClient* client)
|
||||
: client_(client),
|
||||
packet_reader_(kReceiveBufferSize),
|
||||
packet_writer_(kTransmitBufferSize) {}
|
||||
|
||||
} // namespace xdp
|
||||
} // namespace client
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_CLIENT_XDP_XDP_COMMAND_PROCESSOR_H_
|
||||
#define XENIA_DEBUG_CLIENT_XDP_XDP_COMMAND_PROCESSOR_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "xenia/debug/proto/packet_reader.h"
|
||||
#include "xenia/debug/proto/packet_writer.h"
|
||||
#include "xenia/debug/proto/xdp_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace client {
|
||||
namespace xdp {
|
||||
|
||||
class XdpClient;
|
||||
|
||||
class XdpCommandProcessor {
|
||||
public:
|
||||
XdpCommandProcessor(XdpClient* client);
|
||||
~XdpCommandProcessor() = default;
|
||||
|
||||
bool ProcessBuffer(const uint8_t* buffer, size_t buffer_length);
|
||||
|
||||
private:
|
||||
bool ProcessPacket(const proto::Packet* packet);
|
||||
|
||||
void Flush();
|
||||
|
||||
XdpClient* client_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace xdp
|
||||
} // namespace client
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_CLIENT_XDP_XDP_COMMAND_PROCESSOR_H_
|
|
@ -7,8 +7,8 @@
|
|||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_DEBUG_TRANSPORT_H_
|
||||
#define XENIA_DEBUG_TRANSPORT_H_
|
||||
#ifndef XENIA_DEBUG_DEBUG_SERVER_H_
|
||||
#define XENIA_DEBUG_DEBUG_SERVER_H_
|
||||
|
||||
#include "xenia/cpu/function.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
|
@ -19,25 +19,25 @@ namespace debug {
|
|||
|
||||
class Debugger;
|
||||
|
||||
class Transport {
|
||||
class DebugServer {
|
||||
public:
|
||||
virtual ~Transport() = default;
|
||||
virtual ~DebugServer() = default;
|
||||
|
||||
Debugger* debugger() const { return debugger_; }
|
||||
|
||||
virtual bool Initialize() = 0;
|
||||
|
||||
// break/resume state
|
||||
|
||||
// TODO(benvanik): better thread type (XThread?)
|
||||
// virtual void OnThreadCreated(ThreadState* thread_state) = 0;
|
||||
// virtual void OnThreadDestroyed(ThreadState* thread_state) = 0;
|
||||
|
||||
virtual void OnExecutionContinued() {}
|
||||
virtual void OnExecutionInterrupted() {}
|
||||
/*virtual void OnBreakpointHit(xe::cpu::ThreadState* thread_state,
|
||||
Breakpoint* breakpoint) = 0;*/
|
||||
|
||||
protected:
|
||||
Transport(Debugger* debugger) : debugger_(debugger) {}
|
||||
DebugServer(Debugger* debugger) : debugger_(debugger) {}
|
||||
|
||||
Debugger* debugger_ = nullptr;
|
||||
};
|
||||
|
@ -45,4 +45,4 @@ class Transport {
|
|||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_TRANSPORT_H_
|
||||
#endif // XENIA_DEBUG_DEBUG_SERVER_H_
|
|
@ -20,7 +20,9 @@
|
|||
#include "xenia/cpu/backend/code_cache.h"
|
||||
#include "xenia/cpu/function.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/debug/transport/gdb/gdb_transport.h"
|
||||
#include "xenia/debug/server/gdb/gdb_server.h"
|
||||
#include "xenia/debug/server/mi/mi_server.h"
|
||||
#include "xenia/debug/server/xdp/xdp_server.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/kernel/objects/xkernel_module.h"
|
||||
#include "xenia/kernel/objects/xmodule.h"
|
||||
|
@ -40,6 +42,8 @@ DEFINE_bool(wait_for_debugger, false,
|
|||
"Waits for a debugger to attach before starting the game.");
|
||||
DEFINE_bool(exit_with_debugger, true, "Exit whe the debugger disconnects.");
|
||||
|
||||
DEFINE_string(debug_server, "xdp", "Debug server protocol [gdb, mi, xdp].");
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
|
||||
|
@ -86,13 +90,24 @@ bool Debugger::StartSession() {
|
|||
functions_trace_file_ = ChunkedMappedMemoryWriter::Open(
|
||||
functions_trace_path_, 32 * 1024 * 1024, true);
|
||||
|
||||
// Add default transports.
|
||||
auto gdb_transport =
|
||||
std::make_unique<xe::debug::transport::gdb::GdbTransport>(this);
|
||||
if (gdb_transport->Initialize()) {
|
||||
transports_.emplace_back(std::move(gdb_transport));
|
||||
if (FLAGS_debug_server == "gdb") {
|
||||
server_ = std::make_unique<xe::debug::server::gdb::GdbServer>(this);
|
||||
if (!server_->Initialize()) {
|
||||
XELOGE("Unable to initialize GDB debug server");
|
||||
return false;
|
||||
}
|
||||
} else if (FLAGS_debug_server == "gdb") {
|
||||
server_ = std::make_unique<xe::debug::server::mi::MIServer>(this);
|
||||
if (!server_->Initialize()) {
|
||||
XELOGE("Unable to initialize MI debug server");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
XELOGE("Unable to initialize GDB debugger transport");
|
||||
server_ = std::make_unique<xe::debug::server::xdp::XdpServer>(this);
|
||||
if (!server_->Initialize()) {
|
||||
XELOGE("Unable to initialize XDP debug server");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -103,17 +118,24 @@ void Debugger::PreLaunch() {
|
|||
// Wait for the first client.
|
||||
XELOGI("Waiting for debugger because of --wait_for_debugger...");
|
||||
attach_fence_.Wait();
|
||||
XELOGI("Debugger attached, continuing...");
|
||||
}
|
||||
XELOGI("Debugger attached, breaking and waiting for continue...");
|
||||
|
||||
// TODO(benvanik): notify transports?
|
||||
// Start paused.
|
||||
execution_state_ = ExecutionState::kRunning;
|
||||
Interrupt();
|
||||
} else {
|
||||
// Start running.
|
||||
execution_state_ = ExecutionState::kRunning;
|
||||
}
|
||||
}
|
||||
|
||||
void Debugger::StopSession() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
|
||||
FlushSession();
|
||||
|
||||
// Kill all transports.
|
||||
transports_.clear();
|
||||
// Kill debug server.
|
||||
server_.reset();
|
||||
|
||||
functions_file_.reset();
|
||||
functions_trace_file_.reset();
|
||||
|
@ -142,6 +164,78 @@ uint8_t* Debugger::AllocateFunctionTraceData(size_t size) {
|
|||
return functions_trace_file_->Allocate(size);
|
||||
}
|
||||
|
||||
int Debugger::AddBreakpoint(Breakpoint* breakpoint) {
|
||||
// Add to breakpoints map.
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
breakpoints_.insert(
|
||||
std::pair<uint32_t, Breakpoint*>(breakpoint->address(), breakpoint));
|
||||
}
|
||||
|
||||
// Find all functions that contain the breakpoint address.
|
||||
auto fns =
|
||||
emulator_->processor()->FindFunctionsWithAddress(breakpoint->address());
|
||||
|
||||
// Add.
|
||||
for (auto fn : fns) {
|
||||
if (fn->AddBreakpoint(breakpoint)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Debugger::RemoveBreakpoint(Breakpoint* breakpoint) {
|
||||
// Remove from breakpoint map.
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
auto range = breakpoints_.equal_range(breakpoint->address());
|
||||
if (range.first == range.second) {
|
||||
return 1;
|
||||
}
|
||||
bool found = false;
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
if (it->second == breakpoint) {
|
||||
breakpoints_.erase(it);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Find all functions that have the breakpoint set.
|
||||
auto fns =
|
||||
emulator_->processor()->FindFunctionsWithAddress(breakpoint->address());
|
||||
|
||||
// Remove.
|
||||
for (auto fn : fns) {
|
||||
fn->RemoveBreakpoint(breakpoint);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Debugger::FindBreakpoints(uint32_t address,
|
||||
std::vector<Breakpoint*>& out_breakpoints) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
|
||||
out_breakpoints.clear();
|
||||
|
||||
auto range = breakpoints_.equal_range(address);
|
||||
if (range.first == range.second) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
Breakpoint* breakpoint = it->second;
|
||||
out_breakpoints.push_back(breakpoint);
|
||||
}
|
||||
}
|
||||
|
||||
bool Debugger::SuspendAllThreads() {
|
||||
auto threads =
|
||||
emulator_->kernel_state()->object_table()->GetObjectsByType<XThread>(
|
||||
|
@ -174,83 +268,43 @@ bool Debugger::ResumeAllThreads() {
|
|||
return true;
|
||||
}
|
||||
|
||||
int Debugger::AddBreakpoint(Breakpoint* breakpoint) {
|
||||
// Add to breakpoints map.
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(breakpoints_lock_);
|
||||
breakpoints_.insert(
|
||||
std::pair<uint32_t, Breakpoint*>(breakpoint->address(), breakpoint));
|
||||
}
|
||||
|
||||
// Find all functions that contain the breakpoint address.
|
||||
auto fns =
|
||||
emulator_->processor()->FindFunctionsWithAddress(breakpoint->address());
|
||||
|
||||
// Add.
|
||||
for (auto fn : fns) {
|
||||
if (fn->AddBreakpoint(breakpoint)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
void Debugger::Interrupt() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
assert_true(execution_state_ == ExecutionState::kRunning);
|
||||
SuspendAllThreads();
|
||||
execution_state_ = ExecutionState::kStopped;
|
||||
server_->OnExecutionInterrupted();
|
||||
}
|
||||
|
||||
int Debugger::RemoveBreakpoint(Breakpoint* breakpoint) {
|
||||
// Remove from breakpoint map.
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(breakpoints_lock_);
|
||||
auto range = breakpoints_.equal_range(breakpoint->address());
|
||||
if (range.first == range.second) {
|
||||
return 1;
|
||||
}
|
||||
bool found = false;
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
if (it->second == breakpoint) {
|
||||
breakpoints_.erase(it);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Find all functions that have the breakpoint set.
|
||||
auto fns =
|
||||
emulator_->processor()->FindFunctionsWithAddress(breakpoint->address());
|
||||
|
||||
// Remove.
|
||||
for (auto fn : fns) {
|
||||
fn->RemoveBreakpoint(breakpoint);
|
||||
}
|
||||
|
||||
return 0;
|
||||
void Debugger::Continue() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
assert_true(execution_state_ == ExecutionState::kStopped);
|
||||
ResumeAllThreads();
|
||||
execution_state_ = ExecutionState::kRunning;
|
||||
server_->OnExecutionContinued();
|
||||
}
|
||||
|
||||
void Debugger::FindBreakpoints(uint32_t address,
|
||||
std::vector<Breakpoint*>& out_breakpoints) {
|
||||
std::lock_guard<std::mutex> guard(breakpoints_lock_);
|
||||
|
||||
out_breakpoints.clear();
|
||||
|
||||
auto range = breakpoints_.equal_range(address);
|
||||
if (range.first == range.second) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
Breakpoint* breakpoint = it->second;
|
||||
out_breakpoints.push_back(breakpoint);
|
||||
}
|
||||
void Debugger::StepOne(uint32_t thread_id) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
assert_true(execution_state_ == ExecutionState::kStopped);
|
||||
//
|
||||
}
|
||||
|
||||
void Debugger::OnThreadCreated(ThreadState* thread_state) {
|
||||
void Debugger::StepTo(uint32_t thread_id, uint32_t target_pc) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
assert_true(execution_state_ == ExecutionState::kStopped);
|
||||
//
|
||||
}
|
||||
|
||||
void Debugger::OnThreadCreated(xe::kernel::XThread* thread) {
|
||||
// TODO(benvanik): notify transports.
|
||||
}
|
||||
|
||||
void Debugger::OnThreadDestroyed(ThreadState* thread_state) {
|
||||
void Debugger::OnThreadExit(xe::kernel::XThread* thread) {
|
||||
// TODO(benvanik): notify transports.
|
||||
}
|
||||
|
||||
void Debugger::OnThreadDestroyed(xe::kernel::XThread* thread) {
|
||||
// TODO(benvanik): notify transports.
|
||||
}
|
||||
|
||||
|
@ -259,7 +313,7 @@ void Debugger::OnFunctionDefined(cpu::FunctionInfo* symbol_info,
|
|||
// Man, I'd love not to take this lock.
|
||||
std::vector<Breakpoint*> breakpoints;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(breakpoints_lock_);
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
for (uint32_t address = symbol_info->address();
|
||||
address <= symbol_info->end_address(); address += 4) {
|
||||
auto range = breakpoints_.equal_range(address);
|
||||
|
@ -281,7 +335,7 @@ void Debugger::OnFunctionDefined(cpu::FunctionInfo* symbol_info,
|
|||
}
|
||||
}
|
||||
|
||||
void Debugger::OnBreakpointHit(ThreadState* thread_state,
|
||||
void Debugger::OnBreakpointHit(xe::kernel::XThread* thread,
|
||||
Breakpoint* breakpoint) {
|
||||
// Suspend all threads immediately.
|
||||
SuspendAllThreads();
|
||||
|
|
|
@ -29,12 +29,20 @@ DECLARE_bool(debug);
|
|||
|
||||
namespace xe {
|
||||
class Emulator;
|
||||
namespace kernel {
|
||||
class XThread;
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
|
||||
class Transport;
|
||||
class DebugServer;
|
||||
|
||||
enum class ExecutionState {
|
||||
kRunning,
|
||||
kStopped,
|
||||
};
|
||||
|
||||
class Debugger {
|
||||
public:
|
||||
|
@ -54,9 +62,7 @@ class Debugger {
|
|||
bool is_attached() const { return is_attached_; }
|
||||
void set_attached(bool attached);
|
||||
|
||||
bool SuspendAllThreads();
|
||||
bool ResumeThread(uint32_t thread_id);
|
||||
bool ResumeAllThreads();
|
||||
ExecutionState execution_state() const { return execution_state_; }
|
||||
|
||||
int AddBreakpoint(Breakpoint* breakpoint);
|
||||
int RemoveBreakpoint(Breakpoint* breakpoint);
|
||||
|
@ -66,19 +72,28 @@ class Debugger {
|
|||
// TODO(benvanik): utility functions for modification (make function ignored,
|
||||
// etc).
|
||||
|
||||
void OnThreadCreated(cpu::ThreadState* thread_state);
|
||||
void OnThreadDestroyed(cpu::ThreadState* thread_state);
|
||||
void Interrupt();
|
||||
void Continue();
|
||||
void StepOne(uint32_t thread_id);
|
||||
void StepTo(uint32_t thread_id, uint32_t target_pc);
|
||||
|
||||
void OnThreadCreated(xe::kernel::XThread* thread);
|
||||
void OnThreadExit(xe::kernel::XThread* thread);
|
||||
void OnThreadDestroyed(xe::kernel::XThread* thread);
|
||||
|
||||
void OnFunctionDefined(cpu::FunctionInfo* symbol_info,
|
||||
cpu::Function* function);
|
||||
|
||||
void OnBreakpointHit(cpu::ThreadState* thread_state, Breakpoint* breakpoint);
|
||||
void OnBreakpointHit(xe::kernel::XThread* thread, Breakpoint* breakpoint);
|
||||
|
||||
private:
|
||||
void OnMessage(std::vector<uint8_t> buffer);
|
||||
bool SuspendAllThreads();
|
||||
bool ResumeThread(uint32_t thread_id);
|
||||
bool ResumeAllThreads();
|
||||
|
||||
Emulator* emulator_ = nullptr;
|
||||
|
||||
std::vector<std::unique_ptr<Transport>> transports_;
|
||||
std::unique_ptr<DebugServer> server_;
|
||||
bool is_attached_ = false;
|
||||
xe::threading::Fence attach_fence_;
|
||||
|
||||
|
@ -87,7 +102,9 @@ class Debugger {
|
|||
std::wstring functions_trace_path_;
|
||||
std::unique_ptr<ChunkedMappedMemoryWriter> functions_trace_file_;
|
||||
|
||||
std::mutex breakpoints_lock_;
|
||||
std::recursive_mutex mutex_;
|
||||
ExecutionState execution_state_ = ExecutionState::kStopped;
|
||||
|
||||
std::multimap<uint32_t, Breakpoint*> breakpoints_;
|
||||
};
|
||||
|
||||
|
|
|
@ -16,4 +16,6 @@ project("xenia-debug")
|
|||
project_root.."/third_party/flatbuffers/include"
|
||||
})
|
||||
local_platform_files()
|
||||
recursive_platform_files("transport")
|
||||
recursive_platform_files("client")
|
||||
recursive_platform_files("proto")
|
||||
recursive_platform_files("server")
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_PROTO_PACKET_READER_H_
|
||||
#define XENIA_DEBUG_PROTO_PACKET_READER_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/assert.h"
|
||||
#include "xenia/base/math.h"
|
||||
#include "xenia/debug/proto/varint.h"
|
||||
#include "xenia/debug/proto/xdp_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace proto {
|
||||
|
||||
class PacketReader {
|
||||
public:
|
||||
PacketReader(size_t buffer_capacity) : buffer_(buffer_capacity) {}
|
||||
|
||||
const uint8_t* buffer() { return buffer_.data(); }
|
||||
size_t buffer_capacity() const { return buffer_.size(); }
|
||||
size_t buffer_offset() const { return buffer_offset_; }
|
||||
|
||||
void Reset() {
|
||||
buffer_offset_ = 0;
|
||||
buffer_size_ = 0;
|
||||
}
|
||||
|
||||
void AppendBuffer(const uint8_t* buffer, size_t buffer_length) {
|
||||
if (buffer_size_ + buffer_length > buffer_.size()) {
|
||||
size_t new_size =
|
||||
std::max(buffer_.size() * 2,
|
||||
xe::round_up(buffer_size_ + buffer_length, 16 * 1024));
|
||||
buffer_.resize(new_size);
|
||||
}
|
||||
std::memcpy(buffer_.data() + buffer_size_, buffer, buffer_length);
|
||||
buffer_size_ += buffer_length;
|
||||
}
|
||||
|
||||
void Compact() {
|
||||
assert_null(current_packet_);
|
||||
if (!buffer_offset_) {
|
||||
return;
|
||||
}
|
||||
std::memmove(buffer_.data(), buffer_.data() + buffer_offset_,
|
||||
buffer_size_ - buffer_offset_);
|
||||
buffer_size_ -= buffer_offset_;
|
||||
buffer_offset_ = 0;
|
||||
}
|
||||
|
||||
const Packet* Begin() {
|
||||
assert_null(current_packet_);
|
||||
size_t bytes_remaining = buffer_size_ - buffer_offset_;
|
||||
if (bytes_remaining < sizeof(Packet)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto packet =
|
||||
reinterpret_cast<const Packet*>(buffer_.data() + buffer_offset_);
|
||||
if (bytes_remaining < sizeof(Packet) + packet->body_length) {
|
||||
return nullptr;
|
||||
}
|
||||
packet_offset_ = buffer_offset_ + sizeof(Packet);
|
||||
buffer_offset_ += sizeof(Packet) + packet->body_length;
|
||||
current_packet_ = packet;
|
||||
return packet;
|
||||
}
|
||||
|
||||
void End() {
|
||||
assert_not_null(current_packet_);
|
||||
current_packet_ = nullptr;
|
||||
packet_offset_ = 0;
|
||||
}
|
||||
|
||||
const uint8_t* Read(size_t length) {
|
||||
// Can't read into next packet/off of end.
|
||||
assert_not_null(current_packet_);
|
||||
assert_true(packet_offset_ + length <= buffer_offset_);
|
||||
auto ptr = buffer_.data() + packet_offset_;
|
||||
packet_offset_ += length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const T* Read() {
|
||||
return reinterpret_cast<const T*>(Read(sizeof(T)));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::vector<const T*> ReadArray(size_t count) {
|
||||
std::vector<const T*> entries;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
entries.push_back(Read<T>());
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
void Read(void* buffer, size_t buffer_length) {
|
||||
assert_not_null(current_packet_);
|
||||
auto src = Read(buffer_length);
|
||||
std::memcpy(buffer, src, buffer_length);
|
||||
}
|
||||
|
||||
void ReadString(char* buffer, size_t buffer_length) {
|
||||
assert_not_null(current_packet_);
|
||||
varint_t length;
|
||||
auto src = length.Read(buffer_.data() + packet_offset_);
|
||||
assert_not_null(src);
|
||||
assert_true(length < buffer_length);
|
||||
assert_true(length.size() + length < (buffer_offset_ - packet_offset_));
|
||||
std::memcpy(buffer, src, length);
|
||||
packet_offset_ += length;
|
||||
}
|
||||
|
||||
std::string ReadString() {
|
||||
assert_not_null(current_packet_);
|
||||
varint_t length;
|
||||
auto src = length.Read(buffer_.data() + packet_offset_);
|
||||
assert_not_null(src);
|
||||
assert_true(length.size() + length < (buffer_offset_ - packet_offset_));
|
||||
std::string value;
|
||||
value.resize(length.value());
|
||||
std::memcpy(const_cast<char*>(value.data()), src, length);
|
||||
packet_offset_ += length;
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> buffer_;
|
||||
size_t buffer_offset_ = 0;
|
||||
size_t buffer_size_ = 0;
|
||||
|
||||
const Packet* current_packet_ = nullptr;
|
||||
size_t packet_offset_ = 0;
|
||||
};
|
||||
|
||||
} // namespace proto
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_PROTO_PACKET_READER_H_
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_PROTO_PACKET_WRITER_H_
|
||||
#define XENIA_DEBUG_PROTO_PACKET_WRITER_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/assert.h"
|
||||
#include "xenia/base/math.h"
|
||||
#include "xenia/debug/proto/varint.h"
|
||||
#include "xenia/debug/proto/xdp_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace proto {
|
||||
|
||||
class PacketWriter {
|
||||
public:
|
||||
PacketWriter(size_t buffer_capacity) : buffer_(buffer_capacity) {}
|
||||
|
||||
uint8_t* buffer() { return buffer_.data(); }
|
||||
size_t buffer_capacity() const { return buffer_.size(); }
|
||||
size_t buffer_offset() const { return buffer_offset_; }
|
||||
|
||||
void Reset() { buffer_offset_ = 0; }
|
||||
|
||||
void Begin(PacketType packet_type, request_id_t request_id = 0) {
|
||||
assert_null(current_packet_);
|
||||
current_packet_ = Append<Packet>();
|
||||
current_packet_->packet_type = packet_type;
|
||||
current_packet_->request_id = request_id;
|
||||
current_packet_->body_length = 0;
|
||||
}
|
||||
|
||||
void End() {
|
||||
assert_not_null(current_packet_);
|
||||
current_packet_->body_length =
|
||||
uint32_t((buffer_.data() + buffer_offset_) -
|
||||
reinterpret_cast<uint8_t*>(current_packet_) - sizeof(Packet));
|
||||
current_packet_ = nullptr;
|
||||
}
|
||||
|
||||
uint8_t* Append(size_t length) {
|
||||
if (buffer_offset_ + length > buffer_.size()) {
|
||||
size_t new_size = std::max(
|
||||
xe::round_up(buffer_offset_ + length, 16 * 1024), buffer_.size() * 2);
|
||||
buffer_.resize(new_size);
|
||||
}
|
||||
auto ptr = buffer_.data() + buffer_offset_;
|
||||
buffer_offset_ += length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* Append() {
|
||||
return reinterpret_cast<T*>(Append(sizeof(T)));
|
||||
}
|
||||
|
||||
void Append(const void* buffer, size_t buffer_length) {
|
||||
auto dest = Append(buffer_length);
|
||||
std::memcpy(dest, buffer, buffer_length);
|
||||
}
|
||||
|
||||
void AppendString(const char* value) {
|
||||
size_t value_length = std::strlen(value);
|
||||
varint_t length(value_length);
|
||||
auto dest = Append(length.size() + value_length);
|
||||
dest = length.Write(dest);
|
||||
std::memcpy(dest, value, value_length);
|
||||
}
|
||||
void AppendString(const std::string& value) {
|
||||
varint_t length(value.size());
|
||||
auto dest = Append(length.size() + value.size());
|
||||
dest = length.Write(dest);
|
||||
std::memcpy(dest, value.data(), value.size());
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> buffer_;
|
||||
size_t buffer_offset_ = 0;
|
||||
|
||||
Packet* current_packet_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace proto
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_PROTO_PACKET_WRITER_H_
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_PROTO_VARINT_H_
|
||||
#define XENIA_DEBUG_PROTO_VARINT_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace proto {
|
||||
|
||||
struct varint_t {
|
||||
varint_t() = default;
|
||||
varint_t(uint64_t value) : value_(value) {}
|
||||
varint_t(const varint_t& other) : value_(other.value_) {}
|
||||
|
||||
operator uint64_t() const { return value_; }
|
||||
uint64_t value() const { return value_; }
|
||||
|
||||
size_t size() const {
|
||||
uint64_t value = value_;
|
||||
size_t i = 0;
|
||||
do {
|
||||
value >>= 7;
|
||||
++i;
|
||||
} while (value >= 0x80);
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
const uint8_t* Read(const uint8_t* src) {
|
||||
if (!(src[0] & 0x80)) {
|
||||
value_ = src[0];
|
||||
return src + 1;
|
||||
}
|
||||
uint64_t r = src[0] & 0x7F;
|
||||
size_t i;
|
||||
for (i = 1; i < 10; ++i) {
|
||||
r |= (src[i] & 0x7F) << (7 * i);
|
||||
if (!(src[i] & 0x80)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
value_ = r;
|
||||
return src + i + 1;
|
||||
}
|
||||
|
||||
uint8_t* Write(uint8_t* dest) {
|
||||
uint64_t value = value_;
|
||||
size_t i = 0;
|
||||
do {
|
||||
dest[i] = uint8_t(value | 0x80);
|
||||
value >>= 7;
|
||||
++i;
|
||||
} while (value >= 0x80);
|
||||
dest[i] = uint8_t(value);
|
||||
return dest + i + 1;
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t value_ = 0;
|
||||
};
|
||||
|
||||
} // namespace proto
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_PROTO_VARINT_H_
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_PROTO_XDP_PROTOCOL_H_
|
||||
#define XENIA_DEBUG_PROTO_XDP_PROTOCOL_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace proto {
|
||||
|
||||
enum class PacketType : uint16_t {
|
||||
kUnknown = 0,
|
||||
kGenericResponse = 1,
|
||||
|
||||
kExecutionRequest = 10,
|
||||
kExecutionNotification = 11,
|
||||
|
||||
kModuleListRequest = 20,
|
||||
kModuleListResponse = 21,
|
||||
|
||||
kThreadListRequest = 30,
|
||||
kThreadListResponse = 31,
|
||||
};
|
||||
|
||||
using request_id_t = uint16_t;
|
||||
|
||||
struct Packet {
|
||||
PacketType packet_type;
|
||||
request_id_t request_id;
|
||||
uint32_t body_length;
|
||||
uint8_t* body() { return reinterpret_cast<uint8_t*>(this) + sizeof(this); }
|
||||
};
|
||||
static_assert(sizeof(Packet) == 8, "Fixed packet size");
|
||||
|
||||
// Generic response to requests (S->C).
|
||||
// Used for any request type that doesn't define its own response.
|
||||
struct GenericResponse {
|
||||
static const PacketType type = PacketType::kGenericResponse;
|
||||
|
||||
enum class Code : uint32_t {
|
||||
// Command was received and successfully acted upon.
|
||||
// If the request is handled asynchronously this only indicates that the
|
||||
// command was recognized.
|
||||
kSuccess = 0,
|
||||
// Command was invalid or could not be executed.
|
||||
// A string will follow this response containing an error message.
|
||||
kError = 1,
|
||||
};
|
||||
|
||||
Code code;
|
||||
|
||||
bool is_success() const { return code == Code::kSuccess; }
|
||||
};
|
||||
|
||||
// Requests execution state changes (C->S).
|
||||
// Response is a GenericResponse, and as execution changes one or more
|
||||
// ExecutionNotification packets will be sent S->C.
|
||||
struct ExecutionRequest {
|
||||
static const PacketType type = PacketType::kExecutionRequest;
|
||||
|
||||
enum class Action : uint32_t {
|
||||
// Continues execution until the next breakpoint.
|
||||
kContinue,
|
||||
// Steps execution by the given step_mode.
|
||||
kStep,
|
||||
// Interrupts execution and breaks.
|
||||
kInterrupt,
|
||||
// Stops execution and causes the server to exit.
|
||||
kExit,
|
||||
};
|
||||
|
||||
enum class StepMode : uint32_t {
|
||||
// Steps a single statement, following branches.
|
||||
kStepOne,
|
||||
// Runs until the given target_pc.
|
||||
kStepTo,
|
||||
};
|
||||
|
||||
Action action;
|
||||
StepMode step_mode;
|
||||
uint32_t thread_id;
|
||||
uint32_t target_pc;
|
||||
};
|
||||
|
||||
// Notifies clients of changes in execution state (S->C).
|
||||
// This is fired asynchronously from any request and possibly due to events
|
||||
// external to the client requested ones.
|
||||
// After stopping clients should query execution state.
|
||||
struct ExecutionNotification {
|
||||
static const PacketType type = PacketType::kExecutionNotification;
|
||||
|
||||
enum class State : uint32_t {
|
||||
// Execution has stopped for stop_reason.
|
||||
kStopped,
|
||||
// Execution has resumed and the target is now running.
|
||||
kRunning,
|
||||
// The target has exited.
|
||||
kExited,
|
||||
};
|
||||
|
||||
enum class Reason : uint32_t {
|
||||
// Stopped because the client requested it with Action::kInterrupt.
|
||||
kInterrupt,
|
||||
// Stopped due to a completed step.
|
||||
kStep,
|
||||
// Stopped at a breakpoint.
|
||||
kBreakpoint,
|
||||
// TODO(benvanik): others? catches? library loads? etc?
|
||||
};
|
||||
|
||||
State current_state;
|
||||
Reason stop_reason;
|
||||
};
|
||||
|
||||
struct ModuleListRequest {
|
||||
static const PacketType type = PacketType::kModuleListRequest;
|
||||
};
|
||||
struct ModuleListResponse {
|
||||
static const PacketType type = PacketType::kModuleListResponse;
|
||||
|
||||
uint32_t count;
|
||||
// ModuleListEntry[count]
|
||||
};
|
||||
struct ModuleListEntry {
|
||||
uint32_t module_handle;
|
||||
uint32_t module_ptr;
|
||||
bool is_kernel_module;
|
||||
char path[256];
|
||||
char name[256];
|
||||
};
|
||||
|
||||
struct ThreadListRequest {
|
||||
static const PacketType type = PacketType::kThreadListRequest;
|
||||
};
|
||||
struct ThreadListResponse {
|
||||
static const PacketType type = PacketType::kThreadListResponse;
|
||||
|
||||
uint32_t count;
|
||||
// ThreadListEntry[count]
|
||||
};
|
||||
struct ThreadListEntry {
|
||||
uint32_t thread_handle;
|
||||
uint32_t thread_id;
|
||||
bool is_host_thread;
|
||||
char name[64];
|
||||
uint32_t exit_code;
|
||||
int32_t priority;
|
||||
uint32_t affinity;
|
||||
uint32_t xapi_thread_startup;
|
||||
uint32_t start_address;
|
||||
uint32_t start_context;
|
||||
uint32_t creation_flags;
|
||||
uint32_t stack_address;
|
||||
uint32_t stack_size;
|
||||
uint32_t stack_base;
|
||||
uint32_t stack_limit;
|
||||
uint32_t pcr_address;
|
||||
uint32_t tls_address;
|
||||
};
|
||||
|
||||
} // namespace proto
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_PROTO_XDP_PROTOCOL_H_
|
|
@ -0,0 +1,44 @@
|
|||
XDP: Xenia Debug Protocol
|
||||
--------------------------
|
||||
|
||||
## Overview
|
||||
|
||||
### General
|
||||
|
||||
* Packets flow in both directions (client and server).
|
||||
* Each packet is length-prefixed (exclusive) and tagged with a type.
|
||||
* The entire protocol is fully asynchronous, and supports both request/response
|
||||
style RPCs as well as notifications.
|
||||
* Request/response RPCs are matched on ID and multiple are allowed to be in
|
||||
flight at a time.
|
||||
* Notifications may come in any order.
|
||||
|
||||
### Server
|
||||
|
||||
The debug server is single threaded and processes commands as a FIFO. This
|
||||
means that all requests will be responded to in the order they are received.
|
||||
|
||||
### Client
|
||||
|
||||
TODO
|
||||
|
||||
## Packet Structure
|
||||
|
||||
Each packet consists of a length, type, and request ID. The length denotes
|
||||
the total number of bytes within the packet body, excluding the packet header.
|
||||
The request ID is an opaque value sent by the client when making a request and
|
||||
the server returns whatever the ID is when it responds to that request. The ID
|
||||
is unused for notifications.
|
||||
|
||||
```
|
||||
struct Packet {
|
||||
uint16_t packet_type;
|
||||
uint16_t request_id;
|
||||
uint32_t body_length;
|
||||
uint8_t body[body_length];
|
||||
};
|
||||
```
|
||||
|
||||
## Packets
|
||||
|
||||
TODO(benvanik): document the various types, etc.
|
|
@ -0,0 +1,319 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/server/gdb/gdb_command_processor.h"
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/string_buffer.h"
|
||||
#include "xenia/debug/server/gdb/gdb_server.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace gdb {
|
||||
|
||||
constexpr size_t kReceiveBufferSize = 1 * 1024 * 1024;
|
||||
constexpr size_t kTransmitBufferSize = 1 * 1024 * 1024;
|
||||
|
||||
GdbCommandProcessor::GdbCommandProcessor(GdbServer* server, Debugger* debugger)
|
||||
: server_(server), debugger_(debugger) {
|
||||
receive_buffer_.resize(kReceiveBufferSize);
|
||||
transmit_buffer_.resize(kTransmitBufferSize);
|
||||
}
|
||||
|
||||
bool GdbCommandProcessor::ProcessBuffer(const uint8_t* buffer,
|
||||
size_t buffer_length) {
|
||||
// Grow and append the bytes to the receive buffer.
|
||||
while (receive_offset_ + buffer_length > receive_buffer_.capacity()) {
|
||||
receive_buffer_.resize(receive_buffer_.size() * 2);
|
||||
}
|
||||
std::memcpy(receive_buffer_.data() + receive_offset_, buffer, buffer_length);
|
||||
receive_offset_ += buffer_length;
|
||||
|
||||
// While there are bytes pending in the buffer we scan through looking for end
|
||||
// markers. When we find one, we emit the packet and move on.
|
||||
size_t process_offset = 0;
|
||||
while (process_offset < receive_offset_) {
|
||||
// Look for an end marker.
|
||||
size_t end_offset = -1;
|
||||
size_t buffer_end_offset = -1;
|
||||
for (size_t i = process_offset; i < receive_offset_; ++i) {
|
||||
if (receive_buffer_[i] == '#') {
|
||||
end_offset = i - 1;
|
||||
buffer_end_offset = i + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (end_offset == -1) {
|
||||
// No end marker found - break out for now.
|
||||
break;
|
||||
}
|
||||
|
||||
// Emit packet.
|
||||
if (!ProcessPacket((const char*)(receive_buffer_.data() + process_offset),
|
||||
end_offset - process_offset)) {
|
||||
// Failed to process line.
|
||||
std::string line(receive_buffer_.data() + process_offset,
|
||||
receive_buffer_.data() + end_offset);
|
||||
XELOGE("Failed to process incoming GDB line: %s", line.c_str());
|
||||
return false;
|
||||
}
|
||||
process_offset = buffer_end_offset + 1;
|
||||
}
|
||||
|
||||
// If we have leftover unprocessed bytes move them to the front of the buffer.
|
||||
if (process_offset < receive_offset_) {
|
||||
size_t remaining_bytes = receive_offset_ - process_offset;
|
||||
std::memmove(receive_buffer_.data(),
|
||||
receive_buffer_.data() + process_offset, remaining_bytes);
|
||||
receive_offset_ = remaining_bytes;
|
||||
} else {
|
||||
receive_offset_ = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GdbCommandProcessor::ProcessPacket(const char* buffer,
|
||||
size_t buffer_length) {
|
||||
// There may be any number of leading +'s or -'s.
|
||||
size_t offset = 0;
|
||||
for (; offset < buffer_length; ++offset) {
|
||||
if (buffer[offset] == '$') {
|
||||
// Command start.
|
||||
++offset;
|
||||
break;
|
||||
} else if (buffer[offset] == '+') {
|
||||
// Ack.
|
||||
} else if (buffer[offset] == '-') {
|
||||
// No good - means transmission error.
|
||||
XELOGE("GDB client remorted transmission error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string line((const char*)(buffer + offset), buffer_length - 1);
|
||||
XELOGI("GDB -> %s", line.c_str());
|
||||
|
||||
// Immediately send ACK.
|
||||
if (!no_ack_mode_) {
|
||||
server_->client()->Send("+", 1);
|
||||
}
|
||||
|
||||
const char* buffer_ptr = buffer + offset;
|
||||
bool handled = false;
|
||||
switch (buffer[offset]) {
|
||||
case '!':
|
||||
// Enable extended mode.
|
||||
SendResponse("OK");
|
||||
handled = true;
|
||||
break;
|
||||
case 'v':
|
||||
// Verbose packets.
|
||||
if (std::strstr(buffer_ptr, "vRun") == buffer_ptr) {
|
||||
SendResponse("S05");
|
||||
handled = true;
|
||||
} else if (std::strstr(buffer_ptr, "vCont") == buffer_ptr) {
|
||||
SendResponse("S05");
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case 'q':
|
||||
// Query packets.
|
||||
switch (buffer_ptr[1]) {
|
||||
case 'C':
|
||||
// Get current thread ID.
|
||||
SendResponse("QC01");
|
||||
handled = true;
|
||||
break;
|
||||
case 'R':
|
||||
// Command.
|
||||
SendResponse("OK");
|
||||
handled = true;
|
||||
break;
|
||||
default:
|
||||
if (std::strstr(buffer_ptr, "qSupported") == buffer_ptr) {
|
||||
// Get/set feature support.
|
||||
handled = Process_qSupported(line);
|
||||
} else if (std::strstr(buffer_ptr, "qAttached") == buffer_ptr) {
|
||||
// Check attach mode; 0 = new process, 1 = existing process.
|
||||
SendResponse("0");
|
||||
handled = true;
|
||||
} else if (std::strstr(buffer_ptr, "qfThreadInfo") == buffer_ptr) {
|
||||
// Start of thread list request.
|
||||
SendResponse("m01");
|
||||
handled = true;
|
||||
} else if (std::strstr(buffer_ptr, "qsThreadInfo") == buffer_ptr) {
|
||||
// Continuation of thread list request.
|
||||
SendResponse("l"); // l = last.
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'Q':
|
||||
// Set packets.
|
||||
switch (buffer_ptr[1]) {
|
||||
default:
|
||||
if (std::strstr(buffer_ptr, "QStartNoAckMode") == buffer_ptr) {
|
||||
no_ack_mode_ = true;
|
||||
SendResponse("OK");
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'H':
|
||||
// Set thread for subsequent operations.
|
||||
SendResponse("OK");
|
||||
handled = true;
|
||||
break;
|
||||
case 'g':
|
||||
// Read all registers.
|
||||
handled = ProcessReadRegisters(line);
|
||||
break;
|
||||
case 'G':
|
||||
// Write all registers.
|
||||
handled = true;
|
||||
break;
|
||||
case 'p':
|
||||
// Read register.
|
||||
handled = true;
|
||||
break;
|
||||
case 'P':
|
||||
// Write register.
|
||||
handled = true;
|
||||
break;
|
||||
case 'm':
|
||||
// Read memory.
|
||||
handled = true;
|
||||
break;
|
||||
case 'M':
|
||||
// Write memory.
|
||||
handled = true;
|
||||
break;
|
||||
case 'Z':
|
||||
// Insert breakpoint.
|
||||
handled = true;
|
||||
break;
|
||||
case 'z':
|
||||
// Remove breakpoint.
|
||||
handled = true;
|
||||
break;
|
||||
case '?':
|
||||
// Query halt reason.
|
||||
SendResponse("S05");
|
||||
handled = true;
|
||||
break;
|
||||
case 'c':
|
||||
// Continue (vCont should be used instead).
|
||||
// NOTE: reply is sent on halt, not right now.
|
||||
SendResponse("S05");
|
||||
handled = true;
|
||||
break;
|
||||
case 's':
|
||||
// Single step.
|
||||
// NOTE: reply is sent on halt, not right now.
|
||||
SendResponse("S05");
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
if (!handled) {
|
||||
XELOGE("Unknown GDB packet: %s", buffer_ptr);
|
||||
SendResponse("");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GdbCommandProcessor::SendResponse(const char* value, size_t length) {
|
||||
XELOGI("GDB <- %s", value);
|
||||
uint8_t checksum = 0;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
uint8_t c = uint8_t(value[i]);
|
||||
if (!c) {
|
||||
break;
|
||||
}
|
||||
checksum += c;
|
||||
}
|
||||
char crc[4];
|
||||
sprintf(crc, "#%.2X", checksum);
|
||||
std::pair<const void*, size_t> buffers[] = {
|
||||
{"$", 1}, {value, length}, {crc, 3},
|
||||
};
|
||||
server_->client()->Send(buffers, xe::countof(buffers));
|
||||
}
|
||||
|
||||
bool GdbCommandProcessor::Process_qSupported(const std::string& line) {
|
||||
StringBuffer response;
|
||||
|
||||
// Read in the features the client supports.
|
||||
// qSupported[:gdbfeature[;gdbfeature]...]
|
||||
size_t feature_offset = line.find(':');
|
||||
while (feature_offset != std::string::npos) {
|
||||
size_t next_offset = line.find(';', feature_offset + 1);
|
||||
std::string feature =
|
||||
line.substr(feature_offset + 1, next_offset - feature_offset - 1);
|
||||
feature_offset = next_offset;
|
||||
if (feature.find("multiprocess") == 0) {
|
||||
bool is_supported = feature[12] == '+';
|
||||
} else if (feature.find("xmlRegisters") == 0) {
|
||||
// xmlRegisters=A,B,C
|
||||
} else if (feature.find("qRelocInsn") == 0) {
|
||||
bool is_supported = feature[10] == '+';
|
||||
} else if (feature.find("swbreak") == 0) {
|
||||
bool is_supported = feature[7] == '+';
|
||||
} else if (feature.find("hwbreak") == 0) {
|
||||
bool is_supported = feature[7] == '+';
|
||||
} else {
|
||||
XELOGW("Unknown GDB client support feature: %s", feature.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
response.Append("PacketSize=4000;");
|
||||
response.Append("QStartNoAckMode+;");
|
||||
response.Append("qRelocInsn-;");
|
||||
response.Append("multiprocess-;");
|
||||
response.Append("ConditionalBreakpoints-;");
|
||||
response.Append("ConditionalTracepoints-;");
|
||||
response.Append("ReverseContinue-;");
|
||||
response.Append("ReverseStep-;");
|
||||
response.Append("swbreak+;");
|
||||
response.Append("hwbreak+;");
|
||||
|
||||
SendResponse(response.GetString());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GdbCommandProcessor::ProcessReadRegisters(const std::string& line) {
|
||||
StringBuffer response;
|
||||
|
||||
for (int32_t n = 0; n < 32; n++) {
|
||||
// gpr
|
||||
response.AppendFormat("%08X", n);
|
||||
}
|
||||
for (int64_t n = 0; n < 32; n++) {
|
||||
// fpr
|
||||
response.AppendFormat("%016llX", n);
|
||||
}
|
||||
response.AppendFormat("%08X", 0x8202FB40); // pc
|
||||
response.AppendFormat("%08X", 65); // msr
|
||||
response.AppendFormat("%08X", 66); // cr
|
||||
response.AppendFormat("%08X", 67); // lr
|
||||
response.AppendFormat("%08X", 68); // ctr
|
||||
response.AppendFormat("%08X", 69); // xer
|
||||
response.AppendFormat("%08X", 70); // fpscr
|
||||
|
||||
SendResponse(response.GetString());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gdb
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_SERVER_GDB_GDB_COMMAND_PROCESSOR_H_
|
||||
#define XENIA_DEBUG_SERVER_GDB_GDB_COMMAND_PROCESSOR_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/debug/debugger.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace gdb {
|
||||
|
||||
class GdbServer;
|
||||
|
||||
class GdbCommandProcessor {
|
||||
public:
|
||||
GdbCommandProcessor(GdbServer* server, Debugger* debugger);
|
||||
~GdbCommandProcessor() = default;
|
||||
|
||||
bool ProcessBuffer(const uint8_t* buffer, size_t buffer_length);
|
||||
|
||||
private:
|
||||
bool ProcessPacket(const char* buffer, size_t buffer_length);
|
||||
void SendResponse(const char* value, size_t length);
|
||||
void SendResponse(const char* value) {
|
||||
SendResponse(value, std::strlen(value));
|
||||
}
|
||||
void SendResponse(std::string value) {
|
||||
SendResponse(value.data(), value.size());
|
||||
}
|
||||
|
||||
bool Process_qSupported(const std::string& line);
|
||||
bool ProcessReadRegisters(const std::string& line);
|
||||
|
||||
GdbServer* server_ = nullptr;
|
||||
Debugger* debugger_ = nullptr;
|
||||
std::vector<uint8_t> receive_buffer_;
|
||||
size_t receive_offset_ = 0;
|
||||
std::vector<uint8_t> transmit_buffer_;
|
||||
|
||||
bool no_ack_mode_ = false;
|
||||
};
|
||||
|
||||
} // namespace gdb
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_SERVER_GDB_GDB_COMMAND_PROCESSOR_H_
|
|
@ -7,30 +7,32 @@
|
|||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/transport/gdb/gdb_transport.h"
|
||||
#include "xenia/debug/server/gdb/gdb_server.h"
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/debug/debugger.h"
|
||||
#include "xenia/debug/server/gdb/gdb_command_processor.h"
|
||||
|
||||
DEFINE_int32(gdb_port, 9000, "Debugger gdbserver TCP port.");
|
||||
DEFINE_int32(gdb_server_port, 9000, "Debugger gdbserver TCP port.");
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace transport {
|
||||
namespace server {
|
||||
namespace gdb {
|
||||
|
||||
constexpr size_t kReceiveBufferSize = 2 * 1024 * 1024;
|
||||
constexpr size_t kReceiveBufferSize = 32 * 1024;
|
||||
|
||||
GdbTransport::GdbTransport(Debugger* debugger) : Transport(debugger) {
|
||||
GdbServer::GdbServer(Debugger* debugger) : DebugServer(debugger) {
|
||||
receive_buffer_.resize(kReceiveBufferSize);
|
||||
command_processor_ = std::make_unique<GdbCommandProcessor>(this, debugger);
|
||||
}
|
||||
|
||||
GdbTransport::~GdbTransport() = default;
|
||||
GdbServer::~GdbServer() = default;
|
||||
|
||||
bool GdbTransport::Initialize() {
|
||||
socket_server_ = SocketServer::Create(uint16_t(FLAGS_gdb_port),
|
||||
bool GdbServer::Initialize() {
|
||||
socket_server_ = SocketServer::Create(uint16_t(FLAGS_gdb_server_port),
|
||||
[this](std::unique_ptr<Socket> client) {
|
||||
AcceptClient(std::move(client));
|
||||
});
|
||||
|
@ -42,7 +44,7 @@ bool GdbTransport::Initialize() {
|
|||
return true;
|
||||
}
|
||||
|
||||
void GdbTransport::AcceptClient(std::unique_ptr<Socket> client) {
|
||||
void GdbServer::AcceptClient(std::unique_ptr<Socket> client) {
|
||||
// If we have an existing client, kill it and join its thread.
|
||||
if (client_) {
|
||||
// TODO(benvanik): GDB say goodbye?
|
||||
|
@ -63,6 +65,9 @@ void GdbTransport::AcceptClient(std::unique_ptr<Socket> client) {
|
|||
// Let the debugger know we are present.
|
||||
debugger_->set_attached(true);
|
||||
|
||||
// Junk just to poke the remote client.
|
||||
client_->Send("(gdb)\n");
|
||||
|
||||
// Main loop.
|
||||
bool running = true;
|
||||
while (running) {
|
||||
|
@ -91,7 +96,7 @@ void GdbTransport::AcceptClient(std::unique_ptr<Socket> client) {
|
|||
});
|
||||
}
|
||||
|
||||
bool GdbTransport::HandleClientEvent() {
|
||||
bool GdbServer::HandleClientEvent() {
|
||||
if (!client_->is_connected()) {
|
||||
// Known-disconnected.
|
||||
return false;
|
||||
|
@ -107,12 +112,17 @@ bool GdbTransport::HandleClientEvent() {
|
|||
return true;
|
||||
}
|
||||
|
||||
// TODO(benvanik): process incoming command.
|
||||
// Pass off the command processor to do with it what it wants.
|
||||
if (!command_processor_->ProcessBuffer(receive_buffer_.data(), bytes_read)) {
|
||||
// Error.
|
||||
XELOGE("Error processing incoming GDB command; forcing disconnect");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gdb
|
||||
} // namespace transport
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -7,24 +7,28 @@
|
|||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_DEBUG_TRANSPORT_GDB_GDB_TRANSPORT_H_
|
||||
#define XENIA_DEBUG_TRANSPORT_GDB_GDB_TRANSPORT_H_
|
||||
#ifndef XENIA_DEBUG_SERVER_GDB_GDB_SERVER_H_
|
||||
#define XENIA_DEBUG_SERVER_GDB_GDB_SERVER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "xenia/base/socket.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/debug/transport.h"
|
||||
#include "xenia/debug/debug_server.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace transport {
|
||||
namespace server {
|
||||
namespace gdb {
|
||||
|
||||
class GdbTransport : public Transport {
|
||||
class GdbCommandProcessor;
|
||||
|
||||
class GdbServer : public DebugServer {
|
||||
public:
|
||||
GdbTransport(Debugger* debugger);
|
||||
~GdbTransport() override;
|
||||
GdbServer(Debugger* debugger);
|
||||
~GdbServer() override;
|
||||
|
||||
Socket* client() const { return client_.get(); }
|
||||
|
||||
bool Initialize() override;
|
||||
|
||||
|
@ -37,11 +41,13 @@ class GdbTransport : public Transport {
|
|||
std::unique_ptr<Socket> client_;
|
||||
std::unique_ptr<xe::threading::Thread> client_thread_;
|
||||
std::vector<uint8_t> receive_buffer_;
|
||||
|
||||
std::unique_ptr<GdbCommandProcessor> command_processor_;
|
||||
};
|
||||
|
||||
} // namespace gdb
|
||||
} // namespace transport
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_TRANSPORT_GDB_GDB_TRANSPORT_H_
|
||||
#endif // XENIA_DEBUG_SERVER_GDB_GDB_SERVER_H_
|
|
@ -0,0 +1,232 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/server/mi/mi_command_processor.h"
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/string_buffer.h"
|
||||
#include "xenia/debug/server/mi/mi_reader.h"
|
||||
#include "xenia/debug/server/mi/mi_server.h"
|
||||
#include "xenia/debug/server/mi/mi_writer.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
constexpr size_t kReceiveBufferSize = 1 * 1024 * 1024;
|
||||
constexpr size_t kTransmitBufferSize = 1 * 1024 * 1024;
|
||||
|
||||
MICommandProcessor::MICommandProcessor(MIServer* server, Debugger* debugger)
|
||||
: server_(server), debugger_(debugger) {
|
||||
receive_buffer_.resize(kReceiveBufferSize);
|
||||
transmit_buffer_.resize(kTransmitBufferSize);
|
||||
}
|
||||
|
||||
bool MICommandProcessor::ProcessBuffer(const uint8_t* buffer,
|
||||
size_t buffer_length) {
|
||||
// Grow and append the bytes to the receive buffer.
|
||||
while (receive_offset_ + buffer_length > receive_buffer_.capacity()) {
|
||||
receive_buffer_.resize(receive_buffer_.size() * 2);
|
||||
}
|
||||
std::memcpy(receive_buffer_.data() + receive_offset_, buffer, buffer_length);
|
||||
receive_offset_ += buffer_length;
|
||||
|
||||
// While there are bytes pending in the buffer we scan through looking for end
|
||||
// markers. When we find one, we emit the packet and move on.
|
||||
size_t process_offset = 0;
|
||||
while (process_offset < receive_offset_) {
|
||||
// Look for an end marker.
|
||||
size_t end_offset = -1;
|
||||
size_t buffer_end_offset = -1;
|
||||
for (size_t i = process_offset; i < receive_offset_; ++i) {
|
||||
if (receive_buffer_[i] == '\r') {
|
||||
end_offset = i - 1;
|
||||
buffer_end_offset = i + 1;
|
||||
break;
|
||||
} else if (receive_buffer_[i] == '\n') {
|
||||
end_offset = i - 1;
|
||||
buffer_end_offset = i;
|
||||
}
|
||||
}
|
||||
if (end_offset == -1) {
|
||||
// No end marker found - break out for now.
|
||||
break;
|
||||
}
|
||||
|
||||
// Emit packet.
|
||||
if (!ProcessPacket((const char*)(receive_buffer_.data() + process_offset),
|
||||
end_offset - process_offset)) {
|
||||
// Failed to process line.
|
||||
std::string line(receive_buffer_.data() + process_offset,
|
||||
receive_buffer_.data() + end_offset);
|
||||
XELOGE("Failed to process incoming MI line: %s", line.c_str());
|
||||
return false;
|
||||
}
|
||||
process_offset = buffer_end_offset + 1;
|
||||
}
|
||||
|
||||
// If we have leftover unprocessed bytes move them to the front of the buffer.
|
||||
if (process_offset < receive_offset_) {
|
||||
size_t remaining_bytes = receive_offset_ - process_offset;
|
||||
std::memmove(receive_buffer_.data(),
|
||||
receive_buffer_.data() + process_offset, remaining_bytes);
|
||||
receive_offset_ = remaining_bytes;
|
||||
} else {
|
||||
receive_offset_ = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MICommandProcessor::ProcessPacket(const char* buffer,
|
||||
size_t buffer_length) {
|
||||
// Will likely have a request prefix, like '#####-....'.
|
||||
auto buffer_ptr = std::strchr(buffer, '-');
|
||||
if (!buffer_ptr) {
|
||||
XELOGE("Malformed MI command");
|
||||
return false;
|
||||
}
|
||||
std::string token;
|
||||
if (buffer_ptr != buffer) {
|
||||
token = std::string(buffer, buffer_ptr);
|
||||
}
|
||||
++buffer_ptr; // Skip leading '-'.
|
||||
|
||||
std::string line((const char*)buffer_ptr,
|
||||
buffer_length - (buffer_ptr - buffer) + 1);
|
||||
XELOGI("MI -> %s", line.c_str());
|
||||
|
||||
auto command = line.substr(0, line.find(' '));
|
||||
auto args =
|
||||
command.size() == line.size() ? "" : line.substr(command.size() + 1);
|
||||
|
||||
bool handled = false;
|
||||
if (command == "gdb-set") {
|
||||
auto key = args.substr(0, args.find(' '));
|
||||
auto value = args.substr(args.find(' ') + 1);
|
||||
if (key == "mi-async" || key == "target-async") {
|
||||
bool is_enabled = value == "1" || value == "on";
|
||||
SendResult(token, ResultClass::kDone);
|
||||
handled = true;
|
||||
} else if (key == "stop-on-solib-events") {
|
||||
bool is_enabled = value == "1" || value == "on";
|
||||
SendResult(token, ResultClass::kDone);
|
||||
handled = true;
|
||||
}
|
||||
} else if (command == "file-exec-and-symbols") {
|
||||
// args=foo
|
||||
SendResult(token, ResultClass::kDone);
|
||||
handled = true;
|
||||
} else if (command == "break-insert") {
|
||||
// args=main
|
||||
SendResult(token, ResultClass::kDone);
|
||||
handled = true;
|
||||
} else if (command == "exec-run") {
|
||||
exec_token_ = token;
|
||||
SendResult(token, ResultClass::kRunning);
|
||||
handled = true;
|
||||
|
||||
MIWriter writer(transmit_buffer_.data(), transmit_buffer_.size());
|
||||
writer.AppendRaw(exec_token_);
|
||||
writer.BeginAsyncNotifyRecord("library-loaded");
|
||||
writer.AppendString("id=123");
|
||||
writer.AppendString("target-name=\"foo.xex\"");
|
||||
writer.AppendString("host-name=\"foo.xex\"");
|
||||
writer.EndRecord();
|
||||
server_->client()->Send(transmit_buffer_.data(), writer.buffer_offset());
|
||||
} else if (command == "exec-continue") {
|
||||
exec_token_ = token;
|
||||
SendResult(token, ResultClass::kRunning);
|
||||
handled = true;
|
||||
} else if (command == "exec-interrupt") {
|
||||
SendResult(token, ResultClass::kDone);
|
||||
handled = true;
|
||||
|
||||
// later:
|
||||
MIWriter writer(transmit_buffer_.data(), transmit_buffer_.size());
|
||||
writer.AppendRaw(exec_token_);
|
||||
writer.BeginAsyncExecRecord("stopped");
|
||||
writer.AppendString("reason=\"signal-received\"");
|
||||
writer.AppendString("signal-name=\"SIGINT\"");
|
||||
writer.AppendString("signal-meaning=\"Interrupt\"");
|
||||
writer.EndRecord();
|
||||
server_->client()->Send(transmit_buffer_.data(), writer.buffer_offset());
|
||||
} else if (command == "exec-abort") {
|
||||
XELOGI("Debug client requested abort");
|
||||
SendResult(token, ResultClass::kDone);
|
||||
exit(1);
|
||||
handled = true;
|
||||
} else if (command == "gdb-exit") {
|
||||
// TODO(benvanik): die better?
|
||||
XELOGI("Debug client requested exit");
|
||||
exit(1);
|
||||
SendResult(token, ResultClass::kDone);
|
||||
handled = true;
|
||||
}
|
||||
if (!handled) {
|
||||
XELOGE("Unknown GDB packet: %s", buffer_ptr);
|
||||
SendErrorResult(token, "Unknown/unimplemented");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MICommandProcessor::SendConsoleStreamOutput(const char* value) {
|
||||
MIWriter writer(transmit_buffer_.data(), transmit_buffer_.size());
|
||||
writer.BeginConsoleStreamOutput();
|
||||
writer.AppendString(value);
|
||||
writer.EndRecord();
|
||||
server_->client()->Send(transmit_buffer_.data(), writer.buffer_offset());
|
||||
}
|
||||
|
||||
void MICommandProcessor::SendTargetStreamOutput(const char* value) {
|
||||
MIWriter writer(transmit_buffer_.data(), transmit_buffer_.size());
|
||||
writer.BeginTargetStreamOutput();
|
||||
writer.AppendString(value);
|
||||
writer.EndRecord();
|
||||
server_->client()->Send(transmit_buffer_.data(), writer.buffer_offset());
|
||||
}
|
||||
|
||||
void MICommandProcessor::SendLogStreamOutput(const char* value) {
|
||||
MIWriter writer(transmit_buffer_.data(), transmit_buffer_.size());
|
||||
writer.BeginLogStreamOutput();
|
||||
writer.AppendString(value);
|
||||
writer.EndRecord();
|
||||
server_->client()->Send(transmit_buffer_.data(), writer.buffer_offset());
|
||||
}
|
||||
|
||||
void MICommandProcessor::SendResult(const std::string& prefix,
|
||||
ResultClass result_class) {
|
||||
MIWriter writer(transmit_buffer_.data(), transmit_buffer_.size());
|
||||
writer.AppendRaw(prefix);
|
||||
writer.BeginResultRecord(result_class);
|
||||
writer.EndRecord();
|
||||
server_->client()->Send(transmit_buffer_.data(), writer.buffer_offset());
|
||||
}
|
||||
|
||||
void MICommandProcessor::SendErrorResult(const std::string& prefix,
|
||||
const char* message,
|
||||
const char* code) {
|
||||
MIWriter writer(transmit_buffer_.data(), transmit_buffer_.size());
|
||||
writer.AppendRaw(prefix);
|
||||
writer.BeginResultRecord(ResultClass::kError);
|
||||
if (message) {
|
||||
writer.AppendResult("msg", message);
|
||||
}
|
||||
if (code) {
|
||||
writer.AppendResult("code", code);
|
||||
}
|
||||
writer.EndRecord();
|
||||
server_->client()->Send(transmit_buffer_.data(), writer.buffer_offset());
|
||||
}
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_SERVER_MI_MI_COMMAND_PROCESSOR_H_
|
||||
#define XENIA_DEBUG_SERVER_MI_MI_COMMAND_PROCESSOR_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/debug/debugger.h"
|
||||
#include "xenia/debug/server/mi/mi_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
class MIServer;
|
||||
|
||||
class MICommandProcessor {
|
||||
public:
|
||||
MICommandProcessor(MIServer* server, Debugger* debugger);
|
||||
~MICommandProcessor() = default;
|
||||
|
||||
bool ProcessBuffer(const uint8_t* buffer, size_t buffer_length);
|
||||
|
||||
private:
|
||||
bool ProcessPacket(const char* buffer, size_t buffer_length);
|
||||
void SendConsoleStreamOutput(const char* value);
|
||||
void SendTargetStreamOutput(const char* value);
|
||||
void SendLogStreamOutput(const char* value);
|
||||
void SendResult(const std::string& prefix, ResultClass result_class);
|
||||
void SendErrorResult(const std::string& prefix, const char* message = nullptr,
|
||||
const char* code = nullptr);
|
||||
|
||||
MIServer* server_ = nullptr;
|
||||
Debugger* debugger_ = nullptr;
|
||||
std::vector<uint8_t> receive_buffer_;
|
||||
size_t receive_offset_ = 0;
|
||||
std::vector<uint8_t> transmit_buffer_;
|
||||
|
||||
// Token of the last -exec-* command. Sent back on stops.
|
||||
std::string exec_token_;
|
||||
};
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_SERVER_MI_MI_COMMAND_PROCESSOR_H_
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_SERVER_MI_MI_PROTOCOL_H_
|
||||
#define XENIA_DEBUG_SERVER_MI_MI_PROTOCOL_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
enum class RecordToken : char {
|
||||
kResult = '^',
|
||||
kAsyncExec = '*',
|
||||
kAsyncStatus = '+',
|
||||
kAsyncNotify = '=',
|
||||
};
|
||||
|
||||
enum class StreamToken : char {
|
||||
kConsole = '~',
|
||||
kTarget = '@',
|
||||
kLog = '&',
|
||||
};
|
||||
|
||||
enum class ResultClass {
|
||||
kDone,
|
||||
kRunning,
|
||||
kConnected,
|
||||
kError,
|
||||
kExit,
|
||||
};
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_SERVER_MI_MI_PROTOCOL_H_
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/server/mi/mi_reader.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
//
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_SERVER_MI_MI_READER_H_
|
||||
#define XENIA_DEBUG_SERVER_MI_MI_READER_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "xenia/debug/server/mi/mi_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
class MIReader {
|
||||
public:
|
||||
};
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_SERVER_MI_MI_READER_H_
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/server/mi/mi_server.h"
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/debug/debugger.h"
|
||||
#include "xenia/debug/server/mi/mi_command_processor.h"
|
||||
|
||||
DEFINE_int32(mi_server_port, 9001, "Debugger MI server TCP port.");
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
constexpr size_t kReceiveBufferSize = 32 * 1024;
|
||||
|
||||
MIServer::MIServer(Debugger* debugger) : DebugServer(debugger) {
|
||||
receive_buffer_.resize(kReceiveBufferSize);
|
||||
command_processor_ = std::make_unique<MICommandProcessor>(this, debugger);
|
||||
}
|
||||
|
||||
MIServer::~MIServer() = default;
|
||||
|
||||
bool MIServer::Initialize() {
|
||||
socket_server_ = SocketServer::Create(uint16_t(FLAGS_mi_server_port),
|
||||
[this](std::unique_ptr<Socket> client) {
|
||||
AcceptClient(std::move(client));
|
||||
});
|
||||
if (!socket_server_) {
|
||||
XELOGE("Unable to create MI socket server - port in use?");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MIServer::AcceptClient(std::unique_ptr<Socket> client) {
|
||||
// If we have an existing client, kill it and join its thread.
|
||||
if (client_) {
|
||||
// TODO(benvanik): GDB say goodbye?
|
||||
|
||||
client_->Close();
|
||||
xe::threading::Wait(client_thread_.get(), true);
|
||||
client_thread_.reset();
|
||||
}
|
||||
|
||||
// Take ownership of the new one.
|
||||
client_ = std::move(client);
|
||||
|
||||
// Create a thread to manage the connection.
|
||||
client_thread_ = xe::threading::Thread::Create({}, [this]() {
|
||||
// TODO(benvanik): GDB protocol stuff? Do we say hi?
|
||||
// TODO(benvanik): move hello to thread
|
||||
|
||||
// Let the debugger know we are present.
|
||||
debugger_->set_attached(true);
|
||||
|
||||
// Junk just to poke the remote client.
|
||||
// The Microsoft MI engine seems to wait for *something* to come through on
|
||||
// connection.
|
||||
client_->Send("(gdb)\n");
|
||||
|
||||
// Main loop.
|
||||
bool running = true;
|
||||
while (running) {
|
||||
auto wait_result = xe::threading::Wait(client_->wait_handle(), true);
|
||||
switch (wait_result) {
|
||||
case xe::threading::WaitResult::kSuccess:
|
||||
// Event (read or close).
|
||||
running = HandleClientEvent();
|
||||
continue;
|
||||
case xe::threading::WaitResult::kAbandoned:
|
||||
case xe::threading::WaitResult::kFailed:
|
||||
// Error - kill the thread.
|
||||
running = false;
|
||||
continue;
|
||||
default:
|
||||
// Eh. Continue.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Kill client (likely already disconnected).
|
||||
client_.reset();
|
||||
|
||||
// Notify debugger we are no longer attached.
|
||||
debugger_->set_attached(false);
|
||||
});
|
||||
}
|
||||
|
||||
bool MIServer::HandleClientEvent() {
|
||||
if (!client_->is_connected()) {
|
||||
// Known-disconnected.
|
||||
return false;
|
||||
}
|
||||
// Attempt to read into our buffer.
|
||||
size_t bytes_read =
|
||||
client_->Receive(receive_buffer_.data(), receive_buffer_.capacity());
|
||||
if (bytes_read == -1) {
|
||||
// Disconnected.
|
||||
return false;
|
||||
} else if (bytes_read == 0) {
|
||||
// No data available. Wait again.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pass off the command processor to do with it what it wants.
|
||||
if (!command_processor_->ProcessBuffer(receive_buffer_.data(), bytes_read)) {
|
||||
// Error.
|
||||
XELOGE("Error processing incoming GDB command; forcing disconnect");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_SERVER_MI_MI_SERVER_H_
|
||||
#define XENIA_DEBUG_SERVER_MI_MI_SERVER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "xenia/base/socket.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/debug/debug_server.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
class MICommandProcessor;
|
||||
|
||||
class MIServer : public DebugServer {
|
||||
public:
|
||||
MIServer(Debugger* debugger);
|
||||
~MIServer() override;
|
||||
|
||||
Socket* client() const { return client_.get(); }
|
||||
|
||||
bool Initialize() override;
|
||||
|
||||
private:
|
||||
void AcceptClient(std::unique_ptr<Socket> client);
|
||||
bool HandleClientEvent();
|
||||
|
||||
std::unique_ptr<SocketServer> socket_server_;
|
||||
|
||||
std::unique_ptr<Socket> client_;
|
||||
std::unique_ptr<xe::threading::Thread> client_thread_;
|
||||
std::vector<uint8_t> receive_buffer_;
|
||||
|
||||
std::unique_ptr<MICommandProcessor> command_processor_;
|
||||
};
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_SERVER_MI_MI_SERVER_H_
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/server/mi/mi_writer.h"
|
||||
|
||||
#include "xenia/base/assert.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
void MIWriter::AppendRaw(const char* value, size_t length) {
|
||||
assert_true(buffer_offset_ + length < buffer_capacity_);
|
||||
std::memcpy(buffer_ + buffer_offset_, value, length);
|
||||
buffer_offset_ += length;
|
||||
}
|
||||
|
||||
void MIWriter::BeginRecord(RecordToken token, const char* record_class) {
|
||||
assert_false(in_record_);
|
||||
in_record_ = true;
|
||||
size_t record_class_length = std::strlen(record_class);
|
||||
assert_true(buffer_offset_ + 1 + record_class_length < buffer_capacity_);
|
||||
buffer_[buffer_offset_++] = char(token);
|
||||
std::memcpy(buffer_ + buffer_offset_, record_class, record_class_length);
|
||||
buffer_offset_ += record_class_length;
|
||||
}
|
||||
|
||||
void MIWriter::BeginResultRecord(ResultClass result_class) {
|
||||
const char* record_class = "error";
|
||||
switch (result_class) {
|
||||
case ResultClass::kDone:
|
||||
record_class = "done";
|
||||
break;
|
||||
case ResultClass::kRunning:
|
||||
record_class = "running";
|
||||
break;
|
||||
case ResultClass::kConnected:
|
||||
record_class = "connected";
|
||||
break;
|
||||
case ResultClass::kError:
|
||||
record_class = "error";
|
||||
break;
|
||||
case ResultClass::kExit:
|
||||
record_class = "exit";
|
||||
break;
|
||||
}
|
||||
BeginRecord(RecordToken::kResult, record_class);
|
||||
}
|
||||
|
||||
void MIWriter::BeginStreamRecord(StreamToken token) {
|
||||
assert_false(in_record_);
|
||||
in_record_ = true;
|
||||
assert_true(buffer_offset_ + 1 < buffer_capacity_);
|
||||
buffer_[buffer_offset_++] = char(token);
|
||||
}
|
||||
|
||||
void MIWriter::EndRecord() {
|
||||
assert_true(in_record_);
|
||||
in_record_ = false;
|
||||
assert_true(buffer_offset_ + 1 < buffer_capacity_);
|
||||
buffer_[buffer_offset_++] = '\n';
|
||||
}
|
||||
|
||||
void MIWriter::AppendArraySeparator() {
|
||||
if (array_depth_) {
|
||||
if (array_count_[array_depth_]) {
|
||||
assert_true(buffer_offset_ + 1 < buffer_capacity_);
|
||||
buffer_[buffer_offset_++] = ',';
|
||||
}
|
||||
++array_count_[array_depth_];
|
||||
}
|
||||
}
|
||||
|
||||
void MIWriter::PushTuple() {
|
||||
assert_true(in_record_);
|
||||
AppendArraySeparator();
|
||||
++array_depth_;
|
||||
assert_true(array_depth_ < kMaxArrayDepth);
|
||||
array_count_[array_depth_] = 0;
|
||||
assert_true(buffer_offset_ + 1 < buffer_capacity_);
|
||||
buffer_[buffer_offset_++] = '{';
|
||||
}
|
||||
|
||||
void MIWriter::PopTuple() {
|
||||
--array_depth_;
|
||||
assert_true(buffer_offset_ + 1 < buffer_capacity_);
|
||||
buffer_[buffer_offset_++] = '}';
|
||||
}
|
||||
|
||||
void MIWriter::PushList() {
|
||||
assert_true(in_record_);
|
||||
AppendArraySeparator();
|
||||
++array_depth_;
|
||||
assert_true(array_depth_ < kMaxArrayDepth);
|
||||
array_count_[array_depth_] = 0;
|
||||
assert_true(buffer_offset_ + 1 < buffer_capacity_);
|
||||
buffer_[buffer_offset_++] = '[';
|
||||
}
|
||||
|
||||
void MIWriter::PopList() {
|
||||
--array_depth_;
|
||||
assert_true(buffer_offset_ + 1 < buffer_capacity_);
|
||||
buffer_[buffer_offset_++] = ']';
|
||||
}
|
||||
|
||||
void MIWriter::AppendString(const char* value, size_t length) {
|
||||
assert_true(in_record_);
|
||||
AppendArraySeparator();
|
||||
assert_true(buffer_offset_ + length < buffer_capacity_);
|
||||
std::memcpy(buffer_ + buffer_offset_, value, length);
|
||||
buffer_offset_ += length;
|
||||
}
|
||||
|
||||
void MIWriter::AppendResult(const char* variable, const char* value,
|
||||
size_t length) {
|
||||
assert_true(in_record_);
|
||||
AppendArraySeparator();
|
||||
size_t variable_length = std::strlen(variable);
|
||||
assert_true(buffer_offset_ + 1 + variable_length + length < buffer_capacity_);
|
||||
std::memcpy(buffer_ + buffer_offset_, variable, variable_length);
|
||||
buffer_offset_ += variable_length;
|
||||
buffer_[buffer_offset_++] = '=';
|
||||
std::memcpy(buffer_ + buffer_offset_, value, length);
|
||||
buffer_offset_ += length;
|
||||
}
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_SERVER_MI_MI_WRITER_H_
|
||||
#define XENIA_DEBUG_SERVER_MI_MI_WRITER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "xenia/debug/server/mi/mi_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace mi {
|
||||
|
||||
// https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Syntax.html#GDB_002fMI-Output-Syntax
|
||||
class MIWriter {
|
||||
public:
|
||||
MIWriter(uint8_t* buffer, size_t buffer_capacity)
|
||||
: buffer_(buffer), buffer_capacity_(buffer_capacity) {}
|
||||
|
||||
uint8_t* buffer() const { return buffer_; }
|
||||
size_t buffer_capacity() const { return buffer_capacity_; }
|
||||
size_t buffer_offset() const { return buffer_offset_; }
|
||||
|
||||
void AppendRaw(const char* value, size_t length);
|
||||
void AppendRaw(const char* value) { AppendRaw(value, std::strlen(value)); }
|
||||
void AppendRaw(const std::string& value) {
|
||||
AppendRaw(value.c_str(), value.size());
|
||||
}
|
||||
|
||||
void BeginResultRecord(ResultClass result_class);
|
||||
void BeginAsyncExecRecord(const char* async_class) {
|
||||
BeginRecord(RecordToken::kAsyncExec, async_class);
|
||||
array_depth_ = 1;
|
||||
array_count_[array_depth_] = 1;
|
||||
}
|
||||
void BeginAsyncStatusRecord(const char* async_class) {
|
||||
BeginRecord(RecordToken::kAsyncStatus, async_class);
|
||||
array_depth_ = 1;
|
||||
array_count_[array_depth_] = 1;
|
||||
}
|
||||
void BeginAsyncNotifyRecord(const char* async_class) {
|
||||
BeginRecord(RecordToken::kAsyncNotify, async_class);
|
||||
array_depth_ = 1;
|
||||
array_count_[array_depth_] = 1;
|
||||
}
|
||||
void BeginConsoleStreamOutput() { BeginStreamRecord(StreamToken::kConsole); }
|
||||
void BeginTargetStreamOutput() { BeginStreamRecord(StreamToken::kTarget); }
|
||||
void BeginLogStreamOutput() { BeginStreamRecord(StreamToken::kLog); }
|
||||
void EndRecord();
|
||||
|
||||
void PushTuple();
|
||||
void PopTuple();
|
||||
void PushList();
|
||||
void PopList();
|
||||
|
||||
void AppendString(const char* value, size_t length);
|
||||
void AppendString(const char* value) {
|
||||
AppendString(value, std::strlen(value));
|
||||
}
|
||||
void AppendString(const std::string& value) {
|
||||
AppendString(value.c_str(), value.size());
|
||||
}
|
||||
|
||||
void AppendResult(const char* variable, const char* value, size_t length);
|
||||
void AppendResult(const char* variable, const char* value) {
|
||||
AppendResult(variable, value, std::strlen(value));
|
||||
}
|
||||
void AppendResult(const char* variable, const std::string& value) {
|
||||
AppendResult(variable, value.c_str(), value.size());
|
||||
}
|
||||
|
||||
private:
|
||||
void BeginRecord(RecordToken token, const char* record_class);
|
||||
void BeginStreamRecord(StreamToken token);
|
||||
void AppendArraySeparator();
|
||||
|
||||
uint8_t* buffer_ = nullptr;
|
||||
size_t buffer_capacity_ = 0;
|
||||
size_t buffer_offset_ = 0;
|
||||
|
||||
static const int kMaxArrayDepth = 32;
|
||||
bool in_record_ = false;
|
||||
int array_depth_ = 0;
|
||||
int array_count_[kMaxArrayDepth];
|
||||
};
|
||||
|
||||
} // namespace mi
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_SERVER_MI_MI_WRITER_H_
|
|
@ -0,0 +1,313 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/server/xdp/xdp_server.h"
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/debug/debugger.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/objects/xmodule.h"
|
||||
#include "xenia/kernel/objects/xthread.h"
|
||||
|
||||
DEFINE_int32(xdp_server_port, 9002, "Debugger XDP server TCP port.");
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace xdp {
|
||||
|
||||
using namespace xe::debug::proto;
|
||||
using namespace xe::kernel;
|
||||
|
||||
constexpr size_t kReceiveBufferSize = 32 * 1024;
|
||||
constexpr size_t kReadBufferSize = 1 * 1024 * 1024;
|
||||
constexpr size_t kWriteBufferSize = 1 * 1024 * 1024;
|
||||
|
||||
XdpServer::XdpServer(Debugger* debugger)
|
||||
: DebugServer(debugger),
|
||||
packet_reader_(kReadBufferSize),
|
||||
packet_writer_(kWriteBufferSize) {
|
||||
receive_buffer_.resize(kReceiveBufferSize);
|
||||
}
|
||||
|
||||
XdpServer::~XdpServer() = default;
|
||||
|
||||
bool XdpServer::Initialize() {
|
||||
socket_server_ = SocketServer::Create(uint16_t(FLAGS_xdp_server_port),
|
||||
[this](std::unique_ptr<Socket> client) {
|
||||
AcceptClient(std::move(client));
|
||||
});
|
||||
if (!socket_server_) {
|
||||
XELOGE("Unable to create XDP socket server - port in use?");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void XdpServer::AcceptClient(std::unique_ptr<Socket> client) {
|
||||
// If we have an existing client, kill it and join its thread.
|
||||
if (client_) {
|
||||
// TODO(benvanik): XDP say goodbye?
|
||||
|
||||
client_->Close();
|
||||
xe::threading::Wait(client_thread_.get(), true);
|
||||
client_thread_.reset();
|
||||
}
|
||||
|
||||
// Take ownership of the new one.
|
||||
client_ = std::move(client);
|
||||
|
||||
// Create a thread to manage the connection.
|
||||
client_thread_ = xe::threading::Thread::Create({}, [this]() {
|
||||
// TODO(benvanik): GDB protocol stuff? Do we say hi?
|
||||
// TODO(benvanik): move hello to thread
|
||||
|
||||
// Let the debugger know we are present.
|
||||
debugger_->set_attached(true);
|
||||
|
||||
// Notify the client of the current state.
|
||||
if (debugger_->execution_state() == ExecutionState::kRunning) {
|
||||
OnExecutionContinued();
|
||||
} else {
|
||||
OnExecutionInterrupted();
|
||||
}
|
||||
|
||||
// Main loop.
|
||||
bool running = true;
|
||||
while (running) {
|
||||
auto wait_result = xe::threading::Wait(client_->wait_handle(), true);
|
||||
switch (wait_result) {
|
||||
case xe::threading::WaitResult::kSuccess:
|
||||
// Event (read or close).
|
||||
running = HandleClientEvent();
|
||||
continue;
|
||||
case xe::threading::WaitResult::kAbandoned:
|
||||
case xe::threading::WaitResult::kFailed:
|
||||
// Error - kill the thread.
|
||||
running = false;
|
||||
continue;
|
||||
default:
|
||||
// Eh. Continue.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Kill client (likely already disconnected).
|
||||
client_.reset();
|
||||
|
||||
// Notify debugger we are no longer attached.
|
||||
debugger_->set_attached(false);
|
||||
});
|
||||
}
|
||||
|
||||
bool XdpServer::HandleClientEvent() {
|
||||
if (!client_->is_connected()) {
|
||||
// Known-disconnected.
|
||||
return false;
|
||||
}
|
||||
// Attempt to read into our buffer.
|
||||
size_t bytes_read =
|
||||
client_->Receive(receive_buffer_.data(), receive_buffer_.capacity());
|
||||
if (bytes_read == -1) {
|
||||
// Disconnected.
|
||||
return false;
|
||||
} else if (bytes_read == 0) {
|
||||
// No data available. Wait again.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pass off the command processor to do with it what it wants.
|
||||
if (!ProcessBuffer(receive_buffer_.data(), bytes_read)) {
|
||||
// Error.
|
||||
XELOGE("Error processing incoming XDP command; forcing disconnect");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
bool XdpServer::ProcessBuffer(const uint8_t* buffer, size_t buffer_length) {
|
||||
// Grow and append the bytes to the receive buffer.
|
||||
packet_reader_.AppendBuffer(buffer, buffer_length);
|
||||
|
||||
// Process all packets in the buffer.
|
||||
while (true) {
|
||||
auto packet = packet_reader_.Begin();
|
||||
if (!packet) {
|
||||
// Out of packets. Compact any unused space.
|
||||
packet_reader_.Compact();
|
||||
break;
|
||||
}
|
||||
|
||||
// Emit packet.
|
||||
if (!ProcessPacket(packet)) {
|
||||
// Failed to process packet.
|
||||
XELOGE("Failed to process incoming packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
packet_reader_.End();
|
||||
}
|
||||
|
||||
Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XdpServer::ProcessPacket(const proto::Packet* packet) {
|
||||
auto emulator = debugger()->emulator();
|
||||
auto kernel_state = emulator->kernel_state();
|
||||
auto object_table = kernel_state->object_table();
|
||||
|
||||
switch (packet->packet_type) {
|
||||
case PacketType::kExecutionRequest: {
|
||||
auto body = packet_reader_.Read<ExecutionRequest>();
|
||||
switch (body->action) {
|
||||
case ExecutionRequest::Action::kContinue: {
|
||||
debugger_->Continue();
|
||||
} break;
|
||||
case ExecutionRequest::Action::kStep: {
|
||||
switch (body->step_mode) {
|
||||
case ExecutionRequest::StepMode::kStepOne: {
|
||||
debugger_->StepOne(body->thread_id);
|
||||
} break;
|
||||
case ExecutionRequest::StepMode::kStepTo: {
|
||||
debugger_->StepTo(body->thread_id, body->target_pc);
|
||||
} break;
|
||||
default:
|
||||
assert_unhandled_case(body->step_mode);
|
||||
return false;
|
||||
}
|
||||
} break;
|
||||
case ExecutionRequest::Action::kInterrupt: {
|
||||
debugger_->Interrupt();
|
||||
} break;
|
||||
case ExecutionRequest::Action::kExit: {
|
||||
XELOGI("Debug client requested exit");
|
||||
exit(1);
|
||||
} break;
|
||||
}
|
||||
SendSuccess(packet->request_id);
|
||||
} break;
|
||||
case PacketType::kModuleListRequest: {
|
||||
packet_writer_.Begin(PacketType::kModuleListResponse);
|
||||
auto body = packet_writer_.Append<ModuleListResponse>();
|
||||
auto modules =
|
||||
object_table->GetObjectsByType<XModule>(XObject::Type::kTypeModule);
|
||||
body->count = uint32_t(modules.size());
|
||||
for (auto& module : modules) {
|
||||
auto entry = packet_writer_.Append<ModuleListEntry>();
|
||||
entry->module_handle = module->handle();
|
||||
entry->module_ptr = module->hmodule_ptr();
|
||||
entry->is_kernel_module =
|
||||
module->module_type() == XModule::ModuleType::kKernelModule;
|
||||
std::strncpy(entry->path, module->path().c_str(),
|
||||
xe::countof(entry->path));
|
||||
std::strncpy(entry->name, module->name().c_str(),
|
||||
xe::countof(entry->name));
|
||||
}
|
||||
packet_writer_.End();
|
||||
} break;
|
||||
case PacketType::kThreadListRequest: {
|
||||
packet_writer_.Begin(PacketType::kThreadListResponse);
|
||||
auto body = packet_writer_.Append<ThreadListResponse>();
|
||||
auto threads =
|
||||
object_table->GetObjectsByType<XThread>(XObject::Type::kTypeThread);
|
||||
body->count = uint32_t(threads.size());
|
||||
for (auto& thread : threads) {
|
||||
auto entry = packet_writer_.Append<ThreadListEntry>();
|
||||
entry->thread_handle = thread->handle();
|
||||
entry->thread_id = thread->thread_id();
|
||||
entry->is_host_thread = !thread->is_guest_thread();
|
||||
std::strncpy(entry->name, thread->name().c_str(),
|
||||
xe::countof(entry->name));
|
||||
entry->exit_code;
|
||||
entry->priority = thread->priority();
|
||||
entry->xapi_thread_startup =
|
||||
thread->creation_params()->xapi_thread_startup;
|
||||
entry->start_address = thread->creation_params()->start_address;
|
||||
entry->start_context = thread->creation_params()->start_context;
|
||||
entry->creation_flags = thread->creation_params()->creation_flags;
|
||||
entry->stack_address = thread->thread_state()->stack_address();
|
||||
entry->stack_size = thread->thread_state()->stack_size();
|
||||
entry->stack_base = thread->thread_state()->stack_base();
|
||||
entry->stack_limit = thread->thread_state()->stack_limit();
|
||||
entry->pcr_address = thread->pcr_ptr();
|
||||
entry->tls_address = thread->tls_ptr();
|
||||
}
|
||||
packet_writer_.End();
|
||||
} break;
|
||||
default: {
|
||||
XELOGE("Unknown packet type");
|
||||
SendError(packet->request_id, "Unknown packet type");
|
||||
return false;
|
||||
} break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void XdpServer::OnExecutionContinued() {
|
||||
packet_writer_.Begin(PacketType::kExecutionNotification);
|
||||
auto body = packet_writer_.Append<ExecutionNotification>();
|
||||
body->current_state = ExecutionNotification::State::kRunning;
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpServer::OnExecutionInterrupted() {
|
||||
packet_writer_.Begin(PacketType::kExecutionNotification);
|
||||
auto body = packet_writer_.Append<ExecutionNotification>();
|
||||
body->current_state = ExecutionNotification::State::kStopped;
|
||||
body->stop_reason = ExecutionNotification::Reason::kInterrupt;
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpServer::Flush() {
|
||||
if (!packet_writer_.buffer_offset()) {
|
||||
return;
|
||||
}
|
||||
client_->Send(packet_writer_.buffer(), packet_writer_.buffer_offset());
|
||||
packet_writer_.Reset();
|
||||
}
|
||||
|
||||
void XdpServer::SendSuccess(proto::request_id_t request_id) {
|
||||
packet_writer_.Begin(PacketType::kGenericResponse, request_id);
|
||||
auto body = packet_writer_.Append<GenericResponse>();
|
||||
body->code = GenericResponse::Code::kSuccess;
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpServer::SendError(proto::request_id_t request_id,
|
||||
const char* error_message) {
|
||||
packet_writer_.Begin(PacketType::kGenericResponse, request_id);
|
||||
auto body = packet_writer_.Append<GenericResponse>();
|
||||
body->code = GenericResponse::Code::kError;
|
||||
packet_writer_.AppendString(error_message ? error_message : "");
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
void XdpServer::SendError(proto::request_id_t request_id,
|
||||
const std::string& error_message) {
|
||||
packet_writer_.Begin(PacketType::kGenericResponse, request_id);
|
||||
auto body = packet_writer_.Append<GenericResponse>();
|
||||
body->code = GenericResponse::Code::kError;
|
||||
packet_writer_.AppendString(error_message);
|
||||
packet_writer_.End();
|
||||
Flush();
|
||||
}
|
||||
|
||||
} // namespace xdp
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_SERVER_XDP_XDP_SERVER_H_
|
||||
#define XENIA_DEBUG_SERVER_XDP_XDP_SERVER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "xenia/base/socket.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/debug/debug_server.h"
|
||||
#include "xenia/debug/proto/packet_reader.h"
|
||||
#include "xenia/debug/proto/packet_writer.h"
|
||||
#include "xenia/debug/proto/xdp_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace server {
|
||||
namespace xdp {
|
||||
|
||||
class XdpServer : public DebugServer {
|
||||
public:
|
||||
XdpServer(Debugger* debugger);
|
||||
~XdpServer() override;
|
||||
|
||||
Socket* client() const { return client_.get(); }
|
||||
|
||||
bool Initialize() override;
|
||||
|
||||
void OnExecutionContinued() override;
|
||||
void OnExecutionInterrupted() override;
|
||||
|
||||
private:
|
||||
void AcceptClient(std::unique_ptr<Socket> client);
|
||||
bool HandleClientEvent();
|
||||
|
||||
bool ProcessBuffer(const uint8_t* buffer, size_t buffer_length);
|
||||
bool ProcessPacket(const proto::Packet* packet);
|
||||
|
||||
void Flush();
|
||||
void SendSuccess(proto::request_id_t request_id);
|
||||
void SendError(proto::request_id_t request_id,
|
||||
const char* error_message = nullptr);
|
||||
void SendError(proto::request_id_t request_id,
|
||||
const std::string& error_message);
|
||||
|
||||
std::unique_ptr<SocketServer> socket_server_;
|
||||
|
||||
std::unique_ptr<Socket> client_;
|
||||
std::unique_ptr<xe::threading::Thread> client_thread_;
|
||||
std::vector<uint8_t> receive_buffer_;
|
||||
proto::PacketReader packet_reader_;
|
||||
proto::PacketWriter packet_writer_;
|
||||
};
|
||||
|
||||
} // namespace xdp
|
||||
} // namespace server
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_SERVER_XDP_XDP_SERVER_H_
|
|
@ -58,6 +58,15 @@ std::unique_ptr<Application> Application::Create() {
|
|||
}
|
||||
|
||||
bool Application::Initialize() {
|
||||
// Bind the object model to the client so it'll maintain state.
|
||||
system_ = std::make_unique<model::System>(loop(), &client_);
|
||||
client_.set_listener(system_.get());
|
||||
|
||||
// TODO(benvanik): flags and such.
|
||||
if (!client_.Connect("localhost", 9002)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
main_window_ = std::make_unique<MainWindow>(this);
|
||||
if (!main_window_->Initialize()) {
|
||||
XELOGE("Unable to initialize main window");
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
#include <memory>
|
||||
|
||||
#include "xenia/debug/client/xdp/xdp_client.h"
|
||||
#include "xenia/debug/ui/model/system.h"
|
||||
#include "xenia/ui/loop.h"
|
||||
|
||||
namespace xe {
|
||||
|
@ -29,6 +31,8 @@ class Application {
|
|||
|
||||
xe::ui::Loop* loop() { return loop_.get(); }
|
||||
MainWindow* main_window() const { return main_window_.get(); }
|
||||
client::xdp::XdpClient* client() { return &client_; }
|
||||
model::System* system() const { return system_.get(); }
|
||||
|
||||
void Quit();
|
||||
|
||||
|
@ -39,6 +43,9 @@ class Application {
|
|||
|
||||
std::unique_ptr<xe::ui::Loop> loop_;
|
||||
std::unique_ptr<MainWindow> main_window_;
|
||||
client::xdp::XdpClient client_;
|
||||
|
||||
std::unique_ptr<model::System> system_;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
@ -21,6 +21,8 @@ namespace xe {
|
|||
namespace debug {
|
||||
namespace ui {
|
||||
|
||||
using namespace xe::debug::client::xdp;
|
||||
|
||||
using xe::ui::MenuItem;
|
||||
|
||||
const std::wstring kBaseTitle = L"xenia debugger";
|
||||
|
@ -30,6 +32,8 @@ MainWindow::MainWindow(Application* app) : app_(app) {}
|
|||
MainWindow::~MainWindow() = default;
|
||||
|
||||
bool MainWindow::Initialize() {
|
||||
client_ = app_->client();
|
||||
|
||||
platform_window_ = xe::ui::Window::Create(app()->loop(), kBaseTitle);
|
||||
if (!platform_window_) {
|
||||
return false;
|
||||
|
@ -106,11 +110,14 @@ void MainWindow::BuildUI() {
|
|||
.gravity(Gravity::kAll)
|
||||
.distribution(LayoutDistribution::kAvailable)
|
||||
.axis(Axis::kY)
|
||||
.child(LayoutBoxNode()
|
||||
.id("toolbar_box")
|
||||
.gravity(Gravity::kTop | Gravity::kLeftRight)
|
||||
.distribution(LayoutDistribution::kAvailable)
|
||||
.child(LabelNode("toolbar")))
|
||||
.child(
|
||||
LayoutBoxNode()
|
||||
.id("toolbar_box")
|
||||
.gravity(Gravity::kTop | Gravity::kLeftRight)
|
||||
.distribution(LayoutDistribution::kGravity)
|
||||
.distribution_position(LayoutDistributionPosition::kLeftTop)
|
||||
.child(ButtonNode("Pause").id("pause_button"))
|
||||
.child(ButtonNode("Continue").id("resume_button")))
|
||||
.child(
|
||||
SplitContainerNode()
|
||||
.id("split_container")
|
||||
|
@ -132,15 +139,50 @@ void MainWindow::BuildUI() {
|
|||
{TBIDC("tab_container"), &ui_.tab_container},
|
||||
});
|
||||
|
||||
handler_ = std::make_unique<el::EventHandler>(window_.get());
|
||||
handler_->Listen(el::EventType::kClick, TBIDC("pause_button"),
|
||||
[this](const el::Event& ev) {
|
||||
client_->Interrupt();
|
||||
return true;
|
||||
});
|
||||
handler_->Listen(el::EventType::kClick, TBIDC("resume_button"),
|
||||
[this](const el::Event& ev) {
|
||||
client_->Continue();
|
||||
return true;
|
||||
});
|
||||
|
||||
system()->on_execution_state_changed.AddListener(
|
||||
[this]() { UpdateElementState(); });
|
||||
|
||||
ui_.tab_container->tab_bar()->LoadNodeTree(ButtonNode(cpu_view_.name()));
|
||||
ui_.tab_container->content_root()->AddChild(cpu_view_.BuildUI());
|
||||
cpu_view_.Setup(Application::current()->client());
|
||||
|
||||
ui_.tab_container->tab_bar()->LoadNodeTree(ButtonNode(gpu_view_.name()));
|
||||
ui_.tab_container->content_root()->AddChild(gpu_view_.BuildUI());
|
||||
gpu_view_.Setup(Application::current()->client());
|
||||
|
||||
UpdateElementState();
|
||||
|
||||
el::util::ShowDebugInfoSettingsWindow(root_element);
|
||||
}
|
||||
|
||||
void MainWindow::UpdateElementState() {
|
||||
bool is_running = client_->execution_state() == ExecutionState::kRunning;
|
||||
|
||||
el::TabContainer* tab_container;
|
||||
el::Button* pause_button;
|
||||
el::Button* resume_button;
|
||||
window_->GetElementsById({
|
||||
{TBIDC("tab_container"), &tab_container},
|
||||
{TBIDC("pause_button"), &pause_button},
|
||||
{TBIDC("resume_button"), &resume_button},
|
||||
});
|
||||
tab_container->set_enabled(!is_running);
|
||||
pause_button->set_enabled(is_running);
|
||||
resume_button->set_enabled(!is_running);
|
||||
}
|
||||
|
||||
void MainWindow::OnClose() { app_->Quit(); }
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
@ -28,6 +28,8 @@ class MainWindow {
|
|||
~MainWindow();
|
||||
|
||||
Application* app() const { return app_; }
|
||||
xe::ui::Loop* loop() const { return app_->loop(); }
|
||||
model::System* system() const { return app_->system(); }
|
||||
|
||||
bool Initialize();
|
||||
|
||||
|
@ -35,10 +37,12 @@ class MainWindow {
|
|||
|
||||
private:
|
||||
void BuildUI();
|
||||
void UpdateElementState();
|
||||
|
||||
void OnClose();
|
||||
|
||||
Application* app_ = nullptr;
|
||||
xe::debug::client::xdp::XdpClient* client_ = nullptr;
|
||||
|
||||
std::unique_ptr<xe::ui::Window> platform_window_;
|
||||
std::unique_ptr<el::Window> window_;
|
||||
|
@ -47,6 +51,7 @@ class MainWindow {
|
|||
el::LayoutBox* toolbar_box;
|
||||
el::TabContainer* tab_container;
|
||||
} ui_ = {0};
|
||||
std::unique_ptr<el::EventHandler> handler_;
|
||||
views::cpu::CpuView cpu_view_;
|
||||
views::gpu::GpuView gpu_view_;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/ui/model/function.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace ui {
|
||||
namespace model {
|
||||
|
||||
//
|
||||
|
||||
} // namespace model
|
||||
} // namespace ui
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_UI_MODEL_FUNCTION_H_
|
||||
#define XENIA_DEBUG_UI_MODEL_FUNCTION_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace ui {
|
||||
namespace model {
|
||||
|
||||
class Function {
|
||||
public:
|
||||
};
|
||||
|
||||
} // namespace model
|
||||
} // namespace ui
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_UI_MODEL_FUNCTION_H_
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/ui/model/module.h"
|
||||
|
||||
#include "xenia/debug/ui/model/system.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace ui {
|
||||
namespace model {
|
||||
|
||||
void Module::Update(const proto::ModuleListEntry* entry) {
|
||||
if (!entry_.module_handle) {
|
||||
std::memcpy(&entry_, entry, sizeof(entry_));
|
||||
} else {
|
||||
std::memcpy(&temp_entry_, entry, sizeof(temp_entry_));
|
||||
system_->loop()->Post(
|
||||
[this]() { std::memcpy(&entry_, &temp_entry_, sizeof(temp_entry_)); });
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace model
|
||||
} // namespace ui
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_UI_MODEL_MODULE_H_
|
||||
#define XENIA_DEBUG_UI_MODEL_MODULE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "xenia/debug/proto/xdp_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace ui {
|
||||
namespace model {
|
||||
|
||||
class System;
|
||||
|
||||
class Module {
|
||||
public:
|
||||
Module(System* system) : system_(system) {}
|
||||
|
||||
bool is_dead() const { return is_dead_; }
|
||||
void set_dead(bool is_dead) { is_dead_ = is_dead; }
|
||||
|
||||
uint32_t module_handle() const { return entry_.module_handle; }
|
||||
bool is_kernel_module() const { return entry_.is_kernel_module; }
|
||||
std::string name() const { return entry_.name; }
|
||||
const proto::ModuleListEntry* entry() const { return &entry_; }
|
||||
|
||||
void Update(const proto::ModuleListEntry* entry);
|
||||
|
||||
private:
|
||||
System* system_ = nullptr;
|
||||
bool is_dead_ = false;
|
||||
proto::ModuleListEntry entry_ = {0};
|
||||
proto::ModuleListEntry temp_entry_ = {0};
|
||||
};
|
||||
|
||||
} // namespace model
|
||||
} // namespace ui
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_UI_MODEL_MODULE_H_
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/ui/model/system.h"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace ui {
|
||||
namespace model {
|
||||
|
||||
using namespace xe::debug::proto;
|
||||
|
||||
System::System(xe::ui::Loop* loop, client::xdp::XdpClient* client)
|
||||
: loop_(loop), client_(client) {}
|
||||
|
||||
ExecutionState System::execution_state() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
return client_->execution_state();
|
||||
}
|
||||
|
||||
std::vector<Module*> System::modules() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
std::vector<Module*> result;
|
||||
for (auto& module : modules_) {
|
||||
result.push_back(module.get());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Thread*> System::threads() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
std::vector<Thread*> result;
|
||||
for (auto& thread : threads_) {
|
||||
result.push_back(thread.get());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Module* System::GetModuleByHandle(uint32_t module_handle) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
auto it = modules_by_handle_.find(module_handle);
|
||||
return it != modules_by_handle_.end() ? it->second : nullptr;
|
||||
}
|
||||
|
||||
Thread* System::GetThreadByHandle(uint32_t thread_handle) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
auto it = threads_by_handle_.find(thread_handle);
|
||||
return it != threads_by_handle_.end() ? it->second : nullptr;
|
||||
}
|
||||
|
||||
void System::OnExecutionStateChanged(ExecutionState execution_state) {
|
||||
loop_->Post([this]() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
on_execution_state_changed();
|
||||
});
|
||||
}
|
||||
|
||||
void System::OnModulesUpdated(std::vector<const ModuleListEntry*> entries) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
std::unordered_set<uint32_t> extra_modules;
|
||||
for (size_t i = 0; i < modules_.size(); ++i) {
|
||||
extra_modules.emplace(modules_[i]->module_handle());
|
||||
}
|
||||
for (auto entry : entries) {
|
||||
auto existing_module = modules_by_handle_.find(entry->module_handle);
|
||||
if (existing_module == modules_by_handle_.end()) {
|
||||
auto module = std::make_unique<Module>(this);
|
||||
module->Update(entry);
|
||||
modules_by_handle_.emplace(entry->module_handle, module.get());
|
||||
modules_.emplace_back(std::move(module));
|
||||
} else {
|
||||
existing_module->second->Update(entry);
|
||||
extra_modules.erase(existing_module->first);
|
||||
}
|
||||
}
|
||||
for (auto module_handle : extra_modules) {
|
||||
modules_by_handle_[module_handle]->set_dead(true);
|
||||
}
|
||||
}
|
||||
loop_->Post([this]() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
on_modules_updated();
|
||||
});
|
||||
}
|
||||
|
||||
void System::OnThreadsUpdated(std::vector<const ThreadListEntry*> entries) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
std::unordered_set<uint32_t> extra_threads;
|
||||
for (size_t i = 0; i < threads_.size(); ++i) {
|
||||
extra_threads.emplace(threads_[i]->thread_handle());
|
||||
}
|
||||
for (auto entry : entries) {
|
||||
auto existing_thread = threads_by_handle_.find(entry->thread_handle);
|
||||
if (existing_thread == threads_by_handle_.end()) {
|
||||
auto thread = std::make_unique<Thread>(this);
|
||||
thread->Update(entry);
|
||||
threads_by_handle_.emplace(entry->thread_handle, thread.get());
|
||||
threads_.emplace_back(std::move(thread));
|
||||
} else {
|
||||
existing_thread->second->Update(entry);
|
||||
extra_threads.erase(existing_thread->first);
|
||||
}
|
||||
}
|
||||
for (auto thread_handle : extra_threads) {
|
||||
modules_by_handle_[thread_handle]->set_dead(true);
|
||||
}
|
||||
}
|
||||
loop_->Post([this]() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
on_threads_updated();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace model
|
||||
} // namespace ui
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_UI_MODEL_SYSTEM_H_
|
||||
#define XENIA_DEBUG_UI_MODEL_SYSTEM_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/delegate.h"
|
||||
#include "xenia/debug/client/xdp/xdp_client.h"
|
||||
#include "xenia/debug/ui/model/function.h"
|
||||
#include "xenia/debug/ui/model/module.h"
|
||||
#include "xenia/debug/ui/model/thread.h"
|
||||
#include "xenia/ui/loop.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace ui {
|
||||
namespace model {
|
||||
|
||||
using xe::debug::client::xdp::ExecutionState;
|
||||
|
||||
class System : public client::xdp::ClientListener {
|
||||
public:
|
||||
System(xe::ui::Loop* loop, client::xdp::XdpClient* client);
|
||||
|
||||
xe::ui::Loop* loop() const { return loop_; }
|
||||
client::xdp::XdpClient* client() const { return client_; }
|
||||
|
||||
ExecutionState execution_state();
|
||||
|
||||
std::vector<Module*> modules();
|
||||
std::vector<Thread*> threads();
|
||||
|
||||
Module* GetModuleByHandle(uint32_t module_handle);
|
||||
Thread* GetThreadByHandle(uint32_t thread_handle);
|
||||
|
||||
Delegate<void> on_execution_state_changed;
|
||||
Delegate<void> on_modules_updated;
|
||||
Delegate<void> on_threads_updated;
|
||||
|
||||
private:
|
||||
void OnExecutionStateChanged(ExecutionState execution_state) override;
|
||||
void OnModulesUpdated(
|
||||
std::vector<const proto::ModuleListEntry*> entries) override;
|
||||
void OnThreadsUpdated(
|
||||
std::vector<const proto::ThreadListEntry*> entries) override;
|
||||
|
||||
xe::ui::Loop* loop_ = nullptr;
|
||||
client::xdp::XdpClient* client_ = nullptr;
|
||||
|
||||
std::recursive_mutex mutex_;
|
||||
std::vector<std::unique_ptr<Module>> modules_;
|
||||
std::unordered_map<uint32_t, Module*> modules_by_handle_;
|
||||
std::vector<std::unique_ptr<Thread>> threads_;
|
||||
std::unordered_map<uint32_t, Thread*> threads_by_handle_;
|
||||
};
|
||||
|
||||
} // namespace model
|
||||
} // namespace ui
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_UI_MODEL_SYSTEM_H_
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/debug/ui/model/thread.h"
|
||||
|
||||
#include "xenia/debug/ui/model/system.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace ui {
|
||||
namespace model {
|
||||
|
||||
std::string Thread::to_string() {
|
||||
std::string value = entry_.name;
|
||||
if (is_host_thread()) {
|
||||
value += " (host)";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void Thread::Update(const proto::ThreadListEntry* entry) {
|
||||
if (!entry_.thread_handle) {
|
||||
std::memcpy(&entry_, entry, sizeof(entry_));
|
||||
} else {
|
||||
std::memcpy(&temp_entry_, entry, sizeof(temp_entry_));
|
||||
system_->loop()->Post(
|
||||
[this]() { std::memcpy(&entry_, &temp_entry_, sizeof(temp_entry_)); });
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace model
|
||||
} // namespace ui
|
||||
} // namespace debug
|
||||
} // namespace xe
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* 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_DEBUG_UI_MODEL_THREAD_H_
|
||||
#define XENIA_DEBUG_UI_MODEL_THREAD_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "xenia/debug/proto/xdp_protocol.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
namespace ui {
|
||||
namespace model {
|
||||
|
||||
class System;
|
||||
|
||||
class Thread {
|
||||
public:
|
||||
Thread(System* system) : system_(system) {}
|
||||
|
||||
bool is_dead() const { return is_dead_; }
|
||||
void set_dead(bool is_dead) { is_dead_ = is_dead; }
|
||||
|
||||
uint32_t thread_handle() const { return entry_.thread_handle; }
|
||||
uint32_t thread_id() const { return entry_.thread_id; }
|
||||
bool is_host_thread() const { return entry_.is_host_thread; }
|
||||
std::string name() const { return entry_.name; }
|
||||
const proto::ThreadListEntry* entry() const { return &entry_; }
|
||||
|
||||
std::string to_string();
|
||||
|
||||
void Update(const proto::ThreadListEntry* entry);
|
||||
|
||||
private:
|
||||
System* system_ = nullptr;
|
||||
bool is_dead_ = false;
|
||||
proto::ThreadListEntry entry_ = {0};
|
||||
proto::ThreadListEntry temp_entry_ = {0};
|
||||
};
|
||||
|
||||
} // namespace model
|
||||
} // namespace ui
|
||||
} // namespace debug
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_DEBUG_UI_MODEL_THREAD_H_
|
|
@ -14,6 +14,8 @@
|
|||
#include <string>
|
||||
|
||||
#include "el/elements.h"
|
||||
#include "el/event_handler.h"
|
||||
#include "xenia/debug/ui/application.h"
|
||||
|
||||
namespace xe {
|
||||
namespace debug {
|
||||
|
@ -25,14 +27,21 @@ class View {
|
|||
|
||||
std::string name() const { return name_; }
|
||||
el::LayoutBox* root_element() { return &root_element_; }
|
||||
xe::ui::Loop* loop() const { return Application::current()->loop(); }
|
||||
client::xdp::XdpClient* client() const { return client_; }
|
||||
model::System* system() const { return Application::current()->system(); }
|
||||
|
||||
virtual el::Element* BuildUI() = 0;
|
||||
|
||||
virtual void Setup(xe::debug::client::xdp::XdpClient* client) = 0;
|
||||
|
||||
protected:
|
||||
View(std::string name) : name_(name) {}
|
||||
|
||||
std::string name_;
|
||||
el::LayoutBox root_element_;
|
||||
std::unique_ptr<el::EventHandler> handler_;
|
||||
xe::debug::client::xdp::XdpClient* client_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
@ -16,6 +16,8 @@ namespace ui {
|
|||
namespace views {
|
||||
namespace cpu {
|
||||
|
||||
using namespace xe::debug::client::xdp;
|
||||
|
||||
CpuView::CpuView() : View("CPU") {}
|
||||
|
||||
CpuView::~CpuView() = default;
|
||||
|
@ -29,22 +31,13 @@ el::Element* CpuView::BuildUI() {
|
|||
.gravity(Gravity::kAll)
|
||||
.distribution(LayoutDistribution::kAvailable)
|
||||
.axis(Axis::kY)
|
||||
.child(
|
||||
LayoutBoxNode()
|
||||
.gravity(Gravity::kTop | Gravity::kLeftRight)
|
||||
.distribution(LayoutDistribution::kAvailable)
|
||||
.axis(Axis::kX)
|
||||
.skin("button_group")
|
||||
.child(ButtonNode("?"))
|
||||
.child(
|
||||
DropDownButtonNode().item("Module").item("Module").item(
|
||||
"Module")))
|
||||
.child(ListBoxNode()
|
||||
.gravity(Gravity::kAll)
|
||||
.item("fn")
|
||||
.item("fn")
|
||||
.item("fn")
|
||||
.item("fn"))
|
||||
.child(LayoutBoxNode()
|
||||
.gravity(Gravity::kTop | Gravity::kLeftRight)
|
||||
.distribution(LayoutDistribution::kAvailable)
|
||||
.axis(Axis::kX)
|
||||
.skin("button_group")
|
||||
.child(DropDownButtonNode().id("module_dropdown")))
|
||||
.child(ListBoxNode().id("function_listbox").gravity(Gravity::kAll))
|
||||
.child(LayoutBoxNode()
|
||||
.gravity(Gravity::kBottom | Gravity::kLeftRight)
|
||||
.distribution(LayoutDistribution::kAvailable)
|
||||
|
@ -102,10 +95,9 @@ el::Element* CpuView::BuildUI() {
|
|||
.distribution(LayoutDistribution::kGravity)
|
||||
.distribution_position(LayoutDistributionPosition::kLeftTop)
|
||||
.axis(Axis::kX)
|
||||
.child(ButtonNode("button"))
|
||||
.child(ButtonNode("button"))
|
||||
.child(ButtonNode("button")))
|
||||
.child(DropDownButtonNode().id("thread_dropdown")))
|
||||
.child(LayoutBoxNode()
|
||||
.id("source_content")
|
||||
.gravity(Gravity::kAll)
|
||||
.distribution(LayoutDistribution::kAvailable)
|
||||
.child(SplitContainerNode()
|
||||
|
@ -144,9 +136,91 @@ el::Element* CpuView::BuildUI() {
|
|||
root_element_.GetElementsById({
|
||||
//
|
||||
});
|
||||
handler_ = std::make_unique<el::EventHandler>(&root_element_);
|
||||
|
||||
handler_->Listen(el::EventType::kChanged, TBIDC("module_dropdown"),
|
||||
[this](const el::Event& ev) {
|
||||
UpdateFunctionList();
|
||||
return true;
|
||||
});
|
||||
|
||||
return &root_element_;
|
||||
}
|
||||
|
||||
void CpuView::Setup(XdpClient* client) {
|
||||
client_ = client;
|
||||
|
||||
system()->on_execution_state_changed.AddListener(
|
||||
[this]() { UpdateElementState(); });
|
||||
system()->on_modules_updated.AddListener([this]() { UpdateModuleList(); });
|
||||
system()->on_threads_updated.AddListener([this]() { UpdateThreadList(); });
|
||||
}
|
||||
|
||||
void CpuView::UpdateElementState() {
|
||||
root_element_.GetElementsById({
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
void CpuView::UpdateModuleList() {
|
||||
el::DropDownButton* module_dropdown;
|
||||
root_element_.GetElementsById({
|
||||
{TBIDC("module_dropdown"), &module_dropdown},
|
||||
});
|
||||
auto module_items = module_dropdown->default_source();
|
||||
auto modules = system()->modules();
|
||||
bool is_first = module_items->size() == 0;
|
||||
for (size_t i = module_items->size(); i < modules.size(); ++i) {
|
||||
auto module = modules[i];
|
||||
auto item = std::make_unique<el::GenericStringItem>(module->name());
|
||||
item->id = module->module_handle();
|
||||
module_items->push_back(std::move(item));
|
||||
}
|
||||
if (is_first) {
|
||||
module_dropdown->set_value(int(module_items->size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
void CpuView::UpdateFunctionList() {
|
||||
el::DropDownButton* module_dropdown;
|
||||
el::ListBox* function_listbox;
|
||||
root_element_.GetElementsById({
|
||||
{TBIDC("module_dropdown"), &module_dropdown},
|
||||
{TBIDC("function_listbox"), &function_listbox},
|
||||
});
|
||||
auto module_handle = module_dropdown->selected_item_id();
|
||||
auto module = system()->GetModuleByHandle(module_handle);
|
||||
if (!module) {
|
||||
function_listbox->default_source()->clear();
|
||||
return;
|
||||
}
|
||||
// TODO(benvanik): fetch list.
|
||||
}
|
||||
|
||||
void CpuView::UpdateThreadList() {
|
||||
el::DropDownButton* thread_dropdown;
|
||||
root_element_.GetElementsById({
|
||||
{TBIDC("thread_dropdown"), &thread_dropdown},
|
||||
});
|
||||
auto thread_items = thread_dropdown->default_source();
|
||||
auto threads = system()->threads();
|
||||
bool is_first = thread_items->size() == 0;
|
||||
for (size_t i = 0; i < thread_items->size(); ++i) {
|
||||
auto thread = threads[i];
|
||||
auto item = thread_items->at(i);
|
||||
item->str = thread->to_string();
|
||||
}
|
||||
for (size_t i = thread_items->size(); i < threads.size(); ++i) {
|
||||
auto thread = threads[i];
|
||||
auto item = std::make_unique<el::GenericStringItem>(thread->to_string());
|
||||
item->id = thread->thread_handle();
|
||||
thread_items->push_back(std::move(item));
|
||||
}
|
||||
if (is_first) {
|
||||
thread_dropdown->set_value(int(thread_items->size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace cpu
|
||||
} // namespace views
|
||||
} // namespace ui
|
||||
|
|
|
@ -28,7 +28,13 @@ class CpuView : public View {
|
|||
|
||||
el::Element* BuildUI() override;
|
||||
|
||||
void Setup(xe::debug::client::xdp::XdpClient* client) override;
|
||||
|
||||
protected:
|
||||
void UpdateElementState();
|
||||
void UpdateModuleList();
|
||||
void UpdateFunctionList();
|
||||
void UpdateThreadList();
|
||||
};
|
||||
|
||||
} // namespace cpu
|
||||
|
|
|
@ -32,9 +32,14 @@ el::Element* GpuView::BuildUI() {
|
|||
root_element_.GetElementsById({
|
||||
//
|
||||
});
|
||||
handler_ = std::make_unique<el::EventHandler>(&root_element_);
|
||||
return &root_element_;
|
||||
}
|
||||
|
||||
void GpuView::Setup(xe::debug::client::xdp::XdpClient* client) {
|
||||
//
|
||||
}
|
||||
|
||||
} // namespace gpu
|
||||
} // namespace views
|
||||
} // namespace ui
|
||||
|
|
|
@ -28,6 +28,8 @@ class GpuView : public View {
|
|||
|
||||
el::Element* BuildUI() override;
|
||||
|
||||
void Setup(xe::debug::client::xdp::XdpClient* client) override;
|
||||
|
||||
protected:
|
||||
};
|
||||
|
||||
|
|
|
@ -353,7 +353,7 @@ void KernelState::TerminateTitle(bool from_guest_thread) {
|
|||
|
||||
// Second: Kill all guest threads.
|
||||
for (auto it = threads_by_id_.begin(); it != threads_by_id_.end();) {
|
||||
if (it->second->guest_thread()) {
|
||||
if (it->second->is_guest_thread()) {
|
||||
auto thread = it->second;
|
||||
|
||||
if (from_guest_thread && XThread::IsInThread(thread)) {
|
||||
|
@ -362,7 +362,7 @@ void KernelState::TerminateTitle(bool from_guest_thread) {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (thread->running()) {
|
||||
if (thread->is_running()) {
|
||||
thread->Terminate(0);
|
||||
}
|
||||
|
||||
|
@ -457,6 +457,10 @@ void KernelState::OnThreadExit(XThread* thread) {
|
|||
xe::countof(args));
|
||||
}
|
||||
}
|
||||
|
||||
if (emulator()->debugger()) {
|
||||
emulator()->debugger()->OnThreadExit(thread);
|
||||
}
|
||||
}
|
||||
|
||||
object_ref<XThread> KernelState::GetThreadByID(uint32_t thread_id) {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "xenia/base/mutex.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/native_list.h"
|
||||
#include "xenia/kernel/objects/xevent.h"
|
||||
|
@ -70,6 +71,10 @@ XThread::~XThread() {
|
|||
// Unregister first to prevent lookups while deleting.
|
||||
kernel_state_->UnregisterThread(this);
|
||||
|
||||
if (emulator()->debugger()) {
|
||||
emulator()->debugger()->OnThreadDestroyed(this);
|
||||
}
|
||||
|
||||
delete apc_list_;
|
||||
|
||||
thread_.reset();
|
||||
|
@ -329,6 +334,10 @@ X_STATUS XThread::Create() {
|
|||
thread_->set_priority(creation_params_.creation_flags & 0x20 ? 1 : 0);
|
||||
}
|
||||
|
||||
if (emulator()->debugger()) {
|
||||
emulator()->debugger()->OnThreadCreated(this);
|
||||
}
|
||||
|
||||
if ((creation_params_.creation_flags & X_CREATE_SUSPENDED) == 0) {
|
||||
// Start the thread now that we're all setup.
|
||||
thread_->Resume();
|
||||
|
@ -344,17 +353,21 @@ X_STATUS XThread::Exit(int exit_code) {
|
|||
// TODO(benvanik); dispatch events? waiters? etc?
|
||||
RundownAPCs();
|
||||
|
||||
// Set exit code.
|
||||
X_KTHREAD* thread = guest_object<X_KTHREAD>();
|
||||
thread->header.signal_state = 1;
|
||||
thread->exit_status = exit_code;
|
||||
|
||||
kernel_state()->OnThreadExit(this);
|
||||
|
||||
if (emulator()->debugger()) {
|
||||
emulator()->debugger()->OnThreadExit(this);
|
||||
}
|
||||
|
||||
// NOTE: unless PlatformExit fails, expect it to never return!
|
||||
current_thread_tls = nullptr;
|
||||
xe::Profiler::ThreadExit();
|
||||
|
||||
// Set exit code
|
||||
X_KTHREAD* thread = guest_object<X_KTHREAD>();
|
||||
thread->header.signal_state = 1;
|
||||
thread->exit_status = exit_code;
|
||||
|
||||
running_ = false;
|
||||
Release();
|
||||
ReleaseHandle();
|
||||
|
@ -367,11 +380,15 @@ X_STATUS XThread::Exit(int exit_code) {
|
|||
X_STATUS XThread::Terminate(int exit_code) {
|
||||
// TODO: Inform the profiler that this thread is exiting.
|
||||
|
||||
// Set exit code
|
||||
// Set exit code.
|
||||
X_KTHREAD* thread = guest_object<X_KTHREAD>();
|
||||
thread->header.signal_state = 1;
|
||||
thread->exit_status = exit_code;
|
||||
|
||||
if (emulator()->debugger()) {
|
||||
emulator()->debugger()->OnThreadExit(this);
|
||||
}
|
||||
|
||||
running_ = false;
|
||||
Release();
|
||||
ReleaseHandle();
|
||||
|
|
|
@ -103,6 +103,14 @@ static_assert_size(X_KTHREAD, 0xAB0);
|
|||
|
||||
class XThread : public XObject {
|
||||
public:
|
||||
struct CreationParams {
|
||||
uint32_t stack_size;
|
||||
uint32_t xapi_thread_startup;
|
||||
uint32_t start_address;
|
||||
uint32_t start_context;
|
||||
uint32_t creation_flags;
|
||||
};
|
||||
|
||||
XThread(KernelState* kernel_state, uint32_t stack_size,
|
||||
uint32_t xapi_thread_startup, uint32_t start_address,
|
||||
uint32_t start_context, uint32_t creation_flags, bool guest_thread);
|
||||
|
@ -114,10 +122,11 @@ class XThread : public XObject {
|
|||
static uint32_t GetCurrentThreadHandle();
|
||||
static uint32_t GetCurrentThreadId();
|
||||
|
||||
const CreationParams* creation_params() const { return &creation_params_; }
|
||||
uint32_t tls_ptr() const { return tls_address_; }
|
||||
uint32_t pcr_ptr() const { return pcr_address_; }
|
||||
bool guest_thread() const { return guest_thread_; }
|
||||
bool running() const { return running_; }
|
||||
bool is_guest_thread() const { return guest_thread_; }
|
||||
bool is_running() const { return running_; }
|
||||
|
||||
cpu::ThreadState* thread_state() const { return thread_state_; }
|
||||
uint32_t thread_id() const { return thread_id_; }
|
||||
|
@ -163,13 +172,7 @@ class XThread : public XObject {
|
|||
void DeliverAPCs();
|
||||
void RundownAPCs();
|
||||
|
||||
struct {
|
||||
uint32_t stack_size;
|
||||
uint32_t xapi_thread_startup;
|
||||
uint32_t start_address;
|
||||
uint32_t start_context;
|
||||
uint32_t creation_flags;
|
||||
} creation_params_ = {0};
|
||||
CreationParams creation_params_ = {0};
|
||||
|
||||
uint32_t thread_id_ = 0;
|
||||
std::unique_ptr<xe::threading::Thread> thread_;
|
||||
|
|
Loading…
Reference in New Issue