diff --git a/include/js_protocol.pdl b/include/js_protocol.pdl index e35c78acd6..3b4ec166c5 100644 --- a/include/js_protocol.pdl +++ b/include/js_protocol.pdl @@ -169,6 +169,8 @@ domain Debugger # The maximum size in bytes of collected scripts (not referenced by other heap objects) # the debugger can hold. Puts no limit if paramter is omitted. experimental optional number maxScriptsCacheSize + # Whether to report Wasm modules as raw binaries instead of disassembled functions. + experimental optional boolean supportsWasmDwarf returns # Unique identifier of the debugger. experimental Runtime.UniqueDebuggerId debuggerId @@ -227,6 +229,15 @@ domain Debugger # Script source. string scriptSource + # Returns bytecode for the WebAssembly script with given id. + command getWasmBytecode + parameters + # Id of the Wasm script to get source for. + Runtime.ScriptId scriptId + returns + # Script source. + binary bytecode + # Returns stack trace with given `stackTraceId`. experimental command getStackTrace parameters diff --git a/src/api/api.cc b/src/api/api.cc index 081b106730..bffcf8ac48 100644 --- a/src/api/api.cc +++ b/src/api/api.cc @@ -9439,6 +9439,21 @@ int debug::WasmScript::NumImportedFunctions() const { return static_cast(module->num_imported_functions); } +bool debug::WasmScript::HasDwarf() const { + i::Handle script = Utils::OpenHandle(this); + DCHECK_EQ(i::Script::TYPE_WASM, script->type()); + i::wasm::NativeModule* native_module = script->wasm_native_module(); + const i::wasm::WasmModule* module = native_module->module(); + return module->has_dwarf; +} + +MemorySpan debug::WasmScript::Bytecode() const { + i::Handle script = Utils::OpenHandle(this); + i::Vector wire_bytes = + script->wasm_native_module()->wire_bytes(); + return {wire_bytes.begin(), wire_bytes.size()}; +} + std::pair debug::WasmScript::GetFunctionRange( int function_index) const { i::DisallowHeapAllocation no_gc; diff --git a/src/debug/debug-interface.h b/src/debug/debug-interface.h index 59844e740e..d0121aebef 100644 --- a/src/debug/debug-interface.h +++ b/src/debug/debug-interface.h @@ -159,6 +159,8 @@ class WasmScript : public Script { int NumFunctions() const; int NumImportedFunctions() const; + bool HasDwarf() const; + MemorySpan Bytecode() const; std::pair GetFunctionRange(int function_index) const; diff --git a/src/inspector/string-util.h b/src/inspector/string-util.h index 10c3cb4f33..9b6a8bdd5d 100644 --- a/src/inspector/string-util.h +++ b/src/inspector/string-util.h @@ -101,13 +101,23 @@ class StringUtil { // therefore it's unnecessary to provide an implementation here. class Binary { public: - const uint8_t* data() const { UNIMPLEMENTED(); } - size_t size() const { UNIMPLEMENTED(); } + Binary() = default; + + const uint8_t* data() const { return bytes_->data(); } + size_t size() const { return bytes_->size(); } String toBase64() const { UNIMPLEMENTED(); } static Binary fromBase64(const String& base64, bool* success) { UNIMPLEMENTED(); } - static Binary fromSpan(const uint8_t* data, size_t size) { UNIMPLEMENTED(); } + static Binary fromSpan(const uint8_t* data, size_t size) { + return Binary(std::make_shared>(data, data + size)); + } + + private: + std::shared_ptr> bytes_; + + explicit Binary(std::shared_ptr> bytes) + : bytes_(bytes) {} }; } // namespace protocol diff --git a/src/inspector/v8-debugger-agent-impl.cc b/src/inspector/v8-debugger-agent-impl.cc index f5b7de9e1f..a966707bed 100644 --- a/src/inspector/v8-debugger-agent-impl.cc +++ b/src/inspector/v8-debugger-agent-impl.cc @@ -350,11 +350,13 @@ void V8DebuggerAgentImpl::enableImpl() { } Response V8DebuggerAgentImpl::enable(Maybe maxScriptsCacheSize, + Maybe supportsWasmDwarf, String16* outDebuggerId) { m_maxScriptCacheSize = v8::base::saturated_cast( maxScriptsCacheSize.fromMaybe(std::numeric_limits::max())); *outDebuggerId = m_debugger->debuggerIdFor(m_session->contextGroupId()).toString(); + m_supportsWasmDwarf = supportsWasmDwarf.fromMaybe(false); if (enabled()) return Response::OK(); if (!m_inspector->client()->canExecuteScripts(m_session->contextGroupId())) @@ -962,6 +964,20 @@ Response V8DebuggerAgentImpl::getScriptSource(const String16& scriptId, return Response::OK(); } +Response V8DebuggerAgentImpl::getWasmBytecode(const String16& scriptId, + protocol::Binary* bytecode) { + if (!enabled()) return Response::Error(kDebuggerNotEnabled); + ScriptsMap::iterator it = m_scripts.find(scriptId); + if (it == m_scripts.end()) + return Response::Error("No script for id: " + scriptId); + v8::MemorySpan span; + if (!it->second->wasmBytecode().To(&span)) + return Response::Error("Script with id " + scriptId + + " is not WebAssembly"); + *bytecode = protocol::Binary::fromSpan(span.data(), span.size()); + return Response::OK(); +} + void V8DebuggerAgentImpl::pushBreakDetails( const String16& breakReason, std::unique_ptr breakAuxData) { diff --git a/src/inspector/v8-debugger-agent-impl.h b/src/inspector/v8-debugger-agent-impl.h index 0cd237cc53..372b588867 100644 --- a/src/inspector/v8-debugger-agent-impl.h +++ b/src/inspector/v8-debugger-agent-impl.h @@ -43,6 +43,7 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend { // Part of the protocol. Response enable(Maybe maxScriptsCacheSize, + Maybe supportsWasmDwarf, String16* outDebuggerId) override; Response disable() override; Response setBreakpointsActive(bool active) override; @@ -95,6 +96,8 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend { Maybe* asyncStackTraceId) override; Response getScriptSource(const String16& scriptId, String16* scriptSource) override; + Response getWasmBytecode(const String16& scriptId, + protocol::Binary* bytecode) override; Response pause() override; Response resume() override; Response stepOver() override; @@ -126,6 +129,7 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend { positions) override; bool enabled() const { return m_enabled; } + bool supportsWasmDwarf() const { return m_supportsWasmDwarf; } void setBreakpointFor(v8::Local function, v8::Local condition, @@ -229,6 +233,8 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend { std::unordered_map>> m_blackboxedPositions; + bool m_supportsWasmDwarf = false; + DISALLOW_COPY_AND_ASSIGN(V8DebuggerAgentImpl); }; diff --git a/src/inspector/v8-debugger-script.cc b/src/inspector/v8-debugger-script.cc index 5ec64237be..99511fc144 100644 --- a/src/inspector/v8-debugger-script.cc +++ b/src/inspector/v8-debugger-script.cc @@ -141,6 +141,12 @@ class ActualScript : public V8DebuggerScript { static_cast(pos), static_cast(substringLength)); return String16(buffer.get(), substringLength); } + v8::Maybe> wasmBytecode() const override { + v8::HandleScope scope(m_isolate); + auto script = this->script(); + if (!script->IsWasm()) return v8::Nothing>(); + return v8::Just(v8::debug::WasmScript::Cast(*script)->Bytecode()); + } int startLine() const override { return m_startLine; } int startColumn() const override { return m_startColumn; } int endLine() const override { return m_endLine; } @@ -281,9 +287,8 @@ class ActualScript : public V8DebuggerScript { m_startLine = script->LineOffset(); m_startColumn = script->ColumnOffset(); std::vector lineEnds = script->LineEnds(); - CHECK(lineEnds.size()); - int source_length = lineEnds[lineEnds.size() - 1]; if (lineEnds.size()) { + int source_length = lineEnds[lineEnds.size() - 1]; m_endLine = static_cast(lineEnds.size()) + m_startLine - 1; if (lineEnds.size() > 1) { m_endColumn = source_length - lineEnds[lineEnds.size() - 2] - 1; @@ -356,6 +361,9 @@ class WasmVirtualScript : public V8DebuggerScript { return m_wasmTranslation->GetSource(m_id, m_functionIndex) .substring(pos, len); } + v8::Maybe> wasmBytecode() const override { + return v8::Nothing>(); + } int startLine() const override { return m_wasmTranslation->GetStartLine(m_id, m_functionIndex); } diff --git a/src/inspector/v8-debugger-script.h b/src/inspector/v8-debugger-script.h index 82b29413d7..b53d2c15aa 100644 --- a/src/inspector/v8-debugger-script.h +++ b/src/inspector/v8-debugger-script.h @@ -63,6 +63,7 @@ class V8DebuggerScript { virtual const String16& sourceMappingURL() const = 0; virtual String16 source(size_t pos, size_t len = UINT_MAX) const = 0; + virtual v8::Maybe> wasmBytecode() const = 0; virtual const String16& hash() const = 0; virtual int startLine() const = 0; virtual int startColumn() const = 0; diff --git a/src/inspector/v8-debugger.cc b/src/inspector/v8-debugger.cc index 9ee74bcafd..8f4750e54c 100644 --- a/src/inspector/v8-debugger.cc +++ b/src/inspector/v8-debugger.cc @@ -507,31 +507,36 @@ size_t V8Debugger::nearHeapLimitCallback(void* data, size_t current_heap_limit, void V8Debugger::ScriptCompiled(v8::Local script, bool is_live_edited, bool has_compile_error) { + if (m_ignoreScriptParsedEventsCounter != 0) return; + int contextId; if (!script->ContextId().To(&contextId)) return; - if (script->IsWasm() && script->SourceMappingURL().IsEmpty()) { - WasmTranslation* wasmTranslation = &m_wasmTranslation; - m_inspector->forEachSession( - m_inspector->contextGroupId(contextId), - [&script, &wasmTranslation](V8InspectorSessionImpl* session) { - if (!session->debuggerAgent()->enabled()) return; - wasmTranslation->AddScript(script.As(), - session->debuggerAgent()); - }); - } else if (m_ignoreScriptParsedEventsCounter == 0) { - v8::Isolate* isolate = m_isolate; - V8InspectorClient* client = m_inspector->client(); - m_inspector->forEachSession( - m_inspector->contextGroupId(contextId), - [&isolate, &script, &has_compile_error, &is_live_edited, - &client](V8InspectorSessionImpl* session) { - if (!session->debuggerAgent()->enabled()) return; - session->debuggerAgent()->didParseSource( - V8DebuggerScript::Create(isolate, script, is_live_edited, - session->debuggerAgent(), client), - !has_compile_error); - }); - } + + v8::Isolate* isolate = m_isolate; + V8InspectorClient* client = m_inspector->client(); + WasmTranslation& wasmTranslation = m_wasmTranslation; + + m_inspector->forEachSession( + m_inspector->contextGroupId(contextId), + [isolate, &script, has_compile_error, is_live_edited, client, + &wasmTranslation](V8InspectorSessionImpl* session) { + auto agent = session->debuggerAgent(); + if (!agent->enabled()) return; + if (script->IsWasm() && script->SourceMappingURL().IsEmpty() && + !(agent->supportsWasmDwarf() && + script.As()->HasDwarf())) { + wasmTranslation.AddScript(script.As(), agent); + } else { + auto debuggerScript = V8DebuggerScript::Create( + isolate, script, is_live_edited, agent, client); + if (script->IsWasm() && script->SourceMappingURL().IsEmpty()) { + DCHECK(agent->supportsWasmDwarf()); + DCHECK(script.As()->HasDwarf()); + debuggerScript->setSourceMappingURL("wasm://dwarf"); + } + agent->didParseSource(std::move(debuggerScript), !has_compile_error); + } + }); } void V8Debugger::BreakProgramRequested( diff --git a/src/wasm/module-decoder.cc b/src/wasm/module-decoder.cc index f14e75bb00..cad2386ac5 100644 --- a/src/wasm/module-decoder.cc +++ b/src/wasm/module-decoder.cc @@ -30,6 +30,7 @@ namespace { constexpr char kNameString[] = "name"; constexpr char kSourceMappingURLString[] = "sourceMappingURL"; constexpr char kCompilationHintsString[] = "compilationHints"; +constexpr char kDebugInfoString[] = ".debug_info"; template constexpr size_t num_chars(const char (&)[N]) { @@ -88,6 +89,8 @@ const char* SectionName(SectionCode code) { return kNameString; case kSourceMappingURLSectionCode: return kSourceMappingURLString; + case kDebugInfoSectionCode: + return kDebugInfoString; case kCompilationHintsSectionCode: return kCompilationHintsString; default: @@ -398,6 +401,10 @@ class ModuleDecoderImpl : public Decoder { // sourceMappingURL is a custom section and currently can occur anywhere // in the module. In case of multiple sourceMappingURL sections, all // except the first occurrence are ignored. + case kDebugInfoSectionCode: + // .debug_info is a custom section containing core DWARF information + // if produced by compiler. Its presence likely means that Wasm was + // built in a debug mode. case kCompilationHintsSectionCode: // TODO(frgossen): report out of place compilation hints section as a // warning. @@ -452,6 +459,10 @@ class ModuleDecoderImpl : public Decoder { case kSourceMappingURLSectionCode: DecodeSourceMappingURLSection(); break; + case kDebugInfoSectionCode: + module_->has_dwarf = true; + consume_bytes(static_cast(end_ - start_), ".debug_info"); + break; case kCompilationHintsSectionCode: if (enabled_features_.compilation_hints) { DecodeCompilationHintsSection(); @@ -1950,6 +1961,10 @@ SectionCode ModuleDecoder::IdentifyUnknownSection(Decoder* decoder, kCompilationHintsString, num_chars(kCompilationHintsString)) == 0) { return kCompilationHintsSectionCode; + } else if (string.length() == num_chars(kDebugInfoString) && + strncmp(reinterpret_cast(section_name_start), + kDebugInfoString, num_chars(kDebugInfoString)) == 0) { + return kDebugInfoSectionCode; } return kUnknownSectionCode; } diff --git a/src/wasm/wasm-constants.h b/src/wasm/wasm-constants.h index fbbe19396c..2b5cb6c9ec 100644 --- a/src/wasm/wasm-constants.h +++ b/src/wasm/wasm-constants.h @@ -81,6 +81,7 @@ enum SectionCode : int8_t { // to be consistent. kNameSectionCode, // Name section (encoded as a string) kSourceMappingURLSectionCode, // Source Map URL section + kDebugInfoSectionCode, // DWARF section .debug_info kCompilationHintsSectionCode, // Compilation hints section // Helper values diff --git a/src/wasm/wasm-module.h b/src/wasm/wasm-module.h index 600020219b..6367525000 100644 --- a/src/wasm/wasm-module.h +++ b/src/wasm/wasm-module.h @@ -193,6 +193,7 @@ struct V8_EXPORT_PRIVATE WasmModule { bool has_maximum_pages = false; // true if there is a maximum memory size bool has_memory = false; // true if the memory was defined or imported bool mem_export = false; // true if the memory is exported + bool has_dwarf = false; // true if .debug_info section is present int start_function_index = -1; // start function, >= 0 if any std::vector globals; diff --git a/test/inspector/debugger/wasm-scripts-expected.txt b/test/inspector/debugger/wasm-scripts-expected.txt index e04fe30791..2a06ecac24 100644 --- a/test/inspector/debugger/wasm-scripts-expected.txt +++ b/test/inspector/debugger/wasm-scripts-expected.txt @@ -1,15 +1,16 @@ Tests how wasm scripts are reported Check that each inspector gets two wasm scripts at module creation time. -Session #1: Script #0 parsed. URL: wasm://wasm/wasm-7b04570e/wasm-7b04570e-0 -Session #1: Script #1 parsed. URL: wasm://wasm/wasm-7b04570e/wasm-7b04570e-1 -Session #2: Script #0 parsed. URL: wasm://wasm/wasm-7b04570e/wasm-7b04570e-0 -Session #2: Script #1 parsed. URL: wasm://wasm/wasm-7b04570e/wasm-7b04570e-1 -Session #1: Source for wasm://wasm/wasm-7b04570e/wasm-7b04570e-0: +Session #1: Script #0 parsed. URL: wasm://wasm/wasm-77a937ae/wasm-77a937ae-0 +Session #1: Script #1 parsed. URL: wasm://wasm/wasm-77a937ae/wasm-77a937ae-1 +Session #2: Script #0 parsed. URL: wasm://wasm/wasm-77a937ae/wasm-77a937ae-0 +Session #2: Script #1 parsed. URL: wasm://wasm/wasm-77a937ae/wasm-77a937ae-1 +Session #3: Script #0 parsed. URL: wasm-77a937ae +Session #1: Source for wasm://wasm/wasm-77a937ae/wasm-77a937ae-0: func $nopFunction nop end -Session #1: Source for wasm://wasm/wasm-7b04570e/wasm-7b04570e-1: +Session #1: Source for wasm://wasm/wasm-77a937ae/wasm-77a937ae-1: func $main block i32.const 2 @@ -17,15 +18,20 @@ func $main end end -Session #2: Source for wasm://wasm/wasm-7b04570e/wasm-7b04570e-0: +Session #2: Source for wasm://wasm/wasm-77a937ae/wasm-77a937ae-0: func $nopFunction nop end -Session #2: Source for wasm://wasm/wasm-7b04570e/wasm-7b04570e-1: +Session #2: Source for wasm://wasm/wasm-77a937ae/wasm-77a937ae-1: func $main block i32.const 2 drop end end + +Session #3: Source for wasm-77a937ae: +Raw: 00 61 73 6d 01 00 00 00 01 07 02 60 00 00 60 00 00 03 03 02 00 01 07 08 01 04 6d 61 69 6e 00 01 0a 0e 02 03 00 01 0b 08 00 02 40 41 02 1a 0b 0b 00 0c 0b 2e 64 65 62 75 67 5f 69 6e 66 6f 00 1b 04 6e 61 6d 65 01 14 02 00 0b 6e 6f 70 46 75 6e 63 74 69 6f 6e 01 04 6d 61 69 6e +Imports: [] +Exports: [main: function] diff --git a/test/inspector/debugger/wasm-scripts.js b/test/inspector/debugger/wasm-scripts.js index 50d916d14f..3259fcd553 100644 --- a/test/inspector/debugger/wasm-scripts.js +++ b/test/inspector/debugger/wasm-scripts.js @@ -13,6 +13,8 @@ let sessions = [ // Extra session to verify that all inspectors get same messages. // See https://bugs.chromium.org/p/v8/issues/detail?id=9725. trackScripts(), + // Another session to check how raw Wasm is reported. + trackScripts({ supportsWasmDwarf: true }), ]; utils.load('test/mjsunit/wasm/wasm-module-builder.js'); @@ -24,6 +26,7 @@ builder.addFunction('nopFunction', kSig_v_v).addBody([kExprNop]); builder.addFunction('main', kSig_v_v) .addBody([kExprBlock, kWasmStmt, kExprI32Const, 2, kExprDrop, kExprEnd]) .exportAs('main'); +builder.addCustomSection('.debug_info', []); var module_bytes = builder.toArray(); function testFunction(bytes) { @@ -48,30 +51,68 @@ sessions[0].Protocol.Runtime Promise.all(sessions.map(session => session.getScripts())) )) .catch(err => { - InspectorTest.log("FAIL: " + err.message); + InspectorTest.log(err.stack); }) .then(() => InspectorTest.completeTest()); -function trackScripts() { +function decodeBase64(base64) { + const LOOKUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + const paddingLength = base64.match(/=*$/)[0].length; + const bytesLength = base64.length * 0.75 - paddingLength; + + let bytes = new Uint8Array(bytesLength); + + for (let i = 0, p = 0; i < base64.length; i += 4, p += 3) { + let bits = 0; + for (let j = 0; j < 4; j++) { + bits <<= 6; + const c = base64[i + j]; + if (c !== '=') bits |= LOOKUP.indexOf(c); + } + for (let j = p + 2; j >= p; j--) { + if (j < bytesLength) bytes[j] = bits; + bits >>= 8; + } + } + + return bytes; +} + +function trackScripts(debuggerParams) { let {id: sessionId, Protocol} = contextGroup.connect(); let scripts = []; - Protocol.Debugger.enable(); + Protocol.Debugger.enable(debuggerParams); Protocol.Debugger.onScriptParsed(handleScriptParsed); - async function getScript({url, scriptId}) { - let {result: {scriptSource}} = await Protocol.Debugger.getScriptSource({scriptId}); + async function loadScript({url, scriptId}, raw) { + InspectorTest.log(`Session #${sessionId}: Script #${scripts.length} parsed. URL: ${url}`); + let scriptSource; + if (raw) { + let {result: {bytecode}} = await Protocol.Debugger.getWasmBytecode({scriptId}); + // Binary value is represented as base64 in JSON, decode it. + bytecode = decodeBase64(bytecode); + // Check that it can be parsed back to a WebAssembly module. + let module = new WebAssembly.Module(bytecode); + scriptSource = ` +Raw: ${Array.from(bytecode, b => ('0' + b.toString(16)).slice(-2)).join(' ')} +Imports: [${WebAssembly.Module.imports(module).map(i => `${i.name}: ${i.kind} from "${i.module}"`).join(', ')}] +Exports: [${WebAssembly.Module.exports(module).map(e => `${e.name}: ${e.kind}`).join(', ')}] + `.trim(); + } else { + ({result: {scriptSource}} = await Protocol.Debugger.getScriptSource({scriptId})); + } InspectorTest.log(`Session #${sessionId}: Source for ${url}:`); InspectorTest.log(scriptSource); - return {url, scriptSource}; } function handleScriptParsed({params}) { - let {url} = params; - if (!url.startsWith("wasm://")) return; - - InspectorTest.log(`Session #${sessionId}: Script #${scripts.length} parsed. URL: ${url}`); - scripts.push(getScript(params)); + if (params.sourceMapURL === "wasm://dwarf") { + scripts.push(loadScript(params, true)); + } else if (params.url.startsWith("wasm://")) { + scripts.push(loadScript(params)); + } } return {