blob: 8696a70b922376a1447775a1afc93b6f02880b44 [file] [log] [blame]
// Copyright 2020 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 <initializer_list>
#include "src/api/api-inl.h"
#include "src/base/macros.h"
#include "src/codegen/assembler-inl.h"
#include "src/compiler/heap-refs.h"
#include "src/debug/debug-evaluate.h"
#include "src/debug/debug-interface.h"
#include "src/diagnostics/disassembler.h"
#include "src/execution/frames-inl.h"
#include "src/execution/frames.h"
#include "src/objects/js-objects.h"
#include "src/objects/property-descriptor.h"
#include "src/utils/utils.h"
#include "src/utils/vector.h"
#include "src/wasm/compilation-environment.h"
#include "src/wasm/module-decoder.h"
#include "src/wasm/value-type.h"
#include "src/wasm/wasm-constants.h"
#include "src/wasm/wasm-debug-evaluate.h"
#include "src/wasm/wasm-debug.h"
#include "src/wasm/wasm-module-builder.h"
#include "src/wasm/wasm-module.h"
#include "src/wasm/wasm-objects-inl.h"
#include "src/wasm/wasm-opcodes.h"
#include "src/wasm/wasm-tier.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 {
static Handle<String> V8String(Isolate* isolate, const char* str) {
return isolate->factory()->NewStringFromAsciiChecked(str);
}
namespace {
template <typename... FunctionArgsT>
class TestCode {
public:
TestCode(WasmRunnerBase* runner, std::initializer_list<byte> code,
std::initializer_list<ValueType::Kind> locals = {})
: compiler_(&runner->NewFunction<FunctionArgsT...>()),
code_(code),
locals_(static_cast<int32_t>(locals.size())) {
for (ValueType::Kind T : locals) {
compiler_->AllocateLocal(ValueType::Primitive(T));
}
compiler_->Build(code.begin(), code.end());
}
Handle<BreakPoint> BreakOnReturn(WasmRunnerBase* runner) {
runner->TierDown();
uint32_t return_idx = FindReturn();
uint32_t return_offset_in_function =
static_cast<uint32_t>(LEBHelper::sizeof_i32v(locals_)) + 2 * locals_ +
return_idx;
int function_index = compiler_->function_index();
int function_offset =
runner->builder().GetFunctionAt(function_index)->code.offset();
int return_offset_in_module = function_offset + return_offset_in_function;
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());
int expected_breakpoint_position = return_offset_in_module;
CHECK(WasmScript::SetBreakPoint(script, &return_offset_in_module,
break_point));
// Check that the breakpoint doesn't slide
DCHECK_EQ(expected_breakpoint_position, return_offset_in_module);
USE(expected_breakpoint_position);
return break_point;
}
MaybeHandle<Object> Run(WasmRunnerBase* runner) {
Isolate* isolate = runner->main_isolate();
Handle<JSFunction> fun_wrapper =
runner->builder().WrapCode(compiler_->function_index());
Handle<Object> global(isolate->context().global_object(), isolate);
return Execution::Call(isolate, fun_wrapper, global, 0, nullptr);
}
private:
uint32_t FindReturn() const {
for (auto i = code_.begin(); i != code_.end();
i += OpcodeLength(&*i, &*code_.end())) {
if (*i == kExprReturn) {
return static_cast<uint32_t>(std::distance(code_.begin(), i));
}
}
UNREACHABLE();
}
WasmFunctionCompiler* compiler_;
std::vector<byte> code_;
int32_t locals_;
};
class WasmEvaluatorBuilder {
public:
explicit WasmEvaluatorBuilder(TestExecutionTier execution_tier,
uint32_t min_memory = 1,
uint32_t max_memory = 1)
: zone_(&allocator_, ZONE_NAME), builder_(&zone_) {
get_memory_function_index = AddImport<void, uint32_t, uint32_t, uint32_t>(
CStrVector("__getMemory"));
get_local_function_index =
AddImport<void, uint32_t, uint32_t>(CStrVector("__getLocal"));
get_global_function_index =
AddImport<void, uint32_t, uint32_t>(CStrVector("__getGlobal"));
get_operand_function_index =
AddImport<void, uint32_t, uint32_t>(CStrVector("__getOperand"));
sbrk_function_index = AddImport<uint32_t, uint32_t>(CStrVector("__sbrk"));
wasm_format_function =
builder_.AddFunction(WasmRunnerBase::CreateSig<uint32_t>(&zone_));
wasm_format_function->SetName(CStrVector("wasm_format"));
builder_.AddExport(CStrVector("wasm_format"), wasm_format_function);
builder_.SetMinMemorySize(min_memory);
builder_.SetMaxMemorySize(max_memory);
}
template <typename ReturnT, typename... ArgTs>
uint32_t AddImport(Vector<const char> name) {
return builder_.AddImport(
name, WasmRunnerBase::CreateSig<ReturnT, ArgTs...>(&zone_),
CStrVector("env"));
}
void push_back(std::initializer_list<byte> code) {
wasm_format_function->EmitCode(code.begin(),
static_cast<uint32_t>(code.size()));
}
void CallSbrk(std::initializer_list<byte> args) {
push_back(args);
push_back({WASM_CALL_FUNCTION0(sbrk_function_index)});
}
void CallGetOperand(std::initializer_list<byte> args) {
push_back(args);
push_back({WASM_CALL_FUNCTION0(get_operand_function_index)});
}
void CallGetGlobal(std::initializer_list<byte> args) {
push_back(args);
push_back({WASM_CALL_FUNCTION0(get_global_function_index)});
}
void CallGetLocal(std::initializer_list<byte> args) {
push_back(args);
push_back({WASM_CALL_FUNCTION0(get_local_function_index)});
}
void CallGetMemory(std::initializer_list<byte> args) {
push_back(args);
push_back({WASM_CALL_FUNCTION0(get_memory_function_index)});
}
ZoneBuffer bytes() {
ZoneBuffer bytes(&zone_);
builder_.WriteTo(&bytes);
return bytes;
}
private:
v8::internal::AccountingAllocator allocator_;
Zone zone_;
WasmModuleBuilder builder_;
uint32_t get_memory_function_index = 0;
uint32_t get_local_function_index = 0;
uint32_t get_global_function_index = 0;
uint32_t get_operand_function_index = 0;
uint32_t sbrk_function_index = 0;
WasmFunctionBuilder* wasm_format_function = nullptr;
};
class WasmBreakHandler : public debug::DebugDelegate {
public:
struct EvaluationResult {
Maybe<std::string> result = Nothing<std::string>();
Maybe<std::string> error = Nothing<std::string>();
};
WasmBreakHandler(Isolate* isolate, ZoneBuffer evaluator_bytes)
: isolate_(isolate),
evaluator_bytes_(std::move(evaluator_bytes)),
result_(Nothing<EvaluationResult>()) {
v8::debug::SetDebugDelegate(reinterpret_cast<v8::Isolate*>(isolate_), this);
}
~WasmBreakHandler() override {
v8::debug::SetDebugDelegate(reinterpret_cast<v8::Isolate*>(isolate_),
nullptr);
}
const Maybe<EvaluationResult>& result() const { return result_; }
private:
Isolate* isolate_;
ZoneBuffer evaluator_bytes_;
Maybe<EvaluationResult> result_;
Maybe<std::string> GetPendingExceptionAsString() {
if (!isolate_->has_pending_exception()) return Nothing<std::string>();
Handle<Object> exception(isolate_->pending_exception(), isolate_);
isolate_->clear_pending_exception();
Handle<String> exception_string;
if (!Object::ToString(isolate_, exception).ToHandle(&exception_string)) {
return Just<std::string>("");
}
return Just<std::string>(exception_string->ToCString().get());
}
void BreakProgramRequested(v8::Local<v8::Context> paused_context,
const std::vector<int>&) override {
// Check the current position.
StackTraceFrameIterator frame_it(isolate_);
WasmFrame* frame = WasmFrame::cast(frame_it.frame());
Handle<WasmInstanceObject> instance{frame->wasm_instance(), isolate_};
MaybeHandle<String> result_handle = v8::internal::wasm::DebugEvaluate(
{evaluator_bytes_.begin(), evaluator_bytes_.size()}, instance,
frame_it.frame());
Maybe<std::string> error_message = GetPendingExceptionAsString();
Maybe<std::string> result_message =
result_handle.is_null()
? Nothing<std::string>()
: Just<std::string>(
result_handle.ToHandleChecked()->ToCString().get());
isolate_->clear_pending_exception();
result_ = Just<EvaluationResult>({result_message, error_message});
}
};
class WasmJSBreakHandler : public debug::DebugDelegate {
public:
struct EvaluationResult {
Maybe<std::string> result = Nothing<std::string>();
Maybe<std::string> error = Nothing<std::string>();
};
WasmJSBreakHandler(Isolate* isolate, Handle<String> snippet)
: isolate_(isolate),
snippet_(snippet),
result_(Nothing<EvaluationResult>()) {
v8::debug::SetDebugDelegate(reinterpret_cast<v8::Isolate*>(isolate_), this);
}
~WasmJSBreakHandler() override {
v8::debug::SetDebugDelegate(reinterpret_cast<v8::Isolate*>(isolate_),
nullptr);
}
const Maybe<EvaluationResult>& result() const { return result_; }
private:
Isolate* isolate_;
Handle<String> snippet_;
Maybe<EvaluationResult> result_;
Maybe<std::string> GetPendingExceptionAsString() const {
if (!isolate_->has_pending_exception()) return Nothing<std::string>();
Handle<Object> exception(isolate_->pending_exception(), isolate_);
isolate_->clear_pending_exception();
Handle<String> exception_string;
if (!Object::ToString(isolate_, exception).ToHandle(&exception_string)) {
return Just<std::string>("");
}
return Just<std::string>(exception_string->ToCString().get());
}
Maybe<std::string> GetResultAsString(MaybeHandle<Object> result) const {
Handle<Object> just_result;
if (!result.ToHandle(&just_result)) return Nothing<std::string>();
MaybeHandle<String> maybe_string = Object::ToString(isolate_, just_result);
Handle<String> just_string;
if (!maybe_string.ToHandle(&just_string)) return Nothing<std::string>();
return Just<std::string>(just_string->ToCString().get());
}
void BreakProgramRequested(v8::Local<v8::Context> paused_context,
const std::vector<int>&) override {
StackTraceFrameIterator frame_it(isolate_);
WasmFrame* frame = WasmFrame::cast(frame_it.frame());
Handle<WasmInstanceObject> instance{frame->wasm_instance(), isolate_};
MaybeHandle<Object> result_handle = DebugEvaluate::WebAssembly(
instance, frame_it.frame()->id(), snippet_, false);
Maybe<std::string> error_message = GetPendingExceptionAsString();
Maybe<std::string> result_message = GetResultAsString(result_handle);
isolate_->clear_pending_exception();
result_ = Just<EvaluationResult>({result_message, error_message});
}
};
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_CompileFailed) {
WasmRunner<int> runner(execution_tier);
TestCode<int> code(&runner, {WASM_RETURN1(WASM_I32V_1(32))});
code.BreakOnReturn(&runner);
WasmEvaluatorBuilder evaluator(execution_tier);
// Create a module that doesn't compile by missing the END bytecode.
evaluator.push_back({WASM_RETURN1(WASM_I32V_1(33))});
Isolate* isolate = runner.main_isolate();
WasmBreakHandler break_handler(isolate, evaluator.bytes());
CHECK(!code.Run(&runner).is_null());
WasmBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK(result.result.IsNothing());
CHECK_NE(result.error.ToChecked().find(
"function body must end with \"end\" opcode"),
std::string::npos);
}
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_MissingEntrypoint) {
WasmRunner<int> runner(execution_tier);
TestCode<int> code(&runner, {WASM_RETURN1(WASM_I32V_1(32))});
code.BreakOnReturn(&runner);
v8::internal::AccountingAllocator allocator;
Zone zone(&allocator, ZONE_NAME);
WasmModuleBuilder evaluator(&zone);
ZoneBuffer evaluator_bytes(&zone);
evaluator.WriteTo(&evaluator_bytes);
Isolate* isolate = runner.main_isolate();
WasmBreakHandler break_handler(isolate, std::move(evaluator_bytes));
CHECK(!code.Run(&runner).is_null());
WasmBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK(result.result.IsNothing());
CHECK_NE(result.error.ToChecked().find("Missing export: \"wasm_format\""),
std::string::npos);
}
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_ExecuteFailed_SEGV) {
WasmRunner<int> runner(execution_tier);
runner.builder().AddMemoryElems<int32_t>(64);
TestCode<int> code(&runner, {WASM_RETURN1(WASM_I32V_1(32))});
// Use a max memory size of 2 here to verify the precondition for the
// GrowMemory test below.
WasmEvaluatorBuilder evaluator(execution_tier, 1, 2);
code.BreakOnReturn(&runner);
// Load 1 byte from an address that's too high.
evaluator.CallGetMemory(
{WASM_I32V_1(32), WASM_I32V_1(1), WASM_I32V_3((1 << 16) + 1)});
evaluator.push_back({WASM_RETURN1(WASM_I32V_1(33)), WASM_END});
Isolate* isolate = runner.main_isolate();
WasmBreakHandler break_handler(isolate, evaluator.bytes());
CHECK(!code.Run(&runner).is_null());
WasmBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK(result.result.IsNothing());
CHECK_NE(
result.error.ToChecked().find("Illegal access to out-of-bounds memory"),
std::string::npos);
}
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_GrowMemory) {
WasmRunner<int> runner(execution_tier);
runner.builder().AddMemoryElems<int32_t>(64);
TestCode<int> code(
&runner,
{WASM_STORE_MEM(MachineType::Int32(), WASM_I32V_1(32), WASM_I32V_2('A')),
WASM_RETURN1(WASM_LOAD_MEM(MachineType::Int32(), WASM_I32V_1(32)))});
code.BreakOnReturn(&runner);
WasmEvaluatorBuilder evaluator(execution_tier, 1, 2);
// Grow the memory.
evaluator.CallSbrk({WASM_I32V_1(1)});
// Load 1 byte from an address that's too high for the default memory.
evaluator.CallGetMemory(
{WASM_I32V_1(32), WASM_I32V_1(1), WASM_I32V_3((1 << 16) + 1)});
evaluator.push_back({WASM_RETURN1(WASM_I32V_3((1 << 16) + 1)), WASM_END});
Isolate* isolate = runner.main_isolate();
WasmBreakHandler break_handler(isolate, evaluator.bytes());
CHECK(!code.Run(&runner).is_null());
WasmBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK(result.error.IsNothing());
CHECK_EQ(result.result.ToChecked(), "A");
}
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_LinearMemory) {
WasmRunner<int> runner(execution_tier);
runner.builder().AddMemoryElems<int32_t>(64);
TestCode<int> code(
&runner,
{WASM_STORE_MEM(MachineType::Int32(), WASM_I32V_1(32), WASM_I32V_2('A')),
WASM_RETURN1(WASM_LOAD_MEM(MachineType::Int32(), WASM_I32V_1(32)))});
code.BreakOnReturn(&runner);
WasmEvaluatorBuilder evaluator(execution_tier);
// Load 4 bytes from debuggee memory at address 32, and store at the offset 33
// of the linear memory.
evaluator.CallGetMemory({WASM_I32V_1(32), WASM_I32V_1(4), WASM_I32V_1(33)});
evaluator.push_back({WASM_RETURN1(WASM_I32V_1(33)), WASM_END});
Isolate* isolate = runner.main_isolate();
WasmBreakHandler break_handler(isolate, evaluator.bytes());
CHECK(!code.Run(&runner).is_null());
WasmBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK(result.error.IsNothing());
CHECK_EQ(result.result.ToChecked(), "A");
}
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_Locals) {
WasmRunner<int> runner(execution_tier);
runner.builder().AddMemoryElems<int32_t>(64);
TestCode<int> code(
&runner,
{WASM_SET_LOCAL(0, WASM_I32V_2('A')), WASM_RETURN1(WASM_GET_LOCAL(0))},
{ValueType::kI32});
code.BreakOnReturn(&runner);
WasmEvaluatorBuilder evaluator(execution_tier);
evaluator.CallGetLocal({WASM_I32V_1(0), WASM_I32V_1(33)});
evaluator.push_back({WASM_RETURN1(WASM_I32V_1(33)), WASM_END});
Isolate* isolate = runner.main_isolate();
WasmBreakHandler break_handler(isolate, evaluator.bytes());
CHECK(!code.Run(&runner).is_null());
WasmBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK(result.error.IsNothing());
CHECK_EQ(result.result.ToChecked(), "A");
}
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_Globals) {
WasmRunner<int> runner(execution_tier);
runner.builder().AddMemoryElems<int32_t>(64);
runner.builder().AddGlobal<int32_t>();
runner.builder().AddGlobal<int32_t>();
TestCode<void> code(&runner,
{WASM_SET_GLOBAL(0, WASM_I32V_1('4')),
WASM_SET_GLOBAL(1, WASM_I32V_1('5')), WASM_RETURN0},
{});
code.BreakOnReturn(&runner);
WasmEvaluatorBuilder evaluator(execution_tier);
evaluator.CallGetGlobal({WASM_I32V_1(0), WASM_I32V_1(33)});
evaluator.CallGetGlobal({WASM_I32V_1(1), WASM_I32V_1(34)});
evaluator.push_back({WASM_RETURN1(WASM_I32V_1(33)), WASM_END});
Isolate* isolate = runner.main_isolate();
WasmBreakHandler break_handler(isolate, evaluator.bytes());
CHECK(!code.Run(&runner).is_null());
WasmBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK(result.error.IsNothing());
CHECK_EQ(result.result.ToChecked(), "45");
}
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_Operands) {
WasmRunner<int> runner(execution_tier);
runner.builder().AddMemoryElems<int32_t>(64);
TestCode<int> code(&runner,
{WASM_SET_LOCAL(0, WASM_I32V_1('4')), WASM_GET_LOCAL(0),
WASM_RETURN1(WASM_I32V_1('5'))},
{ValueType::kI32});
code.BreakOnReturn(&runner);
WasmEvaluatorBuilder evaluator(execution_tier);
evaluator.CallGetOperand({WASM_I32V_1(0), WASM_I32V_1(33)});
evaluator.CallGetOperand({WASM_I32V_1(1), WASM_I32V_1(34)});
evaluator.push_back({WASM_RETURN1(WASM_I32V_1(33)), WASM_END});
Isolate* isolate = runner.main_isolate();
WasmBreakHandler break_handler(isolate, evaluator.bytes());
CHECK(!code.Run(&runner).is_null());
WasmBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK(result.error.IsNothing());
CHECK_EQ(result.result.ToChecked(), "45");
}
WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_JavaScript) {
WasmRunner<int> runner(execution_tier);
runner.builder().AddGlobal<int32_t>();
runner.builder().AddMemoryElems<int32_t>(64);
uint16_t index = 0;
runner.builder().AddIndirectFunctionTable(&index, 1);
TestCode<int64_t> code(
&runner,
{WASM_SET_GLOBAL(0, WASM_I32V_2('B')),
WASM_SET_LOCAL(0, WASM_I64V_2('A')), WASM_RETURN1(WASM_GET_LOCAL(0))},
{ValueType::kI64});
code.BreakOnReturn(&runner);
Isolate* isolate = runner.main_isolate();
Handle<String> snippet =
V8String(isolate,
"JSON.stringify(["
"$global0, "
"$table0, "
"$var0, "
"$main, "
"$memory0, "
"globals[0], "
"tables[0], "
"locals[0], "
"functions[0], "
"memories[0], "
"memories, "
"tables, "
"stack, "
"imports, "
"exports, "
"globals, "
"locals, "
"functions, "
"], (k, v) => k === 'at' || typeof v === 'undefined' || typeof "
"v === 'object' ? v : v.toString())");
WasmJSBreakHandler break_handler(isolate, snippet);
CHECK(!code.Run(&runner).is_null());
WasmJSBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK_WITH_MSG(result.error.IsNothing(), result.error.ToChecked().c_str());
CHECK_EQ(result.result.ToChecked(),
"[\"66\",{},\"65\",\"function 0() { [native code] }\",{},"
"\"66\",{},\"65\",\"function 0() { [native code] }\",{},"
"{},{},{\"0\":\"65\"},{},{},{},{},{}]");
}
} // namespace
} // namespace wasm
} // namespace internal
} // namespace v8