Add initial support for Wasm debugging with LLDB: implements a GDB-remote stub
This is the first piece of the wasm debugging prototype (besides the changes to
add/remove breakpoints in WasmModuleObject made with
e699f39cae
).
This changelist adds the infrastructure for a GDB-remote stub that will be used
to manage debugging sessions via the gdb-remote protocol.
It enables the creation and termination of debugging sessions over TCP
connections that are managed in a separate thread.
The logic to actually send, receive and decode GDB-remote packets will be part
of a future changelist.
Build with: v8_enable_wasm_gdb_remote_debugging = true
Run with:
--wasm-gdb-remote Enables Wasm debugging with LLDB
(default: false)
--wasm-gdb-remote-port TCP port to be used for debugging
(default: 8765)
--wasm-pause-waiting-for-debugger Pauses the execution of Wasm code waiting
for a debugger (default: false)
--trace-wasm-gdb-remote Enables tracing of Gdb-remote packets
(default: false)
Note that most of this code is "borrowed" from the code of the Chromium NaCL
GDB-remote stub (located in Chromium in src\native_client\src\trusted\debug_stub).
Implementation details:
- class GdbServer acts as a singleton manager for the gdb-remote stub. It is
instantiated as soon as the first Wasm module is loaded in the Wasm engine.
- class GdbServerThread spawns the worker thread for the TCP connection.
- class Transport manages the socket connection, in a portable way.
- class Session represents a remote debugging session.
- class Target represents a debugging target and it’s the place where the
debugging packets will be processed and will implement the logic to debug
a Wasm engine.
Bug: chromium:1010467
Change-Id: Ib2324e5901f5ae1d855b96b99ef0995d407322b6
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1923407
Reviewed-by: Clemens Backes <clemensb@chromium.org>
Reviewed-by: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: Michael Achenbach <machenbach@chromium.org>
Commit-Queue: Paolo Severini <paolosev@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#66379}
This commit is contained in:
parent
9bb73365eb
commit
03fc414908
19
BUILD.gn
19
BUILD.gn
@ -517,6 +517,9 @@ config("features") {
|
||||
if (v8_control_flow_integrity) {
|
||||
defines += [ "V8_ENABLE_CONTROL_FLOW_INTEGRITY" ]
|
||||
}
|
||||
if (v8_enable_wasm_gdb_remote_debugging) {
|
||||
defines += [ "V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING" ]
|
||||
}
|
||||
}
|
||||
|
||||
config("toolchain") {
|
||||
@ -3049,6 +3052,22 @@ v8_source_set("v8_base_without_compiler") {
|
||||
sources += v8_third_party_heap_files
|
||||
}
|
||||
|
||||
if (v8_enable_wasm_gdb_remote_debugging) {
|
||||
sources += [
|
||||
"src/debug/wasm/gdb-server/gdb-server-thread.cc",
|
||||
"src/debug/wasm/gdb-server/gdb-server-thread.h",
|
||||
"src/debug/wasm/gdb-server/gdb-server.cc",
|
||||
"src/debug/wasm/gdb-server/gdb-server.h",
|
||||
"src/debug/wasm/gdb-server/session.cc",
|
||||
"src/debug/wasm/gdb-server/session.h",
|
||||
"src/debug/wasm/gdb-server/target.cc",
|
||||
"src/debug/wasm/gdb-server/target.h",
|
||||
"src/debug/wasm/gdb-server/transport.cc",
|
||||
"src/debug/wasm/gdb-server/transport.h",
|
||||
"src/debug/wasm/gdb-server/util.h",
|
||||
]
|
||||
}
|
||||
|
||||
if (v8_check_header_includes) {
|
||||
# This file will be generated by tools/generate-header-include-checks.py
|
||||
# if the "check_v8_header_includes" gclient variable is set.
|
||||
|
@ -60,6 +60,9 @@ declare_args() {
|
||||
|
||||
# Override global symbol level setting for v8
|
||||
v8_symbol_level = symbol_level
|
||||
|
||||
# Enable WebAssembly debugging via GDB-remote protocol.
|
||||
v8_enable_wasm_gdb_remote_debugging = false
|
||||
}
|
||||
|
||||
if (v8_use_external_startup_data == "") {
|
||||
|
118
src/debug/wasm/gdb-server/gdb-server-thread.cc
Normal file
118
src/debug/wasm/gdb-server/gdb-server-thread.cc
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "src/debug/wasm/gdb-server/gdb-server-thread.h"
|
||||
#include "src/debug/wasm/gdb-server/gdb-server.h"
|
||||
#include "src/debug/wasm/gdb-server/session.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
GdbServerThread::GdbServerThread(GdbServer* gdb_server)
|
||||
: Thread(v8::base::Thread::Options("GdbServerThread")),
|
||||
gdb_server_(gdb_server),
|
||||
start_semaphore_(0) {}
|
||||
|
||||
bool GdbServerThread::StartAndInitialize() {
|
||||
// Executed in the Isolate thread.
|
||||
if (!Start()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to make sure that {Stop} is never called before the thread has
|
||||
// completely initialized {transport_} and {target_}. Otherwise there could be
|
||||
// a race condition where in the main thread {Stop} might get called before
|
||||
// the transport is created, and then in the GDBServer thread we may have time
|
||||
// to setup the transport and block on accept() before the main thread blocks
|
||||
// on joining the thread.
|
||||
// The small performance hit caused by this Wait should be negligeable because
|
||||
// this operation happensat most once per process and only when the
|
||||
// --wasm-gdb-remote flag is set.
|
||||
start_semaphore_.Wait();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GdbServerThread::CleanupThread() {
|
||||
// Executed in the GdbServer thread.
|
||||
v8::base::MutexGuard guard(&mutex_);
|
||||
|
||||
target_ = nullptr;
|
||||
transport_ = nullptr;
|
||||
|
||||
#if _WIN32
|
||||
::WSACleanup();
|
||||
#endif
|
||||
}
|
||||
|
||||
void GdbServerThread::Run() {
|
||||
// Executed in the GdbServer thread.
|
||||
#ifdef _WIN32
|
||||
// Initialize Winsock
|
||||
WSADATA wsaData;
|
||||
int iResult = ::WSAStartup(MAKEWORD(2, 2), &wsaData);
|
||||
if (iResult != 0) {
|
||||
TRACE_GDB_REMOTE("GdbServerThread::Run: WSAStartup failed\n");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// If the default port is not available, try any port.
|
||||
SocketBinding socket_binding = SocketBinding::Bind(FLAG_wasm_gdb_remote_port);
|
||||
if (!socket_binding.IsValid()) {
|
||||
socket_binding = SocketBinding::Bind(0);
|
||||
}
|
||||
if (!socket_binding.IsValid()) {
|
||||
TRACE_GDB_REMOTE("GdbServerThread::Run: Failed to bind any TCP port\n");
|
||||
return;
|
||||
}
|
||||
TRACE_GDB_REMOTE("gdb-remote(%d) : Connect GDB with 'target remote :%d\n",
|
||||
__LINE__, socket_binding.GetBoundPort());
|
||||
|
||||
transport_ = socket_binding.CreateTransport();
|
||||
target_ = std::make_unique<Target>(gdb_server_);
|
||||
|
||||
// Here we have completed the initialization, and the thread that called
|
||||
// {StartAndInitialize} may resume execution.
|
||||
start_semaphore_.Signal();
|
||||
|
||||
while (!target_->IsTerminated()) {
|
||||
// Wait for incoming connections.
|
||||
if (!transport_->AcceptConnection()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new session for this connection
|
||||
Session session(transport_.get());
|
||||
TRACE_GDB_REMOTE("GdbServerThread: Connected\n");
|
||||
|
||||
// Run this session for as long as it lasts
|
||||
target_->Run(&session);
|
||||
}
|
||||
CleanupThread();
|
||||
}
|
||||
|
||||
void GdbServerThread::Stop() {
|
||||
// Executed in the Isolate thread.
|
||||
|
||||
// Synchronized, becauses {Stop} might be called while {Run} is still
|
||||
// initializing {transport_} and {target_}. If this happens and the thread is
|
||||
// blocked waiting for an incoming connection or GdbServer for incoming
|
||||
// packets, it will unblocked when {transport_} is closed.
|
||||
v8::base::MutexGuard guard(&mutex_);
|
||||
|
||||
if (target_) {
|
||||
target_->Terminate();
|
||||
}
|
||||
|
||||
if (transport_) {
|
||||
transport_->Close();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
61
src/debug/wasm/gdb-server/gdb-server-thread.h
Normal file
61
src/debug/wasm/gdb-server/gdb-server-thread.h
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef V8_DEBUG_WASM_GDB_SERVER_GDB_SERVER_THREAD_H_
|
||||
#define V8_DEBUG_WASM_GDB_SERVER_GDB_SERVER_THREAD_H_
|
||||
|
||||
#include "src/base/platform/platform.h"
|
||||
#include "src/base/platform/semaphore.h"
|
||||
#include "src/debug/wasm/gdb-server/target.h"
|
||||
#include "src/debug/wasm/gdb-server/transport.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
class GdbServer;
|
||||
|
||||
// class GdbServerThread spawns a thread where all communication with a debugger
|
||||
// happens.
|
||||
class GdbServerThread : public v8::base::Thread {
|
||||
public:
|
||||
explicit GdbServerThread(GdbServer* gdb_server);
|
||||
|
||||
// base::Thread
|
||||
void Run() override;
|
||||
|
||||
// Starts the GDB-server thread and waits Run() method is called on the new
|
||||
// thread and the initialization completes.
|
||||
bool StartAndInitialize();
|
||||
|
||||
// Stops the GDB-server thread when the V8 process shuts down; gracefully
|
||||
// closes any active debugging session.
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void CleanupThread();
|
||||
|
||||
GdbServer* gdb_server_;
|
||||
|
||||
// Used to block the caller on StartAndInitialize() waiting for the new thread
|
||||
// to have completed its initialization.
|
||||
// (Note that Thread::StartSynchronously() wouldn't work in this case because
|
||||
// it returns as soon as the new thread starts, but before Run() is called).
|
||||
base::Semaphore start_semaphore_;
|
||||
|
||||
base::Mutex mutex_;
|
||||
// Protected by {mutex_}:
|
||||
std::unique_ptr<Transport> transport_;
|
||||
std::unique_ptr<Target> target_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(GdbServerThread);
|
||||
};
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
||||
#endif // V8_DEBUG_WASM_GDB_SERVER_GDB_SERVER_THREAD_H_
|
38
src/debug/wasm/gdb-server/gdb-server.cc
Normal file
38
src/debug/wasm/gdb-server/gdb-server.cc
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "src/debug/wasm/gdb-server/gdb-server.h"
|
||||
|
||||
#include "src/debug/wasm/gdb-server/gdb-server-thread.h"
|
||||
#include "src/wasm/wasm-engine.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
GdbServer::GdbServer() {
|
||||
DCHECK(!thread_);
|
||||
DCHECK(FLAG_wasm_gdb_remote);
|
||||
|
||||
thread_ = std::make_unique<GdbServerThread>(this);
|
||||
// TODO(paolosev): does StartSynchronously hurt performances?
|
||||
if (!thread_->StartAndInitialize()) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Cannot initialize thread, GDB-remote debugging will be disabled.\n");
|
||||
thread_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
GdbServer::~GdbServer() {
|
||||
if (thread_) {
|
||||
thread_->Stop();
|
||||
thread_->Join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
43
src/debug/wasm/gdb-server/gdb-server.h
Normal file
43
src/debug/wasm/gdb-server/gdb-server.h
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef V8_DEBUG_WASM_GDB_SERVER_GDB_SERVER_H_
|
||||
#define V8_DEBUG_WASM_GDB_SERVER_GDB_SERVER_H_
|
||||
|
||||
#include <memory>
|
||||
#include "src/debug/wasm/gdb-server/gdb-server-thread.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
// class GdbServer acts as a manager for the GDB-remote stub. It is instantiated
|
||||
// as soon as the first Wasm module is loaded in the Wasm engine and spawns a
|
||||
// separate thread to accept connections and exchange messages with a debugger.
|
||||
// It will contain the logic to serve debugger queries and access the state of
|
||||
// the Wasm engine.
|
||||
class GdbServer {
|
||||
public:
|
||||
// Spawns a "GDB-remote" thread that will be used to communicate with the
|
||||
// debugger. This should be called once, the first time a Wasm module is
|
||||
// loaded in the Wasm engine.
|
||||
GdbServer();
|
||||
|
||||
// Stops the "GDB-remote" thread and waits for it to complete. This should be
|
||||
// called once, when the Wasm engine shuts down.
|
||||
~GdbServer();
|
||||
|
||||
private:
|
||||
std::unique_ptr<GdbServerThread> thread_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(GdbServer);
|
||||
};
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
||||
#endif // V8_DEBUG_WASM_GDB_SERVER_GDB_SERVER_H_
|
48
src/debug/wasm/gdb-server/session.cc
Normal file
48
src/debug/wasm/gdb-server/session.cc
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "src/debug/wasm/gdb-server/session.h"
|
||||
#include "src/debug/wasm/gdb-server/transport.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
Session::Session(Transport* transport) : io_(transport), connected_(true) {}
|
||||
|
||||
void Session::WaitForDebugStubEvent() { io_->WaitForDebugStubEvent(); }
|
||||
|
||||
bool Session::SignalThreadEvent() { return io_->SignalThreadEvent(); }
|
||||
|
||||
bool Session::IsDataAvailable() const { return io_->IsDataAvailable(); }
|
||||
|
||||
bool Session::IsConnected() const { return connected_; }
|
||||
|
||||
void Session::Disconnect() {
|
||||
io_->Disconnect();
|
||||
connected_ = false;
|
||||
}
|
||||
|
||||
bool Session::GetChar(char* ch) {
|
||||
if (!io_->Read(ch, 1)) {
|
||||
Disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Session::GetPacket() {
|
||||
char ch;
|
||||
if (!GetChar(&ch)) return false;
|
||||
|
||||
// discard the input
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
59
src/debug/wasm/gdb-server/session.h
Normal file
59
src/debug/wasm/gdb-server/session.h
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef V8_DEBUG_WASM_GDB_SERVER_SESSION_H_
|
||||
#define V8_DEBUG_WASM_GDB_SERVER_SESSION_H_
|
||||
|
||||
#include "src/base/macros.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
class Transport;
|
||||
|
||||
// Represents a gdb-remote debugging session.
|
||||
class Session {
|
||||
public:
|
||||
explicit Session(Transport* transport);
|
||||
|
||||
// Attempt to receive a packet.
|
||||
// For the moment this method is only used to check whether the TCP connection
|
||||
// is still active; all bytes read are discarded.
|
||||
bool GetPacket();
|
||||
|
||||
// Return true if there is data to read.
|
||||
bool IsDataAvailable() const;
|
||||
|
||||
// Return true if the connection still valid.
|
||||
bool IsConnected() const;
|
||||
|
||||
// Shutdown the connection.
|
||||
void Disconnect();
|
||||
|
||||
// When a debugging session is active, the GDB-remote thread can block waiting
|
||||
// for events and it will resume execution when one of these two events arise:
|
||||
// - A network event (a new packet arrives, or the connection is dropped)
|
||||
// - A thread event (the execution stopped because of a trap or breakpoint).
|
||||
void WaitForDebugStubEvent();
|
||||
|
||||
// Signal that the debuggee execution stopped because of a trap or breakpoint.
|
||||
bool SignalThreadEvent();
|
||||
|
||||
private:
|
||||
bool GetChar(char* ch);
|
||||
|
||||
Transport* io_; // Transport object not owned by the Session.
|
||||
bool connected_; // Is the connection still valid.
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(Session);
|
||||
};
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
||||
#endif // V8_DEBUG_WASM_GDB_SERVER_SESSION_H_
|
65
src/debug/wasm/gdb-server/target.cc
Normal file
65
src/debug/wasm/gdb-server/target.cc
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "src/debug/wasm/gdb-server/target.h"
|
||||
|
||||
#include "src/base/platform/time.h"
|
||||
#include "src/debug/wasm/gdb-server/gdb-server.h"
|
||||
#include "src/debug/wasm/gdb-server/session.h"
|
||||
#include "src/debug/wasm/gdb-server/transport.h"
|
||||
#include "src/debug/wasm/gdb-server/util.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
Target::Target(GdbServer* gdb_server)
|
||||
: status_(Status::Running), session_(nullptr) {}
|
||||
|
||||
void Target::Terminate() {
|
||||
// Executed in the Isolate thread.
|
||||
status_ = Status::Terminated;
|
||||
}
|
||||
|
||||
void Target::Run(Session* session) {
|
||||
// Executed in the GdbServer thread.
|
||||
|
||||
session_ = session;
|
||||
do {
|
||||
WaitForDebugEvent();
|
||||
ProcessCommands();
|
||||
} while (!IsTerminated() && session_->IsConnected());
|
||||
session_ = nullptr;
|
||||
}
|
||||
|
||||
void Target::WaitForDebugEvent() {
|
||||
// Executed in the GdbServer thread.
|
||||
|
||||
if (status_ != Status::Terminated) {
|
||||
// Wait for either:
|
||||
// * the thread to fault (or single-step)
|
||||
// * an interrupt from LLDB
|
||||
session_->WaitForDebugStubEvent();
|
||||
}
|
||||
}
|
||||
|
||||
void Target::ProcessCommands() {
|
||||
// GDB-remote messages are processed in the GDBServer thread.
|
||||
|
||||
if (IsTerminated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(paolosev)
|
||||
// For the moment just discard any packet we receive from the debugger.
|
||||
do {
|
||||
if (!session_->GetPacket()) continue;
|
||||
} while (session_->IsConnected());
|
||||
}
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
57
src/debug/wasm/gdb-server/target.h
Normal file
57
src/debug/wasm/gdb-server/target.h
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef V8_DEBUG_WASM_GDB_SERVER_TARGET_H_
|
||||
#define V8_DEBUG_WASM_GDB_SERVER_TARGET_H_
|
||||
|
||||
#include <atomic>
|
||||
#include "src/base/macros.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
class GdbServer;
|
||||
class Session;
|
||||
|
||||
// Class Target represents a debugging target. It contains the logic to decode
|
||||
// incoming GDB-remote packets, execute them forwarding the debugger commands
|
||||
// and queries to the Wasm engine, and send back GDB-remote packets.
|
||||
class Target {
|
||||
public:
|
||||
// Contruct a Target object.
|
||||
explicit Target(GdbServer* gdb_server);
|
||||
|
||||
// This function spin on a debugging session, until it closes.
|
||||
void Run(Session* ses);
|
||||
|
||||
void Terminate();
|
||||
bool IsTerminated() const { return status_ == Status::Terminated; }
|
||||
|
||||
private:
|
||||
// Blocks waiting for one of these two events to occur:
|
||||
// - A network packet arrives from the debugger, or the debugger connection is
|
||||
// closed;
|
||||
// - The debuggee suspends execution because of a trap or breakpoint.
|
||||
void WaitForDebugEvent();
|
||||
|
||||
// Processes GDB-remote packets that arrive from the debugger.
|
||||
// This method should be called when the debuggee has suspended its execution.
|
||||
void ProcessCommands();
|
||||
|
||||
enum class Status { Running, Terminated };
|
||||
std::atomic<Status> status_;
|
||||
|
||||
Session* session_; // Session object not owned by the Target.
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(Target);
|
||||
};
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
||||
#endif // V8_DEBUG_WASM_GDB_SERVER_TARGET_H_
|
444
src/debug/wasm/gdb-server/transport.cc
Normal file
444
src/debug/wasm/gdb-server/transport.cc
Normal file
@ -0,0 +1,444 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "src/debug/wasm/gdb-server/transport.h"
|
||||
#include <fcntl.h>
|
||||
|
||||
#ifndef SD_BOTH
|
||||
#define SD_BOTH 2
|
||||
#endif
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
SocketBinding::SocketBinding(SocketHandle socket_handle)
|
||||
: socket_handle_(socket_handle) {}
|
||||
|
||||
// static
|
||||
SocketBinding SocketBinding::Bind(uint16_t tcp_port) {
|
||||
SocketHandle socket_handle = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (socket_handle == InvalidSocket) {
|
||||
TRACE_GDB_REMOTE("Failed to create socket.\n");
|
||||
return SocketBinding(InvalidSocket);
|
||||
}
|
||||
struct sockaddr_in sockaddr;
|
||||
// Clearing sockaddr_in first appears to be necessary on Mac OS X.
|
||||
memset(&sockaddr, 0, sizeof(sockaddr));
|
||||
socklen_t addrlen = static_cast<socklen_t>(sizeof(sockaddr));
|
||||
sockaddr.sin_family = AF_INET;
|
||||
sockaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
sockaddr.sin_port = htons(tcp_port);
|
||||
|
||||
#if _WIN32
|
||||
// On Windows, SO_REUSEADDR has a different meaning than on POSIX systems.
|
||||
// SO_REUSEADDR allows hijacking of an open socket by another process.
|
||||
// The SO_EXCLUSIVEADDRUSE flag prevents this behavior.
|
||||
// See:
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms740621(v=vs.85).aspx
|
||||
//
|
||||
// Additionally, unlike POSIX, TCP server sockets can be bound to
|
||||
// ports in the TIME_WAIT state, without setting SO_REUSEADDR.
|
||||
int exclusive_address = 1;
|
||||
if (setsockopt(socket_handle, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
|
||||
reinterpret_cast<char*>(&exclusive_address),
|
||||
sizeof(exclusive_address))) {
|
||||
TRACE_GDB_REMOTE("Failed to set SO_EXCLUSIVEADDRUSE option.\n");
|
||||
}
|
||||
#else
|
||||
// On POSIX, this is necessary to ensure that the TCP port is released
|
||||
// promptly when sel_ldr exits. Without this, the TCP port might
|
||||
// only be released after a timeout, and later processes can fail
|
||||
// to bind it.
|
||||
int reuse_address = 1;
|
||||
if (setsockopt(socket_handle, SOL_SOCKET, SO_REUSEADDR,
|
||||
reinterpret_cast<char*>(&reuse_address),
|
||||
sizeof(reuse_address))) {
|
||||
TRACE_GDB_REMOTE("Failed to set SO_REUSEADDR option.\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (bind(socket_handle, reinterpret_cast<struct sockaddr*>(&sockaddr),
|
||||
addrlen)) {
|
||||
TRACE_GDB_REMOTE("Failed to bind server.\n");
|
||||
return SocketBinding(InvalidSocket);
|
||||
}
|
||||
|
||||
if (listen(socket_handle, 1)) {
|
||||
TRACE_GDB_REMOTE("Failed to listen.\n");
|
||||
return SocketBinding(InvalidSocket);
|
||||
}
|
||||
return SocketBinding(socket_handle);
|
||||
}
|
||||
|
||||
std::unique_ptr<Transport> SocketBinding::CreateTransport() {
|
||||
return std::make_unique<Transport>(socket_handle_);
|
||||
}
|
||||
|
||||
uint16_t SocketBinding::GetBoundPort() {
|
||||
struct sockaddr_in saddr;
|
||||
struct sockaddr* psaddr = reinterpret_cast<struct sockaddr*>(&saddr);
|
||||
// Clearing sockaddr_in first appears to be necessary on Mac OS X.
|
||||
memset(&saddr, 0, sizeof(saddr));
|
||||
socklen_t addrlen = static_cast<socklen_t>(sizeof(saddr));
|
||||
if (::getsockname(socket_handle_, psaddr, &addrlen)) {
|
||||
TRACE_GDB_REMOTE("Failed to retrieve bound address.\n");
|
||||
return 0;
|
||||
}
|
||||
return ntohs(saddr.sin_port);
|
||||
}
|
||||
|
||||
// Do not delay sending small packets. This significantly speeds up
|
||||
// remote debugging. Debug stub uses buffering to send outgoing packets
|
||||
// so they are not split into more TCP packets than necessary.
|
||||
void DisableNagleAlgorithm(SocketHandle socket) {
|
||||
int nodelay = 1;
|
||||
if (::setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
|
||||
reinterpret_cast<char*>(&nodelay), sizeof(nodelay))) {
|
||||
TRACE_GDB_REMOTE("Failed to set TCP_NODELAY option.\n");
|
||||
}
|
||||
}
|
||||
|
||||
TransportBase::TransportBase(SocketHandle s)
|
||||
: buf_(new char[kBufSize]),
|
||||
pos_(0),
|
||||
size_(0),
|
||||
handle_bind_(s),
|
||||
handle_accept_(InvalidSocket) {}
|
||||
|
||||
TransportBase::~TransportBase() {
|
||||
if (handle_accept_ != InvalidSocket) {
|
||||
CloseSocket(handle_accept_);
|
||||
}
|
||||
}
|
||||
|
||||
void TransportBase::CopyFromBuffer(char** dst, int32_t* len) {
|
||||
int32_t copy_bytes = std::min(*len, size_ - pos_);
|
||||
memcpy(*dst, buf_.get() + pos_, copy_bytes);
|
||||
pos_ += copy_bytes;
|
||||
*len -= copy_bytes;
|
||||
*dst += copy_bytes;
|
||||
}
|
||||
|
||||
bool TransportBase::Read(char* dst, int32_t len) {
|
||||
if (pos_ < size_) {
|
||||
CopyFromBuffer(&dst, &len);
|
||||
}
|
||||
while (len > 0) {
|
||||
pos_ = 0;
|
||||
size_ = 0;
|
||||
if (!ReadSomeData()) {
|
||||
return false;
|
||||
}
|
||||
CopyFromBuffer(&dst, &len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TransportBase::Write(const char* src, int32_t len) {
|
||||
while (len > 0) {
|
||||
ssize_t result = ::send(handle_accept_, src, len, 0);
|
||||
if (result > 0) {
|
||||
src += result;
|
||||
len -= result;
|
||||
continue;
|
||||
}
|
||||
if (result == 0) {
|
||||
return false;
|
||||
}
|
||||
if (SocketGetLastError() != kErrInterrupt) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return true if there is data to read.
|
||||
bool TransportBase::IsDataAvailable() const {
|
||||
if (pos_ < size_) {
|
||||
return true;
|
||||
}
|
||||
fd_set fds;
|
||||
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(handle_accept_, &fds);
|
||||
|
||||
// We want a "non-blocking" check
|
||||
struct timeval timeout;
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = 0;
|
||||
|
||||
// Check if this file handle can select on read
|
||||
int cnt = select(static_cast<int>(handle_accept_) + 1, &fds, 0, 0, &timeout);
|
||||
|
||||
// If we are ready, or if there is an error. We return true
|
||||
// on error, to let the next IO request fail.
|
||||
if (cnt != 0) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void TransportBase::Close() {
|
||||
::shutdown(handle_bind_, SD_BOTH);
|
||||
CloseSocket(handle_bind_);
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
void TransportBase::Disconnect() {
|
||||
if (handle_accept_ != InvalidSocket) {
|
||||
// Shutdown the connection in both directions. This should
|
||||
// always succeed, and nothing we can do if this fails.
|
||||
::shutdown(handle_accept_, SD_BOTH);
|
||||
|
||||
CloseSocket(handle_accept_);
|
||||
handle_accept_ = InvalidSocket;
|
||||
}
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
|
||||
Transport::Transport(SocketHandle s) : TransportBase(s) {
|
||||
socket_event_ = WSA_INVALID_EVENT;
|
||||
faulted_thread_event_ = ::CreateEvent(NULL, TRUE, FALSE, NULL);
|
||||
if (faulted_thread_event_ == NULL) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::Transport: Failed to create event object for faulted"
|
||||
"thread\n");
|
||||
}
|
||||
}
|
||||
|
||||
Transport::~Transport() {
|
||||
if (!CloseHandle(faulted_thread_event_)) {
|
||||
TRACE_GDB_REMOTE("Transport::~Transport: Failed to close event\n");
|
||||
}
|
||||
|
||||
if (socket_event_) {
|
||||
if (!::WSACloseEvent(socket_event_)) {
|
||||
TRACE_GDB_REMOTE("Transport::~Transport: Failed to close socket event\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Transport::AcceptConnection() {
|
||||
CHECK(handle_accept_ == InvalidSocket);
|
||||
handle_accept_ = ::accept(handle_bind_, NULL, 0);
|
||||
if (handle_accept_ != InvalidSocket) {
|
||||
DisableNagleAlgorithm(handle_accept_);
|
||||
|
||||
// Create socket event
|
||||
socket_event_ = ::WSACreateEvent();
|
||||
if (socket_event_ == WSA_INVALID_EVENT) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::AcceptConnection: Failed to create socket event\n");
|
||||
}
|
||||
|
||||
// Listen for close events in order to handle them correctly.
|
||||
// Additionally listen for read readiness as WSAEventSelect sets the socket
|
||||
// to non-blocking mode.
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms738547(v=vs.85).aspx
|
||||
if (::WSAEventSelect(handle_accept_, socket_event_, FD_CLOSE | FD_READ) ==
|
||||
SOCKET_ERROR) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::AcceptConnection: Failed to bind event to socket\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Transport::ReadSomeData() {
|
||||
while (true) {
|
||||
ssize_t result =
|
||||
::recv(handle_accept_, buf_.get() + size_, kBufSize - size_, 0);
|
||||
if (result > 0) {
|
||||
size_ += result;
|
||||
return true;
|
||||
}
|
||||
if (result == 0) {
|
||||
return false; // The connection was gracefully closed.
|
||||
}
|
||||
|
||||
// WSAEventSelect sets socket to non-blocking mode. This is essential
|
||||
// for socket event notification to work, there is no workaround.
|
||||
// See remarks section at the page
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms741576(v=vs.85).aspx
|
||||
if (SocketGetLastError() == WSAEWOULDBLOCK) {
|
||||
if (::WaitForSingleObject(socket_event_, INFINITE) == WAIT_FAILED) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::ReadSomeData: Failed to wait on socket event\n");
|
||||
}
|
||||
if (!::ResetEvent(socket_event_)) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::ReadSomeData: Failed to reset socket event\n");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SocketGetLastError() != kErrInterrupt) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Transport::WaitForDebugStubEvent() {
|
||||
// Don't wait if we already have data to read.
|
||||
bool wait = !(pos_ < size_);
|
||||
|
||||
HANDLE handles[2];
|
||||
handles[0] = faulted_thread_event_;
|
||||
handles[1] = socket_event_;
|
||||
int count = size_ < kBufSize ? 2 : 1;
|
||||
int result =
|
||||
WaitForMultipleObjects(count, handles, FALSE, wait ? INFINITE : 0);
|
||||
if (result == WAIT_OBJECT_0 + 1) {
|
||||
if (!ResetEvent(socket_event_)) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::WaitForDebugStubEvent: Failed to reset socket event\n");
|
||||
}
|
||||
return;
|
||||
} else if (result == WAIT_OBJECT_0) {
|
||||
if (!ResetEvent(faulted_thread_event_)) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::WaitForDebugStubEvent: Failed to reset event\n");
|
||||
}
|
||||
return;
|
||||
} else if (result == WAIT_TIMEOUT) {
|
||||
return;
|
||||
}
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::WaitForDebugStubEvent: Wait for events failed\n");
|
||||
}
|
||||
|
||||
bool Transport::SignalThreadEvent() {
|
||||
if (!SetEvent(faulted_thread_event_)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Transport::Disconnect() {
|
||||
TransportBase::Disconnect();
|
||||
|
||||
if (socket_event_ != WSA_INVALID_EVENT && !::WSACloseEvent(socket_event_)) {
|
||||
TRACE_GDB_REMOTE("Transport::~Transport: Failed to close socket event\n");
|
||||
}
|
||||
socket_event_ = WSA_INVALID_EVENT;
|
||||
SignalThreadEvent();
|
||||
}
|
||||
|
||||
#else // _WIN32
|
||||
|
||||
Transport::Transport(SocketHandle s) : TransportBase(s) {
|
||||
int fds[2];
|
||||
#if defined(__linux__)
|
||||
int ret = pipe2(fds, O_CLOEXEC);
|
||||
#else
|
||||
int ret = pipe(fds);
|
||||
#endif
|
||||
if (ret < 0) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::Transport: Failed to allocate pipe for faulted thread\n");
|
||||
}
|
||||
faulted_thread_fd_read_ = fds[0];
|
||||
faulted_thread_fd_write_ = fds[1];
|
||||
}
|
||||
|
||||
Transport::~Transport() {
|
||||
if (close(faulted_thread_fd_read_) != 0) {
|
||||
TRACE_GDB_REMOTE("Transport::~Transport: Failed to close event\n");
|
||||
}
|
||||
if (close(faulted_thread_fd_write_) != 0) {
|
||||
TRACE_GDB_REMOTE("Transport::~Transport: Failed to close event\n");
|
||||
}
|
||||
}
|
||||
|
||||
bool Transport::AcceptConnection() {
|
||||
CHECK(handle_accept_ == InvalidSocket);
|
||||
handle_accept_ = ::accept(handle_bind_, NULL, 0);
|
||||
if (handle_accept_ != InvalidSocket) {
|
||||
DisableNagleAlgorithm(handle_accept_);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Transport::ReadSomeData() {
|
||||
while (true) {
|
||||
ssize_t result =
|
||||
::recv(handle_accept_, buf_.get() + size_, kBufSize - size_, 0);
|
||||
if (result > 0) {
|
||||
size_ += result;
|
||||
return true;
|
||||
}
|
||||
if (result == 0) {
|
||||
return false; // The connection was gracefully closed.
|
||||
}
|
||||
if (SocketGetLastError() != kErrInterrupt) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Transport::WaitForDebugStubEvent() {
|
||||
// Don't wait if we already have data to read.
|
||||
bool wait = !(pos_ < size_);
|
||||
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(faulted_thread_fd_read_, &fds);
|
||||
int max_fd = faulted_thread_fd_read_;
|
||||
if (size_ < kBufSize) {
|
||||
FD_SET(handle_accept_, &fds);
|
||||
max_fd = std::max(max_fd, handle_accept_);
|
||||
}
|
||||
|
||||
int ret;
|
||||
// We don't need sleep-polling on Linux now, so we set either zero or infinite
|
||||
// timeout.
|
||||
if (wait) {
|
||||
ret = select(max_fd + 1, &fds, NULL, NULL, NULL);
|
||||
} else {
|
||||
struct timeval timeout;
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = 0;
|
||||
ret = select(max_fd + 1, &fds, NULL, NULL, &timeout);
|
||||
}
|
||||
if (ret < 0) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::WaitForDebugStubEvent: Failed to wait for "
|
||||
"debug stub event\n");
|
||||
}
|
||||
|
||||
if (ret > 0) {
|
||||
if (FD_ISSET(faulted_thread_fd_read_, &fds)) {
|
||||
char buf[16];
|
||||
if (read(faulted_thread_fd_read_, &buf, sizeof(buf)) < 0) {
|
||||
TRACE_GDB_REMOTE(
|
||||
"Transport::WaitForDebugStubEvent: Failed to read from "
|
||||
"debug stub event pipe fd\n");
|
||||
}
|
||||
}
|
||||
if (FD_ISSET(handle_accept_, &fds)) ReadSomeData();
|
||||
}
|
||||
}
|
||||
|
||||
bool Transport::SignalThreadEvent() {
|
||||
// Notify the debug stub by marking the thread as faulted.
|
||||
char buf = 0;
|
||||
if (write(faulted_thread_fd_write_, &buf, sizeof(buf)) != sizeof(buf)) {
|
||||
TRACE_GDB_REMOTE("SignalThreadEvent: Can't send debug stub event\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
||||
#undef SD_BOTH
|
183
src/debug/wasm/gdb-server/transport.h
Normal file
183
src/debug/wasm/gdb-server/transport.h
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef V8_DEBUG_WASM_GDB_SERVER_TRANSPORT_H_
|
||||
#define V8_DEBUG_WASM_GDB_SERVER_TRANSPORT_H_
|
||||
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include "src/base/macros.h"
|
||||
#include "src/debug/wasm/gdb-server/util.h"
|
||||
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
#include <winsock2.h>
|
||||
|
||||
typedef SOCKET SocketHandle;
|
||||
|
||||
#define CloseSocket closesocket
|
||||
#define InvalidSocket INVALID_SOCKET
|
||||
#define SocketGetLastError() WSAGetLastError()
|
||||
static const int kErrInterrupt = WSAEINTR;
|
||||
typedef int ssize_t;
|
||||
typedef int socklen_t;
|
||||
|
||||
#else // _WIN32
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <string>
|
||||
|
||||
typedef int SocketHandle;
|
||||
|
||||
#define CloseSocket close
|
||||
#define InvalidSocket (-1)
|
||||
#define SocketGetLastError() errno
|
||||
static const int kErrInterrupt = EINTR;
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
class Transport;
|
||||
|
||||
// Acts as a factory for Transport objects bound to a specified TCP port.
|
||||
class SocketBinding {
|
||||
public:
|
||||
// Wrap existing socket handle.
|
||||
explicit SocketBinding(SocketHandle socket_handle);
|
||||
|
||||
// Bind to the specified TCP port.
|
||||
static SocketBinding Bind(uint16_t tcp_port);
|
||||
|
||||
bool IsValid() const { return socket_handle_ != InvalidSocket; }
|
||||
|
||||
// Create a transport object from this socket binding
|
||||
std::unique_ptr<Transport> CreateTransport();
|
||||
|
||||
// Get port the socket is bound to.
|
||||
uint16_t GetBoundPort();
|
||||
|
||||
private:
|
||||
SocketHandle socket_handle_;
|
||||
};
|
||||
|
||||
class TransportBase {
|
||||
public:
|
||||
explicit TransportBase(SocketHandle s);
|
||||
virtual ~TransportBase();
|
||||
|
||||
// Read {len} bytes from this transport, possibly blocking until enough data
|
||||
// is available.
|
||||
// {dst} must point to a buffer large enough to contain {len} bytes.
|
||||
// Returns true on success.
|
||||
// Returns false if the connection is closed; in that case the {dst} may have
|
||||
// been partially overwritten.
|
||||
bool Read(char* dst, int32_t len);
|
||||
|
||||
// Write {len} bytes to this transport.
|
||||
// Return true on success, false if the connection is closed.
|
||||
bool Write(const char* src, int32_t len);
|
||||
|
||||
// Return true if there is data to read.
|
||||
bool IsDataAvailable() const;
|
||||
|
||||
// Shuts down this transport, gracefully closing the existing connection and
|
||||
// also closing the listening socket. This should be called when the GDB stub
|
||||
// shuts down, when the program terminates.
|
||||
void Close();
|
||||
|
||||
// If a socket connection with a debugger is present, gracefully closes it.
|
||||
// This should be called when a debugging session gets closed.
|
||||
virtual void Disconnect();
|
||||
|
||||
protected:
|
||||
// Copy buffered data to *dst up to len bytes and update dst and len.
|
||||
void CopyFromBuffer(char** dst, int32_t* len);
|
||||
|
||||
// Read available data from the socket. Return false on EOF or error.
|
||||
virtual bool ReadSomeData() = 0;
|
||||
|
||||
static const int kBufSize = 4096;
|
||||
std::unique_ptr<char[]> buf_;
|
||||
int32_t pos_;
|
||||
int32_t size_;
|
||||
SocketHandle handle_bind_;
|
||||
SocketHandle handle_accept_;
|
||||
};
|
||||
|
||||
#if _WIN32
|
||||
|
||||
class Transport : public TransportBase {
|
||||
public:
|
||||
explicit Transport(SocketHandle s);
|
||||
~Transport() override;
|
||||
|
||||
// Waits for an incoming connection on the bound port.
|
||||
bool AcceptConnection();
|
||||
|
||||
// Blocks waiting for one of these two events to occur:
|
||||
// - A network event (a new packet arrives, or the connection is dropped),
|
||||
// - A thread event is signaled (the execution stopped because of a trap or
|
||||
// breakpoint).
|
||||
void WaitForDebugStubEvent();
|
||||
|
||||
// Signal that this transport should leave an alertable wait state because
|
||||
// the execution of the debuggee was stopped because of a trap or breakpoint.
|
||||
bool SignalThreadEvent();
|
||||
|
||||
void Disconnect() override;
|
||||
|
||||
private:
|
||||
bool ReadSomeData() override;
|
||||
|
||||
HANDLE socket_event_;
|
||||
HANDLE faulted_thread_event_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(Transport);
|
||||
};
|
||||
|
||||
#else // _WIN32
|
||||
|
||||
class Transport : public TransportBase {
|
||||
public:
|
||||
explicit Transport(SocketHandle s);
|
||||
~Transport() override;
|
||||
|
||||
// Waits for an incoming connection on the bound port.
|
||||
bool AcceptConnection();
|
||||
|
||||
// Blocks waiting for one of these two events to occur:
|
||||
// - A network event (a new packet arrives, or the connection is dropped),
|
||||
// - The debuggee suspends execution because of a trap or breakpoint.
|
||||
void WaitForDebugStubEvent();
|
||||
|
||||
// Signal that this transport should leave an alertable wait state because
|
||||
// the execution of the debuggee was stopped because of a trap or breakpoint.
|
||||
bool SignalThreadEvent();
|
||||
|
||||
private:
|
||||
bool ReadSomeData() override;
|
||||
|
||||
int faulted_thread_fd_read_;
|
||||
int faulted_thread_fd_write_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(Transport);
|
||||
};
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
||||
#endif // V8_DEBUG_WASM_GDB_SERVER_TRANSPORT_H_
|
27
src/debug/wasm/gdb-server/util.h
Normal file
27
src/debug/wasm/gdb-server/util.h
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef V8_DEBUG_WASM_GDB_SERVER_UTIL_H_
|
||||
#define V8_DEBUG_WASM_GDB_SERVER_UTIL_H_
|
||||
|
||||
#include <string>
|
||||
#include "src/flags/flags.h"
|
||||
#include "src/utils/utils.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
namespace gdb_server {
|
||||
|
||||
#define TRACE_GDB_REMOTE(...) \
|
||||
do { \
|
||||
if (FLAG_trace_wasm_gdb_remote) PrintF("[gdb-remote] " __VA_ARGS__); \
|
||||
} while (false)
|
||||
|
||||
} // namespace gdb_server
|
||||
} // namespace wasm
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
||||
#endif // V8_DEBUG_WASM_GDB_SERVER_UTIL_H_
|
@ -1392,6 +1392,18 @@ DEFINE_BOOL(multi_mapped_mock_allocator, false,
|
||||
"Use a multi-mapped mock ArrayBuffer allocator for testing.")
|
||||
#endif
|
||||
|
||||
// Flags for Wasm GDB remote debugging.
|
||||
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
#define DEFAULT_WASM_GDB_REMOTE_PORT 8765
|
||||
DEFINE_BOOL(wasm_gdb_remote, false,
|
||||
"enable GDB-remote for WebAssembly debugging")
|
||||
DEFINE_INT(wasm_gdb_remote_port, DEFAULT_WASM_GDB_REMOTE_PORT,
|
||||
"default port for WebAssembly debugging with LLDB.")
|
||||
DEFINE_BOOL(wasm_pause_waiting_for_debugger, false,
|
||||
"pause at the first Webassembly instruction waiting for a debugger "
|
||||
"to attach")
|
||||
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
|
||||
//
|
||||
// GDB JIT integration flags.
|
||||
//
|
||||
@ -1477,6 +1489,10 @@ DEFINE_BOOL(print_break_location, false, "print source location on debug break")
|
||||
DEFINE_DEBUG_BOOL(trace_wasm_instances, false,
|
||||
"trace creation and collection of wasm instances")
|
||||
|
||||
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
DEFINE_BOOL(trace_wasm_gdb_remote, false, "trace Webassembly GDB-remote server")
|
||||
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
|
||||
//
|
||||
// Logging and profiling flags
|
||||
//
|
||||
|
@ -23,6 +23,10 @@
|
||||
#include "src/wasm/streaming-decoder.h"
|
||||
#include "src/wasm/wasm-objects-inl.h"
|
||||
|
||||
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
#include "src/debug/wasm/gdb-server/gdb-server.h"
|
||||
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace wasm {
|
||||
@ -358,6 +362,11 @@ struct WasmEngine::NativeModuleInfo {
|
||||
WasmEngine::WasmEngine() : code_manager_(FLAG_wasm_max_code_space * MB) {}
|
||||
|
||||
WasmEngine::~WasmEngine() {
|
||||
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
// Synchronize on the GDB-remote thread, if running.
|
||||
gdb_server_ = nullptr;
|
||||
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
|
||||
// Synchronize on all background compile tasks.
|
||||
background_compile_task_manager_.CancelAndWait();
|
||||
// All AsyncCompileJobs have been canceled.
|
||||
@ -838,6 +847,12 @@ void WasmEngine::LogOutstandingCodesForIsolate(Isolate* isolate) {
|
||||
std::shared_ptr<NativeModule> WasmEngine::NewNativeModule(
|
||||
Isolate* isolate, const WasmFeatures& enabled,
|
||||
std::shared_ptr<const WasmModule> module, size_t code_size_estimate) {
|
||||
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
if (FLAG_wasm_gdb_remote && !gdb_server_) {
|
||||
gdb_server_ = std::make_unique<gdb_server::GdbServer>();
|
||||
}
|
||||
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
|
||||
std::shared_ptr<NativeModule> native_module = code_manager_.NewNativeModule(
|
||||
this, isolate, enabled, code_size_estimate, std::move(module));
|
||||
base::MutexGuard lock(&mutex_);
|
||||
|
@ -36,6 +36,10 @@ class ErrorThrower;
|
||||
struct ModuleWireBytes;
|
||||
class WasmFeatures;
|
||||
|
||||
namespace gdb_server {
|
||||
class GdbServer;
|
||||
}
|
||||
|
||||
class V8_EXPORT_PRIVATE CompilationResultResolver {
|
||||
public:
|
||||
virtual void OnCompilationSucceeded(Handle<WasmModuleObject> result) = 0;
|
||||
@ -367,6 +371,11 @@ class V8_EXPORT_PRIVATE WasmEngine {
|
||||
// engine, they must all be finished because they access the allocator.
|
||||
CancelableTaskManager background_compile_task_manager_;
|
||||
|
||||
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
// Implements a GDB-remote stub for WebAssembly debugging.
|
||||
std::unique_ptr<gdb_server::GdbServer> gdb_server_;
|
||||
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
|
||||
|
||||
// This mutex protects all information which is mutated concurrently or
|
||||
// fields that are initialized lazily on the first access.
|
||||
base::Mutex mutex_;
|
||||
|
5
test/debugging/debugging.status
Normal file
5
test/debugging/debugging.status
Normal file
@ -0,0 +1,5 @@
|
||||
# Copyright 2019 the V8 project authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
[]
|
99
test/debugging/testcfg.py
Normal file
99
test/debugging/testcfg.py
Normal file
@ -0,0 +1,99 @@
|
||||
# Copyright 2019 the V8 project authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
from testrunner.local import testsuite
|
||||
from testrunner.local import utils
|
||||
from testrunner.objects import testcase
|
||||
from testrunner.outproc import message
|
||||
|
||||
PY_FLAGS_PATTERN = re.compile(r"#\s+Flags:(.*)")
|
||||
|
||||
class PYTestCase(testcase.TestCase):
|
||||
|
||||
def get_shell(self):
|
||||
return os.path.splitext(sys.executable)[0]
|
||||
|
||||
def get_command(self):
|
||||
return super(PYTestCase, self).get_command()
|
||||
|
||||
def _get_cmd_params(self):
|
||||
return self._get_files_params() + ['--', os.path.join(self._test_config.shell_dir, 'd8')] + self._get_source_flags()
|
||||
|
||||
def _get_shell_flags(self):
|
||||
return []
|
||||
|
||||
|
||||
class TestCase(PYTestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestCase, self).__init__(*args, **kwargs)
|
||||
|
||||
source = self.get_source()
|
||||
self._source_files = self._parse_source_files(source)
|
||||
self._source_flags = self._parse_source_flags(source)
|
||||
|
||||
def _parse_source_files(self, source):
|
||||
files = []
|
||||
files.append(self._get_source_path())
|
||||
return files
|
||||
|
||||
def _parse_source_flags(self, source=None):
|
||||
source = source or self.get_source()
|
||||
flags = []
|
||||
for match in re.findall(PY_FLAGS_PATTERN, source):
|
||||
flags += shlex.split(match.strip())
|
||||
return flags
|
||||
|
||||
def _expected_fail(self):
|
||||
path = self.path
|
||||
while path:
|
||||
head, tail = os.path.split(path)
|
||||
if tail == 'fail':
|
||||
return True
|
||||
path = head
|
||||
return False
|
||||
|
||||
def _get_files_params(self):
|
||||
return self._source_files
|
||||
|
||||
def _get_source_flags(self):
|
||||
return self._source_flags
|
||||
|
||||
def _get_source_path(self):
|
||||
base_path = os.path.join(self.suite.root, self.path)
|
||||
if os.path.exists(base_path + self._get_suffix()):
|
||||
return base_path + self._get_suffix()
|
||||
return base_path + '.py'
|
||||
|
||||
def skip_predictable(self):
|
||||
return super(TestCase, self).skip_predictable() or self._expected_fail()
|
||||
|
||||
|
||||
class PYTestLoader(testsuite.GenericTestLoader):
|
||||
|
||||
@property
|
||||
def excluded_files(self):
|
||||
return {'gdb_rsp.py', 'testcfg.py', '__init__.py'}
|
||||
|
||||
@property
|
||||
def extensions(self):
|
||||
return ['.py']
|
||||
|
||||
|
||||
class TestSuite(testsuite.TestSuite):
|
||||
|
||||
def _test_loader_class(self):
|
||||
return PYTestLoader
|
||||
|
||||
def _test_class(self):
|
||||
return TestCase
|
||||
|
||||
|
||||
def GetSuite(*args, **kwargs):
|
||||
return TestSuite(*args, **kwargs)
|
41
test/debugging/wasm/gdb-server/connect.py
Normal file
41
test/debugging/wasm/gdb-server/connect.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Copyright 2019 the V8 project authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
# Flags: -expose-wasm --wasm_gdb_remote --wasm-pause-waiting-for-debugger --wasm-interpret-all test/debugging/wasm/gdb-server/test_files/test.js
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
import sys
|
||||
import gdb_rsp
|
||||
|
||||
# These are set up by Main().
|
||||
COMMAND = None
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_disconnect(self):
|
||||
process = gdb_rsp.PopenDebugStub(COMMAND)
|
||||
try:
|
||||
# Connect and record the instruction pointer.
|
||||
connection = gdb_rsp.GdbRspConnection()
|
||||
connection.Close()
|
||||
# Reconnect 3 times.
|
||||
for _ in range(3):
|
||||
connection = gdb_rsp.GdbRspConnection()
|
||||
connection.Close()
|
||||
finally:
|
||||
gdb_rsp.KillProcess(process)
|
||||
|
||||
|
||||
def Main():
|
||||
index = sys.argv.index('--')
|
||||
args = sys.argv[index + 1:]
|
||||
# The remaining arguments go to unittest.main().
|
||||
global COMMAND
|
||||
COMMAND = args
|
||||
unittest.main(argv=sys.argv[:index])
|
||||
|
||||
if __name__ == '__main__':
|
||||
Main()
|
73
test/debugging/wasm/gdb-server/gdb_rsp.py
Normal file
73
test/debugging/wasm/gdb-server/gdb_rsp.py
Normal file
@ -0,0 +1,73 @@
|
||||
# Copyright 2019 the V8 project authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
SOCKET_ADDR = ('localhost', 8765)
|
||||
|
||||
|
||||
def EnsurePortIsAvailable(addr=SOCKET_ADDR):
|
||||
# As a sanity check, check that the TCP port is available by binding to it
|
||||
# ourselves (and then unbinding). Otherwise, we could end up talking to an
|
||||
# old instance of the GDB stub that is still hanging around, or to some
|
||||
# unrelated service that uses the same port number. Of course, there is still
|
||||
# a race condition because an unrelated process could bind the port after we
|
||||
# unbind.
|
||||
sock = socket.socket()
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
|
||||
sock.bind(addr)
|
||||
sock.close()
|
||||
|
||||
|
||||
class GdbRspConnection(object):
|
||||
|
||||
def __init__(self, addr=SOCKET_ADDR):
|
||||
self._socket = self._Connect(addr)
|
||||
|
||||
def _Connect(self, addr):
|
||||
# We have to poll because we do not know when the GDB stub has successfully
|
||||
# done bind() on the TCP port. This is inherently unreliable.
|
||||
timeout_in_seconds = 10
|
||||
poll_time_in_seconds = 0.1
|
||||
for i in xrange(int(timeout_in_seconds / poll_time_in_seconds)):
|
||||
# On Mac OS X, we have to create a new socket FD for each retry.
|
||||
sock = socket.socket()
|
||||
# Do not delay sending small packets. This significantly speeds up debug
|
||||
# stub tests.
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
|
||||
try:
|
||||
sock.connect(addr)
|
||||
except socket.error:
|
||||
# Retry after a delay.
|
||||
time.sleep(poll_time_in_seconds)
|
||||
else:
|
||||
return sock
|
||||
raise Exception('Could not connect to the debug stub in %i seconds'
|
||||
% timeout_in_seconds)
|
||||
|
||||
def Close(self):
|
||||
self._socket.close()
|
||||
|
||||
|
||||
def PopenDebugStub(command):
|
||||
EnsurePortIsAvailable()
|
||||
return subprocess.Popen(command)
|
||||
|
||||
|
||||
def KillProcess(process):
|
||||
if process.returncode is not None:
|
||||
# kill() won't work if we've already wait()'ed on the process.
|
||||
return
|
||||
try:
|
||||
process.kill()
|
||||
except OSError:
|
||||
if sys.platform == 'win32':
|
||||
# If process is already terminated, kill() throws
|
||||
# "WindowsError: [Error 5] Access is denied" on Windows.
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
process.wait()
|
33
test/debugging/wasm/gdb-server/test_files/test.js
Normal file
33
test/debugging/wasm/gdb-server/test_files/test.js
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
// Copyright 2019 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
load("test/mjsunit/wasm/wasm-module-builder.js");
|
||||
|
||||
var builder = new WasmModuleBuilder();
|
||||
builder.addFunction('mul', kSig_i_ii)
|
||||
// input is 2 args of type int and output is int
|
||||
.addBody([
|
||||
kExprLocalGet, 0, // local.get i0
|
||||
kExprLocalGet, 1, // local.get i1
|
||||
kExprI32Mul]) // i32.sub i0 i1
|
||||
.exportFunc();
|
||||
|
||||
const instance = builder.instantiate();
|
||||
const wasm_f = instance.exports.mul;
|
||||
|
||||
function f() {
|
||||
var result = wasm_f(21, 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
let val = 0;
|
||||
while (true) {
|
||||
val += f();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
print('*exception:* ' + e);
|
||||
}
|
Loading…
Reference in New Issue
Block a user