Commit a3b5229b authored by Z Nguyen-Huu's avatar Z Nguyen-Huu Committed by Commit Bot

[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: 's avatarBenedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarClemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#65020}
parent 40b731de
...@@ -855,6 +855,19 @@ void Debug::PrepareStepIn(Handle<JSFunction> function) { ...@@ -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 { ...@@ -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( ...@@ -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(); } ...@@ -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 { ...@@ -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 { ...@@ -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.
......
// 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);
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!
// 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;
}
}
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