// Copyright 2012 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/debug/debug.h"
|
|
#include <memory>
|
#include <unordered_set>
|
|
#include "src/api-inl.h"
|
#include "src/arguments.h"
|
#include "src/assembler-inl.h"
|
#include "src/base/platform/mutex.h"
|
#include "src/bootstrapper.h"
|
#include "src/builtins/builtins.h"
|
#include "src/code-stubs.h"
|
#include "src/compilation-cache.h"
|
#include "src/compiler.h"
|
#include "src/debug/debug-evaluate.h"
|
#include "src/debug/liveedit.h"
|
#include "src/deoptimizer.h"
|
#include "src/execution.h"
|
#include "src/frames-inl.h"
|
#include "src/global-handles.h"
|
#include "src/globals.h"
|
#include "src/interpreter/bytecode-array-accessor.h"
|
#include "src/interpreter/bytecode-array-iterator.h"
|
#include "src/interpreter/interpreter.h"
|
#include "src/isolate-inl.h"
|
#include "src/log.h"
|
#include "src/messages.h"
|
#include "src/objects/debug-objects-inl.h"
|
#include "src/objects/js-generator-inl.h"
|
#include "src/objects/js-promise-inl.h"
|
#include "src/snapshot/natives.h"
|
#include "src/snapshot/snapshot.h"
|
#include "src/wasm/wasm-objects-inl.h"
|
|
namespace v8 {
|
namespace internal {
|
|
class Debug::TemporaryObjectsTracker : public HeapObjectAllocationTracker {
|
public:
|
TemporaryObjectsTracker() = default;
|
~TemporaryObjectsTracker() = default;
|
|
void AllocationEvent(Address addr, int) override { objects_.insert(addr); }
|
|
void MoveEvent(Address from, Address to, int) override {
|
if (from == to) return;
|
base::LockGuard<base::Mutex> guard(&mutex_);
|
auto it = objects_.find(from);
|
if (it == objects_.end()) {
|
// If temporary object was collected we can get MoveEvent which moves
|
// existing non temporary object to the address where we had temporary
|
// object. So we should mark new address as non temporary.
|
objects_.erase(to);
|
return;
|
}
|
objects_.erase(it);
|
objects_.insert(to);
|
}
|
|
bool HasObject(Handle<HeapObject> obj) const {
|
if (obj->IsJSObject() &&
|
Handle<JSObject>::cast(obj)->GetEmbedderFieldCount()) {
|
// Embedder may store any pointers using embedder fields and implements
|
// non trivial logic, e.g. create wrappers lazily and store pointer to
|
// native object inside embedder field. We should consider all objects
|
// with embedder fields as non temporary.
|
return false;
|
}
|
return objects_.find(obj->address()) != objects_.end();
|
}
|
|
private:
|
std::unordered_set<Address> objects_;
|
base::Mutex mutex_;
|
DISALLOW_COPY_AND_ASSIGN(TemporaryObjectsTracker);
|
};
|
|
Debug::Debug(Isolate* isolate)
|
: is_active_(false),
|
hook_on_function_call_(false),
|
is_suppressed_(false),
|
break_disabled_(false),
|
break_points_active_(true),
|
break_on_exception_(false),
|
break_on_uncaught_exception_(false),
|
side_effect_check_failed_(false),
|
debug_info_list_(nullptr),
|
feature_tracker_(isolate),
|
isolate_(isolate) {
|
ThreadInit();
|
}
|
|
Debug::~Debug() { DCHECK_NULL(debug_delegate_); }
|
|
BreakLocation BreakLocation::FromFrame(Handle<DebugInfo> debug_info,
|
JavaScriptFrame* frame) {
|
if (debug_info->CanBreakAtEntry()) {
|
return BreakLocation(Debug::kBreakAtEntryPosition, DEBUG_BREAK_AT_ENTRY);
|
}
|
auto summary = FrameSummary::GetTop(frame).AsJavaScript();
|
int offset = summary.code_offset();
|
Handle<AbstractCode> abstract_code = summary.abstract_code();
|
BreakIterator it(debug_info);
|
it.SkipTo(BreakIndexFromCodeOffset(debug_info, abstract_code, offset));
|
return it.GetBreakLocation();
|
}
|
|
void BreakLocation::AllAtCurrentStatement(
|
Handle<DebugInfo> debug_info, JavaScriptFrame* frame,
|
std::vector<BreakLocation>* result_out) {
|
DCHECK(!debug_info->CanBreakAtEntry());
|
auto summary = FrameSummary::GetTop(frame).AsJavaScript();
|
int offset = summary.code_offset();
|
Handle<AbstractCode> abstract_code = summary.abstract_code();
|
if (abstract_code->IsCode()) offset = offset - 1;
|
int statement_position;
|
{
|
BreakIterator it(debug_info);
|
it.SkipTo(BreakIndexFromCodeOffset(debug_info, abstract_code, offset));
|
statement_position = it.statement_position();
|
}
|
for (BreakIterator it(debug_info); !it.Done(); it.Next()) {
|
if (it.statement_position() == statement_position) {
|
result_out->push_back(it.GetBreakLocation());
|
}
|
}
|
}
|
|
JSGeneratorObject* BreakLocation::GetGeneratorObjectForSuspendedFrame(
|
JavaScriptFrame* frame) const {
|
DCHECK(IsSuspend());
|
DCHECK_GE(generator_obj_reg_index_, 0);
|
|
Object* generator_obj =
|
InterpretedFrame::cast(frame)->ReadInterpreterRegister(
|
generator_obj_reg_index_);
|
|
return JSGeneratorObject::cast(generator_obj);
|
}
|
|
int BreakLocation::BreakIndexFromCodeOffset(Handle<DebugInfo> debug_info,
|
Handle<AbstractCode> abstract_code,
|
int offset) {
|
// Run through all break points to locate the one closest to the address.
|
int closest_break = 0;
|
int distance = kMaxInt;
|
DCHECK(0 <= offset && offset < abstract_code->Size());
|
for (BreakIterator it(debug_info); !it.Done(); it.Next()) {
|
// Check if this break point is closer that what was previously found.
|
if (it.code_offset() <= offset && offset - it.code_offset() < distance) {
|
closest_break = it.break_index();
|
distance = offset - it.code_offset();
|
// Check whether we can't get any closer.
|
if (distance == 0) break;
|
}
|
}
|
return closest_break;
|
}
|
|
bool BreakLocation::HasBreakPoint(Isolate* isolate,
|
Handle<DebugInfo> debug_info) const {
|
// First check whether there is a break point with the same source position.
|
if (!debug_info->HasBreakPoint(isolate, position_)) return false;
|
if (debug_info->CanBreakAtEntry()) {
|
DCHECK_EQ(Debug::kBreakAtEntryPosition, position_);
|
return debug_info->BreakAtEntry();
|
} else {
|
// Then check whether a break point at that source position would have
|
// the same code offset. Otherwise it's just a break location that we can
|
// step to, but not actually a location where we can put a break point.
|
DCHECK(abstract_code_->IsBytecodeArray());
|
BreakIterator it(debug_info);
|
it.SkipToPosition(position_);
|
return it.code_offset() == code_offset_;
|
}
|
}
|
|
debug::BreakLocationType BreakLocation::type() const {
|
switch (type_) {
|
case DEBUGGER_STATEMENT:
|
return debug::kDebuggerStatementBreakLocation;
|
case DEBUG_BREAK_SLOT_AT_CALL:
|
return debug::kCallBreakLocation;
|
case DEBUG_BREAK_SLOT_AT_RETURN:
|
return debug::kReturnBreakLocation;
|
|
// Externally, suspend breaks should look like normal breaks.
|
case DEBUG_BREAK_SLOT_AT_SUSPEND:
|
default:
|
return debug::kCommonBreakLocation;
|
}
|
}
|
|
BreakIterator::BreakIterator(Handle<DebugInfo> debug_info)
|
: debug_info_(debug_info),
|
break_index_(-1),
|
source_position_iterator_(
|
debug_info->DebugBytecodeArray()->SourcePositionTable()) {
|
position_ = debug_info->shared()->StartPosition();
|
statement_position_ = position_;
|
// There is at least one break location.
|
DCHECK(!Done());
|
Next();
|
}
|
|
int BreakIterator::BreakIndexFromPosition(int source_position) {
|
int distance = kMaxInt;
|
int closest_break = break_index();
|
while (!Done()) {
|
int next_position = position();
|
if (source_position <= next_position &&
|
next_position - source_position < distance) {
|
closest_break = break_index();
|
distance = next_position - source_position;
|
// Check whether we can't get any closer.
|
if (distance == 0) break;
|
}
|
Next();
|
}
|
return closest_break;
|
}
|
|
void BreakIterator::Next() {
|
DisallowHeapAllocation no_gc;
|
DCHECK(!Done());
|
bool first = break_index_ == -1;
|
while (!Done()) {
|
if (!first) source_position_iterator_.Advance();
|
first = false;
|
if (Done()) return;
|
position_ = source_position_iterator_.source_position().ScriptOffset();
|
if (source_position_iterator_.is_statement()) {
|
statement_position_ = position_;
|
}
|
DCHECK_LE(0, position_);
|
DCHECK_LE(0, statement_position_);
|
|
DebugBreakType type = GetDebugBreakType();
|
if (type != NOT_DEBUG_BREAK) break;
|
}
|
break_index_++;
|
}
|
|
DebugBreakType BreakIterator::GetDebugBreakType() {
|
BytecodeArray* bytecode_array = debug_info_->OriginalBytecodeArray();
|
interpreter::Bytecode bytecode =
|
interpreter::Bytecodes::FromByte(bytecode_array->get(code_offset()));
|
|
// Make sure we read the actual bytecode, not a prefix scaling bytecode.
|
if (interpreter::Bytecodes::IsPrefixScalingBytecode(bytecode)) {
|
bytecode = interpreter::Bytecodes::FromByte(
|
bytecode_array->get(code_offset() + 1));
|
}
|
|
if (bytecode == interpreter::Bytecode::kDebugger) {
|
return DEBUGGER_STATEMENT;
|
} else if (bytecode == interpreter::Bytecode::kReturn) {
|
return DEBUG_BREAK_SLOT_AT_RETURN;
|
} else if (bytecode == interpreter::Bytecode::kSuspendGenerator) {
|
return DEBUG_BREAK_SLOT_AT_SUSPEND;
|
} else if (interpreter::Bytecodes::IsCallOrConstruct(bytecode)) {
|
return DEBUG_BREAK_SLOT_AT_CALL;
|
} else if (source_position_iterator_.is_statement()) {
|
return DEBUG_BREAK_SLOT;
|
} else {
|
return NOT_DEBUG_BREAK;
|
}
|
}
|
|
void BreakIterator::SkipToPosition(int position) {
|
BreakIterator it(debug_info_);
|
SkipTo(it.BreakIndexFromPosition(position));
|
}
|
|
void BreakIterator::SetDebugBreak() {
|
DebugBreakType debug_break_type = GetDebugBreakType();
|
if (debug_break_type == DEBUGGER_STATEMENT) return;
|
HandleScope scope(isolate());
|
DCHECK(debug_break_type >= DEBUG_BREAK_SLOT);
|
Handle<BytecodeArray> bytecode_array(debug_info_->DebugBytecodeArray(),
|
isolate());
|
interpreter::BytecodeArrayAccessor(bytecode_array, code_offset())
|
.ApplyDebugBreak();
|
}
|
|
void BreakIterator::ClearDebugBreak() {
|
DebugBreakType debug_break_type = GetDebugBreakType();
|
if (debug_break_type == DEBUGGER_STATEMENT) return;
|
DCHECK(debug_break_type >= DEBUG_BREAK_SLOT);
|
BytecodeArray* bytecode_array = debug_info_->DebugBytecodeArray();
|
BytecodeArray* original = debug_info_->OriginalBytecodeArray();
|
bytecode_array->set(code_offset(), original->get(code_offset()));
|
}
|
|
BreakLocation BreakIterator::GetBreakLocation() {
|
Handle<AbstractCode> code(
|
AbstractCode::cast(debug_info_->DebugBytecodeArray()), isolate());
|
DebugBreakType type = GetDebugBreakType();
|
int generator_object_reg_index = -1;
|
if (type == DEBUG_BREAK_SLOT_AT_SUSPEND) {
|
// For suspend break, we'll need the generator object to be able to step
|
// over the suspend as if it didn't return. We get the interpreter register
|
// index that holds the generator object by reading it directly off the
|
// bytecode array, and we'll read the actual generator object off the
|
// interpreter stack frame in GetGeneratorObjectForSuspendedFrame.
|
BytecodeArray* bytecode_array = debug_info_->OriginalBytecodeArray();
|
interpreter::BytecodeArrayAccessor accessor(
|
handle(bytecode_array, isolate()), code_offset());
|
|
DCHECK_EQ(accessor.current_bytecode(),
|
interpreter::Bytecode::kSuspendGenerator);
|
interpreter::Register generator_obj_reg = accessor.GetRegisterOperand(0);
|
generator_object_reg_index = generator_obj_reg.index();
|
}
|
return BreakLocation(code, type, code_offset(), position_,
|
generator_object_reg_index);
|
}
|
|
Isolate* BreakIterator::isolate() { return debug_info_->GetIsolate(); }
|
|
void DebugFeatureTracker::Track(DebugFeatureTracker::Feature feature) {
|
uint32_t mask = 1 << feature;
|
// Only count one sample per feature and isolate.
|
if (bitfield_ & mask) return;
|
isolate_->counters()->debug_feature_usage()->AddSample(feature);
|
bitfield_ |= mask;
|
}
|
|
|
// Threading support.
|
void Debug::ThreadInit() {
|
thread_local_.break_frame_id_ = StackFrame::NO_ID;
|
thread_local_.last_step_action_ = StepNone;
|
thread_local_.last_statement_position_ = kNoSourcePosition;
|
thread_local_.last_frame_count_ = -1;
|
thread_local_.fast_forward_to_return_ = false;
|
thread_local_.ignore_step_into_function_ = Smi::kZero;
|
thread_local_.target_frame_count_ = -1;
|
thread_local_.return_value_ = Smi::kZero;
|
thread_local_.last_breakpoint_id_ = 0;
|
clear_suspended_generator();
|
thread_local_.restart_fp_ = kNullAddress;
|
base::Relaxed_Store(&thread_local_.current_debug_scope_,
|
static_cast<base::AtomicWord>(0));
|
thread_local_.break_on_next_function_call_ = false;
|
UpdateHookOnFunctionCall();
|
}
|
|
|
char* Debug::ArchiveDebug(char* storage) {
|
MemCopy(storage, reinterpret_cast<char*>(&thread_local_),
|
ArchiveSpacePerThread());
|
return storage + ArchiveSpacePerThread();
|
}
|
|
char* Debug::RestoreDebug(char* storage) {
|
MemCopy(reinterpret_cast<char*>(&thread_local_), storage,
|
ArchiveSpacePerThread());
|
|
// Enter the debugger.
|
DebugScope debug_scope(this);
|
|
// Clear any one-shot breakpoints that may have been set by the other
|
// thread, and reapply breakpoints for this thread.
|
ClearOneShot();
|
|
if (thread_local_.last_step_action_ != StepNone) {
|
// Reset the previous step action for this thread.
|
PrepareStep(thread_local_.last_step_action_);
|
}
|
|
return storage + ArchiveSpacePerThread();
|
}
|
|
int Debug::ArchiveSpacePerThread() { return sizeof(ThreadLocal); }
|
|
void Debug::Iterate(RootVisitor* v) {
|
v->VisitRootPointer(Root::kDebug, nullptr, &thread_local_.return_value_);
|
v->VisitRootPointer(Root::kDebug, nullptr,
|
&thread_local_.suspended_generator_);
|
v->VisitRootPointer(Root::kDebug, nullptr,
|
&thread_local_.ignore_step_into_function_);
|
}
|
|
DebugInfoListNode::DebugInfoListNode(Isolate* isolate, DebugInfo* debug_info)
|
: next_(nullptr) {
|
// Globalize the request debug info object and make it weak.
|
GlobalHandles* global_handles = isolate->global_handles();
|
debug_info_ = global_handles->Create(debug_info).location();
|
}
|
|
|
DebugInfoListNode::~DebugInfoListNode() {
|
if (debug_info_ == nullptr) return;
|
GlobalHandles::Destroy(reinterpret_cast<Object**>(debug_info_));
|
debug_info_ = nullptr;
|
}
|
|
void Debug::Unload() {
|
ClearAllBreakPoints();
|
ClearStepping();
|
RemoveAllCoverageInfos();
|
ClearAllDebuggerHints();
|
debug_delegate_ = nullptr;
|
}
|
|
void Debug::Break(JavaScriptFrame* frame, Handle<JSFunction> break_target) {
|
// Initialize LiveEdit.
|
LiveEdit::InitializeThreadLocal(this);
|
|
// Just continue if breaks are disabled or debugger cannot be loaded.
|
if (break_disabled()) return;
|
|
// Enter the debugger.
|
DebugScope debug_scope(this);
|
|
// Postpone interrupt during breakpoint processing.
|
PostponeInterruptsScope postpone(isolate_);
|
DisableBreak no_recursive_break(this);
|
|
// Return if we fail to retrieve debug info.
|
Handle<SharedFunctionInfo> shared(break_target->shared(), isolate_);
|
if (!EnsureBreakInfo(shared)) return;
|
PrepareFunctionForDebugExecution(shared);
|
|
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
|
|
// Find the break location where execution has stopped.
|
BreakLocation location = BreakLocation::FromFrame(debug_info, frame);
|
|
// Find actual break points, if any, and trigger debug break event.
|
MaybeHandle<FixedArray> break_points_hit =
|
CheckBreakPoints(debug_info, &location);
|
if (!break_points_hit.is_null() || break_on_next_function_call()) {
|
// Clear all current stepping setup.
|
ClearStepping();
|
// Notify the debug event listeners.
|
OnDebugBreak(!break_points_hit.is_null()
|
? break_points_hit.ToHandleChecked()
|
: isolate_->factory()->empty_fixed_array());
|
return;
|
}
|
|
// Debug break at function entry, do not worry about stepping.
|
if (location.IsDebugBreakAtEntry()) {
|
DCHECK(debug_info->BreakAtEntry());
|
return;
|
}
|
|
DCHECK_NOT_NULL(frame);
|
|
// No break point. Check for stepping.
|
StepAction step_action = last_step_action();
|
int current_frame_count = CurrentFrameCount();
|
int target_frame_count = thread_local_.target_frame_count_;
|
int last_frame_count = thread_local_.last_frame_count_;
|
|
// StepOut at not return position was requested and return break locations
|
// were flooded with one shots.
|
if (thread_local_.fast_forward_to_return_) {
|
DCHECK(location.IsReturnOrSuspend());
|
// We have to ignore recursive calls to function.
|
if (current_frame_count > target_frame_count) return;
|
ClearStepping();
|
PrepareStep(StepOut);
|
return;
|
}
|
|
bool step_break = false;
|
switch (step_action) {
|
case StepNone:
|
return;
|
case StepOut:
|
// Step out should not break in a deeper frame than target frame.
|
if (current_frame_count > target_frame_count) return;
|
step_break = true;
|
break;
|
case StepNext:
|
// Step next should not break in a deeper frame than target frame.
|
if (current_frame_count > target_frame_count) return;
|
V8_FALLTHROUGH;
|
case StepIn: {
|
// Special case "next" and "in" for generators that are about to suspend.
|
if (location.IsSuspend()) {
|
DCHECK(!has_suspended_generator());
|
thread_local_.suspended_generator_ =
|
location.GetGeneratorObjectForSuspendedFrame(frame);
|
ClearStepping();
|
return;
|
}
|
|
FrameSummary summary = FrameSummary::GetTop(frame);
|
step_break = step_break || location.IsReturn() ||
|
current_frame_count != last_frame_count ||
|
thread_local_.last_statement_position_ !=
|
summary.SourceStatementPosition();
|
break;
|
}
|
}
|
|
// Clear all current stepping setup.
|
ClearStepping();
|
|
if (step_break) {
|
// Notify the debug event listeners.
|
OnDebugBreak(isolate_->factory()->empty_fixed_array());
|
} else {
|
// Re-prepare to continue.
|
PrepareStep(step_action);
|
}
|
}
|
|
|
// Find break point objects for this location, if any, and evaluate them.
|
// Return an array of break point objects that evaluated true, or an empty
|
// handle if none evaluated true.
|
MaybeHandle<FixedArray> Debug::CheckBreakPoints(Handle<DebugInfo> debug_info,
|
BreakLocation* location,
|
bool* has_break_points) {
|
bool has_break_points_to_check =
|
break_points_active_ && location->HasBreakPoint(isolate_, debug_info);
|
if (has_break_points) *has_break_points = has_break_points_to_check;
|
if (!has_break_points_to_check) return {};
|
|
return Debug::GetHitBreakPoints(debug_info, location->position());
|
}
|
|
|
bool Debug::IsMutedAtCurrentLocation(JavaScriptFrame* frame) {
|
HandleScope scope(isolate_);
|
// A break location is considered muted if break locations on the current
|
// statement have at least one break point, and all of these break points
|
// evaluate to false. Aside from not triggering a debug break event at the
|
// break location, we also do not trigger one for debugger statements, nor
|
// an exception event on exception at this location.
|
FrameSummary summary = FrameSummary::GetTop(frame);
|
DCHECK(!summary.IsWasm());
|
Handle<JSFunction> function = summary.AsJavaScript().function();
|
if (!function->shared()->HasBreakInfo()) return false;
|
Handle<DebugInfo> debug_info(function->shared()->GetDebugInfo(), isolate_);
|
// Enter the debugger.
|
DebugScope debug_scope(this);
|
std::vector<BreakLocation> break_locations;
|
BreakLocation::AllAtCurrentStatement(debug_info, frame, &break_locations);
|
bool has_break_points_at_all = false;
|
for (size_t i = 0; i < break_locations.size(); i++) {
|
bool has_break_points;
|
MaybeHandle<FixedArray> check_result =
|
CheckBreakPoints(debug_info, &break_locations[i], &has_break_points);
|
has_break_points_at_all |= has_break_points;
|
if (has_break_points && !check_result.is_null()) return false;
|
}
|
return has_break_points_at_all;
|
}
|
|
// Check whether a single break point object is triggered.
|
bool Debug::CheckBreakPoint(Handle<BreakPoint> break_point,
|
bool is_break_at_entry) {
|
HandleScope scope(isolate_);
|
|
if (!break_point->condition()->length()) return true;
|
Handle<String> condition(break_point->condition(), isolate_);
|
MaybeHandle<Object> maybe_result;
|
Handle<Object> result;
|
|
if (is_break_at_entry) {
|
maybe_result = DebugEvaluate::WithTopmostArguments(isolate_, condition);
|
} else {
|
// Since we call CheckBreakpoint only for deoptimized frame on top of stack,
|
// we can use 0 as index of inlined frame.
|
const int inlined_jsframe_index = 0;
|
const bool throw_on_side_effect = false;
|
maybe_result =
|
DebugEvaluate::Local(isolate_, break_frame_id(), inlined_jsframe_index,
|
condition, throw_on_side_effect);
|
}
|
|
if (!maybe_result.ToHandle(&result)) {
|
if (isolate_->has_pending_exception()) {
|
isolate_->clear_pending_exception();
|
}
|
return false;
|
}
|
return result->BooleanValue(isolate_);
|
}
|
|
bool Debug::SetBreakPoint(Handle<JSFunction> function,
|
Handle<BreakPoint> break_point,
|
int* source_position) {
|
HandleScope scope(isolate_);
|
|
// Make sure the function is compiled and has set up the debug info.
|
Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
|
if (!EnsureBreakInfo(shared)) return false;
|
PrepareFunctionForDebugExecution(shared);
|
|
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
|
// Source positions starts with zero.
|
DCHECK_LE(0, *source_position);
|
|
// Find the break point and change it.
|
*source_position = FindBreakablePosition(debug_info, *source_position);
|
DebugInfo::SetBreakPoint(isolate_, debug_info, *source_position, break_point);
|
// At least one active break point now.
|
DCHECK_LT(0, debug_info->GetBreakPointCount(isolate_));
|
|
ClearBreakPoints(debug_info);
|
ApplyBreakPoints(debug_info);
|
|
feature_tracker()->Track(DebugFeatureTracker::kBreakPoint);
|
return true;
|
}
|
|
bool Debug::SetBreakPointForScript(Handle<Script> script,
|
Handle<String> condition,
|
int* source_position, int* id) {
|
*id = ++thread_local_.last_breakpoint_id_;
|
Handle<BreakPoint> break_point =
|
isolate_->factory()->NewBreakPoint(*id, condition);
|
if (script->type() == Script::TYPE_WASM) {
|
Handle<WasmModuleObject> module_object(
|
WasmModuleObject::cast(script->wasm_module_object()), isolate_);
|
return WasmModuleObject::SetBreakPoint(module_object, source_position,
|
break_point);
|
}
|
|
HandleScope scope(isolate_);
|
|
// Obtain shared function info for the function.
|
Handle<Object> result =
|
FindSharedFunctionInfoInScript(script, *source_position);
|
if (result->IsUndefined(isolate_)) return false;
|
|
// Make sure the function has set up the debug info.
|
Handle<SharedFunctionInfo> shared = Handle<SharedFunctionInfo>::cast(result);
|
if (!EnsureBreakInfo(shared)) return false;
|
PrepareFunctionForDebugExecution(shared);
|
|
// Find position within function. The script position might be before the
|
// source position of the first function.
|
if (shared->StartPosition() > *source_position) {
|
*source_position = shared->StartPosition();
|
}
|
|
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
|
|
// Find breakable position returns first breakable position after
|
// *source_position, it can return 0 if no break location is found after
|
// *source_position.
|
int breakable_position = FindBreakablePosition(debug_info, *source_position);
|
if (breakable_position < *source_position) return false;
|
*source_position = breakable_position;
|
|
DebugInfo::SetBreakPoint(isolate_, debug_info, *source_position, break_point);
|
// At least one active break point now.
|
DCHECK_LT(0, debug_info->GetBreakPointCount(isolate_));
|
|
ClearBreakPoints(debug_info);
|
ApplyBreakPoints(debug_info);
|
|
feature_tracker()->Track(DebugFeatureTracker::kBreakPoint);
|
return true;
|
}
|
|
int Debug::FindBreakablePosition(Handle<DebugInfo> debug_info,
|
int source_position) {
|
if (debug_info->CanBreakAtEntry()) {
|
return kBreakAtEntryPosition;
|
} else {
|
DCHECK(debug_info->HasInstrumentedBytecodeArray());
|
BreakIterator it(debug_info);
|
it.SkipToPosition(source_position);
|
return it.position();
|
}
|
}
|
|
void Debug::ApplyBreakPoints(Handle<DebugInfo> debug_info) {
|
DisallowHeapAllocation no_gc;
|
if (debug_info->CanBreakAtEntry()) {
|
debug_info->SetBreakAtEntry();
|
} else {
|
if (!debug_info->HasInstrumentedBytecodeArray()) return;
|
FixedArray* break_points = debug_info->break_points();
|
for (int i = 0; i < break_points->length(); i++) {
|
if (break_points->get(i)->IsUndefined(isolate_)) continue;
|
BreakPointInfo* info = BreakPointInfo::cast(break_points->get(i));
|
if (info->GetBreakPointCount(isolate_) == 0) continue;
|
DCHECK(debug_info->HasInstrumentedBytecodeArray());
|
BreakIterator it(debug_info);
|
it.SkipToPosition(info->source_position());
|
it.SetDebugBreak();
|
}
|
}
|
debug_info->SetDebugExecutionMode(DebugInfo::kBreakpoints);
|
}
|
|
void Debug::ClearBreakPoints(Handle<DebugInfo> debug_info) {
|
if (debug_info->CanBreakAtEntry()) {
|
debug_info->ClearBreakAtEntry();
|
} else {
|
// If we attempt to clear breakpoints but none exist, simply return. This
|
// can happen e.g. CoverageInfos exist but no breakpoints are set.
|
if (!debug_info->HasInstrumentedBytecodeArray() ||
|
!debug_info->HasBreakInfo()) {
|
return;
|
}
|
|
DisallowHeapAllocation no_gc;
|
for (BreakIterator it(debug_info); !it.Done(); it.Next()) {
|
it.ClearDebugBreak();
|
}
|
}
|
}
|
|
void Debug::ClearBreakPoint(Handle<BreakPoint> break_point) {
|
HandleScope scope(isolate_);
|
|
for (DebugInfoListNode* node = debug_info_list_; node != nullptr;
|
node = node->next()) {
|
if (!node->debug_info()->HasBreakInfo()) continue;
|
Handle<Object> result = DebugInfo::FindBreakPointInfo(
|
isolate_, node->debug_info(), break_point);
|
if (result->IsUndefined(isolate_)) continue;
|
Handle<DebugInfo> debug_info = node->debug_info();
|
if (DebugInfo::ClearBreakPoint(isolate_, debug_info, break_point)) {
|
ClearBreakPoints(debug_info);
|
if (debug_info->GetBreakPointCount(isolate_) == 0) {
|
RemoveBreakInfoAndMaybeFree(debug_info);
|
} else {
|
ApplyBreakPoints(debug_info);
|
}
|
return;
|
}
|
}
|
}
|
|
int Debug::GetFunctionDebuggingId(Handle<JSFunction> function) {
|
Handle<SharedFunctionInfo> shared = handle(function->shared(), isolate_);
|
Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared);
|
int id = debug_info->debugging_id();
|
if (id == DebugInfo::kNoDebuggingId) {
|
id = isolate_->heap()->NextDebuggingId();
|
debug_info->set_debugging_id(id);
|
}
|
return id;
|
}
|
|
bool Debug::SetBreakpointForFunction(Handle<JSFunction> function,
|
Handle<String> condition, int* id) {
|
*id = ++thread_local_.last_breakpoint_id_;
|
Handle<BreakPoint> breakpoint =
|
isolate_->factory()->NewBreakPoint(*id, condition);
|
int source_position = 0;
|
return SetBreakPoint(function, breakpoint, &source_position);
|
}
|
|
void Debug::RemoveBreakpoint(int id) {
|
Handle<BreakPoint> breakpoint = isolate_->factory()->NewBreakPoint(
|
id, isolate_->factory()->empty_string());
|
ClearBreakPoint(breakpoint);
|
}
|
|
// Clear out all the debug break code.
|
void Debug::ClearAllBreakPoints() {
|
ClearAllDebugInfos([=](Handle<DebugInfo> info) {
|
ClearBreakPoints(info);
|
info->ClearBreakInfo(isolate_);
|
});
|
}
|
|
void Debug::FloodWithOneShot(Handle<SharedFunctionInfo> shared,
|
bool returns_only) {
|
if (IsBlackboxed(shared)) return;
|
// Make sure the function is compiled and has set up the debug info.
|
if (!EnsureBreakInfo(shared)) return;
|
PrepareFunctionForDebugExecution(shared);
|
|
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
|
// Flood the function with break points.
|
DCHECK(debug_info->HasInstrumentedBytecodeArray());
|
for (BreakIterator it(debug_info); !it.Done(); it.Next()) {
|
if (returns_only && !it.GetBreakLocation().IsReturnOrSuspend()) continue;
|
it.SetDebugBreak();
|
}
|
}
|
|
void Debug::ChangeBreakOnException(ExceptionBreakType type, bool enable) {
|
if (type == BreakUncaughtException) {
|
break_on_uncaught_exception_ = enable;
|
} else {
|
break_on_exception_ = enable;
|
}
|
}
|
|
|
bool Debug::IsBreakOnException(ExceptionBreakType type) {
|
if (type == BreakUncaughtException) {
|
return break_on_uncaught_exception_;
|
} else {
|
return break_on_exception_;
|
}
|
}
|
|
MaybeHandle<FixedArray> Debug::GetHitBreakPoints(Handle<DebugInfo> debug_info,
|
int position) {
|
Handle<Object> break_points = debug_info->GetBreakPoints(isolate_, position);
|
bool is_break_at_entry = debug_info->BreakAtEntry();
|
DCHECK(!break_points->IsUndefined(isolate_));
|
if (!break_points->IsFixedArray()) {
|
if (!CheckBreakPoint(Handle<BreakPoint>::cast(break_points),
|
is_break_at_entry)) {
|
return {};
|
}
|
Handle<FixedArray> break_points_hit = isolate_->factory()->NewFixedArray(1);
|
break_points_hit->set(0, *break_points);
|
return break_points_hit;
|
}
|
|
Handle<FixedArray> array(FixedArray::cast(*break_points), isolate_);
|
int num_objects = array->length();
|
Handle<FixedArray> break_points_hit =
|
isolate_->factory()->NewFixedArray(num_objects);
|
int break_points_hit_count = 0;
|
for (int i = 0; i < num_objects; ++i) {
|
Handle<Object> break_point(array->get(i), isolate_);
|
if (CheckBreakPoint(Handle<BreakPoint>::cast(break_point),
|
is_break_at_entry)) {
|
break_points_hit->set(break_points_hit_count++, *break_point);
|
}
|
}
|
if (break_points_hit_count == 0) return {};
|
break_points_hit->Shrink(isolate_, break_points_hit_count);
|
return break_points_hit;
|
}
|
|
void Debug::SetBreakOnNextFunctionCall() {
|
// This method forces V8 to break on next function call regardless current
|
// last_step_action_. If any break happens between SetBreakOnNextFunctionCall
|
// and ClearBreakOnNextFunctionCall, we will clear this flag and stepping. If
|
// break does not happen, e.g. all called functions are blackboxed or no
|
// function is called, then we will clear this flag and let stepping continue
|
// its normal business.
|
thread_local_.break_on_next_function_call_ = true;
|
UpdateHookOnFunctionCall();
|
}
|
|
void Debug::ClearBreakOnNextFunctionCall() {
|
thread_local_.break_on_next_function_call_ = false;
|
UpdateHookOnFunctionCall();
|
}
|
|
void Debug::PrepareStepIn(Handle<JSFunction> function) {
|
CHECK(last_step_action() >= StepIn || break_on_next_function_call());
|
if (ignore_events()) return;
|
if (in_debug_scope()) return;
|
if (break_disabled()) return;
|
Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
|
if (IsBlackboxed(shared)) return;
|
if (*function == thread_local_.ignore_step_into_function_) return;
|
thread_local_.ignore_step_into_function_ = Smi::kZero;
|
FloodWithOneShot(Handle<SharedFunctionInfo>(function->shared(), isolate_));
|
}
|
|
void Debug::PrepareStepInSuspendedGenerator() {
|
CHECK(has_suspended_generator());
|
if (ignore_events()) return;
|
if (in_debug_scope()) return;
|
if (break_disabled()) return;
|
thread_local_.last_step_action_ = StepIn;
|
UpdateHookOnFunctionCall();
|
Handle<JSFunction> function(
|
JSGeneratorObject::cast(thread_local_.suspended_generator_)->function(),
|
isolate_);
|
FloodWithOneShot(Handle<SharedFunctionInfo>(function->shared(), isolate_));
|
clear_suspended_generator();
|
}
|
|
void Debug::PrepareStepOnThrow() {
|
if (last_step_action() == StepNone) return;
|
if (ignore_events()) return;
|
if (in_debug_scope()) return;
|
if (break_disabled()) return;
|
|
ClearOneShot();
|
|
int current_frame_count = CurrentFrameCount();
|
|
// Iterate through the JavaScript stack looking for handlers.
|
JavaScriptFrameIterator it(isolate_);
|
while (!it.done()) {
|
JavaScriptFrame* frame = it.frame();
|
if (frame->LookupExceptionHandlerInTable(nullptr, nullptr) > 0) break;
|
std::vector<SharedFunctionInfo*> infos;
|
frame->GetFunctions(&infos);
|
current_frame_count -= infos.size();
|
it.Advance();
|
}
|
|
// No handler found. Nothing to instrument.
|
if (it.done()) return;
|
|
bool found_handler = false;
|
// Iterate frames, including inlined frames. First, find the handler frame.
|
// Then skip to the frame we want to break in, then instrument for stepping.
|
for (; !it.done(); it.Advance()) {
|
JavaScriptFrame* frame = JavaScriptFrame::cast(it.frame());
|
if (last_step_action() == StepIn) {
|
// Deoptimize frame to ensure calls are checked for step-in.
|
Deoptimizer::DeoptimizeFunction(frame->function());
|
}
|
std::vector<FrameSummary> summaries;
|
frame->Summarize(&summaries);
|
for (size_t i = summaries.size(); i != 0; i--, current_frame_count--) {
|
const FrameSummary& summary = summaries[i - 1];
|
if (!found_handler) {
|
// We have yet to find the handler. If the frame inlines multiple
|
// functions, we have to check each one for the handler.
|
// If it only contains one function, we already found the handler.
|
if (summaries.size() > 1) {
|
Handle<AbstractCode> code = summary.AsJavaScript().abstract_code();
|
CHECK_EQ(AbstractCode::INTERPRETED_FUNCTION, code->kind());
|
HandlerTable table(code->GetBytecodeArray());
|
int code_offset = summary.code_offset();
|
HandlerTable::CatchPrediction prediction;
|
int index = table.LookupRange(code_offset, nullptr, &prediction);
|
if (index > 0) found_handler = true;
|
} else {
|
found_handler = true;
|
}
|
}
|
|
if (found_handler) {
|
// We found the handler. If we are stepping next or out, we need to
|
// iterate until we found the suitable target frame to break in.
|
if ((last_step_action() == StepNext || last_step_action() == StepOut) &&
|
current_frame_count > thread_local_.target_frame_count_) {
|
continue;
|
}
|
Handle<SharedFunctionInfo> info(
|
summary.AsJavaScript().function()->shared(), isolate_);
|
if (IsBlackboxed(info)) continue;
|
FloodWithOneShot(info);
|
return;
|
}
|
}
|
}
|
}
|
|
|
void Debug::PrepareStep(StepAction step_action) {
|
HandleScope scope(isolate_);
|
|
DCHECK(in_debug_scope());
|
|
// Get the frame where the execution has stopped and skip the debug frame if
|
// any. The debug frame will only be present if execution was stopped due to
|
// hitting a break point. In other situations (e.g. unhandled exception) the
|
// debug frame is not present.
|
StackFrame::Id frame_id = break_frame_id();
|
// If there is no JavaScript stack don't do anything.
|
if (frame_id == StackFrame::NO_ID) return;
|
|
feature_tracker()->Track(DebugFeatureTracker::kStepping);
|
|
thread_local_.last_step_action_ = step_action;
|
|
StackTraceFrameIterator frames_it(isolate_, frame_id);
|
StandardFrame* frame = frames_it.frame();
|
|
// Handle stepping in wasm functions via the wasm interpreter.
|
if (frame->is_wasm()) {
|
// If the top frame is compiled, we cannot step.
|
if (frame->is_wasm_compiled()) return;
|
WasmInterpreterEntryFrame* wasm_frame =
|
WasmInterpreterEntryFrame::cast(frame);
|
wasm_frame->debug_info()->PrepareStep(step_action);
|
return;
|
}
|
|
JavaScriptFrame* js_frame = JavaScriptFrame::cast(frame);
|
DCHECK(js_frame->function()->IsJSFunction());
|
|
// Get the debug info (create it if it does not exist).
|
auto summary = FrameSummary::GetTop(frame).AsJavaScript();
|
Handle<JSFunction> function(summary.function());
|
Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
|
if (!EnsureBreakInfo(shared)) return;
|
PrepareFunctionForDebugExecution(shared);
|
|
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
|
|
BreakLocation location = BreakLocation::FromFrame(debug_info, js_frame);
|
|
// Any step at a return is a step-out, and a step-out at a suspend behaves
|
// like a return.
|
if (location.IsReturn() || (location.IsSuspend() && step_action == StepOut)) {
|
// On StepOut we'll ignore our further calls to current function in
|
// PrepareStepIn callback.
|
if (last_step_action() == StepOut) {
|
thread_local_.ignore_step_into_function_ = *function;
|
}
|
step_action = StepOut;
|
thread_local_.last_step_action_ = StepIn;
|
}
|
|
// We need to schedule DebugOnFunction call callback
|
UpdateHookOnFunctionCall();
|
|
// A step-next in blackboxed function is a step-out.
|
if (step_action == StepNext && IsBlackboxed(shared)) step_action = StepOut;
|
|
thread_local_.last_statement_position_ =
|
summary.abstract_code()->SourceStatementPosition(summary.code_offset());
|
int current_frame_count = CurrentFrameCount();
|
thread_local_.last_frame_count_ = current_frame_count;
|
// No longer perform the current async step.
|
clear_suspended_generator();
|
|
switch (step_action) {
|
case StepNone:
|
UNREACHABLE();
|
break;
|
case StepOut: {
|
// Clear last position info. For stepping out it does not matter.
|
thread_local_.last_statement_position_ = kNoSourcePosition;
|
thread_local_.last_frame_count_ = -1;
|
if (!location.IsReturnOrSuspend() && !IsBlackboxed(shared)) {
|
// At not return position we flood return positions with one shots and
|
// will repeat StepOut automatically at next break.
|
thread_local_.target_frame_count_ = current_frame_count;
|
thread_local_.fast_forward_to_return_ = true;
|
FloodWithOneShot(shared, true);
|
return;
|
}
|
// Skip the current frame, find the first frame we want to step out to
|
// and deoptimize every frame along the way.
|
bool in_current_frame = true;
|
for (; !frames_it.done(); frames_it.Advance()) {
|
// TODO(clemensh): Implement stepping out from JS to wasm.
|
if (frames_it.frame()->is_wasm()) continue;
|
JavaScriptFrame* frame = JavaScriptFrame::cast(frames_it.frame());
|
if (last_step_action() == StepIn) {
|
// Deoptimize frame to ensure calls are checked for step-in.
|
Deoptimizer::DeoptimizeFunction(frame->function());
|
}
|
HandleScope scope(isolate_);
|
std::vector<Handle<SharedFunctionInfo>> infos;
|
frame->GetFunctions(&infos);
|
for (; !infos.empty(); current_frame_count--) {
|
Handle<SharedFunctionInfo> info = infos.back();
|
infos.pop_back();
|
if (in_current_frame) {
|
// We want to skip out, so skip the current frame.
|
in_current_frame = false;
|
continue;
|
}
|
if (IsBlackboxed(info)) continue;
|
FloodWithOneShot(info);
|
thread_local_.target_frame_count_ = current_frame_count;
|
return;
|
}
|
}
|
break;
|
}
|
case StepNext:
|
thread_local_.target_frame_count_ = current_frame_count;
|
V8_FALLTHROUGH;
|
case StepIn:
|
// TODO(clemensh): Implement stepping from JS into wasm.
|
FloodWithOneShot(shared);
|
break;
|
}
|
}
|
|
// Simple function for returning the source positions for active break points.
|
Handle<Object> Debug::GetSourceBreakLocations(
|
Isolate* isolate, Handle<SharedFunctionInfo> shared) {
|
if (!shared->HasBreakInfo()) {
|
return isolate->factory()->undefined_value();
|
}
|
|
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate);
|
if (debug_info->GetBreakPointCount(isolate) == 0) {
|
return isolate->factory()->undefined_value();
|
}
|
Handle<FixedArray> locations = isolate->factory()->NewFixedArray(
|
debug_info->GetBreakPointCount(isolate));
|
int count = 0;
|
for (int i = 0; i < debug_info->break_points()->length(); ++i) {
|
if (!debug_info->break_points()->get(i)->IsUndefined(isolate)) {
|
BreakPointInfo* break_point_info =
|
BreakPointInfo::cast(debug_info->break_points()->get(i));
|
int break_points = break_point_info->GetBreakPointCount(isolate);
|
if (break_points == 0) continue;
|
for (int j = 0; j < break_points; ++j) {
|
locations->set(count++,
|
Smi::FromInt(break_point_info->source_position()));
|
}
|
}
|
}
|
return locations;
|
}
|
|
void Debug::ClearStepping() {
|
// Clear the various stepping setup.
|
ClearOneShot();
|
|
thread_local_.last_step_action_ = StepNone;
|
thread_local_.last_statement_position_ = kNoSourcePosition;
|
thread_local_.ignore_step_into_function_ = Smi::kZero;
|
thread_local_.fast_forward_to_return_ = false;
|
thread_local_.last_frame_count_ = -1;
|
thread_local_.target_frame_count_ = -1;
|
thread_local_.break_on_next_function_call_ = false;
|
UpdateHookOnFunctionCall();
|
}
|
|
|
// Clears all the one-shot break points that are currently set. Normally this
|
// function is called each time a break point is hit as one shot break points
|
// are used to support stepping.
|
void Debug::ClearOneShot() {
|
// The current implementation just runs through all the breakpoints. When the
|
// last break point for a function is removed that function is automatically
|
// removed from the list.
|
for (DebugInfoListNode* node = debug_info_list_; node != nullptr;
|
node = node->next()) {
|
Handle<DebugInfo> debug_info = node->debug_info();
|
ClearBreakPoints(debug_info);
|
ApplyBreakPoints(debug_info);
|
}
|
}
|
|
class RedirectActiveFunctions : public ThreadVisitor {
|
public:
|
explicit RedirectActiveFunctions(SharedFunctionInfo* shared)
|
: shared_(shared) {
|
DCHECK(shared->HasBytecodeArray());
|
}
|
|
void VisitThread(Isolate* isolate, ThreadLocalTop* top) {
|
for (JavaScriptFrameIterator it(isolate, top); !it.done(); it.Advance()) {
|
JavaScriptFrame* frame = it.frame();
|
JSFunction* function = frame->function();
|
if (!frame->is_interpreted()) continue;
|
if (function->shared() != shared_) continue;
|
InterpretedFrame* interpreted_frame =
|
reinterpret_cast<InterpretedFrame*>(frame);
|
BytecodeArray* debug_copy = shared_->GetDebugInfo()->DebugBytecodeArray();
|
interpreted_frame->PatchBytecodeArray(debug_copy);
|
}
|
}
|
|
private:
|
SharedFunctionInfo* shared_;
|
DisallowHeapAllocation no_gc_;
|
};
|
|
void Debug::DeoptimizeFunction(Handle<SharedFunctionInfo> shared) {
|
// Deoptimize all code compiled from this shared function info including
|
// inlining.
|
isolate_->AbortConcurrentOptimization(BlockingBehavior::kBlock);
|
|
// Make sure we abort incremental marking.
|
isolate_->heap()->CollectAllGarbage(Heap::kMakeHeapIterableMask,
|
GarbageCollectionReason::kDebugger);
|
|
bool found_something = false;
|
Code::OptimizedCodeIterator iterator(isolate_);
|
while (Code* code = iterator.Next()) {
|
if (code->Inlines(*shared)) {
|
code->set_marked_for_deoptimization(true);
|
found_something = true;
|
}
|
}
|
|
if (found_something) {
|
// Only go through with the deoptimization if something was found.
|
Deoptimizer::DeoptimizeMarkedCode(isolate_);
|
}
|
}
|
|
void Debug::PrepareFunctionForDebugExecution(
|
Handle<SharedFunctionInfo> shared) {
|
// To prepare bytecode for debugging, we already need to have the debug
|
// info (containing the debug copy) upfront, but since we do not recompile,
|
// preparing for break points cannot fail.
|
DCHECK(shared->is_compiled());
|
DCHECK(shared->HasDebugInfo());
|
Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared);
|
if (debug_info->flags() & DebugInfo::kPreparedForDebugExecution) return;
|
|
// Make a copy of the bytecode array if available.
|
Handle<Object> maybe_original_bytecode_array =
|
isolate_->factory()->undefined_value();
|
if (shared->HasBytecodeArray()) {
|
Handle<BytecodeArray> original_bytecode_array =
|
handle(shared->GetBytecodeArray(), isolate_);
|
Handle<BytecodeArray> debug_bytecode_array =
|
isolate_->factory()->CopyBytecodeArray(original_bytecode_array);
|
shared->SetDebugBytecodeArray(*debug_bytecode_array);
|
maybe_original_bytecode_array = original_bytecode_array;
|
}
|
debug_info->set_original_bytecode_array(*maybe_original_bytecode_array);
|
|
if (debug_info->CanBreakAtEntry()) {
|
// Deopt everything in case the function is inlined anywhere.
|
Deoptimizer::DeoptimizeAll(isolate_);
|
InstallDebugBreakTrampoline();
|
} else {
|
DeoptimizeFunction(shared);
|
// Update PCs on the stack to point to recompiled code.
|
RedirectActiveFunctions redirect_visitor(*shared);
|
redirect_visitor.VisitThread(isolate_, isolate_->thread_local_top());
|
isolate_->thread_manager()->IterateArchivedThreads(&redirect_visitor);
|
}
|
debug_info->set_flags(debug_info->flags() |
|
DebugInfo::kPreparedForDebugExecution);
|
}
|
|
void Debug::InstallDebugBreakTrampoline() {
|
// Check the list of debug infos whether the debug break trampoline needs to
|
// be installed. If that's the case, iterate the heap for functions to rewire
|
// to the trampoline.
|
HandleScope scope(isolate_);
|
// If there is a breakpoint at function entry, we need to install trampoline.
|
bool needs_to_use_trampoline = false;
|
// If there we break at entry to an api callback, we need to clear ICs.
|
bool needs_to_clear_ic = false;
|
for (DebugInfoListNode* current = debug_info_list_; current != nullptr;
|
current = current->next()) {
|
if (current->debug_info()->CanBreakAtEntry()) {
|
needs_to_use_trampoline = true;
|
if (current->debug_info()->shared()->IsApiFunction()) {
|
needs_to_clear_ic = true;
|
break;
|
}
|
}
|
}
|
|
if (!needs_to_use_trampoline) return;
|
|
Handle<Code> trampoline = BUILTIN_CODE(isolate_, DebugBreakTrampoline);
|
std::vector<Handle<JSFunction>> needs_compile;
|
{
|
HeapIterator iterator(isolate_->heap());
|
while (HeapObject* obj = iterator.next()) {
|
if (needs_to_clear_ic && obj->IsFeedbackVector()) {
|
FeedbackVector::cast(obj)->ClearSlots(isolate_);
|
continue;
|
} else if (obj->IsJSFunction()) {
|
JSFunction* fun = JSFunction::cast(obj);
|
SharedFunctionInfo* shared = fun->shared();
|
if (!shared->HasDebugInfo()) continue;
|
if (!shared->GetDebugInfo()->CanBreakAtEntry()) continue;
|
if (!fun->is_compiled()) {
|
needs_compile.push_back(handle(fun, isolate_));
|
} else {
|
fun->set_code(*trampoline);
|
}
|
}
|
}
|
}
|
// By overwriting the function code with DebugBreakTrampoline, which tailcalls
|
// to shared code, we bypass CompileLazy. Perform CompileLazy here instead.
|
for (Handle<JSFunction> fun : needs_compile) {
|
Compiler::Compile(fun, Compiler::CLEAR_EXCEPTION);
|
fun->set_code(*trampoline);
|
}
|
}
|
|
namespace {
|
template <typename Iterator>
|
void GetBreakablePositions(Iterator* it, int start_position, int end_position,
|
std::vector<BreakLocation>* locations) {
|
while (!it->Done()) {
|
if (it->position() >= start_position && it->position() < end_position) {
|
locations->push_back(it->GetBreakLocation());
|
}
|
it->Next();
|
}
|
}
|
|
void FindBreakablePositions(Handle<DebugInfo> debug_info, int start_position,
|
int end_position,
|
std::vector<BreakLocation>* locations) {
|
DCHECK(debug_info->HasInstrumentedBytecodeArray());
|
BreakIterator it(debug_info);
|
GetBreakablePositions(&it, start_position, end_position, locations);
|
}
|
} // namespace
|
|
bool Debug::GetPossibleBreakpoints(Handle<Script> script, int start_position,
|
int end_position, bool restrict_to_function,
|
std::vector<BreakLocation>* locations) {
|
if (restrict_to_function) {
|
Handle<Object> result =
|
FindSharedFunctionInfoInScript(script, start_position);
|
if (result->IsUndefined(isolate_)) return false;
|
|
// Make sure the function has set up the debug info.
|
Handle<SharedFunctionInfo> shared =
|
Handle<SharedFunctionInfo>::cast(result);
|
if (!EnsureBreakInfo(shared)) return false;
|
PrepareFunctionForDebugExecution(shared);
|
|
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
|
FindBreakablePositions(debug_info, start_position, end_position, locations);
|
return true;
|
}
|
|
while (true) {
|
HandleScope scope(isolate_);
|
std::vector<Handle<SharedFunctionInfo>> candidates;
|
SharedFunctionInfo::ScriptIterator iterator(isolate_, *script);
|
for (SharedFunctionInfo* info = iterator.Next(); info != nullptr;
|
info = iterator.Next()) {
|
if (info->EndPosition() < start_position ||
|
info->StartPosition() >= end_position) {
|
continue;
|
}
|
if (!info->IsSubjectToDebugging()) continue;
|
if (!info->is_compiled() && !info->allows_lazy_compilation()) continue;
|
candidates.push_back(i::handle(info, isolate_));
|
}
|
|
bool was_compiled = false;
|
for (const auto& candidate : candidates) {
|
// Code that cannot be compiled lazily are internal and not debuggable.
|
DCHECK(candidate->allows_lazy_compilation());
|
if (!candidate->is_compiled()) {
|
if (!Compiler::Compile(candidate, Compiler::CLEAR_EXCEPTION)) {
|
return false;
|
} else {
|
was_compiled = true;
|
}
|
}
|
if (!EnsureBreakInfo(candidate)) return false;
|
PrepareFunctionForDebugExecution(candidate);
|
}
|
if (was_compiled) continue;
|
|
for (const auto& candidate : candidates) {
|
CHECK(candidate->HasBreakInfo());
|
Handle<DebugInfo> debug_info(candidate->GetDebugInfo(), isolate_);
|
FindBreakablePositions(debug_info, start_position, end_position,
|
locations);
|
}
|
return true;
|
}
|
UNREACHABLE();
|
}
|
|
class SharedFunctionInfoFinder {
|
public:
|
explicit SharedFunctionInfoFinder(int target_position)
|
: current_candidate_(nullptr),
|
current_candidate_closure_(nullptr),
|
current_start_position_(kNoSourcePosition),
|
target_position_(target_position) {}
|
|
void NewCandidate(SharedFunctionInfo* shared, JSFunction* closure = nullptr) {
|
if (!shared->IsSubjectToDebugging()) return;
|
int start_position = shared->function_token_position();
|
if (start_position == kNoSourcePosition) {
|
start_position = shared->StartPosition();
|
}
|
|
if (start_position > target_position_) return;
|
if (target_position_ > shared->EndPosition()) return;
|
|
if (current_candidate_ != nullptr) {
|
if (current_start_position_ == start_position &&
|
shared->EndPosition() == current_candidate_->EndPosition()) {
|
// If we already have a matching closure, do not throw it away.
|
if (current_candidate_closure_ != nullptr && closure == nullptr) return;
|
// If a top-level function contains only one function
|
// declaration the source for the top-level and the function
|
// is the same. In that case prefer the non top-level function.
|
if (!current_candidate_->is_toplevel() && shared->is_toplevel()) return;
|
} else if (start_position < current_start_position_ ||
|
current_candidate_->EndPosition() < shared->EndPosition()) {
|
return;
|
}
|
}
|
|
current_start_position_ = start_position;
|
current_candidate_ = shared;
|
current_candidate_closure_ = closure;
|
}
|
|
SharedFunctionInfo* Result() { return current_candidate_; }
|
|
JSFunction* ResultClosure() { return current_candidate_closure_; }
|
|
private:
|
SharedFunctionInfo* current_candidate_;
|
JSFunction* current_candidate_closure_;
|
int current_start_position_;
|
int target_position_;
|
DisallowHeapAllocation no_gc_;
|
};
|
|
|
// We need to find a SFI for a literal that may not yet have been compiled yet,
|
// and there may not be a JSFunction referencing it. Find the SFI closest to
|
// the given position, compile it to reveal possible inner SFIs and repeat.
|
// While we are at this, also ensure code with debug break slots so that we do
|
// not have to compile a SFI without JSFunction, which is paifu for those that
|
// cannot be compiled without context (need to find outer compilable SFI etc.)
|
Handle<Object> Debug::FindSharedFunctionInfoInScript(Handle<Script> script,
|
int position) {
|
for (int iteration = 0;; iteration++) {
|
// Go through all shared function infos associated with this script to
|
// find the inner most function containing this position.
|
// If there is no shared function info for this script at all, there is
|
// no point in looking for it by walking the heap.
|
|
SharedFunctionInfo* shared;
|
{
|
SharedFunctionInfoFinder finder(position);
|
SharedFunctionInfo::ScriptIterator iterator(isolate_, *script);
|
for (SharedFunctionInfo* info = iterator.Next(); info != nullptr;
|
info = iterator.Next()) {
|
finder.NewCandidate(info);
|
}
|
shared = finder.Result();
|
if (shared == nullptr) break;
|
// We found it if it's already compiled.
|
if (shared->is_compiled()) {
|
Handle<SharedFunctionInfo> shared_handle(shared, isolate_);
|
// If the iteration count is larger than 1, we had to compile the outer
|
// function in order to create this shared function info. So there can
|
// be no JSFunction referencing it. We can anticipate creating a debug
|
// info while bypassing PrepareFunctionForDebugExecution.
|
if (iteration > 1) {
|
AllowHeapAllocation allow_before_return;
|
CreateBreakInfo(shared_handle);
|
}
|
return shared_handle;
|
}
|
}
|
// If not, compile to reveal inner functions.
|
HandleScope scope(isolate_);
|
// Code that cannot be compiled lazily are internal and not debuggable.
|
DCHECK(shared->allows_lazy_compilation());
|
if (!Compiler::Compile(handle(shared, isolate_), Compiler::CLEAR_EXCEPTION))
|
break;
|
}
|
return isolate_->factory()->undefined_value();
|
}
|
|
|
// Ensures the debug information is present for shared.
|
bool Debug::EnsureBreakInfo(Handle<SharedFunctionInfo> shared) {
|
// Return if we already have the break info for shared.
|
if (shared->HasBreakInfo()) return true;
|
if (!shared->IsSubjectToDebugging() && !CanBreakAtEntry(shared)) {
|
return false;
|
}
|
if (!shared->is_compiled() &&
|
!Compiler::Compile(shared, Compiler::CLEAR_EXCEPTION)) {
|
return false;
|
}
|
if (shared->GetCode() ==
|
isolate_->builtins()->builtin(Builtins::kDeserializeLazy)) {
|
Snapshot::EnsureBuiltinIsDeserialized(isolate_, shared);
|
}
|
CreateBreakInfo(shared);
|
return true;
|
}
|
|
void Debug::CreateBreakInfo(Handle<SharedFunctionInfo> shared) {
|
HandleScope scope(isolate_);
|
Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared);
|
|
// Initialize with break information.
|
|
DCHECK(!debug_info->HasBreakInfo());
|
|
Factory* factory = isolate_->factory();
|
Handle<FixedArray> break_points(
|
factory->NewFixedArray(DebugInfo::kEstimatedNofBreakPointsInFunction));
|
|
int flags = debug_info->flags();
|
flags |= DebugInfo::kHasBreakInfo;
|
if (CanBreakAtEntry(shared)) flags |= DebugInfo::kCanBreakAtEntry;
|
debug_info->set_flags(flags);
|
debug_info->set_break_points(*break_points);
|
}
|
|
Handle<DebugInfo> Debug::GetOrCreateDebugInfo(
|
Handle<SharedFunctionInfo> shared) {
|
if (shared->HasDebugInfo()) return handle(shared->GetDebugInfo(), isolate_);
|
|
// Create debug info and add it to the list.
|
Handle<DebugInfo> debug_info = isolate_->factory()->NewDebugInfo(shared);
|
DebugInfoListNode* node = new DebugInfoListNode(isolate_, *debug_info);
|
node->set_next(debug_info_list_);
|
debug_info_list_ = node;
|
|
return debug_info;
|
}
|
|
void Debug::InstallCoverageInfo(Handle<SharedFunctionInfo> shared,
|
Handle<CoverageInfo> coverage_info) {
|
DCHECK(!coverage_info.is_null());
|
|
Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared);
|
|
DCHECK(!debug_info->HasCoverageInfo());
|
|
debug_info->set_flags(debug_info->flags() | DebugInfo::kHasCoverageInfo);
|
debug_info->set_coverage_info(*coverage_info);
|
}
|
|
void Debug::RemoveAllCoverageInfos() {
|
ClearAllDebugInfos(
|
[=](Handle<DebugInfo> info) { info->ClearCoverageInfo(isolate_); });
|
}
|
|
void Debug::ClearAllDebuggerHints() {
|
ClearAllDebugInfos(
|
[=](Handle<DebugInfo> info) { info->set_debugger_hints(0); });
|
}
|
|
void Debug::FindDebugInfo(Handle<DebugInfo> debug_info,
|
DebugInfoListNode** prev, DebugInfoListNode** curr) {
|
HandleScope scope(isolate_);
|
*prev = nullptr;
|
*curr = debug_info_list_;
|
while (*curr != nullptr) {
|
if ((*curr)->debug_info().is_identical_to(debug_info)) return;
|
*prev = *curr;
|
*curr = (*curr)->next();
|
}
|
|
UNREACHABLE();
|
}
|
|
void Debug::ClearAllDebugInfos(DebugInfoClearFunction clear_function) {
|
DebugInfoListNode* prev = nullptr;
|
DebugInfoListNode* current = debug_info_list_;
|
while (current != nullptr) {
|
DebugInfoListNode* next = current->next();
|
Handle<DebugInfo> debug_info = current->debug_info();
|
clear_function(debug_info);
|
if (debug_info->IsEmpty()) {
|
FreeDebugInfoListNode(prev, current);
|
current = next;
|
} else {
|
prev = current;
|
current = next;
|
}
|
}
|
}
|
|
void Debug::RemoveBreakInfoAndMaybeFree(Handle<DebugInfo> debug_info) {
|
debug_info->ClearBreakInfo(isolate_);
|
if (debug_info->IsEmpty()) {
|
DebugInfoListNode* prev;
|
DebugInfoListNode* node;
|
FindDebugInfo(debug_info, &prev, &node);
|
FreeDebugInfoListNode(prev, node);
|
}
|
}
|
|
void Debug::FreeDebugInfoListNode(DebugInfoListNode* prev,
|
DebugInfoListNode* node) {
|
DCHECK(node->debug_info()->IsEmpty());
|
|
// Unlink from list. If prev is nullptr we are looking at the first element.
|
if (prev == nullptr) {
|
debug_info_list_ = node->next();
|
} else {
|
prev->set_next(node->next());
|
}
|
|
// Pack script back into the
|
// SFI::script_or_debug_info field.
|
Handle<DebugInfo> debug_info(node->debug_info());
|
debug_info->shared()->set_script_or_debug_info(debug_info->script());
|
|
delete node;
|
}
|
|
bool Debug::IsBreakAtReturn(JavaScriptFrame* frame) {
|
HandleScope scope(isolate_);
|
|
// Get the executing function in which the debug break occurred.
|
Handle<SharedFunctionInfo> shared(frame->function()->shared(), isolate_);
|
|
// With no debug info there are no break points, so we can't be at a return.
|
if (!shared->HasBreakInfo()) return false;
|
|
DCHECK(!frame->is_optimized());
|
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
|
BreakLocation location = BreakLocation::FromFrame(debug_info, frame);
|
return location.IsReturn();
|
}
|
|
void Debug::ScheduleFrameRestart(StackFrame* frame) {
|
// Set a target FP for the FrameDropperTrampoline builtin to drop to once
|
// we return from the debugger.
|
DCHECK(frame->is_java_script());
|
// Only reschedule to a frame further below a frame we already scheduled for.
|
if (frame->fp() <= thread_local_.restart_fp_) return;
|
// If the frame is optimized, trigger a deopt and jump into the
|
// FrameDropperTrampoline in the deoptimizer.
|
thread_local_.restart_fp_ = frame->fp();
|
|
// Reset break frame ID to the frame below the restarted frame.
|
StackTraceFrameIterator it(isolate_);
|
thread_local_.break_frame_id_ = StackFrame::NO_ID;
|
for (StackTraceFrameIterator it(isolate_); !it.done(); it.Advance()) {
|
if (it.frame()->fp() > thread_local_.restart_fp_) {
|
thread_local_.break_frame_id_ = it.frame()->id();
|
return;
|
}
|
}
|
}
|
|
Handle<FixedArray> Debug::GetLoadedScripts() {
|
isolate_->heap()->CollectAllGarbage(Heap::kFinalizeIncrementalMarkingMask,
|
GarbageCollectionReason::kDebugger);
|
Factory* factory = isolate_->factory();
|
if (!factory->script_list()->IsWeakArrayList()) {
|
return factory->empty_fixed_array();
|
}
|
Handle<WeakArrayList> array =
|
Handle<WeakArrayList>::cast(factory->script_list());
|
Handle<FixedArray> results = factory->NewFixedArray(array->length());
|
int length = 0;
|
{
|
Script::Iterator iterator(isolate_);
|
Script* script;
|
while ((script = iterator.Next()) != nullptr) {
|
if (script->HasValidSource()) results->set(length++, script);
|
}
|
}
|
return FixedArray::ShrinkOrEmpty(isolate_, results, length);
|
}
|
|
void Debug::OnThrow(Handle<Object> exception) {
|
if (in_debug_scope() || ignore_events()) return;
|
// Temporarily clear any scheduled_exception to allow evaluating
|
// JavaScript from the debug event handler.
|
HandleScope scope(isolate_);
|
Handle<Object> scheduled_exception;
|
if (isolate_->has_scheduled_exception()) {
|
scheduled_exception = handle(isolate_->scheduled_exception(), isolate_);
|
isolate_->clear_scheduled_exception();
|
}
|
OnException(exception, isolate_->GetPromiseOnStackOnThrow());
|
if (!scheduled_exception.is_null()) {
|
isolate_->thread_local_top()->scheduled_exception_ = *scheduled_exception;
|
}
|
PrepareStepOnThrow();
|
}
|
|
void Debug::OnPromiseReject(Handle<Object> promise, Handle<Object> value) {
|
if (in_debug_scope() || ignore_events()) return;
|
HandleScope scope(isolate_);
|
// Check whether the promise has been marked as having triggered a message.
|
Handle<Symbol> key = isolate_->factory()->promise_debug_marker_symbol();
|
if (!promise->IsJSObject() ||
|
JSReceiver::GetDataProperty(Handle<JSObject>::cast(promise), key)
|
->IsUndefined(isolate_)) {
|
OnException(value, promise);
|
}
|
}
|
|
bool Debug::IsExceptionBlackboxed(bool uncaught) {
|
// Uncaught exception is blackboxed if all current frames are blackboxed,
|
// caught exception if top frame is blackboxed.
|
StackTraceFrameIterator it(isolate_);
|
while (!it.done() && it.is_wasm()) it.Advance();
|
bool is_top_frame_blackboxed =
|
!it.done() ? IsFrameBlackboxed(it.javascript_frame()) : true;
|
if (!uncaught || !is_top_frame_blackboxed) return is_top_frame_blackboxed;
|
return AllFramesOnStackAreBlackboxed();
|
}
|
|
bool Debug::IsFrameBlackboxed(JavaScriptFrame* frame) {
|
HandleScope scope(isolate_);
|
std::vector<Handle<SharedFunctionInfo>> infos;
|
frame->GetFunctions(&infos);
|
for (const auto& info : infos) {
|
if (!IsBlackboxed(info)) return false;
|
}
|
return true;
|
}
|
|
void Debug::OnException(Handle<Object> exception, Handle<Object> promise) {
|
// TODO(kozyatinskiy): regress-662674.js test fails on arm without this.
|
if (!AllowJavascriptExecution::IsAllowed(isolate_)) return;
|
|
Isolate::CatchType catch_type = isolate_->PredictExceptionCatcher();
|
|
// Don't notify listener of exceptions that are internal to a desugaring.
|
if (catch_type == Isolate::CAUGHT_BY_DESUGARING) return;
|
|
bool uncaught = catch_type == Isolate::NOT_CAUGHT;
|
if (promise->IsJSObject()) {
|
Handle<JSObject> jspromise = Handle<JSObject>::cast(promise);
|
// Mark the promise as already having triggered a message.
|
Handle<Symbol> key = isolate_->factory()->promise_debug_marker_symbol();
|
JSObject::SetProperty(isolate_, jspromise, key, key, LanguageMode::kStrict)
|
.Assert();
|
// Check whether the promise reject is considered an uncaught exception.
|
uncaught = !isolate_->PromiseHasUserDefinedRejectHandler(jspromise);
|
}
|
|
if (!debug_delegate_) return;
|
|
// Bail out if exception breaks are not active
|
if (uncaught) {
|
// Uncaught exceptions are reported by either flags.
|
if (!(break_on_uncaught_exception_ || break_on_exception_)) return;
|
} else {
|
// Caught exceptions are reported is activated.
|
if (!break_on_exception_) return;
|
}
|
|
{
|
JavaScriptFrameIterator it(isolate_);
|
// Check whether the top frame is blackboxed or the break location is muted.
|
if (!it.done() && (IsMutedAtCurrentLocation(it.frame()) ||
|
IsExceptionBlackboxed(uncaught))) {
|
return;
|
}
|
if (it.done()) return; // Do not trigger an event with an empty stack.
|
}
|
|
DebugScope debug_scope(this);
|
HandleScope scope(isolate_);
|
DisableBreak no_recursive_break(this);
|
|
Handle<Context> native_context(isolate_->native_context());
|
debug_delegate_->ExceptionThrown(v8::Utils::ToLocal(native_context),
|
v8::Utils::ToLocal(exception),
|
v8::Utils::ToLocal(promise), uncaught);
|
}
|
|
void Debug::OnDebugBreak(Handle<FixedArray> break_points_hit) {
|
DCHECK(!break_points_hit.is_null());
|
// The caller provided for DebugScope.
|
AssertDebugContext();
|
// Bail out if there is no listener for this event
|
if (ignore_events()) return;
|
|
#ifdef DEBUG
|
PrintBreakLocation();
|
#endif // DEBUG
|
|
if (!debug_delegate_) return;
|
HandleScope scope(isolate_);
|
PostponeInterruptsScope no_interrupts(isolate_);
|
DisableBreak no_recursive_break(this);
|
|
std::vector<int> inspector_break_points_hit;
|
int inspector_break_points_count = 0;
|
// This array contains breakpoints installed using JS debug API.
|
for (int i = 0; i < break_points_hit->length(); ++i) {
|
BreakPoint* break_point = BreakPoint::cast(break_points_hit->get(i));
|
inspector_break_points_hit.push_back(break_point->id());
|
++inspector_break_points_count;
|
}
|
|
Handle<Context> native_context(isolate_->native_context());
|
debug_delegate_->BreakProgramRequested(v8::Utils::ToLocal(native_context),
|
inspector_break_points_hit);
|
}
|
|
namespace {
|
debug::Location GetDebugLocation(Handle<Script> script, int source_position) {
|
Script::PositionInfo info;
|
Script::GetPositionInfo(script, source_position, &info, Script::WITH_OFFSET);
|
// V8 provides ScriptCompiler::CompileFunctionInContext method which takes
|
// expression and compile it as anonymous function like (function() ..
|
// expression ..). To produce correct locations for stmts inside of this
|
// expression V8 compile this function with negative offset. Instead of stmt
|
// position blackboxing use function start position which is negative in
|
// described case.
|
return debug::Location(std::max(info.line, 0), std::max(info.column, 0));
|
}
|
} // namespace
|
|
bool Debug::IsBlackboxed(Handle<SharedFunctionInfo> shared) {
|
if (!debug_delegate_) return !shared->IsSubjectToDebugging();
|
Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared);
|
if (!debug_info->computed_debug_is_blackboxed()) {
|
bool is_blackboxed =
|
!shared->IsSubjectToDebugging() || !shared->script()->IsScript();
|
if (!is_blackboxed) {
|
SuppressDebug while_processing(this);
|
HandleScope handle_scope(isolate_);
|
PostponeInterruptsScope no_interrupts(isolate_);
|
DisableBreak no_recursive_break(this);
|
DCHECK(shared->script()->IsScript());
|
Handle<Script> script(Script::cast(shared->script()), isolate_);
|
DCHECK(script->IsUserJavaScript());
|
debug::Location start = GetDebugLocation(script, shared->StartPosition());
|
debug::Location end = GetDebugLocation(script, shared->EndPosition());
|
is_blackboxed = debug_delegate_->IsFunctionBlackboxed(
|
ToApiHandle<debug::Script>(script), start, end);
|
}
|
debug_info->set_debug_is_blackboxed(is_blackboxed);
|
debug_info->set_computed_debug_is_blackboxed(true);
|
}
|
return debug_info->debug_is_blackboxed();
|
}
|
|
bool Debug::AllFramesOnStackAreBlackboxed() {
|
HandleScope scope(isolate_);
|
for (StackTraceFrameIterator it(isolate_); !it.done(); it.Advance()) {
|
if (!IsFrameBlackboxed(it.javascript_frame())) return false;
|
}
|
return true;
|
}
|
|
bool Debug::CanBreakAtEntry(Handle<SharedFunctionInfo> shared) {
|
// Allow break at entry for builtin functions.
|
if (shared->native() || shared->IsApiFunction()) {
|
// Functions that are subject to debugging can have regular breakpoints.
|
DCHECK(!shared->IsSubjectToDebugging());
|
return true;
|
}
|
return false;
|
}
|
|
bool Debug::SetScriptSource(Handle<Script> script, Handle<String> source,
|
bool preview, debug::LiveEditResult* result) {
|
DebugScope debug_scope(this);
|
running_live_edit_ = true;
|
LiveEdit::PatchScript(isolate_, script, source, preview, result);
|
running_live_edit_ = false;
|
return result->status == debug::LiveEditResult::OK;
|
}
|
|
void Debug::OnCompileError(Handle<Script> script) {
|
ProcessCompileEvent(true, script);
|
}
|
|
void Debug::OnAfterCompile(Handle<Script> script) {
|
ProcessCompileEvent(false, script);
|
}
|
|
void Debug::ProcessCompileEvent(bool has_compile_error, Handle<Script> script) {
|
// TODO(kozyatinskiy): teach devtools to work with liveedit scripts better
|
// first and then remove this fast return.
|
if (running_live_edit_) return;
|
// Attach the correct debug id to the script. The debug id is used by the
|
// inspector to filter scripts by native context.
|
script->set_context_data(isolate_->native_context()->debug_context_id());
|
if (ignore_events()) return;
|
if (!script->IsUserJavaScript() && script->type() != i::Script::TYPE_WASM) {
|
return;
|
}
|
if (!debug_delegate_) return;
|
SuppressDebug while_processing(this);
|
DebugScope debug_scope(this);
|
HandleScope scope(isolate_);
|
DisableBreak no_recursive_break(this);
|
AllowJavascriptExecution allow_script(isolate_);
|
debug_delegate_->ScriptCompiled(ToApiHandle<debug::Script>(script),
|
running_live_edit_, has_compile_error);
|
}
|
|
int Debug::CurrentFrameCount() {
|
StackTraceFrameIterator it(isolate_);
|
if (break_frame_id() != StackFrame::NO_ID) {
|
// Skip to break frame.
|
DCHECK(in_debug_scope());
|
while (!it.done() && it.frame()->id() != break_frame_id()) it.Advance();
|
}
|
int counter = 0;
|
while (!it.done()) {
|
if (it.frame()->is_optimized()) {
|
std::vector<SharedFunctionInfo*> infos;
|
OptimizedFrame::cast(it.frame())->GetFunctions(&infos);
|
counter += infos.size();
|
} else {
|
counter++;
|
}
|
it.Advance();
|
}
|
return counter;
|
}
|
|
void Debug::SetDebugDelegate(debug::DebugDelegate* delegate) {
|
debug_delegate_ = delegate;
|
UpdateState();
|
}
|
|
void Debug::UpdateState() {
|
bool is_active = debug_delegate_ != nullptr;
|
if (is_active == is_active_) return;
|
if (is_active) {
|
// Note that the debug context could have already been loaded to
|
// bootstrap test cases.
|
isolate_->compilation_cache()->Disable();
|
is_active = true;
|
feature_tracker()->Track(DebugFeatureTracker::kActive);
|
} else {
|
isolate_->compilation_cache()->Enable();
|
Unload();
|
}
|
is_active_ = is_active;
|
if (is_active && isolate_->IsPromiseHookProtectorIntact()) {
|
isolate_->InvalidatePromiseHookProtector();
|
}
|
}
|
|
void Debug::UpdateHookOnFunctionCall() {
|
STATIC_ASSERT(LastStepAction == StepIn);
|
hook_on_function_call_ =
|
thread_local_.last_step_action_ == StepIn ||
|
isolate_->debug_execution_mode() == DebugInfo::kSideEffects ||
|
thread_local_.break_on_next_function_call_;
|
}
|
|
void Debug::HandleDebugBreak(IgnoreBreakMode ignore_break_mode) {
|
// Initialize LiveEdit.
|
LiveEdit::InitializeThreadLocal(this);
|
// Ignore debug break during bootstrapping.
|
if (isolate_->bootstrapper()->IsActive()) return;
|
// Just continue if breaks are disabled.
|
if (break_disabled()) return;
|
// Ignore debug break if debugger is not active.
|
if (!is_active()) return;
|
|
StackLimitCheck check(isolate_);
|
if (check.HasOverflowed()) return;
|
|
{ JavaScriptFrameIterator it(isolate_);
|
DCHECK(!it.done());
|
Object* fun = it.frame()->function();
|
if (fun && fun->IsJSFunction()) {
|
HandleScope scope(isolate_);
|
Handle<JSFunction> function(JSFunction::cast(fun), isolate_);
|
// Don't stop in builtin and blackboxed functions.
|
Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
|
bool ignore_break = ignore_break_mode == kIgnoreIfTopFrameBlackboxed
|
? IsBlackboxed(shared)
|
: AllFramesOnStackAreBlackboxed();
|
if (ignore_break) return;
|
// Don't stop if the break location is muted.
|
if (IsMutedAtCurrentLocation(it.frame())) return;
|
}
|
}
|
|
// Clear stepping to avoid duplicate breaks.
|
ClearStepping();
|
|
HandleScope scope(isolate_);
|
DebugScope debug_scope(this);
|
|
OnDebugBreak(isolate_->factory()->empty_fixed_array());
|
}
|
|
#ifdef DEBUG
|
void Debug::PrintBreakLocation() {
|
if (!FLAG_print_break_location) return;
|
HandleScope scope(isolate_);
|
StackTraceFrameIterator iterator(isolate_);
|
if (iterator.done()) return;
|
StandardFrame* frame = iterator.frame();
|
FrameSummary summary = FrameSummary::GetTop(frame);
|
int source_position = summary.SourcePosition();
|
Handle<Object> script_obj = summary.script();
|
PrintF("[debug] break in function '");
|
summary.FunctionName()->PrintOn(stdout);
|
PrintF("'.\n");
|
if (script_obj->IsScript()) {
|
Handle<Script> script = Handle<Script>::cast(script_obj);
|
Handle<String> source(String::cast(script->source()), isolate_);
|
Script::InitLineEnds(script);
|
int line =
|
Script::GetLineNumber(script, source_position) - script->line_offset();
|
int column = Script::GetColumnNumber(script, source_position) -
|
(line == 0 ? script->column_offset() : 0);
|
Handle<FixedArray> line_ends(FixedArray::cast(script->line_ends()),
|
isolate_);
|
int line_start = line == 0 ? 0 : Smi::ToInt(line_ends->get(line - 1)) + 1;
|
int line_end = Smi::ToInt(line_ends->get(line));
|
DisallowHeapAllocation no_gc;
|
String::FlatContent content = source->GetFlatContent();
|
if (content.IsOneByte()) {
|
PrintF("[debug] %.*s\n", line_end - line_start,
|
content.ToOneByteVector().start() + line_start);
|
PrintF("[debug] ");
|
for (int i = 0; i < column; i++) PrintF(" ");
|
PrintF("^\n");
|
} else {
|
PrintF("[debug] at line %d column %d\n", line, column);
|
}
|
}
|
}
|
#endif // DEBUG
|
|
DebugScope::DebugScope(Debug* debug)
|
: debug_(debug),
|
prev_(reinterpret_cast<DebugScope*>(
|
base::Relaxed_Load(&debug->thread_local_.current_debug_scope_))),
|
no_interrupts_(debug_->isolate_) {
|
// Link recursive debugger entry.
|
base::Relaxed_Store(&debug_->thread_local_.current_debug_scope_,
|
reinterpret_cast<base::AtomicWord>(this));
|
|
// Store the previous frame id and return value.
|
break_frame_id_ = debug_->break_frame_id();
|
|
// Create the new break info. If there is no proper frames there is no break
|
// frame id.
|
StackTraceFrameIterator it(isolate());
|
bool has_frames = !it.done();
|
debug_->thread_local_.break_frame_id_ =
|
has_frames ? it.frame()->id() : StackFrame::NO_ID;
|
|
debug_->UpdateState();
|
}
|
|
|
DebugScope::~DebugScope() {
|
// Leaving this debugger entry.
|
base::Relaxed_Store(&debug_->thread_local_.current_debug_scope_,
|
reinterpret_cast<base::AtomicWord>(prev_));
|
|
// Restore to the previous break state.
|
debug_->thread_local_.break_frame_id_ = break_frame_id_;
|
|
debug_->UpdateState();
|
}
|
|
ReturnValueScope::ReturnValueScope(Debug* debug) : debug_(debug) {
|
return_value_ = debug_->return_value_handle();
|
}
|
|
ReturnValueScope::~ReturnValueScope() {
|
debug_->set_return_value(*return_value_);
|
}
|
|
void Debug::UpdateDebugInfosForExecutionMode() {
|
// Walk all debug infos and update their execution mode if it is different
|
// from the isolate execution mode.
|
DebugInfoListNode* current = debug_info_list_;
|
while (current != nullptr) {
|
Handle<DebugInfo> debug_info = current->debug_info();
|
if (debug_info->HasInstrumentedBytecodeArray() &&
|
debug_info->DebugExecutionMode() != isolate_->debug_execution_mode()) {
|
DCHECK(debug_info->shared()->HasBytecodeArray());
|
if (isolate_->debug_execution_mode() == DebugInfo::kBreakpoints) {
|
ClearSideEffectChecks(debug_info);
|
ApplyBreakPoints(debug_info);
|
} else {
|
ClearBreakPoints(debug_info);
|
ApplySideEffectChecks(debug_info);
|
}
|
}
|
current = current->next();
|
}
|
}
|
|
void Debug::StartSideEffectCheckMode() {
|
DCHECK(isolate_->debug_execution_mode() != DebugInfo::kSideEffects);
|
isolate_->set_debug_execution_mode(DebugInfo::kSideEffects);
|
UpdateHookOnFunctionCall();
|
side_effect_check_failed_ = false;
|
|
DCHECK(!temporary_objects_);
|
temporary_objects_.reset(new TemporaryObjectsTracker());
|
isolate_->heap()->AddHeapObjectAllocationTracker(temporary_objects_.get());
|
Handle<FixedArray> array(isolate_->native_context()->regexp_last_match_info(),
|
isolate_);
|
regexp_match_info_ =
|
Handle<RegExpMatchInfo>::cast(isolate_->factory()->CopyFixedArray(array));
|
|
// Update debug infos to have correct execution mode.
|
UpdateDebugInfosForExecutionMode();
|
}
|
|
void Debug::StopSideEffectCheckMode() {
|
DCHECK(isolate_->debug_execution_mode() == DebugInfo::kSideEffects);
|
if (side_effect_check_failed_) {
|
DCHECK(isolate_->has_pending_exception());
|
DCHECK_EQ(ReadOnlyRoots(isolate_).termination_exception(),
|
isolate_->pending_exception());
|
// Convert the termination exception into a regular exception.
|
isolate_->CancelTerminateExecution();
|
isolate_->Throw(*isolate_->factory()->NewEvalError(
|
MessageTemplate::kNoSideEffectDebugEvaluate));
|
}
|
isolate_->set_debug_execution_mode(DebugInfo::kBreakpoints);
|
UpdateHookOnFunctionCall();
|
side_effect_check_failed_ = false;
|
|
DCHECK(temporary_objects_);
|
isolate_->heap()->RemoveHeapObjectAllocationTracker(temporary_objects_.get());
|
temporary_objects_.reset();
|
isolate_->native_context()->set_regexp_last_match_info(*regexp_match_info_);
|
regexp_match_info_ = Handle<RegExpMatchInfo>::null();
|
|
// Update debug infos to have correct execution mode.
|
UpdateDebugInfosForExecutionMode();
|
}
|
|
void Debug::ApplySideEffectChecks(Handle<DebugInfo> debug_info) {
|
DCHECK(debug_info->HasInstrumentedBytecodeArray());
|
Handle<BytecodeArray> debug_bytecode(debug_info->DebugBytecodeArray(),
|
isolate_);
|
DebugEvaluate::ApplySideEffectChecks(debug_bytecode);
|
debug_info->SetDebugExecutionMode(DebugInfo::kSideEffects);
|
}
|
|
void Debug::ClearSideEffectChecks(Handle<DebugInfo> debug_info) {
|
DCHECK(debug_info->HasInstrumentedBytecodeArray());
|
Handle<BytecodeArray> debug_bytecode(debug_info->DebugBytecodeArray(),
|
isolate_);
|
Handle<BytecodeArray> original(debug_info->OriginalBytecodeArray(), isolate_);
|
for (interpreter::BytecodeArrayIterator it(debug_bytecode); !it.done();
|
it.Advance()) {
|
// Restore from original. This may copy only the scaling prefix, which is
|
// correct, since we patch scaling prefixes to debug breaks if exists.
|
debug_bytecode->set(it.current_offset(),
|
original->get(it.current_offset()));
|
}
|
}
|
|
bool Debug::PerformSideEffectCheck(Handle<JSFunction> function,
|
Handle<Object> receiver) {
|
DCHECK_EQ(isolate_->debug_execution_mode(), DebugInfo::kSideEffects);
|
DisallowJavascriptExecution no_js(isolate_);
|
if (!function->is_compiled() &&
|
!Compiler::Compile(function, Compiler::KEEP_EXCEPTION)) {
|
return false;
|
}
|
Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
|
Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared);
|
DebugInfo::SideEffectState side_effect_state =
|
debug_info->GetSideEffectState(isolate_);
|
switch (side_effect_state) {
|
case DebugInfo::kHasSideEffects:
|
if (FLAG_trace_side_effect_free_debug_evaluate) {
|
PrintF("[debug-evaluate] Function %s failed side effect check.\n",
|
function->shared()->DebugName()->ToCString().get());
|
}
|
side_effect_check_failed_ = true;
|
// Throw an uncatchable termination exception.
|
isolate_->TerminateExecution();
|
return false;
|
case DebugInfo::kRequiresRuntimeChecks: {
|
if (!shared->HasBytecodeArray()) {
|
return PerformSideEffectCheckForObject(receiver);
|
}
|
// If function has bytecode array then prepare function for debug
|
// execution to perform runtime side effect checks.
|
DCHECK(shared->is_compiled());
|
if (shared->GetCode() ==
|
isolate_->builtins()->builtin(Builtins::kDeserializeLazy)) {
|
Snapshot::EnsureBuiltinIsDeserialized(isolate_, shared);
|
}
|
PrepareFunctionForDebugExecution(shared);
|
ApplySideEffectChecks(debug_info);
|
return true;
|
}
|
case DebugInfo::kHasNoSideEffect:
|
return true;
|
case DebugInfo::kNotComputed:
|
UNREACHABLE();
|
return false;
|
}
|
UNREACHABLE();
|
return false;
|
}
|
|
Handle<Object> Debug::return_value_handle() {
|
return handle(thread_local_.return_value_, isolate_);
|
}
|
|
bool Debug::PerformSideEffectCheckForCallback(Handle<Object> callback_info) {
|
DCHECK_EQ(isolate_->debug_execution_mode(), DebugInfo::kSideEffects);
|
if (!callback_info.is_null() && callback_info->IsCallHandlerInfo() &&
|
i::CallHandlerInfo::cast(*callback_info)->NextCallHasNoSideEffect()) {
|
return true;
|
}
|
// TODO(7515): always pass a valid callback info object.
|
if (!callback_info.is_null() &&
|
DebugEvaluate::CallbackHasNoSideEffect(*callback_info)) {
|
return true;
|
}
|
side_effect_check_failed_ = true;
|
// Throw an uncatchable termination exception.
|
isolate_->TerminateExecution();
|
isolate_->OptionalRescheduleException(false);
|
return false;
|
}
|
|
bool Debug::PerformSideEffectCheckAtBytecode(InterpretedFrame* frame) {
|
using interpreter::Bytecode;
|
|
DCHECK_EQ(isolate_->debug_execution_mode(), DebugInfo::kSideEffects);
|
SharedFunctionInfo* shared = frame->function()->shared();
|
BytecodeArray* bytecode_array = shared->GetBytecodeArray();
|
int offset = frame->GetBytecodeOffset();
|
interpreter::BytecodeArrayAccessor bytecode_accessor(
|
handle(bytecode_array, isolate_), offset);
|
|
Bytecode bytecode = bytecode_accessor.current_bytecode();
|
interpreter::Register reg;
|
switch (bytecode) {
|
case Bytecode::kStaCurrentContextSlot:
|
reg = interpreter::Register::current_context();
|
break;
|
default:
|
reg = bytecode_accessor.GetRegisterOperand(0);
|
break;
|
}
|
Handle<Object> object =
|
handle(frame->ReadInterpreterRegister(reg.index()), isolate_);
|
return PerformSideEffectCheckForObject(object);
|
}
|
|
bool Debug::PerformSideEffectCheckForObject(Handle<Object> object) {
|
DCHECK_EQ(isolate_->debug_execution_mode(), DebugInfo::kSideEffects);
|
|
if (object->IsHeapObject()) {
|
if (temporary_objects_->HasObject(Handle<HeapObject>::cast(object))) {
|
return true;
|
}
|
}
|
if (FLAG_trace_side_effect_free_debug_evaluate) {
|
PrintF("[debug-evaluate] failed runtime side effect check.\n");
|
}
|
side_effect_check_failed_ = true;
|
// Throw an uncatchable termination exception.
|
isolate_->TerminateExecution();
|
return false;
|
}
|
} // namespace internal
|
} // namespace v8
|