Wasm debugging with LLDB: access Wasm engine state

This changelist makes the GDB-stub actually execute GDB-remote commands, by
accessing the Wasm engine state. More precisely:
- class GdbServer registers DebugDelegates that receive debug notifications when
  a new Wasm module is loaded, when execution suspends at a breakpoint or for an
  unhandled exception.
- Since the GDB-remote commands arrive on a separate thread, all
  queries from the debugger are transformed into Task objects, that are posted
  into a TaskRunner that runs in the Isolate thread.
- class WasmModuleDebug contains the logic to retrieve the value of globals, locals, memory ranges from the
  Wasm engine and to add/remove breakpoints.

Build with: v8_enable_wasm_gdb_remote_debugging = true
Run with: --wasm-gdb-remote
Test with: python tools\run-tests.py --outdir=out\debug_x64 debugging -j 1

Bug: chromium:1010467
Change-Id: I9703894620a027d3c920926db92e2ff809d84ab8
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1941139
Reviewed-by: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: Clemens Backes <clemensb@chromium.org>
Commit-Queue: Paolo Severini <paolosev@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#67412}
This commit is contained in:
Paolo Severini 2020-04-24 15:41:16 -07:00 committed by Commit Bot
parent 97a4b795be
commit 74e9318689
34 changed files with 2100 additions and 141 deletions

View File

@ -3150,6 +3150,8 @@ v8_source_set("v8_base_without_compiler") {
"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/wasm-module-debug.cc",
"src/debug/wasm/gdb-server/wasm-module-debug.h",
]
}

View File

@ -73,7 +73,7 @@ std::vector<std::string> StringSplit(const string& instr, const char* delim) {
// If we still have something to process
if (*in) {
const char* start = in;
int len = 0;
size_t len = 0;
// Keep moving forward for all valid chars
while (*in && (strchr(delim, *in) == nullptr)) {
len++;

View File

@ -31,21 +31,20 @@ bool HexToUInt8(const char chars[2], uint8_t* byte);
// input char is unexpected.
bool NibbleToUInt8(char ch, uint8_t* byte);
std::vector<std::string> V8_EXPORT_PRIVATE StringSplit(const std::string& instr,
const char* delim);
// Convert the memory pointed to by {mem} into a hex string in GDB-remote
// format.
std::string Mem2Hex(const uint8_t* mem, size_t count);
std::string Mem2Hex(const std::string& str);
std::vector<std::string> V8_EXPORT_PRIVATE StringSplit(const std::string& instr,
const char* delim);
// For LLDB debugging, an address in a Wasm module code space is represented
// with 64 bits, where the first 32 bits identify the module id:
//
// 63 32 0
// +---------------+---------------+
// | module_id | offset |
// +---------------+---------------+
// +--------------------+--------------------+
// | module_id | offset |
// +--------------------+--------------------+
// <----- 32 bit -----> <----- 32 bit ----->
class wasm_addr_t {
public:
wasm_addr_t(uint32_t module_id, uint32_t offset)

View File

@ -34,6 +34,8 @@ class GdbServerThread : public v8::base::Thread {
// closes any active debugging session.
void Stop();
Target& GetTarget() { return *target_; }
private:
void CleanupThread();

View File

@ -4,19 +4,124 @@
#include "src/debug/wasm/gdb-server/gdb-server.h"
#include <inttypes.h>
#include <functional>
#include "src/api/api-inl.h"
#include "src/api/api.h"
#include "src/debug/debug.h"
#include "src/debug/wasm/gdb-server/gdb-server-thread.h"
#include "src/wasm/wasm-engine.h"
#include "src/utils/locked-queue-inl.h"
namespace v8 {
namespace internal {
namespace wasm {
namespace gdb_server {
static const uint32_t kMaxWasmCallStack = 20;
// A TaskRunner is an object that runs posted tasks (in the form of closure
// objects). Tasks are queued and run, in order, in the thread where the
// TaskRunner::RunMessageLoop() is called.
class TaskRunner {
public:
// Class Task wraps a std::function with a semaphore to signal its completion.
// This logic would be neatly implemented with std::packaged_tasks but we
// cannot use <future> in V8.
class Task {
public:
Task(base::Semaphore* ready_semaphore, std::function<void()> func)
: ready_semaphore_(ready_semaphore), func_(func) {}
void Run() {
func_();
ready_semaphore_->Signal();
}
// A semaphore object passed by the thread that posts a task.
// The sender can Wait on this semaphore to block until the task has
// completed execution in the TaskRunner thread.
base::Semaphore* ready_semaphore_;
// The function to run.
std::function<void()> func_;
};
TaskRunner()
: process_queue_semaphore_(0),
nested_loop_count_(0),
is_terminated_(false) {}
// Starts the task runner. All tasks posted are run, in order, in the thread
// that calls this function.
void Run() {
is_terminated_ = false;
int loop_number = ++nested_loop_count_;
while (nested_loop_count_ == loop_number && !is_terminated_) {
std::shared_ptr<Task> task = GetNext();
if (task) {
task->Run();
}
}
}
// Terminates the task runner. Tasks that are still pending in the queue are
// not discarded and will be executed when the task runner is restarted.
void Terminate() {
DCHECK_LT(0, nested_loop_count_);
--nested_loop_count_;
is_terminated_ = true;
process_queue_semaphore_.Signal();
}
// Posts a task to the task runner, to be executed in the task runner thread.
template <typename Functor>
auto Append(base::Semaphore* ready_semaphore, Functor&& task) {
queue_.Enqueue(std::make_shared<Task>(ready_semaphore, task));
process_queue_semaphore_.Signal();
}
private:
std::shared_ptr<Task> GetNext() {
while (!is_terminated_) {
if (queue_.IsEmpty()) {
process_queue_semaphore_.Wait();
}
std::shared_ptr<Task> task;
if (queue_.Dequeue(&task)) {
return task;
}
}
return nullptr;
}
LockedQueue<std::shared_ptr<Task>> queue_;
v8::base::Semaphore process_queue_semaphore_;
int nested_loop_count_;
std::atomic<bool> is_terminated_;
DISALLOW_COPY_AND_ASSIGN(TaskRunner);
};
GdbServer::GdbServer() { task_runner_ = std::make_unique<TaskRunner>(); }
template <typename Functor>
auto GdbServer::RunSyncTask(Functor&& callback) const {
// Executed in the GDBServerThread.
v8::base::Semaphore ready_semaphore(0);
task_runner_->Append(&ready_semaphore, callback);
ready_semaphore.Wait();
}
// static
std::unique_ptr<GdbServer> GdbServer::Create() {
DCHECK(FLAG_wasm_gdb_remote);
std::unique_ptr<GdbServer> gdb_server(new GdbServer());
// Spawns the GDB-stub thread where all the communication with the debugger
// happens.
gdb_server->thread_ = std::make_unique<GdbServerThread>(gdb_server.get());
if (!gdb_server->thread_->StartAndInitialize()) {
TRACE_GDB_REMOTE(
@ -27,57 +132,290 @@ std::unique_ptr<GdbServer> GdbServer::Create() {
}
GdbServer::~GdbServer() {
// All Isolates have been deregistered.
DCHECK(isolate_delegates_.empty());
if (thread_) {
// Waits for the GDB-stub thread to terminate.
thread_->Stop();
thread_->Join();
}
}
// All the following methods require interaction with the actual V8 Wasm engine.
// They will be implemented later.
void GdbServer::RunMessageLoopOnPause() { task_runner_->Run(); }
std::vector<GdbServer::WasmModuleInfo> GdbServer::GetLoadedModules() const {
// TODO(paolosev)
return {};
void GdbServer::QuitMessageLoopOnPause() { task_runner_->Terminate(); }
std::vector<GdbServer::WasmModuleInfo> GdbServer::GetLoadedModules() {
// Executed in the GDBServerThread.
std::vector<GdbServer::WasmModuleInfo> modules;
RunSyncTask([this, &modules]() {
// Executed in the isolate thread.
for (const auto& pair : scripts_) {
uint32_t module_id = pair.first;
const WasmModuleDebug& module_debug = pair.second;
modules.push_back({module_id, module_debug.GetModuleName()});
}
});
return modules;
}
bool GdbServer::GetWasmGlobal(uint32_t wasm_module_id, uint32_t index,
uint64_t* value) {
// TODO(paolosev)
bool GdbServer::GetModuleDebugHandler(uint32_t module_id,
WasmModuleDebug** wasm_module_debug) {
// Always executed in the isolate thread.
ScriptsMap::iterator scriptIterator = scripts_.find(module_id);
if (scriptIterator != scripts_.end()) {
*wasm_module_debug = &scriptIterator->second;
return true;
}
wasm_module_debug = nullptr;
return false;
}
bool GdbServer::GetWasmLocal(uint32_t wasm_module_id, uint32_t frame_index,
uint32_t index, uint64_t* value) {
// TODO(paolosev)
return false;
bool GdbServer::GetWasmGlobal(uint32_t frame_index, uint32_t index,
uint8_t* buffer, uint32_t buffer_size,
uint32_t* size) {
// Executed in the GDBServerThread.
bool result = false;
RunSyncTask([this, &result, frame_index, index, buffer, buffer_size, size]() {
// Executed in the isolate thread.
result = WasmModuleDebug::GetWasmGlobal(GetTarget().GetCurrentIsolate(),
frame_index, index, buffer,
buffer_size, size);
});
return result;
}
uint32_t GdbServer::GetWasmMemory(uint32_t wasm_module_id, uint32_t offset,
bool GdbServer::GetWasmLocal(uint32_t frame_index, uint32_t index,
uint8_t* buffer, uint32_t buffer_size,
uint32_t* size) {
// Executed in the GDBServerThread.
bool result = false;
RunSyncTask([this, &result, frame_index, index, buffer, buffer_size, size]() {
// Executed in the isolate thread.
result = WasmModuleDebug::GetWasmLocal(GetTarget().GetCurrentIsolate(),
frame_index, index, buffer,
buffer_size, size);
});
return result;
}
bool GdbServer::GetWasmStackValue(uint32_t frame_index, uint32_t index,
uint8_t* buffer, uint32_t buffer_size,
uint32_t* size) {
// Executed in the GDBServerThread.
bool result = false;
RunSyncTask([this, &result, frame_index, index, buffer, buffer_size, size]() {
// Executed in the isolate thread.
result = WasmModuleDebug::GetWasmStackValue(GetTarget().GetCurrentIsolate(),
frame_index, index, buffer,
buffer_size, size);
});
return result;
}
uint32_t GdbServer::GetWasmMemory(uint32_t frame_index, uint32_t offset,
uint8_t* buffer, uint32_t size) {
// TODO(paolosev)
return 0;
// Executed in the GDBServerThread.
uint32_t bytes_read = 0;
RunSyncTask([this, &bytes_read, frame_index, offset, buffer, size]() {
// Executed in the isolate thread.
bytes_read = WasmModuleDebug::GetWasmMemory(
GetTarget().GetCurrentIsolate(), frame_index, offset, buffer, size);
});
return bytes_read;
}
uint32_t GdbServer::GetWasmModuleBytes(wasm_addr_t address, uint8_t* buffer,
uint32_t GdbServer::GetWasmModuleBytes(wasm_addr_t wasm_addr, uint8_t* buffer,
uint32_t size) {
// TODO(paolosev)
return 0;
// Executed in the GDBServerThread.
uint32_t bytes_read = 0;
RunSyncTask([this, &bytes_read, wasm_addr, buffer, size]() {
// Executed in the isolate thread.
WasmModuleDebug* module_debug;
if (GetModuleDebugHandler(wasm_addr.ModuleId(), &module_debug)) {
bytes_read = module_debug->GetWasmModuleBytes(wasm_addr, buffer, size);
}
});
return bytes_read;
}
bool GdbServer::AddBreakpoint(uint32_t module_id, uint32_t offset) {
// TODO(paolosev)
return false;
bool GdbServer::AddBreakpoint(uint32_t wasm_module_id, uint32_t offset) {
// Executed in the GDBServerThread.
bool result = false;
RunSyncTask([this, &result, wasm_module_id, offset]() {
// Executed in the isolate thread.
WasmModuleDebug* module_debug;
if (GetModuleDebugHandler(wasm_module_id, &module_debug)) {
int breakpoint_id = 0;
if (module_debug->AddBreakpoint(offset, &breakpoint_id)) {
breakpoints_[wasm_addr_t(wasm_module_id, offset)] = breakpoint_id;
result = true;
}
}
});
return result;
}
bool GdbServer::RemoveBreakpoint(uint32_t module_id, uint32_t offset) {
// TODO(paolosev)
return false;
bool GdbServer::RemoveBreakpoint(uint32_t wasm_module_id, uint32_t offset) {
// Executed in the GDBServerThread.
bool result = false;
RunSyncTask([this, &result, wasm_module_id, offset]() {
// Executed in the isolate thread.
BreakpointsMap::iterator it =
breakpoints_.find(wasm_addr_t(wasm_module_id, offset));
if (it != breakpoints_.end()) {
int breakpoint_id = it->second;
breakpoints_.erase(it);
WasmModuleDebug* module_debug;
if (GetModuleDebugHandler(wasm_module_id, &module_debug)) {
module_debug->RemoveBreakpoint(offset, breakpoint_id);
result = true;
}
}
});
return result;
}
std::vector<wasm_addr_t> GdbServer::GetWasmCallStack() const {
// TODO(paolosev)
return {};
// Executed in the GDBServerThread.
std::vector<wasm_addr_t> result;
RunSyncTask([this, &result]() {
// Executed in the isolate thread.
result = GetTarget().GetCallStack();
});
return result;
}
void GdbServer::AddIsolate(Isolate* isolate) {
// Executed in the isolate thread.
if (isolate_delegates_.find(isolate) == isolate_delegates_.end()) {
isolate_delegates_[isolate] =
std::make_unique<DebugDelegate>(isolate, this);
}
}
void GdbServer::RemoveIsolate(Isolate* isolate) {
// Executed in the isolate thread.
auto it = isolate_delegates_.find(isolate);
if (it != isolate_delegates_.end()) {
for (auto it = scripts_.begin(); it != scripts_.end();) {
if (it->second.GetIsolate() == isolate) {
it = scripts_.erase(it);
} else {
++it;
}
}
isolate_delegates_.erase(it);
}
}
void GdbServer::Suspend() {
// Executed in the GDBServerThread.
auto it = isolate_delegates_.begin();
if (it != isolate_delegates_.end()) {
Isolate* isolate = it->first;
v8::Isolate* v8Isolate = (v8::Isolate*)isolate;
v8Isolate->RequestInterrupt(
// Executed in the isolate thread.
[](v8::Isolate* isolate, void*) {
if (v8::debug::AllFramesOnStackAreBlackboxed(isolate)) {
v8::debug::SetBreakOnNextFunctionCall(isolate);
} else {
v8::debug::BreakRightNow(isolate);
}
},
this);
}
}
void GdbServer::PrepareStep() {
// Executed in the GDBServerThread.
wasm_addr_t pc = GetTarget().GetCurrentPc();
RunSyncTask([this, pc]() {
// Executed in the isolate thread.
WasmModuleDebug* module_debug;
if (GetModuleDebugHandler(pc.ModuleId(), &module_debug)) {
module_debug->PrepareStep();
}
});
}
void GdbServer::AddWasmModule(uint32_t module_id,
Local<debug::WasmScript> wasm_script) {
// Executed in the isolate thread.
DCHECK_EQ(Script::TYPE_WASM, Utils::OpenHandle(*wasm_script)->type());
v8::Isolate* isolate = wasm_script->GetIsolate();
scripts_.insert(
std::make_pair(module_id, WasmModuleDebug(isolate, wasm_script)));
if (FLAG_wasm_pause_waiting_for_debugger && scripts_.size() == 1) {
TRACE_GDB_REMOTE("Paused, waiting for a debugger to attach...\n");
Suspend();
}
}
Target& GdbServer::GetTarget() const { return thread_->GetTarget(); }
// static
std::atomic<uint32_t> GdbServer::DebugDelegate::id_s;
GdbServer::DebugDelegate::DebugDelegate(Isolate* isolate, GdbServer* gdb_server)
: isolate_(isolate), id_(id_s++), gdb_server_(gdb_server) {
isolate_->SetCaptureStackTraceForUncaughtExceptions(
true, kMaxWasmCallStack, v8::StackTrace::kOverview);
// Register the delegate
isolate_->debug()->SetDebugDelegate(this);
v8::debug::TierDownAllModulesPerIsolate((v8::Isolate*)isolate_);
v8::debug::ChangeBreakOnException((v8::Isolate*)isolate_,
v8::debug::BreakOnUncaughtException);
}
GdbServer::DebugDelegate::~DebugDelegate() {
// Deregister the delegate
isolate_->debug()->SetDebugDelegate(nullptr);
}
void GdbServer::DebugDelegate::ScriptCompiled(Local<debug::Script> script,
bool is_live_edited,
bool has_compile_error) {
// Executed in the isolate thread.
if (script->IsWasm()) {
DCHECK_EQ(reinterpret_cast<v8::Isolate*>(isolate_), script->GetIsolate());
gdb_server_->AddWasmModule(GetModuleId(script->Id()),
script.As<debug::WasmScript>());
}
}
void GdbServer::DebugDelegate::BreakProgramRequested(
// Executed in the isolate thread.
Local<v8::Context> paused_context,
const std::vector<debug::BreakpointId>& inspector_break_points_hit) {
gdb_server_->GetTarget().OnProgramBreak(
isolate_, WasmModuleDebug::GetCallStack(id_, isolate_));
gdb_server_->RunMessageLoopOnPause();
}
void GdbServer::DebugDelegate::ExceptionThrown(
// Executed in the isolate thread.
Local<v8::Context> paused_context, Local<Value> exception,
Local<Value> promise, bool is_uncaught,
debug::ExceptionType exception_type) {
if (exception_type == v8::debug::kException && is_uncaught) {
gdb_server_->GetTarget().OnException(
isolate_, WasmModuleDebug::GetCallStack(id_, isolate_));
gdb_server_->RunMessageLoopOnPause();
}
}
bool GdbServer::DebugDelegate::IsFunctionBlackboxed(
// Executed in the isolate thread.
Local<debug::Script> script, const debug::Location& start,
const debug::Location& end) {
return false;
}
} // namespace gdb_server

View File

@ -5,14 +5,18 @@
#ifndef V8_DEBUG_WASM_GDB_SERVER_GDB_SERVER_H_
#define V8_DEBUG_WASM_GDB_SERVER_GDB_SERVER_H_
#include <map>
#include <memory>
#include "src/debug/wasm/gdb-server/gdb-server-thread.h"
#include "src/debug/wasm/gdb-server/wasm-module-debug.h"
namespace v8 {
namespace internal {
namespace wasm {
namespace gdb_server {
class TaskRunner;
// 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.
@ -37,26 +41,35 @@ class GdbServer {
uint32_t module_id;
std::string module_name;
};
std::vector<WasmModuleInfo> GetLoadedModules() const;
std::vector<WasmModuleInfo> GetLoadedModules();
// Queries the value of the {index} global value in the Wasm module identified
// by {wasm_module_id}.
bool GetWasmGlobal(uint32_t wasm_module_id, uint32_t index, uint64_t* value);
// by {frame_index}.
//
bool GetWasmGlobal(uint32_t frame_index, uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size);
// Queries the value of the {index} local value in the {frame_index}th stack
// frame in the Wasm module identified by {wasm_module_id}.
bool GetWasmLocal(uint32_t wasm_module_id, uint32_t frame_index,
uint32_t index, uint64_t* value);
// frame in the Wasm module identified by {frame_index}.
//
bool GetWasmLocal(uint32_t frame_index, uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size);
// Queries the value of the {index} value in the operand stack.
//
bool GetWasmStackValue(uint32_t frame_index, uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size);
// Reads {size} bytes, starting from {offset}, from the Memory instance
// associated to the Wasm module identified by {wasm_module_id}.
// associated to the Wasm module identified by {frame_index}.
// Returns the number of bytes copied to {buffer}, or 0 is case of error.
// Note: only one Memory for Module is currently supported.
uint32_t GetWasmMemory(uint32_t wasm_module_id, uint32_t offset,
uint8_t* buffer, uint32_t size);
//
uint32_t GetWasmMemory(uint32_t frame_index, uint32_t offset, uint8_t* buffer,
uint32_t size);
// Reads {size} bytes, starting from {address}, from the Code space of the
// Wasm module identified by {wasm_module_id}.
// Reads {size} bytes, starting from the low dword of {address}, from the Code
// space of th Wasm module identified by high dword of {address}.
// Returns the number of bytes copied to {buffer}, or 0 is case of error.
uint32_t GetWasmModuleBytes(wasm_addr_t address, uint8_t* buffer,
uint32_t size);
@ -64,21 +77,119 @@ class GdbServer {
// Inserts a breakpoint at the offset {offset} of the Wasm module identified
// by {wasm_module_id}.
// Returns true if the breakpoint was successfully added.
bool AddBreakpoint(uint32_t module_id, uint32_t offset);
bool AddBreakpoint(uint32_t wasm_module_id, uint32_t offset);
// Removes a breakpoint at the offset {offset} of the Wasm module identified
// by {wasm_module_id}.
// Returns true if the breakpoint was successfully removed.
bool RemoveBreakpoint(uint32_t module_id, uint32_t offset);
bool RemoveBreakpoint(uint32_t wasm_module_id, uint32_t offset);
// Returns the current call stack as a vector of program counters.
std::vector<wasm_addr_t> GetWasmCallStack() const;
private:
GdbServer() {}
// Manage the set of Isolates for this GdbServer.
void AddIsolate(Isolate* isolate);
void RemoveIsolate(Isolate* isolate);
// Requests that the thread suspend execution at the next Wasm instruction.
void Suspend();
// Handle stepping in wasm functions via the wasm interpreter.
void PrepareStep();
// Called when the target debuggee can resume execution (for example after
// having been suspended on a breakpoint). Terminates the task runner leaving
// all pending tasks in the queue.
void QuitMessageLoopOnPause();
private:
GdbServer();
// When the target debuggee is suspended for a breakpoint or exception, blocks
// the main (isolate) thread and enters in a message loop. Here it waits on a
// queue of Task objects that are posted by the GDB-stub thread and that
// represent queries received from the debugger via the GDB-remote protocol.
void RunMessageLoopOnPause();
// Post a task to run a callback in the isolate thread.
template <typename Callback>
auto RunSyncTask(Callback&& callback) const;
void AddWasmModule(uint32_t module_id, Local<debug::WasmScript> wasm_script);
// Given a Wasm module id, retrieves the corresponding debugging WasmScript
// object.
bool GetModuleDebugHandler(uint32_t module_id,
WasmModuleDebug** wasm_module_debug);
// Returns the debugging target.
Target& GetTarget() const;
// Class DebugDelegate implements the debug::DebugDelegate interface to
// receive notifications when debug events happen in a given isolate, like a
// script being loaded, a breakpoint being hit, an exception being thrown.
class DebugDelegate : public debug::DebugDelegate {
public:
DebugDelegate(Isolate* isolate, GdbServer* gdb_server);
~DebugDelegate();
// debug::DebugDelegate
void ScriptCompiled(Local<debug::Script> script, bool is_live_edited,
bool has_compile_error) override;
void BreakProgramRequested(Local<v8::Context> paused_context,
const std::vector<debug::BreakpointId>&
inspector_break_points_hit) override;
void ExceptionThrown(Local<v8::Context> paused_context,
Local<Value> exception, Local<Value> promise,
bool is_uncaught,
debug::ExceptionType exception_type) override;
bool IsFunctionBlackboxed(Local<debug::Script> script,
const debug::Location& start,
const debug::Location& end) override;
private:
// Calculates module_id as:
// +--------------------+------------------- +
// | DebugDelegate::id_ | Script::Id() |
// +--------------------+------------------- +
// <----- 16 bit -----> <----- 16 bit ----->
uint32_t GetModuleId(uint32_t script_id) const {
DCHECK_LT(script_id, 0x10000);
DCHECK_LT(id_, 0x10000);
return id_ << 16 | script_id;
}
Isolate* isolate_;
uint32_t id_;
GdbServer* gdb_server_;
static std::atomic<uint32_t> id_s;
};
// The GDB-stub thread where all the communication with the debugger happens.
std::unique_ptr<GdbServerThread> thread_;
// Used to transform the queries that arrive in the GDB-stub thread into
// tasks executed in the main (isolate) thread.
std::unique_ptr<TaskRunner> task_runner_;
//////////////////////////////////////////////////////////////////////////////
// Always accessed in the isolate thread.
// Set of breakpoints currently defines in Wasm code.
typedef std::map<uint64_t, int> BreakpointsMap;
BreakpointsMap breakpoints_;
typedef std::map<uint32_t, WasmModuleDebug> ScriptsMap;
ScriptsMap scripts_;
typedef std::map<Isolate*, std::unique_ptr<DebugDelegate>>
IsolateDebugDelegateMap;
IsolateDebugDelegateMap isolate_delegates_;
// End of fields always accessed in the isolate thread.
//////////////////////////////////////////////////////////////////////////////
DISALLOW_COPY_AND_ASSIGN(GdbServer);
};

View File

@ -36,18 +36,20 @@ bool Session::GetChar(char* ch) {
return true;
}
bool Session::SendPacket(Packet* pkt) {
bool Session::SendPacket(Packet* pkt, bool expect_ack) {
char ch;
do {
std::string data = pkt->GetPacketData();
TRACE_GDB_REMOTE("TX %s\n", data.c_str());
TRACE_GDB_REMOTE("TX %s\n", data.size() < 160
? data.c_str()
: (data.substr(0, 160) + "...").c_str());
if (!io_->Write(data.data(), static_cast<int32_t>(data.length()))) {
return false;
}
// If ACKs are off, we are done.
if (!ack_enabled_) {
if (!expect_ack || !ack_enabled_) {
break;
}

View File

@ -21,7 +21,7 @@ class V8_EXPORT_PRIVATE Session {
explicit Session(TransportBase* transport);
// Attempt to send a packet and optionally wait for an ACK from the receiver.
bool SendPacket(Packet* packet);
bool SendPacket(Packet* packet, bool expect_ack = true);
// Attempt to receive a packet.
bool GetPacket(Packet* packet);

View File

@ -19,16 +19,25 @@ namespace gdb_server {
static const int kThreadId = 1;
// Signals.
static const int kSigTrace = 5;
static const int kSigSegv = 11;
Target::Target(GdbServer* gdb_server)
: gdb_server_(gdb_server),
status_(Status::Running),
cur_signal_(0),
session_(nullptr) {}
session_(nullptr),
debugger_initial_suspension_(true),
semaphore_(0),
current_isolate_(nullptr) {
InitQueryPropertyMap();
}
void Target::InitQueryPropertyMap() {
// Request LLDB to send packets up to 4000 bytes for bulk transfers.
query_properties_["Supported"] =
"PacketSize=4000;vContSupported-;qXfer:libraries:read+;";
"PacketSize=1000;vContSupported-;qXfer:libraries:read+;";
query_properties_["Attached"] = "1";
@ -51,8 +60,32 @@ void Target::InitQueryPropertyMap() {
}
void Target::Terminate() {
// Executed in the Isolate thread.
status_ = Status::Terminated;
// Executed in the Isolate thread, when the process shuts down.
SetStatus(Status::Terminated);
}
void Target::OnProgramBreak(Isolate* isolate,
const std::vector<wasm_addr_t>& call_frames) {
OnSuspended(isolate, kSigTrace, call_frames);
}
void Target::OnException(Isolate* isolate,
const std::vector<wasm_addr_t>& call_frames) {
OnSuspended(isolate, kSigSegv, call_frames);
}
void Target::OnSuspended(Isolate* isolate, int signal,
const std::vector<wasm_addr_t>& call_frames) {
// This function will be called in the isolate thread, when the wasm
// interpreter gets suspended.
bool isWaitingForSuspension = (status_ == Status::WaitingForSuspension);
SetStatus(Status::Suspended, signal, call_frames, isolate);
if (isWaitingForSuspension) {
// Wake the GdbServer thread that was blocked waiting for the Target
// to suspend.
semaphore_.Signal();
} else if (session_) {
session_->SignalThreadEvent();
}
}
void Target::Run(Session* session) {
@ -60,6 +93,7 @@ void Target::Run(Session* session) {
session_ = session;
do {
WaitForDebugEvent();
ProcessDebugEvent();
ProcessCommands();
} while (!IsTerminated() && session_->IsConnected());
session_ = nullptr;
@ -68,7 +102,7 @@ void Target::Run(Session* session) {
void Target::WaitForDebugEvent() {
// Executed in the GdbServer thread.
if (status_ != Status::Terminated) {
if (status_ == Status::Running) {
// Wait for either:
// * the thread to fault (or single-step)
// * an interrupt from LLDB
@ -76,11 +110,53 @@ void Target::WaitForDebugEvent() {
}
}
void Target::ProcessDebugEvent() {
// Executed in the GdbServer thread
if (status_ == Status::Running) {
// Blocks, waiting for the engine to suspend.
Suspend();
}
// Here, the wasm interpreter has suspended and we have updated the current
// thread info.
if (debugger_initial_suspension_) {
// First time on a connection, we don't send the signal.
// All other times, send the signal that triggered us.
debugger_initial_suspension_ = false;
} else {
Packet pktOut;
SetStopReply(&pktOut);
session_->SendPacket(&pktOut, false);
}
}
void Target::Suspend() {
// Executed in the GdbServer thread
if (status_ == Status::Running) {
// TODO(paolosev) - this only suspends the wasm interpreter.
gdb_server_->Suspend();
status_ = Status::WaitingForSuspension;
}
while (status_ == Status::WaitingForSuspension) {
if (semaphore_.WaitFor(base::TimeDelta::FromMilliseconds(500))) {
// Here the wasm interpreter is suspended.
return;
}
}
}
void Target::ProcessCommands() {
// GDB-remote messages are processed in the GDBServer thread.
if (IsTerminated()) {
return;
} else if (status_ != Status::Suspended) {
// Don't process commands if we haven't stopped.
return;
}
// Now we are ready to process commands.
@ -99,13 +175,16 @@ void Target::ProcessCommands() {
break;
case ProcessPacketResult::Continue:
DCHECK_EQ(status_, Status::Running);
// If this is a continue type command, break out of this loop.
gdb_server_->QuitMessageLoopOnPause();
return;
case ProcessPacketResult::Detach:
SetStatus(Status::Running);
session_->SendPacket(&reply);
session_->Disconnect();
cur_signal_ = 0; // // Reset the signal value
gdb_server_->QuitMessageLoopOnPause();
return;
case ProcessPacketResult::Kill:
@ -116,6 +195,10 @@ void Target::ProcessCommands() {
UNREACHABLE();
}
}
if (!session_->IsConnected()) {
debugger_initial_suspension_ = true;
}
}
Target::ProcessPacketResult Target::ProcessPacket(Packet* pkt_in,
@ -152,9 +235,8 @@ Target::ProcessPacketResult Target::ProcessPacket(Packet* pkt_in,
// IN : $c
// OUT: A Stop-reply packet is sent later, when the execution halts.
case 'c':
// TODO(paolosev) - Not implemented yet
err = ErrorCode::Failed;
break;
SetStatus(Status::Running);
return ProcessPacketResult::Continue;
// Detaches the debugger from this target
// IN : $D
@ -276,9 +358,11 @@ Target::ProcessPacketResult Target::ProcessPacket(Packet* pkt_in,
// IN : $s
// OUT: A Stop-reply packet is sent later, when the execution halts.
case 's': {
// TODO(paolosev) - Not implemented yet
err = ErrorCode::Failed;
break;
if (status_ == Status::Suspended) {
gdb_server_->PrepareStep();
SetStatus(Status::Running);
}
return ProcessPacketResult::Continue;
}
// Find out if the thread 'id' is alive.
@ -299,15 +383,14 @@ Target::ProcessPacketResult Target::ProcessPacket(Packet* pkt_in,
}
// Z: Adds a breakpoint
// IN : $Ztype,addr,kind
// IN : $Z<type>,<addr>,<kind>
// <type>: 0: sw breakpoint, 1: hw breakpoint, 2: watchpoint
// OUT: $OK (success) or $Enn (error)
case 'Z': {
// Only software breakpoints are supported.
uint64_t breakpoint_type; // 0: sw breakpoint,
// 1: hw breakpoint,
// 2: watchpoint
uint64_t breakpoint_type;
uint64_t breakpoint_address;
uint64_t breakpoint_kind; // Ignored for Wasm.
uint64_t breakpoint_kind;
// Only software breakpoints are supported.
if (!pkt_in->GetNumberSep(&breakpoint_type, 0) || breakpoint_type != 0 ||
!pkt_in->GetNumberSep(&breakpoint_address, 0) ||
!pkt_in->GetNumberSep(&breakpoint_kind, 0)) {
@ -327,7 +410,8 @@ Target::ProcessPacketResult Target::ProcessPacket(Packet* pkt_in,
}
// z: Removes a breakpoint
// IN : $ztype,addr,kind
// IN : $z<type>,<addr>,<kind>
// <type>: 0: sw breakpoint, 1: hw breakpoint, 2: watchpoint
// OUT: $OK (success) or $Enn (error)
case 'z': {
uint64_t breakpoint_type;
@ -353,8 +437,6 @@ Target::ProcessPacketResult Target::ProcessPacket(Packet* pkt_in,
// If the command is not recognized, ignore it by sending an empty reply.
default: {
std::string str;
pkt_in->GetString(&str);
TRACE_GDB_REMOTE("Unknown command: %s\n", pkt_in->GetPayload());
}
}
@ -428,25 +510,29 @@ Target::ErrorCode Target::ProcessQueryPacket(const Packet* pkt_in,
// consecutive 8-bytes blocks).
std::vector<std::string> toks = StringSplit(str, ":;");
if (toks[0] == "WasmCallStack") {
std::vector<wasm_addr_t> callStackPCs = gdb_server_->GetWasmCallStack();
pkt_out->AddBlock(
callStackPCs.data(),
static_cast<uint32_t>(sizeof(wasm_addr_t) * callStackPCs.size()));
std::vector<wasm_addr_t> call_stack_pcs = gdb_server_->GetWasmCallStack();
std::vector<uint64_t> buffer;
for (wasm_addr_t pc : call_stack_pcs) {
buffer.push_back(pc);
}
pkt_out->AddBlock(buffer.data(),
static_cast<uint32_t>(sizeof(uint64_t) * buffer.size()));
return ErrorCode::None;
}
// Get a Wasm global value in the Wasm module specified.
// IN : $qWasmGlobal:moduleId;index
// IN : $qWasmGlobal:frame_index;index
// OUT: $xx..xx
if (toks[0] == "WasmGlobal") {
if (toks.size() == 3) {
uint32_t module_id =
uint32_t frame_index =
static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
uint32_t index =
static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
uint64_t value = 0;
if (gdb_server_->GetWasmGlobal(module_id, index, &value)) {
pkt_out->AddBlock(&value, sizeof(value));
uint8_t buff[16];
uint32_t size = 0;
if (gdb_server_->GetWasmGlobal(frame_index, index, buff, 16, &size)) {
pkt_out->AddBlock(buff, size);
return ErrorCode::None;
} else {
return ErrorCode::Failed;
@ -456,19 +542,39 @@ Target::ErrorCode Target::ProcessQueryPacket(const Packet* pkt_in,
}
// Get a Wasm local value in the stack frame specified.
// IN : $qWasmLocal:moduleId;frameIndex;index
// IN : $qWasmLocal:frame_index;index
// OUT: $xx..xx
if (toks[0] == "WasmLocal") {
if (toks.size() == 4) {
uint32_t module_id =
static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
if (toks.size() == 3) {
uint32_t frame_index =
static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
uint32_t index =
static_cast<uint32_t>(strtol(toks[3].data(), nullptr, 10));
uint64_t value = 0;
if (gdb_server_->GetWasmLocal(module_id, frame_index, index, &value)) {
pkt_out->AddBlock(&value, sizeof(value));
static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
uint8_t buff[16];
uint32_t size = 0;
if (gdb_server_->GetWasmLocal(frame_index, index, buff, 16, &size)) {
pkt_out->AddBlock(buff, size);
return ErrorCode::None;
} else {
return ErrorCode::Failed;
}
}
return ErrorCode::BadFormat;
}
// Get a Wasm local from the operand stack at the index specified.
// IN : qWasmStackValue:frame_index;index
// OUT: $xx..xx
if (toks[0] == "WasmStackValue") {
if (toks.size() == 3) {
uint32_t frame_index =
static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
uint32_t index =
static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
uint8_t buff[16];
uint32_t size = 0;
if (gdb_server_->GetWasmStackValue(frame_index, index, buff, 16, &size)) {
pkt_out->AddBlock(buff, size);
return ErrorCode::None;
} else {
return ErrorCode::Failed;
@ -478,11 +584,11 @@ Target::ErrorCode Target::ProcessQueryPacket(const Packet* pkt_in,
}
// Read Wasm memory.
// IN : $qWasmMem:memId;addr;len
// IN : $qWasmMem:frame_index;addr;len
// OUT: $xx..xx
if (toks[0] == "WasmMem") {
if (toks.size() == 4) {
uint32_t module_id =
uint32_t frame_index =
static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
uint32_t address =
static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 16));
@ -493,7 +599,7 @@ Target::ErrorCode Target::ProcessQueryPacket(const Packet* pkt_in,
}
uint8_t buff[Transport::kBufSize];
uint32_t read =
gdb_server_->GetWasmMemory(module_id, address, buff, length);
gdb_server_->GetWasmMemory(frame_index, address, buff, length);
if (read > 0) {
pkt_out->AddBlock(buff, read);
return ErrorCode::None;
@ -536,7 +642,36 @@ void Target::SetStopReply(Packet* pkt_out) const {
pkt_out->AddNumberSep(kThreadId, ';');
}
wasm_addr_t Target::GetCurrentPc() const { return wasm_addr_t(0); }
void Target::SetStatus(Status status, int8_t signal,
std::vector<wasm_addr_t> call_frames, Isolate* isolate) {
v8::base::MutexGuard guard(&mutex_);
DCHECK((status == Status::Suspended && signal != 0 &&
call_frames.size() > 0 && isolate != nullptr) ||
(status != Status::Suspended && signal == 0 &&
call_frames.size() == 0 && isolate == nullptr));
current_isolate_ = isolate;
status_ = status;
cur_signal_ = signal;
call_frames_ = call_frames;
}
const std::vector<wasm_addr_t> Target::GetCallStack() const {
v8::base::MutexGuard guard(&mutex_);
return call_frames_;
}
wasm_addr_t Target::GetCurrentPc() const {
v8::base::MutexGuard guard(&mutex_);
wasm_addr_t pc{0};
if (call_frames_.size() > 0) {
pc = call_frames_[0];
}
return pc;
}
} // namespace gdb_server
} // namespace wasm

View File

@ -33,7 +33,23 @@ class Target {
void Terminate();
bool IsTerminated() const { return status_ == Status::Terminated; }
// Notifies that the debuggee thread suspended at a breakpoint.
void OnProgramBreak(Isolate* isolate,
const std::vector<wasm_addr_t>& call_frames);
// Notifies that the debuggee thread suspended because of an unhandled
// exception.
void OnException(Isolate* isolate,
const std::vector<wasm_addr_t>& call_frames);
// Returns the state at the moment of the thread suspension.
const std::vector<wasm_addr_t> GetCallStack() const;
wasm_addr_t GetCurrentPc() const;
Isolate* GetCurrentIsolate() const { return current_isolate_; }
private:
void OnSuspended(Isolate* isolate, int signal,
const std::vector<wasm_addr_t>& call_frames);
// Initializes a map used to make fast lookups when handling query packets
// that have a constant response.
void InitQueryPropertyMap();
@ -43,11 +59,15 @@ class Target {
// closed;
// - The debuggee suspends execution because of a trap or breakpoint.
void WaitForDebugEvent();
void ProcessDebugEvent();
// Processes GDB-remote packets that arrive from the debugger.
// This method should be called when the debuggee has suspended its execution.
void ProcessCommands();
// Requests that the thread suspends execution at the next Wasm instruction.
void Suspend();
enum class ErrorCode { None = 0, BadFormat = 1, BadArgs = 2, Failed = 3 };
enum class ProcessPacketResult {
@ -69,22 +89,46 @@ class Target {
// (continue), 's' (step) and '?' (query halt reason) commands.
void SetStopReply(Packet* pkt_out) const;
wasm_addr_t GetCurrentPc() const;
enum class Status { Running, WaitingForSuspension, Suspended, Terminated };
void SetStatus(Status status, int8_t signal = 0,
std::vector<wasm_addr_t> call_frames_ = {},
Isolate* isolate = nullptr);
GdbServer* gdb_server_;
enum class Status { Running, Terminated };
std::atomic<Status> status_;
// Signal being processed.
int8_t cur_signal_;
std::atomic<int8_t> cur_signal_;
Session* session_; // Session object not owned by the Target.
// Session object not owned by the Target.
Session* session_;
// Map used to make fast lookups when handling query packets.
typedef std::map<std::string, std::string> QueryPropertyMap;
QueryPropertyMap query_properties_;
bool debugger_initial_suspension_;
// Used to block waiting for suspension
v8::base::Semaphore semaphore_;
mutable v8::base::Mutex mutex_;
//////////////////////////////////////////////////////////////////////////////
// Protected by {mutex_}:
// Current isolate. This is not null only when the target is in a Suspended
// state and it is the isolate associated to the current call stack and used
// for all debugging activities.
Isolate* current_isolate_;
// Call stack when the execution is suspended.
std::vector<wasm_addr_t> call_frames_;
// End of fields protected by {mutex_}.
//////////////////////////////////////////////////////////////////////////////
DISALLOW_COPY_AND_ASSIGN(Target);
};

View File

@ -0,0 +1,393 @@
// 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/wasm-module-debug.h"
#include "src/api/api-inl.h"
#include "src/api/api.h"
#include "src/execution/frames-inl.h"
#include "src/execution/frames.h"
#include "src/objects/script.h"
#include "src/wasm/wasm-debug.h"
#include "src/wasm/wasm-value.h"
namespace v8 {
namespace internal {
namespace wasm {
namespace gdb_server {
WasmModuleDebug::WasmModuleDebug(v8::Isolate* isolate,
Local<debug::WasmScript> wasm_script) {
DCHECK_EQ(Script::TYPE_WASM, Utils::OpenHandle(*wasm_script)->type());
isolate_ = isolate;
wasm_script_ = Global<debug::WasmScript>(isolate, wasm_script);
}
std::string WasmModuleDebug::GetModuleName() const {
v8::Local<debug::WasmScript> wasm_script = wasm_script_.Get(isolate_);
v8::Local<v8::String> name;
std::string module_name;
if (wasm_script->Name().ToLocal(&name)) {
module_name = *(v8::String::Utf8Value(isolate_, name));
}
return module_name;
}
Handle<WasmInstanceObject> WasmModuleDebug::GetFirstWasmInstance() {
v8::Local<debug::WasmScript> wasm_script = wasm_script_.Get(isolate_);
Handle<Script> script = Utils::OpenHandle(*wasm_script);
Handle<WeakArrayList> weak_instance_list(script->wasm_weak_instance_list(),
GetIsolate());
if (weak_instance_list->length() > 0) {
MaybeObject maybe_instance = weak_instance_list->Get(0);
if (maybe_instance->IsWeak()) {
Handle<WasmInstanceObject> instance(
WasmInstanceObject::cast(maybe_instance->GetHeapObjectAssumeWeak()),
GetIsolate());
return instance;
}
}
return Handle<WasmInstanceObject>::null();
}
int GetLEB128Size(Vector<const uint8_t> module_bytes, int offset) {
int index = offset;
while (module_bytes[index] & 0x80) index++;
return index + 1 - offset;
}
int ReturnPc(const NativeModule* native_module, int pc) {
Vector<const uint8_t> wire_bytes = native_module->wire_bytes();
uint8_t opcode = wire_bytes[pc];
switch (opcode) {
case kExprCallFunction: {
// skip opcode
pc++;
// skip function index
return pc + GetLEB128Size(wire_bytes, pc);
}
case kExprCallIndirect: {
// skip opcode
pc++;
// skip signature index
pc += GetLEB128Size(wire_bytes, pc);
// skip table index
return pc + GetLEB128Size(wire_bytes, pc);
}
default:
UNREACHABLE();
}
}
// static
std::vector<wasm_addr_t> WasmModuleDebug::GetCallStack(
uint32_t debug_context_id, Isolate* isolate) {
std::vector<wasm_addr_t> call_stack;
for (StackFrameIterator frame_it(isolate); !frame_it.done();
frame_it.Advance()) {
StackFrame* const frame = frame_it.frame();
switch (frame->type()) {
case StackFrame::JAVA_SCRIPT_BUILTIN_CONTINUATION:
case StackFrame::JAVA_SCRIPT_BUILTIN_CONTINUATION_WITH_CATCH:
case StackFrame::OPTIMIZED:
case StackFrame::INTERPRETED:
case StackFrame::BUILTIN:
case StackFrame::WASM_COMPILED: {
// A standard frame may include many summarized frames, due to inlining.
std::vector<FrameSummary> frames;
StandardFrame::cast(frame)->Summarize(&frames);
for (size_t i = frames.size(); i-- != 0;) {
int offset = 0;
Handle<Script> script;
auto& summary = frames[i];
if (summary.IsJavaScript()) {
FrameSummary::JavaScriptFrameSummary const& java_script =
summary.AsJavaScript();
offset = java_script.code_offset();
script = Handle<Script>::cast(java_script.script());
} else if (summary.IsWasmCompiled()) {
FrameSummary::WasmCompiledFrameSummary const& wasm_compiled =
summary.AsWasmCompiled();
offset =
GetWasmFunctionOffset(wasm_compiled.wasm_instance()->module(),
wasm_compiled.function_index()) +
wasm_compiled.byte_offset();
script = wasm_compiled.script();
bool zeroth_frame = call_stack.empty();
if (!zeroth_frame) {
const NativeModule* native_module = wasm_compiled.wasm_instance()
->module_object()
.native_module();
offset = ReturnPc(native_module, offset);
}
}
if (offset > 0) {
call_stack.push_back(
{debug_context_id << 16 | script->id(), uint32_t(offset)});
}
}
break;
}
case StackFrame::WASM_INTERPRETER_ENTRY:
case StackFrame::BUILTIN_EXIT:
default:
// ignore the frame.
break;
}
}
if (call_stack.empty()) call_stack.push_back({1, 0});
return call_stack;
}
// static
std::vector<FrameSummary> WasmModuleDebug::FindWasmFrame(
StackTraceFrameIterator* frame_it, uint32_t* frame_index) {
while (!frame_it->done()) {
StackFrame* const frame = frame_it->frame();
switch (frame->type()) {
case StackFrame::JAVA_SCRIPT_BUILTIN_CONTINUATION:
case StackFrame::JAVA_SCRIPT_BUILTIN_CONTINUATION_WITH_CATCH:
case StackFrame::OPTIMIZED:
case StackFrame::INTERPRETED:
case StackFrame::BUILTIN:
case StackFrame::WASM_COMPILED:
case StackFrame::WASM_INTERPRETER_ENTRY: {
// A standard frame may include many summarized frames, due to inlining.
std::vector<FrameSummary> frames;
StandardFrame::cast(frame)->Summarize(&frames);
const size_t frame_count = frames.size();
DCHECK_GT(frame_count, 0);
if (frame_count > *frame_index) {
if (frame_it->is_wasm())
return frames;
else
return {};
} else {
*frame_index -= frame_count;
frame_it->Advance();
}
break;
}
case StackFrame::BUILTIN_EXIT:
default:
// ignore the frame.
break;
}
}
return {};
}
// static
Handle<WasmInstanceObject> WasmModuleDebug::GetWasmInstance(
Isolate* isolate, uint32_t frame_index) {
StackTraceFrameIterator frame_it(isolate);
std::vector<FrameSummary> frames = FindWasmFrame(&frame_it, &frame_index);
if (frames.empty()) {
return Handle<WasmInstanceObject>::null();
}
int reversed_index = static_cast<int>(frames.size() - 1 - frame_index);
const FrameSummary::WasmFrameSummary& summary =
frames[reversed_index].AsWasm();
return summary.wasm_instance();
}
// static
bool WasmModuleDebug::GetWasmGlobal(Isolate* isolate, uint32_t frame_index,
uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size) {
HandleScope handles(isolate);
Handle<WasmInstanceObject> instance = GetWasmInstance(isolate, frame_index);
if (!instance.is_null()) {
Handle<WasmModuleObject> module_object(instance->module_object(), isolate);
const wasm::WasmModule* module = module_object->module();
if (index < module->globals.size()) {
wasm::WasmValue wasm_value =
WasmInstanceObject::GetGlobalValue(instance, module->globals[index]);
return GetWasmValue(wasm_value, buffer, buffer_size, size);
}
}
return false;
}
// static
bool WasmModuleDebug::GetWasmLocal(Isolate* isolate, uint32_t frame_index,
uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size) {
HandleScope handles(isolate);
StackTraceFrameIterator frame_it(isolate);
std::vector<FrameSummary> frames = FindWasmFrame(&frame_it, &frame_index);
if (frames.empty()) {
return false;
}
int reversed_index = static_cast<int>(frames.size() - 1 - frame_index);
const FrameSummary& summary = frames[reversed_index];
if (summary.IsWasmCompiled()) {
Handle<WasmInstanceObject> instance = summary.AsWasm().wasm_instance();
if (!instance.is_null()) {
Handle<WasmModuleObject> module_object(instance->module_object(),
isolate);
wasm::NativeModule* native_module = module_object->native_module();
DebugInfo* debug_info = native_module->GetDebugInfo();
if (static_cast<uint32_t>(debug_info->GetNumLocals(
isolate, frame_it.frame()->pc())) > index) {
wasm::WasmValue wasm_value = debug_info->GetLocalValue(
index, isolate, frame_it.frame()->pc(), frame_it.frame()->fp(),
frame_it.frame()->callee_fp());
return GetWasmValue(wasm_value, buffer, buffer_size, size);
}
}
}
return false;
}
// static
bool WasmModuleDebug::GetWasmStackValue(Isolate* isolate, uint32_t frame_index,
uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size) {
HandleScope handles(isolate);
StackTraceFrameIterator frame_it(isolate);
std::vector<FrameSummary> frames = FindWasmFrame(&frame_it, &frame_index);
if (frames.empty()) {
return false;
}
int reversed_index = static_cast<int>(frames.size() - 1 - frame_index);
const FrameSummary& summary = frames[reversed_index];
if (summary.IsWasmCompiled()) {
Handle<WasmInstanceObject> instance = summary.AsWasm().wasm_instance();
if (!instance.is_null()) {
Handle<WasmModuleObject> module_object(instance->module_object(),
isolate);
wasm::NativeModule* native_module = module_object->native_module();
DebugInfo* debug_info = native_module->GetDebugInfo();
if (static_cast<uint32_t>(debug_info->GetStackDepth(
isolate, frame_it.frame()->pc())) > index) {
WasmValue wasm_value = debug_info->GetStackValue(
index, isolate, frame_it.frame()->pc(), frame_it.frame()->fp(),
frame_it.frame()->callee_fp());
return GetWasmValue(wasm_value, buffer, buffer_size, size);
}
}
}
return false;
}
// static
uint32_t WasmModuleDebug::GetWasmMemory(Isolate* isolate, uint32_t frame_index,
uint32_t offset, uint8_t* buffer,
uint32_t size) {
HandleScope handles(isolate);
uint32_t bytes_read = 0;
Handle<WasmInstanceObject> instance = GetWasmInstance(isolate, frame_index);
if (!instance.is_null()) {
uint8_t* mem_start = instance->memory_start();
size_t mem_size = instance->memory_size();
if (static_cast<uint64_t>(offset) + size <= mem_size) {
memcpy(buffer, mem_start + offset, size);
bytes_read = size;
} else if (offset < mem_size) {
bytes_read = static_cast<uint32_t>(mem_size) - offset;
memcpy(buffer, mem_start + offset, bytes_read);
}
}
return bytes_read;
}
uint32_t WasmModuleDebug::GetWasmModuleBytes(wasm_addr_t wasm_addr,
uint8_t* buffer, uint32_t size) {
uint32_t bytes_read = 0;
// Any instance will work.
Handle<WasmInstanceObject> instance = GetFirstWasmInstance();
if (!instance.is_null()) {
Handle<WasmModuleObject> module_object(instance->module_object(),
GetIsolate());
wasm::NativeModule* native_module = module_object->native_module();
const wasm::ModuleWireBytes wire_bytes(native_module->wire_bytes());
uint32_t offset = wasm_addr.Offset();
if (offset < wire_bytes.length()) {
uint32_t module_size = static_cast<uint32_t>(wire_bytes.length());
bytes_read = module_size - offset >= size ? size : module_size - offset;
memcpy(buffer, wire_bytes.start() + offset, bytes_read);
}
}
return bytes_read;
}
bool WasmModuleDebug::AddBreakpoint(uint32_t offset, int* breakpoint_id) {
v8::Local<debug::WasmScript> wasm_script = wasm_script_.Get(isolate_);
Handle<Script> script = Utils::OpenHandle(*wasm_script);
Handle<String> condition = GetIsolate()->factory()->empty_string();
int breakpoint_address = static_cast<int>(offset);
return GetIsolate()->debug()->SetBreakPointForScript(
script, condition, &breakpoint_address, breakpoint_id);
}
void WasmModuleDebug::RemoveBreakpoint(uint32_t offset, int breakpoint_id) {
v8::Local<debug::WasmScript> wasm_script = wasm_script_.Get(isolate_);
Handle<Script> script = Utils::OpenHandle(*wasm_script);
GetIsolate()->debug()->RemoveBreakpointForWasmScript(script, breakpoint_id);
}
void WasmModuleDebug::PrepareStep() {
i::Isolate* isolate = GetIsolate();
DebugScope debug_scope(isolate->debug());
debug::PrepareStep(reinterpret_cast<v8::Isolate*>(isolate),
debug::StepAction::StepIn);
}
template <typename T>
bool StoreValue(const T& value, uint8_t* buffer, uint32_t buffer_size,
uint32_t* size) {
*size = sizeof(value);
if (*size > buffer_size) return false;
memcpy(buffer, &value, *size);
return true;
}
// static
bool WasmModuleDebug::GetWasmValue(const wasm::WasmValue& wasm_value,
uint8_t* buffer, uint32_t buffer_size,
uint32_t* size) {
switch (wasm_value.type().kind()) {
case wasm::kWasmI32.kind():
return StoreValue(wasm_value.to_i32(), buffer, buffer_size, size);
case wasm::kWasmI64.kind():
return StoreValue(wasm_value.to_i64(), buffer, buffer_size, size);
case wasm::kWasmF32.kind():
return StoreValue(wasm_value.to_f32(), buffer, buffer_size, size);
case wasm::kWasmF64.kind():
return StoreValue(wasm_value.to_f64(), buffer, buffer_size, size);
case wasm::kWasmS128.kind():
return StoreValue(wasm_value.to_s128(), buffer, buffer_size, size);
case wasm::kWasmStmt.kind():
case wasm::kWasmAnyRef.kind():
case wasm::kWasmFuncRef.kind():
case wasm::kWasmNullRef.kind():
case wasm::kWasmExnRef.kind():
case wasm::kWasmBottom.kind():
default:
// Not supported
return false;
}
}
} // namespace gdb_server
} // namespace wasm
} // namespace internal
} // namespace v8

View File

@ -0,0 +1,105 @@
// 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_WASM_MODULE_DEBUG_H_
#define V8_DEBUG_WASM_GDB_SERVER_WASM_MODULE_DEBUG_H_
#include "src/debug/debug.h"
#include "src/debug/wasm/gdb-server/gdb-remote-util.h"
#include "src/execution/frames.h"
namespace v8 {
namespace internal {
namespace wasm {
class WasmValue;
namespace gdb_server {
// Represents the interface to access the Wasm engine state for a given module.
// For the moment it only works with interpreted functions, in the future it
// could be extended to also support Liftoff.
class WasmModuleDebug {
public:
WasmModuleDebug(v8::Isolate* isolate, Local<debug::WasmScript> script);
std::string GetModuleName() const;
i::Isolate* GetIsolate() const {
return reinterpret_cast<i::Isolate*>(isolate_);
}
// Gets the value of the {index}th global value.
static bool GetWasmGlobal(Isolate* isolate, uint32_t frame_index,
uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size);
// Gets the value of the {index}th local value in the {frame_index}th stack
// frame.
static bool GetWasmLocal(Isolate* isolate, uint32_t frame_index,
uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size);
// Gets the value of the {index}th value in the operand stack.
static bool GetWasmStackValue(Isolate* isolate, uint32_t frame_index,
uint32_t index, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size);
// Reads {size} bytes, starting from {offset}, from the Memory instance
// associated to this module.
// Returns the number of byte copied to {buffer}, or 0 is case of error.
// Note: only one Memory for Module is currently supported.
static uint32_t GetWasmMemory(Isolate* isolate, uint32_t frame_index,
uint32_t offset, uint8_t* buffer,
uint32_t size);
// Gets {size} bytes, starting from {offset}, from the Code space of this
// module.
// Returns the number of byte copied to {buffer}, or 0 is case of error.
uint32_t GetWasmModuleBytes(wasm_addr_t wasm_addr, uint8_t* buffer,
uint32_t size);
// Inserts a breakpoint at the offset {offset} of this module.
// Returns {true} if the breakpoint was successfully added.
bool AddBreakpoint(uint32_t offset, int* breakpoint_id);
// Removes a breakpoint at the offset {offset} of the this module.
void RemoveBreakpoint(uint32_t offset, int breakpoint_id);
// Handle stepping in wasm functions via the wasm interpreter.
void PrepareStep();
// Returns the current stack trace as a vector of instruction pointers.
static std::vector<wasm_addr_t> GetCallStack(uint32_t debug_context_id,
Isolate* isolate);
private:
// Returns the module WasmInstance associated to the {frame_index}th frame
// in the call stack.
static Handle<WasmInstanceObject> GetWasmInstance(Isolate* isolate,
uint32_t frame_index);
// Returns its first WasmInstance for this Wasm module.
Handle<WasmInstanceObject> GetFirstWasmInstance();
// Iterates on current stack frames and return frame information for the
// {frame_index} specified.
// Returns an empty array if the frame specified does not correspond to a Wasm
// stack frame.
static std::vector<FrameSummary> FindWasmFrame(
StackTraceFrameIterator* frame_it, uint32_t* frame_index);
// Converts a WasmValue into an array of bytes.
static bool GetWasmValue(const wasm::WasmValue& wasm_value, uint8_t* buffer,
uint32_t buffer_size, uint32_t* size);
v8::Isolate* isolate_;
Global<debug::WasmScript> wasm_script_;
};
} // namespace gdb_server
} // namespace wasm
} // namespace internal
} // namespace v8
#endif // V8_DEBUG_WASM_GDB_SERVER_WASM_MODULE_DEBUG_H_

View File

@ -1435,6 +1435,7 @@ DEFINE_BOOL(multi_mapped_mock_allocator, false,
#define DEFAULT_WASM_GDB_REMOTE_PORT 8765
DEFINE_BOOL(wasm_gdb_remote, false,
"enable GDB-remote for WebAssembly debugging")
DEFINE_NEG_IMPLICATION(wasm_gdb_remote, wasm_tier_up)
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,

View File

@ -868,9 +868,20 @@ void WasmEngine::AddIsolate(Isolate* isolate) {
};
isolate->heap()->AddGCEpilogueCallback(callback, v8::kGCTypeMarkSweepCompact,
nullptr);
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
if (gdb_server_) {
gdb_server_->AddIsolate(isolate);
}
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
}
void WasmEngine::RemoveIsolate(Isolate* isolate) {
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
if (gdb_server_) {
gdb_server_->RemoveIsolate(isolate);
}
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
base::MutexGuard guard(&mutex_);
auto it = isolates_.find(isolate);
DCHECK_NE(isolates_.end(), it);
@ -956,6 +967,7 @@ std::shared_ptr<NativeModule> WasmEngine::NewNativeModule(
#ifdef V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING
if (FLAG_wasm_gdb_remote && !gdb_server_) {
gdb_server_ = gdb_server::GdbServer::Create();
gdb_server_->AddIsolate(isolate);
}
#endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING

View File

@ -79,7 +79,8 @@ class PYTestLoader(testsuite.GenericTestLoader):
@property
def excluded_files(self):
return {'gdb_rsp.py', 'testcfg.py', '__init__.py'}
return {'gdb_rsp.py', 'testcfg.py', '__init__.py', 'test_basic.py',
'test_float.py', 'test_memory.py', 'test_trap.py'}
@property
def extensions(self):

View File

@ -0,0 +1,3 @@
paolosev@microsoft.com
# COMPONENT: Blink>JavaScript>WebAssembly

View File

@ -0,0 +1,58 @@
# 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.
# Flags: -expose-wasm --wasm-gdb-remote --wasm-pause-waiting-for-debugger test/debugging/wasm/gdb-server/test_files/test_basic.js
import sys
import unittest
import gdb_rsp
import test_files.test_basic as test_basic
class Tests(unittest.TestCase):
def test_initial_breakpoint(self):
# Testing that the debuggee suspends when the debugger attaches.
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
reply = connection.RspRequest('?')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
def test_setting_removing_breakpoint(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
func_addr = module_load_addr + test_basic.BREAK_ADDRESS_1
# Set a breakpoint.
reply = connection.RspRequest('Z0,%x,1' % func_addr)
self.assertEqual(reply, 'OK')
# When we run the program, we should hit the breakpoint.
reply = connection.RspRequest('c')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
gdb_rsp.CheckInstructionPtr(connection, func_addr)
# Check that we can remove the breakpoint.
reply = connection.RspRequest('z0,%x,0' % func_addr)
self.assertEqual(reply, 'OK')
# Requesting removing a breakpoint on an address that does not
# have one should return an error.
reply = connection.RspRequest('z0,%x,0' % func_addr)
self.assertEqual(reply, 'E03')
def test_setting_breakpoint_on_invalid_address(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
# Requesting a breakpoint on an invalid address should give an error.
reply = connection.RspRequest('Z0,%x,1' % (1 << 32))
self.assertEqual(reply, 'E03')
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()

View File

@ -1,24 +1,27 @@
# Copyright 2019 the V8 project authors. All rights reserved.
# 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.
# Flags: -expose-wasm --wasm_gdb_remote --wasm-pause-waiting-for-debugger --wasm-interpret-all test/debugging/wasm/gdb-server/test_files/test.js
# Flags: -expose-wasm --wasm-gdb-remote --wasm-pause-waiting-for-debugger test/debugging/wasm/gdb-server/test_files/test_basic.js
from ctypes import *
import os
import subprocess
import unittest
import sys
import gdb_rsp
import test_files.test_basic as test_basic
# 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.
# Connect.
connection = gdb_rsp.GdbRspConnection()
connection.Close()
# Reconnect 3 times.
@ -28,6 +31,29 @@ class Tests(unittest.TestCase):
finally:
gdb_rsp.KillProcess(process)
def test_kill(self):
process = gdb_rsp.PopenDebugStub(COMMAND)
try:
connection = gdb_rsp.GdbRspConnection()
# Request killing the target.
reply = connection.RspRequest('k')
self.assertEqual(reply, 'OK')
signal = c_byte(process.wait()).value
self.assertEqual(signal, gdb_rsp.RETURNCODE_KILL)
finally:
gdb_rsp.KillProcess(process)
def test_detach(self):
process = gdb_rsp.PopenDebugStub(COMMAND)
try:
connection = gdb_rsp.GdbRspConnection()
# Request detaching from the target.
# This resumes execution, so we get the normal exit() status.
reply = connection.RspRequest('D')
self.assertEqual(reply, 'OK')
finally:
gdb_rsp.KillProcess(process)
def Main():
index = sys.argv.index('--')

View File

@ -0,0 +1,71 @@
# 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.
# Flags: -expose-wasm --wasm-gdb-remote --wasm-pause-waiting-for-debugger test/debugging/wasm/gdb-server/test_files/test_float.js
import os
import re
import struct
import subprocess
import sys
import unittest
import gdb_rsp
import test_files.test_float as test_float
# These are set up by Main().
COMMAND = None
class Tests(unittest.TestCase):
def RunToWasm(self, connection):
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
breakpoint_addr = module_load_addr + test_float.FUNC_START_ADDR
# Set a breakpoint.
reply = connection.RspRequest('Z0,%x,1' % breakpoint_addr)
self.assertEqual(reply, 'OK')
# When we run the program, we should hit the breakpoint.
reply = connection.RspRequest('c')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
# Remove the breakpoint.
reply = connection.RspRequest('z0,%x,1' % breakpoint_addr)
self.assertEqual(reply, 'OK')
def test_loaded_modules(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
modules = gdb_rsp.GetLoadedModules(connection)
connection.Close()
assert(len(modules) > 0)
def test_wasm_local_float(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
self.RunToWasm(connection)
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
reply = connection.RspRequest('qWasmLocal:0;0')
value = struct.unpack('f', gdb_rsp.DecodeHex(reply))[0]
self.assertEqual(test_float.ARG_0, value)
reply = connection.RspRequest('qWasmLocal:0;1')
value = struct.unpack('f', gdb_rsp.DecodeHex(reply))[0]
self.assertEqual(test_float.ARG_1, value)
# invalid local
reply = connection.RspRequest('qWasmLocal:0;9')
self.assertEqual("E03", reply)
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()

View File

@ -1,13 +1,25 @@
# Copyright 2019 the V8 project authors. All rights reserved.
# 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.
import re
import socket
import struct
import subprocess
import time
import xml.etree.ElementTree
SOCKET_ADDR = ('localhost', 8765)
SIGTRAP = 5
SIGSEGV = 11
RETURNCODE_KILL = -9
ARCH = 'wasm32'
REG_DEFS = {
ARCH: [('pc', 'Q'), ],
}
def EnsurePortIsAvailable(addr=SOCKET_ADDR):
# As a sanity check, check that the TCP port is available by binding to it
@ -21,6 +33,11 @@ def EnsurePortIsAvailable(addr=SOCKET_ADDR):
sock.bind(addr)
sock.close()
def RspChecksum(data):
checksum = 0
for char in data:
checksum = (checksum + ord(char)) & 0xff
return checksum
class GdbRspConnection(object):
@ -48,6 +65,40 @@ class GdbRspConnection(object):
raise Exception('Could not connect to the debug stub in %i seconds'
% timeout_in_seconds)
def _GetReply(self):
reply = ''
message_finished = re.compile('#[0-9a-fA-F]{2}')
while True:
data = self._socket.recv(1024)
if len(data) == 0:
raise AssertionError('EOF on socket reached with '
'incomplete reply message: %r' % reply)
reply += data
if message_finished.match(reply[-3:]):
break
match = re.match('\+?\$([^#]*)#([0-9a-fA-F]{2})$', reply)
if match is None:
raise AssertionError('Unexpected reply message: %r' % reply)
reply_body = match.group(1)
checksum = match.group(2)
expected_checksum = '%02x' % RspChecksum(reply_body)
if checksum != expected_checksum:
raise AssertionError('Bad RSP checksum: %r != %r' %
(checksum, expected_checksum))
# Send acknowledgement.
self._socket.send('+')
return reply_body
# Send an rsp message, but don't wait for or expect a reply.
def RspSendOnly(self, data):
msg = '$%s#%02x' % (data, RspChecksum(data))
return self._socket.send(msg)
def RspRequest(self, data):
self.RspSendOnly(data)
reply = self._GetReply()
return reply
def Close(self):
self._socket.close()
@ -71,3 +122,87 @@ def KillProcess(process):
else:
raise
process.wait()
class LaunchDebugStub(object):
def __init__(self, command):
self._proc = PopenDebugStub(command)
def __enter__(self):
try:
return GdbRspConnection()
except:
KillProcess(self._proc)
raise
def __exit__(self, exc_type, exc_value, traceback):
KillProcess(self._proc)
def AssertEquals(x, y):
if x != y:
raise AssertionError('%r != %r' % (x, y))
def DecodeHex(data):
assert len(data) % 2 == 0, data
return bytes(bytearray([int(data[index * 2 : (index + 1) * 2], 16) for index in xrange(len(data) // 2)]))
def EncodeHex(data):
return ''.join('%02x' % ord(byte) for byte in data)
def DecodeUInt64Array(data):
assert len(data) % 16 == 0, data
result = []
for index in xrange(len(data) // 16):
value = 0
for digit in xrange(7, -1, -1):
value = value * 256 + int(data[index * 16 + digit * 2 : index * 16 + (digit + 1) * 2], 16)
result.append(value)
return result
def AssertReplySignal(reply, signal):
AssertEquals(ParseThreadStopReply(reply)['signal'], signal)
def ParseThreadStopReply(reply):
match = re.match('T([0-9a-f]{2})thread-pcs:([0-9a-f]+);thread:([0-9a-f]+);$', reply)
if not match:
raise AssertionError('Bad thread stop reply: %r' % reply)
return {'signal': int(match.group(1), 16),
'thread_pc': int(match.group(2), 16),
'thread_id': int(match.group(3), 16)}
def CheckInstructionPtr(connection, expected_ip):
ip_value = DecodeRegs(connection.RspRequest('g'))['pc']
AssertEquals(ip_value, expected_ip)
def DecodeRegs(reply):
defs = REG_DEFS[ARCH]
names = [reg_name for reg_name, reg_fmt in defs]
fmt = ''.join([reg_fmt for reg_name, reg_fmt in defs])
values = struct.unpack_from(fmt, DecodeHex(reply))
return dict(zip(names, values))
def GetLoadedModules(connection):
modules = {}
reply = connection.RspRequest('qXfer:libraries:read')
AssertEquals(reply[0], 'l')
library_list = xml.etree.ElementTree.fromstring(reply[1:])
AssertEquals(library_list.tag, 'library-list')
for library in library_list:
AssertEquals(library.tag, 'library')
section = library.find('section')
address = section.get('address')
assert long(address) > 0
modules[long(address)] = library.get('name')
return modules
def GetLoadedModuleAddress(connection):
modules = GetLoadedModules(connection)
assert len(modules) > 0
return modules.keys()[0]
def ReadCodeMemory(connection, address, size):
reply = connection.RspRequest('m%x,%x' % (address, size))
assert not reply.startswith('E'), reply
return DecodeHex(reply)

View File

@ -0,0 +1,96 @@
# 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.
# Flags: -expose-wasm --wasm-gdb-remote --wasm-pause-waiting-for-debugger test/debugging/wasm/gdb-server/test_files/test_memory.js
import struct
import sys
import unittest
import gdb_rsp
import test_files.test_memory as test_memory
# These are set up by Main().
COMMAND = None
class Tests(unittest.TestCase):
# Test that reading from an unreadable address gives a sensible error.
def CheckReadMemoryAtInvalidAddr(self, connection):
mem_addr = 0xffffffff
result = connection.RspRequest('m%x,%x' % (mem_addr, 1))
self.assertEquals(result, 'E02')
def RunToWasm(self, connection, breakpoint_addr):
# Set a breakpoint.
reply = connection.RspRequest('Z0,%x,1' % breakpoint_addr)
self.assertEqual(reply, 'OK')
# When we run the program, we should hit the breakpoint.
reply = connection.RspRequest('c')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
# Remove the breakpoint.
reply = connection.RspRequest('z0,%x,1' % breakpoint_addr)
self.assertEqual(reply, 'OK')
def test_reading_and_writing_memory(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
breakpoint_addr = module_load_addr + test_memory.FUNC0_START_ADDR
self.RunToWasm(connection, breakpoint_addr)
self.CheckReadMemoryAtInvalidAddr(connection)
# Check reading code memory space.
expected_data = b'\0asm'
result = gdb_rsp.ReadCodeMemory(connection, module_load_addr, len(expected_data))
self.assertEqual(result, expected_data)
# Check reading instance memory at a valid range.
reply = connection.RspRequest('qWasmMem:0;%x;%x' % (32, 4))
value = struct.unpack('I', gdb_rsp.DecodeHex(reply))[0]
self.assertEquals(int(value), 0)
# Check reading instance memory at an invalid range.
reply = connection.RspRequest('qWasmMem:0;%x;%x' % (0xf0000000, 4))
self.assertEqual(reply, 'E03')
def test_wasm_global(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
breakpoint_addr = module_load_addr + test_memory.FUNC0_START_ADDR
self.RunToWasm(connection, breakpoint_addr)
# Check reading valid global.
reply = connection.RspRequest('qWasmGlobal:0;0')
value = struct.unpack('I', gdb_rsp.DecodeHex(reply))[0]
self.assertEqual(0, value)
# Check reading invalid global.
reply = connection.RspRequest('qWasmGlobal:0;9')
self.assertEqual("E03", reply)
def test_wasm_call_stack(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
breakpoint_addr = module_load_addr + test_memory.FUNC0_START_ADDR
self.RunToWasm(connection, breakpoint_addr)
reply = connection.RspRequest('qWasmCallStack')
stack = gdb_rsp.DecodeUInt64Array(reply)
assert(len(stack) > 2) # At least two Wasm frames, plus one or more JS frames.
self.assertEqual(stack[0], module_load_addr + test_memory.FUNC0_START_ADDR)
self.assertEqual(stack[1], module_load_addr + test_memory.FUNC1_RETURN_ADDR)
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()

View File

@ -0,0 +1,109 @@
# 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.
# Flags: -expose-wasm --wasm-gdb-remote --wasm-pause-waiting-for-debugger test/debugging/wasm/gdb-server/test_files/test_basic.js
import re
import struct
import sys
import unittest
import gdb_rsp
import test_files.test_basic as test_basic
# These are set up by Main().
COMMAND = None
class Tests(unittest.TestCase):
def test_loaded_modules(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
modules = gdb_rsp.GetLoadedModules(connection)
connection.Close()
assert(len(modules) > 0)
def test_checking_thread_state(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
# Query wasm thread id
reply = connection.RspRequest('qfThreadInfo')
match = re.match('m([0-9])$', reply)
if match is None:
raise AssertionError('Bad active thread list reply: %r' % reply)
thread_id = int(match.group(1), 10)
# There should not be other threads.
reply = connection.RspRequest('qsThreadInfo')
self.assertEqual("l", reply)
# Test that valid thread should be alive.
reply = connection.RspRequest('T%d' % (thread_id))
self.assertEqual("OK", reply)
# Test invalid thread id.
reply = connection.RspRequest('T42')
self.assertEqual("E02", reply)
def test_wasm_local(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
breakpoint_addr = module_load_addr + test_basic.BREAK_ADDRESS_2
reply = connection.RspRequest('Z0,%x,1' % breakpoint_addr)
self.assertEqual("OK", reply)
reply = connection.RspRequest('c')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
reply = connection.RspRequest('qWasmLocal:0;0')
value = struct.unpack('I', gdb_rsp.DecodeHex(reply))[0]
self.assertEqual(test_basic.ARG_0, value)
reply = connection.RspRequest('qWasmLocal:0;1')
value = struct.unpack('I', gdb_rsp.DecodeHex(reply))[0]
self.assertEqual(test_basic.ARG_1, value)
# invalid local
reply = connection.RspRequest('qWasmLocal:0;9')
self.assertEqual("E03", reply)
def test_wasm_stack_value(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
breakpoint_addr = module_load_addr + test_basic.BREAK_ADDRESS_2
reply = connection.RspRequest('Z0,%x,1' % breakpoint_addr)
self.assertEqual("OK", reply)
reply = connection.RspRequest('c')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
reply = connection.RspRequest('qWasmStackValue:0;0')
value = struct.unpack('I', gdb_rsp.DecodeHex(reply))[0]
self.assertEqual(test_basic.ARG_0, value)
reply = connection.RspRequest('qWasmStackValue:0;1')
value = struct.unpack('I', gdb_rsp.DecodeHex(reply))[0]
self.assertEqual(test_basic.ARG_1, value)
# invalid index
reply = connection.RspRequest('qWasmStackValue:0;2')
self.assertEqual("E03", reply)
def test_modifying_code_is_disallowed(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
# Pick an arbitrary address in the code segment.
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
breakpoint_addr = module_load_addr + test_basic.BREAK_ADDRESS_1
# Writing to the code area should be disallowed.
data = '\x00'
write_command = 'M%x,%x:%s' % (breakpoint_addr, len(data), gdb_rsp.EncodeHex(data))
reply = connection.RspRequest(write_command)
self.assertEquals(reply, 'E03')
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()

View File

@ -0,0 +1,56 @@
# 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.
# Flags: -expose-wasm --wasm-gdb-remote --wasm-pause-waiting-for-debugger test/debugging/wasm/gdb-server/test_files/test_basic.js
import sys
import unittest
import gdb_rsp
import test_files.test_basic as test_basic
# These are set up by Main().
COMMAND = None
class Tests(unittest.TestCase):
def test_single_step(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
bp_addr = module_load_addr + test_basic.BREAK_ADDRESS_0
reply = connection.RspRequest('Z0,%x,1' % bp_addr)
self.assertEqual("OK", reply)
reply = connection.RspRequest('c')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
# We expect 's' to stop at the next instruction.
reply = connection.RspRequest('s')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
tid = gdb_rsp.ParseThreadStopReply(reply)['thread_id']
self.assertEqual(tid, 1)
regs = gdb_rsp.DecodeRegs(connection.RspRequest('g'))
self.assertEqual(regs['pc'], module_load_addr + test_basic.BREAK_ADDRESS_1)
# Again.
reply = connection.RspRequest('s')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
tid = gdb_rsp.ParseThreadStopReply(reply)['thread_id']
self.assertEqual(tid, 1)
regs = gdb_rsp.DecodeRegs(connection.RspRequest('g'))
self.assertEqual(regs['pc'], module_load_addr + test_basic.BREAK_ADDRESS_2)
# Check that we can continue after single-stepping.
reply = connection.RspRequest('c')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGTRAP)
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()

View File

@ -1,33 +0,0 @@
// 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);
}

View File

@ -0,0 +1,31 @@
// 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.
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.mul 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 = f();
f();
} catch (e) {
print('*exception:* ' + e);
}

View File

@ -0,0 +1,23 @@
# 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.
# 0x00 (module
# 0x08 [type]
# (type $type0 (func (param i32 i32) (result i32)))
# 0x11 [function]
# 0x15 (export "mul" (func $func0))
# 0x1e [code]
# 0x20 (func $func0 (param $var0 i32) (param $var1 i32) (result i32)
# 0x23 get_local $var0
# 0x25 get_local $var1
# 0x27 i32.mul
# )
# )
# 0x29 [name]
BREAK_ADDRESS_0 = 0x0023
BREAK_ADDRESS_1 = 0x0025
BREAK_ADDRESS_2 = 0x0027
ARG_0 = 21
ARG_1 = 2

View File

@ -0,0 +1,31 @@
// 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.
load('test/mjsunit/wasm/wasm-module-builder.js');
var builder = new WasmModuleBuilder();
builder
.addFunction('mul', kSig_f_ff)
// input is 2 args of type float and output is float
.addBody([
kExprLocalGet, 0, // local.get f0
kExprLocalGet, 1, // local.get f1
kExprF32Mul, // f32.mul i0 i1
])
.exportFunc();
const instance = builder.instantiate();
const wasm_f = instance.exports.mul;
function f() {
var result = wasm_f(12.0, 3.5);
return result;
}
try {
let val = f();
print('float result: ' + val);
} catch (e) {
print('*exception:* ' + e);
}

View File

@ -0,0 +1,21 @@
# 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.
# 0x00 (module
# 0x08 [type]
# (type $type0 (func (param f32 f32) (result f32)))
# 0x11 [function]
# 0x15 (export "mul" (func $func0))
# 0x1e [code]
# 0x20 (func $func0 (param $var0 f32) (param $var1 f32) (result f32)
# 0x23 get_local $var0
# 0x25 get_local $var1
# 0x27 f32.mul
# )
# )
# 0x29 [name]
ARG_0 = 12.0
ARG_1 = 3.5
FUNC_START_ADDR = 0x23

View 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.
load('test/mjsunit/wasm/wasm-module-builder.js');
var builder = new WasmModuleBuilder();
builder.addGlobal(kWasmI32).exportAs('g_n');
builder.addMemory(32, 128).exportMemoryAs('mem')
var func_a_idx =
builder.addFunction('wasm_A', kSig_v_i).addBody([kExprNop, kExprNop]).index;
// wasm_B calls wasm_A <param0> times.
builder.addFunction('wasm_B', kSig_v_i)
.addBody([
kExprLoop,
kWasmStmt, // while
kExprLocalGet,
0, // -
kExprIf,
kWasmStmt, // if <param0> != 0
kExprLocalGet,
0, // -
kExprI32Const,
1, // -
kExprI32Sub, // -
kExprLocalSet,
0, // decrease <param0>
...wasmI32Const(1024), // some longer i32 const (2 byte imm)
kExprCallFunction,
func_a_idx, // -
kExprBr,
1, // continue
kExprEnd, // -
kExprEnd, // break
])
.exportAs('main');
const instance = builder.instantiate();
const wasm_main = instance.exports.main;
function f() {
wasm_main(42);
}
f();

View 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.
# 0x00 (module
# 0x08 [type]
# 0x0a (type $type0 (func (param i32)))
# 0x0f (type $type1 (func (param i32)))
# 0x13 [function]
# 0x18 [memory]
# (memory (;0;) 32 128)
# 0x1f [global]
# 0x27 (global $global0 i32 (i32.const 0))
# 0x29 (export "g_n" (global $global0))
# 0x30 (export "mem" (memory 0))
# 0x36 (export "main" (func $func1))
# 0x3d [code]
# 0x3f (func $func0 (param $var0 i32)
# 0x42 nop
# 0x43 nop
# )
# 0x45 (func $func1 (param $var0 i32)
# 0x47 loop $label0
# 0x49 get_local $var0
# 0x4b if
# 0x4d get_local $var0
# 0x4f i32.const 1
# 0x51 i32.sub
# 0x52 set_local $var0
# 0x54 i32.const 1024
# 0x57 call $func0
# 0x59 br $label0
# 0x5b end
# 0x5c end $label0
# )
# )
# 0x5e [name]
MEM_MIN = 32
MEM_MAX = 128
FUNC0_START_ADDR = 0x42
FUNC1_RETURN_ADDR = 0x59
FUNC1_START_ADDR = 0x47

View File

@ -0,0 +1,31 @@
// 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.
load('test/mjsunit/wasm/wasm-module-builder.js');
var builder = new WasmModuleBuilder();
builder.addMemory(32, 128).exportMemoryAs('mem')
var func_a_idx =
builder.addFunction('wasm_A', kSig_v_v).addBody([
kExprI32Const, 0, // i32.const 0
kExprI32Const, 42, // i32.const 42
kExprI32StoreMem, 0, 0xff, 0xff, 0xff, 0xff, 0x0f, // i32.store offset = -1
]).index;
builder.addFunction('main', kSig_i_v).addBody([
kExprCallFunction, func_a_idx, // call $wasm_A
kExprI32Const, 0 // i32.const 0
])
.exportFunc();
const instance = builder.instantiate();
const main_f = instance.exports.main;
function f() {
var result = main_f();
return result;
}
f();

View File

@ -0,0 +1,28 @@
# 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.
# 0x00 (module
# 0x08 [type]
# 0x0a (type $type0 (func))
# 0x0e (type $type1 (func (result i32)))
# 0x12 [function]
# 0x17 [memory]
# 0x19 (memory $memory0 32 128)
# 0x1e [export]
# 0x20 (export "mem" (memory $memory0))
# 0x27 (export "main" (func $func1))
# 0x2e [code]
# 0x30 (func $func0
# 0x33 i32.const 0
# 0x35 i32.const 42
# 0x37 i32.store offset=-1 align=1
# 0x3e )
# 0x3f (func $func1 (result i32)
# 0x41 call $func0
# 0x43 i32.const 0
# 0x45 )
# 0x46 ...
# )
TRAP_ADDRESS = 0x0037

View File

@ -0,0 +1,37 @@
# 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.
# Flags: -expose-wasm --wasm-gdb-remote --wasm-pause-waiting-for-debugger test/debugging/wasm/gdb-server/test_files/test_trap.js
import sys
import unittest
import gdb_rsp
import test_files.test_trap as test_trap
# These are set up by Main().
COMMAND = None
class Tests(unittest.TestCase):
def test_trap(self):
with gdb_rsp.LaunchDebugStub(COMMAND) as connection:
module_load_addr = gdb_rsp.GetLoadedModuleAddress(connection)
reply = connection.RspRequest('c')
gdb_rsp.AssertReplySignal(reply, gdb_rsp.SIGSEGV)
tid = gdb_rsp.ParseThreadStopReply(reply)['thread_id']
self.assertEqual(tid, 1)
regs = gdb_rsp.DecodeRegs(connection.RspRequest('g'))
self.assertEqual(regs['pc'], module_load_addr + test_trap.TRAP_ADDRESS)
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()