v8/test/cctest/wasm/test-wasm-breakpoints.cc
Benedikt Meurer 3740764cca [debug][cleanup] Use consistent StepInto and StepOver naming.
In the Chrome DevTools Protocol, the step actions are named StepOut,
StepOver, and StepInto, but internally we used StepOut, StepNext, and
StepIn instead. This change adjusts the naming to be consistent.

Bug: chromium:901814, chromium:1162229
Change-Id: Id3502a1b0a4aadd94734ec3d1fef73c1782fa220
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2928510
Reviewed-by: Yang Guo <yangguo@chromium.org>
Reviewed-by: Clemens Backes <clemensb@chromium.org>
Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#74877}
2021-06-01 11:26:57 +00:00

586 lines
21 KiB
C++

// Copyright 2016 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.
#include "src/codegen/assembler-inl.h"
#include "src/debug/debug-interface.h"
#include "src/execution/frames-inl.h"
#include "src/objects/property-descriptor.h"
#include "src/utils/utils.h"
#include "src/wasm/wasm-debug.h"
#include "src/wasm/wasm-objects-inl.h"
#include "test/cctest/cctest.h"
#include "test/cctest/compiler/value-helper.h"
#include "test/cctest/wasm/wasm-run-utils.h"
#include "test/common/wasm/test-signatures.h"
#include "test/common/wasm/wasm-macro-gen.h"
namespace v8 {
namespace internal {
namespace wasm {
namespace {
debug::Location TranslateLocation(WasmRunnerBase* runner,
const debug::Location& loc) {
// Convert locations from {func_index, offset_in_func} to
// {0, offset_in_module}.
int func_index = loc.GetLineNumber();
int func_offset = runner->builder().GetFunctionAt(func_index)->code.offset();
int offset = loc.GetColumnNumber() + func_offset;
return {0, offset};
}
void CheckLocations(
WasmRunnerBase* runner, NativeModule* native_module, debug::Location start,
debug::Location end,
std::initializer_list<debug::Location> expected_locations_init) {
std::vector<debug::BreakLocation> locations;
std::vector<debug::Location> expected_locations;
for (auto loc : expected_locations_init) {
expected_locations.push_back(TranslateLocation(runner, loc));
}
bool success = WasmScript::GetPossibleBreakpoints(
native_module, TranslateLocation(runner, start),
TranslateLocation(runner, end), &locations);
CHECK(success);
printf("got %d locations: ", static_cast<int>(locations.size()));
for (size_t i = 0, e = locations.size(); i != e; ++i) {
printf("%s<%d,%d>", i == 0 ? "" : ", ", locations[i].GetLineNumber(),
locations[i].GetColumnNumber());
}
printf("\n");
CHECK_EQ(expected_locations.size(), locations.size());
for (size_t i = 0, e = locations.size(); i != e; ++i) {
CHECK_EQ(expected_locations[i].GetLineNumber(),
locations[i].GetLineNumber());
CHECK_EQ(expected_locations[i].GetColumnNumber(),
locations[i].GetColumnNumber());
}
}
void CheckLocationsFail(WasmRunnerBase* runner, NativeModule* native_module,
debug::Location start, debug::Location end) {
std::vector<debug::BreakLocation> locations;
bool success = WasmScript::GetPossibleBreakpoints(
native_module, TranslateLocation(runner, start),
TranslateLocation(runner, end), &locations);
CHECK(!success);
}
class BreakHandler : public debug::DebugDelegate {
public:
enum Action {
Continue = StepAction::LastStepAction + 1,
StepOver = StepAction::StepOver,
StepInto = StepAction::StepInto,
StepOut = StepAction::StepOut
};
struct BreakPoint {
int position;
Action action;
std::function<void(void)> pre_action;
BreakPoint(int position, Action action)
: position(position), action(action), pre_action([]() {}) {}
BreakPoint(int position, Action action,
std::function<void(void)> pre_action)
: position(position), action(action), pre_action(pre_action) {}
};
explicit BreakHandler(Isolate* isolate,
std::initializer_list<BreakPoint> expected_breaks)
: isolate_(isolate), expected_breaks_(expected_breaks) {
v8::debug::SetDebugDelegate(reinterpret_cast<v8::Isolate*>(isolate_), this);
}
~BreakHandler() override {
// Check that all expected breakpoints have been hit.
CHECK_EQ(count_, expected_breaks_.size());
v8::debug::SetDebugDelegate(reinterpret_cast<v8::Isolate*>(isolate_),
nullptr);
}
int count() const { return count_; }
private:
Isolate* isolate_;
int count_ = 0;
std::vector<BreakPoint> expected_breaks_;
void BreakProgramRequested(v8::Local<v8::Context> paused_context,
const std::vector<int>&) override {
printf("Break #%d\n", count_);
CHECK_GT(expected_breaks_.size(), count_);
// Check the current position.
StackTraceFrameIterator frame_it(isolate_);
auto summ = FrameSummary::GetTop(frame_it.frame()).AsWasm();
CHECK_EQ(expected_breaks_[count_].position, summ.byte_offset());
expected_breaks_[count_].pre_action();
Action next_action = expected_breaks_[count_].action;
switch (next_action) {
case Continue:
break;
case StepOver:
case StepInto:
case StepOut:
isolate_->debug()->PrepareStep(static_cast<StepAction>(next_action));
break;
default:
UNREACHABLE();
}
++count_;
}
};
Handle<BreakPoint> SetBreakpoint(WasmRunnerBase* runner, int function_index,
int byte_offset,
int expected_set_byte_offset = -1) {
runner->TierDown();
int func_offset =
runner->builder().GetFunctionAt(function_index)->code.offset();
int code_offset = func_offset + byte_offset;
if (expected_set_byte_offset == -1) expected_set_byte_offset = byte_offset;
Handle<WasmInstanceObject> instance = runner->builder().instance_object();
Handle<Script> script(instance->module_object().script(),
runner->main_isolate());
static int break_index = 0;
Handle<BreakPoint> break_point =
runner->main_isolate()->factory()->NewBreakPoint(
break_index++, runner->main_isolate()->factory()->empty_string());
CHECK(WasmScript::SetBreakPoint(script, &code_offset, break_point));
return break_point;
}
void ClearBreakpoint(WasmRunnerBase* runner, int function_index,
int byte_offset, Handle<BreakPoint> break_point) {
int func_offset =
runner->builder().GetFunctionAt(function_index)->code.offset();
int code_offset = func_offset + byte_offset;
Handle<WasmInstanceObject> instance = runner->builder().instance_object();
Handle<Script> script(instance->module_object().script(),
runner->main_isolate());
CHECK(WasmScript::ClearBreakPoint(script, code_offset, break_point));
}
// Wrapper with operator<<.
struct WasmValWrapper {
WasmValue val;
bool operator==(const WasmValWrapper& other) const {
return val == other.val;
}
};
// Only needed in debug builds. Avoid unused warning otherwise.
#ifdef DEBUG
std::ostream& operator<<(std::ostream& out, const WasmValWrapper& wrapper) {
switch (wrapper.val.type().kind()) {
case kI32:
out << "i32: " << wrapper.val.to<int32_t>();
break;
case kI64:
out << "i64: " << wrapper.val.to<int64_t>();
break;
case kF32:
out << "f32: " << wrapper.val.to<float>();
break;
case kF64:
out << "f64: " << wrapper.val.to<double>();
break;
default:
UNIMPLEMENTED();
}
return out;
}
#endif
class CollectValuesBreakHandler : public debug::DebugDelegate {
public:
struct BreakpointValues {
std::vector<WasmValue> locals;
std::vector<WasmValue> stack;
};
explicit CollectValuesBreakHandler(
Isolate* isolate, std::initializer_list<BreakpointValues> expected_values)
: isolate_(isolate), expected_values_(expected_values) {
v8::debug::SetDebugDelegate(reinterpret_cast<v8::Isolate*>(isolate_), this);
}
~CollectValuesBreakHandler() override {
v8::debug::SetDebugDelegate(reinterpret_cast<v8::Isolate*>(isolate_),
nullptr);
}
private:
Isolate* isolate_;
int count_ = 0;
std::vector<BreakpointValues> expected_values_;
void BreakProgramRequested(v8::Local<v8::Context> paused_context,
const std::vector<int>&) override {
printf("Break #%d\n", count_);
CHECK_GT(expected_values_.size(), count_);
auto& expected = expected_values_[count_];
++count_;
HandleScope handles(isolate_);
StackTraceFrameIterator frame_it(isolate_);
WasmFrame* frame = WasmFrame::cast(frame_it.frame());
DebugInfo* debug_info = frame->native_module()->GetDebugInfo();
int num_locals = debug_info->GetNumLocals(frame->pc());
CHECK_EQ(expected.locals.size(), num_locals);
for (int i = 0; i < num_locals; ++i) {
WasmValue local_value = debug_info->GetLocalValue(
i, frame->pc(), frame->fp(), frame->callee_fp(), isolate_);
CHECK_EQ(WasmValWrapper{expected.locals[i]}, WasmValWrapper{local_value});
}
int stack_depth = debug_info->GetStackDepth(frame->pc());
CHECK_EQ(expected.stack.size(), stack_depth);
for (int i = 0; i < stack_depth; ++i) {
WasmValue stack_value = debug_info->GetStackValue(
i, frame->pc(), frame->fp(), frame->callee_fp(), isolate_);
CHECK_EQ(WasmValWrapper{expected.stack[i]}, WasmValWrapper{stack_value});
}
isolate_->debug()->PrepareStep(StepAction::StepInto);
}
};
// Special template to explicitly cast to WasmValue.
template <typename Arg>
WasmValue MakeWasmVal(Arg arg) {
return WasmValue(arg);
}
// Translate long to i64 (ambiguous otherwise).
template <>
WasmValue MakeWasmVal(long arg) { // NOLINT: allow long parameter
return WasmValue(static_cast<int64_t>(arg));
}
template <typename... Args>
std::vector<WasmValue> wasmVec(Args... args) {
std::array<WasmValue, sizeof...(args)> arr{{MakeWasmVal(args)...}};
return std::vector<WasmValue>{arr.begin(), arr.end()};
}
int GetIntReturnValue(MaybeHandle<Object> retval) {
CHECK(!retval.is_null());
int result;
CHECK(retval.ToHandleChecked()->ToInt32(&result));
return result;
}
} // namespace
WASM_COMPILED_EXEC_TEST(WasmCollectPossibleBreakpoints) {
WasmRunner<int> runner(execution_tier);
BUILD(runner, WASM_NOP, WASM_I32_ADD(WASM_ZERO, WASM_ONE));
WasmInstanceObject instance = *runner.builder().instance_object();
NativeModule* native_module = instance.module_object().native_module();
std::vector<debug::Location> locations;
// Check all locations for function 0.
CheckLocations(&runner, native_module, {0, 0}, {0, 10},
{{0, 1}, {0, 2}, {0, 4}, {0, 6}, {0, 7}});
// Check a range ending at an instruction.
CheckLocations(&runner, native_module, {0, 2}, {0, 4}, {{0, 2}});
// Check a range ending one behind an instruction.
CheckLocations(&runner, native_module, {0, 2}, {0, 5}, {{0, 2}, {0, 4}});
// Check a range starting at an instruction.
CheckLocations(&runner, native_module, {0, 7}, {0, 8}, {{0, 7}});
// Check from an instruction to beginning of next function.
CheckLocations(&runner, native_module, {0, 7}, {0, 10}, {{0, 7}});
// Check from end of one function (no valid instruction position) to beginning
// of next function. Must be empty, but not fail.
CheckLocations(&runner, native_module, {0, 8}, {0, 10}, {});
// Check from one after the end of the function. Must fail.
CheckLocationsFail(&runner, native_module, {0, 9}, {0, 10});
}
WASM_COMPILED_EXEC_TEST(WasmSimpleBreak) {
WasmRunner<int> runner(execution_tier);
Isolate* isolate = runner.main_isolate();
BUILD(runner, WASM_NOP, WASM_I32_ADD(WASM_I32V_1(11), WASM_I32V_1(3)));
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(runner.function_index());
SetBreakpoint(&runner, runner.function_index(), 4, 4);
BreakHandler count_breaks(isolate, {{4, BreakHandler::Continue}});
Handle<Object> global(isolate->context().global_object(), isolate);
MaybeHandle<Object> retval =
Execution::Call(isolate, main_fun_wrapper, global, 0, nullptr);
CHECK_EQ(14, GetIntReturnValue(retval));
}
WASM_COMPILED_EXEC_TEST(WasmNonBreakablePosition) {
WasmRunner<int> runner(execution_tier);
Isolate* isolate = runner.main_isolate();
BUILD(runner, WASM_RETURN1(WASM_I32V_2(1024)));
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(runner.function_index());
SetBreakpoint(&runner, runner.function_index(), 2, 4);
BreakHandler count_breaks(isolate, {{4, BreakHandler::Continue}});
Handle<Object> global(isolate->context().global_object(), isolate);
MaybeHandle<Object> retval =
Execution::Call(isolate, main_fun_wrapper, global, 0, nullptr);
CHECK_EQ(1024, GetIntReturnValue(retval));
}
WASM_COMPILED_EXEC_TEST(WasmSimpleStepping) {
WasmRunner<int> runner(execution_tier);
BUILD(runner, WASM_I32_ADD(WASM_I32V_1(11), WASM_I32V_1(3)));
Isolate* isolate = runner.main_isolate();
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(runner.function_index());
// Set breakpoint at the first I32Const.
SetBreakpoint(&runner, runner.function_index(), 1, 1);
BreakHandler count_breaks(isolate,
{
{1, BreakHandler::StepOver}, // I32Const
{3, BreakHandler::StepOver}, // I32Const
{5, BreakHandler::Continue} // I32Add
});
Handle<Object> global(isolate->context().global_object(), isolate);
MaybeHandle<Object> retval =
Execution::Call(isolate, main_fun_wrapper, global, 0, nullptr);
CHECK_EQ(14, GetIntReturnValue(retval));
}
WASM_COMPILED_EXEC_TEST(WasmStepInAndOut) {
WasmRunner<int, int> runner(execution_tier);
runner.TierDown();
WasmFunctionCompiler& f2 = runner.NewFunction<void>();
f2.AllocateLocal(kWasmI32);
// Call f2 via indirect call, because a direct call requires f2 to exist when
// we compile main, but we need to compile main first so that the order of
// functions in the code section matches the function indexes.
// return arg0
BUILD(runner, WASM_RETURN1(WASM_LOCAL_GET(0)));
// for (int i = 0; i < 10; ++i) { f2(i); }
BUILD(f2, WASM_LOOP(
WASM_BR_IF(0, WASM_BINOP(kExprI32GeU, WASM_LOCAL_GET(0),
WASM_I32V_1(10))),
WASM_LOCAL_SET(
0, WASM_BINOP(kExprI32Sub, WASM_LOCAL_GET(0), WASM_ONE)),
WASM_CALL_FUNCTION(runner.function_index(), WASM_LOCAL_GET(0)),
WASM_DROP, WASM_BR(1)));
Isolate* isolate = runner.main_isolate();
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(f2.function_index());
// Set first breakpoint on the LocalGet (offset 19) before the Call.
SetBreakpoint(&runner, f2.function_index(), 19, 19);
BreakHandler count_breaks(isolate,
{
{19, BreakHandler::StepInto}, // LocalGet
{21, BreakHandler::StepInto}, // Call
{1, BreakHandler::StepOut}, // in f2
{23, BreakHandler::Continue} // After Call
});
Handle<Object> global(isolate->context().global_object(), isolate);
CHECK(!Execution::Call(isolate, main_fun_wrapper, global, 0, nullptr)
.is_null());
}
WASM_COMPILED_EXEC_TEST(WasmGetLocalsAndStack) {
WasmRunner<void, int> runner(execution_tier);
runner.AllocateLocal(kWasmI64);
runner.AllocateLocal(kWasmF32);
runner.AllocateLocal(kWasmF64);
BUILD(runner,
// set [1] to 17
WASM_LOCAL_SET(1, WASM_I64V_1(17)),
// set [2] to <arg0> = 7
WASM_LOCAL_SET(2, WASM_F32_SCONVERT_I32(WASM_LOCAL_GET(0))),
// set [3] to <arg1>/2 = 8.5
WASM_LOCAL_SET(3, WASM_F64_DIV(WASM_F64_SCONVERT_I64(WASM_LOCAL_GET(1)),
WASM_F64(2))));
Isolate* isolate = runner.main_isolate();
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(runner.function_index());
// Set breakpoint at the first instruction (7 bytes for local decls: num
// entries + 3x<count, type>).
SetBreakpoint(&runner, runner.function_index(), 7, 7);
CollectValuesBreakHandler break_handler(
isolate,
{
// params + locals stack
{wasmVec(7, 0L, 0.f, 0.), wasmVec()}, // 0: i64.const[17]
{wasmVec(7, 0L, 0.f, 0.), wasmVec(17L)}, // 1: set_local[1]
{wasmVec(7, 17L, 0.f, 0.), wasmVec()}, // 2: get_local[0]
{wasmVec(7, 17L, 0.f, 0.), wasmVec(7)}, // 3: f32.convert_s
{wasmVec(7, 17L, 0.f, 0.), wasmVec(7.f)}, // 4: set_local[2]
{wasmVec(7, 17L, 7.f, 0.), wasmVec()}, // 5: get_local[1]
{wasmVec(7, 17L, 7.f, 0.), wasmVec(17L)}, // 6: f64.convert_s
{wasmVec(7, 17L, 7.f, 0.), wasmVec(17.)}, // 7: f64.const[2]
{wasmVec(7, 17L, 7.f, 0.), wasmVec(17., 2.)}, // 8: f64.div
{wasmVec(7, 17L, 7.f, 0.), wasmVec(8.5)}, // 9: set_local[3]
{wasmVec(7, 17L, 7.f, 8.5), wasmVec()}, // 10: end
});
Handle<Object> global(isolate->context().global_object(), isolate);
Handle<Object> args[]{handle(Smi::FromInt(7), isolate)};
CHECK(!Execution::Call(isolate, main_fun_wrapper, global, 1, args).is_null());
}
WASM_COMPILED_EXEC_TEST(WasmRemoveBreakPoint) {
WasmRunner<int> runner(execution_tier);
Isolate* isolate = runner.main_isolate();
BUILD(runner, WASM_NOP, WASM_NOP, WASM_NOP, WASM_NOP, WASM_NOP,
WASM_I32V_1(14));
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(runner.function_index());
SetBreakpoint(&runner, runner.function_index(), 1, 1);
SetBreakpoint(&runner, runner.function_index(), 2, 2);
Handle<BreakPoint> to_delete =
SetBreakpoint(&runner, runner.function_index(), 3, 3);
SetBreakpoint(&runner, runner.function_index(), 4, 4);
BreakHandler count_breaks(isolate, {{1, BreakHandler::Continue},
{2, BreakHandler::Continue,
[&runner, &to_delete]() {
ClearBreakpoint(
&runner, runner.function_index(),
3, to_delete);
}},
{4, BreakHandler::Continue}});
Handle<Object> global(isolate->context().global_object(), isolate);
MaybeHandle<Object> retval =
Execution::Call(isolate, main_fun_wrapper, global, 0, nullptr);
CHECK_EQ(14, GetIntReturnValue(retval));
}
WASM_COMPILED_EXEC_TEST(WasmRemoveLastBreakPoint) {
WasmRunner<int> runner(execution_tier);
Isolate* isolate = runner.main_isolate();
BUILD(runner, WASM_NOP, WASM_NOP, WASM_NOP, WASM_NOP, WASM_NOP,
WASM_I32V_1(14));
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(runner.function_index());
SetBreakpoint(&runner, runner.function_index(), 1, 1);
SetBreakpoint(&runner, runner.function_index(), 2, 2);
Handle<BreakPoint> to_delete =
SetBreakpoint(&runner, runner.function_index(), 3, 3);
BreakHandler count_breaks(
isolate, {{1, BreakHandler::Continue},
{2, BreakHandler::Continue, [&runner, &to_delete]() {
ClearBreakpoint(&runner, runner.function_index(), 3,
to_delete);
}}});
Handle<Object> global(isolate->context().global_object(), isolate);
MaybeHandle<Object> retval =
Execution::Call(isolate, main_fun_wrapper, global, 0, nullptr);
CHECK_EQ(14, GetIntReturnValue(retval));
}
WASM_COMPILED_EXEC_TEST(WasmRemoveAllBreakPoint) {
WasmRunner<int> runner(execution_tier);
Isolate* isolate = runner.main_isolate();
BUILD(runner, WASM_NOP, WASM_NOP, WASM_NOP, WASM_NOP, WASM_NOP,
WASM_I32V_1(14));
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(runner.function_index());
Handle<BreakPoint> bp1 =
SetBreakpoint(&runner, runner.function_index(), 1, 1);
Handle<BreakPoint> bp2 =
SetBreakpoint(&runner, runner.function_index(), 2, 2);
Handle<BreakPoint> bp3 =
SetBreakpoint(&runner, runner.function_index(), 3, 3);
BreakHandler count_breaks(
isolate, {{1, BreakHandler::Continue, [&runner, &bp1, &bp2, &bp3]() {
ClearBreakpoint(&runner, runner.function_index(), 1, bp1);
ClearBreakpoint(&runner, runner.function_index(), 3, bp3);
ClearBreakpoint(&runner, runner.function_index(), 2, bp2);
}}});
Handle<Object> global(isolate->context().global_object(), isolate);
MaybeHandle<Object> retval =
Execution::Call(isolate, main_fun_wrapper, global, 0, nullptr);
CHECK_EQ(14, GetIntReturnValue(retval));
}
WASM_COMPILED_EXEC_TEST(WasmBreakInPostMVP) {
// This test checks that we don't fail if experimental / post-MVP opcodes are
// being used. There was a bug where we were trying to update the "detected"
// features set, but we were passing a nullptr when compiling with
// breakpoints.
WasmRunner<int> runner(execution_tier);
Isolate* isolate = runner.main_isolate();
// [] -> [i32, i32]
ValueType sig_types[] = {kWasmI32, kWasmI32};
FunctionSig sig{2, 0, sig_types};
uint8_t sig_idx = runner.builder().AddSignature(&sig);
constexpr int kReturn = 13;
constexpr int kIgnored = 23;
BUILD(runner,
WASM_BLOCK_X(sig_idx, WASM_I32V_1(kReturn), WASM_I32V_1(kIgnored)),
WASM_DROP);
Handle<JSFunction> main_fun_wrapper =
runner.builder().WrapCode(runner.function_index());
SetBreakpoint(&runner, runner.function_index(), 3, 3);
BreakHandler count_breaks(isolate, {{3, BreakHandler::Continue}});
Handle<Object> global(isolate->context().global_object(), isolate);
MaybeHandle<Object> retval =
Execution::Call(isolate, main_fun_wrapper, global, 0, nullptr);
CHECK_EQ(kReturn, GetIntReturnValue(retval));
}
WASM_COMPILED_EXEC_TEST(Regress10889) {
FLAG_SCOPE(print_wasm_code);
WasmRunner<int> runner(execution_tier);
BUILD(runner, WASM_I32V_1(0));
SetBreakpoint(&runner, runner.function_index(), 1, 1);
}
} // namespace wasm
} // namespace internal
} // namespace v8