diff --git a/.gitmodules b/.gitmodules index e69de29b..10b902db 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/flatbuffers"] + path = vendor/flatbuffers + url = https://github.com/google/flatbuffers.git diff --git a/CMakeLists.txt b/CMakeLists.txt index bba15b80..1b7eb3bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,32 @@ if (NOT BN_INTERNAL_BUILD) message("CMAKE_PREFIX_PATH is: ${CMAKE_PREFIX_PATH}") endif() +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# FlatBuffers is used for the x2win RPC protocol. Its C++ runtime is header-only (see +# FlatBuffers_Library_SRCS in vendor/flatbuffers/CMakeLists.txt -- every entry is a .h, no .cpp), +# so unlike Protobuf/Abseil (formerly vendored here for the same protocol, since removed) there's +# no compiled static lib whose CRT/ABI settings need to match whatever links against it -- only +# flatc (the schema compiler) is an actual build-time binary. That's what let x2winstub drop its +# MSVC-ABI-pinned toolchain requirement for MinGW-w64 once the protocol finished migrating over. +# flatbuffers_generate_headers() (used by core/CMakeLists.txt and x2winstub/CMakeLists.txt) comes +# from vendor/flatbuffers/CMake/BuildFlatBuffers.cmake, which flatbuffers' own CMakeLists.txt +# already include()s, so no separate include() is needed here the way protobuf-generate.cmake was. +set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(FLATBUFFERS_INSTALL OFF CACHE BOOL "" FORCE) +set(FLATBUFFERS_BUILD_FLATC ON CACHE BOOL "" FORCE) +add_subdirectory(vendor/flatbuffers) + +# Generated once here and shared via target_link_libraries(... x2win_fbs) from both +# core/CMakeLists.txt and x2winstub/CMakeLists.txt, rather than calling +# flatbuffers_generate_headers() separately from each (which would define two CMake targets +# both named "x2win_fbs" and fail to configure -- target names must be unique project-wide). +flatbuffers_generate_headers( + TARGET x2win_fbs + SCHEMAS ${CMAKE_SOURCE_DIR}/protocol/x2win.fbs +) + add_subdirectory(core) add_subdirectory(api) @@ -58,6 +84,7 @@ endif() # WinDbg installer CLI (standalone, spawned by debuggercore API) if(WIN32) add_subdirectory(installer) + add_subdirectory(x2winstub) endif() # Documentation validation target diff --git a/build.md b/build.md index 10555c96..15d00e86 100644 --- a/build.md +++ b/build.md @@ -22,9 +22,14 @@ git checkout dev - Build the debugger + FlatBuffers (needed for `X2WinRpcAdapter`'s wire protocol) is vendored as a git submodule + under `vendor/` and built as part of this project's own CMake configure/build -- no separate + install step needed, just make sure submodules are cloned (`--recurse-submodules` below, or + `git submodule update --init --recursive` after the fact). + ```bash # Get the source -git clone https://github.com/Vector35/debugger.git +git clone --recurse-submodules https://github.com/Vector35/debugger.git # Do an out-of-source build mkdir -p build diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 840a6f36..cd53e9cb 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -38,6 +38,8 @@ file(GLOB ADAPTER_SOURCES CONFIGURE_DEPENDS adapters/esrevenadapter.h adapters/lldbcoredumpadapter.cpp adapters/lldbcoredumpadapter.h + adapters/x2winrpcadapter.cpp + adapters/x2winrpcadapter.h ) if(WIN32) @@ -212,6 +214,26 @@ else() ) endif() +# FlatBuffers for the x2win RPC protocol (protocol/x2win.fbs), generated once at the top-level +# CMakeLists.txt and shared with x2winstub/CMakeLists.txt. +# +# Deliberately not target_link_libraries(debuggercore x2win_fbs): every other +# target_link_libraries() call on debuggercore in this file uses the plain (no PUBLIC/PRIVATE) +# signature, and CMake forbids mixing plain and keyword signatures for the same target anywhere +# in the project -- so a PRIVATE-only x2win_fbs link isn't an option here. Plain/public would +# instead propagate x2win_fbs's generated-header "source" to every downstream consumer of +# debuggercore (ui, cli), which fails to configure because that generated file, from their +# directory scope, isn't recognized as a build product (GENERATED doesn't propagate cross-directory +# pre-CMake 3.20 semantics). Depending on the include dir + generation step directly sidesteps +# target_link_libraries entirely, so it stays private to debuggercore without touching the +# project's existing plain-signature convention. +add_dependencies(debuggercore GENERATE_x2win_fbs) +target_include_directories(debuggercore PRIVATE + ${CMAKE_BINARY_DIR}/x2win_fbs + ${CMAKE_SOURCE_DIR}/vendor/flatbuffers/include +) + + if (WIN32) add_custom_command(TARGET debuggercore PRE_LINK COMMAND ${CMAKE_COMMAND} -E echo "Copying DbgEng DLLs" diff --git a/core/adapters/x2winrpcadapter.cpp b/core/adapters/x2winrpcadapter.cpp new file mode 100644 index 00000000..03135b4e --- /dev/null +++ b/core/adapters/x2winrpcadapter.cpp @@ -0,0 +1,1117 @@ +#include "./x2winrpcadapter.h" +#include + +using namespace BinaryNinjaDebugger; + +// Just forwards to the DebugAdapter base constructor; socket/thread state is set up later in +// Attach()/Connect(), not here. +X2WinRpcAdapter::X2WinRpcAdapter(BinaryView* data): DebugAdapter(data){ + GenerateDefaultAdapterSettings(data); +} + +// Same pattern as WindowsNativeAdapter::GenerateDefaultAdapterSettings (core/adapters/windowsnativeadapter.cpp): +// only fill in a default when the setting was never explicitly set for this resource, so a value the user +// already typed/picked (e.g. via common.inputFile's uiSelectionAction:"file") is never clobbered. +void X2WinRpcAdapter::GenerateDefaultAdapterSettings(BinaryView* data){ + auto adapterSettings = GetAdapterSettings(); + BNSettingsScope scope = SettingsResourceScope; + adapterSettings->Get("common.inputFile", data, &scope); + if(scope != SettingsResourceScope) + adapterSettings->Set("common.inputFile", data->GetFile()->GetOriginalFilename(), data, SettingsResourceScope); +} + +X2WinRpcAdapter::~X2WinRpcAdapter(){ + LogInfo("X2WinRpcAdapter::~X2WinRpcAdapter: adapter object being destroyed (connected=%d)", (int)m_connected); + // Force the blocking Recv() inside ReaderLoop() to fail and return, so the loop can exit + // and join() below won't hang forever waiting for a thread that never stops on its own. + TeardownConnection(); +} + +Ref X2WinRpcAdapter::GetAdapterSettings(){ + return X2WinRpcAdapterType::GetAdapterSettings(); +} + +bool X2WinRpcAdapter::ConnectSocket(const std::string& ip, uint16_t port){ + if(m_connected){ + return true; + } + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + inet_pton(AF_INET, ip.c_str(), &addr.sin_addr); + + m_socket = Socket(AF_INET, SOCK_STREAM, 0); + if(!m_socket.Connect(addr)){ + LogWarn("X2WinRpcAdapter: failed to connect to %s:%u", ip.c_str(), (unsigned)port); + return false; + } + + m_readerThread = std::thread([this]() {ReaderLoop();}); + m_connected = true; + + LogInfo("X2WinRpcAdapter: connected to %s:%u", ip.c_str(), (unsigned)port); + return true; +} + +bool X2WinRpcAdapter::ConnectFromSettings(){ + auto adapterSettings = GetAdapterSettings(); + auto data = GetData(); + + BNSettingsScope scope = SettingsResourceScope; + auto ipAddress = adapterSettings->Get("connect.ipAddress", data, &scope); + scope = SettingsResourceScope; + auto port = adapterSettings->Get("connect.port", data, &scope); + + return ConnectSocket(ipAddress, (uint16_t)port); +} + +// Connects to the stub and asks it to attach to an already-running Windows process by pid. +bool X2WinRpcAdapter::Attach(std::uint32_t pid){ + if(!ConnectFromSettings()){ + LogWarn("X2WinRpcAdapter::Attach: failed to connect to stub"); + return false; + } + + X2WinEnvelopeBuffer response = CallSync(x2win::Body_AttachRequest, [pid](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateAttachRequest(b, pid).Union(); + }); + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success) + LogWarn("X2WinRpcAdapter::Attach: stub rejected attach to pid %u", (unsigned)pid); + else + m_lastConnectionWasTargetMode = false; + + ApplyBreakPoints(); + return success; +} + +bool X2WinRpcAdapter::Connect(const std::string& server, std::uint32_t port){ + if(!ConnectSocket(server, (uint16_t) port)){ + return false; + } + m_lastConnectionWasTargetMode = true; + ApplyBreakPoints(); + return true; +} + +bool X2WinRpcAdapter::Execute(const std::string& path, const LaunchConfigurations& configs){ + return ExecuteWithArgs(path, "", "", configs); +} + +bool X2WinRpcAdapter::ConnectToDebugServer(const std::string &server, std::uint32_t port){ + if(!ConnectSocket(server, (uint16_t)port)) return false; + + X2WinEnvelopeBuffer response = CallSync(x2win::Body_ConnectServerRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateConnectServerRequest(b).Union(); + }); + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success) + LogWarn("X2WinRpcAdapter::ConnectToDebugServer: stub rejected connect_server_request (stub not in server mode?)"); + else + m_lastConnectionWasTargetMode = false; + + return success; +} + +bool X2WinRpcAdapter::DisconnectDebugServer(){ + if(!m_connected){ + return true; + } + + CallSync(x2win::Body_QuitRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateQuitRequest(b).Union(); + }); + + LogInfo("X2WinRpcAdapter::DisconnectDebugServer: closing connection to stub"); + TeardownConnection(); + return true; +} + +bool X2WinRpcAdapter::ExecuteWithArgs(const std::string& path, const std::string& args, + const std::string& workingDir, const LaunchConfigurations& configs){ + if(m_lastConnectionWasTargetMode){ + LogWarn("X2WinRpcAdapter::ExecuteWithArgs: refusing to launch -- last connection was " + "target mode, which only ever supports its original debuggee."); + return false; + } + if(!ConnectFromSettings()){ + LogWarn("X2WinRpcAdapter::ExecuteWithArgs: failed to connect to stub"); + return false; + } + + X2WinEnvelopeBuffer response = CallSync(x2win::Body_LaunchRequest, + [&path, &args, &workingDir](flatbuffers::FlatBufferBuilder& b){ + auto pathOff = b.CreateString(path); + auto argsOff = b.CreateString(args); + auto workingDirOff = b.CreateString(workingDir); + return x2win::CreateLaunchRequest(b, pathOff, argsOff, workingDirOff).Union(); + }); + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success) + LogWarn("X2WinRpcAdapter::ExecuteWithArgs: stub failed to launch \"%s\"", path.c_str()); + + ApplyBreakPoints(); + return success; +} + +// TCP is a byte stream, not a message stream: a single Recv() call may return fewer bytes than +// requested. Loop until exactly `size` bytes have been collected (or the connection dies). +bool X2WinRpcAdapter::RecvExact(void* buffer, size_t size){ + uint8_t* p = (uint8_t*) buffer; + size_t received = 0; + while(received < size){ + intptr_t n = m_socket.Recv((char*)p+received, (int32_t)(size-received)); + if(n <= 0){ + LogWarn("X2WinRpcAdapter: RecvExact failed after %zu/%zu bytes (n=%lld, %s)", + received, size, (long long)n, n == 0 ? "connection closed" : "socket error"); + return false; // 0 connection cloased, <0 error + } + received += (size_t) n; + } + return true; +} + +bool X2WinRpcAdapter::SendExact(const void *buffer, size_t size){ + const uint8_t* p = (const uint8_t*) buffer; + size_t sent = 0; + while (sent < size) { + intptr_t n = m_socket.Send((char*)p + sent, (int32_t)(size - sent)); + if(n <= 0){ + LogWarn("X2WinRpcAdapter: SendExact failed after %zu/%zu bytes (n=%lld)", + sent, size, (long long)n); + return false; + } + sent += (size_t) n; + } + return true; +} + +// Sends one Request frame and blocks the calling thread until ReaderLoop() receives the +// matching Response (matched by requestId) and fulfills the promise registered below. +// Multiple concurrent callers each get their own request_id/promise, so a slow response to one +// call never blocks another call's response from being delivered. +X2WinEnvelopeBuffer X2WinRpcAdapter::CallSync(x2win::Body bodyType, + const std::function(flatbuffers::FlatBufferBuilder&)>& buildBody){ + uint64_t requestId = m_nextRequestId++; + + // Bottom-up construction: the body table (built by the caller's callback) has to be + // finished before the Envelope that wraps it, so both have to share this one builder. + flatbuffers::FlatBufferBuilder builder; + flatbuffers::Offset bodyOffset = buildBody(builder); + auto envelope = x2win::CreateEnvelope(builder, requestId, bodyType, bodyOffset); + builder.Finish(envelope); + + std::promise promise; + std::future future = promise.get_future(); + + { + std::lock_guard lock(m_pendingMutex); + m_pendingRequests[requestId] = std::move(promise); + } + + std::vector frame; + uint32_t bodyLen = (uint32_t)builder.GetSize(); + for(int i = 0; i < 4; i++){ + frame.push_back((bodyLen >> (i*8)) & 0xff); + } + const uint8_t* bufPtr = builder.GetBufferPointer(); + frame.insert(frame.end(), bufPtr, bufPtr + builder.GetSize()); + + // Unconditional, not just on failure: this is the only way to tell "we're stuck waiting for + // a response that's never coming" (send succeeded, future.get() below just never returns) + // apart from a plain teardown/failure -- without this, a hang below is indistinguishable + // from "nothing happened yet" in the log. LogInfo, not LogDebug -- the Log panel filters + // Debug-level messages out by default, which would make this call invisible right when we + // need it most. + LogInfo("X2WinRpcAdapter::CallSync: sending request_id=%llu body_type=%d", + (unsigned long long)requestId, (int)bodyType); + + { + std::lock_guard lock(m_sendMutex); + if(!SendExact(frame.data(), frame.size())){ + LogWarn("X2WinRpcAdapter::CallSync: failed to send request_id=%llu body_type=%d, treating as failed call", + (unsigned long long)requestId, (int)bodyType); + std::lock_guard pendingLock(m_pendingMutex); + m_pendingRequests.erase(requestId); + return X2WinEnvelopeBuffer(); + } + } + + X2WinEnvelopeBuffer response = future.get(); + LogInfo("X2WinRpcAdapter::CallSync: received response for request_id=%llu", + (unsigned long long)requestId); + return response; +} + +// Dedicated socket-reader loop, run on m_readerThread. Never used for a "write then read" +// call -- it just pulls frames forever and dispatches them, so unsolicited Event frames can +// arrive at any time, even while some other call is waiting inside CallSync() above. +void X2WinRpcAdapter::ReaderLoop(){ + while (true) { + uint8_t lenBuf[4]; + if(!RecvExact(lenBuf, 4)){ + LogInfo("X2WinRpcAdapter::ReaderLoop: failed to read frame length prefix, exiting reader loop"); + break; + } + uint32_t bodyLen = (uint32_t)lenBuf[0] | ((uint32_t)lenBuf[1] << 8) | ((uint32_t)lenBuf[2] << 16) | ((uint32_t)lenBuf[3] << 24); + + X2WinEnvelopeBuffer envelopeBuf; + envelopeBuf.bytes.resize(bodyLen); + if(!RecvExact(envelopeBuf.bytes.data(), bodyLen)){ + LogWarn("X2WinRpcAdapter::ReaderLoop: failed to read %u-byte frame body, exiting reader loop", bodyLen); + break; + } + + // Unlike Protobuf's ParseFromArray, FlatBuffers does no validation on access by default -- + // GetEnvelope() below just reinterprets these bytes as a table, and reading fields out of + // a truncated/corrupted buffer is an out-of-bounds read, not a clean failure. Verifier is + // what actually plays ParseFromArray's role here: walking the buffer to confirm every + // offset/vector/string is in-bounds before anything touches it. + flatbuffers::Verifier verifier(envelopeBuf.bytes.data(), envelopeBuf.bytes.size()); + if(!x2win::VerifyEnvelopeBuffer(verifier)){ + // A verify failure here almost always means the length-prefixed framing has desynced + // (e.g. an unsynchronized/partial Send() on the other end split a frame) -- everything + // received after this point on this connection is suspect until reconnecting. + LogWarn("X2WinRpcAdapter::ReaderLoop: failed to verify %u-byte envelope -- protocol framing " + "may be desynced, treating connection as unreliable", bodyLen); + continue; + } + + const x2win::Envelope* envelope = envelopeBuf.Get(); + + if(envelope->body_type() == x2win::Body_TargetStoppedEvent){ + const auto* evt = envelope->body_as(); + if(evt->reason() == x2win::StopReason_EXITED){ + LogInfo("X2WinRpcAdapter::ReaderLoop: received TargetStoppedEvent reason=EXITED exit_code=%llu", + (unsigned long long)evt->exit_code()); + m_lastStopReason = DebugStopReason::ProcessExited; + m_exitCode = evt->exit_code(); + + DebuggerEvent event; + event.type = TargetExitedEventType; + event.data.exitData.exitCode = evt->exit_code(); + PostDebuggerEvent(event); + continue; + } + BNDebugStopReason reason = (evt->reason() == x2win::StopReason_BREAKPOINT) ? DebugStopReason::Breakpoint + : (evt->reason() == x2win::StopReason_SINGLE_STEP) ? DebugStopReason::SingleStep + : (evt->reason() == x2win::StopReason_INITIAL_BREAKPOINT) ? DebugStopReason::InitialBreakpoint + : DebugStopReason::UnknownReason; + + LogInfo("X2WinRpcAdapter::ReaderLoop: received TargetStoppedEvent reason=%d address=0x%llx", + (int)evt->reason(), (unsigned long long)evt->address()); + + m_lastStopReason = reason; + m_lastStopAddress = evt->address(); + + // Second chance for any breakpoint that couldn't resolve right after Attach/Launch/Connect + // (module list not populated yet at that point) -- by the time any stop event arrives, the + // module list is guaranteed complete. + ApplyBreakPoints(); + + DebuggerEvent event; + event.type = AdapterStoppedEventType; + event.data.targetStoppedData.reason = reason; + PostDebuggerEvent(event); + continue; + } + + // Otherwise this is a reply to something CallSync() is blocked waiting on. + std::lock_guard lock(m_pendingMutex); + auto it = m_pendingRequests.find(envelope->request_id()); + if(it != m_pendingRequests.end()){ + it->second.set_value(std::move(envelopeBuf)); + m_pendingRequests.erase(it); + }else{ + // No CallSync() is waiting on this request_id -- either a duplicate/late response, or + // (more likely if this shows up unexpectedly) evidence of the framing desync described + // above: bytes from a corrupted frame happened to parse into a plausible-looking envelope. + LogWarn("X2WinRpcAdapter::ReaderLoop: received response for unknown request_id=%llu body_type=%d, dropping", + (unsigned long long)envelope->request_id(), (int)envelope->body_type()); + } + } +} + +// Simplest example of the repeating "send request, decode response" shape most methods follow: +// the reply payload is just the architecture string's raw bytes. +std::string X2WinRpcAdapter::GetTargetArchitecture(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_GetTargetArchRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGetTargetArchRequest(b).Union(); + }); + const auto* resp = response.BodyAs(); + return (resp && resp->architecture()) ? resp->architecture()->str() : std::string(); +} + +// --- Lifecycle --- +bool X2WinRpcAdapter::Detach(){ + LogInfo("X2WinRpcAdapter::Detach: called"); + X2WinEnvelopeBuffer response = CallSync(x2win::Body_DetachRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateDetachRequest(b).Union(); + }); + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success) + LogWarn("X2WinRpcAdapter::Detach: stub reported failure"); + + if(m_lastConnectionWasTargetMode){ + TeardownConnection(); + }else{ + ResetSessionState(); + } + + DebuggerEvent event; + event.type = DetachedEventType; + PostDebuggerEvent(event); + + return success; +} + +bool X2WinRpcAdapter::Quit(){ + LogInfo("X2WinRpcAdapter::Quit: called"); + X2WinEnvelopeBuffer response = CallSync(x2win::Body_QuitRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateQuitRequest(b).Union(); + }); + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success) + LogWarn("X2WinRpcAdapter::Quit: stub reported failure"); + + if(m_lastConnectionWasTargetMode){ + TeardownConnection(); + }else{ + ResetSessionState(); + } + + DebuggerEvent event; + event.type = TargetExitedEventType; + event.data.exitData.exitCode = 0; + PostDebuggerEvent(event); + + return success; +} + +std::vector X2WinRpcAdapter::GetProcessList(){ + if(!m_connected){ + LogWarn("X2WinRpcAdapter::GetProcessList: not connected -- connect to the debug server first"); + return {}; + } + + X2WinEnvelopeBuffer response = CallSync(x2win::Body_GetProcessListRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGetProcessListRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + + std::vector result; + if(resp && resp->processes()){ + for(const auto* p : *resp->processes()){ + result.emplace_back(p->pid(), p->name() ? p->name()->str() : std::string()); + } + } + + LogDebug("X2WinRpcAdapter::GetProcessList: got %zu process(es)", result.size()); + return result; +} + +std::uint32_t X2WinRpcAdapter::GetActivePID(){ return 0; } +std::vector X2WinRpcAdapter::GetThreadList(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_GetThreadListRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGetThreadListRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + std::vector result; + if(resp && resp->threads()){ + for(const auto* t : *resp->threads()){ + // DebugThread has no ctor that takes is_frozen -- build with (tid, rip), then set + // the field directly (m_isFrozen is a plain public bool, same as every other member). + DebugThread thread((std::uint32_t)t->tid(), (std::uintptr_t)t->rip()); + thread.m_isFrozen = t->is_frozen(); + result.push_back(thread); + } + } + LogDebug("X2WinRpcAdapter::GetThreadList: got %zu thread(s)", result.size()); + return result; +} + +DebugThread X2WinRpcAdapter::GetActiveThread() const { + // CallSync() isn't const (it does real socket I/O) but this override has to be -- same + // const_cast workaround GdbMiAdapter::GetActiveThread() uses (core/adapters/gdbmiadapter.cpp). + auto* self = const_cast(this); + X2WinEnvelopeBuffer response = self->CallSync(x2win::Body_GetActiveThreadIdRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGetActiveThreadIdRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + std::uint32_t tid = resp ? resp->tid() : 0; + + // See the comment on GetActiveThreadIdResponse in x2win.fbs -- rip comes from the last + // reported stop, not a separate RPC round trip. + return DebugThread(tid, (std::uintptr_t)self->GetInstructionOffset()); +} + +std::uint32_t X2WinRpcAdapter::GetActiveThreadId() const { + auto* self = const_cast(this); + X2WinEnvelopeBuffer response = self->CallSync(x2win::Body_GetActiveThreadIdRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGetActiveThreadIdRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + return resp ? resp->tid() : 0; +} + +bool X2WinRpcAdapter::SetActiveThread(const DebugThread& thread){ + return SetActiveThreadId(thread.m_tid); +} + +bool X2WinRpcAdapter::SetActiveThreadId(std::uint32_t tid){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_SetActiveThreadIdRequest, [tid](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateSetActiveThreadIdRequest(b, tid).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::SetActiveThreadId: stub rejected switch to tid %u", (unsigned)tid); + } + return success; +} + +bool X2WinRpcAdapter::SuspendThread(std::uint32_t tid){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_SuspendThreadRequest, [tid](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateSuspendThreadRequest(b, tid).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::SuspendThread: stub rejected suspending tid %u", (unsigned)tid); + } + return success; +} + +bool X2WinRpcAdapter::ResumeThread(std::uint32_t tid){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_ResumeThreadRequest, [tid](flatbuffers::FlatBufferBuilder&b){ + return x2win::CreateResumeThreadRequest(b, tid).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::ResumeThread: stub rejected resuming tid %u", (unsigned)tid); + } + return success; +} + + +std::vector X2WinRpcAdapter::GetFramesOfThread(std::uint32_t tid){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_GetFramesOfThreadRequest, [tid](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGetFramesOfThreadRequest(b, tid).Union(); + }); + + const auto* resp = response.BodyAs(); + std::vector result; + if(resp && resp->frames()){ + for(const auto* f: *resp->frames()){ + std::string functionName = f->function_name() ? f->function_name()->str() : std::string(); + std::string module = f->module_() ? f->module_()->str() : std::string(""); + result.emplace_back((size_t)f->index(), f->pc(), f->sp(), f->fp(), functionName, f->function_start(), module); + } + } + LogDebug("X2WinRpcAdapter::GetFramesOfThread: got %zu frame(s) for tid %u", result.size(), (unsigned)tid); + return result; +} + +DebugBreakpoint X2WinRpcAdapter::AddBreakpoint(const std::uintptr_t address, unsigned long breakpoint_type){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_SetBreakpointRequest, [address](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateSetBreakpointRequest(b, address, x2win::BreakpointType_SOFTWARE).Union(); + }); + + const auto* resp = response.BodyAs(); + if(!resp || !resp->success()){ + LogWarn("X2WinRpcAdapter::AddBreakpoint: stub rejected breakpoint at 0x%llx", (unsigned long long)address); + return DebugBreakpoint(); + } + + DebugBreakpoint bp(address, (unsigned long)resp->breakpoint_id(), true, SoftwareBreakpoint); + m_breakpoints.push_back(bp); + + return bp; +} +DebugBreakpoint X2WinRpcAdapter::AddBreakpoint(const ModuleNameAndOffset& address, unsigned long breakpoint_type){ + // DebuggerBreakpoints::Apply() replays every breakpoint BN core already knows about as soon as + // CreateDebugAdapter() creates/reuses this adapter -- which happens BEFORE Attach()/ + // ExecuteWithArgs()/Connect() has actually opened the socket. Trying to resolve+send at that + // point just fails silently (not connected yet), and the breakpoint never makes it to a freshly + // (re)connected stub -- this is exactly what was happening after a host-initiated disconnect + + // stub restart. Stage it instead; ApplyBreakpoints() flushes the staged list for real once + // connected. This has to happen here, at the ModuleNameAndOffset level, not in the uintptr_t + // overload above -- module+offset is the only form that can still be resolved after a later + // reconnect, once ResolveModuleAddress()/GetModuleList() actually works again. + if(!m_connected){ + if(std::find(m_pendingBreakpoints.begin(), m_pendingBreakpoints.end(), address) == m_pendingBreakpoints.end()){ + m_pendingBreakpoints.push_back(address); + } + return DebugBreakpoint(); + } + + uint64_t resolved = 0; + if(!ResolveModuleAddress(address, resolved)){ + // Connected, but the module isn't loaded/resolvable yet (e.g. ApplyBreakpoints() ran right + // after Launch succeeded, before the stub's module list reflects the new process). Re-stage + // rather than dropping it -- the next ApplyBreakpoints() call (see ReaderLoop()'s handling of + // the initial-breakpoint stop event) gets another chance once modules are guaranteed populated. + if(std::find(m_pendingBreakpoints.begin(), m_pendingBreakpoints.end(), address) == m_pendingBreakpoints.end()){ + m_pendingBreakpoints.push_back(address); + } + LogWarn("X2WinRpcAdapter::AddBreakpoint: failed to resolve module \"%s\"+0x%llx", + address.module.c_str(), (unsigned long long)address.offset); + return DebugBreakpoint(); + } + + return AddBreakpoint(resolved, breakpoint_type); +} + +void X2WinRpcAdapter::ApplyBreakPoints(){ + std::vector pending; + pending.swap(m_pendingBreakpoints); + + for(const auto& bp : pending){ + AddBreakpoint(bp); + } + + std::vector pendingHw; + pendingHw.swap(m_pendingHardwareBreakpoints); + + for(const auto& hwbp : pendingHw){ + if(hwbp.isRelative){ + AddHardwareBreakpoint(hwbp.location, hwbp.type, hwbp.size); + } else { + AddHardwareBreakpoint(hwbp.address, hwbp.type, hwbp.size); + } + } +} + +bool X2WinRpcAdapter::RemoveBreakpoint(const DebugBreakpoint& breakpoint){ + for(auto it = m_pendingBreakpoints.begin(); it != m_pendingBreakpoints.end(); ++it){ + uint64_t resolved = 0; + if(ResolveModuleAddress(*it, resolved) && resolved == breakpoint.m_address){ + m_pendingBreakpoints.erase(it); + return true; + } + } + + X2WinEnvelopeBuffer response = CallSync(x2win::Body_RemoveBreakpointRequest, [&breakpoint](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateRemoveBreakpointRequest(b, breakpoint.m_address).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::RemoveBreakpoint: stub rejected removal at 0x%llx", + (unsigned long long)breakpoint.m_address); + return false; + } + + auto it = std::find(m_breakpoints.begin(), m_breakpoints.end(), breakpoint); + if(it != m_breakpoints.end()){ + m_breakpoints.erase(it); + } + + return true; +} +std::vector X2WinRpcAdapter::GetBreakpointList() const { return m_breakpoints;} + +bool X2WinRpcAdapter::AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size){ + if(!m_connected){ + // Not connected yet (Apply() firing before Attach()/ExecuteWithArgs()/Connect()) -- stage + // it, same reason AddBreakpoint(ModuleNameAndOffset) stages below. + PendingHardwareBreakpoint pending(address, type, size); + if(std::find(m_pendingHardwareBreakpoints.begin(), m_pendingHardwareBreakpoints.end(), pending) + == m_pendingHardwareBreakpoints.end()){ + m_pendingHardwareBreakpoints.push_back(pending); + } + return true; + } + + X2WinEnvelopeBuffer response = CallSync(x2win::Body_SetHardwareBreakpointRequest,[address, type, size](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateSetHardwareBreakpointRequest(b, address, (x2win::BreakpointType)type, (uint8_t)size).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::AddHardwareBreakpoint: stub rejected hw breakpoint at 0x%llx", + (unsigned long long)address); + } + return success; +} +bool X2WinRpcAdapter::RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size){ + // Still-staged (never actually sent) -- just drop it locally, same shape as the pending-list + // check RemoveBreakpoint() does for software breakpoints. + PendingHardwareBreakpoint pending(address, type, size); + auto it = std::find(m_pendingHardwareBreakpoints.begin(), m_pendingHardwareBreakpoints.end(), pending); + if(it != m_pendingHardwareBreakpoints.end()){ + m_pendingHardwareBreakpoints.erase(it); + return true; + } + + if(!m_connected){ + return false; + } + + X2WinEnvelopeBuffer response = CallSync(x2win::Body_RemoveHardwareBreakpointRequest, + [address, type, size](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateRemoveHardwareBreakpointRequest(b, address, (x2win::BreakpointType)type, (uint8_t)size).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::RemoveHardwareBreakpoint: stub rejected removal at 0x%llx", + (unsigned long long)address); + } + return success; +} +bool X2WinRpcAdapter::AddHardwareBreakpoint(const ModuleNameAndOffset& location, DebugBreakpointType type, size_t size){ + if(!m_connected){ + PendingHardwareBreakpoint pending(location, type, size); + if(std::find(m_pendingHardwareBreakpoints.begin(), m_pendingHardwareBreakpoints.end(), pending) + == m_pendingHardwareBreakpoints.end()){ + m_pendingHardwareBreakpoints.push_back(pending); + } + return true; + } + + uint64_t resolved = 0; + if(!ResolveModuleAddress(location, resolved)){ + // Connected, but not resolvable yet (module not loaded) -- re-stage, same as + // AddBreakpoint(ModuleNameAndOffset)'s equivalent branch. + PendingHardwareBreakpoint pending(location, type, size); + if(std::find(m_pendingHardwareBreakpoints.begin(), m_pendingHardwareBreakpoints.end(), pending) + == m_pendingHardwareBreakpoints.end()){ + m_pendingHardwareBreakpoints.push_back(pending); + } + LogWarn("X2WinRpcAdapter::AddHardwareBreakpoint: failed to resolve module \"%s\"+0x%llx", + location.module.c_str(), (unsigned long long)location.offset); + return false; + } + + return AddHardwareBreakpoint(resolved, type, size); +} +bool X2WinRpcAdapter::RemoveHardwareBreakpoint(const ModuleNameAndOffset& location, DebugBreakpointType type, size_t size){ + PendingHardwareBreakpoint pending(location, type, size); + auto it = std::find(m_pendingHardwareBreakpoints.begin(), m_pendingHardwareBreakpoints.end(), pending); + if(it != m_pendingHardwareBreakpoints.end()){ + m_pendingHardwareBreakpoints.erase(it); + return true; + } + + uint64_t resolved = 0; + if(!ResolveModuleAddress(location, resolved)){ + return false; + } + + return RemoveHardwareBreakpoint(resolved, type, size); +} + + +std::unordered_map X2WinRpcAdapter::ReadAllRegisters(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_ReadAllRegistersRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateReadAllRegistersRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + + std::unordered_map result; + if(resp && resp->registers()){ + for(const auto* r: * resp->registers()){ + std::string name = r->name() ? r->name()->str() : std::string(); + result.emplace(name, DebugRegister(name, r->value(), r->width(), r->register_index())); + } + } + LogDebug("X2WinRpcAdapter::ReadAllRegisters: got %zu register(s)", result.size()); + return result; +} + +DebugRegister X2WinRpcAdapter::ReadRegister(const std::string& reg){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_ReadRegisterRequest, [®](flatbuffers::FlatBufferBuilder& b){ + auto nameOff = b.CreateString(reg); + return x2win::CreateReadRegisterRequest(b, nameOff).Union(); + }); + const auto* resp = response.BodyAs(); + if(!resp || !resp->success()){ + LogDebug("X2WinRpcAdapter::ReadRegister: stub doesn't reognize regiser \"%s\"", reg.c_str()); + return DebugRegister(); + } + + return DebugRegister(reg, resp->value(), resp->width(), resp->register_index()); +} + +bool X2WinRpcAdapter::WriteRegister(const std::string& reg, intx::uint512 value){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_WriteRegisterRequest, [®, value](flatbuffers::FlatBufferBuilder& b){ + auto nameOff = b.CreateString(reg); + // Narrow the 512-bit value down to the 64 bits the wire format ( and every real X2win + // register) actually needs + uint64_t narrowed = (uint64_t)value; + return x2win::CreateWriteRegisterRequest(b, nameOff, narrowed).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::WriteRegister: sutb rejected write to \"%s\"", reg.c_str()); + } + return success; +} +DataBuffer X2WinRpcAdapter::ReadMemory(std::uintptr_t address, std::size_t size){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_ReadMemoryRequest, [address, size](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateReadMemoryRequest(b, address, size).Union(); + }); + + const auto* resp = response.BodyAs(); + if(!resp || !resp->success() || !resp->data()){ + // LogDebug, not LogWarn -- the analysis engine routinely probes unmapped addresses + // (e.g. speculative reads past the end of a section), so this is expected to fire often + // and would flood the Log pane at a higher severity. + LogDebug("X2WinRpcAdapter::ReadMemory: failed to read 0x%zx bytes at 0x%llx", + size, (unsigned long long)address); + return DataBuffer(); + } + + return DataBuffer(resp->data()->data(), resp->data()->size()); +} +bool X2WinRpcAdapter::WriteMemory(std::uintptr_t address, const DataBuffer& buffer){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_WriteMemoryRequest, [address, &buffer](flatbuffers::FlatBufferBuilder& b){ + auto dataOff = b.CreateVector(reinterpret_cast(buffer.GetData()), buffer.GetLength()); + return x2win::CreateWriteMemoryRequest(b, address, dataOff).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::WriteMemory: stub rejected write of %zu byte(s) at 0x%llx", + buffer.GetLength(), (unsigned long long)address); + } + return success; +} + +// Extracts the filename portion of a path, recognizing both '/' and '\' as separators. +// Needed because module names come over the wire in Windows path format (backslashes), but +// DebugModule::GetPathBaseName() (core/debugadapter.cpp) only recognizes '\' when *this* process +// is itself compiled for Windows -- X2WinRpcAdapter is the first adapter where BN core can run on +// a different OS (macOS) than the debug target (always Windows), so that assumption breaks here. +// Extracting the basename ourselves, up front, sidesteps the problem entirely. +static std::string ExtractFileName(const std::string& path){ + size_t pos = path.find_last_of("/\\"); + return (pos == std::string::npos) ? path : path.substr(pos + 1); +} + +// --- Modules --- + +std::vector X2WinRpcAdapter::GetModuleList(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_GetModuleListRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGetModuleListRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + std::vector result; + if(resp && resp->modules()){ + for(const auto* m : *resp->modules()){ + std::string name = m->name() ? m->name()->str() : std::string(); + std::string shortName = ExtractFileName(name); + result.emplace_back(name, shortName, (std::uintptr_t)m->base(), (std::size_t)m->size(), true); + LogDebug("X2WinRpcAdapter::GetModuleList: module \"%s\" base=0x%llx size=0x%llx", + name.c_str(), (unsigned long long)m->base(), (unsigned long long)m->size()); + } + } + if(result.empty()) + LogWarn("X2WinRpcAdapter::GetModuleList: stub returned no modules -- rebase to the remote base will not happen"); + return result; +} + +std::vectorX2WinRpcAdapter::GetMemoryMap(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_GetMemoryMapRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGetMemoryMapRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + std::vector result; + if(resp && resp->regions()){ + for(const auto* r : *resp->regions()){ + std::string name = r->name() ? r->name()->str() : std::string(); + result.emplace_back((std::uintptr_t)r->start(), (std::size_t)r->size(), name, + r->read(), r->write(), r->execute(), r->shared()); + } + } + + LogDebug("X2WinRpcAdapter::GetMemoryMap: got %zu region(s)", result.size()); + return result; +} + +// --- Execution control --- +DebugStopReason X2WinRpcAdapter::StopReason(){ return m_lastStopReason.load(); } +uint64_t X2WinRpcAdapter::ExitCode(){ + return m_exitCode.load(); +} +bool X2WinRpcAdapter::BreakInto(){ + if(m_lastStopReason.load() == DebugStopReason::ProcessExited){ + // Nothing to break into -- the process is already gone (ReaderLoop()'s StopReason_EXITED + // handling sets this). RequestInterrupt() (core/debuggercontroller.cpp) fires BreakInto() + // unconditionally before every Detach()/Quit(), regardless of whether the target is still + // running -- skip the round trip instead of logging a "stub reported failure" that isn't + // actually telling us anything new at that point. + return false; + } + X2WinEnvelopeBuffer response = CallSync(x2win::Body_BreakIntoRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateBreakIntoRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::BreakInto: stub reported failure"); + }else{ + DebuggerEvent event; + event.type = ResumeEventType; + PostDebuggerEvent(event); + } + return success; +} +bool X2WinRpcAdapter::Go(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_GoRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateGoRequest(b).Union(); + }); + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success) + LogWarn("X2WinRpcAdapter::Go: stub reported failure"); + return success; +} +bool X2WinRpcAdapter::StepInto(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_StepIntoRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateStepIntoRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::StepInto: stub reported failure"); + }else{ + DebuggerEvent event; + event.type = StepIntoEventType; + PostDebuggerEvent(event); + } + return success; +} +bool X2WinRpcAdapter::StepOver(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_StepOverRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateStepOverRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::StepOver: stub reported failure"); + }else{ + DebuggerEvent event; + event.type = StepOverEventType; + PostDebuggerEvent(event); + } + return success; +} + +bool X2WinRpcAdapter::StepReturn(){ + X2WinEnvelopeBuffer response = CallSync(x2win::Body_StepReturnRequest, [](flatbuffers::FlatBufferBuilder& b){ + return x2win::CreateStepReturnRequest(b).Union(); + }); + + const auto* resp = response.BodyAs(); + bool success = resp && resp->success(); + if(!success){ + LogWarn("X2WinRpcAdapter::StepReturn: stub reported failure"); + }else{ + DebuggerEvent event; + event.type = StepReturnEventType; + PostDebuggerEvent(event); + } + return success; +} + +std::string X2WinRpcAdapter::InvokeBackendCommand(const std::string& command){ return ""; } +uint64_t X2WinRpcAdapter::GetInstructionOffset(){ return m_lastStopAddress.load(); } +uint64_t X2WinRpcAdapter::GetStackPointer(){ + std::string spRegistername = (GetTargetArchitecture() == "x86") ? "esp" : "rsp"; + return (uint64_t)ReadRegister(spRegistername).m_value; +} +bool X2WinRpcAdapter::SupportFeature(DebugAdapterCapacity feature){ + switch(feature){ + // StepOver/Go/BreakInto/GetModuleList are all wired over RPC to the stub -- report the + // capabilities that actually correspond to real, implemented functionality so + // DebuggerController uses them instead of silently falling back to its software + // emulation paths (see StepOverAndWaitInternal() in debuggercontroller.cpp). + case DebugAdapterSupportStepOver: + return true; + case DebugAdapterSupportModules: + return true; + case DebugAdapterSupportThreads: + return true; + case DebugAdapterSupportStepReturn: + return true; + // Not yet implemented on the stub side. + case DebugAdapterSupportStepOverReverse: + case DebugAdapterSupportTTD: + default: + return false; + } +} + +Ref X2WinRpcAdapterType::RegisterAdapterSettings(){ + Ref settings = Settings::Instance("X2WinRpcAdapterSettings"); + settings->SetResourceId("x2win_rpc_adapter_settings"); + + settings->RegisterSetting("connect.ipAddress", + R"({ + "title" : "IP Address", + "type" : "string", + "default" : "127.0.0.1", + "description" : "IP address of the x2win stub to connect to", + "readOnly" : false + })"); + + settings->RegisterSetting("common.inputFile", R"({ + "title" : "Input File", + "type" : "string", + "default" : "", + "description" : "Input file to use to find the base address of the binary view", + "readOnly" : false, + "uiSelectionAction" : "file" + })"); + + settings->RegisterSetting("connect.port", + R"({ + "title" : "Port", + "type" : "number", + "default" : 31338, + "minValue" : 0, + "maxValue" : 65535, + "description" : "Port of the x2win stub to connect to", + "readOnly" : false + })"); + + settings->RegisterSetting("attach.pid", + R"({ + "title" : "PID to attach to", + "type" : "number", + "default" : 0, + "minValue" : 0, + "maxValue" : 4294967295, + "description" : "PID of the process to attach to", + "readOnly" : false + })"); + + settings->RegisterSetting("launch.executablePath", + R"({ + "title" : "Executable Path", + "type" : "string", + "default" : "", + "description" : "Windows-side path of the executable for the stub to launch (e.g. C:\\\\path\\\\to\\\\target.exe) -- NOT the local path of the analyzed binary.", + "readOnly" : false + })"); + + settings->RegisterSetting("launch.workingDirectory", + R"({ + "title" : "Working Directory", + "type" : "string", + "default" : "", + "description" : "Windows-side working directory to launch the target in.", + "readOnly" : false + })"); + + settings->RegisterSetting("launch.commandLineArguments", + R"({ + "title" : "Command Line Arguments", + "type" : "string", + "default" : "", + "description" : "Command line arguments to pass to the target", + "readOnly" : false + })"); + + + return settings; +} + +X2WinRpcAdapterType::X2WinRpcAdapterType() : DebugAdapterType("X2WIN_RPC"){ +} + +Ref X2WinRpcAdapterType::GetAdapterSettings(){ + static Ref settings = X2WinRpcAdapterType::RegisterAdapterSettings(); + return settings; +} + +DebugAdapter* X2WinRpcAdapterType::Create(BinaryNinja::BinaryView* data){ + return new X2WinRpcAdapter(data); +} + +bool X2WinRpcAdapterType::IsValidForData(BinaryNinja::BinaryView* data){ + return true; +} + +bool X2WinRpcAdapterType::CanExecute(BinaryNinja::BinaryView* data){ + return data->GetTypeName() == "PE"; +} + +bool X2WinRpcAdapterType::CanConnect(BinaryNinja::BinaryView* data){ + return data->GetTypeName() == "PE"; +} + +void BinaryNinjaDebugger::InitX2WinRpcAdapterType(){ + static X2WinRpcAdapterType x2winType; + DebugAdapterType::Register(&x2winType); +} + + +// --- Helper Functions --- + +void X2WinRpcAdapter::TeardownConnection(){ + LogInfo("X2WinRpcAdapter::TeardownConnection: closing connection to stub"); + m_socket.Kill(); + if(m_readerThread.joinable()){ + m_readerThread.join(); + } + m_connected = false; + ResetSessionState(); +} + +void X2WinRpcAdapter::ResetSessionState(){ + // Every entry here was set on (or is a leftover of) the debuggee this connection was just + // talking to -- once that debuggee is gone (Detach/Quit) or the connection itself dies, none + // of it is trustworthy for whatever comes next: a reconnect might land on a brand-new stub + // session that's never heard of these breakpoints, or a same-connection Attach()/Launch() might + // target a completely different process where these addresses/stop info mean nothing. The + // *authoritative* breakpoint list lives in DebuggerBreakpoints (core/debuggerstate.cpp) anyway -- + // it re-sends every known breakpoint via ApplyBreakpoints() on the next successful connect + // regardless, so clearing these caches here just avoids stale/duplicate entries, never loses + // anything BN core still cares about. + m_breakpoints.clear(); + m_pendingBreakpoints.clear(); + m_pendingHardwareBreakpoints.clear(); + + m_lastStopReason = DebugStopReason::UnknownReason; + m_lastStopAddress = 0; + m_exitCode = 0; +} + +bool X2WinRpcAdapter::ResolveModuleAddress(const ModuleNameAndOffset &location, uint64_t &address){ + for(const auto& module : GetModuleList()){ + if(module.IsSameBaseModule(location.module)){ + address = module.m_address + location.offset; + return true; + } + } + return false; +} \ No newline at end of file diff --git a/core/adapters/x2winrpcadapter.h b/core/adapters/x2winrpcadapter.h new file mode 100644 index 00000000..7e31594a --- /dev/null +++ b/core/adapters/x2winrpcadapter.h @@ -0,0 +1,210 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "../debugadapter.h" +#include "../debugadaptertype.h" +#include "./socket.h" +#include +#include +#include +#include +#include +#include +#include + +namespace BinaryNinjaDebugger { + + // A parsed x2win::Envelope is just a read-only view into a byte buffer (unlike a Protobuf + // message, it owns no state of its own) -- something has to keep that buffer alive for as + // long as the view is used. This pairs the two: Get()/BodyAs() are only valid while this + // object (or a copy of its `bytes`) is alive. An empty `bytes` (default-constructed, or a + // send failure in CallSync()) is a valid "no response" state -- Get()/BodyAs() return + // nullptr rather than dereferencing a nonexistent buffer. + struct X2WinEnvelopeBuffer + { + std::vector bytes; + + const x2win::Envelope* Get() const + { + return bytes.empty() ? nullptr : x2win::GetEnvelope(bytes.data()); + } + + template + const T* BodyAs() const + { + const x2win::Envelope* envelope = Get(); + return envelope ? envelope->body_as() : nullptr; + } + }; + + class X2WinRpcAdapter : public DebugAdapter + { + private: + Socket m_socket; + bool m_connected = false; + std::thread m_readerThread; + std::atomic m_lastStopReason {DebugStopReason::UnknownReason}; + std::atomic m_lastStopAddress {0}; + std::atomic m_exitCode{0}; + + // True once Connect() (the one-shot "target mode" style entry point, UI: "Connect to Remote + // Process") has succeeded -- deliberately NOT reset in TeardownConnection(), because the + // whole point is to remember this *across* a disconnect. A target-mode stub only ever owns + // the one debuggee it was started with; ExecuteWithArgs() checks this to refuse a Launch + // (e.g. Restart's Quit-then-Launch sequence) before ever touching the network, instead of + // trying to reconnect to a stub that has, by design, already exited. + bool m_lastConnectionWasTargetMode = false; + + // request_id -> promise, fulfilled by ReaderLoop() when the matching RESPONSE arrives. + // EVENT frames (id == 0) never go through this table; they go straight to PostDebuggerEvent(). + std::mutex m_pendingMutex; + std::mutex m_sendMutex; + std::unordered_map> m_pendingRequests; + std::vector m_breakpoints; + std::vector m_pendingBreakpoints; + std::vector m_pendingHardwareBreakpoints; + std::atomic m_nextRequestId {1}; + + Ref GetAdapterSettings() override; + + // Helper to resolve module+offset to an absolute address using GetModuleList(). + // Same purpose as LldbAdapter::ResolveModuleAddress; every adapter needs its own copy + // since there is no shared base-class implementation for this. + bool ResolveModuleAddress(const ModuleNameAndOffset& location, uint64_t& address); + + // Flushes every breakpoint staged by AddBreakpoint(ModuleNameAndOffset&) while not yet connected + // Called once Attach()/ExecuteWithArgs()/Connect() acutally connects (never from ConnectToDebugServer() + // Because server mode has no debuggee yet, nothing to resolve against). + void ApplyBreakPoints(); + + bool ConnectSocket(const std::string& ip, uint16_t port); + bool ConnectFromSettings(); + void TeardownConnection(); + void ResetSessionState(); + + // Populates common.inputFile (used by DetectLoadedModule()/GetRemoteBase() to match this + // adapter's GetModuleList() entries against the currently-open BinaryView, which is what + // drives auto-rebase on connect) from the BinaryView's own file path, same convention as + // every other adapter (see e.g. WindowsNativeAdapter::GenerateDefaultAdapterSettings) -- + // only when the setting has never been explicitly set for this resource. + void GenerateDefaultAdapterSettings(BinaryView* data); + + public: + X2WinRpcAdapter(BinaryView* data); + virtual ~X2WinRpcAdapter(); + + // --- Lifecycle --- + bool Execute(const std::string& path, const LaunchConfigurations& configs) override; + bool ExecuteWithArgs(const std::string& path, const std::string& args, const std::string& workingDir, + const LaunchConfigurations& configs) override; + bool Attach(std::uint32_t pid) override; + bool Connect(const std::string& server, std::uint32_t port) override; + bool ConnectToDebugServer(const std::string& server, std::uint32_t port) override; + bool DisconnectDebugServer() override; + bool Detach() override; + bool Quit() override; + + // --- Process / thread enumeration --- + std::vector GetProcessList() override; + std::uint32_t GetActivePID() override; + std::vector GetThreadList() override; + DebugThread GetActiveThread() const override; + std::uint32_t GetActiveThreadId() const override; + bool SetActiveThread(const DebugThread& thread) override; + bool SetActiveThreadId(std::uint32_t tid) override; + bool SuspendThread(std::uint32_t tid) override; + bool ResumeThread(std::uint32_t tid) override; + + std::vector GetFramesOfThread(std::uint32_t tid) override; + + // --- Breakpoints --- + // Software breakpoints: the stub owns the VirtualProtectEx/write/restore dance, not us. + DebugBreakpoint AddBreakpoint(const std::uintptr_t address, unsigned long breakpoint_type = 0) override; + DebugBreakpoint AddBreakpoint(const ModuleNameAndOffset& address, unsigned long breakpoint_type = 0) override; + bool RemoveBreakpoint(const DebugBreakpoint& breakpoint) override; + std::vector GetBreakpointList() const override; + + // Hardware breakpoints / watchpoints + bool AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + bool RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + bool AddHardwareBreakpoint(const ModuleNameAndOffset& location, DebugBreakpointType type, size_t size = 1) override; + bool RemoveHardwareBreakpoint(const ModuleNameAndOffset& location, DebugBreakpointType type, size_t size = 1) override; + + // --- Registers / memory --- + std::unordered_map ReadAllRegisters() override; + DebugRegister ReadRegister(const std::string& reg) override; + bool WriteRegister(const std::string& reg, intx::uint512 value) override; + DataBuffer ReadMemory(std::uintptr_t address, std::size_t size) override; + bool WriteMemory(std::uintptr_t address, const DataBuffer& buffer) override; + + // --- Modules / target info --- + std::vector GetModuleList() override; + std::vector GetMemoryMap() override; + std::string GetTargetArchitecture() override; + + // --- Execution control --- + // Go/StepInto/StepOver only confirm the stub accepted the request. The resulting stop is + // never inline in that response; it always arrives later as its own out-of-band Event. + DebugStopReason StopReason() override; + uint64_t ExitCode() override; + bool BreakInto() override; + bool Go() override; + bool StepInto() override; + bool StepOver() override; + bool StepReturn() override; + + // --- Misc --- + std::string InvokeBackendCommand(const std::string& command) override; + uint64_t GetInstructionOffset() override; + uint64_t GetStackPointer() override; + bool SupportFeature(DebugAdapterCapacity feature) override; + + // Dedicated socket-reader loop (runs on m_readerThread): pulls frames forever, routes + // RESPONSE by id to m_pendingRequests, routes EVENT (id == 0) to PostDebuggerEvent(). + void ReaderLoop(); + + // --- Helper function --- + bool RecvExact(void* buffer, size_t size); + bool SendExact(const void* buffer, size_t size); + + // Unlike Protobuf, a FlatBuffers table can't be built standalone and handed over -- + // nested objects (strings, the request's own body table) must be constructed bottom-up + // with the *same* FlatBufferBuilder that will go on to wrap them in the Envelope, which + // only CallSync() itself owns. So callers hand CallSync() a builder function for just + // their request body instead of a pre-built Envelope; CallSync() supplies the builder, + // wraps the result in an Envelope with the request_id it assigns, and does the + // send/wait/response bookkeeping exactly as before. + X2WinEnvelopeBuffer CallSync(x2win::Body bodyType, + const std::function(flatbuffers::FlatBufferBuilder&)>& buildBody); + + }; + + + class X2WinRpcAdapterType : public DebugAdapterType + { + static Ref RegisterAdapterSettings(); + public: + X2WinRpcAdapterType(); + static Ref GetAdapterSettings(); + virtual DebugAdapter* Create(BinaryNinja::BinaryView* data); + virtual bool IsValidForData(BinaryNinja::BinaryView* data); + virtual bool CanExecute(BinaryNinja::BinaryView* data); + virtual bool CanConnect(BinaryNinja::BinaryView* data); + }; + + + void InitX2WinRpcAdapterType(); +} // namespace BinaryNinjaDebugger diff --git a/core/debugger.cpp b/core/debugger.cpp index 4aea0bdb..6db7389b 100644 --- a/core/debugger.cpp +++ b/core/debugger.cpp @@ -21,6 +21,7 @@ limitations under the License. #include "adapters/corelliumadapter.h" #include "adapters/lldbcoredumpadapter.h" #include "adapters/esrevenadapter.h" +#include "adapters/x2winrpcadapter.h" #ifdef WIN32 #include "adapters/dbgengadapter.h" #include "adapters/dbgengttdadapter.h" @@ -56,6 +57,7 @@ void InitDebugAdapterTypes() InitLldbAdapterType(); InitEsrevenAdapterType(); InitLldbCoreDumpAdapterType(); + InitX2WinRpcAdapterType(); } diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index 573eb9d7..9996dedf 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -2016,6 +2016,13 @@ void DebuggerController::ApplyOwnStateForEvent(const DebuggerEvent& event) m_state->SetExecutionStatus(DebugAdapterRunningStatus); break; } + case StepOverEventType: + { + // Add support for StepOverEventType with same logic as StepIntoEventType + m_state->SetConnectionStatus(DebugAdapterConnectedStatus); + m_state->SetExecutionStatus(DebugAdapterRunningStatus); + break; + } case TargetExitedEventType: m_exitCode = (uint32_t)event.data.exitData.exitCode; [[fallthrough]]; diff --git a/protocol/x2win.fbs b/protocol/x2win.fbs new file mode 100644 index 00000000..97eef9f8 --- /dev/null +++ b/protocol/x2win.fbs @@ -0,0 +1,235 @@ +namespace x2win; + +enum BreakpointType : byte { + SOFTWARE = 0, + HARDWARE_EXECUTE = 1, + HARDWARE_READ = 2, + HARDWARE_WRITE = 3, + HARDWARE_ACCESS = 4, +} + +enum StopReason : byte { + UNKNOWN = 0, + BREAKPOINT = 1, + SINGLE_STEP = 2, + // The very first breakpoint hit after Launch/Attach (the OS-injected loader breakpoint, + // not a user-set one). Binary Ninja's DebugStopReason distinguishes this from an ordinary + // BREAKPOINT (see DebugStopReason::InitialBreakpoint) -- report it exactly once per + // session, the first time any breakpoint exception is seen, regardless of address. + INITIAL_BREAKPOINT = 3, + EXITED = 4, +} + +table LaunchRequest { path: string; args: string; working_dir: string; } +table LaunchResponse { success: bool; } + +table AttachRequest { pid: uint32; } +table AttachResponse { success: bool; } + +table GetTargetArchRequest {} +table GetTargetArchResponse { architecture: string; } + +table DetachRequest {} +table DetachResponse { success: bool; } + +table QuitRequest {} +table QuitResponse { success: bool; } + +table ProcessInfo { pid: uint32; name: string; } +table GetProcessListRequest {} +table GetProcessListResponse { processes: [ProcessInfo]; } + +// One thread's tid + its current instruction pointer + whether it's suspended/frozen -- +// mirrors BN's DebugThread (core/debugadapter.h). rip is uint64 for the same reason +// RegisterEntry.value is: X2Win only ever targets x86/x64 Windows. +table ThreadEntry { tid: uint32; rip: uint64; is_frozen: bool; } + +table GetThreadListRequest {} +table GetThreadListResponse { threads: [ThreadEntry]; } + +// Just the tid -- the adapter derives the active thread's rip from the last reported stop +// address instead of asking the stub for it separately (BN only ever stops the whole process, +// never a single thread, so GetInstructionOffset() is already the active thread's rip). +table GetActiveThreadIdRequest {} +table GetActiveThreadIdResponse { tid: uint32; } + +table SetActiveThreadIdRequest { tid: uint32; } +table SetActiveThreadIdResponse { success: bool; } + +table SuspendThreadRequest { tid: uint32; } +table SuspendThreadResponse { success: bool; } + +table ResumeThreadRequest { tid: uint32; } +table ResumeThreadResponse { success: bool; } + +table ConnectServerRequest {} +table ConnectServerResponse { success: bool; } + +// Resumes a stopped target (equivalent of DebugAdapter::Go()). Like Launch/Attach, this only +// confirms the stub accepted the request -- the next stop is reported separately and +// asynchronously as a TargetStoppedEvent, never inline in this response. +table GoRequest {} +table GoResponse { success: bool; } + +table StepIntoRequest {} +table StepIntoResponse { success: bool; } + +table StepOverRequest {} +table StepOverResponse { success: bool; } + +table StepReturnRequest {} +table StepReturnResponse { success: bool; } + +table SetBreakpointRequest { address: uint64; type: BreakpointType; } +table SetBreakpointResponse { success: bool; breakpoint_id: uint64; } + +table RemoveBreakpointRequest { address: uint64; } +table RemoveBreakpointResponse { success: bool; } + +table BreakIntoRequest {} +table BreakIntoResponse { success: bool; } + +// Hardware breakpoint/watchpoint, keyed by (address, type, size) as a triple rather than an +// id like SetBreakpointRequest -- mirrors WindowsDebugEngine/WindowsNativeAdapter's own +// identity rule for these (a debug register slot, not an allocated id). +table SetHardwareBreakpointRequest { address: uint64; type: BreakpointType; size: ubyte; } +table SetHardwareBreakpointResponse { success: bool; } + +table RemoveHardwareBreakpointRequest { address: uint64; type: BreakpointType; size: ubyte; } +table RemoveHardwareBreakpointResponse { success: bool; } + +table TargetStoppedEvent { reason: StopReason; address: uint64; exit_code: uint64; } + +// Reads raw bytes from the target's address space (equivalent of ReadProcessMemory). Unlike +// Go/Launch/Attach, this is a plain synchronous request/response -- there is no separate async +// event involved. A partial or failed read (e.g. address not mapped) is reported as +// success=false with an empty `data`, not a short `data` buffer -- callers should not try to +// use a truncated result. +table ReadMemoryRequest { address: uint64; size: uint64; } +table ReadMemoryResponse { success: bool; data: [ubyte]; } + +table WriteMemoryRequest { address: uint64; data: [ubyte]; } +table WriteMemoryResponse { success: bool; } + +// One register's value + BN's DebugRegister layout metadata (width in bytes, index for display +// ordering). Value is uint64 -- X2Win only ever targets x86/x64 Windows, nothing there is wider. +table RegisterEntry { name: string; value: uint64; width: uint32; register_index: uint32; } + +table ReadAllRegistersRequest {} +table ReadAllRegistersResponse { registers: [RegisterEntry]; } + +table ReadRegisterRequest { name: string; } +// A register name the stub doesn't recognize is reported as success:false, not a zeroed/garbage +// value -- same "don't fabricate a plausible-looking failure" contract as ReadMemoryResponse. +table ReadRegisterResponse { success: bool; value: uint64; width: uint32; register_index: uint32; } + +table WriteRegisterRequest { name: string; value: uint64; } +table WriteRegisterResponse { success: bool; } + +table ModuleEntry { name: string; base: uint64; size: uint64; } +table GetModuleListRequest {} +table GetModuleListResponse { modules: [ModuleEntry]; } + +table FrameEntry { + index: uint32; + pc: uint64; + sp: uint64; + fp: uint64; + function_name: string; + function_start: uint64; + module: string; +} + +table GetFramesOfThreadRequest { tid: uint32; } +table GetFramesOfThreadResponse { frames: [FrameEntry]; } + +// One mapped region of the target's virtual address space -- mirrors BN's DebugMemoryRegion +// (core/debugadapter.h). name is a file path for file-backed mappings, a well-known name like +// "[stack]"/"[heap]" where the backend provides one, or empty for anonymous mappings. +table MemoryRegionEntry { + start: uint64; + size: uint64; + name: string; + read: bool; + write: bool; + execute: bool; + shared: bool; +} + +table GetMemoryMapRequest {} +table GetMemoryMapResponse { regions: [MemoryRegionEntry]; } + +union Body { + // request BN core -> stub + LaunchRequest, + AttachRequest, + GetTargetArchRequest, + DetachRequest, + QuitRequest, + GetProcessListRequest, + GetThreadListRequest, + GetActiveThreadIdRequest, + SetActiveThreadIdRequest, + SuspendThreadRequest, + ResumeThreadRequest, + ConnectServerRequest, + GoRequest, + StepIntoRequest, + StepOverRequest, + StepReturnRequest, + BreakIntoRequest, + SetBreakpointRequest, + RemoveBreakpointRequest, + SetHardwareBreakpointRequest, + RemoveHardwareBreakpointRequest, + ReadMemoryRequest, + WriteMemoryRequest, + ReadAllRegistersRequest, + ReadRegisterRequest, + WriteRegisterRequest, + GetModuleListRequest, + GetFramesOfThreadRequest, + GetMemoryMapRequest, + + // response stub -> BN core + LaunchResponse, + AttachResponse, + GetTargetArchResponse, + DetachResponse, + QuitResponse, + GetProcessListResponse, + GetThreadListResponse, + GetActiveThreadIdResponse, + SetActiveThreadIdResponse, + SuspendThreadResponse, + ResumeThreadResponse, + ConnectServerResponse, + GoResponse, + StepIntoResponse, + StepOverResponse, + StepReturnResponse, + BreakIntoResponse, + SetBreakpointResponse, + RemoveBreakpointResponse, + SetHardwareBreakpointResponse, + RemoveHardwareBreakpointResponse, + ReadMemoryResponse, + WriteMemoryResponse, + ReadAllRegistersResponse, + ReadRegisterResponse, + WriteRegisterResponse, + GetModuleListResponse, + GetFramesOfThreadResponse, + GetMemoryMapResponse, + + // event stub -> BN core, no response required + TargetStoppedEvent, + +} + +table Envelope { + request_id: uint64; + body: Body; +} + +root_type Envelope; diff --git a/vendor/flatbuffers b/vendor/flatbuffers new file mode 160000 index 00000000..7e163021 --- /dev/null +++ b/vendor/flatbuffers @@ -0,0 +1 @@ +Subproject commit 7e163021e59cca4f8e1e35a7c828b5c6b7915953 diff --git a/x2winstub/CMakeLists.txt b/x2winstub/CMakeLists.txt new file mode 100644 index 00000000..ac378408 --- /dev/null +++ b/x2winstub/CMakeLists.txt @@ -0,0 +1,96 @@ +cmake_minimum_required(VERSION 3.20) +project(x2winstub CXX) + +if(NOT WIN32) + message(STATUS "x2winstub is Windows-only, skipping") + return() +endif() + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Only meaningful for MSVC-ABI-compatible compilers (cl/clang-cl); silently ignored otherwise, so +# guarding it isn't strictly required, but MinGW-w64 doesn't use /MT-style runtime selection at +# all and this being unconditional read as "x2winstub still needs MSVC" even after it no longer does. +if(MSVC) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() + +add_executable(x2winstub + main.cpp + net/connection.cpp + debug/windows_debug_engine.cpp + x2win_session.cpp +) + +# NOMINMAX: without it, #defines max/min as function-like macros, which then mangle +# any std::numeric_limits::max()/min() call textually (e.g. inside flatbuffers' +# flatbuffer_builder.h) into a syntax error -- this hit main.cpp/net/connection.cpp/x2win_session.cpp +# the moment they got rebuilt from a clean build directory (previously masked by incremental builds +# reusing stale, pre-existing .obj files instead of recompiling against the current headers). +# WIN32_LEAN_AND_MEAN trims further (excludes rarely-needed APIs like GDI/Winsock v1); +# harmless here since net/connection.cpp already pulls in Winsock v2 explicitly. +target_compile_definitions(x2winstub PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN) + +target_include_directories(x2winstub PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# FlatBuffers replacement for the x2win RPC protocol (protocol/x2win.fbs). x2winstub actually ships +# and builds on its own (github.com/Vector35/X2WinStub), *not* nested under the main debugger +# monorepo -- the copy of this file (and this whole directory) inside the monorepo is a local +# mirror kept in sync by hand (see git log), not what runs the real build. So this can't assume a +# shared top-level CMakeLists.txt already vendored FlatBuffers for it the way core/CMakeLists.txt's +# debuggercore can: if x2win_fbs isn't already defined (i.e. we're building standalone, the way the +# real build does), vendor and generate it right here, exactly like the monorepo's top-level +# CMakeLists.txt does for core/ -- using this repo's own vendor/flatbuffers submodule. If it *is* +# already defined (this file was add_subdirectory()'d from that monorepo top-level after all), +# reuse that one instead of redefining the same target twice. +if(NOT TARGET x2win_fbs) + set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(FLATBUFFERS_INSTALL OFF CACHE BOOL "" FORCE) + set(FLATBUFFERS_BUILD_FLATC ON CACHE BOOL "" FORCE) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vendor/flatbuffers ${CMAKE_CURRENT_BINARY_DIR}/vendor/flatbuffers) + + # Not using flatbuffers_generate_headers() here (unlike the monorepo's top-level CMakeLists.txt, + # where the schema lives under the same directory the function is called from) -- its + # source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} ...) call, meant purely for IDE file grouping, + # hard-errors ("is not a prefix of file") because our schema is in a *sibling* directory + # (../protocol/x2win.fbs), not underneath x2winstub/ itself. This is the same flatc invocation + # that function does internally, just without that IDE-only step. + set(X2WIN_FBS_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/../protocol/x2win.fbs) + set(X2WIN_FBS_GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/x2win_fbs) + set(X2WIN_FBS_GENERATED_HEADER ${X2WIN_FBS_GENERATED_DIR}/x2win_generated.h) + add_custom_command( + OUTPUT ${X2WIN_FBS_GENERATED_HEADER} + COMMAND flatc -o ${X2WIN_FBS_GENERATED_DIR} -c ${X2WIN_FBS_SCHEMA} + DEPENDS flatc ${X2WIN_FBS_SCHEMA} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Building ${X2WIN_FBS_SCHEMA} flatbuffers...") + add_custom_target(GENERATE_x2win_fbs ALL DEPENDS ${X2WIN_FBS_GENERATED_HEADER}) + add_library(x2win_fbs INTERFACE) + add_dependencies(x2win_fbs GENERATE_x2win_fbs) + target_include_directories(x2win_fbs INTERFACE ${X2WIN_FBS_GENERATED_DIR}) +endif() + +# vendor/flatbuffers/include (CMAKE_SOURCE_DIR here resolves to whichever of the two vendor/ +# copies above actually got used -- this repo's own when standalone, the monorepo's when nested, +# since CMAKE_SOURCE_DIR always means "root of the current build") is needed on top of the +# x2win_fbs link below because flatbuffers_generate_headers() only exposes the *generated* +# x2win_generated.h's directory via its INTERFACE target -- it doesn't add the FlatBuffers runtime +# headers (flatbuffers/flatbuffers.h etc.) that generated file itself #includes. +target_include_directories(x2winstub PRIVATE ${CMAKE_SOURCE_DIR}/vendor/flatbuffers/include) + +# dbghelp is needed for WindowsDebugEngine::GetFramesOfThread's stack walking +# (SymInitialize/StackWalk64/etc.) -- the old debug_loop.cpp never did stack unwinding so never +# needed it. Linked normally here; WindowsNativeAdapter's original delay-load hook for dbghelp.dll +# (to avoid a version clash with the DbgEng adapter in the same *BN* process) doesn't apply to this +# standalone process, so that hook was dropped rather than ported -- see windows_debug_engine.cpp. +target_link_libraries(x2winstub PRIVATE x2win_fbs ws2_32 dbghelp) + +if(BN_INTERNAL_BUILD) + set_target_properties(x2winstub PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR}) +else() + set_target_properties(x2winstub PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/out/plugins) +endif() diff --git a/x2winstub/STATUS.md b/x2winstub/STATUS.md new file mode 100644 index 00000000..fddd3f9d --- /dev/null +++ b/x2winstub/STATUS.md @@ -0,0 +1,107 @@ +# Status + +What this codebase currently supports, and the known issues in it that aren't fixed yet. Each issue +entry lists where the problem lives, how to reproduce it, and its root cause. + +## Build status + +Builds and runs against the remote Windows box this stub is developed/tested on. Whether it passes +this repo's Jenkins CI build is not yet confirmed. + +## Current feature coverage + +**Supported**, end to end (BN-core `X2WinRpcAdapter` <-> stub `WindowsDebugEngine`, over the FlatBuffers +RPC protocol): + +- Connecting: Server mode two-phase (`ConnectToDebugServer` then `Launch`/`Attach`) and Target mode + one-phase (`Connect` to a stub already running a target), matching `GdbAdapter`/`LldbAdapter`'s shapes. +- Launching a target exe on the remote Windows box (path/args/working directory), attaching to an + existing pid, listing processes, detaching, quitting. +- Execution control: Go/continue, step into, step over, step return, break-into (interrupt). +- Breakpoints: software (set/remove) and hardware (set/remove). +- Memory: read, write, memory map query. +- Registers: read all, read one, write one. +- Threads: list, get/set active thread, suspend, resume. +- Modules: module list. Stack: frames-of-thread, stack pointer. Target architecture query. + +**Not supported / not wired up:** + +- Reverse step-over and Time Travel Debugging (TTD) -- `X2WinRpcAdapter::SupportFeature()` + (`core/adapters/x2winrpcadapter.cpp`) reports both `false`; no stub-side support exists for either. +- Everything else in this file, until each entry's `Status` says otherwise. + +## Known issues + +### 1. Detach can terminate a multi-threaded target instead of leaving it running + +**Where:** `debug/windows_debug_engine.cpp`, `WindowsDebugEngine::DebugLoop()`. + +**Symptom:** If more than one thread of the debuggee is executing the same code path (e.g. several +threads sharing a loop body) and a software breakpoint is set on that shared path, detaching while +stopped there terminates the whole target process instead of detaching cleanly. Single-threaded +targets, and breakpoints not on a path executed by multiple threads concurrently, detach as expected. +Reproducible with `testBinaries/helloworld_thread.exe`. + +**Root cause:** `DebugLoop()`'s `Detach()`-triggered cleanup only calls `ContinueDebugEvent()` for the +single debug event most recently retrieved via `WaitForDebugEvent()`, then calls +`DebugActiveProcessStop()`. If a second thread concurrently raised the same breakpoint exception, its +debug event is still queued in the kernel, never retrieved, and therefore never continued. +`DebugActiveProcessStop()` requires every outstanding debug event to be continued before it can detach +cleanly; the thread left with a pending event causes the detach to instead tear the process down. + +The same code (including the pending-event gap) exists in `core/adapters/windowsnativeadapter.cpp` +(BinaryView-hosted native Windows adapter this engine was ported from), which this issue does not +cover. + +**Status:** Fix identified (drain and continue any pending debug events before calling +`DebugActiveProcessStop()`), not yet implemented. + +### 2. Breakpoints can carry over to an unrelated process after Detach + re-Attach + +**Where:** `debug/windows_debug_engine.cpp`, `WindowsDebugEngine::Reset()` / +`ApplyPendingBreakpoints()`. + +**Symptom:** Not yet observed in practice, but reachable once a stub session's TCP connection is +reused across multiple Attach/Launch cycles (server mode) instead of reconnecting each time: a +breakpoint set while debugging one process can get silently re-applied, by raw address, to a +different, unrelated process attached afterward on the same connection. + +**Root cause:** `Reset()` (run at the start of every `Execute()`/`Attach()`) does not clear +`m_breakpoints`/`m_pendingBreakpoints` -- it only marks entries inactive, so a later +`ApplyPendingBreakpoints()` re-applies them by their stored absolute address. This is correct for +restarting the *same* binary (addresses stay meaningful), but unsafe once the same engine instance can +be reused for an unrelated target, since nothing here checks whether the new process has anything to +do with the old one. + +**Status:** Fix identified (clear breakpoint state fully in `Reset()` rather than only marking it +inactive; the BN-core client already re-sends every breakpoint it cares about on every successful +connect, so nothing is lost), not yet implemented. + +### 3. Binary Ninja's UI doesn't show the target as running while it's running freely + +**Where:** `core/adapters/x2winrpcadapter.cpp`, `X2WinRpcAdapter::Go()` (BN-core side, not the stub). + +**Symptom:** After clicking Go/Continue (or the target otherwise resumes and doesn't immediately hit +a breakpoint), the Binary Ninja UI keeps showing whatever it displayed while stopped -- status bar +doesn't say "Running", register/stack/disassembly views don't refresh or grey out -- with no visual +indication anything is happening on the remote target, until either a breakpoint is eventually hit +(the next `TargetStoppedEvent` arrives and everything jumps to the new state at once) or the target +exits. If the target runs for a long time without hitting a breakpoint, the UI looks identical to being +idle/stopped the entire time. + +**Root cause:** `X2WinRpcAdapter::Go()` sends `GoRequest` and returns whether the stub *accepted* the +resume request, but never calls `PostDebuggerEvent()` with a `ResumeEventType` event on success. +`DebuggerController::ApplyOwnStateForEvent()` (`core/debuggercontroller.cpp`) is what flips +`m_state`'s execution status to `DebugAdapterRunningStatus` on `ResumeEventType` (also on +`StepIntoEventType`/`StepOverEventType`, which is why stepping doesn't have this problem), and both +`DebuggerStatusBarWidget::updateStatusText()` (`ui/statusbar.cpp`, sets "Running") and +`DebuggerWidget`'s `ResumeEventType` handler (`ui/ui.cpp`, `refreshCurrentViewContents()`) key off the +same event. With no event posted, none of that fires until the next event this adapter *does* post +(`TargetStoppedEvent`/`TargetExitedEventType`), so the whole "running" interval is invisible to the UI. +`GdbAdapter::Go()` (`core/adapters/gdbadapter.cpp`) posts `ResumeEventType` as the very first thing it +does, before it actually resumes the target -- `X2WinRpcAdapter::Go()` is missing the equivalent call. +Note `X2WinRpcAdapter::BreakInto()` already posts `ResumeEventType` on success (existing code, unrelated +to this fix), which is a separate, already-correct case. + +**Status:** Fix identified (post a `ResumeEventType` `DebuggerEvent` at the start of +`X2WinRpcAdapter::Go()`, mirroring `GdbAdapter::Go()`), not yet implemented. diff --git a/x2winstub/debug/debug_loop.cpp.superseded b/x2winstub/debug/debug_loop.cpp.superseded new file mode 100644 index 00000000..aa1ae899 --- /dev/null +++ b/x2winstub/debug/debug_loop.cpp.superseded @@ -0,0 +1,449 @@ +#include "debug_loop.h" +#include "net/connection.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace{ + struct BreakpointInfo{ + uint64_t address; + uint8_t originalByte; + }; + + bool g_initialBreakpointSeen = false; + + std::mutex g_resumeMutex; + std::condition_variable g_resumeCv; + bool g_resumeRequested = false; + + std::atomic g_lastStopAddress{0}; + HANDLE g_debugeeProcess = nullptr; + std::promise g_initialStopSignal; + std::mutex g_initStopMutex; + bool g_initialStopFired = false; + + std::mutex g_commandMutex; + std::deque> g_commandQueue; + + struct ThreadInfo{uint32_t tid; HANDLE handle;}; + struct ModuleInfo{uint64_t base; std::string path; }; + std::mutex g_targetStateMutex; + std::unordered_map g_threads; + std::map g_modules; + + void SignalResume(){ + std::lock_guard lock(g_resumeMutex); + g_resumeRequested = true; + g_resumeCv.notify_one(); + } + + void WaitForResume(){ + std::unique_lock lock(g_resumeMutex); + g_resumeCv.wait(lock, []{ return g_resumeRequested; }); + g_resumeRequested = false; + } + + void fireInitialStop(){ + std::lock_guard lock(g_initStopMutex); + if(!g_initialStopFired){ + g_initialStopFired = true; + g_initialStopSignal.set_value(); + } + } + + void DrainCommandQueue(){ + std::deque> pending; + { + std::lock_guard lock(g_commandMutex); + pending.swap(g_commandQueue); + } + for(auto& cmd : pending) cmd(); + } + + bool WriteInt3(HANDLE hProcess, uint64_t address, uint8_t& outOriginalByte){ + SIZE_T bytesRead = 0; + if(!ReadProcessMemory(hProcess, reinterpret_cast(address), &outOriginalByte, 1, &bytesRead) || bytesRead!=1){ + fprintf(stderr, "WriteInt3: ReadProcessMemory failed at 0x%llx: %lu\n", address, GetLastError()); + return false; + } + + DWORD oldProtect = 0; + if(!VirtualProtectEx(hProcess, reinterpret_cast(address), 1, PAGE_EXECUTE_READWRITE, &oldProtect)){ + fprintf(stderr, "WriteInt3: VirtualProtectEx failed: %lu\n", GetLastError()); + return false; + } + + uint8_t int3 = 0xCC; + SIZE_T bytesWritten = 0; + bool ok = WriteProcessMemory(hProcess, reinterpret_cast(address), &int3, 1, &bytesWritten) && bytesWritten == 1; + + DWORD ignored; + VirtualProtectEx(hProcess, reinterpret_cast(address), 1, oldProtect, &ignored); + + if(!ok){ + fprintf(stderr, "WriteInt3: WriteProcessMemory failed: %lu\n", GetLastError()); + return false; + } + return true; + } + + bool RestoreOriginalByte(HANDLE hProcess, uint64_t address, uint8_t originalByte){ + DWORD oldProtect = 0; + VirtualProtectEx(hProcess, reinterpret_cast(address), 1, PAGE_EXECUTE_READWRITE, &oldProtect); + + SIZE_T bytesWritten = 0; + bool ok = WriteProcessMemory(hProcess, reinterpret_cast(address), &originalByte, 1, &bytesWritten) && bytesWritten == 1; + + DWORD ignored; + VirtualProtectEx(hProcess, reinterpret_cast(address), 1, oldProtect, &ignored); + + return ok; + } + + bool SendLaunchResponse(Connection* conn, uint64_t requestId, bool success){ + x2win::Envelope response; + response.set_request_id(requestId); + response.mutable_launch_response()->set_success(success); + return conn->WriteEnvelope(response); + } + + class BreakpointTable{ + std::mutex m_mutex; + std::unordered_map m_breakpoints; + uint64_t m_nextId = 1; + + public: + std::optional Add(HANDLE process, uint64_t address){ + std::lock_guard lock(m_mutex); + for(auto& [id, bp] : m_breakpoints){ + if(bp.address == address) return id; + } + + uint8_t originalByte = 0; + if(!WriteInt3(process, address, originalByte)) return std::nullopt; + + uint64_t id = m_nextId++; + m_breakpoints[id] = BreakpointInfo{address, originalByte}; + return id; + } + + std::optional OnHit(HANDLE process, uint64_t address){ + std::lock_guard lock(m_mutex); + for(auto& [id, bp] : m_breakpoints){ + if(bp.address == address){ + RestoreOriginalByte(process, address, bp.originalByte); + return bp; + } + } + return std::nullopt; + } + + void RestoreBytesInBuffer(uint8_t* buffer, uint64_t address, uint64_t size){ + std::lock_guard lock(m_mutex); + for(auto& [id, bp] : m_breakpoints){ + if(address <= bp.address && bp.address < address + size){ + buffer[bp.address - address] = bp.originalByte; + } + } + } + + void RestoreAll(HANDLE process){ + std::lock_guard lock(m_mutex); + for(auto& [id, bp] : m_breakpoints){ + RestoreOriginalByte(process, bp.address, bp.originalByte); + } + m_breakpoints.clear(); + } + + void Clear(){ + std::lock_guard lock(m_mutex); + m_breakpoints.clear(); + m_nextId = 1; + } + }; + + BreakpointTable g_breakpoints; +} + +namespace x2win{ + void PrepareNewSession(){ + g_initialBreakpointSeen = false; + g_debugeeProcess = nullptr; + g_breakpoints.Clear(); + { + std::lock_guard lock(g_targetStateMutex); + g_threads.clear(); + g_modules.clear(); + } + { + std::lock_guard lock(g_resumeMutex); + g_resumeRequested = false; + } + { + std::lock_guard lock(g_initStopMutex); + g_initialStopSignal = std::promise(); + g_initialStopFired = false; + } + } + + bool AddBreakpoint(uint64_t address, uint64_t &breakpointId){ + if(!g_debugeeProcess) return false; + + auto id = g_breakpoints.Add(g_debugeeProcess, address); + if(!id) return false; + + breakpointId = *id; + + fprintf(stderr, "[breakpoint] armed id=%llu at 0x%llx\n", *id, address); + return true; + } + + bool ReadTargetMemory(uint64_t address, uint64_t size, std::vector &outBuffer){ + if(!g_debugeeProcess) return false; + + outBuffer.resize(size); + SIZE_T bytesRead = 0; + bool ok = ReadProcessMemory(g_debugeeProcess, reinterpret_cast(address), outBuffer.data(), size, &bytesRead) && bytesRead == size; + + if(!ok){ + outBuffer.clear(); + return false; + } + + g_breakpoints.RestoreBytesInBuffer(outBuffer.data(), address, size); + return true; + } + + std::vector GetModuleList(){ + std::vector result; + std::lock_guard lock(g_targetStateMutex); + for(const auto& [base, info] : g_modules){ + result.push_back(ModuleRecord{base, info.path}); + } + return result; + } + + bool RunOnDebugLoop(std::function fn){ + if(!g_debugeeProcess) return false; + + auto promise = std::make_shared>(); + std::future future = promise->get_future(); + { + std::lock_guard lock(g_commandMutex); + g_commandQueue.push_back([fn = std::move(fn), promise]() mutable{ + promise->set_value(fn()); + }); + } + SignalResume(); + DebugBreakProcess(g_debugeeProcess); + return future.get(); + } + + int RunDebugLoop(const std::string &targetPath, Connection* conn, uint64_t requestId){ + STARTUPINFOA si{}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{}; + + std::string cmdLine = targetPath; + if(!CreateProcessA( + nullptr, cmdLine.data(), + nullptr, nullptr, FALSE, + DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS, + nullptr, nullptr, + &si, &pi)){ + fprintf(stderr, "CreateProcess failed: %lu\n", GetLastError()); + if(conn) SendLaunchResponse(conn, requestId, false); + return 1; + } + + if(conn) SendLaunchResponse(conn, requestId, true); + g_debugeeProcess = pi.hProcess; + DebugSetProcessKillOnExit(FALSE); + + fprintf(stderr, "launched ###pid = %lu### tid = %lu\n", pi.dwProcessId, pi.dwThreadId); + + bool running = true; + while(running){ + DEBUG_EVENT event{}; + if(!WaitForDebugEvent(&event, INFINITE)){ + fprintf(stderr, "WaitForDebugEvent failed: %lu\n", GetLastError()); + break; + } + + DrainCommandQueue(); + if(!g_debugeeProcess){ + running = false; + continue; + } + + DWORD continueStatus = DBG_CONTINUE; + switch (event.dwDebugEventCode) { + case CREATE_PROCESS_DEBUG_EVENT:{ + fprintf(stderr, "[event] CREATE_PROCESS pid=%lu\n", event.dwProcessId); + uint64_t base = reinterpret_cast(event.u.CreateProcessInfo.lpBaseOfImage); + fprintf(stderr, "[event] main module base = 0x%llx\n", base); + { + std::lock_guard lock(g_targetStateMutex); + auto slash = targetPath.find_last_of("\\/"); + std::string baseName = (slash == std::string::npos) ? targetPath : targetPath.substr(slash + 1); + g_modules[base] = ModuleInfo{base, baseName}; + } + CloseHandle(event.u.CreateProcessInfo.hFile); + break; + } + case EXIT_PROCESS_DEBUG_EVENT: + fprintf(stderr, "[event] EXIT_PROCESS pid=%lu\n", event.dwProcessId); + running = false; + break; + case CREATE_THREAD_DEBUG_EVENT: + fprintf(stderr, "[event] CREATE_THREAD tid=%lu\n", event.dwThreadId); + { + std::lock_guard lock(g_targetStateMutex); + g_threads[event.dwThreadId] = ThreadInfo{event.dwThreadId, event.u.CreateThread.hThread}; + } + break; + case EXIT_THREAD_DEBUG_EVENT: + fprintf(stderr, "[event] EXIT_THREAD tid=%lu\n", event.dwThreadId); + { + std::lock_guard lock(g_targetStateMutex); + g_threads.erase(event.dwThreadId); + } + break; + case LOAD_DLL_DEBUG_EVENT: + fprintf(stderr, "[event] LOAD_DLL base=%p\n", event.u.LoadDll.lpBaseOfDll); + { + std::lock_guard lock(g_targetStateMutex); + uint64_t base = reinterpret_cast(event.u.LoadDll.lpBaseOfDll); + g_modules[base] = ModuleInfo{base, ""}; + } + CloseHandle(event.u.LoadDll.hFile); + break; + case UNLOAD_DLL_DEBUG_EVENT: + fprintf(stderr, "[event] UNLOAD_DLL base=%p\n", event.u.UnloadDll.lpBaseOfDll); + { + std::lock_guard lock(g_targetStateMutex); + g_modules.erase(reinterpret_cast(event.u.UnloadDll.lpBaseOfDll)); + } + break; + case EXCEPTION_DEBUG_EVENT:{ + auto code = event.u.Exception.ExceptionRecord.ExceptionCode; + auto address = reinterpret_cast(event.u.Exception.ExceptionRecord.ExceptionAddress); + fprintf(stderr, "[event] EXCEPTION code=0x%lx firstChance=%lu address=0x%llx\n", + code, event.u.Exception.dwFirstChance, address); + + if(code == EXCEPTION_BREAKPOINT && !g_initialBreakpointSeen){ + g_initialBreakpointSeen = true; + fprintf(stderr, "[breakpoint] INITIAL system breakpoint at 0x%llx\n", address); + g_lastStopAddress = address; + fireInitialStop(); + + // We have 2 different behaviour in here + // 1 conn not establised which is target mode, need upper hanlder to send the + // the stopped event back to host + // 2 conn established whichi is server mode, can send stopped event immdiatilaly + if(conn){ + Envelope stoppedEvent; + stoppedEvent.mutable_target_stopped_event()->set_reason(STOP_REASON_INITIAL_BREAKPOINT); + conn->WriteEnvelope(stoppedEvent); + } + fprintf(stderr, "[debug loop] reported initial breakpoint, waiting for GoRequest...\n"); + + WaitForResume(); + fprintf(stderr, "[debug loop] resumed\n"); + }else if(code == EXCEPTION_BREAKPOINT){ + auto hit = g_breakpoints.OnHit(pi.hProcess, address); + if(hit){ + fprintf(stderr, "[breakpoint] hit at 0x%llx\n", address); + HANDLE hThread = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, event.dwThreadId); + if(hThread){ + CONTEXT ctx{}; + ctx.ContextFlags = CONTEXT_CONTROL; + if(GetThreadContext(hThread, &ctx)){ + ctx.Rip = address; + if(!SetThreadContext(hThread, &ctx)){ + fprintf(stderr, "[breakpoint] SetThreadContext failed: %lu\n", GetLastError()); + } + }else{ + fprintf(stderr, "[breakpoint] GetThreadContext failed: %lu\n", GetLastError()); + } + CloseHandle(hThread); + }else{ + fprintf(stderr, "[breakpoint] OpenThread failed: %lu\n", GetLastError()); + } + + g_lastStopAddress = address; + if(conn){ + Envelope stoppedEvent; + stoppedEvent.mutable_target_stopped_event()->set_reason(STOP_REASON_BREAKPOINT); + stoppedEvent.mutable_target_stopped_event()->set_address(address); + conn->WriteEnvelope(stoppedEvent); + } + fprintf(stderr, "[debug loop] reported breakpoint, waiting for GoRequest...\n"); + + WaitForResume(); + fprintf(stderr, "[debug loop] resumed\n"); + } + }else if(code != EXCEPTION_BREAKPOINT && code != EXCEPTION_SINGLE_STEP && !event.u.Exception.dwFirstChance){ + continueStatus = DBG_EXCEPTION_NOT_HANDLED; + } + break; + } + default: + break; + } + if(!ContinueDebugEvent(event.dwProcessId, event.dwThreadId, continueStatus)){ + fprintf(stderr, "ContinueDebugEvent failed: %lu\n", GetLastError()); + break; + } + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + g_debugeeProcess = nullptr; + return 0; + } + + uint64_t GetLastStopAddress(){ return g_lastStopAddress.load();} + + void SignalGo(){ + SignalResume(); + } + + void WaitForInitialStop(){ + g_initialStopSignal.get_future().wait(); + } + + void TerminateTarget(int exitCode){ + if(g_debugeeProcess){ + TerminateProcess(g_debugeeProcess, exitCode); + SignalResume(); + } + } + + void HandleDisconnect(){ + if(g_debugeeProcess){ + fprintf(stderr, "[debug loop] client disconnected, terminating orphaned debuggee\n"); + TerminateTarget(1); + } + } + + bool RequestDetach(){ + return RunOnDebugLoop([]() -> bool{ + g_breakpoints.RestoreAll(g_debugeeProcess); + DebugSetProcessKillOnExit(FALSE); + bool ok = DebugActiveProcessStop(GetProcessId(g_debugeeProcess)); + g_debugeeProcess = nullptr; + return ok; + }); + } +} \ No newline at end of file diff --git a/x2winstub/debug/debug_loop.h.superseded b/x2winstub/debug/debug_loop.h.superseded new file mode 100644 index 00000000..5a602a20 --- /dev/null +++ b/x2winstub/debug/debug_loop.h.superseded @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include +#include + +class Connection; + +namespace x2win{ + void PrepareNewSession(); + int RunDebugLoop(const std::string& targetPath, Connection* conn = nullptr, uint64_t requestId = 0); + void WaitForInitialStop(); + void SignalGo(); + void HandleDisconnect(); + bool AddBreakpoint(uint64_t address, uint64_t& breakpointId); + bool RunOnDebugLoop(std::function fn); + void TerminateTarget(int exitCode=1); + uint64_t GetLastStopAddress(); + bool RequestDetach(); + bool ReadTargetMemory(uint64_t address, uint64_t size, std::vector& outBuffer); + struct ModuleRecord{uint64_t base; std::string name;}; + std::vector GetModuleList(); +} \ No newline at end of file diff --git a/x2winstub/debug/debug_types.h b/x2winstub/debug/debug_types.h new file mode 100644 index 00000000..a47950d4 --- /dev/null +++ b/x2winstub/debug/debug_types.h @@ -0,0 +1,265 @@ +#pragma once +// Plain data types used by WindowsDebugEngine, copied from core/debugadapter.h and +// core/debuggercommon.h. Those headers are BN-API-free themselves, but they transitively pull in +// binaryninjaapi.h through core/debugadapter.h, so the types are copied here rather than included, +// to keep x2winstub entirely independent of Binary Ninja. +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace x2win { + + struct ModuleNameAndOffset + { + std::string module; + uint64_t offset; + + ModuleNameAndOffset() : module(""), offset(0) {} + ModuleNameAndOffset(std::string mod, uint64_t off) : module(mod), offset(off) {} + + bool operator==(const ModuleNameAndOffset& other) const + { + return IsSameBaseModule(other) && (offset == other.offset); + } + + static std::string GetPathBaseName(const std::string& path) + { +#ifdef _WIN32 + char baseName[MAX_PATH]; + char ext[MAX_PATH]; + _splitpath_s(path.c_str(), NULL, 0, NULL, 0, baseName, MAX_PATH, ext, MAX_PATH); + return std::string(baseName) + std::string(ext); +#else + auto slash = path.find_last_of("/\\"); + return slash == std::string::npos ? path : path.substr(slash + 1); +#endif + } + + bool IsSameBaseModule(const ModuleNameAndOffset& other) const + { + return (module == other.module) || (GetPathBaseName(module) == GetPathBaseName(other.module)); + } + + bool IsSameBaseModule(const std::string& other) const + { + return (module == other) || (GetPathBaseName(module) == GetPathBaseName(other)); + } + }; + + // Breakpoint types - used to specify the type of breakpoint to set + enum DebugBreakpointType + { + SoftwareBreakpoint = 0, + HardwareExecuteBreakpoint = 1, + HardwareReadBreakpoint = 2, + HardwareWriteBreakpoint = 3, + HardwareAccessBreakpoint = 4 + }; + + // Subset of BNDebugStopReason (core/api/ffi.h) actually produced by WindowsDebugEngine. + enum DebugStopReason + { + UnknownReason, + InitialBreakpoint, + ProcessExited, + AccessViolation, + SingleStep, + Calculation, + Breakpoint, + IllegalInstruction + }; + + struct LaunchConfigurations + { + bool requestTerminalEmulator; + std::string inputFile; + bool connectedToDebugServer; + + LaunchConfigurations() : requestTerminalEmulator(true), connectedToDebugServer(false) {} + }; + + struct DebugProcess + { + std::uint32_t m_pid {}; + std::string m_processName {}; + std::string m_commandLine {}; + + DebugProcess() {} + DebugProcess(std::uint32_t pid) : m_pid(pid) {} + DebugProcess(std::uint32_t pid, std::string name) : m_pid(pid), m_processName(name) {} + DebugProcess(std::uint32_t pid, std::string name, std::string commandLine) : + m_pid(pid), m_processName(name), m_commandLine(commandLine) {} + }; + + struct DebugThread + { + std::uint32_t m_tid {}; + std::uintptr_t m_rip {}; + bool m_isFrozen {}; + + DebugThread() {} + DebugThread(std::uint32_t tid) : m_tid(tid) {} + DebugThread(std::uint32_t tid, std::uintptr_t rip) : m_tid(tid), m_rip(rip) {} + }; + + struct DebugBreakpoint + { + std::uintptr_t m_address {}; + unsigned long m_id {}; + bool m_is_active {}; + DebugBreakpointType m_type = SoftwareBreakpoint; + + DebugBreakpoint(std::uintptr_t address, unsigned long id, bool active, DebugBreakpointType type = SoftwareBreakpoint) : + m_address(address), m_id(id), m_is_active(active), m_type(type) + {} + DebugBreakpoint(std::uintptr_t address, DebugBreakpointType type = SoftwareBreakpoint) : + m_address(address), m_type(type) {} + DebugBreakpoint() {} + + bool operator==(const DebugBreakpoint& rhs) const { return m_address == rhs.m_address; } + }; + + // Pending hardware breakpoint info (to be applied when target becomes active) + struct PendingHardwareBreakpoint + { + ModuleNameAndOffset location; + uint64_t address; + DebugBreakpointType type; + size_t size; + bool isRelative; + + PendingHardwareBreakpoint(uint64_t addr, DebugBreakpointType bpType, size_t bpSize) + : location(), address(addr), type(bpType), size(bpSize), isRelative(false) {} + PendingHardwareBreakpoint(const ModuleNameAndOffset& loc, DebugBreakpointType bpType, size_t bpSize) + : location(loc), address(0), type(bpType), size(bpSize), isRelative(true) {} + }; + + struct DebugRegister + { + std::string m_name {}; + uint64_t m_value {}; + std::size_t m_width {}, m_registerIndex {}; + + DebugRegister() = default; + DebugRegister(std::string name, uint64_t value, std::size_t width, std::size_t register_index) : + m_name(std::move(name)), m_value(value), m_width(width), m_registerIndex(register_index) + {} + }; + + struct DebugModule + { + std::string m_name {}, m_short_name {}; + std::uintptr_t m_address {}; + std::size_t m_size {}; + bool m_loaded {}; + // Matches BN's "debugger.caseInsensitiveModuleName" setting, default true. + bool m_caseInsensitive {true}; + + DebugModule() = default; + DebugModule(std::string name, std::string short_name, std::uintptr_t address, std::size_t size, bool loaded) : + m_name(std::move(name)), m_short_name(std::move(short_name)), m_address(address), m_size(size), m_loaded(loaded) + {} + + static std::string GetPathBaseName(const std::string& path) + { + return ModuleNameAndOffset::GetPathBaseName(path); + } + + static bool StringsEqual(const std::string& a, const std::string& b, bool caseInsensitive) + { + if (!caseInsensitive) + return a == b; + if (a.size() != b.size()) + return false; + return std::equal(a.begin(), a.end(), b.begin(), + [](char c1, char c2) { return std::tolower((unsigned char)c1) == std::tolower((unsigned char)c2); }); + } + + bool IsSameBaseModule(const DebugModule& other) const + { + return StringsEqual(m_name, other.m_name, m_caseInsensitive) + || StringsEqual(m_short_name, other.m_short_name, m_caseInsensitive) + || StringsEqual(GetPathBaseName(m_name), GetPathBaseName(other.m_name), m_caseInsensitive) + || StringsEqual(GetPathBaseName(m_short_name), GetPathBaseName(other.m_short_name), m_caseInsensitive); + } + + bool IsSameBaseModule(const std::string& name) const + { + return StringsEqual(m_name, name, m_caseInsensitive) + || StringsEqual(m_short_name, name, m_caseInsensitive) + || StringsEqual(GetPathBaseName(m_name), GetPathBaseName(name), m_caseInsensitive) + || StringsEqual(GetPathBaseName(m_short_name), GetPathBaseName(name), m_caseInsensitive); + } + }; + + struct DebugMemoryRegion + { + std::uintptr_t m_start {}; + std::size_t m_size {}; + std::string m_name {}; + bool m_read {}; + bool m_write {}; + bool m_execute {}; + bool m_shared {}; + + DebugMemoryRegion() = default; + }; + + struct DebugFrame + { + size_t m_index = 0; + uint64_t m_pc = 0; + uint64_t m_sp = 0; + uint64_t m_fp = 0; + std::string m_functionName; + uint64_t m_functionStart = 0; + std::string m_module = ""; + + DebugFrame() = default; + }; + + // Used by WindowsDebugEngine to query capacities; mirrors DebugAdapterCapacity from + // core/debugadapter.h (subset actually referenced by SupportFeature()). + enum DebugAdapterCapacity + { + DebugAdapterSupportStepOver, + DebugAdapterSupportStepReturn, + DebugAdapterSupportStepOverReverse, + DebugAdapterSupportModules, + DebugAdapterSupportThreads, + DebugAdapterSupportTTD, + }; + + // Replaces DebugAdapter::PostDebuggerEvent()/DebuggerEvent from core/debugadapter.h -- only the + // subset of fields WindowsDebugEngine actually populates across its 8 event call sites. + enum class EngineEventType + { + LaunchFailure, + TargetExited, + TargetStopped, + Resumed, + StepIntoComplete + }; + + struct EngineEvent + { + EngineEventType type = EngineEventType::TargetStopped; + + // TargetStopped + DebugStopReason stopReason = UnknownReason; + uint32_t lastActiveThread = 0; + + // TargetExited + uint64_t exitCode = 0; + + // LaunchFailure + std::string error; + std::string shortError; + }; + +} // namespace x2win diff --git a/x2winstub/debug/windows_debug_engine.cpp b/x2winstub/debug/windows_debug_engine.cpp new file mode 100644 index 00000000..e6e4915d --- /dev/null +++ b/x2winstub/debug/windows_debug_engine.cpp @@ -0,0 +1,3118 @@ +/* +Ported from core/adapters/windowsnativeadapter.cpp (BinaryNinjaDebugger::WindowsNativeAdapter). +See windows_debug_engine.h for what changed and why. Summary of the non-mechanical changes (beyond +renaming the class and dropping BN-only code): + - The dbghelp.dll delay-load hook (originally there to avoid a DLL-version clash with the DbgEng + adapter *running in the same BN process*) is dropped entirely -- x2winstub is its own process, + so that clash can't happen; dbghelp.dll is now just linked normally. + - Settings::Instance() lookups become plain local fields (see header) with the same defaults as + the BN debugger.* settings they replace. + - The "auto-breakpoint at the BinaryView's analyzed entry function" part of the initial-breakpoint + handling is dropped -- it required BinaryView analysis data this standalone engine doesn't have. + The rest of that logic (the stopAtSystemEntryPoint check, which needs no analysis data) is kept. + - DataBuffer becomes std::vector; DebuggerEvent/PostDebuggerEvent become EngineEvent/ + PostEngineEvent (see debug_types.h for the mapping, checked against all 8 original call sites). + - ExecuteWithArgs() now actually uses its path/args/workingDir parameters (the original read them + from BN Settings instead and ignored the parameters -- a BN-GUI-specific quirk that doesn't + apply here; the proto LaunchRequest already carries these explicitly). +*/ +#include "windows_debug_engine.h" +#include +#include +#include +#include +#include +#include +#include + +namespace x2win { + + void LogWarn(const char* fmt, ...) + { + va_list args; + va_start(args, fmt); + fprintf(stderr, "[x2winstub][WARN] "); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); + } + + void LogError(const char* fmt, ...) + { + va_list args; + va_start(args, fmt); + fprintf(stderr, "[x2winstub][ERROR] "); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); + } + + // INT3 instruction opcode + constexpr uint8_t INT3_OPCODE = 0xCC; + + WindowsDebugEngine::WindowsDebugEngine() + { + } + + + WindowsDebugEngine::~WindowsDebugEngine() + { + if (m_activelyDebugging) + Quit(); + + // If the target exited on its own, HandleExitProcess cleared m_activelyDebugging and the + // debug loop returned, but nobody joined the thread. Destroying a joinable std::thread + // calls std::terminate, so join here to cover that path. + if (m_debugThread.joinable()) + m_debugThread.join(); + } + + + void WindowsDebugEngine::PostEngineEvent(const EngineEvent& event) + { + if (m_eventCallback) + m_eventCallback(event); + } + + + bool WindowsDebugEngine::Execute(const std::string& path, const LaunchConfigurations& configs) + { + return ExecuteWithArgs(path, "", "", configs); + } + + + bool WindowsDebugEngine::ExecuteWithArgs(const std::string& path, const std::string& args, + const std::string& workingDir, const LaunchConfigurations& configs) + { + // Reset any previous state + Reset(); + + m_launchExecutable = path; + m_launchWorkingDir = workingDir; + m_launchCommandLine = path; + if (!args.empty()) + m_launchCommandLine += " " + args; + m_isAttaching = false; + m_launchResult = false; + m_launchError.clear(); + + // Start the debug loop thread - it will create the process + m_debugThread = std::thread(&WindowsDebugEngine::DebugLoop, this); + + // Wait for the debug thread to signal success or failure + { + std::unique_lock lock(m_launchMutex); + m_launchCondition.wait(lock, [this] { return m_launchResult.load() || !m_launchError.empty(); }); + } + + if (!m_launchError.empty()) + { + LogError("Failed to create process: %s", m_launchError.c_str()); + EngineEvent event; + event.type = EngineEventType::LaunchFailure; + event.error = m_launchError; + event.shortError = "CreateProcess failed"; + PostEngineEvent(event); + + // Wait for debug thread to finish + if (m_debugThread.joinable()) + m_debugThread.join(); + return false; + } + + return true; + } + + + bool WindowsDebugEngine::Attach(std::uint32_t pid) + { + // Reset any previous state + Reset(); + + m_attachPID = static_cast(pid); + m_isAttaching = true; + m_launchResult = false; + m_launchError.clear(); + + // Start the debug loop thread - it will attach to the process + m_debugThread = std::thread(&WindowsDebugEngine::DebugLoop, this); + + // Wait for the debug thread to signal success or failure + { + std::unique_lock lock(m_launchMutex); + m_launchCondition.wait(lock, [this] { return m_launchResult.load() || !m_launchError.empty(); }); + } + + if (!m_launchError.empty()) + { + LogError("Failed to attach to process: %s", m_launchError.c_str()); + EngineEvent event; + event.type = EngineEventType::LaunchFailure; + event.error = m_launchError; + event.shortError = "Attach failed"; + PostEngineEvent(event); + + // Wait for debug thread to finish + if (m_debugThread.joinable()) + m_debugThread.join(); + return false; + } + + return true; + } + + + bool WindowsDebugEngine::Detach() + { + if (!m_activelyDebugging) + return true; + + // Set the stop flag under m_debugMutex so the DebugLoop's condition_variable wait + // (whose predicate reads m_shouldStop) can't miss the wakeup if it is between evaluating + // the predicate and parking. Modifying the flag without the lock races with that window + // and can lose the notify, hanging the join() below forever even though m_shouldStop is + // atomic. + { + std::lock_guard lock(m_debugMutex); + m_shouldStop = true; + } + + // Wake up the debug thread if it's waiting + m_debugCondition.notify_one(); + + if (m_debugThread.joinable()) + m_debugThread.join(); + + // Thread handles in m_threads come from debug events (CREATE_PROCESS/CREATE_THREAD); + // Windows closes those automatically when debugging ends, so we must not close them + // here (see HandleExitThread). Doing so raises STATUS_INVALID_HANDLE under a debugger. + m_threads.clear(); + + // The initial thread handle from CreateProcess is owned by us. + if (m_threadHandle) + { + CloseHandle(m_threadHandle); + m_threadHandle = nullptr; + } + + if (m_processHandle) + { + CloseHandle(m_processHandle); + m_processHandle = nullptr; + } + + m_activelyDebugging = false; + m_targetRunning = false; + + EngineEvent event; + event.type = EngineEventType::TargetExited; + event.exitCode = 0; + PostEngineEvent(event); + + return true; + } + + + bool WindowsDebugEngine::Quit() + { + if (!m_activelyDebugging) + return true; + + // Set the stop flag under m_debugMutex so the DebugLoop's condition_variable wait + // (whose predicate reads m_shouldStop) can't miss the wakeup if it is between evaluating + // the predicate and parking. Modifying the flag without the lock races with that window + // and can lose the notify, hanging the join() below forever even though m_shouldStop is + // atomic. + { + std::lock_guard lock(m_debugMutex); + m_shouldStop = true; + } + + // Wake up the debug thread if it's waiting + m_debugCondition.notify_one(); + + // Terminate the process + if (m_processHandle) + TerminateProcess(m_processHandle, 0); + + if (m_debugThread.joinable()) + m_debugThread.join(); + + // Thread handles in m_threads come from debug events (CREATE_PROCESS/CREATE_THREAD); + // Windows closes those automatically when debugging ends, so we must not close them + // here (see HandleExitThread). Doing so raises STATUS_INVALID_HANDLE under a debugger. + m_threads.clear(); + + // The initial thread handle from CreateProcess is owned by us. + if (m_threadHandle) + { + CloseHandle(m_threadHandle); + m_threadHandle = nullptr; + } + + if (m_processHandle) + { + CloseHandle(m_processHandle); + m_processHandle = nullptr; + } + + m_activelyDebugging = false; + m_targetRunning = false; + + EngineEvent event; + event.type = EngineEventType::TargetExited; + event.exitCode = m_exitCode; + PostEngineEvent(event); + + return true; + } + + + void WindowsDebugEngine::Reset() + { + // Wait for any existing debug thread to finish + if (m_debugThread.joinable()) + m_debugThread.join(); + + // Thread handles in m_threads come from debug events (CREATE_PROCESS/CREATE_THREAD); + // Windows closes those automatically when debugging ends, so we must not close them + // here (see HandleExitThread). Doing so raises STATUS_INVALID_HANDLE under a debugger. + m_threads.clear(); + + // The initial thread handle from CreateProcess is owned by us. + if (m_threadHandle) + { + CloseHandle(m_threadHandle); + m_threadHandle = nullptr; + } + + // Close process handle + if (m_processHandle) + { + CloseHandle(m_processHandle); + m_processHandle = nullptr; + } + + // Reset state variables + m_threadHandle = nullptr; + m_processId = 0; + m_threadId = 0; + m_activeThreadId = 0; + m_hasLastDebugEvent = false; + m_activelyDebugging = false; + m_targetRunning = false; + m_shouldStop = false; + m_stopReason = UnknownReason; + m_exitCode = 0; + + // Clear modules + { + std::lock_guard lock(m_modulesMutex); + m_modules.clear(); + } + + // Clear breakpoints (but keep them for re-apply on restart) + { + std::lock_guard lock(m_breakpointsMutex); + for (auto& bp : m_breakpoints) + { + bp.isActive = false; + bp.originalByte = 0; // Clear stale original byte from previous session + bp.hasOriginalByte = false; // ...and mark it as no longer known, not just zeroed + } + } + + // Clear hardware breakpoints state + { + std::lock_guard lock(m_hwBreakpointsMutex); + for (auto& hwbp : m_hardwareBreakpoints) + { + hwbp.isActive = false; + hwbp.drIndex = -1; + } + } + + // Reset step tracking + m_singleStepping = false; + m_stepOverBreakpointAddress = 0; + m_hasStepOverBreakpoint = false; + m_stepOverBreakpointContinue = false; + + // Reset hardware breakpoint step-over tracking + m_stepOverHwBreakpointIndex = -1; + m_hasStepOverHwBreakpoint = false; + m_stepOverHwBreakpointContinue = false; + + // Reset temp breakpoint + m_hasTempBreakpoint = false; + m_tempBreakpointAddress = 0; + m_tempBreakpointOriginalByte = 0; + + // Reset initial breakpoint tracking + m_initialBreakpointSeen = false; + m_wow64InitialBreakpointSeen = false; + + // Reset WOW64 flag (will be re-detected on next process start) + m_isTargetWow64 = false; + + // Reset launch state + m_launchResult = false; + m_launchError.clear(); + } + + + bool WindowsDebugEngine::StartDebugging() + { + LogVerbose("WindowsDebugEngine::StartDebugging - isAttaching=%d", m_isAttaching); + + if (m_isAttaching) + { + // Attach to existing process + if (!DebugActiveProcess(m_attachPID)) + { + char buf[256]; + snprintf(buf, sizeof(buf), "Failed to attach to process %lu: %lu", m_attachPID, GetLastError()); + m_launchError = buf; + LogError("%s", m_launchError.c_str()); + return false; + } + + m_processId = m_attachPID; + m_processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_attachPID); + if (!m_processHandle) + { + char buf[256]; + snprintf(buf, sizeof(buf), "Failed to open process %lu: %lu", m_attachPID, GetLastError()); + m_launchError = buf; + LogError("%s", m_launchError.c_str()); + DebugActiveProcessStop(m_attachPID); + return false; + } + } + else + { + // Launch new process + STARTUPINFOA si {}; + PROCESS_INFORMATION pi {}; + si.cb = sizeof(si); + + DWORD creationFlags = DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE; + + LogVerbose("CreateProcessA: %s, workingDir=%s", + m_launchCommandLine.c_str(), m_launchWorkingDir.c_str()); + + if (!CreateProcessA( + nullptr, + const_cast(m_launchCommandLine.c_str()), + nullptr, + nullptr, + FALSE, + creationFlags, + nullptr, + m_launchWorkingDir.empty() ? nullptr : m_launchWorkingDir.c_str(), + &si, + &pi)) + { + char buf[256]; + snprintf(buf, sizeof(buf), "Failed to create process: %lu", GetLastError()); + m_launchError = buf; + LogError("%s", m_launchError.c_str()); + return false; + } + + m_processHandle = pi.hProcess; + m_threadHandle = pi.hThread; + m_processId = pi.dwProcessId; + m_threadId = pi.dwThreadId; + m_activeThreadId = pi.dwThreadId; + + // Add the initial thread to our tracking + m_threads[pi.dwThreadId] = pi.hThread; + + LogVerbose("Process created: PID=%d, TID=%d", m_processId, m_threadId); + } + + // Detect if the target is a WOW64 (32-bit) process + BOOL isWow64 = FALSE; + if (IsWow64Process(m_processHandle, &isWow64)) + { + m_isTargetWow64 = (isWow64 != FALSE); + LogVerbose("Target process WOW64 status: %s", m_isTargetWow64 ? "32-bit (WOW64)" : "64-bit"); + } + + m_activelyDebugging = true; + m_targetRunning = true; + + return true; + } + + + void WindowsDebugEngine::DebugLoop() + { + LogVerbose("WindowsDebugEngine::DebugLoop started"); + + // Create/attach to process on this thread (required by Windows debug API) + if (!StartDebugging()) + { + // Signal failure to the calling thread + { + std::lock_guard lock(m_launchMutex); + // m_launchError is already set by StartDebugging + } + m_launchCondition.notify_one(); + return; + } + + // Signal success to the calling thread + { + std::lock_guard lock(m_launchMutex); + m_launchResult = true; + } + m_launchCondition.notify_one(); + + DEBUG_EVENT debugEvent; + + while (m_activelyDebugging && !m_shouldStop) + { + if (!WaitForDebugEvent(&debugEvent, 100)) + { + if (GetLastError() == ERROR_SEM_TIMEOUT) + continue; + LogWarn("WaitForDebugEvent failed with error: %d", GetLastError()); + break; + } + + LogVerbose("Received debug event: code=%d, pid=%d, tid=%d", + debugEvent.dwDebugEventCode, debugEvent.dwProcessId, debugEvent.dwThreadId); + + m_lastDebugEvent = debugEvent; + m_hasLastDebugEvent = true; + + DWORD continueStatus = DBG_CONTINUE; + + bool shouldBreak = HandleDebugEvent(debugEvent); + LogVerbose("HandleDebugEvent returned shouldBreak=%d", shouldBreak); + + if (shouldBreak) + { + m_targetRunning = false; + + // Notify the controller that we've stopped + LogVerbose("Posting TargetStopped with reason=%d, thread=%d", m_stopReason, m_activeThreadId); + EngineEvent event; + event.type = EngineEventType::TargetStopped; + event.stopReason = m_stopReason; + event.lastActiveThread = m_activeThreadId; + event.exitCode = 0; + PostEngineEvent(event); + + // Wait for Go() or other commands + LogVerbose("Waiting for Go() or stop signal..."); + std::unique_lock lock(m_debugMutex); + m_debugCondition.wait(lock, [this] { return m_targetRunning || m_shouldStop; }); + LogVerbose("Wait completed: m_targetRunning=%d, m_shouldStop=%d", m_targetRunning.load(), m_shouldStop.load()); + + if (m_shouldStop) + { + RemoveAllBreakpoints(); + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); + + // DebugActiveProcessStop must be called from the same thread that started debugging + if (!DebugActiveProcessStop(m_processId)) + { + LogWarn("DebugActiveProcessStop failed (error %d) -- killing target", GetLastError()); + TerminateProcess(m_processHandle, 1); + } + + // Mark detach as already handled so the post-loop cleanup below (which exists for + // the "still running, never stopped" case) doesn't see m_activelyDebugging still + // true and redundantly call DebugActiveProcessStop a second time -- that second + // call would fail (already detached) and trip its own TerminateProcess fallback, + // killing the target even on a plain Detach(). + m_activelyDebugging = false; + + break; + } + } + + // Handle exception continue status + if (debugEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) + { + DWORD exCode = debugEvent.u.Exception.ExceptionRecord.ExceptionCode; + if (exCode == EXCEPTION_BREAKPOINT || + exCode == EXCEPTION_SINGLE_STEP || + exCode == 0x4000001F || // STATUS_WX86_BREAKPOINT + exCode == 0x4000001E) // STATUS_WX86_SINGLE_STEP + { + continueStatus = DBG_CONTINUE; + } + else if (!debugEvent.u.Exception.dwFirstChance) + { + continueStatus = DBG_EXCEPTION_NOT_HANDLED; + } + } + + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, continueStatus); + } + + // If we exited the loop due to m_shouldStop while the target was running (not stopped at a + // breakpoint), we still need to detach. The stopped-at-breakpoint case is handled inside the loop. + if (m_shouldStop && m_activelyDebugging) + { + RemoveAllBreakpoints(); + + if (!DebugActiveProcessStop(m_processId)) + { + LogWarn("DebugActiveProcessStop failed (error %d) -- killing target", GetLastError()); + TerminateProcess(m_processHandle, 1); + } + } + + m_activelyDebugging = false; + } + + + bool WindowsDebugEngine::HandleDebugEvent(const DEBUG_EVENT& event) + { + switch (event.dwDebugEventCode) + { + case EXCEPTION_DEBUG_EVENT: + return HandleException(event.u.Exception); + + case CREATE_PROCESS_DEBUG_EVENT: + return HandleCreateProcess(event.u.CreateProcessInfo); + + case EXIT_PROCESS_DEBUG_EVENT: + return HandleExitProcess(event.u.ExitProcess); + + case CREATE_THREAD_DEBUG_EVENT: + return HandleCreateThread(event.u.CreateThread, event.dwThreadId); + + case EXIT_THREAD_DEBUG_EVENT: + return HandleExitThread(event.u.ExitThread, event.dwThreadId); + + case LOAD_DLL_DEBUG_EVENT: + return HandleLoadDll(event.u.LoadDll); + + case UNLOAD_DLL_DEBUG_EVENT: + return HandleUnloadDll(event.u.UnloadDll); + + case OUTPUT_DEBUG_STRING_EVENT: + return HandleOutputDebugString(event.u.DebugString); + + default: + return false; + } + } + + + bool WindowsDebugEngine::HandleException(const EXCEPTION_DEBUG_INFO& info) + { + m_activeThreadId = m_lastDebugEvent.dwThreadId; + + LogVerbose("HandleException: code=0x%08X, address=0x%llX, firstChance=%d", + info.ExceptionRecord.ExceptionCode, + (uint64_t)info.ExceptionRecord.ExceptionAddress, + info.dwFirstChance); + + switch (info.ExceptionRecord.ExceptionCode) + { + case EXCEPTION_BREAKPOINT: + case 0x4000001F: // STATUS_WX86_BREAKPOINT - WOW64 breakpoint exception + { + uint64_t address = (uint64_t)info.ExceptionRecord.ExceptionAddress; + + // Check if this is a temporary breakpoint (from StepOver/StepReturn) + if (m_hasTempBreakpoint && address == m_tempBreakpointAddress) + { + // Remove the temporary breakpoint + RemoveTempBreakpoint(); + + // Set IP back to the breakpoint address so the instruction executes + HANDLE threadHandle = m_threads[m_activeThreadId]; + if (threadHandle) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (Wow64GetThreadContext(threadHandle, &ctx)) + { + ctx.Eip = static_cast(address); + Wow64SetThreadContext(threadHandle, &ctx); + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (GetThreadContext(threadHandle, &ctx)) + { + ctx.Rip = address; + SetThreadContext(threadHandle, &ctx); + } + } + } + + m_stopReason = SingleStep; // Report as step completion + return true; + } + + // Check if this is one of our breakpoints + { + std::lock_guard lock(m_breakpointsMutex); + for (auto& bp : m_breakpoints) + { + if (bp.address == address && bp.isActive) + { + // Restore the original byte + WriteMemory(address, std::vector{bp.originalByte}); + + // Set IP back to the breakpoint address + HANDLE threadHandle = m_threads[m_activeThreadId]; + if (threadHandle) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (Wow64GetThreadContext(threadHandle, &ctx)) + { + ctx.Eip = static_cast(address); + Wow64SetThreadContext(threadHandle, &ctx); + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (GetThreadContext(threadHandle, &ctx)) + { + ctx.Rip = address; + SetThreadContext(threadHandle, &ctx); + } + } + } + + m_stopReason = Breakpoint; + return true; + } + } + } + + // Initial breakpoint (system breakpoint) + if (!m_initialBreakpointSeen) + { + m_initialBreakpointSeen = true; + + // Note: the original WindowsNativeAdapter also placed a breakpoint at the + // BinaryView's analyzed entry function here when "debugger.stopAtEntryPoint" was + // enabled. That requires BinaryView analysis data this standalone engine + // deliberately doesn't have -- dropped for this port. + + // When attaching to a running process, always stop at the attach breakpoint. + // When launching a new process, respect m_stopAtSystemEntryPoint. + if (!m_isAttaching && !m_stopAtSystemEntryPoint) + { + return false; // Don't stop, continue running + } + + m_stopReason = InitialBreakpoint; + return true; + } + + // WOW64 processes have a second system breakpoint (LdrpDoDebuggerBreak in 32-bit ntdll) + if (m_isTargetWow64 && !m_wow64InitialBreakpointSeen) + { + m_wow64InitialBreakpointSeen = true; + + // When attaching, always stop at the attach breakpoint (even for WOW64 second breakpoint) + if (!m_isAttaching && !m_stopAtSystemEntryPoint) + { + return false; // Don't stop, continue running + } + + m_stopReason = InitialBreakpoint; + return true; + } + + // Unknown breakpoint - stop and report + m_stopReason = Breakpoint; + return true; + } + + case EXCEPTION_SINGLE_STEP: + case 0x4000001E: // STATUS_WX86_SINGLE_STEP - WOW64 single step exception + { + // If we were stepping over a software breakpoint, re-apply it + if (m_hasStepOverBreakpoint) + { + std::lock_guard lock(m_breakpointsMutex); + for (auto& bp : m_breakpoints) + { + if (bp.address == m_stepOverBreakpointAddress) + { + ApplyBreakpoint(bp.address, bp.id); + break; + } + } + m_hasStepOverBreakpoint = false; + + // Resume all other threads that we suspended + for (const auto& [tid, handle] : m_threads) + { + if (tid != m_activeThreadId && handle) + { + ::ResumeThread(handle); + } + } + + // If this was from Go(), continue execution; if from StepInto(), stop + if (m_stepOverBreakpointContinue) + { + m_stepOverBreakpointContinue = false; + return false; // Don't stop, continue execution + } + // Fall through to normal single step handling (will stop) + } + + // If we were stepping over a hardware breakpoint, re-enable it + if (m_hasStepOverHwBreakpoint) + { + HANDLE threadHandle = m_threads[m_activeThreadId]; + if (threadHandle && m_stepOverHwBreakpointIndex >= 0) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(threadHandle, &ctx)) + { + ctx.Dr7 |= (1UL << (m_stepOverHwBreakpointIndex * 2)); + Wow64SetThreadContext(threadHandle, &ctx); + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(threadHandle, &ctx)) + { + ctx.Dr7 |= (1ULL << (m_stepOverHwBreakpointIndex * 2)); + SetThreadContext(threadHandle, &ctx); + } + } + } + m_hasStepOverHwBreakpoint = false; + + // If this was from Go(), continue execution + if (m_stepOverHwBreakpointContinue) + { + m_stepOverHwBreakpointContinue = false; + m_stepOverHwBreakpointIndex = -1; + return false; // Don't stop, continue execution + } + m_stepOverHwBreakpointIndex = -1; + // Fall through to normal single step handling (will stop) + } + + // Check if a hardware breakpoint was hit + HANDLE threadHandle = m_threads[m_activeThreadId]; + if (threadHandle) + { + int hitIndex = -1; + bool hwBpHit = false; + + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(threadHandle, &ctx)) + { + if (ctx.Dr6 & 0xF) + { + hwBpHit = true; + for (int i = 0; i < 4; i++) + { + if (ctx.Dr6 & (1 << i)) + { + hitIndex = i; + break; + } + } + ctx.Dr6 = 0; + Wow64SetThreadContext(threadHandle, &ctx); + } + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(threadHandle, &ctx)) + { + if (ctx.Dr6 & 0xF) + { + hwBpHit = true; + for (int i = 0; i < 4; i++) + { + if (ctx.Dr6 & (1 << i)) + { + hitIndex = i; + break; + } + } + ctx.Dr6 = 0; + SetThreadContext(threadHandle, &ctx); + } + } + } + + if (hwBpHit) + { + m_stepOverHwBreakpointIndex = hitIndex; + m_stopReason = Breakpoint; + return true; + } + } + + // Resume all other threads that were suspended during stepping + for (const auto& [tid, handle] : m_threads) + { + if (tid != m_activeThreadId && handle) + { + ::ResumeThread(handle); + } + } + + m_stopReason = SingleStep; + m_singleStepping = false; + return true; + } + + // Calculation exceptions (divide by zero, overflow, etc.) + case EXCEPTION_FLT_DENORMAL_OPERAND: + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_INEXACT_RESULT: + case EXCEPTION_FLT_INVALID_OPERATION: + case EXCEPTION_FLT_OVERFLOW: + case EXCEPTION_FLT_STACK_CHECK: + case EXCEPTION_FLT_UNDERFLOW: + case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_INT_OVERFLOW: + m_stopReason = Calculation; + return true; + + // Illegal instruction + case EXCEPTION_ILLEGAL_INSTRUCTION: + case EXCEPTION_PRIV_INSTRUCTION: + m_stopReason = IllegalInstruction; + return true; + + // Memory access violations and other fatal exceptions + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + case EXCEPTION_DATATYPE_MISALIGNMENT: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_INVALID_DISPOSITION: + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + case EXCEPTION_STACK_OVERFLOW: + m_stopReason = AccessViolation; + return true; + + default: + // First chance exceptions that we don't handle + if (info.dwFirstChance) + return false; + m_stopReason = AccessViolation; + return true; + } + } + + + bool WindowsDebugEngine::HandleCreateProcess(const CREATE_PROCESS_DEBUG_INFO& info) + { + LogVerbose("HandleCreateProcess: baseOfImage=0x%llX, startAddress=0x%llX", + (uint64_t)info.lpBaseOfImage, (uint64_t)info.lpStartAddress); + + // Store the initial thread handle + m_threads[m_lastDebugEvent.dwThreadId] = info.hThread; + m_activeThreadId = m_lastDebugEvent.dwThreadId; + + // Get module name + std::string moduleName = GetModuleNameFromHandle(info.hFile, info.lpBaseOfImage); + + // Add main module to module list + { + std::lock_guard lock(m_modulesMutex); + DebugModule module; + module.m_name = moduleName; + module.m_short_name = DebugModule::GetPathBaseName(moduleName); + module.m_address = (uintptr_t)info.lpBaseOfImage; + + // Get module size from PE header + IMAGE_DOS_HEADER dosHeader; + if (ReadProcessMemory(m_processHandle, info.lpBaseOfImage, &dosHeader, sizeof(dosHeader), nullptr)) + { + IMAGE_NT_HEADERS ntHeaders; + if (ReadProcessMemory(m_processHandle, + (LPVOID)((BYTE*)info.lpBaseOfImage + dosHeader.e_lfanew), + &ntHeaders, sizeof(ntHeaders), nullptr)) + { + module.m_size = ntHeaders.OptionalHeader.SizeOfImage; + } + } + module.m_loaded = true; + m_modules.push_back(module); + } + + if (info.hFile) + CloseHandle(info.hFile); + + // Apply pending breakpoints + ApplyPendingBreakpoints(); + + return false; // Don't stop on process creation + } + + + bool WindowsDebugEngine::HandleExitProcess(const EXIT_PROCESS_DEBUG_INFO& info) + { + m_exitCode = info.dwExitCode; + m_activelyDebugging = false; + m_stopReason = ProcessExited; + + EngineEvent event; + event.type = EngineEventType::TargetExited; + event.exitCode = info.dwExitCode; + PostEngineEvent(event); + + return false; + } + + + bool WindowsDebugEngine::HandleCreateThread(const CREATE_THREAD_DEBUG_INFO& info, DWORD threadId) + { + m_threads[threadId] = info.hThread; + + // Apply hardware breakpoints to the new thread + ApplyHardwareBreakpointsToThread(info.hThread); + + return false; + } + + + bool WindowsDebugEngine::HandleExitThread(const EXIT_THREAD_DEBUG_INFO& info, DWORD threadId) + { + auto it = m_threads.find(threadId); + if (it != m_threads.end()) + { + // Don't close the handle - Windows will do it + m_threads.erase(it); + } + + if (m_activeThreadId == threadId && !m_threads.empty()) + m_activeThreadId = m_threads.begin()->first; + + return false; + } + + + bool WindowsDebugEngine::HandleLoadDll(const LOAD_DLL_DEBUG_INFO& info) + { + std::string moduleName = GetModuleNameFromHandle(info.hFile, info.lpBaseOfDll); + LogVerbose("HandleLoadDll: %s at 0x%llX", moduleName.c_str(), (uint64_t)info.lpBaseOfDll); + + { + std::lock_guard lock(m_modulesMutex); + DebugModule module; + module.m_name = moduleName; + module.m_short_name = DebugModule::GetPathBaseName(moduleName); + module.m_address = (uintptr_t)info.lpBaseOfDll; + + // Get module size + IMAGE_DOS_HEADER dosHeader; + if (ReadProcessMemory(m_processHandle, info.lpBaseOfDll, &dosHeader, sizeof(dosHeader), nullptr)) + { + IMAGE_NT_HEADERS ntHeaders; + if (ReadProcessMemory(m_processHandle, + (LPVOID)((BYTE*)info.lpBaseOfDll + dosHeader.e_lfanew), + &ntHeaders, sizeof(ntHeaders), nullptr)) + { + module.m_size = ntHeaders.OptionalHeader.SizeOfImage; + } + } + module.m_loaded = true; + m_modules.push_back(module); + } + + if (info.hFile) + CloseHandle(info.hFile); + + // Try to apply pending breakpoints + ApplyPendingBreakpoints(); + + return false; + } + + + bool WindowsDebugEngine::HandleUnloadDll(const UNLOAD_DLL_DEBUG_INFO& info) + { + std::lock_guard lock(m_modulesMutex); + auto it = std::remove_if(m_modules.begin(), m_modules.end(), + [&info](const DebugModule& m) { return m.m_address == (uintptr_t)info.lpBaseOfDll; }); + m_modules.erase(it, m_modules.end()); + return false; + } + + + bool WindowsDebugEngine::HandleOutputDebugString(const OUTPUT_DEBUG_STRING_INFO& info) + { + std::vector buffer(info.nDebugStringLength); + SIZE_T bytesRead; + if (ReadProcessMemory(m_processHandle, info.lpDebugStringData, buffer.data(), + info.nDebugStringLength, &bytesRead)) + { + std::string message(buffer.data(), bytesRead); + LogVerbose("Debug output: %s", message.c_str()); + } + return false; + } + + + std::string WindowsDebugEngine::GetModuleNameFromHandle(HANDLE fileHandle, LPVOID baseAddress) + { + char filename[MAX_PATH] = {}; + + if (fileHandle) + { + if (GetFinalPathNameByHandleA(fileHandle, filename, MAX_PATH, 0) > 0) + { + // Remove the "\\?\" prefix if present + std::string result = filename; + if (result.substr(0, 4) == "\\\\?\\") + result = result.substr(4); + return result; + } + } + + // Fallback: try to get from process memory + if (GetMappedFileNameA(m_processHandle, baseAddress, filename, MAX_PATH) > 0) + { + // Convert device path to DOS path + char drives[256]; + if (GetLogicalDriveStringsA(sizeof(drives), drives)) + { + char* drive = drives; + while (*drive) + { + char driveLetter[3] = { drive[0], ':', 0 }; + char devicePath[MAX_PATH]; + if (QueryDosDeviceA(driveLetter, devicePath, MAX_PATH)) + { + size_t len = strlen(devicePath); + if (_strnicmp(filename, devicePath, len) == 0) + { + std::string result = driveLetter; + result += (filename + len); + return result; + } + } + drive += strlen(drive) + 1; + } + } + return filename; + } + + return ""; + } + + + // winternl.h provides forward declarations but not full definitions + // Define a local structure for command line info to avoid conflicts + struct CommandLineInfo { + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; + }; + + // Helper function to get command line of a process + static std::string GetProcessCommandLine(DWORD pid, const std::string& exeName) + { + // Can't get command line for system processes, fallback to executable name + if (pid == 0 || pid == 4) + return exeName; + + // Try with PROCESS_QUERY_LIMITED_INFORMATION first (less intrusive, works on more processes) + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!hProcess) + { + // Fallback to executable name if we can't open the process + return exeName; + } + + // Get NtQueryInformationProcess from ntdll + // Use the declaration from winternl.h + typedef NTSTATUS (NTAPI *NtQueryInformationProcessFn)( + HANDLE ProcessHandle, + PROCESSINFOCLASS ProcessInformationClass, + PVOID ProcessInformation, + ULONG ProcessInformationLength, + PULONG ReturnLength + ); + + static NtQueryInformationProcessFn NtQueryInformationProcess = nullptr; + if (!NtQueryInformationProcess) + { + HMODULE ntdll = GetModuleHandleA("ntdll.dll"); + if (ntdll) + NtQueryInformationProcess = (NtQueryInformationProcessFn)GetProcAddress(ntdll, "NtQueryInformationProcess"); + } + + if (!NtQueryInformationProcess) + { + CloseHandle(hProcess); + return exeName; + } + + // ProcessCommandLineInformation = 60 (available since Windows 8.1) + // Cast to PROCESSINFOCLASS from winternl.h + const PROCESSINFOCLASS ProcessCommandLineInformation = static_cast(60); + + // First call to get required buffer size + ULONG returnLength = 0; + NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessCommandLineInformation, nullptr, 0, &returnLength); + + if (returnLength == 0) + { + CloseHandle(hProcess); + return exeName; + } + + // Allocate buffer and query again + std::vector buffer(returnLength); + status = NtQueryInformationProcess(hProcess, ProcessCommandLineInformation, buffer.data(), returnLength, &returnLength); + + if (status != 0) + { + CloseHandle(hProcess); + return exeName; + } + + // The buffer contains a UNICODE_STRING-like structure (same layout as CommandLineInfo) + CommandLineInfo* cmdLine = reinterpret_cast(buffer.data()); + if (cmdLine->Length > 0 && cmdLine->Buffer) + { + // Convert wide string to UTF-8 + int size = WideCharToMultiByte(CP_UTF8, 0, cmdLine->Buffer, cmdLine->Length / sizeof(WCHAR), nullptr, 0, nullptr, nullptr); + if (size > 0) + { + std::string result(size, '\0'); + WideCharToMultiByte(CP_UTF8, 0, cmdLine->Buffer, cmdLine->Length / sizeof(WCHAR), &result[0], size, nullptr, nullptr); + CloseHandle(hProcess); + return result; + } + } + + CloseHandle(hProcess); + return exeName; + } + + + std::vector WindowsDebugEngine::GetProcessList() + { + std::vector processes; + + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) + return processes; + + PROCESSENTRY32 pe32; + pe32.dwSize = sizeof(PROCESSENTRY32); + + if (Process32First(snapshot, &pe32)) + { + do + { + DebugProcess proc; + proc.m_pid = pe32.th32ProcessID; + proc.m_processName = pe32.szExeFile; + proc.m_commandLine = GetProcessCommandLine(pe32.th32ProcessID, pe32.szExeFile); + processes.push_back(proc); + } while (Process32Next(snapshot, &pe32)); + } + + CloseHandle(snapshot); + return processes; + } + + + std::vector WindowsDebugEngine::GetThreadList() + { + std::vector threads; + + for (const auto& [tid, handle] : m_threads) + { + DebugThread thread; + thread.m_tid = tid; + + // Get thread instruction pointer + if (handle) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (Wow64GetThreadContext(handle, &ctx)) + thread.m_rip = ctx.Eip; + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (GetThreadContext(handle, &ctx)) + thread.m_rip = ctx.Rip; + } + } + threads.push_back(thread); + } + + return threads; + } + + + DebugThread WindowsDebugEngine::GetActiveThread() const + { + DebugThread thread; + thread.m_tid = m_activeThreadId; + + auto it = m_threads.find(m_activeThreadId); + if (it != m_threads.end() && it->second) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (Wow64GetThreadContext(it->second, &ctx)) + thread.m_rip = ctx.Eip; + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (GetThreadContext(it->second, &ctx)) + thread.m_rip = ctx.Rip; + } + } + + return thread; + } + + + std::uint32_t WindowsDebugEngine::GetActiveThreadId() const + { + return m_activeThreadId; + } + + + bool WindowsDebugEngine::SetActiveThread(const DebugThread& thread) + { + return SetActiveThreadId(thread.m_tid); + } + + + bool WindowsDebugEngine::SetActiveThreadId(std::uint32_t tid) + { + if (m_threads.find(tid) == m_threads.end()) + return false; + + m_activeThreadId = tid; + return true; + } + + + bool WindowsDebugEngine::SuspendThread(std::uint32_t tid) + { + auto it = m_threads.find(tid); + if (it == m_threads.end()) + return false; + + return ::SuspendThread(it->second) != (DWORD)-1; + } + + + bool WindowsDebugEngine::ResumeThread(std::uint32_t tid) + { + auto it = m_threads.find(tid); + if (it == m_threads.end()) + return false; + + return ::ResumeThread(it->second) != (DWORD)-1; + } + + + DebugBreakpoint WindowsDebugEngine::AddBreakpoint(const std::uintptr_t address, unsigned long breakpoint_flags) + { + std::lock_guard lock(m_breakpointsMutex); + + // Check if breakpoint already exists + for (auto& bp : m_breakpoints) + { + if (bp.address == address) + { + // If the breakpoint exists but isn't active yet, try to apply it now + if (!bp.isActive && m_processHandle) + { + ApplyBreakpoint(address, bp.id); + } + return DebugBreakpoint(address, bp.id, bp.isActive); + } + } + + unsigned long id = m_nextBreakpointId++; + + InternalBreakpoint bp; + bp.address = address; + bp.id = id; + bp.isActive = false; + bp.originalByte = 0; + + // Add to vector first so ApplyBreakpoint can update it + m_breakpoints.push_back(bp); + + // Try to apply the breakpoint if we're attached + if (m_processHandle) + { + if (!ApplyBreakpoint(address, id)) + { + LogWarn("Failed to apply breakpoint at 0x%llX", address); + } + else + { + LogVerbose("Successfully applied breakpoint at 0x%llX", address); + } + } + + // Return the updated state + for (const auto& b : m_breakpoints) + { + if (b.address == address) + return DebugBreakpoint(address, b.id, b.isActive); + } + return DebugBreakpoint(address, id, false); + } + + + DebugBreakpoint WindowsDebugEngine::AddBreakpoint(const ModuleNameAndOffset& address, unsigned long breakpoint_type) + { + // Try to resolve the address immediately + uint64_t resolved = ResolveModuleOffset(address); + + if (resolved != 0) + { + return AddBreakpoint(resolved, breakpoint_type); + } + + // Add to pending breakpoints + std::lock_guard lock(m_breakpointsMutex); + m_pendingBreakpoints.push_back(address); + + // Return a placeholder breakpoint + return DebugBreakpoint(0, m_nextBreakpointId++, false); + } + + + bool WindowsDebugEngine::ApplyBreakpoint(uint64_t address, unsigned long id) + { + // Find the breakpoint record first + InternalBreakpoint* targetBp = nullptr; + for (auto& bp : m_breakpoints) + { + if (bp.address == address) + { + targetBp = &bp; + break; + } + } + + if (!targetBp) + return false; + + // Read the current byte from memory - this is the actual original byte we need to save + uint8_t currentByte; + SIZE_T bytesRead; + if (!ReadProcessMemory(m_processHandle, (LPCVOID)address, ¤tByte, 1, &bytesRead) || bytesRead != 1) + { + LogWarn("ApplyBreakpoint: Failed to read memory at 0x%llX, error=%d", address, GetLastError()); + return false; + } + + // If the byte is already INT3, the breakpoint is already applied + if (currentByte == INT3_OPCODE) + { + // If we already have a saved original byte, we're good - just ensure isActive is set + if (targetBp->hasOriginalByte) + { + targetBp->isActive = true; + return true; + } + // Otherwise we have a problem - INT3 is there but we don't know the original byte + // This shouldn't happen in normal operation + LogWarn("ApplyBreakpoint: INT3 already at 0x%llX but no original byte saved", address); + return false; + } + + // Save the original byte read from memory (the actual byte, not from binary view) + targetBp->originalByte = currentByte; + targetBp->hasOriginalByte = true; + + // Write INT3 + DWORD oldProtect; + if (!VirtualProtectEx(m_processHandle, (LPVOID)address, 1, PAGE_EXECUTE_READWRITE, &oldProtect)) + { + LogWarn("ApplyBreakpoint: Failed to change protection at 0x%llX, error=%d", address, GetLastError()); + return false; + } + + SIZE_T bytesWritten; + uint8_t int3 = INT3_OPCODE; + bool success = WriteProcessMemory(m_processHandle, (LPVOID)address, &int3, 1, &bytesWritten) && bytesWritten == 1; + + if (!success) + { + LogWarn("ApplyBreakpoint: Failed to write INT3 at 0x%llX, error=%d", address, GetLastError()); + } + + VirtualProtectEx(m_processHandle, (LPVOID)address, 1, oldProtect, &oldProtect); + + if (success) + { + targetBp->isActive = true; + } + + return success; + } + + + bool WindowsDebugEngine::RemoveBreakpoint(const DebugBreakpoint& breakpoint) + { + std::lock_guard lock(m_breakpointsMutex); + + for (auto it = m_breakpoints.begin(); it != m_breakpoints.end(); ++it) + { + if (it->address == breakpoint.m_address || it->id == breakpoint.m_id) + { + if (it->isActive) + RemoveBreakpointInternal(it->address); + + m_breakpoints.erase(it); + return true; + } + } + + return false; + } + + + bool WindowsDebugEngine::RemoveBreakpoint(const ModuleNameAndOffset& breakpoint) + { + uint64_t address = ResolveModuleOffset(breakpoint); + if (address == 0) + { + // Remove from pending + std::lock_guard lock(m_breakpointsMutex); + auto it = std::find(m_pendingBreakpoints.begin(), m_pendingBreakpoints.end(), breakpoint); + if (it != m_pendingBreakpoints.end()) + { + m_pendingBreakpoints.erase(it); + return true; + } + return false; + } + + return RemoveBreakpoint(DebugBreakpoint(address)); + } + + + void WindowsDebugEngine::RemoveAllBreakpoints() + { + // Remove software breakpoints + { + std::lock_guard lock(m_breakpointsMutex); + for (auto& bp : m_breakpoints) + { + if (bp.isActive) + RemoveBreakpointInternal(bp.address); + } + m_breakpoints.clear(); + } + + // Remove hardware breakpoints from all threads + { + std::lock_guard lock(m_hwBreakpointsMutex); + for (const auto& hwbp : m_hardwareBreakpoints) + { + if (hwbp.isActive) + { + for (auto& [tid, handle] : m_threads) + { + if (!handle) + continue; + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(handle, &ctx)) + { + if (ClearHardwareBreakpointInContext(ctx, hwbp.drIndex)) + Wow64SetThreadContext(handle, &ctx); + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(handle, &ctx)) + { + if (ClearHardwareBreakpointInContext(ctx, hwbp.drIndex)) + SetThreadContext(handle, &ctx); + } + } + } + } + } + m_hardwareBreakpoints.clear(); + } + } + + + bool WindowsDebugEngine::RemoveBreakpointInternal(uint64_t address) + { + // Find the breakpoint to get the original byte + uint8_t originalByte = 0; + for (const auto& bp : m_breakpoints) + { + if (bp.address == address) + { + originalByte = bp.originalByte; + break; + } + } + + DWORD oldProtect; + if (!VirtualProtectEx(m_processHandle, (LPVOID)address, 1, PAGE_EXECUTE_READWRITE, &oldProtect)) + return false; + + SIZE_T bytesWritten; + bool success = WriteProcessMemory(m_processHandle, (LPVOID)address, &originalByte, 1, &bytesWritten) && bytesWritten == 1; + + VirtualProtectEx(m_processHandle, (LPVOID)address, 1, oldProtect, &oldProtect); + + return success; + } + + + void WindowsDebugEngine::ApplyPendingBreakpoints() + { + std::lock_guard lock(m_breakpointsMutex); + + // Re-apply existing breakpoints that are inactive (e.g., from a previous debug session) + for (auto& bp : m_breakpoints) + { + if (!bp.isActive) + { + ApplyBreakpoint(bp.address, bp.id); + } + } + + // Apply pending breakpoints (ModuleNameAndOffset style that need resolution) + auto it = m_pendingBreakpoints.begin(); + while (it != m_pendingBreakpoints.end()) + { + uint64_t address = ResolveModuleOffset(*it); + if (address != 0) + { + // Create and apply the breakpoint + InternalBreakpoint bp; + bp.address = address; + bp.id = m_nextBreakpointId++; + bp.isActive = false; + bp.originalByte = 0; + + // Add to vector first so ApplyBreakpoint can update it + m_breakpoints.push_back(bp); + + ApplyBreakpoint(address, bp.id); + + it = m_pendingBreakpoints.erase(it); + } + else + { + ++it; + } + } + + // Re-apply existing hardware breakpoints that are inactive (e.g., from a previous debug session) + { + std::lock_guard hwLock(m_hwBreakpointsMutex); + for (auto& hwbp : m_hardwareBreakpoints) + { + if (!hwbp.isActive) + { + // Find a free debug register + int drIndex = FindFreeDebugRegister(); + if (drIndex < 0) + { + LogError("No free debug registers available for hardware breakpoint re-apply"); + continue; + } + + hwbp.drIndex = drIndex; + hwbp.isActive = true; + + // Apply to all threads + for (auto& [tid, handle] : m_threads) + { + if (handle) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(handle, &ctx)) + { + if (SetHardwareBreakpointInContext(ctx, drIndex, hwbp.address, hwbp.type, hwbp.size)) + { + Wow64SetThreadContext(handle, &ctx); + } + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(handle, &ctx)) + { + if (SetHardwareBreakpointInContext(ctx, drIndex, hwbp.address, hwbp.type, hwbp.size)) + { + SetThreadContext(handle, &ctx); + } + } + } + } + } + } + } + } + + // Also try pending hardware breakpoints - collect them first, then apply outside the lock + std::vector toApply; + { + std::lock_guard hwLock(m_hwBreakpointsMutex); + auto hwIt = m_pendingHardwareBreakpoints.begin(); + while (hwIt != m_pendingHardwareBreakpoints.end()) + { + if (hwIt->isRelative) + { + uint64_t address = ResolveModuleOffset(hwIt->location); + if (address != 0) + { + PendingHardwareBreakpoint resolved(address, hwIt->type, hwIt->size); + toApply.push_back(resolved); + hwIt = m_pendingHardwareBreakpoints.erase(hwIt); + continue; + } + } + ++hwIt; + } + } + + // Apply the resolved pending hardware breakpoints outside the lock + for (const auto& pending : toApply) + { + AddHardwareBreakpoint(pending.address, pending.type, pending.size); + } + } + + + uint64_t WindowsDebugEngine::ResolveModuleOffset(const ModuleNameAndOffset& location) + { + std::lock_guard lock(m_modulesMutex); + + for (const auto& module : m_modules) + { + if (module.IsSameBaseModule(location.module)) + { + return module.m_address + location.offset; + } + } + + return 0; + } + + + bool WindowsDebugEngine::SetTempBreakpoint(uint64_t address) + { + if (m_hasTempBreakpoint) + RemoveTempBreakpoint(); + + // Read original byte + SIZE_T bytesRead; + if (!ReadProcessMemory(m_processHandle, (LPCVOID)address, &m_tempBreakpointOriginalByte, 1, &bytesRead) || bytesRead != 1) + return false; + + // Write INT3 + DWORD oldProtect; + if (!VirtualProtectEx(m_processHandle, (LPVOID)address, 1, PAGE_EXECUTE_READWRITE, &oldProtect)) + return false; + + SIZE_T bytesWritten; + uint8_t int3 = INT3_OPCODE; + bool success = WriteProcessMemory(m_processHandle, (LPVOID)address, &int3, 1, &bytesWritten) && bytesWritten == 1; + + VirtualProtectEx(m_processHandle, (LPVOID)address, 1, oldProtect, &oldProtect); + + if (success) + { + m_tempBreakpointAddress = address; + m_hasTempBreakpoint = true; + } + + return success; + } + + + bool WindowsDebugEngine::RemoveTempBreakpoint() + { + if (!m_hasTempBreakpoint) + return true; + + // Restore original byte + DWORD oldProtect; + if (!VirtualProtectEx(m_processHandle, (LPVOID)m_tempBreakpointAddress, 1, PAGE_EXECUTE_READWRITE, &oldProtect)) + return false; + + SIZE_T bytesWritten; + bool success = WriteProcessMemory(m_processHandle, (LPVOID)m_tempBreakpointAddress, + &m_tempBreakpointOriginalByte, 1, &bytesWritten) && bytesWritten == 1; + + VirtualProtectEx(m_processHandle, (LPVOID)m_tempBreakpointAddress, 1, oldProtect, &oldProtect); + + m_hasTempBreakpoint = false; + m_tempBreakpointAddress = 0; + + return success; + } + + + bool WindowsDebugEngine::IsCallInstruction(uint64_t address, size_t& instrLength) + { + uint8_t bytes[16]; + SIZE_T bytesRead; + + if (!ReadProcessMemory(m_processHandle, (LPCVOID)address, bytes, sizeof(bytes), &bytesRead) || bytesRead < 2) + return false; + + // Check for various call instruction encodings + // E8 xx xx xx xx - near relative call (5 bytes) + if (bytes[0] == 0xE8) + { + instrLength = 5; + return true; + } + + // 9A xx xx xx xx xx xx - far absolute call (7 bytes, rare in 64-bit) + if (bytes[0] == 0x9A) + { + instrLength = 7; + return true; + } + + // FF /2 - call r/m (variable length) + if (bytes[0] == 0xFF) + { + uint8_t modrm = bytes[1]; + uint8_t reg = (modrm >> 3) & 7; + if (reg == 2) // /2 = CALL + { + uint8_t mod = modrm >> 6; + uint8_t rm = modrm & 7; + + instrLength = 2; // opcode + modrm + + if (mod == 3) + { + // Register direct - just 2 bytes + return true; + } + + // Handle SIB byte + if (rm == 4 && mod != 3) + instrLength++; + + // Handle displacement + if (mod == 1) + instrLength += 1; // disp8 + else if (mod == 2 || (mod == 0 && rm == 5)) + instrLength += 4; // disp32 + + return true; + } + } + + // REX prefix + FF /2 (64-bit) + if ((bytes[0] >= 0x40 && bytes[0] <= 0x4F) && bytes[1] == 0xFF) + { + uint8_t modrm = bytes[2]; + uint8_t reg = (modrm >> 3) & 7; + if (reg == 2) // /2 = CALL + { + uint8_t mod = modrm >> 6; + uint8_t rm = modrm & 7; + + instrLength = 3; // rex + opcode + modrm + + if (mod == 3) + return true; + + // Handle SIB byte + if (rm == 4 && mod != 3) + instrLength++; + + // Handle displacement + if (mod == 1) + instrLength += 1; + else if (mod == 2 || (mod == 0 && rm == 5)) + instrLength += 4; + + return true; + } + } + + return false; + } + + + uint64_t WindowsDebugEngine::GetReturnAddress() + { + auto it = m_threads.find(m_activeThreadId); + if (it == m_threads.end() || !it->second) + return 0; + + uint64_t sp; + SIZE_T bytesRead; + uint64_t returnAddr = 0; + + if (m_isTargetWow64) + { + // 32-bit process + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (!Wow64GetThreadContext(it->second, &ctx)) + return 0; + + sp = ctx.Esp; + + // Read 32-bit return address from stack + uint32_t addr32; + if (!ReadProcessMemory(m_processHandle, (LPCVOID)sp, &addr32, 4, &bytesRead) || bytesRead != 4) + return 0; + returnAddr = addr32; + } + else + { + // 64-bit process + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (!GetThreadContext(it->second, &ctx)) + return 0; + + sp = ctx.Rsp; + + // Read 64-bit return address from stack + if (!ReadProcessMemory(m_processHandle, (LPCVOID)sp, &returnAddr, 8, &bytesRead) || bytesRead != 8) + return 0; + } + + return returnAddr; + } + + + std::vector WindowsDebugEngine::GetBreakpointList() const + { + std::vector result; + + // Note: Can't lock mutex in const method, but this is called from the session layer + // which should ensure proper synchronization. + for (const auto& bp : m_breakpoints) + { + result.emplace_back(bp.address, bp.id, bp.isActive); + } + + return result; + } + + + bool WindowsDebugEngine::AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) + { + std::lock_guard lock(m_hwBreakpointsMutex); + + // Check if we already have this breakpoint + for (auto& bp : m_hardwareBreakpoints) + { + if (bp.address == address && bp.type == type && bp.size == size) + { + // If already active, nothing to do + if (bp.isActive) + return true; + + // Re-apply the inactive breakpoint + int drIndex = FindFreeDebugRegister(); + if (drIndex < 0) + { + LogError("No free debug registers available"); + return false; + } + + bp.drIndex = drIndex; + bp.isActive = true; + + // Apply to all threads + for (auto& [tid, handle] : m_threads) + { + if (handle) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(handle, &ctx)) + { + if (SetHardwareBreakpointInContext(ctx, drIndex, address, type, size)) + { + Wow64SetThreadContext(handle, &ctx); + } + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(handle, &ctx)) + { + if (SetHardwareBreakpointInContext(ctx, drIndex, address, type, size)) + { + SetThreadContext(handle, &ctx); + } + } + } + } + } + return true; + } + } + + // Find a free debug register + int drIndex = FindFreeDebugRegister(); + if (drIndex < 0) + { + LogError("No free debug registers available"); + return false; + } + + InternalHardwareBreakpoint hwBp(address, type, size, drIndex); + hwBp.isActive = true; + + // Apply to all threads + for (auto& [tid, handle] : m_threads) + { + if (handle) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(handle, &ctx)) + { + if (SetHardwareBreakpointInContext(ctx, drIndex, address, type, size)) + { + Wow64SetThreadContext(handle, &ctx); + } + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(handle, &ctx)) + { + if (SetHardwareBreakpointInContext(ctx, drIndex, address, type, size)) + { + SetThreadContext(handle, &ctx); + } + } + } + } + } + + m_hardwareBreakpoints.push_back(hwBp); + return true; + } + + + bool WindowsDebugEngine::RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) + { + std::lock_guard lock(m_hwBreakpointsMutex); + + for (auto it = m_hardwareBreakpoints.begin(); it != m_hardwareBreakpoints.end(); ++it) + { + if (it->address == address && it->type == type && it->size == size) + { + int drIndex = it->drIndex; + + // Remove from all threads + for (auto& [tid, handle] : m_threads) + { + if (handle) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(handle, &ctx)) + { + if (ClearHardwareBreakpointInContext(ctx, drIndex)) + { + Wow64SetThreadContext(handle, &ctx); + } + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(handle, &ctx)) + { + if (ClearHardwareBreakpointInContext(ctx, drIndex)) + { + SetThreadContext(handle, &ctx); + } + } + } + } + } + + m_hardwareBreakpoints.erase(it); + return true; + } + } + + return false; + } + + + bool WindowsDebugEngine::AddHardwareBreakpoint(const ModuleNameAndOffset& location, DebugBreakpointType type, size_t size) + { + uint64_t address = ResolveModuleOffset(location); + if (address != 0) + { + return AddHardwareBreakpoint(address, type, size); + } + + // Add to pending + std::lock_guard lock(m_hwBreakpointsMutex); + m_pendingHardwareBreakpoints.emplace_back(location, type, size); + return true; + } + + + bool WindowsDebugEngine::RemoveHardwareBreakpoint(const ModuleNameAndOffset& location, DebugBreakpointType type, size_t size) + { + uint64_t address = ResolveModuleOffset(location); + if (address != 0) + { + return RemoveHardwareBreakpoint(address, type, size); + } + + // Remove from pending + std::lock_guard lock(m_hwBreakpointsMutex); + for (auto it = m_pendingHardwareBreakpoints.begin(); it != m_pendingHardwareBreakpoints.end(); ++it) + { + if (it->isRelative && it->location == location && it->type == type && it->size == size) + { + m_pendingHardwareBreakpoints.erase(it); + return true; + } + } + + return false; + } + + + int WindowsDebugEngine::FindFreeDebugRegister() + { + bool used[4] = { false, false, false, false }; + + for (const auto& bp : m_hardwareBreakpoints) + { + if (bp.drIndex >= 0 && bp.drIndex < 4) + used[bp.drIndex] = true; + } + + for (int i = 0; i < 4; ++i) + { + if (!used[i]) + return i; + } + + return -1; + } + + + bool WindowsDebugEngine::SetHardwareBreakpointInContext(CONTEXT& ctx, int drIndex, uint64_t address, DebugBreakpointType type, size_t size) + { + // Set the address in the debug register + switch (drIndex) + { + case 0: ctx.Dr0 = address; break; + case 1: ctx.Dr1 = address; break; + case 2: ctx.Dr2 = address; break; + case 3: ctx.Dr3 = address; break; + default: return false; + } + + // Calculate condition bits (RW field) + // 00 = Execute, 01 = Write, 10 = I/O (not used), 11 = Read/Write + DWORD64 condition; + switch (type) + { + case HardwareExecuteBreakpoint: condition = 0; break; + case HardwareWriteBreakpoint: condition = 1; break; + case HardwareReadBreakpoint: condition = 3; break; // Use R/W for read + case HardwareAccessBreakpoint: condition = 3; break; + default: return false; + } + + // Calculate size bits (LEN field) + // 00 = 1 byte, 01 = 2 bytes, 10 = 8 bytes (x64), 11 = 4 bytes + DWORD64 len; + switch (size) + { + case 1: len = 0; break; + case 2: len = 1; break; + case 4: len = 3; break; + case 8: len = 2; break; + default: return false; + } + + // Clear existing bits for this breakpoint + int shift = drIndex * 4 + 16; + ctx.Dr7 &= ~(0xFULL << shift); + ctx.Dr7 &= ~(3ULL << (drIndex * 2)); + + // Set the new bits + ctx.Dr7 |= (condition << shift); + ctx.Dr7 |= (len << (shift + 2)); + ctx.Dr7 |= (1ULL << (drIndex * 2)); // Enable local breakpoint + + return true; + } + + + bool WindowsDebugEngine::ClearHardwareBreakpointInContext(CONTEXT& ctx, int drIndex) + { + // Clear the address + switch (drIndex) + { + case 0: ctx.Dr0 = 0; break; + case 1: ctx.Dr1 = 0; break; + case 2: ctx.Dr2 = 0; break; + case 3: ctx.Dr3 = 0; break; + default: return false; + } + + // Clear the control bits + int shift = drIndex * 4 + 16; + ctx.Dr7 &= ~(0xFULL << shift); + ctx.Dr7 &= ~(3ULL << (drIndex * 2)); + + return true; + } + + + // WOW64 overload for SetHardwareBreakpointInContext + bool WindowsDebugEngine::SetHardwareBreakpointInContext(WOW64_CONTEXT& ctx, int drIndex, uint64_t address, DebugBreakpointType type, size_t size) + { + // Set the address in the debug register (32-bit for WOW64) + DWORD addr32 = static_cast(address); + switch (drIndex) + { + case 0: ctx.Dr0 = addr32; break; + case 1: ctx.Dr1 = addr32; break; + case 2: ctx.Dr2 = addr32; break; + case 3: ctx.Dr3 = addr32; break; + default: return false; + } + + // Calculate condition bits (RW field) + DWORD condition; + switch (type) + { + case HardwareExecuteBreakpoint: condition = 0; break; + case HardwareWriteBreakpoint: condition = 1; break; + case HardwareReadBreakpoint: condition = 3; break; + case HardwareAccessBreakpoint: condition = 3; break; + default: return false; + } + + // Calculate size bits (LEN field) + // 00 = 1 byte, 01 = 2 bytes, 11 = 4 bytes (no 8-byte for 32-bit) + DWORD len; + switch (size) + { + case 1: len = 0; break; + case 2: len = 1; break; + case 4: len = 3; break; + default: len = 0; break; // Default to 1 byte + } + + // Update DR7 + int shift = drIndex * 4 + 16; + ctx.Dr7 &= ~(0xFUL << shift); + ctx.Dr7 |= (condition << shift); + ctx.Dr7 |= (len << (shift + 2)); + ctx.Dr7 |= (1UL << (drIndex * 2)); // Enable local breakpoint + + return true; + } + + + // WOW64 overload for ClearHardwareBreakpointInContext + bool WindowsDebugEngine::ClearHardwareBreakpointInContext(WOW64_CONTEXT& ctx, int drIndex) + { + // Clear the address + switch (drIndex) + { + case 0: ctx.Dr0 = 0; break; + case 1: ctx.Dr1 = 0; break; + case 2: ctx.Dr2 = 0; break; + case 3: ctx.Dr3 = 0; break; + default: return false; + } + + // Clear the control bits + int shift = drIndex * 4 + 16; + ctx.Dr7 &= ~(0xFUL << shift); + ctx.Dr7 &= ~(3UL << (drIndex * 2)); + + return true; + } + + + bool WindowsDebugEngine::ApplyHardwareBreakpointsToThread(HANDLE threadHandle) + { + if (m_hardwareBreakpoints.empty()) + return true; + + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (!Wow64GetThreadContext(threadHandle, &ctx)) + return false; + + for (const auto& bp : m_hardwareBreakpoints) + { + if (bp.isActive) + { + SetHardwareBreakpointInContext(ctx, bp.drIndex, bp.address, bp.type, bp.size); + } + } + + return Wow64SetThreadContext(threadHandle, &ctx) != 0; + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (!GetThreadContext(threadHandle, &ctx)) + return false; + + for (const auto& bp : m_hardwareBreakpoints) + { + if (bp.isActive) + { + SetHardwareBreakpointInContext(ctx, bp.drIndex, bp.address, bp.type, bp.size); + } + } + + return SetThreadContext(threadHandle, &ctx) != 0; + } + } + + + std::unordered_map WindowsDebugEngine::ReadAllRegisters() + { + std::unordered_map registers; + + auto it = m_threads.find(m_activeThreadId); + if (it == m_threads.end() || !it->second) + return registers; + + if (m_isTargetWow64) + { + // 32-bit process on 64-bit Windows - use Wow64 API + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_ALL; + if (!Wow64GetThreadContext(it->second, &ctx)) + return registers; + + registers["eax"] = DebugRegister("eax", ctx.Eax, 4, 0); + registers["ebx"] = DebugRegister("ebx", ctx.Ebx, 4, 1); + registers["ecx"] = DebugRegister("ecx", ctx.Ecx, 4, 2); + registers["edx"] = DebugRegister("edx", ctx.Edx, 4, 3); + registers["esi"] = DebugRegister("esi", ctx.Esi, 4, 4); + registers["edi"] = DebugRegister("edi", ctx.Edi, 4, 5); + registers["ebp"] = DebugRegister("ebp", ctx.Ebp, 4, 6); + registers["esp"] = DebugRegister("esp", ctx.Esp, 4, 7); + registers["eip"] = DebugRegister("eip", ctx.Eip, 4, 8); + registers["eflags"] = DebugRegister("eflags", ctx.EFlags, 4, 9); + registers["cs"] = DebugRegister("cs", ctx.SegCs, 2, 10); + registers["ds"] = DebugRegister("ds", ctx.SegDs, 2, 11); + registers["es"] = DebugRegister("es", ctx.SegEs, 2, 12); + registers["fs"] = DebugRegister("fs", ctx.SegFs, 2, 13); + registers["gs"] = DebugRegister("gs", ctx.SegGs, 2, 14); + registers["ss"] = DebugRegister("ss", ctx.SegSs, 2, 15); + } + else + { + // 64-bit process + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_ALL; + if (!GetThreadContext(it->second, &ctx)) + return registers; + + registers["rax"] = DebugRegister("rax", ctx.Rax, 8, 0); + registers["rbx"] = DebugRegister("rbx", ctx.Rbx, 8, 1); + registers["rcx"] = DebugRegister("rcx", ctx.Rcx, 8, 2); + registers["rdx"] = DebugRegister("rdx", ctx.Rdx, 8, 3); + registers["rsi"] = DebugRegister("rsi", ctx.Rsi, 8, 4); + registers["rdi"] = DebugRegister("rdi", ctx.Rdi, 8, 5); + registers["rbp"] = DebugRegister("rbp", ctx.Rbp, 8, 6); + registers["rsp"] = DebugRegister("rsp", ctx.Rsp, 8, 7); + registers["r8"] = DebugRegister("r8", ctx.R8, 8, 8); + registers["r9"] = DebugRegister("r9", ctx.R9, 8, 9); + registers["r10"] = DebugRegister("r10", ctx.R10, 8, 10); + registers["r11"] = DebugRegister("r11", ctx.R11, 8, 11); + registers["r12"] = DebugRegister("r12", ctx.R12, 8, 12); + registers["r13"] = DebugRegister("r13", ctx.R13, 8, 13); + registers["r14"] = DebugRegister("r14", ctx.R14, 8, 14); + registers["r15"] = DebugRegister("r15", ctx.R15, 8, 15); + registers["rip"] = DebugRegister("rip", ctx.Rip, 8, 16); + registers["rflags"] = DebugRegister("rflags", ctx.EFlags, 4, 17); + registers["cs"] = DebugRegister("cs", ctx.SegCs, 2, 18); + registers["ds"] = DebugRegister("ds", ctx.SegDs, 2, 19); + registers["es"] = DebugRegister("es", ctx.SegEs, 2, 20); + registers["fs"] = DebugRegister("fs", ctx.SegFs, 2, 21); + registers["gs"] = DebugRegister("gs", ctx.SegGs, 2, 22); + registers["ss"] = DebugRegister("ss", ctx.SegSs, 2, 23); + } + + return registers; + } + + + DebugRegister WindowsDebugEngine::ReadRegister(const std::string& reg) + { + auto registers = ReadAllRegisters(); + auto it = registers.find(reg); + if (it != registers.end()) + return it->second; + + return DebugRegister(); + } + + + bool WindowsDebugEngine::WriteRegister(const std::string& reg, uint64_t value) + { + auto it = m_threads.find(m_activeThreadId); + if (it == m_threads.end() || !it->second) + return false; + + uint64_t val64 = value; + + if (m_isTargetWow64) + { + // 32-bit process on 64-bit Windows - use WOW64_CONTEXT + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_ALL; + if (!Wow64GetThreadContext(it->second, &ctx)) + return false; + + DWORD val32 = static_cast(val64); + if (reg == "eax") ctx.Eax = val32; + else if (reg == "ebx") ctx.Ebx = val32; + else if (reg == "ecx") ctx.Ecx = val32; + else if (reg == "edx") ctx.Edx = val32; + else if (reg == "esi") ctx.Esi = val32; + else if (reg == "edi") ctx.Edi = val32; + else if (reg == "ebp") ctx.Ebp = val32; + else if (reg == "esp") ctx.Esp = val32; + else if (reg == "eip") ctx.Eip = val32; + else if (reg == "eflags") ctx.EFlags = val32; + else return false; + + return Wow64SetThreadContext(it->second, &ctx) != 0; + } + else + { + // Native 64-bit process + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_ALL; + if (!GetThreadContext(it->second, &ctx)) + return false; + + if (reg == "rax") ctx.Rax = val64; + else if (reg == "rbx") ctx.Rbx = val64; + else if (reg == "rcx") ctx.Rcx = val64; + else if (reg == "rdx") ctx.Rdx = val64; + else if (reg == "rsi") ctx.Rsi = val64; + else if (reg == "rdi") ctx.Rdi = val64; + else if (reg == "rbp") ctx.Rbp = val64; + else if (reg == "rsp") ctx.Rsp = val64; + else if (reg == "r8") ctx.R8 = val64; + else if (reg == "r9") ctx.R9 = val64; + else if (reg == "r10") ctx.R10 = val64; + else if (reg == "r11") ctx.R11 = val64; + else if (reg == "r12") ctx.R12 = val64; + else if (reg == "r13") ctx.R13 = val64; + else if (reg == "r14") ctx.R14 = val64; + else if (reg == "r15") ctx.R15 = val64; + else if (reg == "rip") ctx.Rip = val64; + else if (reg == "rflags") ctx.EFlags = static_cast(val64); + else return false; + + return SetThreadContext(it->second, &ctx) != 0; + } + } + + + std::vector WindowsDebugEngine::ReadMemory(std::uintptr_t address, std::size_t size) + { + std::vector buffer(size); + SIZE_T bytesRead = 0; + + if (!ReadProcessMemory(m_processHandle, (LPCVOID)address, buffer.data(), size, &bytesRead)) + { + return {}; + } + + // Shadow breakpoint bytes - replace 0xCC with original bytes so reads/disassembly are correct + { + std::lock_guard lock(m_breakpointsMutex); + for (const auto& bp : m_breakpoints) + { + if (bp.isActive && bp.address >= address && bp.address < address + bytesRead) + { + size_t offset = bp.address - address; + buffer[offset] = bp.originalByte; + } + } + } + + // Also shadow temporary breakpoint + if (m_hasTempBreakpoint && m_tempBreakpointAddress >= address && m_tempBreakpointAddress < address + bytesRead) + { + size_t offset = m_tempBreakpointAddress - address; + buffer[offset] = m_tempBreakpointOriginalByte; + } + + buffer.resize(bytesRead); + return buffer; + } + + + bool WindowsDebugEngine::WriteMemory(std::uintptr_t address, const std::vector& buffer) + { + SIZE_T bytesWritten; + DWORD oldProtect; + + // Try to make memory writable + VirtualProtectEx(m_processHandle, (LPVOID)address, buffer.size(), PAGE_EXECUTE_READWRITE, &oldProtect); + + bool success = WriteProcessMemory(m_processHandle, (LPVOID)address, buffer.data(), + buffer.size(), &bytesWritten) && bytesWritten == buffer.size(); + + // Restore protection + VirtualProtectEx(m_processHandle, (LPVOID)address, buffer.size(), oldProtect, &oldProtect); + + return success; + } + + + std::vector WindowsDebugEngine::GetModuleList() + { + std::lock_guard lock(m_modulesMutex); + return m_modules; + } + + + std::vector WindowsDebugEngine::GetMemoryMap() + { + if (!m_processHandle) + return {}; + + std::vector result; + + // Walk the whole virtual address space with VirtualQueryEx, starting at 0 and advancing by each + // region's size. The query fails once we walk past the end of the user address space, which + // terminates the loop. Free/reserved regions are reported too (with a size that spans the gap), so + // skipping them still advances efficiently. + uintptr_t address = 0; + MEMORY_BASIC_INFORMATION info = {}; + while (VirtualQueryEx(m_processHandle, (LPCVOID)address, &info, sizeof(info)) == sizeof(info)) + { + if (info.RegionSize == 0) + break; + + // Only committed pages are actually mapped. Guard pages and no-access pages are committed but + // cannot be read, so we exclude them from the "readable" map. + const DWORD protect = info.Protect & 0xff; // strip PAGE_GUARD / PAGE_NOCACHE / PAGE_WRITECOMBINE + if (info.State == MEM_COMMIT && !(info.Protect & PAGE_GUARD) && protect != PAGE_NOACCESS) + { + DebugMemoryRegion region; + region.m_start = (uint64_t)info.BaseAddress; + region.m_size = info.RegionSize; + region.m_read = true; // any committed, non-no-access, non-guard page is readable on x86/x64 + region.m_write = (protect == PAGE_READWRITE) || (protect == PAGE_WRITECOPY) + || (protect == PAGE_EXECUTE_READWRITE) || (protect == PAGE_EXECUTE_WRITECOPY); + region.m_execute = (protect == PAGE_EXECUTE) || (protect == PAGE_EXECUTE_READ) + || (protect == PAGE_EXECUTE_READWRITE) || (protect == PAGE_EXECUTE_WRITECOPY); + // MEM_MAPPED sections (file/pagefile-backed) can be shared between processes; MEM_IMAGE is + // copy-on-write and MEM_PRIVATE is private. + region.m_shared = (info.Type == MEM_MAPPED); + + // Image- and file-backed regions have a backing file we can name. Leave the name empty + // (rather than the helper's "" sentinel) for mappings with no resolvable file. + if (info.Type == MEM_IMAGE || info.Type == MEM_MAPPED) + { + std::string name = GetModuleNameFromHandle(nullptr, info.BaseAddress); + if (name != "") + region.m_name = name; + } + + result.push_back(region); + } + + // Advance past this region; stop if the address would wrap around at the top of the space. + uintptr_t next = (uintptr_t)info.BaseAddress + info.RegionSize; + if (next <= address) + break; + address = next; + } + + return result; + } + + + std::string WindowsDebugEngine::GetTargetArchitecture() + { + // Use cached WOW64 detection result + if (m_isTargetWow64) + return "x86"; + return "x86_64"; + } + + + DebugStopReason WindowsDebugEngine::StopReason() + { + return m_stopReason; + } + + + uint64_t WindowsDebugEngine::ExitCode() + { + return m_exitCode; + } + + + bool WindowsDebugEngine::BreakInto() + { + if (!m_processHandle) + return false; + + return DebugBreakProcess(m_processHandle) != 0; + } + + + bool WindowsDebugEngine::Go() + { + if (!m_activelyDebugging) + return false; + + // If we're at a hardware breakpoint, we need to step over it first + if (m_stepOverHwBreakpointIndex >= 0) + { + auto it = m_threads.find(m_activeThreadId); + if (it != m_threads.end() && it->second) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL | WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(it->second, &ctx)) + { + ctx.Dr7 &= ~(1UL << (m_stepOverHwBreakpointIndex * 2)); + ctx.EFlags |= 0x100; + Wow64SetThreadContext(it->second, &ctx); + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL | CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(it->second, &ctx)) + { + ctx.Dr7 &= ~(1ULL << (m_stepOverHwBreakpointIndex * 2)); + ctx.EFlags |= 0x100; + SetThreadContext(it->second, &ctx); + } + } + } + m_hasStepOverHwBreakpoint = true; + m_stepOverHwBreakpointContinue = true; + } + + // If we're at a software breakpoint, we need to step over it first + { + std::lock_guard lock(m_breakpointsMutex); + uint64_t ip = GetInstructionOffset(); + for (const auto& bp : m_breakpoints) + { + if (bp.address == ip && bp.isActive) + { + // Remove the INT3 so we can execute the actual instruction + RemoveBreakpointInternal(ip); + + // Need to single step past the breakpoint first + m_stepOverBreakpointAddress = ip; + m_hasStepOverBreakpoint = true; + m_stepOverBreakpointContinue = true; // Continue after re-applying breakpoint + + // CRITICAL: Suspend all other threads while stepping over the breakpoint + // This prevents race conditions where another thread could execute the + // breakpoint location while we have the INT3 removed + for (const auto& [tid, handle] : m_threads) + { + if (tid != m_activeThreadId && handle) + { + ::SuspendThread(handle); + } + } + + // Set single step flag + auto it = m_threads.find(m_activeThreadId); + if (it != m_threads.end() && it->second) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (Wow64GetThreadContext(it->second, &ctx)) + { + ctx.EFlags |= 0x100; + Wow64SetThreadContext(it->second, &ctx); + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (GetThreadContext(it->second, &ctx)) + { + ctx.EFlags |= 0x100; + SetThreadContext(it->second, &ctx); + } + } + } + break; + } + } + } + + // Note: We don't suspend other threads when we have a temp breakpoint (StepOver/StepReturn). + // StepOver internally does a "continue" operation with a breakpoint at the return address. + // During this continue, all threads should run normally. If another thread hits a breakpoint, + // that's expected behavior (the debugger stops). Only StepInto() uses scheduler-locking. + + // Publish the resume under m_debugMutex so the parked DebugLoop predicate observes it and + // the notify can't be lost (see Quit for the race detail). + { + std::lock_guard lock(m_debugMutex); + m_targetRunning = true; + } + m_debugCondition.notify_one(); + + // Notify that the target has resumed + EngineEvent event; + event.type = EngineEventType::Resumed; + PostEngineEvent(event); + + return true; + } + + + bool WindowsDebugEngine::StepInto() + { + if (!m_activelyDebugging) + return false; + + auto it = m_threads.find(m_activeThreadId); + if (it == m_threads.end() || !it->second) + return false; + + // If we're at a hardware breakpoint, we need to temporarily disable it + if (m_stepOverHwBreakpointIndex >= 0) + { + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_DEBUG_REGISTERS; + if (Wow64GetThreadContext(it->second, &ctx)) + { + ctx.Dr7 &= ~(1UL << (m_stepOverHwBreakpointIndex * 2)); + Wow64SetThreadContext(it->second, &ctx); + } + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (GetThreadContext(it->second, &ctx)) + { + ctx.Dr7 &= ~(1ULL << (m_stepOverHwBreakpointIndex * 2)); + SetThreadContext(it->second, &ctx); + } + } + m_hasStepOverHwBreakpoint = true; + m_stepOverHwBreakpointContinue = false; // Stop after re-applying + } + + // Check if we're at a software breakpoint and need to re-apply it after stepping + { + std::lock_guard lock(m_breakpointsMutex); + uint64_t ip = GetInstructionOffset(); + for (const auto& bp : m_breakpoints) + { + if (bp.address == ip && bp.isActive) + { + // Remove the INT3 so we can execute the actual instruction + RemoveBreakpointInternal(ip); + + // Need to re-apply breakpoint after stepping + m_stepOverBreakpointAddress = ip; + m_hasStepOverBreakpoint = true; + m_stepOverBreakpointContinue = false; // Stop after re-applying breakpoint + break; + } + } + } + + // Set the trap flag for single stepping + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (!Wow64GetThreadContext(it->second, &ctx)) + return false; + + ctx.EFlags |= 0x100; + if (!Wow64SetThreadContext(it->second, &ctx)) + return false; + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (!GetThreadContext(it->second, &ctx)) + return false; + + ctx.EFlags |= 0x100; + if (!SetThreadContext(it->second, &ctx)) + return false; + } + + m_singleStepping = true; + + // Suspend all other threads when stepping to prevent them from hitting breakpoints + // This implements GDB-style "scheduler-locking step" behavior + for (const auto& [tid, handle] : m_threads) + { + if (tid != m_activeThreadId && handle) + { + ::SuspendThread(handle); + } + } + + // Publish the resume under m_debugMutex so the parked DebugLoop predicate observes it and + // the notify can't be lost (see Quit for the race detail). + { + std::lock_guard lock(m_debugMutex); + m_targetRunning = true; + } + m_debugCondition.notify_one(); + + // Notify that the target has resumed + EngineEvent event; + event.type = EngineEventType::StepIntoComplete; + PostEngineEvent(event); + + return true; + } + + + bool WindowsDebugEngine::StepOver() + { + if (!m_activelyDebugging) + return false; + + uint64_t ip = GetInstructionOffset(); + size_t instrLength = 0; + + // Check if current instruction is a call + if (IsCallInstruction(ip, instrLength)) + { + // Set temporary breakpoint after the call instruction + uint64_t nextAddr = ip + instrLength; + if (!SetTempBreakpoint(nextAddr)) + return false; + + // Resume execution - will stop at the temp breakpoint + return Go(); + } + + // Not a call, just do a single step + return StepInto(); + } + + + bool WindowsDebugEngine::StepReturn() + { + if (!m_activelyDebugging) + return false; + + // Use stack unwinding to get the return address reliably + // Frame 0 is the current frame, frame 1 is the caller + auto frames = GetFramesOfThread(m_activeThreadId); + if (frames.size() < 2) + { + // Fallback to simple stack read if unwinding fails + uint64_t returnAddr = GetReturnAddress(); + if (returnAddr == 0) + return false; + + if (!SetTempBreakpoint(returnAddr)) + return false; + + return Go(); + } + + // The return address is the PC of the caller's frame + uint64_t returnAddr = frames[1].m_pc; + if (returnAddr == 0) + return false; + + // Set temporary breakpoint at return address + if (!SetTempBreakpoint(returnAddr)) + return false; + + // Resume execution - will stop when function returns + return Go(); + } + + + uint64_t WindowsDebugEngine::GetInstructionOffset() + { + auto it = m_threads.find(m_activeThreadId); + if (it == m_threads.end() || !it->second) + return 0; + + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (!Wow64GetThreadContext(it->second, &ctx)) + return 0; + return ctx.Eip; + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (!GetThreadContext(it->second, &ctx)) + return 0; + return ctx.Rip; + } + } + + + uint64_t WindowsDebugEngine::GetStackPointer() + { + auto it = m_threads.find(m_activeThreadId); + if (it == m_threads.end() || !it->second) + return 0; + + if (m_isTargetWow64) + { + WOW64_CONTEXT ctx {}; + ctx.ContextFlags = WOW64_CONTEXT_CONTROL; + if (!Wow64GetThreadContext(it->second, &ctx)) + return 0; + return ctx.Esp; + } + else + { + CONTEXT ctx {}; + ctx.ContextFlags = CONTEXT_CONTROL; + if (!GetThreadContext(it->second, &ctx)) + return 0; + return ctx.Rsp; + } + } + + + std::uint32_t WindowsDebugEngine::GetActivePID() + { + return m_processId; + } + + + bool WindowsDebugEngine::SupportFeature(DebugAdapterCapacity feature) + { + switch (feature) + { + case DebugAdapterSupportStepOver: + return true; + case DebugAdapterSupportStepReturn: + return true; + case DebugAdapterSupportModules: + return true; + case DebugAdapterSupportThreads: + return true; + case DebugAdapterSupportStepOverReverse: + case DebugAdapterSupportTTD: + return false; + default: + return false; + } + } + + + std::vector WindowsDebugEngine::GetFramesOfThread(uint32_t tid) + { + std::vector frames; + + auto it = m_threads.find(tid); + if (it == m_threads.end() || !it->second) + return frames; + + HANDLE threadHandle = it->second; + + // Initialize symbol handler (needed for StackWalk64) + SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); + SymInitialize(m_processHandle, nullptr, TRUE); + + STACKFRAME64 stackFrame {}; + DWORD machineType; + + // Storage for both context types - StackWalk64 takes PVOID + CONTEXT ctx64 {}; + WOW64_CONTEXT ctx32 {}; + PVOID contextPtr; + + if (m_isTargetWow64) + { + // 32-bit process on 64-bit Windows + ctx32.ContextFlags = WOW64_CONTEXT_FULL; + if (!Wow64GetThreadContext(threadHandle, &ctx32)) + return frames; + + machineType = IMAGE_FILE_MACHINE_I386; + stackFrame.AddrPC.Offset = ctx32.Eip; + stackFrame.AddrPC.Mode = AddrModeFlat; + stackFrame.AddrFrame.Offset = ctx32.Ebp; + stackFrame.AddrFrame.Mode = AddrModeFlat; + stackFrame.AddrStack.Offset = ctx32.Esp; + stackFrame.AddrStack.Mode = AddrModeFlat; + contextPtr = &ctx32; + } + else + { + // Native 64-bit process + ctx64.ContextFlags = CONTEXT_FULL; + if (!GetThreadContext(threadHandle, &ctx64)) + return frames; + + machineType = IMAGE_FILE_MACHINE_AMD64; + stackFrame.AddrPC.Offset = ctx64.Rip; + stackFrame.AddrPC.Mode = AddrModeFlat; + stackFrame.AddrFrame.Offset = ctx64.Rbp; + stackFrame.AddrFrame.Mode = AddrModeFlat; + stackFrame.AddrStack.Offset = ctx64.Rsp; + stackFrame.AddrStack.Mode = AddrModeFlat; + contextPtr = &ctx64; + } + + int frameIndex = 0; + const int maxFrames = 256; + + while (frameIndex < maxFrames) + { + if (!StackWalk64( + machineType, + m_processHandle, + threadHandle, + &stackFrame, + contextPtr, + nullptr, + SymFunctionTableAccess64, + SymGetModuleBase64, + nullptr)) + { + break; + } + + // Check for invalid frame + if (stackFrame.AddrPC.Offset == 0) + break; + + DebugFrame frame; + frame.m_index = frameIndex; + frame.m_pc = stackFrame.AddrPC.Offset; + frame.m_sp = stackFrame.AddrStack.Offset; + frame.m_fp = stackFrame.AddrFrame.Offset; + + // Find which module this address belongs to + { + std::lock_guard lock(m_modulesMutex); + for (const auto& mod : m_modules) + { + if (frame.m_pc >= mod.m_address && frame.m_pc < mod.m_address + mod.m_size) + { + frame.m_module = mod.m_short_name; + break; + } + } + } + + frames.push_back(frame); + + frameIndex++; + } + + SymCleanup(m_processHandle); + + return frames; + } + +} // namespace x2win diff --git a/x2winstub/debug/windows_debug_engine.h b/x2winstub/debug/windows_debug_engine.h new file mode 100644 index 00000000..cae41c91 --- /dev/null +++ b/x2winstub/debug/windows_debug_engine.h @@ -0,0 +1,269 @@ +/* +Ported from core/adapters/windowsnativeadapter.cpp/.h (BinaryNinjaDebugger::WindowsNativeAdapter). +This is the same Windows debug engine (Win32 debug-loop, software/hardware breakpoints, stepping, +registers, memory map, WOW64 handling) with the Binary Ninja dependencies removed: no BinaryView, +no Settings, no BN logging, no DebugAdapter base class. +*/ +#pragma once +#include "debug_types.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace x2win { + + // Minimal printf-style logging, replacing BN's global LogWarn()/LogError() free functions. + // Declared here (not just in the .cpp) so WindowsDebugEngine::LogVerbose, a member template + // defined inline below, sees them at its point of definition. + void LogWarn(const char* fmt, ...); + void LogError(const char* fmt, ...); + + // Internal breakpoint tracking structure + struct InternalBreakpoint + { + uint64_t address; + uint8_t originalByte; + bool hasOriginalByte; // true once originalByte holds a real saved value (0x00 is a valid byte, so we can't use originalByte itself as the sentinel) + bool isActive; + unsigned long id; + + InternalBreakpoint() : address(0), originalByte(0), hasOriginalByte(false), isActive(false), id(0) {} + InternalBreakpoint(uint64_t addr, uint8_t orig, bool active, unsigned long bpId) + : address(addr), originalByte(orig), hasOriginalByte(true), isActive(active), id(bpId) {} + }; + + // Internal hardware breakpoint tracking + struct InternalHardwareBreakpoint + { + uint64_t address; + DebugBreakpointType type; + size_t size; + int drIndex; // Which debug register (0-3) + bool isActive; + + InternalHardwareBreakpoint() : address(0), type(HardwareExecuteBreakpoint), size(1), drIndex(-1), isActive(false) {} + InternalHardwareBreakpoint(uint64_t addr, DebugBreakpointType t, size_t s, int idx) + : address(addr), type(t), size(s), drIndex(idx), isActive(false) {} + }; + + class WindowsDebugEngine + { + private: + // Process and thread handles + HANDLE m_processHandle = nullptr; + HANDLE m_threadHandle = nullptr; + DWORD m_processId = 0; + DWORD m_threadId = 0; + + // Debug event handling + DEBUG_EVENT m_lastDebugEvent {}; + bool m_hasLastDebugEvent = false; + + // State tracking + std::atomic m_activelyDebugging {false}; + std::atomic m_targetRunning {false}; + std::atomic m_shouldStop {false}; + DebugStopReason m_stopReason = UnknownReason; + unsigned long m_exitCode = 0; + + // Thread management + std::thread m_debugThread; + std::mutex m_debugMutex; + std::condition_variable m_debugCondition; + + // Thread tracking + std::map m_threads; + DWORD m_activeThreadId = 0; + + // Module tracking + std::vector m_modules; + std::mutex m_modulesMutex; + + // Breakpoint tracking + std::vector m_breakpoints; + std::vector m_pendingBreakpoints; + unsigned long m_nextBreakpointId = 1; + std::mutex m_breakpointsMutex; + + // Hardware breakpoints + std::vector m_hardwareBreakpoints; + std::vector m_pendingHardwareBreakpoints; + std::mutex m_hwBreakpointsMutex; + + // Single step tracking + bool m_singleStepping = false; + uint64_t m_stepOverBreakpointAddress = 0; + bool m_hasStepOverBreakpoint = false; + bool m_stepOverBreakpointContinue = false; // If true, continue after re-applying breakpoint + + // Hardware breakpoint step-over tracking + int m_stepOverHwBreakpointIndex = -1; // DR index of hardware breakpoint being stepped over + bool m_hasStepOverHwBreakpoint = false; + bool m_stepOverHwBreakpointContinue = false; + + // Temporary breakpoint for step over/return (removed after hit) + uint64_t m_tempBreakpointAddress = 0; + uint8_t m_tempBreakpointOriginalByte = 0; + bool m_hasTempBreakpoint = false; + + // Architecture info (WOW64 is runtime-detected once attached; see StartDebugging()) + bool m_isTargetWow64 = false; // True if debugging a 32-bit process on 64-bit Windows + + // Settings (plain local flags, replacing BN's Settings::Instance() lookups -- defaults + // match the BN debugger.* settings' registered defaults, see core/debugger.cpp) + bool m_verboseLogging = false; // was "common.verboseLogging" (default false) + bool m_stopAtSystemEntryPoint = true; // was "debugger.stopAtSystemEntryPoint" (default false) + // In here we set default as true, because we removed binaryview + // so that there is no more break point at program entry. + + // Initial breakpoint tracking + bool m_initialBreakpointSeen = false; + bool m_wow64InitialBreakpointSeen = false; // WOW64 processes have a second system breakpoint + + // Launch/attach parameters (for passing to debug thread) + std::string m_launchExecutable; + std::string m_launchWorkingDir; + std::string m_launchCommandLine; + DWORD m_attachPID = 0; + bool m_isAttaching = false; + std::atomic m_launchResult {false}; + std::string m_launchError; + std::condition_variable m_launchCondition; + std::mutex m_launchMutex; + + // Event delivery -- replaces DebugAdapter::PostDebuggerEvent()/m_eventCallback. + std::function m_eventCallback; + void PostEngineEvent(const EngineEvent& event); + + // Internal methods + void DebugLoop(); + bool StartDebugging(); // Called from debug thread to create/attach process + void Reset(); // Reset state for a new debug session + bool HandleDebugEvent(const DEBUG_EVENT& event); + bool HandleException(const EXCEPTION_DEBUG_INFO& info); + bool HandleCreateProcess(const CREATE_PROCESS_DEBUG_INFO& info); + bool HandleExitProcess(const EXIT_PROCESS_DEBUG_INFO& info); + bool HandleCreateThread(const CREATE_THREAD_DEBUG_INFO& info, DWORD threadId); + bool HandleExitThread(const EXIT_THREAD_DEBUG_INFO& info, DWORD threadId); + bool HandleLoadDll(const LOAD_DLL_DEBUG_INFO& info); + bool HandleUnloadDll(const UNLOAD_DLL_DEBUG_INFO& info); + bool HandleOutputDebugString(const OUTPUT_DEBUG_STRING_INFO& info); + + std::string GetModuleNameFromHandle(HANDLE fileHandle, LPVOID baseAddress); + bool ApplyBreakpoint(uint64_t address, unsigned long id); + bool RemoveBreakpointInternal(uint64_t address); + void ApplyPendingBreakpoints(); + void RemoveAllBreakpoints(); + bool ApplyHardwareBreakpointsToThread(HANDLE threadHandle); + int FindFreeDebugRegister(); + bool SetHardwareBreakpointInContext(CONTEXT& ctx, int drIndex, uint64_t address, DebugBreakpointType type, size_t size); + bool SetHardwareBreakpointInContext(WOW64_CONTEXT& ctx, int drIndex, uint64_t address, DebugBreakpointType type, size_t size); + bool ClearHardwareBreakpointInContext(CONTEXT& ctx, int drIndex); + bool ClearHardwareBreakpointInContext(WOW64_CONTEXT& ctx, int drIndex); + + uint64_t ResolveModuleOffset(const ModuleNameAndOffset& location); + + // Verbose logging helper + template + void LogVerbose(const char* fmt, Args&&... args) + { + if (m_verboseLogging) + LogWarn(fmt, std::forward(args)...); + } + + // Temporary breakpoint helpers for step over/return + bool SetTempBreakpoint(uint64_t address); + bool RemoveTempBreakpoint(); + + // Instruction helpers + bool IsCallInstruction(uint64_t address, size_t& instrLength); + uint64_t GetReturnAddress(); + + public: + WindowsDebugEngine(); + ~WindowsDebugEngine(); + + void SetEventCallback(std::function callback) { m_eventCallback = std::move(callback); } + + [[nodiscard]] bool Execute(const std::string& path, const LaunchConfigurations& configs = {}); + [[nodiscard]] bool ExecuteWithArgs(const std::string& path, const std::string& args, + const std::string& workingDir, const LaunchConfigurations& configs = {}); + [[nodiscard]] bool Attach(std::uint32_t pid); + + bool Detach(); + bool Quit(); + + std::vector GetProcessList(); + + std::vector GetThreadList(); + DebugThread GetActiveThread() const; + std::uint32_t GetActiveThreadId() const; + bool SetActiveThread(const DebugThread& thread); + bool SetActiveThreadId(std::uint32_t tid); + + DebugBreakpoint AddBreakpoint(const std::uintptr_t address, unsigned long breakpoint_flags = 0); + DebugBreakpoint AddBreakpoint(const ModuleNameAndOffset& address, unsigned long breakpoint_type = 0); + + bool RemoveBreakpoint(const DebugBreakpoint& breakpoint); + bool RemoveBreakpoint(const ModuleNameAndOffset& breakpoint); + + std::vector GetBreakpointList() const; + + // Hardware breakpoint support + bool AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1); + bool RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1); + bool AddHardwareBreakpoint(const ModuleNameAndOffset& location, DebugBreakpointType type, size_t size = 1); + bool RemoveHardwareBreakpoint(const ModuleNameAndOffset& location, DebugBreakpointType type, size_t size = 1); + + std::unordered_map ReadAllRegisters(); + DebugRegister ReadRegister(const std::string& reg); + bool WriteRegister(const std::string& reg, uint64_t value); + + std::vector ReadMemory(std::uintptr_t address, std::size_t size); + bool WriteMemory(std::uintptr_t address, const std::vector& buffer); + + std::vector GetModuleList(); + + std::vector GetMemoryMap(); + + std::string GetTargetArchitecture(); + + DebugStopReason StopReason(); + uint64_t ExitCode(); + + bool BreakInto(); + bool Go(); + bool StepInto(); + bool StepOver(); + bool StepReturn(); + + uint64_t GetInstructionOffset(); + uint64_t GetStackPointer(); + std::uint32_t GetActivePID(); + + // True once Attach()/Execute() has actually started a debug session, false again after + // Detach()/Quit() (or the debuggee exits on its own) -- unlike GetActivePID(), which keeps + // returning the last-known pid even after the session has ended, this is the right signal for + // "is there still something to supervise right now". + bool IsActivelyDebugging() const { return m_activelyDebugging; } + + bool SupportFeature(DebugAdapterCapacity feature); + + std::vector GetFramesOfThread(uint32_t tid); + + bool SuspendThread(std::uint32_t tid); + bool ResumeThread(std::uint32_t tid); + }; + +} // namespace x2win diff --git a/x2winstub/main.cpp b/x2winstub/main.cpp new file mode 100644 index 00000000..a47f9906 --- /dev/null +++ b/x2winstub/main.cpp @@ -0,0 +1,268 @@ +#include "net/winsock_library.h" +#include "net/socket_handle.h" +#include "net/connection.h" +#include "x2win_session.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +static constexpr uint16_t kListenPort = 31338; + +namespace { + void PrintUsage(const char* argv0){ + fprintf(stderr, + "usage: \n" + " %s target [--ip
] [--port ]\n" + " %s server [--ip
] [--port ]\n", + argv0, argv0); + } + + struct Options{ + enum class Mode{Server, Target}; + + Mode mode = Mode::Server; + std::string targetPath; + std::string listenIp = "0.0.0.0"; + uint16_t listenPort = kListenPort; + }; + + std::optional ParsePort(const char* text){ + try{ + int port = std::stoi(text); + if(port < 0 || port > 65535) return std::nullopt; + return static_cast(port); + }catch(const std::exception){ + return std::nullopt; + } + } + + std::optional ParseArgs(int argc, char** argv){ + if(argc < 2){ + PrintUsage(argv[0]); + return std::nullopt; + } + + Options options; + + std::string_view command = argv[1]; + int nextArg = 2; + if(command == "server"){ + options.mode = Options::Mode::Server; + }else if(command == "target"){ + options.mode = Options::Mode::Target; + if(argc < 3){ + fprintf(stderr, "target mode requires a path to the target executable\n"); + PrintUsage(argv[0]); + return std::nullopt; + } + options.targetPath = argv[2]; + nextArg = 3; + }else{ + fprintf(stderr, "unknown command: %s\n", argv[1]); + PrintUsage(argv[0]); + return std::nullopt; + } + + for(int i = nextArg; i < argc; ++i){ + std::string_view arg = argv[i]; + if(arg == "--ip" && i + 1 < argc){ + // --ip's value is an address string, just store it as-is -- no need to validate the + // format here, CreateListenSocket()'s inet_pton() already reports "invalid --ip address" + // and bails out if it can't parse it, so re-validating here would be redundant. + options.listenIp = argv[++i]; + }else if(arg == "--port" && i + 1 < argc){ + // --port's value is numeric, hand it to ParsePort for range checking (0-65535) and conversion. + auto port = ParsePort(argv[++i]); + if(!port){ + fprintf(stderr, "invalid port: %s\n", argv[i]); + PrintUsage(argv[0]); + return std::nullopt; + } + options.listenPort = *port; + }else{ + // Neither known flag matched (unknown flag name, or --ip/--port missing its value so + // i + 1 < argc was false) -- treat it as an unrecognized argument and bail out. + fprintf(stderr, "unrecognized argument: %s\n", argv[i]); + PrintUsage(argv[0]); + return std::nullopt; + } + } + + return options; + } + + // Shared per-connection request loop, used by both server mode (a fresh session per connection) + // and target mode (a session that was already launched and stopped at its initial breakpoint + // before the connection existed -- see main()). This is the dispatch loop that used to be + // inline in HandleClient(), now delegating each request to X2WinStubSession::HandleRequest -- + // the class that owns the WindowsDebugEngine and does the proto command parsing. + void RunRequestLoop(Connection* conn, x2win::X2WinStubSession& session){ + X2WinEnvelopeBuffer requestBuf; + while(conn->ReadEnvelope(requestBuf)){ + const x2win::Envelope* request = requestBuf.Get(); + if(!request) continue; // ReadEnvelope() already verified the buffer; shouldn't happen + + flatbuffers::FlatBufferBuilder builder; + if(session.HandleRequest(*request, builder)){ + if(!conn->WriteEnvelope(builder)){ + fprintf(stderr, "WriteEnvelope failed: %d\n", WSAGetLastError()); + break; + } + } + } + + // Server mode: the debuggee this connection Launched/Attached is this client's own + // creation -- nobody else knows about it once this client is gone, so clean it up rather + // than leak an orphaned debugged process (matching the old debug_loop.cpp's + // HandleDisconnect()). Target mode: the debuggee belongs to the process itself (launched + // at startup, independent of any one client) -- a disconnect just means nobody's watching + // right now, not that the session is over. main()'s target-mode loop decides whether to + // wait for a reconnect or give up, based on whether the debuggee is still alive. + if(session.Mode() == x2win::SessionMode::Server && session.Engine().GetActivePID() != 0){ + session.Engine().Quit(); + } + } + + void HandleClient(std::shared_ptr conn, Options::Mode mode){ + x2win::X2WinStubSession session(conn.get(), + mode == Options::Mode::Server ? x2win::SessionMode::Server : x2win::SessionMode::Target); + RunRequestLoop(conn.get(), session); + } +} + +std::optional CreateListenSocket(const Options& options){ + SocketHandle listener(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); + if(listener.get() == INVALID_SOCKET){ + fprintf(stderr, "socket() failed: %d\n", WSAGetLastError()); + return std::nullopt; + } + + int reuse = 1; + setsockopt(listener.get(), SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&reuse), sizeof(reuse)); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(options.listenPort); + if(inet_pton(AF_INET, options.listenIp.c_str(), &addr.sin_addr) != 1){ + fprintf(stderr, "invalid --ip address: %s\n", options.listenIp.c_str()); + return std::nullopt; + } + + if(bind(listener.get(), reinterpret_cast(&addr), sizeof(addr)) == SOCKET_ERROR){ + fprintf(stderr, "bind() failed: %d\n", WSAGetLastError()); + return std::nullopt; + } + + if(listen(listener.get(), 1) == SOCKET_ERROR){ + fprintf(stderr, "listen() failed: %d\n", WSAGetLastError()); + return std::nullopt; + } + + fprintf(stderr, "x2winstub listening on %s:%d\n", options.listenIp.c_str(), options.listenPort); + return listener; + +} + +int main(int argc, char** argv){ + std::optional options = ParseArgs(argc, argv); + if(!options) return 1; + + // Target mode: launch the debuggee immediately (before any client is connected) and wait for + // its initial breakpoint, then open the listen socket and, once the adapter connects, tell it + // about the stop that already happened -- same shape as the old debug_loop.cpp's + // RunDebugLoop()/WaitForInitialStop() split, just backed by WindowsDebugEngine/X2WinStubSession. + if(options->mode == Options::Mode::Target){ + // No connection yet -- X2WinStubSession::WaitForFirstStop() fires independent of one. + x2win::X2WinStubSession session(nullptr, x2win::SessionMode::Target); + + fprintf(stderr, "target mode: launching %s, waiting for initial breakpoint...\n", options->targetPath.c_str()); + if(!session.Engine().Execute(options->targetPath)){ + fprintf(stderr, "failed to launch target\n"); + return 1; + } + session.WaitForFirstStop(); + fprintf(stderr, "target stopped at initial breakpoint, waiting for adapter...\n"); + + int result = 0; + try{ + WinsockLibrary winsock; + auto listener = CreateListenSocket(*options); + if(!listener){ + result = 1; + }else{ + for(;;){ + SocketHandle clientSocket(accept(listener->get(), nullptr, nullptr)); + if(clientSocket.get() == INVALID_SOCKET){ + fprintf(stderr, "accept() failed: %d\n", WSAGetLastError()); + continue; + } + + fprintf(stderr, "client connected\n"); + auto conn = std::make_shared(std::move(clientSocket)); + session.SetConnection(conn.get()); + + // Tell the (re)connecting client what's currently going on. Covers the very + // first connection too (WaitForFirstStop() above guarantees OnEngineEvent() + // already ran and set m_isStopped/m_lastStopReason before we ever get here), + // so the old hardcoded "always send INITIAL_BREAKPOINT" push before the loop + // is gone -- this does the same thing generically, with whatever the actual + // current stop reason is. + if(session.IsStopped()){ + flatbuffers::FlatBufferBuilder stoppedBuilder; + auto stoppedEventBody = x2win::CreateTargetStoppedEvent(stoppedBuilder, + session.LastStopReason(), session.Engine().GetInstructionOffset(), /*exit_code=*/0); + auto stoppedEnvelope = x2win::CreateEnvelope(stoppedBuilder, /*request_id=*/0, + x2win::Body_TargetStoppedEvent, stoppedEventBody.Union()); + stoppedBuilder.Finish(stoppedEnvelope); + conn->WriteEnvelope(stoppedBuilder); + } + + RunRequestLoop(conn.get(), session); + fprintf(stderr, "client disconnected\n"); + + if(!session.Engine().IsActivelyDebugging()){ + fprintf(stderr, "no active debug session, exiting\n"); + break; + } + fprintf(stderr, "debuggee still running, waiting for a new connection...\n"); + } + } + }catch(const std::exception& e){ + fprintf(stderr, "%s\n", e.what()); + result = 1; + } + + // session (and its WindowsDebugEngine) goes out of scope here; ~WindowsDebugEngine() Quit()s + // and joins the debug thread if the target is somehow still alive and wasn't already handled + // by RunRequestLoop's disconnect cleanup above. + return result; + } + + try{ + WinsockLibrary winsock; + auto listener = CreateListenSocket(*options); + if(!listener) return 1; + + for(;;){ + SocketHandle clientSocket(accept(listener->get(), nullptr, nullptr)); + if(clientSocket.get() == INVALID_SOCKET){ + fprintf(stderr, "accept() failed: %d\n", WSAGetLastError()); + continue; + } + + fprintf(stderr, "client connected\n"); + auto conn = std::make_shared(std::move(clientSocket)); + HandleClient(conn, options->mode); + fprintf(stderr, "client disconnected\n"); + } + }catch(const std::exception& e){ + fprintf(stderr, "%s\n", e.what()); + return 1; + } +} diff --git a/x2winstub/net/connection.cpp b/x2winstub/net/connection.cpp new file mode 100644 index 00000000..92cc310d --- /dev/null +++ b/x2winstub/net/connection.cpp @@ -0,0 +1,52 @@ +#include "connection.h" + +#include +#include + +bool Connection::RecvAll(char* buf, int len){ + int received = 0; + while (received < len) { + int n = recv(m_socket.get(), buf + received, len - received, 0); + if(n <= 0) return false; + received += n; + } + + return true; +} + +bool Connection::SendAll(const char *buf, int len){ + int sent = 0; + while(sent < len){ + int n = send(m_socket.get(), buf+sent, len-sent, 0); + if(n <= 0) return false; + sent += n; + } + return true; +} + +bool Connection::ReadEnvelope(X2WinEnvelopeBuffer &out){ + uint32_t bodyLen = 0; + if(!RecvAll(reinterpret_cast(&bodyLen), sizeof(bodyLen))) return false; + + out.bytes.resize(bodyLen); + if(bodyLen > 0 && !RecvAll(reinterpret_cast(out.bytes.data()), static_cast(bodyLen))) return false; + + // Unlike Protobuf's ParseFromString, FlatBuffers does no validation on access by default -- + // Get()/BodyAs() below would just reinterpret these bytes as a table, and reading fields out + // of a truncated/corrupted buffer is an out-of-bounds read, not a clean failure. Verifier is + // what actually plays ParseFromString's role here: walking the buffer to confirm every + // offset/vector/string is in-bounds before anything touches it. + flatbuffers::Verifier verifier(out.bytes.data(), out.bytes.size()); + return x2win::VerifyEnvelopeBuffer(verifier); +} + +bool Connection::WriteEnvelope(const flatbuffers::FlatBufferBuilder &builder){ + std::lock_guard lock(m_writeMutex); + + uint32_t bodyLen = static_cast(builder.GetSize()); + if(!SendAll(reinterpret_cast(&bodyLen), sizeof(bodyLen))) return false; + + if(bodyLen > 0 && !SendAll(reinterpret_cast(builder.GetBufferPointer()), static_cast(bodyLen))) return false; + + return true; +} \ No newline at end of file diff --git a/x2winstub/net/connection.h b/x2winstub/net/connection.h new file mode 100644 index 00000000..0b50c1df --- /dev/null +++ b/x2winstub/net/connection.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +#include "socket_handle.h" +#include + +// A parsed x2win::Envelope is just a read-only view into a byte buffer (unlike a Protobuf +// message, it owns no state of its own) -- something has to keep that buffer alive for as long as +// the view is used. This pairs the two: Get()/BodyAs() are only valid while this object (or a +// copy of its `bytes`) is alive. An empty `bytes` (default-constructed, or a send/receive failure) +// is a valid "no message" state -- Get()/BodyAs() return nullptr rather than dereferencing a +// nonexistent buffer. Duplicated from core/adapters/x2winrpcadapter.h's identical helper rather +// than shared through a common header -- x2winstub is intentionally built independent of anything +// in core/ (see debug_types.h's comment on the same tradeoff), and this is small enough that +// duplicating it keeps that independence intact. +struct X2WinEnvelopeBuffer +{ + std::vector bytes; + + const x2win::Envelope* Get() const + { + return bytes.empty() ? nullptr : x2win::GetEnvelope(bytes.data()); + } + + template + const T* BodyAs() const + { + const x2win::Envelope* envelope = Get(); + return envelope ? envelope->body_as() : nullptr; + } +}; + +class Connection{ + SocketHandle m_socket; + std::mutex m_writeMutex; + + bool RecvAll(char* buf, int len); + bool SendAll(const char* buf, int len); + + public: + explicit Connection(SocketHandle&& socket) : m_socket(std::move(socket)){} + + bool ReadEnvelope(X2WinEnvelopeBuffer& out); + bool WriteEnvelope(const flatbuffers::FlatBufferBuilder& builder); + +}; diff --git a/x2winstub/net/socket_handle.h b/x2winstub/net/socket_handle.h new file mode 100644 index 00000000..667ce6a9 --- /dev/null +++ b/x2winstub/net/socket_handle.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +class SocketHandle{ + SOCKET m_socket = INVALID_SOCKET; + + public: + SocketHandle() = default; + explicit SocketHandle(SOCKET s) : m_socket(s){} + ~SocketHandle() {reset();} + + SocketHandle(const SocketHandle&) = delete; + SocketHandle& operator=(const SocketHandle&) = delete; + + SocketHandle(SocketHandle&& other) noexcept : m_socket(other.m_socket){ + other.m_socket = INVALID_SOCKET; + } + SocketHandle& operator=(SocketHandle&& other) noexcept{ + if(this != &other){ + reset(); + m_socket = other.m_socket; + other.m_socket = INVALID_SOCKET; + } + return *this; + } + + SOCKET get() const { return m_socket; } + + void reset(SOCKET s = INVALID_SOCKET){ + if(m_socket != INVALID_SOCKET){ + closesocket(m_socket); + } + m_socket = s; + } +}; \ No newline at end of file diff --git a/x2winstub/net/winsock_library.h b/x2winstub/net/winsock_library.h new file mode 100644 index 00000000..05a73e93 --- /dev/null +++ b/x2winstub/net/winsock_library.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +class WinsockLibrary{ + public: + WinsockLibrary(){ + WSADATA wsaData; + int result =WSAStartup(MAKEWORD(2, 2), &wsaData); + if(result != 0){ + throw std::runtime_error("WSAStartup failed: " + std::to_string(result)); + } + } + + ~WinsockLibrary(){ + WSACleanup(); + } + + WinsockLibrary(const WinsockLibrary&) = delete; + WinsockLibrary& operator=(const WinsockLibrary&) = delete; + WinsockLibrary(WinsockLibrary&&) = delete; + WinsockLibrary& operator=(WinsockLibrary&&) = delete; +}; \ No newline at end of file diff --git a/x2winstub/x2win_session.cpp b/x2winstub/x2win_session.cpp new file mode 100644 index 00000000..081d9d55 --- /dev/null +++ b/x2winstub/x2win_session.cpp @@ -0,0 +1,467 @@ +#include "x2win_session.h" +#include "net/connection.h" +#include + +namespace x2win { + + X2WinStubSession::X2WinStubSession(Connection* connection, SessionMode mode) : + m_connection(connection), m_mode(mode) + { + m_engine.SetEventCallback([this](const EngineEvent& event) { OnEngineEvent(event); }); + } + + + void X2WinStubSession::OnEngineEvent(const EngineEvent& event) + { + if(event.type == EngineEventType::TargetExited){ + m_isStopped = true; + m_lastStopReason = StopReason_EXITED; + + if(!m_connection) return; + + flatbuffers::FlatBufferBuilder builder; + auto eventBody = CreateTargetStoppedEvent(builder, StopReason_EXITED, /*address=*/0, event.exitCode); + auto envelope = CreateEnvelope(builder, /*request_id=*/0, Body_TargetStoppedEvent, eventBody.Union()); + builder.Finish(envelope); + m_connection->WriteEnvelope(builder); + return; + } + + // Only TargetStopped has a wire representation today (TargetStoppedEvent). LaunchFailure/ + // TargetExited/Resumed/StepIntoComplete don't have a proto event yet -- future work, same as + // the other WindowsDebugEngine capabilities (hardware breakpoints, registers, stepping) that + // aren't wired through the proto surface yet either. + if (event.type != EngineEventType::TargetStopped) + return; + + // Fire the first-stop signal exactly once, regardless of whether a client is connected yet + // -- target mode waits on this (WaitForFirstStop()) before it has even opened the listen + // socket, let alone accepted a connection. + bool expected = false; + if (m_firstStopSeen.compare_exchange_strong(expected, true)) + m_firstStopPromise.set_value(); + + StopReason reason = StopReason_UNKNOWN; + switch (event.stopReason) + { + case InitialBreakpoint: reason = StopReason_INITIAL_BREAKPOINT; break; + case Breakpoint: reason = StopReason_BREAKPOINT; break; + case SingleStep: reason = StopReason_SINGLE_STEP; break; + default: break; + } + + // Track current stop state regardless of whether a client is connected -- a client that + // reconnects later (target mode) needs to know this even though it wasn't around when the + // stop actually happened. + m_isStopped = true; + m_lastStopReason = reason; + + if (!m_connection) + return; // no client connected yet; target mode sends this stop manually once one is + + flatbuffers::FlatBufferBuilder builder; + auto eventBody = CreateTargetStoppedEvent(builder, reason, m_engine.GetInstructionOffset(), /*exit_code=*/0); + auto envelope = CreateEnvelope(builder, /*request_id=*/0, Body_TargetStoppedEvent, eventBody.Union()); + builder.Finish(envelope); + m_connection->WriteEnvelope(builder); + } + + + bool X2WinStubSession::HandleRequest(const Envelope& request, flatbuffers::FlatBufferBuilder& builder) + { + switch (request.body_type()) + { + case Body_GetTargetArchRequest:{ + auto archOff = builder.CreateString(m_engine.GetTargetArchitecture()); + auto respBody = CreateGetTargetArchResponse(builder, archOff); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_GetTargetArchResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_ConnectServerRequest:{ + auto respBody = CreateConnectServerResponse(builder, m_mode == SessionMode::Server); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_ConnectServerResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_AttachRequest:{ + const auto* req = request.body_as(); + bool success = false; + if(req && m_mode == SessionMode::Server){ + success = m_engine.Attach(req->pid()); + } + auto respBody = CreateAttachResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_AttachResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_GetProcessListRequest:{ + std::vector> processOffsets; + for(const auto& process : m_engine.GetProcessList()){ + auto nameOff = builder.CreateString(process.m_processName); + processOffsets.push_back(CreateProcessInfo(builder, process.m_pid, nameOff)); + } + + auto processesVec = builder.CreateVector(processOffsets); + auto respBody = CreateGetProcessListResponse(builder, processesVec); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_GetProcessListResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_LaunchRequest:{ + // Real gdbserver (non--multi) doesn't support this either -- it only ever serves the one + // debuggee it was started with, and gdb can't "run" a new one over the same connection; + // that needs gdbserver --multi (our server mode). Target mode here is the non-multi + // equivalent, so a LaunchRequest arriving in target mode -- whether from user error or from + // BN core's Restart (Quit() then a fresh LaunchRequest) -- gets rejected the same way + // Body_AttachRequest already rejects an out-of-place AttachRequest, rather than trying (and + // likely failing, or launching the wrong thing) to execute it. + if(m_mode != SessionMode::Server){ + auto respBody = CreateLaunchResponse(builder, false); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_LaunchResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + // Copied into owned strings (rather than kept as FlatBuffers string views into + // `request`) because `request` is only valid for the duration of this call -- the + // caller's underlying byte buffer gets reused for the next request as soon as + // HandleRequest() returns, but the detached thread below runs well after that. + const auto* req = request.body_as(); + std::string path = (req && req->path()) ? req->path()->str() : std::string(); + std::string args = (req && req->args()) ? req->args()->str() : std::string(); + std::string workingDir = (req && req->working_dir()) ? req->working_dir()->str() : std::string(); + uint64_t requestId = request.request_id(); + + std::thread([this, path, args, workingDir, requestId]() { + bool ok = m_engine.ExecuteWithArgs(path, args, workingDir); + + flatbuffers::FlatBufferBuilder launchBuilder; + auto respBody = CreateLaunchResponse(launchBuilder, ok); + auto envelope = CreateEnvelope(launchBuilder, requestId, Body_LaunchResponse, respBody.Union()); + launchBuilder.Finish(envelope); + m_connection->WriteEnvelope(launchBuilder); + }).detach(); + + return false; // response already sent asynchronously above + } + + case Body_GoRequest:{ + bool success = m_engine.Go(); + if(success) m_isStopped = false; + auto respBody = CreateGoResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_GoResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_StepIntoRequest:{ + bool success = m_engine.StepInto(); + if(success) m_isStopped = false; + auto respBody = CreateStepIntoResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_StepIntoResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_StepOverRequest:{ + bool success = m_engine.StepOver(); + if(success) m_isStopped = false; + auto respBody = CreateStepOverResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_StepOverResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_StepReturnRequest:{ + bool success = m_engine.StepReturn(); + if(success) m_isStopped = false; + + auto respBody = CreateStepIntoResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_StepReturnResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_BreakIntoRequest:{ + auto respBody = CreateBreakIntoResponse(builder, m_engine.BreakInto()); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_BreakIntoResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_RemoveBreakpointRequest:{ + const auto* req = request.body_as(); + bool success = false; + if(req){ + success = m_engine.RemoveBreakpoint(DebugBreakpoint(static_cast(req->address()))); + } + auto respBody = CreateRemoveBreakpointResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_RemoveBreakpointResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_SetBreakpointRequest:{ + const auto* req = request.body_as(); + bool success = false; + uint64_t breakpointId = 0; + if (!req || req->type() != BreakpointType_SOFTWARE) + { + LogError("SetBreakpointRequest: unsupported breakpoint type %d", req ? static_cast(req->type()) : -1); + } + else + { + DebugBreakpoint bp = m_engine.AddBreakpoint(static_cast(req->address())); + success = bp.m_is_active; + breakpointId = bp.m_is_active ? bp.m_id : 0; + } + auto respBody = CreateSetBreakpointResponse(builder, success, breakpointId); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_SetBreakpointResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_SetHardwareBreakpointRequest:{ + const auto* req = request.body_as(); + bool success = false; + if (req) + { + success = m_engine.AddHardwareBreakpoint( + req->address(), + static_cast(req->type()), + static_cast(req->size())); + } + auto respBody = CreateSetHardwareBreakpointResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_SetHardwareBreakpointResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_RemoveHardwareBreakpointRequest:{ + const auto* req = request.body_as(); + bool success = false; + if (req) + { + success = m_engine.RemoveHardwareBreakpoint( + req->address(), + static_cast(req->type()), + static_cast(req->size())); + } + auto respBody = CreateRemoveHardwareBreakpointResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_RemoveHardwareBreakpointResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_ReadMemoryRequest:{ + const auto* req = request.body_as(); + bool ok = false; + flatbuffers::Offset> dataOff; + if (req) + { + auto data = m_engine.ReadMemory(req->address(), req->size()); + // A short/partial read is reported as failure, never a truncated buffer -- see the + // contract documented on ReadMemoryResponse in protocol/x2win.fbs. + ok = (data.size() == req->size()); + if (ok) + dataOff = builder.CreateVector(data.data(), data.size()); + } + auto respBody = CreateReadMemoryResponse(builder, ok, dataOff); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_ReadMemoryResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_WriteMemoryRequest:{ + const auto* req = request.body_as(); + bool ok = false; + + if(req && req->data()){ + std::vector buffer(req->data()->begin(), req->data()->end()); + ok = m_engine.WriteMemory(req->address(), buffer); + } + + auto respBody = CreateWriteMemoryResponse(builder, ok); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_WriteMemoryResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_ReadAllRegistersRequest:{ + std::vector> regOffsets; + for (const auto& [name, reg] : m_engine.ReadAllRegisters()){ + auto nameOff = builder.CreateString(reg.m_name); + regOffsets.push_back(CreateRegisterEntry(builder, nameOff,reg.m_value, + static_cast(reg.m_width), static_cast(reg.m_registerIndex))); + } + + auto regsVec = builder.CreateVector(regOffsets); + auto respBody = CreateReadAllRegistersResponse(builder, regsVec); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_ReadAllRegistersResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_ReadRegisterRequest:{ + const auto* req = request.body_as(); + bool success = false; + DebugRegister reg; + if(req && req->name()){ + reg = m_engine.ReadRegister(req->name()->str()); + success = !reg.m_name.empty(); + } + + auto respBody = CreateReadRegisterResponse(builder, success, reg.m_value, + static_cast(reg.m_width), static_cast(reg.m_registerIndex)); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_ReadRegisterResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_WriteRegisterRequest:{ + const auto* req = request.body_as(); + bool success = false; + if(req && req->name()){ + success = m_engine.WriteRegister(req->name()->str(), req->value()); + } + auto respBody = CreateWriteRegisterResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_WriteRegisterResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_GetModuleListRequest:{ + std::vector> moduleOffsets; + for (const auto& module : m_engine.GetModuleList()) + { + auto nameOff = builder.CreateString(module.m_name); + moduleOffsets.push_back(CreateModuleEntry(builder, nameOff, module.m_address, module.m_size)); + } + auto modulesVec = builder.CreateVector(moduleOffsets); + auto respBody = CreateGetModuleListResponse(builder, modulesVec); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_GetModuleListResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_GetThreadListRequest:{ + std::vector> threadOffsets; + for(const auto& thread : m_engine.GetThreadList()){ + threadOffsets.push_back(CreateThreadEntry(builder, thread.m_tid, thread.m_rip, thread.m_isFrozen)); + } + auto threadsVec = builder.CreateVector(threadOffsets); + auto respBody = CreateGetThreadListResponse(builder, threadsVec); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_GetThreadListResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_GetFramesOfThreadRequest:{ + const auto* req = request.body_as(); + std::vector> frameOffsets; + if(req){ + for(const auto& frame : m_engine.GetFramesOfThread(req->tid())){ + auto functionNameOff = builder.CreateString(frame.m_functionName); + auto moduleOff = builder.CreateString(frame.m_module); + + frameOffsets.push_back(CreateFrameEntry(builder, + static_cast(frame.m_index), frame.m_pc, frame.m_sp, frame.m_fp, + functionNameOff, frame.m_functionStart, moduleOff)); + } + } + + auto framesVec = builder.CreateVector(frameOffsets); + auto respBody = CreateGetFramesOfThreadResponse(builder, framesVec); + + auto envelope = CreateEnvelope(builder, request.request_id(), Body_GetFramesOfThreadResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_GetActiveThreadIdRequest:{ + auto respBody = CreateGetActiveThreadIdResponse(builder, m_engine.GetActiveThreadId()); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_GetActiveThreadIdResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_GetMemoryMapRequest:{ + std::vector> regionOffsets; + for(const auto& region : m_engine.GetMemoryMap()){ + auto nameOff = builder.CreateString(region.m_name); + + regionOffsets.push_back(CreateMemoryRegionEntry(builder, + region.m_start, region.m_size, nameOff, + region.m_read, region.m_write, region.m_execute, region.m_shared)); + } + + auto regionsVec = builder.CreateVector(regionOffsets); + auto respBody = CreateGetMemoryMapResponse(builder, regionsVec); + + auto envelope = CreateEnvelope(builder, request.request_id(), Body_GetMemoryMapResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_SetActiveThreadIdRequest:{ + const auto* req = request.body_as(); + bool success = false; + if(req){ + success = m_engine.SetActiveThreadId(req->tid()); + } + auto respBody = CreateSetActiveThreadIdResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_SetActiveThreadIdResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_SuspendThreadRequest:{ + const auto* req = request.body_as(); + bool success = false; + if(req){ + success = m_engine.SuspendThread(req->tid()); + } + auto respBody = CreateSuspendThreadResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_SuspendThreadResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_ResumeThreadRequest:{ + const auto* req = request.body_as(); + bool success = false; + if(req){ + success = m_engine.ResumeThread(req->tid()); + } + auto respBody = CreateResumeThreadResponse(builder, success); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_ResumeThreadResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_QuitRequest:{ + auto respBody = CreateQuitResponse(builder, m_engine.Quit()); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_QuitResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + case Body_DetachRequest:{ + auto respBody = CreateDetachResponse(builder, m_engine.Detach()); + auto envelope = CreateEnvelope(builder, request.request_id(), Body_DetachResponse, respBody.Union()); + builder.Finish(envelope); + return true; + } + + default: + LogError("unhandled request body_type=%d", static_cast(request.body_type())); + return false; + } + } + +} // namespace x2win diff --git a/x2winstub/x2win_session.h b/x2winstub/x2win_session.h new file mode 100644 index 00000000..dc93469d --- /dev/null +++ b/x2winstub/x2win_session.h @@ -0,0 +1,75 @@ +#pragma once +#include "debug/windows_debug_engine.h" +#include +#include +#include + +class Connection; + +namespace x2win { + + enum class SessionMode + { + Target, // this process launched/owns the debuggee (x2winstub target ) + Server // this process is a standalone RPC server (x2winstub server), no owned debuggee + }; + + // Owns one WindowsDebugEngine and parses/dispatches x2win::Envelope proto commands to it -- + // this is the "new class that contains WindowsDebugEngine and does the proto command parsing" + // that replaces main.cpp's inline HandleClient() switch + the free-function debug_loop.h API. + // + // The engine's async events (currently just the initial/regular breakpoint stop) are translated + // into TargetStoppedEvent envelopes and written to the connection as they happen, via the + // callback registered in the constructor. + class X2WinStubSession + { + private: + WindowsDebugEngine m_engine; + Connection* m_connection; + SessionMode m_mode; + + // Fulfilled the first time the engine reports TargetStopped, independent of whether a + // connection is attached yet. Target mode launches the debuggee and waits on this before a + // client has even connected (see WaitForFirstStop()); once a client is connected, that same + // first stop is otherwise indistinguishable from any later one. + std::promise m_firstStopPromise; + std::atomic m_firstStopSeen {false}; + + // Tracks the *current* stop state (independent of whether a client is connected right now) -- + // unlike m_firstStopSeen (one-shot, fires once ever), this reflects "are we stopped right now", + // so a client reconnecting after a disconnect (target mode) can be told immediately instead of + // only ever getting this on the very first connection. + std::atomic m_isStopped {false}; + std::atomic m_lastStopReason {StopReason_UNKNOWN}; + + void OnEngineEvent(const EngineEvent& event); + + public: + X2WinStubSession(Connection* connection, SessionMode mode); + + // Dispatches one already-parsed request. `builder` ends up holding a finished Envelope that + // should be written by the caller -- unless this returns false, meaning the request either + // has no reply (an unhandled request kind) or already sent its own reply asynchronously + // (LaunchRequest, whose LaunchResponse is sent from a background thread once CreateProcess + // returns). `builder` is caller-owned (rather than built internally and returned) for the + // same reason CallSync's callers own theirs on the BN-core side of this protocol: a + // FlatBuffers table can only be built bottom-up with one builder, and the response body + // table built by each case below has to share the builder that goes on to wrap it in the + // Envelope. + bool HandleRequest(const Envelope& request, flatbuffers::FlatBufferBuilder& builder); + + WindowsDebugEngine& Engine() { return m_engine; } + + // Attaches (or reattaches) the connection used for outgoing events. Target mode constructs + // the session before any client has connected -- see main.cpp. + void SetConnection(Connection* connection) { m_connection = connection; } + + SessionMode Mode() const { return m_mode; } + bool IsStopped() const { return m_isStopped; } + StopReason LastStopReason() const { return m_lastStopReason; } + + // Blocks until the engine's first TargetStopped event (target mode's initial breakpoint). + void WaitForFirstStop() { m_firstStopPromise.get_future().wait(); } + }; + +} // namespace x2win