From 74ca199c946497f5cccf6bc8f3be841abf074449 Mon Sep 17 00:00:00 2001 From: Michael Scire Date: Sun, 18 Jul 2021 23:22:44 -0700 Subject: [PATCH] dmnt: add attach support to gdbstub --- .../dmnt.gen2/source/dmnt2_gdb_server.cpp | 3 +- .../source/dmnt2_gdb_server_impl.cpp | 473 +++++++++++++++++- .../source/dmnt2_gdb_server_impl.hpp | 50 +- 3 files changed, 507 insertions(+), 19 deletions(-) diff --git a/stratosphere/dmnt.gen2/source/dmnt2_gdb_server.cpp b/stratosphere/dmnt.gen2/source/dmnt2_gdb_server.cpp index 0cca175b1..5b9474be7 100644 --- a/stratosphere/dmnt.gen2/source/dmnt2_gdb_server.cpp +++ b/stratosphere/dmnt.gen2/source/dmnt2_gdb_server.cpp @@ -25,6 +25,7 @@ namespace ams::dmnt { constexpr size_t ServerThreadStackSize = util::AlignUp(4 * GdbPacketBufferSize + os::MemoryPageSize, os::ThreadStackAlignment); alignas(os::ThreadStackAlignment) constinit u8 g_server_thread_stack[ServerThreadStackSize]; + alignas(os::ThreadStackAlignment) constinit u8 g_events_thread_stack[16_KB]; constinit os::ThreadType g_server_thread; @@ -68,7 +69,7 @@ namespace ams::dmnt { { /* Create gdb server for the socket. */ - util::ConstructAt(g_gdb_server, client_fd); + util::ConstructAt(g_gdb_server, client_fd, g_events_thread_stack, sizeof(g_events_thread_stack)); ON_SCOPE_EXIT { util::DestroyAt(g_gdb_server); }; /* Process for the server. */ diff --git a/stratosphere/dmnt.gen2/source/dmnt2_gdb_server_impl.cpp b/stratosphere/dmnt.gen2/source/dmnt2_gdb_server_impl.cpp index 18e83b40b..781fdaa5c 100644 --- a/stratosphere/dmnt.gen2/source/dmnt2_gdb_server_impl.cpp +++ b/stratosphere/dmnt.gen2/source/dmnt2_gdb_server_impl.cpp @@ -20,13 +20,11 @@ namespace ams::dmnt { namespace { - constexpr const char TargetXmlAarch64[] = "l" "" "" "aarch64" - "GNU/Linux" "" "" ""; @@ -299,6 +297,34 @@ namespace ams::dmnt { } } + constexpr u64 DecodeHex(const char *s) { + u64 value = 0; + + while (true) { + const char c = *(s++); + + if (int v = DecodeHex(c); v >= 0) { + value <<= 4; + value |= v & 0xF; + } else { + break; + } + } + + return value; + } + + void MemoryToHex(char *dst, const void *mem, size_t size) { + const u8 *mem_u8 = static_cast(mem); + + while (size-- > 0) { + const u8 v = *(mem_u8++); + *(dst++) = "0123456789abcdef"[v >> 4]; + *(dst++) = "0123456789abcdef"[v & 0xF]; + } + *dst = 0; + } + void ParseOffsetLength(const char *packet, u32 &offset, u32 &length) { /* Default to zero. */ offset = 0; @@ -325,11 +351,194 @@ namespace ams::dmnt { constinit char g_process_list_buffer[0x2000]; + constinit os::SdkMutex g_event_request_lock; + constinit os::SdkMutex g_event_lock; + constinit os::SdkConditionVariable g_event_request_cv; + constinit os::SdkConditionVariable g_event_done_cv; + } - GdbServerImpl::GdbServerImpl(int socket) : m_socket(socket), m_session(socket), m_packet_io() { /* ... */ } + GdbServerImpl::GdbServerImpl(int socket, void *stack, size_t stack_size) : m_socket(socket), m_session(socket), m_packet_io(), m_state(State::Initial), m_event(os::EventClearMode_AutoClear) { + /* Create and start the events thread. */ + R_ABORT_UNLESS(os::CreateThread(std::addressof(m_events_thread), DebugEventsThreadEntry, this, stack, stack_size, os::HighestThreadPriority - 1)); + os::StartThread(std::addressof(m_events_thread)); - GdbServerImpl::~GdbServerImpl() { /* ... */ } + /* Set our state. */ + m_state = State::Running; + } + + GdbServerImpl::~GdbServerImpl() { + /* Set ourselves as killed. */ + m_killed = true; + + /* Signal to our events thread. */ + { + std::scoped_lock lk(g_event_request_lock); + g_event_request_cv.Signal(); + } + + /* Signal our event. */ + m_event.Signal(); + + /* Wait for our thread to finish. */ + os::WaitThread(std::addressof(m_events_thread)); + os::DestroyThread(std::addressof(m_events_thread)); + + /* Clear our state. */ + m_state = State::Destroyed; + + /* Detach. */ + if (m_attached) { + R_ABORT_UNLESS(svc::CloseHandle(m_debug_handle)); + } + } + + void GdbServerImpl::DebugEventsThread() { + /* Process events. */ + { + std::scoped_lock lk(g_event_lock); + + /* Loop while we're not killed. */ + while (!m_killed) { + /* Wait for a request to come in. */ + g_event_request_cv.Wait(g_event_lock); + + /* Check that we're not killed now. */ + if (m_killed) { + break; + } + + /* Set ourselves as unattached. */ + m_attached = false; + + /* If we have a process id, attach. */ + if (R_FAILED(svc::DebugActiveProcess(std::addressof(m_debug_handle), m_process_id.value))) { + AMS_DMNT2_GDB_LOG_DEBUG("Failed to attach to %016lx\n", m_process_id.value); + m_debug_handle = svc::InvalidHandle; + g_event_done_cv.Signal(); + continue; + } + + /* Get the process id. */ + if (R_FAILED(svc::GetProcessId(std::addressof(m_process_id.value), m_debug_handle))) { + AMS_DMNT2_GDB_LOG_DEBUG("Failed to get process id for debug handle\n"); + R_ABORT_UNLESS(svc::CloseHandle(m_debug_handle)); + m_debug_handle = svc::InvalidHandle; + g_event_done_cv.Signal(); + continue; + } + + /* Set ourselves as attached. */ + m_attached = true; + + /* Get the create process event. */ + { + svc::DebugEventInfo d; + R_ABORT_UNLESS(svc::GetDebugEvent(std::addressof(d), m_debug_handle)); + AMS_ABORT_UNLESS(d.type == svc::DebugEvent_CreateProcess); + + m_create_process_info = d.info.create_process; + } + + /* Signal that we're done attaching. */ + g_event_done_cv.Signal(); + + /* Process debug events without the lock held. */ + { + g_event_lock.Unlock(); + this->ProcessDebugEvents(); + g_event_lock.Lock(); + } + + /* Clear our process ids. */ + m_process_id = os::InvalidProcessId; + m_target_process_id = os::InvalidProcessId; + } + } + + /* Set our state. */ + m_state = State::Exited; + } + + void GdbServerImpl::ProcessDebugEvents() { + AMS_DMNT2_GDB_LOG_DEBUG("Processing debug events for %016lx\n", m_process_id.value); + + while (true) { + /* Wait for an event to come in. */ + const Result wait_result = [&] ALWAYS_INLINE_LAMBDA { + std::scoped_lock lk(g_event_lock); + + s32 dummy = -1; + return svc::WaitSynchronization(std::addressof(dummy), std::addressof(m_debug_handle), 1, TimeSpan::FromMilliSeconds(20).GetNanoSeconds()); + }(); + + /* Check if we're killed. */ + if (m_killed) { + break; + } + + /* If we didn't get an event, try again. */ + if (svc::ResultTimedOut::Includes(wait_result)) { + continue; + } + + /* Try to get the event. */ + svc::DebugEventInfo d; + if (R_FAILED(svc::GetDebugEvent(std::addressof(d), m_debug_handle))) { + continue; + } + + /* Process the event. */ + switch (d.type) { + default: + AMS_DMNT2_GDB_LOG_DEBUG("Unhandled ProcessEvent %u\n", static_cast(d.type)); + break; + } + } + } + + void GdbServerImpl::SetStopReplyPacket(GdbSignal signal) { + /* Set the signal. */ + SetReply(m_reply_packet, "T%02X", static_cast(signal)); + + /* Get the thread context. */ + svc::ThreadContext thread_context = {}; + svc::GetDebugThreadContext(std::addressof(thread_context), m_debug_handle, m_thread_id, svc::ThreadContextFlag_General | svc::ThreadContextFlag_Control); + + /* Add important registers. */ + /* TODO: aarch32 */ + { + if (thread_context.fp != 0) { + AppendReply(m_reply_packet, "1d:%016lx", util::ConvertToBigEndian(thread_context.fp)); + } else { + AppendReply(m_reply_packet, "1d:0*,"); + } + + if (thread_context.sp != 0) { + AppendReply(m_reply_packet, ";1f:%016lx", util::ConvertToBigEndian(thread_context.sp)); + } else { + AppendReply(m_reply_packet, ";1f:0*,"); + } + + if (thread_context.pc != 0) { + AppendReply(m_reply_packet, ";20:%016lx", util::ConvertToBigEndian(thread_context.pc)); + } else { + AppendReply(m_reply_packet, ";20:0*,"); + } + } + + /* Add the thread id. */ + AppendReply(m_reply_packet, ";thread:p%lx.%lx", m_target_process_id.value, m_thread_id); + + /* Add the thread core. */ + { + u64 dummy; + u32 core = 0; + svc::GetDebugThreadParam(std::addressof(dummy), std::addressof(core), m_debug_handle, m_thread_id, svc::DebugThreadParam_CurrentCore); + + AppendReply(m_reply_packet, ";core:%u;", core); + } + } void GdbServerImpl::LoopProcess() { /* Process packets. */ @@ -366,6 +575,17 @@ namespace ams::dmnt { case 'H': this->H(); break; + case 'g': + if (!this->g()) { + m_killed = true; + } + break; + case 'm': + this->m(); + break; + case 'v': + this->v(); + break; case 'q': this->q(); break; @@ -383,8 +603,164 @@ namespace ams::dmnt { void GdbServerImpl::H() { if (this->HasDebugProcess()) { - /* TODO */ + if (ParsePrefix(m_receive_packet, "Hg") || ParsePrefix(m_receive_packet, "HG")) { + this->Hg(); + } else { + SetReplyError(m_reply_packet, "E01"); + } + } else { SetReplyError(m_reply_packet, "E01"); + } + } + + void GdbServerImpl::Hg() { + if (const char *dot = std::strchr(m_receive_packet, '.'); dot != nullptr) { + m_thread_id = DecodeHex(dot + 1); + AMS_DMNT2_GDB_LOG_DEBUG("Set thread id = %lx\n", m_thread_id); + + /* TODO: Validate thread exists for the process? */ + } + + SetReplyOk(m_reply_packet); + } + + bool GdbServerImpl::g() { + /* TODO: aarch32 */ + + /* Get thread context. */ + svc::ThreadContext thread_context; + if (R_FAILED(svc::GetDebugThreadContext(std::addressof(thread_context), m_debug_handle, m_thread_id, svc::ThreadContextFlag_All))) { + return false; + } + + /* Copy general purpose registers. */ + for (size_t i = 0; i < util::size(thread_context.r); ++i) { + if (thread_context.r[i] != 0) { + AppendReply(m_reply_packet, "%016lx", util::ConvertToBigEndian(thread_context.r[i])); + } else { + AppendReply(m_reply_packet, "0*,"); + } + } + + /* Copy special registers. */ + if (thread_context.fp != 0) { + AppendReply(m_reply_packet, "%016lx", util::ConvertToBigEndian(thread_context.fp)); + } else { + AppendReply(m_reply_packet, "0*,"); + } + + if (thread_context.lr != 0) { + AppendReply(m_reply_packet, "%016lx", util::ConvertToBigEndian(thread_context.lr)); + } else { + AppendReply(m_reply_packet, "0*,"); + } + + if (thread_context.sp != 0) { + AppendReply(m_reply_packet, "%016lx", util::ConvertToBigEndian(thread_context.sp)); + } else { + AppendReply(m_reply_packet, "0*,"); + } + + if (thread_context.pc != 0) { + AppendReply(m_reply_packet, "%016lx", util::ConvertToBigEndian(thread_context.pc)); + } else { + AppendReply(m_reply_packet, "0*,"); + } + + if (thread_context.pstate != 0) { + AppendReply(m_reply_packet, "%08x", util::ConvertToBigEndian(thread_context.pstate)); + } else { + AppendReply(m_reply_packet, "0*\"0"); + } + + /* Copy FPU registers. */ + for (size_t i = 0; i < util::size(thread_context.v); ++i) { + if (thread_context.v[i] != 0) { + AppendReply(m_reply_packet, "%016lx%016lx", util::ConvertToBigEndian(static_cast(thread_context.v[i] >> 0)), util::ConvertToBigEndian(static_cast(thread_context.v[i] >> BITSIZEOF(u64)))); + } else { + AppendReply(m_reply_packet, "0*<"); + } + } + + return true; + } + + void GdbServerImpl::m() { + ++m_receive_packet; + + /* Validate format. */ + const char *comma = std::strchr(m_receive_packet, ','); + if (comma == nullptr) { + SetReplyError(m_reply_packet, "E01"); + return; + } + + /* Parse address/length. */ + const u64 address = DecodeHex(m_receive_packet); + const u64 length = DecodeHex(comma + 1); + if (length >= sizeof(m_buffer)) { + SetReplyError(m_reply_packet, "E01"); + return; + } + + /* Read the memory. */ + /* TODO: Detect partial readability? */ + if (R_FAILED(svc::ReadDebugProcessMemory(reinterpret_cast(m_buffer), m_debug_handle, address, length))) { + SetReplyError(m_reply_packet, "E01"); + return; + } + + /* Encode the memory. */ + MemoryToHex(m_reply_packet, m_buffer, length); + } + + void GdbServerImpl::v() { + if (ParsePrefix(m_receive_packet, "vAttach;")) { + this->vAttach(); + } else { + AMS_DMNT2_GDB_LOG_DEBUG("Not Implemented v: %s\n", m_receive_packet); + } + } + + void GdbServerImpl::vAttach() { + if (!this->HasDebugProcess()) { + /* Get the process id. */ + if (const u64 process_id = DecodeHex(m_receive_packet); process_id != 0) { + /* Set our process id. */ + m_process_id = { process_id }; + m_target_process_id = { process_id }; + + /* Wait for us to be attached. */ + bool attached; + { + std::scoped_lock lk(g_event_request_lock); + g_event_request_cv.Signal(); + if (!g_event_done_cv.TimedWait(g_event_request_lock, TimeSpan::FromSeconds(2))) { + m_event.Signal(); + } + + attached = m_attached; + } + + /* If we're attached, send a stop reply packet. */ + if (attached) { + /* Set our initial thread id. */ + { + s32 dummy; + u64 thread_id = 0; + R_ABORT_UNLESS(svc::GetThreadList(std::addressof(dummy), std::addressof(thread_id), 1, m_debug_handle)); + + m_thread_id = thread_id; + } + + /* Set the stop reply packet. */ + this->SetStopReplyPacket(GdbSignal_Signal0); + } else { + SetReplyError(m_reply_packet, "E01"); + } + } else { + SetReplyError(m_reply_packet, "E01"); + } } else { SetReplyError(m_reply_packet, "E01"); } @@ -406,7 +782,11 @@ namespace ams::dmnt { void GdbServerImpl::qAttached() { if (this->HasDebugProcess()) { - /* TODO: Parse/Save the requested process id */ + /* Parse/Save the requested process id */ + if (const char *colon = std::strchr(m_receive_packet, ':'); colon != nullptr) { + m_target_process_id.value = DecodeHex(colon + 1); + AMS_DMNT2_GDB_LOG_DEBUG("Queried for attached to %016lx\n", m_target_process_id.value); + } SetReply(m_reply_packet, "1"); } else { SetReplyError(m_reply_packet, "E01"); @@ -415,8 +795,20 @@ namespace ams::dmnt { void GdbServerImpl::qC() { if (this->HasDebugProcess()) { - /* TODO */ - SetReplyError(m_reply_packet, "E01"); + /* Potentially get our thread id. */ + if (m_thread_id == 0) { + s32 dummy; + u64 thread_id = 0; + if (R_FAILED(svc::GetThreadList(std::addressof(dummy), std::addressof(thread_id), 1, m_debug_handle))) { + SetReplyError(m_reply_packet, "E01"); + return; + } + + m_thread_id = thread_id; + } + + /* Send the thread id. */ + SetReply(m_reply_packet, "QCp%lx.%lx", m_target_process_id.value, m_thread_id); } else { SetReplyError(m_reply_packet, "E01"); } @@ -430,10 +822,12 @@ namespace ams::dmnt { AppendReply(m_reply_packet, ";multiprocess+"); AppendReply(m_reply_packet, ";qXfer:osdata:read+"); AppendReply(m_reply_packet, ";qXfer:features:read+"); - AppendReply(m_reply_packet, ";qXfer:libraries:read+"); + AppendReply(m_reply_packet, ";qXfer:libraries-svr4:read+"); + AppendReply(m_reply_packet, ";augmented-libraries-svr4-read+"); AppendReply(m_reply_packet, ";qXfer:threads:read+"); AppendReply(m_reply_packet, ";swbreak+"); AppendReply(m_reply_packet, ";hwbreak+"); + AppendReply(m_reply_packet, ";vContSupported+"); } void GdbServerImpl::qXfer() { @@ -450,6 +844,11 @@ namespace ams::dmnt { /* Process. */ if (ParsePrefix(m_receive_packet, "features:read:")) { this->qXferFeaturesRead(); + } else if (ParsePrefix(m_receive_packet, "threads:read:")) { + if (!this->qXferThreadsRead()) { + m_killed = true; + SetReplyError(m_reply_packet, "E01"); + } } else { AMS_DMNT2_GDB_LOG_DEBUG("Not Implemented qxfer: %s\n", m_receive_packet); SetReplyError(m_reply_packet, "E01"); @@ -539,13 +938,10 @@ namespace ams::dmnt { /* Get the create process event. */ svc::DebugEventInfo d; - while (true) { - R_ABORT_UNLESS(svc::GetDebugEvent(std::addressof(d), handle)); - if (d.type == svc::DebugEvent_CreateProcess) { - AppendReply(g_process_list_buffer, "\n%lu\n%s\n\n", d.info.create_process.process_id, d.info.create_process.name); - break; - } - } + R_ABORT_UNLESS(svc::GetDebugEvent(std::addressof(d), handle)); + AMS_ABORT_UNLESS(d.type == svc::DebugEvent_CreateProcess); + + AppendReply(g_process_list_buffer, "\n%lu\n%s\n\n", d.info.create_process.process_id, d.info.create_process.name); } } } @@ -577,6 +973,51 @@ namespace ams::dmnt { } } + bool GdbServerImpl::qXferThreadsRead() { + /* Set header. */ + SetReply(m_reply_packet, "l"); + + /* Get thread list. */ + s32 num_threads; + u64 thread_ids[0x80]; + if (R_FAILED(svc::GetThreadList(std::addressof(num_threads), thread_ids, util::size(thread_ids), m_debug_handle))) { + return false; + } + + /* Cap thread limit at 128. */ + if (num_threads > 0x80) { + num_threads = 0x80; + } + + /* Add all threads. */ + for (s32 i = 0; i < num_threads; ++i) { + const auto thread_id = thread_ids[num_threads - 1 - i]; + + /* Check that we can get the thread context. */ + svc::ThreadContext thread_context; + if (R_FAILED(svc::GetDebugThreadContext(std::addressof(thread_context), m_debug_handle, thread_id, svc::ThreadContextFlag_All))) { + continue; + } + + /* Get the thread core. */ + u32 core = 0; + { + u64 dummy; + u32 val32; + if (R_SUCCEEDED(svc::GetDebugThreadParam(std::addressof(dummy), std::addressof(val32), m_debug_handle, m_thread_id, svc::DebugThreadParam_CurrentCore))) { + core = val32; + } + } + + /* TODO: `name=\"%s\"`? */ + AppendReply(m_reply_packet, "", m_target_process_id.value, thread_id, core); + } + + AppendReply(m_reply_packet, ""); + + return true; + } + void GdbServerImpl::QuestionMark() { /* TODO */ AMS_DMNT2_GDB_LOG_DEBUG("Not Implemented QuestionMark\n"); diff --git a/stratosphere/dmnt.gen2/source/dmnt2_gdb_server_impl.hpp b/stratosphere/dmnt.gen2/source/dmnt2_gdb_server_impl.hpp index dc868c848..63a253105 100644 --- a/stratosphere/dmnt.gen2/source/dmnt2_gdb_server_impl.hpp +++ b/stratosphere/dmnt.gen2/source/dmnt2_gdb_server_impl.hpp @@ -20,6 +20,26 @@ namespace ams::dmnt { class GdbServerImpl { + private: + enum class State { + Initial, + Running, + Exited, + Destroyed, + }; + + enum GdbSignal { + GdbSignal_Signal0 = 0, + GdbSignal_Interrupt = 2, + GdbSignal_IllegalInstruction = 4, + GdbSignal_BreakpointTrap = 5, + GdbSignal_EmulationTrap = 7, + GdbSignal_ArithmeticException = 8, + GdbSignal_Killed = 9, + GdbSignal_BusError = 10, + GdbSignal_SegmentationFault = 11, + GdbSignal_BadSystemCall = 12, + }; private: int m_socket; HtcsSession m_session; @@ -27,9 +47,18 @@ namespace ams::dmnt { char *m_receive_packet{nullptr}; char *m_reply_packet{nullptr}; char m_buffer[GdbPacketBufferSize / 2]; + bool m_killed{false}; svc::Handle m_debug_handle{svc::InvalidHandle}; + os::ThreadType m_events_thread; + State m_state; + bool m_attached{false}; + u64 m_thread_id{0}; + os::ProcessId m_process_id{os::InvalidProcessId}; + os::ProcessId m_target_process_id{os::InvalidProcessId}; + svc::DebugInfoCreateProcess m_create_process_info; + os::Event m_event; public: - GdbServerImpl(int socket); + GdbServerImpl(int socket, void *thread_stack, size_t stack_size); ~GdbServerImpl(); void LoopProcess(); @@ -40,9 +69,25 @@ namespace ams::dmnt { char *ReceivePacket(bool *out_break, char *dst, size_t size) { return m_packet_io.ReceivePacket(out_break, dst, size, std::addressof(m_session)); } private: bool HasDebugProcess() const { return m_debug_handle != svc::InvalidHandle; } - bool Is64Bit() const { return true; /* TODO: Retrieve from debug process info. */ } + bool Is64Bit() const { return (m_create_process_info.flags & svc::CreateProcessFlag_Is64Bit) == svc::CreateProcessFlag_Is64Bit; } + bool Is64BitAddressSpace() const { return (m_create_process_info.flags & svc::CreateProcessFlag_AddressSpaceMask) == svc::CreateProcessFlag_AddressSpace64Bit; } + private: + static void DebugEventsThreadEntry(void *arg) { static_cast(arg)->DebugEventsThread(); } + void DebugEventsThread(); + void ProcessDebugEvents(); + void SetStopReplyPacket(GdbSignal signal); private: void H(); + void Hg(); + + bool g(); + + void m(); + + void v(); + + void vAttach(); + void q(); void qAttached(); @@ -51,6 +96,7 @@ namespace ams::dmnt { void qXfer(); void qXferFeaturesRead(); void qXferOsdataRead(); + bool qXferThreadsRead(); void QuestionMark(); };