Commit 966e6f02 authored by Jakob Kummerow's avatar Jakob Kummerow Committed by V8 LUCI CQ

[wasm] Expose disassembler to DevTools

Bug: v8:12917
Change-Id: I8942664831c591f9b5566ee5b1609f68948601e7
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3749208Reviewed-by: 's avatarManos Koukoutos <manoskouk@chromium.org>
Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
Reviewed-by: 's avatarPhilip Pfaffe <pfaffe@chromium.org>
Cr-Commit-Position: refs/heads/main@{#81826}
parent 7cad31f4
......@@ -2540,6 +2540,7 @@ filegroup(
"src/wasm/wasm-debug.cc",
"src/wasm/wasm-debug.h",
"src/wasm/wasm-disassembler.cc",
"src/wasm/wasm-disassembler.h",
"src/wasm/wasm-disassembler-impl.h",
"src/wasm/wasm-engine.cc",
"src/wasm/wasm-engine.h",
......
......@@ -3646,6 +3646,7 @@ v8_header_set("v8_internal_headers") {
"src/wasm/wasm-code-manager.h",
"src/wasm/wasm-debug.h",
"src/wasm/wasm-disassembler-impl.h",
"src/wasm/wasm-disassembler.h",
"src/wasm/wasm-engine.h",
"src/wasm/wasm-external-refs.h",
"src/wasm/wasm-feature-flags.h",
......
......@@ -24,6 +24,7 @@
#if V8_ENABLE_WEBASSEMBLY
#include "src/debug/debug-wasm-objects-inl.h"
#include "src/wasm/wasm-disassembler.h"
#include "src/wasm/wasm-engine.h"
#endif // V8_ENABLE_WEBASSEMBLY
......@@ -846,6 +847,37 @@ int WasmScript::GetContainingFunction(int byte_offset) const {
return i::wasm::GetContainingWasmFunction(module, byte_offset);
}
void WasmScript::GetAllFunctionStarts(std::vector<int>& starts) const {
i::DisallowGarbageCollection no_gc;
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();
size_t num_functions = module->functions.size();
starts.resize(num_functions + 1);
for (size_t i = 0; i < num_functions; i++) {
const i::wasm::WasmFunction& f = module->functions[i];
starts[i] = f.code.offset();
}
if (num_functions > 0) {
starts[num_functions] =
module->functions[num_functions - 1].code.end_offset();
} else {
starts[0] = 0;
}
}
void WasmScript::Disassemble(DisassemblyCollector* collector) {
i::DisallowGarbageCollection no_gc;
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();
i::wasm::ModuleWireBytes wire_bytes(native_module->wire_bytes());
i::wasm::Disassemble(module, wire_bytes, native_module->GetNamesProvider(),
collector);
}
uint32_t WasmScript::GetFunctionHash(int function_index) {
i::DisallowGarbageCollection no_gc;
i::Handle<i::Script> script = Utils::OpenHandle(this);
......
......@@ -237,6 +237,13 @@ class V8_EXPORT_PRIVATE Script {
bool SetInstrumentationBreakpoint(BreakpointId* id) const;
};
class DisassemblyCollector {
public:
virtual void ReserveLineCount(size_t count) = 0;
virtual void AddLine(const char* src, size_t length,
uint32_t bytecode_offset) = 0;
};
#if V8_ENABLE_WEBASSEMBLY
// Specialization for wasm Scripts.
class WasmScript : public Script {
......@@ -251,6 +258,11 @@ class WasmScript : public Script {
std::pair<int, int> GetFunctionRange(int function_index) const;
int GetContainingFunction(int byte_offset) const;
// For N functions, {starts} will have N+1 entries: the last is the offset of
// the first byte after the end of the last function.
void GetAllFunctionStarts(std::vector<int>& starts) const;
void Disassemble(DisassemblyCollector* collector);
uint32_t GetFunctionHash(int function_index);
......
......@@ -443,6 +443,7 @@ Response V8DebuggerAgentImpl::disable() {
}
m_breakpointIdToDebuggerBreakpointIds.clear();
m_debuggerBreakpointIdToBreakpointId.clear();
m_wasmDisassemblies.clear();
m_debugger->setAsyncCallStackDepth(this, 0);
clearBreakDetails();
m_skipAllPauses = false;
......@@ -1131,23 +1132,109 @@ Response V8DebuggerAgentImpl::getScriptSource(
#endif // V8_ENABLE_WEBASSEMBLY
return Response::Success();
}
struct DisassemblyChunk {
DisassemblyChunk() = default;
DisassemblyChunk(const DisassemblyChunk& other) = delete;
DisassemblyChunk& operator=(const DisassemblyChunk& other) = delete;
DisassemblyChunk(DisassemblyChunk&& other) V8_NOEXCEPT = default;
DisassemblyChunk& operator=(DisassemblyChunk&& other) V8_NOEXCEPT = default;
std::vector<String16> lines;
std::vector<int> lineOffsets;
void Reserve(size_t size) {
lines.reserve(size);
lineOffsets.reserve(size);
}
};
class DisassemblyCollectorImpl final : public v8::debug::DisassemblyCollector {
public:
DisassemblyCollectorImpl() = default;
void ReserveLineCount(size_t count) override {
if (count == 0) return;
size_t num_chunks = (count + kLinesPerChunk - 1) / kLinesPerChunk;
chunks_.resize(num_chunks);
for (size_t i = 0; i < num_chunks - 1; i++) {
chunks_[i].Reserve(kLinesPerChunk);
}
size_t last = num_chunks - 1;
size_t last_size = count % kLinesPerChunk;
if (last_size == 0) last_size = kLinesPerChunk;
chunks_[last].Reserve(last_size);
}
void AddLine(const char* src, size_t length,
uint32_t bytecode_offset) override {
chunks_[writing_chunk_index_].lines.emplace_back(src, length);
chunks_[writing_chunk_index_].lineOffsets.push_back(
static_cast<int>(bytecode_offset));
if (chunks_[writing_chunk_index_].lines.size() == kLinesPerChunk) {
writing_chunk_index_++;
}
total_number_of_lines_++;
}
size_t total_number_of_lines() { return total_number_of_lines_; }
bool HasNextChunk() { return reading_chunk_index_ < chunks_.size(); }
DisassemblyChunk NextChunk() {
return std::move(chunks_[reading_chunk_index_++]);
}
private:
// For a large Ritz module, the average is about 50 chars per line,
// so (with 2-byte String16 chars) this should give approximately 20 MB
// per chunk.
static constexpr size_t kLinesPerChunk = 200'000;
size_t writing_chunk_index_ = 0;
size_t reading_chunk_index_ = 0;
size_t total_number_of_lines_ = 0;
std::vector<DisassemblyChunk> chunks_;
};
Response V8DebuggerAgentImpl::disassembleWasmModule(
const String16& in_scriptId, Maybe<String16>* out_streamId,
int* out_totalNumberOfLines,
std::unique_ptr<protocol::Array<int>>* out_functionBodyOffsets,
std::unique_ptr<protocol::Debugger::WasmDisassemblyChunk>* out_chunk) {
#if V8_ENABLE_WEBASSEMBLY
std::vector<String16> lines{{"a", "b", "c"}};
std::vector<int> lineOffsets{{0, 4, 8}};
*out_functionBodyOffsets = std::make_unique<protocol::Array<int>>();
*out_chunk =
protocol::Debugger::WasmDisassemblyChunk::create()
.setBytecodeOffsets(
std::make_unique<protocol::Array<int>>(std::move(lineOffsets)))
.setLines(
std::make_unique<protocol::Array<String16>>(std::move(lines)))
.build();
*out_totalNumberOfLines = 3;
if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
ScriptsMap::iterator it = m_scripts.find(in_scriptId);
if (it == m_scripts.end()) {
return Response::InvalidParams("No script for id: " + in_scriptId.utf8());
}
V8DebuggerScript* script = it->second.get();
if (script->getLanguage() != V8DebuggerScript::Language::WebAssembly) {
return Response::InvalidParams("Script with id " + in_scriptId.utf8() +
" is not WebAssembly");
}
std::unique_ptr<DisassemblyCollectorImpl> collector =
std::make_unique<DisassemblyCollectorImpl>();
script->Disassemble(collector.get());
*out_totalNumberOfLines =
static_cast<int>(collector->total_number_of_lines());
std::vector<int> functionBodyOffsets;
script->GetAllFunctionStarts(functionBodyOffsets);
*out_functionBodyOffsets =
std::make_unique<protocol::Array<int>>(std::move(functionBodyOffsets));
// Even an empty module would disassemble to "(module)", never to zero lines.
DCHECK(collector->HasNextChunk());
DisassemblyChunk chunk(collector->NextChunk());
*out_chunk = protocol::Debugger::WasmDisassemblyChunk::create()
.setBytecodeOffsets(std::make_unique<protocol::Array<int>>(
std::move(chunk.lineOffsets)))
.setLines(std::make_unique<protocol::Array<String16>>(
std::move(chunk.lines)))
.build();
if (collector->HasNextChunk()) {
String16 streamId = String16::fromInteger(m_nextWasmDisassemblyStreamId++);
*out_streamId = streamId;
m_wasmDisassemblies[streamId] = std::move(collector);
}
return Response::Success();
#else
return Response::ServerError("WebAssembly is disabled");
......@@ -1157,10 +1244,28 @@ Response V8DebuggerAgentImpl::nextWasmDisassemblyChunk(
const String16& in_streamId,
std::unique_ptr<protocol::Debugger::WasmDisassemblyChunk>* out_chunk) {
#if V8_ENABLE_WEBASSEMBLY
*out_chunk = protocol::Debugger::WasmDisassemblyChunk::create()
.setBytecodeOffsets(std::make_unique<protocol::Array<int>>())
.setLines(std::make_unique<protocol::Array<String16>>())
.build();
if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
auto it = m_wasmDisassemblies.find(in_streamId);
if (it == m_wasmDisassemblies.end()) {
return Response::InvalidParams("No chunks available for stream " +
in_streamId.utf8());
}
if (it->second->HasNextChunk()) {
DisassemblyChunk chunk(it->second->NextChunk());
*out_chunk = protocol::Debugger::WasmDisassemblyChunk::create()
.setBytecodeOffsets(std::make_unique<protocol::Array<int>>(
std::move(chunk.lineOffsets)))
.setLines(std::make_unique<protocol::Array<String16>>(
std::move(chunk.lines)))
.build();
} else {
*out_chunk =
protocol::Debugger::WasmDisassemblyChunk::create()
.setBytecodeOffsets(std::make_unique<protocol::Array<int>>())
.setLines(std::make_unique<protocol::Array<String16>>())
.build();
m_wasmDisassemblies.erase(it);
}
return Response::Success();
#else
return Response::ServerError("WebAssembly is disabled");
......
......@@ -19,6 +19,7 @@
namespace v8_inspector {
struct ScriptBreakpoint;
class DisassemblyCollectorImpl;
class V8Debugger;
class V8DebuggerScript;
class V8InspectorImpl;
......@@ -234,6 +235,9 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend {
std::unordered_map<v8::debug::BreakpointId,
std::unique_ptr<protocol::DictionaryValue>>
m_breakpointsOnScriptRun;
std::map<String16, std::unique_ptr<DisassemblyCollectorImpl>>
m_wasmDisassemblies;
size_t m_nextWasmDisassemblyStreamId = 0;
size_t m_maxScriptCacheSize = 0;
size_t m_cachedScriptSize = 0;
......
......@@ -97,6 +97,20 @@ class ActualScript : public V8DebuggerScript {
if (external_url.size() == 0) return v8::Nothing<String16>();
return v8::Just(String16(external_url.data(), external_url.size()));
}
void GetAllFunctionStarts(std::vector<int>& starts) const override {
v8::HandleScope scope(m_isolate);
v8::Local<v8::debug::Script> script = this->script();
DCHECK(script->IsWasm());
v8::debug::WasmScript::Cast(*script)->GetAllFunctionStarts(starts);
}
void Disassemble(v8::debug::DisassemblyCollector* collector) const override {
v8::HandleScope scope(m_isolate);
v8::Local<v8::debug::Script> script = this->script();
DCHECK(script->IsWasm());
v8::debug::WasmScript::Cast(*script)->Disassemble(collector);
}
#endif // V8_ENABLE_WEBASSEMBLY
int startLine() const override { return m_startLine; }
......
......@@ -105,6 +105,9 @@ class V8DebuggerScript {
getDebugSymbolsType() const = 0;
virtual v8::Maybe<String16> getExternalDebugSymbolsURL() const = 0;
void removeWasmBreakpoint(int id);
virtual void GetAllFunctionStarts(std::vector<int>& starts) const = 0;
virtual void Disassemble(
v8::debug::DisassemblyCollector* collector) const = 0;
#endif // V8_ENABLE_WEBASSEMBLY
protected:
......
......@@ -17,6 +17,11 @@
#include "src/wasm/string-builder.h"
namespace v8 {
namespace debug {
class DisassemblyCollector;
} // namespace debug
namespace internal {
namespace wasm {
......@@ -107,6 +112,9 @@ class MultiLineStringBuilder : public StringBuilder {
l.len = patched_length;
}
// Note: implemented in wasm-disassembler.cc (which is also the only user).
void ToDisassemblyCollector(v8::debug::DisassemblyCollector* collector);
void DumpToStdout() {
if (length() != 0) NextLine(0);
......
......@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/wasm/wasm-disassembler.h"
#include "src/debug/debug-interface.h"
#include "src/numbers/conversions.h"
#include "src/wasm/module-decoder-impl.h"
#include "src/wasm/names-provider.h"
......@@ -12,6 +15,30 @@ namespace v8 {
namespace internal {
namespace wasm {
////////////////////////////////////////////////////////////////////////////////
// Public interface.
void Disassemble(const WasmModule* module, ModuleWireBytes wire_bytes,
NamesProvider* names,
v8::debug::DisassemblyCollector* collector) {
MultiLineStringBuilder out;
AccountingAllocator allocator;
ModuleDisassembler md(out, module, names, wire_bytes,
ModuleDisassembler::kIncludeByteOffsets, &allocator);
md.PrintModule({0, 2});
out.ToDisassemblyCollector(collector);
}
void MultiLineStringBuilder::ToDisassemblyCollector(
v8::debug::DisassemblyCollector* collector) {
if (length() != 0) NextLine(0); // Finalize last line.
collector->ReserveLineCount(lines_.size());
for (const Line& l : lines_) {
// Don't include trailing '\n'.
collector->AddLine(l.data, l.len - 1, l.bytecode_offset);
}
}
////////////////////////////////////////////////////////////////////////////////
// Helpers.
......@@ -156,6 +183,7 @@ void FunctionBodyDisassembler::DecodeAsWat(MultiLineStringBuilder& out,
out.NextLine(pc_offset());
}
consume_bytes(locals_length);
out.set_current_line_bytecode_offset(pc_offset());
// Main loop.
while (pc_ < end_) {
......@@ -198,8 +226,8 @@ void FunctionBodyDisassembler::DecodeAsWat(MultiLineStringBuilder& out,
}
length = PrintImmediatesAndGetLength(out);
out.NextLine(pc_offset());
pc_ += length;
out.NextLine(pc_offset());
}
if (pc_ != end_) {
......@@ -728,7 +756,7 @@ void ModuleDisassembler::PrintModule(Indentation indentation) {
// I. Module name.
out_ << indentation << "(module";
if (module_->name.is_set()) {
out_ << " ";
out_ << " $";
const byte* name_start = start_ + module_->name.offset();
out_.write(name_start, module_->name.length());
}
......@@ -931,6 +959,8 @@ void ModuleDisassembler::PrintModule(Indentation indentation) {
}
indentation.decrease();
out_.set_current_line_bytecode_offset(
static_cast<uint32_t>(wire_bytes_.length()));
out_ << indentation << ")"; // End of the module.
out_.NextLine(0);
}
......
// Copyright 2022 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.
#if !V8_ENABLE_WEBASSEMBLY
#error This header should only be included if WebAssembly is enabled.
#endif // !V8_ENABLE_WEBASSEMBLY
#ifndef V8_WASM_WASM_DISASSEMBLER_H_
#define V8_WASM_WASM_DISASSEMBLER_H_
#include "src/wasm/wasm-module.h"
namespace v8 {
namespace debug {
class DisassemblyCollector;
} // namespace debug
namespace internal {
namespace wasm {
class NamesProvider;
void Disassemble(const WasmModule* module, ModuleWireBytes wire_bytes,
NamesProvider* names,
v8::debug::DisassemblyCollector* collector);
} // namespace wasm
} // namespace internal
} // namespace v8
#endif // V8_WASM_WASM_DISASSEMBLER_H_
Tests disassembling wasm scripts
Session #1: Script parsed. URL: wasm://wasm/moduleName-70e7540a.
Session #1: Source for wasm://wasm/moduleName-70e7540a:
bytecode:
0x00 0x61 0x73 0x6d 0x01 0x00 0x00 0x00 ;; offset 0..7
0x01 0x09 0x02 0x60 0x01 0x6f 0x01 0x7f ;; offset 8..15
0x60 0x00 0x00 0x03 0x03 0x02 0x00 0x01 ;; offset 16..23
0x0a 0x12 0x02 0x0b 0x02 0x01 0x7f 0x01 ;; offset 24..31
0x7c 0x20 0x01 0x1a 0x41 0x2a 0x0b 0x04 ;; offset 32..39
0x00 0x01 0x01 0x0b 0x00 0x27 0x04 0x6e ;; offset 40..47
0x61 0x6d 0x65 0x00 0x0b 0x0a 0x6d 0x6f ;; offset 48..55
0x64 0x75 0x6c 0x65 0x4e 0x61 0x6d 0x65 ;; offset 56..63
0x01 0x09 0x02 0x00 0x02 0x66 0x31 0x01 ;; offset 64..71
0x02 0x66 0x32 0x02 0x08 0x01 0x00 0x01 ;; offset 72..79
0x01 0x03 0x78 0x79 0x7a
streamid: undefined
functionBodyOffsets: 28,40,44
totalNumberOfLines: 13
lines:
(module $moduleName
(func $f1 (;0;) (param $var0 externref) (result i32)
(local $xyz i32)
(local $var2 f64)
local.get $xyz
drop
i32.const 42
)
(func $f2 (;1;)
nop
nop
)
)
bytecodeOffsets: 0,28,28,28,33,35,36,38,40,41,42,43,85
// Copyright 2022 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.
utils.load('test/inspector/wasm-inspector-test.js');
InspectorTest.log("Tests disassembling wasm scripts");
let contextGroup = new InspectorTest.ContextGroup();
let { id: sessionId, Protocol } = contextGroup.connect();
Protocol.Debugger.enable();
const scriptPromise = new Promise(resolve => {
Protocol.Debugger.onScriptParsed(event => {
if (event.params.url.startsWith('wasm://')) {
resolve(event.params);
}
});
}).then(params => loadScript(params));
async function loadScript({url, scriptId}) {
InspectorTest.log(`Session #${sessionId}: Script parsed. URL: ${url}.`);
({result: {scriptSource, bytecode}} =
await Protocol.Debugger.getScriptSource({scriptId}));
({result: {
streamId,
totalNumberOfLines,
functionBodyOffsets,
chunk: {lines, bytecodeOffsets}
}} = await Protocol.Debugger.disassembleWasmModule({scriptId}));
InspectorTest.log(`Session #${sessionId}: Source for ${url}:`);
bytecode = InspectorTest.decodeBase64(bytecode);
const bytes = [];
for (let i = 0; i < bytecode.length; i++) {
let byte = bytecode[i];
bytes.push((byte < 0x10 ? '0x0' : '0x') + byte.toString(16) + " ");
if ((i & 7) == 7) bytes.push(` ;; offset ${i-7}..${i}\n`);
}
InspectorTest.log(`bytecode:\n${bytes.join("")}`);
InspectorTest.log(`streamid: ${streamId}`);
InspectorTest.log(`functionBodyOffsets: ${functionBodyOffsets}`);
InspectorTest.log(`totalNumberOfLines: ${totalNumberOfLines}`);
InspectorTest.log(`lines: \n${lines.join("\n")}`);
InspectorTest.log(`bytecodeOffsets: ${bytecodeOffsets}`);
if (streamId) {
({result: {chunk: {lines, bytecodeOffsets}}} =
await Protocol.Debugger.nextWasmDissassemblyChunk({streamId}));
InspectorTest.log(`chunk #2:`);
InspectorTest.log(`lines: \n${lines.join("\n")}`);
InspectorTest.log(`bytecodeOffsets: ${bytecodeOffsets}`);
}
}
const builder = new WasmModuleBuilder();
builder.addFunction('f1', kSig_i_r)
.addLocals(kWasmI32, 1, ["xyz"])
.addLocals(kWasmF64, 1)
.addBody([
kExprLocalGet, 1, kExprDrop, kExprI32Const, 42
]);
builder.addFunction('f2', kSig_v_v).addBody([kExprNop, kExprNop]);
builder.setName('moduleName');
const module_bytes = builder.toArray();
function testFunction(bytes) {
// Compilation triggers registration of wasm scripts.
new WebAssembly.Module(new Uint8Array(bytes));
}
contextGroup.addScript(testFunction.toString(), 0, 0, 'v8://test/testFunction');
contextGroup.addScript('const module_bytes = ' + JSON.stringify(module_bytes));
Protocol.Runtime
.evaluate({
'expression': '//# sourceURL=v8://test/runTestFunction\n' +
'testFunction(module_bytes);'
})
.then(() => scriptPromise)
.catch(err => {
InspectorTest.log(err.stack);
})
.then(() => InspectorTest.completeTest());
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment