Add support for reporting raw Wasm scripts

This addition will allow to experiment with parsing DWARF information from
WebAssembly on the frontend side for improved debugging.

The frontend must explicitly opt-in to this experiment by setting
`supportsWasmDwarf: true` in `Debugger.enable` params.

When this option is present, and Wasm appears to contain DWARF information
(heuristic: `.debug_info` custom section is present), V8 will not try to
disassemble and report each WebAssembly function as a separate fake script, but
instead will report Wasm module as a whole.

Note that V8 already does this when Wasm is associated with a source map.

Additionally, this CL adds a dedicated `Debugger.getWasmBytecode` command that
accepts scriptId and returns raw wire bytes of the chosen WebAssembly module.

Change-Id: I7a6e80daf8d91ffaaba04fa15688f2ba9552870f
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1809375
Commit-Queue: Ingvar Stepanyan <rreverser@google.com>
Reviewed-by: Yang Guo <yangguo@chromium.org>
Reviewed-by: Michael Starzinger <mstarzinger@chromium.org>
Cr-Commit-Position: refs/heads/master@{#63969}
This commit is contained in:
Ingvar Stepanyan 2019-09-24 19:46:49 +00:00 committed by Commit Bot
parent 01e52d8068
commit c7848612d8
14 changed files with 185 additions and 47 deletions

View File

@ -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

View File

@ -9439,6 +9439,21 @@ int debug::WasmScript::NumImportedFunctions() const {
return static_cast<int>(module->num_imported_functions);
}
bool debug::WasmScript::HasDwarf() const {
i::Handle<i::Script> 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<const uint8_t> debug::WasmScript::Bytecode() const {
i::Handle<i::Script> script = Utils::OpenHandle(this);
i::Vector<const uint8_t> wire_bytes =
script->wasm_native_module()->wire_bytes();
return {wire_bytes.begin(), wire_bytes.size()};
}
std::pair<int, int> debug::WasmScript::GetFunctionRange(
int function_index) const {
i::DisallowHeapAllocation no_gc;

View File

@ -159,6 +159,8 @@ class WasmScript : public Script {
int NumFunctions() const;
int NumImportedFunctions() const;
bool HasDwarf() const;
MemorySpan<const uint8_t> Bytecode() const;
std::pair<int, int> GetFunctionRange(int function_index) const;

View File

@ -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<std::vector<uint8_t>>(data, data + size));
}
private:
std::shared_ptr<std::vector<uint8_t>> bytes_;
explicit Binary(std::shared_ptr<std::vector<uint8_t>> bytes)
: bytes_(bytes) {}
};
} // namespace protocol

View File

@ -350,11 +350,13 @@ void V8DebuggerAgentImpl::enableImpl() {
}
Response V8DebuggerAgentImpl::enable(Maybe<double> maxScriptsCacheSize,
Maybe<bool> supportsWasmDwarf,
String16* outDebuggerId) {
m_maxScriptCacheSize = v8::base::saturated_cast<size_t>(
maxScriptsCacheSize.fromMaybe(std::numeric_limits<double>::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<const uint8_t> 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<protocol::DictionaryValue> breakAuxData) {

View File

@ -43,6 +43,7 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend {
// Part of the protocol.
Response enable(Maybe<double> maxScriptsCacheSize,
Maybe<bool> supportsWasmDwarf,
String16* outDebuggerId) override;
Response disable() override;
Response setBreakpointsActive(bool active) override;
@ -95,6 +96,8 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend {
Maybe<protocol::Runtime::StackTraceId>* 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<v8::Function> function,
v8::Local<v8::String> condition,
@ -229,6 +233,8 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend {
std::unordered_map<String16, std::vector<std::pair<int, int>>>
m_blackboxedPositions;
bool m_supportsWasmDwarf = false;
DISALLOW_COPY_AND_ASSIGN(V8DebuggerAgentImpl);
};

View File

@ -141,6 +141,12 @@ class ActualScript : public V8DebuggerScript {
static_cast<int>(pos), static_cast<int>(substringLength));
return String16(buffer.get(), substringLength);
}
v8::Maybe<v8::MemorySpan<const uint8_t>> wasmBytecode() const override {
v8::HandleScope scope(m_isolate);
auto script = this->script();
if (!script->IsWasm()) return v8::Nothing<v8::MemorySpan<const uint8_t>>();
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<int> 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<int>(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<v8::MemorySpan<const uint8_t>> wasmBytecode() const override {
return v8::Nothing<v8::MemorySpan<const uint8_t>>();
}
int startLine() const override {
return m_wasmTranslation->GetStartLine(m_id, m_functionIndex);
}

View File

@ -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<v8::MemorySpan<const uint8_t>> wasmBytecode() const = 0;
virtual const String16& hash() const = 0;
virtual int startLine() const = 0;
virtual int startColumn() const = 0;

View File

@ -507,31 +507,36 @@ size_t V8Debugger::nearHeapLimitCallback(void* data, size_t current_heap_limit,
void V8Debugger::ScriptCompiled(v8::Local<v8::debug::Script> 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<v8::debug::WasmScript>(),
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<v8::debug::WasmScript>()->HasDwarf())) {
wasmTranslation.AddScript(script.As<v8::debug::WasmScript>(), 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<v8::debug::WasmScript>()->HasDwarf());
debuggerScript->setSourceMappingURL("wasm://dwarf");
}
agent->didParseSource(std::move(debuggerScript), !has_compile_error);
}
});
}
void V8Debugger::BreakProgramRequested(

View File

@ -30,6 +30,7 @@ namespace {
constexpr char kNameString[] = "name";
constexpr char kSourceMappingURLString[] = "sourceMappingURL";
constexpr char kCompilationHintsString[] = "compilationHints";
constexpr char kDebugInfoString[] = ".debug_info";
template <size_t N>
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<uint32_t>(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<const char*>(section_name_start),
kDebugInfoString, num_chars(kDebugInfoString)) == 0) {
return kDebugInfoSectionCode;
}
return kUnknownSectionCode;
}

View File

@ -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

View File

@ -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<WasmGlobal> globals;

View File

@ -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]

View File

@ -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 {