[wasm] Support stepping into Wasm from Javascript
We detect a stepping in Wasm from Javascript into Wasm then prepare the target function for debugging. The trick is redirect the target to interpreter and set a 'fake' breakpoint in the first instruction. Currently we don't need to clear this 'fake' breakpoint since it won't notify unless user intend to step in. Change-Id: Ibe1f9ba31dc6c7919895d3fe31967e9c4699ef63 Bug: chromium:1019606 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1902259 Commit-Queue: Z Nguyen-Huu <duongn@microsoft.com> Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Clemens Backes <clemensb@chromium.org> Cr-Commit-Position: refs/heads/master@{#65020}
This commit is contained in:
parent
40b731de45
commit
a3b5229bdd
@ -855,6 +855,19 @@ void Debug::PrepareStepIn(Handle<JSFunction> function) {
|
|||||||
if (in_debug_scope()) return;
|
if (in_debug_scope()) return;
|
||||||
if (break_disabled()) return;
|
if (break_disabled()) return;
|
||||||
Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
|
Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
|
||||||
|
// If stepping from JS into Wasm, prepare for it.
|
||||||
|
if (shared->HasWasmExportedFunctionData()) {
|
||||||
|
auto imported_function = Handle<WasmExportedFunction>::cast(function);
|
||||||
|
Handle<WasmInstanceObject> wasm_instance(imported_function->instance(),
|
||||||
|
isolate_);
|
||||||
|
Handle<WasmDebugInfo> wasm_debug_info =
|
||||||
|
WasmInstanceObject::GetOrCreateDebugInfo(wasm_instance);
|
||||||
|
int func_index = shared->wasm_exported_function_data().function_index();
|
||||||
|
WasmDebugInfo::PrepareStepIn(wasm_debug_info, func_index);
|
||||||
|
// We need to reset all of this since break would be
|
||||||
|
// handled in Wasm Interpreter now. Otherwise it would be a loop here.
|
||||||
|
ClearStepping();
|
||||||
|
}
|
||||||
if (IsBlackboxed(shared)) return;
|
if (IsBlackboxed(shared)) return;
|
||||||
if (*function == thread_local_.ignore_step_into_function_) return;
|
if (*function == thread_local_.ignore_step_into_function_) return;
|
||||||
thread_local_.ignore_step_into_function_ = Smi::zero();
|
thread_local_.ignore_step_into_function_ = Smi::zero();
|
||||||
|
@ -234,6 +234,7 @@ class InterpreterHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FinishActivation(frame_pointer, activation_id);
|
FinishActivation(frame_pointer, activation_id);
|
||||||
|
ClearStepping();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -531,6 +532,18 @@ wasm::WasmInterpreter* WasmDebugInfo::SetupForTesting(
|
|||||||
return interp_handle->raw()->interpreter();
|
return interp_handle->raw()->interpreter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
void WasmDebugInfo::PrepareStepIn(Handle<WasmDebugInfo> debug_info,
|
||||||
|
int func_index) {
|
||||||
|
Isolate* isolate = debug_info->GetIsolate();
|
||||||
|
auto* handle = GetOrCreateInterpreterHandle(isolate, debug_info);
|
||||||
|
RedirectToInterpreter(debug_info, Vector<int>(&func_index, 1));
|
||||||
|
const wasm::WasmFunction* func = &handle->module()->functions[func_index];
|
||||||
|
handle->interpreter()->PrepareStepIn(func);
|
||||||
|
// Debug break would be considered as a step-in inside wasm.
|
||||||
|
handle->PrepareStep(StepAction::StepIn);
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void WasmDebugInfo::SetBreakpoint(Handle<WasmDebugInfo> debug_info,
|
void WasmDebugInfo::SetBreakpoint(Handle<WasmDebugInfo> debug_info,
|
||||||
int func_index, int offset) {
|
int func_index, int offset) {
|
||||||
|
@ -4123,6 +4123,13 @@ void WasmInterpreter::Run() { internals_->threads_[0].Run(); }
|
|||||||
|
|
||||||
void WasmInterpreter::Pause() { internals_->threads_[0].Pause(); }
|
void WasmInterpreter::Pause() { internals_->threads_[0].Pause(); }
|
||||||
|
|
||||||
|
void WasmInterpreter::PrepareStepIn(const WasmFunction* function) {
|
||||||
|
// Set a breakpoint at the start of function.
|
||||||
|
InterpreterCode* code = internals_->codemap_.GetCode(function);
|
||||||
|
pc_t pc = code->locals.encoded_size;
|
||||||
|
SetBreakpoint(function, pc, true);
|
||||||
|
}
|
||||||
|
|
||||||
bool WasmInterpreter::SetBreakpoint(const WasmFunction* function, pc_t pc,
|
bool WasmInterpreter::SetBreakpoint(const WasmFunction* function, pc_t pc,
|
||||||
bool enabled) {
|
bool enabled) {
|
||||||
InterpreterCode* code = internals_->codemap_.GetCode(function);
|
InterpreterCode* code = internals_->codemap_.GetCode(function);
|
||||||
|
@ -180,6 +180,9 @@ class V8_EXPORT_PRIVATE WasmInterpreter {
|
|||||||
void Run();
|
void Run();
|
||||||
void Pause();
|
void Pause();
|
||||||
|
|
||||||
|
// Prepare {function} for stepping in from Javascript.
|
||||||
|
void PrepareStepIn(const WasmFunction* function);
|
||||||
|
|
||||||
// Set a breakpoint at {pc} in {function} to be {enabled}. Returns the
|
// Set a breakpoint at {pc} in {function} to be {enabled}. Returns the
|
||||||
// previous state of the breakpoint at {pc}.
|
// previous state of the breakpoint at {pc}.
|
||||||
bool SetBreakpoint(const WasmFunction* function, pc_t pc, bool enabled);
|
bool SetBreakpoint(const WasmFunction* function, pc_t pc, bool enabled);
|
||||||
|
@ -830,6 +830,10 @@ class WasmDebugInfo : public Struct {
|
|||||||
V8_EXPORT_PRIVATE static wasm::WasmInterpreter* SetupForTesting(
|
V8_EXPORT_PRIVATE static wasm::WasmInterpreter* SetupForTesting(
|
||||||
Handle<WasmInstanceObject>);
|
Handle<WasmInstanceObject>);
|
||||||
|
|
||||||
|
// Prepare WasmDebugInfo for stepping in the given function.
|
||||||
|
V8_EXPORT_PRIVATE static void PrepareStepIn(Handle<WasmDebugInfo>,
|
||||||
|
int func_index);
|
||||||
|
|
||||||
// Set a breakpoint in the given function at the given byte offset within that
|
// Set a breakpoint in the given function at the given byte offset within that
|
||||||
// function. This will redirect all future calls to this function to the
|
// function. This will redirect all future calls to this function to the
|
||||||
// interpreter and will always pause at the given offset.
|
// interpreter and will always pause at the given offset.
|
||||||
|
92
test/debugger/debug/wasm/debug-step-into-wasm.js
Normal file
92
test/debugger/debug/wasm/debug-step-into-wasm.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// 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('sub', 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
|
||||||
|
kExprI32Sub]) // i32.sub i0 i1
|
||||||
|
.exportFunc();
|
||||||
|
const instance = builder.instantiate();
|
||||||
|
const wasm_f = instance.exports.sub;
|
||||||
|
|
||||||
|
Debug = debug.Debug;
|
||||||
|
|
||||||
|
var exception = null;
|
||||||
|
var js_break_line = 0;
|
||||||
|
var break_count = 0;
|
||||||
|
var wasm_break_count = 0;
|
||||||
|
|
||||||
|
function listener(event, exec_state, event_data, data) {
|
||||||
|
if (event != Debug.DebugEvent.Break) return;
|
||||||
|
try {
|
||||||
|
print(event_data.sourceLineText());
|
||||||
|
print(event_data.functionName());
|
||||||
|
if (event_data.sourceLineText() == 'Debug.setListener(null);') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event_data.functionName() == 'f') {
|
||||||
|
break_count++;
|
||||||
|
assertTrue(
|
||||||
|
event_data.sourceLineText().indexOf(`Line ${js_break_line}.`) > 0);
|
||||||
|
js_break_line += 2;
|
||||||
|
} else {
|
||||||
|
assertTrue(event_data.functionName() == 'sub');
|
||||||
|
wasm_break_count++;
|
||||||
|
}
|
||||||
|
exec_state.prepareStep(Debug.StepAction.StepIn);
|
||||||
|
} catch (e) {
|
||||||
|
exception = e;
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function f() {
|
||||||
|
var result = wasm_f(3, 2); // Line 0.
|
||||||
|
result++; // Line 1.
|
||||||
|
return result; // Line 2.
|
||||||
|
}
|
||||||
|
assertEquals(2, f());
|
||||||
|
|
||||||
|
Debug.setListener(listener);
|
||||||
|
// Set a breakpoint on line 0.
|
||||||
|
Debug.setBreakPoint(f, 1);
|
||||||
|
// Set a breakpoint on line 2.
|
||||||
|
Debug.setBreakPoint(f, 3);
|
||||||
|
f();
|
||||||
|
Debug.setListener(null);
|
||||||
|
|
||||||
|
var break_count2 = 0;
|
||||||
|
// In the second execution, only break at javascript frame.
|
||||||
|
function listener2(event, exec_state, event_data, data) {
|
||||||
|
if (event != Debug.DebugEvent.Break) return;
|
||||||
|
try {
|
||||||
|
print(event_data.sourceLineText());
|
||||||
|
if (event_data.sourceLineText() == 'Debug.setListener(null);') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
print(event_data.functionName());
|
||||||
|
assertTrue(event_data.sourceLineText().indexOf(`Line `) > 0);
|
||||||
|
assertEquals(event_data.functionName(), 'f');
|
||||||
|
break_count2++;
|
||||||
|
exec_state.prepareStep(Debug.StepAction.StepOut);
|
||||||
|
} catch (e) {
|
||||||
|
exception = e;
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Debug.setListener(listener2);
|
||||||
|
f();
|
||||||
|
Debug.setListener(null);
|
||||||
|
|
||||||
|
assertEquals(break_count, 2);
|
||||||
|
assertEquals(js_break_line, 4);
|
||||||
|
assertEquals(wasm_break_count, 4);
|
||||||
|
assertEquals(break_count2, 2);
|
||||||
|
assertNull(exception);
|
@ -0,0 +1,39 @@
|
|||||||
|
Tests stepping from javascript into wasm
|
||||||
|
Installing code and global variable.
|
||||||
|
Calling instantiate function.
|
||||||
|
Waiting for wasm scripts to be parsed.
|
||||||
|
Ignoring script with url v8://test/callInstantiate
|
||||||
|
Ignoring script with url wasm://wasm/fa045c1e
|
||||||
|
Got wasm script: wasm://wasm/fa045c1e/fa045c1e-0
|
||||||
|
Setting breakpoint on line 3 of wasm function
|
||||||
|
{
|
||||||
|
columnNumber : 2
|
||||||
|
lineNumber : 3
|
||||||
|
scriptId : <scriptId>
|
||||||
|
}
|
||||||
|
paused
|
||||||
|
function test() {
|
||||||
|
#debugger;
|
||||||
|
instance.exports.main(1);
|
||||||
|
|
||||||
|
Debugger.stepInto
|
||||||
|
paused
|
||||||
|
debugger;
|
||||||
|
#instance.exports.main(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Debugger.stepInto
|
||||||
|
paused
|
||||||
|
func $wasm_A (param i32) (result i32)
|
||||||
|
#local.get 0
|
||||||
|
i32.const 1
|
||||||
|
|
||||||
|
Debugger.resume
|
||||||
|
paused
|
||||||
|
i32.const 1
|
||||||
|
#i32.sub
|
||||||
|
end
|
||||||
|
|
||||||
|
Debugger.resume
|
||||||
|
exports.main returned!
|
||||||
|
Finished!
|
104
test/inspector/debugger/wasm-stepping-in-from-js.js
Normal file
104
test/inspector/debugger/wasm-stepping-in-from-js.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
let {session, contextGroup, Protocol} = InspectorTest.start('Tests stepping from javascript into wasm');
|
||||||
|
session.setupScriptMap();
|
||||||
|
|
||||||
|
utils.load('test/mjsunit/wasm/wasm-module-builder.js');
|
||||||
|
|
||||||
|
let builder = new WasmModuleBuilder();
|
||||||
|
|
||||||
|
// wasm_A
|
||||||
|
builder.addFunction('wasm_A', kSig_i_i)
|
||||||
|
.addBody([
|
||||||
|
// clang-format off
|
||||||
|
kExprLocalGet, 0, // Line 1: get input
|
||||||
|
kExprI32Const, 1, // Line 2: get constant 1
|
||||||
|
kExprI32Sub // Line 3: decrease
|
||||||
|
// clang-format on
|
||||||
|
])
|
||||||
|
.exportAs('main');
|
||||||
|
|
||||||
|
let module_bytes = builder.toArray();
|
||||||
|
|
||||||
|
function instantiate(bytes) {
|
||||||
|
let buffer = new ArrayBuffer(bytes.length);
|
||||||
|
let view = new Uint8Array(buffer);
|
||||||
|
for (let i = 0; i < bytes.length; ++i) {
|
||||||
|
view[i] = bytes[i] | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let module = new WebAssembly.Module(buffer);
|
||||||
|
// Set global variable.
|
||||||
|
instance = new WebAssembly.Instance(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
let evalWithUrl = (code, url) => Protocol.Runtime.evaluate(
|
||||||
|
{'expression': code + '\n//# sourceURL=v8://test/' + url});
|
||||||
|
|
||||||
|
Protocol.Debugger.onPaused(async message => {
|
||||||
|
InspectorTest.log("paused");
|
||||||
|
var frames = message.params.callFrames;
|
||||||
|
await session.logSourceLocation(frames[0].location);
|
||||||
|
let action = step_actions.shift() || 'resume';
|
||||||
|
InspectorTest.log('Debugger.' + action)
|
||||||
|
await Protocol.Debugger[action]();
|
||||||
|
})
|
||||||
|
|
||||||
|
let step_actions = [
|
||||||
|
'stepInto', // # debugger
|
||||||
|
'stepInto', // step into instance.exports.main(1)
|
||||||
|
'resume', // move to breakpoint
|
||||||
|
// then just resume.
|
||||||
|
'resume',
|
||||||
|
];
|
||||||
|
|
||||||
|
contextGroup.addScript(`
|
||||||
|
function test() {
|
||||||
|
debugger;
|
||||||
|
instance.exports.main(1);
|
||||||
|
}
|
||||||
|
//# sourceURL=test.js`);
|
||||||
|
|
||||||
|
(async function Test() {
|
||||||
|
await Protocol.Debugger.enable();
|
||||||
|
InspectorTest.log('Installing code and global variable.');
|
||||||
|
await evalWithUrl('var instance;\n' + instantiate.toString(), 'setup');
|
||||||
|
InspectorTest.log('Calling instantiate function.');
|
||||||
|
evalWithUrl(
|
||||||
|
'instantiate(' + JSON.stringify(module_bytes) + ')', 'callInstantiate');
|
||||||
|
const scriptId = await waitForWasmScript();
|
||||||
|
InspectorTest.log(
|
||||||
|
'Setting breakpoint on line 3 of wasm function');
|
||||||
|
let msg = await Protocol.Debugger.setBreakpoint(
|
||||||
|
{'location': {'scriptId': scriptId, 'lineNumber': 3}});
|
||||||
|
printFailure(msg);
|
||||||
|
InspectorTest.logMessage(msg.result.actualLocation);
|
||||||
|
await Protocol.Runtime.evaluate({ expression: 'test()' });
|
||||||
|
InspectorTest.log('exports.main returned!');
|
||||||
|
InspectorTest.log('Finished!');
|
||||||
|
InspectorTest.completeTest();
|
||||||
|
})();
|
||||||
|
|
||||||
|
function printFailure(message) {
|
||||||
|
if (!message.result) {
|
||||||
|
InspectorTest.logMessage(message);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForWasmScript() {
|
||||||
|
InspectorTest.log('Waiting for wasm scripts to be parsed.');
|
||||||
|
while (true) {
|
||||||
|
let msg = await Protocol.Debugger.onceScriptParsed();
|
||||||
|
let url = msg.params.url;
|
||||||
|
if (!url.startsWith('wasm://') || url.split('/').length != 5) {
|
||||||
|
InspectorTest.log('Ignoring script with url ' + url);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let scriptId = msg.params.scriptId;
|
||||||
|
InspectorTest.log('Got wasm script: ' + url);
|
||||||
|
return scriptId;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user