| // 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 <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/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( |
| "Cannot initialize thread, GDB-remote debugging will be disabled.\n"); |
| return nullptr; |
| } |
| return gdb_server; |
| } |
| |
| 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(); |
| } |
| } |
| |
| void GdbServer::RunMessageLoopOnPause() { task_runner_->Run(); } |
| |
| 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::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::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; |
| } |
| |
| 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) { |
| // 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 wasm_addr, uint8_t* buffer, |
| uint32_t size) { |
| // 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 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 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 { |
| // 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 |
| } // namespace wasm |
| } // namespace internal |
| } // namespace v8 |