Files
sdk/runtime/vm/debugger.cc
T
Tess Strickland 7558e63725 [vm,dyn_modules] Change handling of source positions for async returns.
The debugger assumes a null suspend state variable in an asynchronous
function means that the function is still in the prologue prior
to setting up the suspend state. However, the interpreter clears the
suspend state variable before returning, and the debugger needs to be
able to pause before returning when single stepping, so earlier a hack
was added to the debugger that detects being at the direct call of the
async return method and/or the return instruction with a null suspend
state variable.

However, there's a much simpler way of ensuring the debugger pauses
before returning: just emit the source position for the return prior to
clearing the suspend state variable. This also ensures that the debugger
still has access to the function's suspend state when pausing before
the return, instead of waiting until it has been cleared and thus is no
longer accessible.

TEST=ci (should not change the result of any current tests)

Cq-Include-Trybots: luci.dart.try:vm-dyn-linux-debug-x64-try
Change-Id: I401cceb169d8692ac379cdc5a531e07cafbe9a65
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/500740
Commit-Queue: Tess Strickland <sstrickl@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
2026-06-02 04:28:41 -07:00

4750 lines
170 KiB
C++

// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#include "vm/debugger.h"
#include "include/dart_api.h"
#include "vm/bytecode_reader.h"
#include "vm/closure_functions_cache.h"
#include "vm/code_descriptors.h"
#include "vm/code_patcher.h"
#include "vm/compiler/api/deopt_id.h"
#include "vm/compiler/assembler/disassembler.h"
#include "vm/compiler/assembler/disassembler_kbc.h"
#include "vm/compiler/jit/compiler.h"
#include "vm/dart_entry.h"
#include "vm/flags.h"
#include "vm/globals.h"
#include "vm/isolate_reload.h"
#include "vm/json_stream.h"
#include "vm/kernel.h"
#include "vm/longjump.h"
#include "vm/message_handler.h"
#include "vm/object.h"
#include "vm/object_store.h"
#include "vm/os.h"
#include "vm/parser.h"
#include "vm/port.h"
#include "vm/runtime_entry.h"
#include "vm/service.h"
#include "vm/service_event.h"
#include "vm/service_isolate.h"
#include "vm/stack_frame.h"
#include "vm/stack_trace.h"
#include "vm/stub_code.h"
#include "vm/symbols.h"
#include "vm/thread_interrupter.h"
#include "vm/timeline.h"
#include "vm/token_position.h"
#include "vm/visitor.h"
#include "vm/zone_text_buffer.h"
#if !defined(DART_PRECOMPILED_RUNTIME)
#include "vm/deopt_instructions.h"
#endif // !defined(DART_PRECOMPILED_RUNTIME)
namespace dart {
DEFINE_FLAG(bool,
trace_debugger_stacktrace,
false,
"Trace debugger stacktrace collection");
DEFINE_FLAG(bool,
trace_debugger_stacktrace_verbose,
false,
"Additional output when tracing debugger stacktraces");
DEFINE_FLAG(bool, trace_rewind, false, "Trace frame rewind");
DEFINE_FLAG(bool, verbose_debug, false, "Verbose debugger messages");
DECLARE_FLAG(bool, trace_deoptimization);
DECLARE_FLAG(bool, warn_on_pause_with_no_debugger);
#ifndef PRODUCT
// Create an unresolved breakpoint in given token range and script.
BreakpointLocation::BreakpointLocation(
Debugger* debugger,
const GrowableHandlePtrArray<const Script>& scripts,
TokenPosition token_pos,
TokenPosition end_token_pos,
intptr_t requested_line_number,
intptr_t requested_column_number)
: debugger_(debugger),
scripts_(MallocGrowableArray<ScriptPtr>(scripts.length())),
url_(scripts.At(0).url()),
line_number_lock_(new SafepointRwLock()),
line_number_(-1), // lazily computed
token_pos_(token_pos),
end_token_pos_(end_token_pos),
next_(nullptr),
conditions_(nullptr),
requested_line_number_(requested_line_number),
requested_column_number_(requested_column_number),
code_token_pos_(TokenPosition::kNoSource) {
ASSERT(scripts.length() > 0);
ASSERT(token_pos.IsReal());
for (intptr_t i = 0; i < scripts.length(); ++i) {
scripts_.Add(scripts.At(i).ptr());
}
}
// Create a latent breakpoint at given url and line number.
BreakpointLocation::BreakpointLocation(Debugger* debugger,
const String& url,
intptr_t requested_line_number,
intptr_t requested_column_number)
: debugger_(debugger),
scripts_(MallocGrowableArray<ScriptPtr>(0)),
url_(url.ptr()),
line_number_lock_(new SafepointRwLock()),
line_number_(-1), // lazily computed
token_pos_(TokenPosition::kNoSource),
end_token_pos_(TokenPosition::kNoSource),
next_(nullptr),
conditions_(nullptr),
requested_line_number_(requested_line_number),
requested_column_number_(requested_column_number),
code_token_pos_(TokenPosition::kNoSource) {
ASSERT(requested_line_number_ >= 0);
}
BreakpointLocation::~BreakpointLocation() {
Breakpoint* bpt = breakpoints();
while (bpt != nullptr) {
Breakpoint* temp = bpt;
bpt = bpt->next();
delete temp;
}
}
bool BreakpointLocation::AnyEnabled() const {
Breakpoint* bpt = breakpoints();
while (bpt != nullptr) {
if (bpt->is_enabled()) {
return true;
}
bpt = bpt->next();
}
return false;
}
void BreakpointLocation::SetResolved(const Function& func,
TokenPosition token_pos) {
#if defined(DEBUG)
const Script& func_script = Script::Handle(func.script());
const String& func_url = String::Handle(func_script.url());
const String& script_url = String::Handle(url_);
ASSERT(script_url.Equals(func_url));
#endif // defined(DEBUG)
ASSERT(!IsLatent());
ASSERT(token_pos.IsWithin(func.token_pos(), func.end_token_pos()));
ASSERT(func.is_debuggable());
token_pos_.store(token_pos);
end_token_pos_.store(token_pos);
code_token_pos_ = token_pos;
}
void BreakpointLocation::GetCodeLocation(Script* script,
TokenPosition* pos) const {
if (IsLatent()) {
*script = Script::null();
*pos = TokenPosition::kNoSource;
} else {
*script = this->script();
*pos = token_pos_;
}
}
intptr_t BreakpointLocation::line_number() {
// Compute line number lazily since it causes scanning of the script.
{
SafepointReadRwLocker sl(Thread::Current(), line_number_lock());
if (line_number_ >= 0) {
return line_number_;
}
}
SafepointWriteRwLocker sl(Thread::Current(), line_number_lock());
if (line_number_ < 0) {
Script::Handle(script()).GetTokenLocation(token_pos(), &line_number_);
}
return line_number_;
}
void Breakpoint::set_bpt_location(BreakpointLocation* new_bpt_location) {
// Only latent breakpoints can be moved.
ASSERT((new_bpt_location == nullptr) || bpt_location_->IsLatent());
bpt_location_ = new_bpt_location;
}
void Breakpoint::VisitObjectPointers(ObjectPointerVisitor* visitor) {
visitor->VisitPointer(reinterpret_cast<ObjectPtr*>(&closure_));
}
void BreakpointLocation::VisitObjectPointers(ObjectPointerVisitor* visitor) {
for (intptr_t i = 0; i < scripts_.length(); ++i) {
visitor->VisitPointer(reinterpret_cast<ObjectPtr*>(&scripts_.data()[i]));
}
visitor->VisitPointer(reinterpret_cast<ObjectPtr*>(&url_));
Breakpoint* bpt = conditions_;
while (bpt != nullptr) {
bpt->VisitObjectPointers(visitor);
bpt = bpt->next();
}
}
void Breakpoint::PrintJSON(JSONStream* stream) {
JSONObject jsobj(stream);
jsobj.AddProperty("type", "Breakpoint");
jsobj.AddFixedServiceId("breakpoints/%" Pd "", id());
jsobj.AddProperty("enabled", enabled_);
jsobj.AddProperty("breakpointNumber", id());
jsobj.AddProperty("resolved", bpt_location_->IsResolved());
if (bpt_location_->IsResolved()) {
jsobj.AddLocation(bpt_location_);
} else {
jsobj.AddUnresolvedLocation(bpt_location_);
}
}
void CodeBreakpoint::VisitObjectPointers(ObjectPointerVisitor* visitor) {
visitor->VisitPointer(reinterpret_cast<ObjectPtr*>(&code_));
visitor->VisitPointer(reinterpret_cast<ObjectPtr*>(&saved_value_));
}
const char* CodeBreakpoint::ToCString() const {
if (breakpoint_locations_.length() == 0) {
return "unlinked breakpoint";
}
char buffer[1024];
BufferFormatter f(buffer, sizeof(buffer));
// Pick the first, all other should have same script/line number.
BreakpointLocation* breakpoint_location = breakpoint_locations_.At(0);
String& source_url = String::Handle(breakpoint_location->url());
intptr_t line_number = breakpoint_location->line_number();
f.Printf("breakpoint at %s:%" Pd, source_url.ToCString(), line_number);
return Thread::Current()->zone()->MakeCopyOfString(buffer);
}
ActivationFrame::ActivationFrame(uword pc,
uword fp,
uword sp,
const Function& function,
const Object& code_or_bytecode,
const Array& deopt_frame,
intptr_t deopt_frame_offset)
: pc_(pc),
fp_(fp),
sp_(sp),
code_or_bytecode_(Object::ZoneHandle(code_or_bytecode.ptr())),
function_(Function::ZoneHandle(function.ptr())),
closure_(Closure::null_closure()),
deopt_frame_(Array::ZoneHandle(deopt_frame.ptr())),
deopt_frame_offset_(deopt_frame_offset),
kind_(kRegular),
desc_indices_(8),
pc_desc_(PcDescriptors::ZoneHandle()) {
ASSERT(!function.IsNull());
ASSERT(code_or_bytecode.IsCode() || code_or_bytecode.IsBytecode());
}
ActivationFrame::ActivationFrame(uword pc,
const Function& function,
const Object& code_or_bytecode,
const Closure& closure)
: pc_(pc),
code_or_bytecode_(Object::ZoneHandle(code_or_bytecode.ptr())),
function_(Function::ZoneHandle(function.ptr())),
closure_(Closure::ZoneHandle(closure.ptr())),
deopt_frame_(Array::empty_array()),
deopt_frame_offset_(0),
kind_(kAsyncAwaiter) {
ASSERT(code_or_bytecode.IsCode() || code_or_bytecode.IsBytecode());
}
ActivationFrame::ActivationFrame(Kind kind)
: code_or_bytecode_(Object::null_object()),
function_(Function::null_function()),
closure_(Closure::null_closure()),
deopt_frame_(Array::empty_array()),
deopt_frame_offset_(0),
kind_(kind) {
ASSERT(kind == kAsyncSuspensionMarker);
}
bool Debugger::NeedsIsolateEvents() {
ASSERT(isolate_ == Isolate::Current());
return !Isolate::IsSystemIsolate(isolate_) &&
Service::isolate_stream.enabled();
}
bool Debugger::NeedsDebugEvents() {
if (Isolate::Current() == nullptr) {
// E.g., NoActiveIsolateScope.
return false;
}
RELEASE_ASSERT(isolate_ == Isolate::Current());
ASSERT(!Isolate::IsSystemIsolate(isolate_));
return FLAG_warn_on_pause_with_no_debugger || Service::debug_stream.enabled();
}
static void InvokeEventHandler(ServiceEvent* event) {
ASSERT(!event->IsPause()); // For pause events, call Pause instead.
Service::HandleEvent(event, /*enter_safepoint*/ false);
}
ErrorPtr Debugger::PauseInterrupted() {
return PauseRequest(ServiceEvent::kPauseInterrupted);
}
ErrorPtr Debugger::PausePostRequest() {
return PauseRequest(ServiceEvent::kPausePostRequest);
}
ErrorPtr Debugger::PauseRequest(ServiceEvent::EventKind kind) {
if (ignore_breakpoints_ || IsPaused()) {
// We don't let the isolate get interrupted if we are already
// paused or ignoring breakpoints.
return Thread::Current()->StealStickyError();
}
ServiceEvent event(isolate_, kind);
DebuggerStackTrace* trace = DebuggerStackTrace::Collect();
if (trace->Length() > 0) {
event.set_top_frame(trace->FrameAt(0));
}
CacheStackTraces(trace, DebuggerStackTrace::CollectAsyncAwaiters());
set_resume_action(kContinue);
Pause(&event);
HandleSteppingRequest();
ClearCachedStackTraces();
// If any error occurred while in the debug message loop, return it here.
NoSafepointScope no_safepoint;
ErrorPtr error = Thread::Current()->StealStickyError();
ASSERT((error == Error::null()) || error->IsUnwindError());
return error;
}
void Debugger::SendBreakpointEvent(ServiceEvent::EventKind kind,
Breakpoint* bpt) {
if (NeedsDebugEvents()) {
// TODO(turnidge): Currently we send single-shot breakpoint events
// to the vm service. Do we want to change this?
ServiceEvent event(isolate_, kind);
event.set_breakpoint(bpt);
InvokeEventHandler(&event);
}
}
void BreakpointLocation::AddBreakpoint(Breakpoint* bpt, Debugger* dbg) {
bpt->set_next(breakpoints());
set_breakpoints(bpt);
bpt->Enable();
dbg->group_debugger()->SyncBreakpointLocation(this);
dbg->SendBreakpointEvent(ServiceEvent::kBreakpointAdded, bpt);
}
Breakpoint* BreakpointLocation::AddRepeated(Debugger* dbg) {
return AddBreakpoint(dbg, Closure::Handle(), /*single_shot=*/false);
}
Breakpoint* BreakpointLocation::AddSingleShot(Debugger* dbg) {
return AddBreakpoint(dbg, Closure::Handle(), /*single_shot=*/true);
}
Breakpoint* BreakpointLocation::AddBreakpoint(Debugger* dbg,
const Closure& closure,
bool single_shot) {
Breakpoint* bpt = breakpoints();
while (bpt != nullptr) {
if ((bpt->closure() == closure.ptr()) &&
(bpt->is_single_shot() == single_shot)) {
break;
}
bpt = bpt->next();
}
if (bpt == nullptr) {
bpt = new Breakpoint(dbg->nextId(), this, single_shot, closure);
AddBreakpoint(bpt, dbg);
}
return bpt;
}
static const char* QualifiedFunctionName(const Function& func) {
const String& func_name = String::Handle(func.name());
Class& func_class = Class::Handle(func.Owner());
String& class_name = String::Handle(func_class.Name());
return OS::SCreate(Thread::Current()->zone(), "%s%s%s",
func_class.IsTopLevel() ? "" : class_name.ToCString(),
func_class.IsTopLevel() ? "" : ".", func_name.ToCString());
}
// Returns true if the function |func| overlaps the token range
// [|token_pos|, |end_token_pos|] in |script|.
static bool FunctionOverlaps(const Function& func,
const String& script_url,
TokenPosition token_pos,
TokenPosition end_token_pos) {
const TokenPosition& func_start = func.token_pos();
if (token_pos.IsWithin(func_start, func.end_token_pos()) ||
func_start.IsWithin(token_pos, end_token_pos)) {
// Check script equality last because it allocates handles as a side effect.
Script& func_script = Script::Handle(func.script());
String& url = String::Handle(func_script.url());
return script_url.Equals(url);
}
return false;
}
static bool IsImplicitFunction(const Function& func) {
switch (func.kind()) {
case UntaggedFunction::kImplicitGetter:
case UntaggedFunction::kImplicitSetter:
case UntaggedFunction::kImplicitStaticGetter:
case UntaggedFunction::kFieldInitializer:
case UntaggedFunction::kMethodExtractor:
case UntaggedFunction::kNoSuchMethodDispatcher:
case UntaggedFunction::kInvokeFieldDispatcher:
case UntaggedFunction::kIrregexpFunction:
case UntaggedFunction::kRecordFieldGetter:
return true;
default:
if (func.token_pos() == func.end_token_pos()) {
// |func| could be an implicit constructor for example.
return true;
}
}
return false;
}
bool GroupDebugger::HasCodeBreakpointInFunctionUnsafe(const Function& func) {
DEBUG_ASSERT(code_breakpoints_lock()->IsCurrentThreadReader() ||
Thread::Current()->IsInStoppedMutatorsScope());
CodeBreakpoint* cbpt = code_breakpoints_;
while (cbpt != nullptr) {
if (func.ptr() == cbpt->function()) {
return true;
}
cbpt = cbpt->next_;
}
return false;
}
bool GroupDebugger::HasCodeBreakpointInFunction(const Function& func) {
auto thread = Thread::Current();
// Don't need to worry about the lock if mutators are stopped.
if (thread->IsInStoppedMutatorsScope()) {
return HasCodeBreakpointInFunctionUnsafe(func);
} else {
SafepointReadRwLocker sl(thread, code_breakpoints_lock());
return HasCodeBreakpointInFunctionUnsafe(func);
}
}
bool GroupDebugger::HasBreakpointInCode(const Code& code) {
auto thread = Thread::Current();
SafepointReadRwLocker sl(thread, code_breakpoints_lock());
CodeBreakpoint* cbpt = code_breakpoints_;
while (cbpt != nullptr) {
if (code.ptr() == cbpt->code_) {
return true;
}
cbpt = cbpt->next_;
}
return false;
}
void Debugger::PrintBreakpointsToJSONArray(JSONArray* jsarr) const {
PrintBreakpointsListToJSONArray(breakpoint_locations_, jsarr);
PrintBreakpointsListToJSONArray(latent_locations_, jsarr);
}
void Debugger::PrintBreakpointsListToJSONArray(BreakpointLocation* sbpt,
JSONArray* jsarr) const {
while (sbpt != nullptr) {
Breakpoint* bpt = sbpt->breakpoints();
while (bpt != nullptr) {
jsarr->AddValue(bpt);
bpt = bpt->next();
}
sbpt = sbpt->next_;
}
}
void Debugger::PrintSettingsToJSONObject(JSONObject* jsobj) const {
// This won't cut it when we support filtering by class, etc.
switch (GetExceptionPauseInfo()) {
case kNoPauseOnExceptions:
jsobj->AddProperty("_exceptions", "none");
break;
case kPauseOnAllExceptions:
jsobj->AddProperty("_exceptions", "all");
break;
case kPauseOnUnhandledExceptions:
jsobj->AddProperty("_exceptions", "unhandled");
break;
default:
UNREACHABLE();
}
}
ActivationFrame::Relation ActivationFrame::CompareTo(bool is_interpreted,
uword fp,
uword other_fp) {
if (fp == other_fp) {
return kSelf;
}
#if defined(DART_DYNAMIC_MODULES)
if (is_interpreted) {
// Unlike compiled code, interpreted stacks grow towards higher addresses.
return fp > other_fp ? kCallee : kCaller;
}
#endif
return IsCalleeFrameOf(other_fp, fp) ? kCallee : kCaller;
}
StringPtr ActivationFrame::QualifiedFunctionName() {
return String::New(::dart::QualifiedFunctionName(function()));
}
StringPtr ActivationFrame::SourceUrl() {
const Script& script = Script::Handle(SourceScript());
return script.url();
}
ScriptPtr ActivationFrame::SourceScript() {
return function().script();
}
LibraryPtr ActivationFrame::Library() {
const Class& cls = Class::Handle(function().Owner());
return cls.library();
}
void ActivationFrame::GetPcDescriptors() {
ASSERT(!IsInterpreted());
if (pc_desc_.IsNull()) {
pc_desc_ = code().pc_descriptors();
ASSERT(!pc_desc_.IsNull());
}
}
// If not token_pos_initialized_, compute token_pos_, try_index_ and,
// if not IsInterpreted(), also compute deopt_id_.
TokenPosition ActivationFrame::TokenPos() {
if (!token_pos_initialized_) {
token_pos_ = TokenPosition::kNoSource;
if (IsInterpreted()) {
token_pos_ = bytecode().GetTokenIndexOfPC(pc_);
try_index_ = bytecode().GetTryIndexAtPc(pc_);
} else {
GetPcDescriptors();
PcDescriptors::Iterator iter(pc_desc_, UntaggedPcDescriptors::kAnyKind);
const uword pc_offset = pc_ - code().PayloadStart();
while (iter.MoveNext()) {
if (iter.PcOffset() == pc_offset) {
try_index_ = iter.TryIndex();
token_pos_ = iter.TokenPos();
deopt_id_ = iter.DeoptId();
break;
}
}
}
token_pos_initialized_ = true;
}
return token_pos_;
}
intptr_t ActivationFrame::TryIndex() {
if (!token_pos_initialized_) {
TokenPos(); // Side effect: computes token_pos_initialized_, try_index_.
}
return try_index_;
}
intptr_t ActivationFrame::DeoptId() {
ASSERT(!IsInterpreted());
if (!token_pos_initialized_) {
TokenPos(); // Side effect: computes token_pos_initialized_, try_index_.
}
return deopt_id_;
}
intptr_t ActivationFrame::LineNumber() {
// Compute line number lazily since it causes scanning of the script.
const TokenPosition& token_pos = TokenPos().ToRealIfSynthetic();
if ((line_number_ < 0) && token_pos.IsReal()) {
const Script& script = Script::Handle(SourceScript());
script.GetTokenLocation(token_pos, &line_number_, &column_number_);
}
return line_number_;
}
intptr_t ActivationFrame::ColumnNumber() {
// Compute column number lazily since it causes scanning of the script.
const TokenPosition& token_pos = TokenPos().ToRealIfSynthetic();
if ((column_number_ < 0) && token_pos.IsReal()) {
const Script& script = Script::Handle(SourceScript());
script.GetTokenLocation(token_pos, &line_number_, &column_number_);
}
return column_number_;
}
void ActivationFrame::GetVarDescriptors() {
if (var_descriptors_.IsNull()) {
if (IsInterpreted()) {
#if defined(DART_PRECOMPILED_RUNTIME)
UNREACHABLE();
#else
var_descriptors_ = bytecode().GetLocalVarDescriptors();
ASSERT(!var_descriptors_.IsNull());
#endif
return;
}
Code& unoptimized_code = Code::Handle(function().unoptimized_code());
if (unoptimized_code.IsNull()) {
if (function().ForceOptimize()) {
var_descriptors_ = Object::empty_var_descriptors().ptr();
return;
}
Thread* thread = Thread::Current();
Zone* zone = thread->zone();
const Error& error = Error::Handle(
zone, Compiler::EnsureUnoptimizedCode(thread, function()));
if (!error.IsNull()) {
Exceptions::PropagateError(error);
}
unoptimized_code = function().unoptimized_code();
}
ASSERT(!unoptimized_code.IsNull());
var_descriptors_ = unoptimized_code.GetLocalVarDescriptors();
ASSERT(!var_descriptors_.IsNull());
}
}
bool ActivationFrame::IsDebuggable() const {
ASSERT(!function().IsNull());
return Debugger::IsDebuggable(function());
}
void ActivationFrame::PrintContextLevelError(const char* message) {
OS::PrintErr("Cannot locate context level: %s\n", message);
OS::PrintErr("function %s\n", function().ToQualifiedCString());
OS::PrintErr("pc_ %" Px "\n", pc_);
OS::PrintErr("deopt_id_ %" Px "\n", deopt_id_);
OS::PrintErr("context_level_ %" Px "\n", context_level_);
OS::PrintErr("token_pos_ %s\n", token_pos_.ToCString());
if (IsInterpreted() && bytecode().HasLocalVariablesInfo()) {
#if defined(DART_DYNAMIC_MODULES)
Zone* const zone = Thread::Current()->zone();
ZoneTextBuffer buffer(zone);
KernelBytecodeDisassembler::PrintLocalVariablesInfo(
zone, &buffer, bytecode(), PayloadStart());
OS::PrintErr("%s\n", buffer.buffer());
#endif
} else {
DisassembleToStdout formatter;
code().Disassemble(&formatter);
PcDescriptors::Handle(code().pc_descriptors()).Print();
}
StackFrameIterator frames(ValidationPolicy::kDontValidateFrames,
Thread::Current(),
StackFrameIterator::kNoCrossThreadIteration);
StackFrame* frame = frames.NextFrame();
while (frame != nullptr) {
OS::PrintErr("%s\n", frame->ToCString());
frame = frames.NextFrame();
}
OS::Abort();
}
// Calculate the context level at the current pc of the frame.
intptr_t ActivationFrame::ContextLevel() {
ASSERT(kind_ == kRegular);
const Context& ctx = GetSavedCurrentContext();
if (context_level_ < 0 && !ctx.IsNull()) {
ASSERT(IsInterpreted() || !code().is_optimized());
bool found = false;
if (IsInterpreted()) {
#if defined(DART_DYNAMIC_MODULES) && !defined(PRODUCT) && \
!defined(DART_PRECOMPILED_RUNTIME)
const intptr_t pc_offset = pc() - PayloadStart();
DEBUG_ONLY(intptr_t closest_start = 0);
bytecode::BytecodeLocalVariablesIterator local_vars(
Thread::Current()->zone(), bytecode());
while (local_vars.MoveNext()) {
if (local_vars.IsScope()) {
if (local_vars.StartPC() <= pc_offset &&
pc_offset <= local_vars.EndPC()) {
DEBUG_ASSERT(!found || local_vars.StartPC() > closest_start);
found = true;
context_level_ = local_vars.ContextLevel();
DEBUG_ONLY(closest_start = local_vars.StartPC());
} else if (local_vars.StartPC() > pc_offset) {
// The scopes in the local variables info are ordered by starting
// PC offset, so no need to search further.
break;
}
}
}
if (!found) {
PrintContextLevelError(
"No Scope local variable info for the current PC");
}
#else
UNREACHABLE();
#endif
} else {
GetVarDescriptors();
intptr_t var_desc_len = var_descriptors_.Length();
// We store the deopt ids as real token positions.
intptr_t deopt_id = DeoptId();
if (deopt_id == DeoptId::kNone) {
PrintContextLevelError("Missing deopt id");
}
const TokenPosition to_compare = TokenPosition::Deserialize(deopt_id);
for (intptr_t cur_idx = 0; cur_idx < var_desc_len; cur_idx++) {
UntaggedLocalVarDescriptors::VarInfo var_info;
var_descriptors_.GetInfo(cur_idx, &var_info);
const int8_t kind = var_info.kind();
if ((kind == UntaggedLocalVarDescriptors::kContextLevel) &&
to_compare.IsWithin(var_info.begin_pos, var_info.end_pos)) {
context_level_ = var_info.index();
found = true;
break;
}
}
if (!found) {
PrintContextLevelError(
"No ContextLevel var descriptor that contains the deopt id");
}
}
ASSERT(context_level_ >= 0);
}
return context_level_;
}
bool ActivationFrame::HandlesException(const Instance& exc_obj) {
if (kind_ == kAsyncSuspensionMarker) {
return false;
}
intptr_t try_index = TryIndex();
auto& handlers = ExceptionHandlers::Handle();
if (IsInterpreted()) {
handlers = bytecode().exception_handlers();
} else {
handlers = code().exception_handlers();
}
ASSERT(!handlers.IsNull());
if ((try_index < 0) && !handlers.has_async_handler()) {
return false;
}
Array& handled_types = Array::Handle();
AbstractType& type = Type::Handle();
intptr_t num_handlers_checked = 0;
while (try_index != kInvalidTryIndex) {
// Detect circles in the exception handler data.
num_handlers_checked++;
ASSERT(num_handlers_checked <= handlers.num_entries());
// Only consider user written handlers and ignore synthesized try/catch in
// async methods as well as synthetic try/catch hiding inside try/finally.
if (!handlers.IsGenerated(try_index)) {
handled_types = handlers.GetHandledTypes(try_index);
const intptr_t num_types = handled_types.Length();
for (intptr_t k = 0; k < num_types; k++) {
type ^= handled_types.At(k);
ASSERT(!type.IsNull());
// Uninstantiated types are not added to ExceptionHandlers data.
ASSERT(type.IsInstantiated());
if (type.IsDynamicType()) {
return true;
}
if (exc_obj.IsInstanceOf(type, Object::null_type_arguments(),
Object::null_type_arguments())) {
return true;
}
}
}
try_index = handlers.OuterTryIndex(try_index);
}
return false;
}
// Get the saved current context of this activation.
const Context& ActivationFrame::GetSavedCurrentContext(intptr_t* index) {
// If the index of the variable was requested, do the retrieval again.
if (context_initialized_ && index == nullptr) return ctx_;
context_initialized_ = true;
GetVarDescriptors();
intptr_t var_desc_len = var_descriptors_.Length();
Object& obj = Object::Handle();
for (intptr_t i = 0; i < var_desc_len; i++) {
UntaggedLocalVarDescriptors::VarInfo var_info;
var_descriptors_.GetInfo(i, &var_info);
const int8_t kind = var_info.kind();
if (kind == UntaggedLocalVarDescriptors::kSavedCurrentContext) {
if (index != nullptr) {
*index = var_info.index();
}
const auto variable_index = VariableIndex(var_info.index());
obj = GetStackVar(variable_index);
if (function_.IsImplicitInstanceClosureFunction()) {
// The context of an implicit instance closure contains only one slot
// for "this", so the instance is stored as the context and accessed
// directly by generated code to avoid allocating a separate context.
// Here, we create the elided context object for debugging purposes.
ctx_ = Context::New(1);
ctx_.SetAt(0, obj);
} else if (obj.IsClosure()) {
ASSERT(function().IsClosureCallDispatcher());
// Closure.call frames.
ctx_ = Closure::Cast(obj).GetContext();
} else if (obj.IsContext()) {
ctx_ = Context::Cast(obj).ptr();
} else {
ASSERT(obj.IsNull() || obj.ptr() == Object::optimized_out().ptr());
ctx_ = Context::null();
}
return ctx_;
}
}
return ctx_;
}
ActivationFrame* DebuggerStackTrace::GetHandlerFrame(
const Instance& exc_obj) const {
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr("GetHandlerFrame(%s)\n", exc_obj.ToCString());
}
for (intptr_t frame_index = 0; frame_index < Length(); frame_index++) {
ActivationFrame* frame = FrameAt(frame_index);
const bool can_handle = frame->HandlesException(exc_obj);
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr(" #%04" Pd " (%s) %s", frame_index,
can_handle ? "+" : "-", frame->ToCString());
}
if (can_handle) {
return frame;
}
}
return nullptr;
}
void ActivationFrame::GetDescIndices() {
if (vars_initialized_) {
return;
}
GetVarDescriptors();
TokenPosition activation_token_pos = TokenPos();
if (!activation_token_pos.IsDebugPause() || kind_ != kRegular) {
// We don't have a token position for this frame, so can't determine
// which variables are visible.
vars_initialized_ = true;
return;
}
GrowableArray<String*> var_names(8);
intptr_t var_desc_len = var_descriptors_.Length();
for (intptr_t cur_idx = 0; cur_idx < var_desc_len; cur_idx++) {
ASSERT(var_names.length() == desc_indices_.length());
UntaggedLocalVarDescriptors::VarInfo var_info;
var_descriptors_.GetInfo(cur_idx, &var_info);
const int8_t kind = var_info.kind();
if ((kind != UntaggedLocalVarDescriptors::kStackVar) &&
(kind != UntaggedLocalVarDescriptors::kContextVar)) {
continue;
}
if (!activation_token_pos.IsWithin(var_info.begin_pos, var_info.end_pos)) {
continue;
}
if ((kind == UntaggedLocalVarDescriptors::kContextVar) &&
(ContextLevel() < var_info.scope_id)) {
// The variable is textually in scope but the context level
// at the activation frame's PC is lower than the context
// level of the variable. The context containing the variable
// has already been removed from the chain. This can happen when we
// break at a return statement, since the contexts get discarded
// before the debugger gets called.
continue;
}
// The current variable is textually in scope. Now check whether
// there is another local variable with the same name that shadows
// or is shadowed by this variable.
String& var_name = String::Handle(var_descriptors_.GetName(cur_idx));
intptr_t indices_len = desc_indices_.length();
bool name_match_found = false;
for (intptr_t i = 0; i < indices_len; i++) {
if (var_name.Equals(*var_names[i])) {
// Found two local variables with the same name. Now determine
// which one is shadowed.
name_match_found = true;
UntaggedLocalVarDescriptors::VarInfo i_var_info;
var_descriptors_.GetInfo(desc_indices_[i], &i_var_info);
if (i_var_info.begin_pos < var_info.begin_pos) {
// The variable we found earlier is in an outer scope
// and is shadowed by the current variable. Replace the
// descriptor index of the previously found variable
// with the descriptor index of the current variable.
desc_indices_[i] = cur_idx;
} else {
// The variable we found earlier is in an inner scope
// and shadows the current variable. Skip the current
// variable. (Nothing to do.)
}
break; // Stop looking for name matches.
}
}
if (!name_match_found) {
// No duplicate name found. Add the current descriptor index to the
// list of visible variables.
desc_indices_.Add(cur_idx);
var_names.Add(&var_name);
}
}
vars_initialized_ = true;
}
intptr_t ActivationFrame::NumLocalVariables() {
GetDescIndices();
return desc_indices_.length();
}
DART_FORCE_INLINE static ObjectPtr GetVariableValue(uword addr) {
return *reinterpret_cast<ObjectPtr*>(addr);
}
// Caution: GetParameter only works for fixed parameters.
ObjectPtr ActivationFrame::GetParameter(intptr_t index) {
intptr_t num_parameters = function().num_fixed_parameters();
ASSERT(0 <= index && index < num_parameters);
// fp will be a nullptr if the frame isn't active on the stack.
if (fp() == 0) {
return Object::null();
}
if (function().MakesCopyOfParameters()) {
// Function parameters are copied to a fixed place in the callee's frame.
if (function().IsSuspendableFunction()) {
++index; // Skip slot reserved for :suspend_state variable.
}
if (IsInterpreted()) {
return GetStackVar(VariableIndex(-index));
}
return GetVariableValue(LocalVarAddress(
fp(), runtime_frame_layout.FrameSlotForVariableIndex(-index)));
} else {
intptr_t reverse_index = num_parameters - index;
if (IsInterpreted()) {
return GetStackVar(VariableIndex(reverse_index));
}
return GetVariableValue(ParamAddress(fp(), reverse_index));
}
}
ClosurePtr ActivationFrame::GetClosure() {
ASSERT(function().IsClosureFunction());
Object& param = Object::Handle(GetParameter(0));
ASSERT(param.IsInstance());
ASSERT(Instance::Cast(param).IsClosure());
return Closure::Cast(param).ptr();
}
ObjectPtr ActivationFrame::GetSuspendStateVar() {
ASSERT(function().IsSuspendableFunction());
return GetStackVar(VariableIndex(SuspendState::kSuspendStateVarIndex));
}
ObjectPtr ActivationFrame::GetSuspendableFunctionData() {
Object& suspend_state = Object::Handle(GetSuspendStateVar());
if (suspend_state.IsSuspendState()) {
return SuspendState::Cast(suspend_state).function_data();
}
return suspend_state.ptr();
}
ObjectPtr ActivationFrame::GetStackVar(VariableIndex variable_index) {
if (IsInterpreted()) {
intptr_t slot_index = -variable_index.value();
if (slot_index < 0) {
slot_index -= kKBCParamEndSlotFromFp; // Accessing a parameter.
}
return GetVariableValue(fp() + slot_index * kWordSize);
}
const intptr_t slot_index =
runtime_frame_layout.FrameSlotForVariableIndex(variable_index.value());
if (deopt_frame_.IsNull()) {
return GetVariableValue(LocalVarAddress(fp(), slot_index));
} else {
return deopt_frame_.At(LocalVarIndex(deopt_frame_offset_, slot_index));
}
}
bool ActivationFrame::IsRewindable() const {
if (deopt_frame_.IsNull()) {
return true;
}
// TODO(turnidge): This is conservative. It looks at all values in
// the deopt_frame_ even though some of them may correspond to other
// inlined frames.
Object& obj = Object::Handle();
for (int i = 0; i < deopt_frame_.Length(); i++) {
obj = deopt_frame_.At(i);
if (obj.ptr() == Object::optimized_out().ptr()) {
return false;
}
}
return true;
}
void ActivationFrame::PrintContextMismatchError(intptr_t ctx_slot,
intptr_t frame_ctx_level,
intptr_t var_ctx_level) {
OS::PrintErr(
"-------------------------\n"
"Encountered context mismatch\n"
"\tctx_slot: %" Pd
"\n"
"\tframe_ctx_level: %" Pd
"\n"
"\tvar_ctx_level: %" Pd "\n\n",
ctx_slot, frame_ctx_level, var_ctx_level);
OS::PrintErr(
"-------------------------\n"
"Current frame:\n%s\n",
this->ToCString());
OS::PrintErr(
"-------------------------\n"
"Context contents:\n");
const Context& ctx = GetSavedCurrentContext();
ctx.Dump(8);
OS::PrintErr(
"-------------------------\n"
"Debugger stack trace...\n\n");
DebuggerStackTrace* stack = Isolate::Current()->debugger()->StackTrace();
intptr_t num_frames = stack->Length();
for (intptr_t i = 0; i < num_frames; i++) {
ActivationFrame* frame = stack->FrameAt(i);
OS::PrintErr("#%04" Pd " %s", i, frame->ToCString());
}
OS::PrintErr(
"-------------------------\n"
"All frames...\n\n");
StackFrameIterator iterator(ValidationPolicy::kDontValidateFrames,
Thread::Current(),
StackFrameIterator::kNoCrossThreadIteration);
StackFrame* frame = iterator.NextFrame();
intptr_t num = 0;
while ((frame != nullptr)) {
OS::PrintErr("#%04" Pd " %s\n", num++, frame->ToCString());
frame = iterator.NextFrame();
}
}
void ActivationFrame::VariableAt(intptr_t i,
String* name,
TokenPosition* declaration_token_pos,
TokenPosition* visible_start_token_pos,
TokenPosition* visible_end_token_pos,
Object* value) {
GetDescIndices();
ASSERT(i < desc_indices_.length());
intptr_t desc_index = desc_indices_[i];
ASSERT(name != nullptr);
*name = var_descriptors_.GetName(desc_index);
UntaggedLocalVarDescriptors::VarInfo var_info;
var_descriptors_.GetInfo(desc_index, &var_info);
ASSERT(declaration_token_pos != nullptr);
*declaration_token_pos = var_info.declaration_pos;
ASSERT(visible_start_token_pos != nullptr);
*visible_start_token_pos = var_info.begin_pos;
ASSERT(visible_end_token_pos != nullptr);
*visible_end_token_pos = var_info.end_pos;
ASSERT(value != nullptr);
const int8_t kind = var_info.kind();
const auto variable_index = VariableIndex(var_info.index());
if (kind == UntaggedLocalVarDescriptors::kStackVar) {
*value = GetStackVar(variable_index);
} else {
ASSERT(kind == UntaggedLocalVarDescriptors::kContextVar);
*value = GetContextVar(var_info.scope_id, variable_index.value());
}
}
ObjectPtr ActivationFrame::GetContextVar(intptr_t var_ctx_level,
intptr_t ctx_slot) {
// The context level at the PC/token index of this activation frame.
intptr_t frame_ctx_level = ContextLevel();
return GetRelativeContextVar(var_ctx_level, ctx_slot, frame_ctx_level);
}
ObjectPtr ActivationFrame::GetRelativeContextVar(intptr_t var_ctx_level,
intptr_t ctx_slot,
intptr_t frame_ctx_level) {
const Context& ctx = GetSavedCurrentContext();
// It's possible that ctx was optimized out as no locals were captured by the
// context. See issue #38182.
if (ctx.IsNull()) {
return Object::optimized_out().ptr();
}
intptr_t level_diff = frame_ctx_level - var_ctx_level;
if (level_diff == 0) {
if ((ctx_slot < 0) || (ctx_slot >= ctx.num_variables())) {
PrintContextMismatchError(ctx_slot, frame_ctx_level, var_ctx_level);
}
ASSERT((ctx_slot >= 0) && (ctx_slot < ctx.num_variables()));
return ctx.At(ctx_slot);
} else if (level_diff > 0) {
Context& var_ctx = Context::Handle(ctx.ptr());
while (level_diff > 0 && !var_ctx.IsNull()) {
level_diff--;
var_ctx = var_ctx.parent();
}
if (var_ctx.IsNull() || (ctx_slot < 0) ||
(ctx_slot >= var_ctx.num_variables())) {
PrintContextMismatchError(ctx_slot, frame_ctx_level, var_ctx_level);
}
ASSERT(!var_ctx.IsNull());
ASSERT((ctx_slot >= 0) && (ctx_slot < var_ctx.num_variables()));
return var_ctx.At(ctx_slot);
} else {
PrintContextMismatchError(ctx_slot, frame_ctx_level, var_ctx_level);
return Object::null();
}
}
ObjectPtr ActivationFrame::GetReceiver() {
GetDescIndices();
intptr_t num_variables = desc_indices_.length();
String& var_name = String::Handle();
Instance& value = Instance::Handle();
for (intptr_t i = 0; i < num_variables; i++) {
TokenPosition ignore = TokenPosition::kNoSource;
VariableAt(i, &var_name, &ignore, &ignore, &ignore, &value);
if (var_name.Equals(Symbols::This())) {
return value.ptr();
}
}
return Object::optimized_out().ptr();
}
static bool IsSyntheticVariableName(const String& var_name) {
return (var_name.Length() >= 1) && (var_name.CharAt(0) == ':');
}
static bool IsPrivateVariableName(const String& var_name) {
return (var_name.Length() >= 1) && (var_name.CharAt(0) == '_');
}
ObjectPtr ActivationFrame::EvaluateCompiledExpression(
const ExternalTypedData& kernel_buffer,
const Array& type_definitions,
const Array& arguments,
const TypeArguments& type_arguments) {
auto thread = Thread::Current();
auto zone = thread->zone();
// The expression evaluation function will get all it's captured state passed
// as parameters (with `this` being the exception). As a result, we treat the
// expression evaluation function as either a top-level, static or instance
// method.
const auto& outermost =
Function::Handle(zone, function().GetOutermostFunction());
const auto& klass = Class::Handle(zone, outermost.Owner());
const auto& library = Library::Handle(zone, klass.library());
auto& receiver = Object::Handle(zone);
if (!klass.IsTopLevel() && !outermost.is_static()) {
receiver = GetReceiver();
RELEASE_ASSERT(receiver.IsInstance() ||
receiver.ptr() == Object::optimized_out().ptr());
}
return Instance::EvaluateCompiledExpression(thread, receiver, library, klass,
kernel_buffer, type_definitions,
arguments, type_arguments);
}
TypeArgumentsPtr ActivationFrame::BuildParameters(
const GrowableObjectArray& param_names,
const GrowableObjectArray& param_values,
const GrowableObjectArray& type_params_names,
const GrowableObjectArray& type_params_bounds,
const GrowableObjectArray& type_params_defaults) {
GetDescIndices();
bool type_arguments_available = false;
String& name = String::Handle();
String& existing_name = String::Handle();
Object& value = Instance::Handle();
TypeArguments& type_arguments = TypeArguments::Handle();
intptr_t num_variables = desc_indices_.length();
for (intptr_t i = 0; i < num_variables; i++) {
TokenPosition ignore = TokenPosition::kNoSource;
VariableAt(i, &name, &ignore, &ignore, &ignore, &value);
if (name.Equals(Symbols::FunctionTypeArgumentsVar())) {
type_arguments_available = true;
type_arguments ^= value.ptr();
} else if (
!name.Equals(Symbols::This()) && !IsSyntheticVariableName(name) &&
value.ptr() != Object::optimized_out().ptr() &&
// TODO(57048): We currently pretend that uninitialized late variables
// don't exist to avoid crashing when evaluating expressions that need
// to access the late variables' values. The CFE team is working on a
// better solution.
value.ptr() != Object::sentinel().ptr()) {
if (IsPrivateVariableName(name)) {
name = Symbols::New(Thread::Current(), String::ScrubName(name));
}
bool conflict = false;
for (intptr_t j = 0; j < param_names.Length(); j++) {
existing_name ^= param_names.At(j);
if (name.Equals(existing_name)) {
conflict = true;
break;
}
}
// If local has the same name as a binding in the incoming scope, prefer
// the one from the incoming scope, since it is logically a child scope
// of the activation's current scope.
if (!conflict) {
param_names.Add(name);
param_values.Add(value);
}
}
}
if ((function().IsGeneric() || function().HasGenericParent()) &&
type_arguments_available) {
intptr_t num_vars = function().NumTypeArguments();
type_params_names.Grow(num_vars);
type_params_names.SetLength(num_vars);
type_params_bounds.Grow(num_vars);
type_params_bounds.SetLength(num_vars);
type_params_defaults.Grow(num_vars);
type_params_defaults.SetLength(num_vars);
AbstractType& bound = AbstractType::Handle();
AbstractType& defaultType = AbstractType::Handle();
TypeParameters& type_params = TypeParameters::Handle();
Function& current = Function::Handle(function().ptr());
intptr_t mapping_offset = num_vars;
for (; !current.IsNull(); current = current.parent_function()) {
type_params = current.type_parameters();
if (type_params.IsNull()) continue;
intptr_t size = current.NumTypeParameters();
ASSERT(size > 0 && type_params.Length() == size);
ASSERT(mapping_offset >= size);
mapping_offset -= size;
for (intptr_t j = 0; j < size; ++j) {
name = type_params.NameAt(j);
bound = type_params.BoundAt(j);
defaultType = type_params.DefaultAt(j);
// Write the names in backwards in terms of chain of functions.
// But keep the order of names within the same function. so they
// match up with the order of the types in 'type_arguments'.
// Index:0 1 2 3 ...
// |Names in Grandparent| |Names in Parent| ..|Names in Child|
type_params_names.SetAt(mapping_offset + j, name);
type_params_bounds.SetAt(mapping_offset + j, bound);
type_params_defaults.SetAt(mapping_offset + j, defaultType);
}
}
if (!type_arguments.IsNull()) {
if (type_arguments.Length() == 0) {
for (intptr_t i = 0; i < num_vars; ++i) {
type_arguments.SetTypeAt(i, Object::dynamic_type());
}
}
ASSERT(type_arguments.Length() == num_vars);
}
}
return type_arguments.ptr();
}
const char* ActivationFrame::ToCString() {
if (function().IsNull()) {
return Thread::Current()->zone()->PrintToString("[ Frame kind: %s]\n",
KindToCString(kind_));
}
const String& url = String::Handle(SourceUrl());
intptr_t line = LineNumber();
const char* func_name = function().ToFullyQualifiedCString();
if (kind_ == kRegular) {
return Thread::Current()->zone()->PrintToString(
"[ Frame pc(0x%" Px ") fp(0x%" Px ") sp(0x%" Px
")\n"
"\t%s offset = 0x%" Px
"\n"
"\tfunction = %s\n"
"\turl = %s\n"
"\tline = %" Pd
"\n"
"\tcontext = %s\n"
"\tcontext level = %" Pd " ]\n",
pc(), fp(), sp(), IsInterpreted() ? "bytecode" : "code",
pc() - PayloadStart(), func_name, url.ToCString(), line,
ctx_.ToCString(), ContextLevel());
} else {
return Thread::Current()->zone()->PrintToString(
"[ Frame code function = %s\n"
"\turl = %s\n"
"\tline = %" Pd
"\n"
"\tcontext = %s]\n",
func_name, url.ToCString(), line, ctx_.ToCString());
}
}
void ActivationFrame::PrintToJSONObject(JSONObject* jsobj) {
if (kind_ == kRegular) {
PrintToJSONObjectRegular(jsobj);
} else if (kind_ == kAsyncAwaiter) {
PrintToJSONObjectAsyncAwaiter(jsobj);
} else if (kind_ == kAsyncSuspensionMarker) {
PrintToJSONObjectAsyncSuspensionMarker(jsobj);
} else {
UNIMPLEMENTED();
}
}
void ActivationFrame::PrintToJSONObjectRegular(JSONObject* jsobj) {
const Script& script = Script::Handle(SourceScript());
jsobj->AddProperty("type", "Frame");
jsobj->AddProperty("kind", KindToCString(kind_));
const TokenPosition& pos = TokenPos();
jsobj->AddLocation(script, pos);
jsobj->AddProperty("function", function());
if (IsInterpreted()) {
jsobj->AddProperty("code", bytecode());
} else {
jsobj->AddProperty("code", code());
}
{
JSONArray jsvars(jsobj, "vars");
const int num_vars = NumLocalVariables();
for (intptr_t v = 0; v < num_vars; v++) {
String& var_name = String::Handle();
Instance& var_value = Instance::Handle();
TokenPosition declaration_token_pos = TokenPosition::kNoSource;
TokenPosition visible_start_token_pos = TokenPosition::kNoSource;
TokenPosition visible_end_token_pos = TokenPosition::kNoSource;
VariableAt(v, &var_name, &declaration_token_pos, &visible_start_token_pos,
&visible_end_token_pos, &var_value);
if (!IsSyntheticVariableName(var_name)) {
JSONObject jsvar(&jsvars);
jsvar.AddProperty("type", "BoundVariable");
const char* scrubbed_var_name = String::ScrubName(var_name);
jsvar.AddProperty("name", scrubbed_var_name);
jsvar.AddProperty("value", var_value);
// Where was the variable declared?
jsvar.AddProperty("declarationTokenPos", declaration_token_pos);
// When the variable becomes visible to the scope.
jsvar.AddProperty("scopeStartTokenPos", visible_start_token_pos);
// When the variable stops being visible to the scope.
jsvar.AddProperty("scopeEndTokenPos", visible_end_token_pos);
}
}
}
}
void ActivationFrame::PrintToJSONObjectAsyncAwaiter(JSONObject* jsobj) {
jsobj->AddProperty("type", "Frame");
jsobj->AddProperty("kind", KindToCString(kind_));
const Script& script = Script::Handle(SourceScript());
const TokenPosition& pos = TokenPos();
jsobj->AddLocation(script, pos);
jsobj->AddProperty("function", function());
if (IsInterpreted()) {
jsobj->AddProperty("code", bytecode());
} else {
jsobj->AddProperty("code", code());
}
}
void ActivationFrame::PrintToJSONObjectAsyncSuspensionMarker(
JSONObject* jsobj) {
jsobj->AddProperty("type", "Frame");
jsobj->AddProperty("kind", KindToCString(kind_));
jsobj->AddProperty("marker", "AsynchronousSuspension");
}
static bool IsFunctionVisible(const Function& function) {
return FLAG_show_invisible_frames || function.is_visible();
}
void DebuggerStackTrace::AddActivation(ActivationFrame* frame) {
if (IsFunctionVisible(frame->function())) {
trace_.Add(frame);
}
}
void DebuggerStackTrace::AddAsyncSuspension() {
// We might start asynchronous unwinding in one of the internal
// dart:async functions which would make synchronous part of the
// stack empty. This would not happen normally but might happen
// with stress flags.
if (trace_.is_empty() ||
trace_.Last()->kind() != ActivationFrame::kAsyncSuspensionMarker) {
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr(" async suspension\n");
}
trace_.Add(new ActivationFrame(ActivationFrame::kAsyncSuspensionMarker));
} else if (FLAG_trace_debugger_stacktrace &&
FLAG_trace_debugger_stacktrace_verbose) {
OS::PrintErr(" skipping repeated async suspension\n");
}
}
void DebuggerStackTrace::AddAsyncAwaiterFrame(uword pc,
const Function& function,
const Object& code_or_bytecode,
const Closure& closure) {
trace_.Add(new ActivationFrame(pc, function, code_or_bytecode, closure));
}
const uint8_t kSafepointKind = UntaggedPcDescriptors::kIcCall |
UntaggedPcDescriptors::kUnoptStaticCall |
UntaggedPcDescriptors::kRuntimeCall;
CodeBreakpoint::CodeBreakpoint(const Code& code,
BreakpointLocation* breakpoint_location,
uword pc,
UntaggedPcDescriptors::Kind kind)
: code_(code.ptr()),
bytecode_(Bytecode::null()),
pc_(pc),
enabled_count_(0),
next_(nullptr),
breakpoint_kind_(kind),
saved_value_(Code::null()),
saved_opcode_(kMaxUint32) {
ASSERT(!code.IsNull());
ASSERT(pc_ != 0);
ASSERT((breakpoint_kind_ & kSafepointKind) != 0);
AddBreakpointLocation(breakpoint_location);
ASSERT(breakpoint_location->token_pos().IsReal());
}
CodeBreakpoint::CodeBreakpoint(const Bytecode& bytecode,
BreakpointLocation* breakpoint_location,
uword pc)
: code_(Code::null()),
bytecode_(bytecode.ptr()),
pc_(pc),
enabled_count_(0),
next_(nullptr),
breakpoint_kind_(UntaggedPcDescriptors::kRuntimeCall),
saved_value_(Code::null()),
saved_opcode_(kMaxUint32) {
ASSERT(!bytecode.IsNull());
ASSERT(pc_ != 0);
AddBreakpointLocation(breakpoint_location);
ASSERT(breakpoint_location->token_pos().IsReal());
}
CodeBreakpoint::~CodeBreakpoint() {
// Make sure we don't leave patched code behind.
ASSERT(!IsEnabled());
// Poison the data so we catch use after free errors.
#ifdef DEBUG
code_ = Code::null();
bytecode_ = Bytecode::null();
pc_ = 0ul;
next_ = nullptr;
breakpoint_kind_ = UntaggedPcDescriptors::kOther;
#endif
}
void CodeBreakpoint::Enable() {
if (enabled_count_ == 0) {
if (bytecode_ != Bytecode::null()) {
#if defined(DART_DYNAMIC_MODULES)
ASSERT_EQUAL(saved_opcode_, kMaxUint32);
saved_opcode_ =
BytecodePatcher::AddBreakpointAt(pc_, Bytecode::Handle(bytecode_));
#else
UNREACHABLE();
#endif
} else {
PatchCode();
}
}
++enabled_count_;
}
void CodeBreakpoint::Disable() {
if (enabled_count_ == 1) {
if (bytecode_ != Bytecode::null()) {
#if defined(DART_DYNAMIC_MODULES)
BytecodePatcher::RemoveBreakpointAt(pc_, Bytecode::Handle(bytecode_),
saved_opcode_);
saved_opcode_ = kMaxUint32;
#else
UNREACHABLE();
#endif
} else {
RestoreCode();
}
}
--enabled_count_;
}
bool CodeBreakpoint::HasBreakpointLocation(
BreakpointLocation* breakpoint_location) {
for (intptr_t i = 0; i < breakpoint_locations_.length(); i++) {
if (breakpoint_locations_[i] == breakpoint_location) {
return true;
}
}
return false;
}
bool CodeBreakpoint::FindAndDeleteBreakpointLocation(
BreakpointLocation* breakpoint_location) {
for (intptr_t i = 0; i < breakpoint_locations_.length(); i++) {
if (breakpoint_locations_[i] == breakpoint_location) {
breakpoint_locations_.EraseAt(i);
return true;
}
}
return false;
}
BreakpointLocation* CodeBreakpoint::FindBreakpointForDebugger(
Debugger* debugger) {
for (intptr_t i = 0; i < breakpoint_locations_.length(); i++) {
if (breakpoint_locations_[i]->debugger() == debugger) {
return breakpoint_locations_[i];
}
}
return nullptr;
}
GroupDebugger::GroupDebugger(IsolateGroup* isolate_group)
: isolate_group_(isolate_group),
code_breakpoints_lock_(new SafepointRwLock()),
code_breakpoints_(nullptr),
breakpoint_locations_lock_(new SafepointRwLock()),
single_stepping_set_lock_(new RwLock()),
needs_breakpoint_cleanup_(false) {}
GroupDebugger::~GroupDebugger() {
while (code_breakpoints_ != nullptr) {
CodeBreakpoint* cbpt = code_breakpoints_;
code_breakpoints_ = code_breakpoints_->next();
ASSERT(!cbpt->IsEnabled());
delete cbpt;
}
}
Debugger::Debugger(Isolate* isolate)
: isolate_(isolate),
next_id_(1),
latent_locations_(nullptr),
breakpoint_locations_(nullptr),
resume_action_(kContinue),
resume_frame_index_(-1),
post_deopt_frame_index_(-1),
ignore_breakpoints_(false),
pause_event_(nullptr),
stack_trace_(nullptr),
async_awaiter_stack_trace_(nullptr),
stepping_fp_(0),
last_stepping_fp_(0),
last_stepping_pos_(TokenPosition::kNoSource),
exc_pause_info_(kNoPauseOnExceptions) {}
Debugger::~Debugger() {
ASSERT(!IsPaused());
ASSERT(latent_locations_ == nullptr);
ASSERT(breakpoint_locations_ == nullptr);
ASSERT(stack_trace_ == nullptr);
ASSERT(async_awaiter_stack_trace_ == nullptr);
}
void Debugger::Shutdown() {
// TODO(johnmccutchan): Do not create a debugger for isolates that don't need
// them. Then, assert here that isolate_ is not one of those isolates.
if (Isolate::IsSystemIsolate(isolate_)) {
return;
}
{
SafepointWriteRwLocker sl(Thread::Current(),
group_debugger()->breakpoint_locations_lock());
while (breakpoint_locations_ != nullptr) {
BreakpointLocation* loc = breakpoint_locations_;
group_debugger()->UnlinkCodeBreakpoints(loc);
group_debugger()->UnregisterBreakpointLocation(loc);
breakpoint_locations_ = breakpoint_locations_->next();
delete loc;
}
while (latent_locations_ != nullptr) {
BreakpointLocation* loc = latent_locations_;
group_debugger()->UnlinkCodeBreakpoints(loc);
group_debugger()->UnregisterBreakpointLocation(loc);
latent_locations_ = latent_locations_->next();
delete loc;
}
}
if (NeedsIsolateEvents()) {
ServiceEvent event(isolate_, ServiceEvent::kIsolateExit);
InvokeEventHandler(&event);
}
}
static ActivationFrame* TopDartFrame();
static bool IsAtAsyncJump(ActivationFrame* top_frame);
bool Debugger::SetupStepOverAsyncSuspension(const char** error) {
ActivationFrame* top_frame = TopDartFrame();
if (!IsAtAsyncJump(top_frame)) {
// Not at an async operation.
if (error != nullptr) {
*error = "Isolate must be paused at an async suspension point";
}
return false;
}
ASSERT(top_frame->function().IsAsyncFunction() ||
top_frame->function().IsAsyncGenerator());
const auto& function_data =
Object::Handle(top_frame->GetSuspendableFunctionData());
SetBreakpointAtResumption(function_data);
return true;
}
static bool CanRewindFrame(intptr_t frame_index, const char** error);
bool Debugger::SetResumeAction(ResumeAction action,
intptr_t frame_index,
const char** error) {
if (error != nullptr) {
*error = nullptr;
}
resume_frame_index_ = -1;
switch (action) {
case kStepInto:
case kStepOver:
case kStepOut:
case kContinue:
set_resume_action(action);
return true;
case kStepRewind:
if (!CanRewindFrame(frame_index, error)) {
return false;
}
set_resume_action(kStepRewind);
resume_frame_index_ = frame_index;
return true;
case kStepOverAsyncSuspension:
return SetupStepOverAsyncSuspension(error);
default:
UNREACHABLE();
return false;
}
}
// Deoptimize all functions in the isolate.
// TODO(hausner): Actually we only need to deoptimize those functions
// that inline the function that contains the newly created breakpoint.
// We currently don't have this info so we deoptimize all functions.
void Debugger::DeoptimizeWorld() {
#if defined(DART_PRECOMPILED_RUNTIME)
UNREACHABLE();
#else
if (FLAG_trace_deoptimization) {
THR_Print("Deopt for debugger\n");
}
isolate_->group()->set_has_attempted_stepping(true);
DeoptimizeFunctionsOnStack();
// Iterate over all classes, deoptimize functions.
// TODO(hausner): Could possibly be combined with RemoveOptimizedCode()
const ClassTable& class_table = *isolate_->group()->class_table();
auto thread = Thread::Current();
auto isolate_group = thread->isolate_group();
auto zone = thread->zone();
CallSiteResetter resetter(zone);
Class& cls = Class::Handle(zone);
Array& functions = Array::Handle(zone);
Function& function = Function::Handle(zone);
Code& code = Code::Handle(zone);
const intptr_t num_classes = class_table.NumCids();
const intptr_t num_tlc_classes = class_table.NumTopLevelCids();
SafepointWriteRwLocker ml(thread, isolate_group->program_lock());
for (intptr_t i = 1; i < num_classes + num_tlc_classes; i++) {
const intptr_t cid =
i < num_classes ? i : ClassTable::CidFromTopLevelIndex(i - num_classes);
if (class_table.HasValidClassAt(cid)) {
cls = class_table.At(cid);
// Disable optimized functions.
functions = cls.functions();
if (!functions.IsNull()) {
intptr_t num_functions = functions.Length();
for (intptr_t pos = 0; pos < num_functions; pos++) {
function ^= functions.At(pos);
ASSERT(!function.IsNull());
// Force-optimized functions don't have unoptimized code and can't
// deoptimize. Their optimized codes are still valid.
if (!function.ForceOptimize()) {
if (function.HasOptimizedCode()) {
function.SwitchToUnoptimizedCode();
}
code = function.unoptimized_code();
if (!code.IsNull()) {
resetter.ResetSwitchableCalls(code);
}
}
// Also disable any optimized implicit closure functions.
if (function.HasImplicitClosureFunction()) {
function = function.ImplicitClosureFunction();
if (!function.ForceOptimize()) {
if (function.HasOptimizedCode()) {
function.SwitchToUnoptimizedCode();
}
code = function.unoptimized_code();
if (!code.IsNull()) {
resetter.ResetSwitchableCalls(code);
}
}
}
}
}
}
}
// Disable optimized closure functions.
ClosureFunctionsCache::ForAllClosureFunctions([&](const Function& function) {
if (!function.ForceOptimize()) {
if (function.HasOptimizedCode()) {
function.SwitchToUnoptimizedCode();
}
code = function.unoptimized_code();
if (!code.IsNull()) {
resetter.ResetSwitchableCalls(code);
}
}
return true; // Continue iteration.
});
#endif // defined(DART_PRECOMPILED_RUNTIME)
}
void Debugger::RunWithStoppedDeoptimizedWorld(std::function<void()> fun) {
#if !defined(DART_PRECOMPILED_RUNTIME)
// RELOAD_OPERATION_SCOPE is used here because is is guaranteed that
// isolates at reload safepoints hold no safepoint locks.
RELOAD_OPERATION_SCOPE(Thread::Current());
group_debugger()->isolate_group()->RunWithStoppedMutators([&]() {
DeoptimizeWorld();
fun();
});
#endif
}
void Debugger::NotifySingleStepping(bool value) {
RELEASE_ASSERT(isolate_->mutator_thread() != nullptr);
if (value) {
// Setting breakpoint requires unoptimized code, make sure we stop all
// isolates to prevent racing reoptimization.
RunWithStoppedDeoptimizedWorld([&] {
isolate_->mutator_thread()->set_single_step(value);
// Ensure other isolates in the isolate group keep
// unoptimized code unoptimized, won't attempt to optimize it.
group_debugger()->RegisterSingleSteppingDebugger(Thread::Current(), this);
});
} else {
isolate_->mutator_thread()->set_single_step(value);
group_debugger()->UnregisterSingleSteppingDebugger(Thread::Current(), this);
}
}
static ActivationFrame* CollectDartFrame(uword pc,
StackFrame* frame,
const Function& function,
const Object& code_or_bytecode,
const Array& deopt_frame,
intptr_t deopt_frame_offset) {
ASSERT((code_or_bytecode.IsCode() &&
Code::Cast(code_or_bytecode).ContainsInstructionAt(pc)) ||
(code_or_bytecode.IsBytecode() &&
Bytecode::Cast(code_or_bytecode).ContainsInstructionAt(pc)));
ActivationFrame* activation =
new ActivationFrame(pc, frame->fp(), frame->sp(), function,
code_or_bytecode, deopt_frame, deopt_frame_offset);
if (FLAG_trace_debugger_stacktrace) {
intptr_t index = -1;
const Context& ctx = activation->GetSavedCurrentContext(&index);
if (index >= 0) {
OS::PrintErr(" Current context (index %" Pu "): %s\n", index,
ctx.ToCString());
} else if (!ctx.IsNull()) {
OS::PrintErr(" Current context: %s\n", ctx.ToCString());
}
OS::PrintErr(" Line number: %" Pd "\n", activation->LineNumber());
}
return activation;
}
#if !defined(DART_PRECOMPILED_RUNTIME)
static ArrayPtr DeoptimizeToArray(Thread* thread,
StackFrame* frame,
const Code& code) {
ASSERT(code.is_optimized() && !code.is_force_optimized());
// Create the DeoptContext for this deoptimization.
DeoptContext* deopt_context =
new DeoptContext(frame, code, DeoptContext::kDestIsAllocated, nullptr,
nullptr, true, false /* deoptimizing_code */);
thread->set_deopt_context(deopt_context);
deopt_context->FillDestFrame();
deopt_context->MaterializeDeferredObjects();
const Array& dest_frame =
Array::Handle(thread->zone(), deopt_context->DestFrameAsArray());
thread->set_deopt_context(nullptr);
delete deopt_context;
return dest_frame.ptr();
}
#endif // !defined(DART_PRECOMPILED_RUNTIME)
DebuggerStackTrace* DebuggerStackTrace::Collect() {
Thread* thread = Thread::Current();
Zone* zone = thread->zone();
auto& function = Function::Handle(zone);
auto& code = Code::Handle(zone);
auto& bytecode = Bytecode::Handle(zone);
DebuggerStackTrace* stack_trace = new DebuggerStackTrace(8);
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr("CollectStackTrace: starting collection\n");
}
StackFrameIterator iterator(ValidationPolicy::kDontValidateFrames, thread,
StackFrameIterator::kNoCrossThreadIteration);
for (StackFrame* frame = iterator.NextFrame(); frame != nullptr;
frame = iterator.NextFrame()) {
ASSERT(frame->IsValid());
if (frame->IsDartFrame()) {
if (FLAG_trace_debugger_stacktrace) {
// StackFrame::ToCString() prepends two spaces in its output.
OS::PrintErr("%s\n", frame->ToCString());
}
if (frame->is_interpreted()) {
function = frame->LookupDartFunction();
bytecode = frame->LookupDartBytecode();
stack_trace->AppendBytecodeFrame(frame, function, bytecode);
} else {
code = frame->LookupDartCode();
stack_trace->AppendCodeFrames(frame, code);
}
} else if (FLAG_trace_debugger_stacktrace &&
FLAG_trace_debugger_stacktrace_verbose) {
// StackFrame::ToCString() prepends two spaces in its output.
OS::PrintErr("%s\n", frame->ToCString());
OS::PrintErr(" non-Dart frame skipped\n");
}
}
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr("CollectStackTrace: collection finished\n\n");
}
return stack_trace;
}
// Appends at least one stack frame. Multiple frames will be appended
// if |code| at the frame's pc contains inlined functions.
void DebuggerStackTrace::AppendCodeFrames(StackFrame* frame, const Code& code) {
auto& function = Function::Handle(zone_, code.function());
#if !defined(DART_PRECOMPILED_RUNTIME)
if (code.is_optimized()) {
if (code.is_force_optimized()) {
if (FLAG_trace_debugger_stacktrace) {
ASSERT(!function.IsNull());
OS::PrintErr(" skipping force-optimized function: %s\n",
function.ToFullyQualifiedCString());
}
return; // Skip frame of force-optimized (and non-debuggable) function.
}
// TODO(rmacnak): Use CodeSourceMap
deopt_frame_ = DeoptimizeToArray(thread_, frame, code);
for (InlinedFunctionsIterator it(code, frame->pc()); !it.Done();
it.Advance()) {
inlined_code_ = it.code();
function = it.function();
if (FLAG_trace_debugger_stacktrace) {
ASSERT(!function.IsNull());
OS::PrintErr(" visiting inlined function: %s\n",
function.ToFullyQualifiedCString());
}
intptr_t deopt_frame_offset = it.GetDeoptFpOffset();
AddActivation(CollectDartFrame(it.pc(), frame, function, inlined_code_,
deopt_frame_, deopt_frame_offset));
}
return;
}
#endif // !defined(DART_PRECOMPILED_RUNTIME)
AddActivation(CollectDartFrame(frame->pc(), frame, function, code,
Object::null_array(), 0));
}
// Appends one interpreted stack frame.
void DebuggerStackTrace::AppendBytecodeFrame(StackFrame* frame,
const Function& function,
const Bytecode& bytecode) {
AddActivation(CollectDartFrame(frame->pc(), frame, function, bytecode,
Object::null_array(), 0));
}
DebuggerStackTrace* DebuggerStackTrace::CollectAsyncAwaiters() {
Thread* thread = Thread::Current();
Zone* zone = thread->zone();
Function& function = Function::Handle(zone);
constexpr intptr_t kDefaultStackAllocation = 8;
auto stack_trace = new DebuggerStackTrace(kDefaultStackAllocation);
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr("CollectStackTrace: starting async awaiters collection\n");
}
bool has_async = false;
bool has_async_catch_error = false;
StackTraceUtils::CollectFrames(
thread, /*skip_frames=*/0,
[&](const StackTraceUtils::Frame& frame) {
if (frame.code.IsNull() && frame.bytecode.IsNull()) {
return;
}
if (frame.frame != nullptr) { // Synchronous portion of the stack.
if (FLAG_trace_debugger_stacktrace) {
// StackFrame::ToCString() prepends two spaces in its output.
OS::PrintErr("%s\n", frame.frame->ToCString());
}
if (!frame.bytecode.IsNull()) {
function = frame.frame->LookupDartFunction();
stack_trace->AppendBytecodeFrame(frame.frame, function,
frame.bytecode);
} else {
stack_trace->AppendCodeFrames(frame.frame, frame.code);
}
} else {
has_async = true;
if (frame.code.ptr() == StubCode::AsynchronousGapMarker().ptr()) {
stack_trace->AddAsyncSuspension();
return;
}
uword start = 0;
const Object* obj = nullptr;
const char* name = nullptr;
if (!frame.bytecode.IsNull()) {
function = frame.bytecode.function();
start = frame.bytecode.PayloadStart();
obj = &frame.bytecode;
if (FLAG_trace_debugger_stacktrace) {
name = frame.bytecode.FullyQualifiedName();
}
} else {
function = frame.code.function();
start = frame.code.PayloadStart();
obj = &frame.code;
if (FLAG_trace_debugger_stacktrace) {
name = frame.code.QualifiedName(
NameFormattingParams(Object::kInternalName));
}
}
ASSERT(!function.IsNull());
if (!function.is_visible()) {
return;
}
const uword absolute_pc = start + frame.pc_offset;
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr(" async awaiter: %s+%#" Px "\n", name,
frame.pc_offset);
}
stack_trace->AddAsyncAwaiterFrame(absolute_pc, function, *obj,
frame.closure);
}
},
&has_async_catch_error);
// If the entire stack is sync, return no (async) trace.
if (!has_async) {
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr(
"CollectStackTrace: discarding async awaiters collection\n\n");
}
return nullptr;
}
stack_trace->set_has_async_catch_error(has_async_catch_error);
if (FLAG_trace_debugger_stacktrace) {
OS::PrintErr("CollectStackTrace: async awaiters collection finished\n\n");
}
return stack_trace;
}
static ActivationFrame* TopDartFrame() {
StackFrameIterator iterator(ValidationPolicy::kDontValidateFrames,
Thread::Current(),
StackFrameIterator::kNoCrossThreadIteration);
StackFrame* frame;
while (true) {
frame = iterator.NextFrame();
RELEASE_ASSERT(frame != nullptr);
if (!frame->IsDartFrame()) {
continue;
}
auto& function = Function::Handle();
auto& code_or_bytecode = Object::Handle();
if (frame->is_interpreted()) {
code_or_bytecode = frame->LookupDartBytecode();
function = frame->LookupDartFunction();
} else {
code_or_bytecode = frame->LookupDartCode();
function = Code::Cast(code_or_bytecode).function();
}
ActivationFrame* activation =
new ActivationFrame(frame->pc(), frame->fp(), frame->sp(), function,
code_or_bytecode, Object::null_array(), 0);
return activation;
}
}
DebuggerStackTrace* Debugger::StackTrace() {
return (stack_trace_ != nullptr) ? stack_trace_
: DebuggerStackTrace::Collect();
}
DebuggerStackTrace* Debugger::AsyncAwaiterStackTrace() {
return (async_awaiter_stack_trace_ != nullptr)
? async_awaiter_stack_trace_
: DebuggerStackTrace::CollectAsyncAwaiters();
}
DebuggerStackTrace* DebuggerStackTrace::From(const class StackTrace& ex_trace) {
DebuggerStackTrace* stack_trace = new DebuggerStackTrace(8);
Function& function = Function::Handle();
Object& code_object = Object::Handle();
Code& code = Code::Handle();
const uword fp = 0;
const uword sp = 0;
const Array& deopt_frame = Array::Handle();
const intptr_t deopt_frame_offset = -1;
for (intptr_t i = 0; i < ex_trace.Length(); i++) {
code_object = ex_trace.CodeAtFrame(i);
// Pre-allocated StackTraces may include empty slots, either (a) to indicate
// where frames were omitted in the case a stack has more frames than the
// pre-allocated trace (such as a stack overflow) or (b) because a stack has
// fewer frames that the pre-allocated trace (such as memory exhaustion with
// a shallow stack).
function = Function::null();
uword start = 0;
bool is_optimized_code = false;
if (code_object.IsCode()) {
const auto& code = Code::Cast(code_object);
ASSERT(code.IsFunctionCode());
function = code.function();
start = code.PayloadStart();
is_optimized_code = code.is_optimized();
} else if (code_object.IsBytecode()) {
const auto& bytecode = Bytecode::Cast(code_object);
function = bytecode.function();
start = bytecode.PayloadStart();
}
if (function.IsNull() || !function.is_visible()) continue;
const uword pc = start + ex_trace.PcOffsetAtFrame(i);
if (is_optimized_code && ex_trace.expand_inlined()) {
// Traverse inlined frames.
code ^= code_object.ptr();
for (InlinedFunctionsIterator it(code, pc); !it.Done(); it.Advance()) {
function = it.function();
code = it.code();
ASSERT(function.ptr() == code.function());
uword pc = it.pc();
ASSERT(pc != 0);
ASSERT(code.PayloadStart() <= pc);
ASSERT(pc < (code.PayloadStart() + code.Size()));
auto* const activation = new ActivationFrame(
pc, fp, sp, function, code, deopt_frame, deopt_frame_offset);
stack_trace->AddActivation(activation);
}
}
auto* const activation = new ActivationFrame(
pc, fp, sp, function, code_object, deopt_frame, deopt_frame_offset);
stack_trace->AddActivation(activation);
}
return stack_trace;
}
void Debugger::SetExceptionPauseInfo(Dart_ExceptionPauseInfo pause_info) {
ASSERT((pause_info == kNoPauseOnExceptions) ||
(pause_info == kPauseOnUnhandledExceptions) ||
(pause_info == kPauseOnAllExceptions));
exc_pause_info_ = pause_info;
}
Dart_ExceptionPauseInfo Debugger::GetExceptionPauseInfo() const {
return exc_pause_info_;
}
bool Debugger::ShouldPauseOnException(DebuggerStackTrace* stack_trace,
const Instance& exception) {
if (exc_pause_info_ == kNoPauseOnExceptions) {
return false;
}
if (exc_pause_info_ == kPauseOnAllExceptions) {
return true;
}
ASSERT(exc_pause_info_ == kPauseOnUnhandledExceptions);
// There might be no Dart stack if we hit an exception in the runtime, most
// likely OutOfMemory.
if (stack_trace->Length() == 0) {
return false;
}
// Exceptions coming from invalid token positions should be skipped
ActivationFrame* top_frame = stack_trace->FrameAt(0);
if (!top_frame->TokenPos().IsReal() && top_frame->TryIndex() != -1) {
return false;
}
ActivationFrame* handler_frame = stack_trace->GetHandlerFrame(exception);
if (handler_frame == nullptr) {
// Did not find an exception handler that catches this exception.
// Note that this check is not precise, since we can't check
// uninstantiated types, i.e. types containing type parameters.
// Thus, we may report an exception as unhandled when in fact
// it will be caught once we unwind the stack.
return !stack_trace->has_async_catch_error();
}
auto& handler_function = Function::Handle(handler_frame->function().ptr());
// If handler_frame's function is annotated with
// @pragma('vm:notify-debugger-on-exception'), we specifically want to notify
// the debugger of this otherwise ignored exception.
if (!handler_function.IsNull() &&
Library::FindPragma(Thread::Current(), /*only_core=*/false,
handler_function,
Symbols::vm_notify_debugger_on_exception())) {
return true;
}
return false;
}
void Debugger::PauseException(const Instance& exc) {
if (FLAG_stress_async_stacks) {
DebuggerStackTrace::CollectAsyncAwaiters();
}
// We ignore this exception event when the VM is executing code invoked
// by the debugger to evaluate variables values, when we see a nested
// breakpoint or exception event, or if the debugger is not
// interested in exception events.
if (ignore_breakpoints_ || IsPaused() ||
(exc_pause_info_ == kNoPauseOnExceptions)) {
return;
}
DebuggerStackTrace* async_awaiter_stack_trace =
DebuggerStackTrace::CollectAsyncAwaiters();
DebuggerStackTrace* stack_trace = DebuggerStackTrace::Collect();
if (async_awaiter_stack_trace != nullptr) {
if (!ShouldPauseOnException(async_awaiter_stack_trace, exc)) {
return;
}
} else {
if (!ShouldPauseOnException(stack_trace, exc)) {
return;
}
}
ServiceEvent event(isolate_, ServiceEvent::kPauseException);
event.set_exception(&exc);
if (stack_trace->Length() > 0) {
event.set_top_frame(stack_trace->FrameAt(0));
}
CacheStackTraces(stack_trace, async_awaiter_stack_trace);
Pause(&event);
HandleSteppingRequest(); // we may get a rewind request
ClearCachedStackTraces();
}
// Helper that refines the resolved token pos.
//
// If |requested_column| is |-1|, then |exact_token_pos| must be
// |TokenPosition::kNoSource|.
static void RefineBreakpointPos(const Script& script,
TokenPosition pos,
TokenPosition next_closest_token_position,
TokenPosition requested_token_pos,
TokenPosition last_token_pos,
intptr_t requested_column,
TokenPosition exact_token_pos,
TokenPosition* best_fit_pos,
intptr_t* best_column,
intptr_t* best_line) {
ASSERT(
(requested_column == -1 && exact_token_pos == TokenPosition::kNoSource) ||
requested_column > -1);
intptr_t token_start_column = -1;
intptr_t token_line = -1;
if (requested_column >= 0) {
TokenPosition ignored = TokenPosition::kNoSource;
TokenPosition end_of_line_pos = TokenPosition::kNoSource;
script.GetTokenLocation(pos, &token_line, &token_start_column);
script.TokenRangeAtLine(token_line, &ignored, &end_of_line_pos);
TokenPosition token_end_pos = TokenPosition::Min(
TokenPosition::Deserialize(next_closest_token_position.Pos() - 1),
end_of_line_pos);
if ((token_end_pos.IsReal() && exact_token_pos.IsReal() &&
(token_end_pos < exact_token_pos)) ||
(token_start_column > *best_column)) {
// We prefer the token with the lowest column number compatible with the
// requested column. The current token under consideration either ends
// before the requested column, and is thus incompatible with it, or
// has a higher column number than the best token we've found so far, so
// we reject the current token under consideration.
return;
}
}
// Prefer the lowest (first) token pos.
if (pos < *best_fit_pos) {
*best_fit_pos = pos;
*best_line = token_line;
*best_column = token_start_column;
}
}
// Returns the best fit token position for a breakpoint.
//
// Takes a range of tokens [requested_token_pos, last_token_pos] and
// an optional column (requested_column). The range of tokens usually
// represents one line of the program text, but can represent a larger
// range on recursive calls.
//
// If |requested_column| is |-1|, then |exact_token_pos| must be
// |TokenPosition::kNoSource|.
//
// The best fit is found in two passes.
//
// The first pass finds a candidate token which:
//
// - is a safepoint,
// - has the lowest column number compatible with the requested column
// if a column has been specified,
// and:
// - has the lowest token position number which satisfies the above.
//
// When we consider a column number, we look for the token which
// intersects the desired column. For example:
//
// 1 2 3
// 12345678901234567890 0
//
// var x = function(function(y));
// ^
//
// If we request a breakpoint at column 14, the lowest column number
// compatible with that would for column 11 (beginning of the
// 'function' token) in the example above.
//
// Once this candidate token from the first pass is found, we then
// have a second pass which considers only those tokens on the same
// line as the candidate token.
//
// The second pass finds a best fit token which:
//
// - is a safepoint,
// - has the same column number as the candidate token (perhaps
// more than one token has the same column number),
// and:
// - has the lowest code address in the generated code.
//
// We prefer the lowest compiled code address, because this tends to
// select the first subexpression on a line. For example in a line
// with nested function calls f(g(x)), the call to g() will have a
// lower compiled code address than the call to f().
//
// If no best fit token can be found, the search is expanded,
// searching through the rest of the current function by calling this
// function recursively.
//
// TODO(turnidge): Given that we usually call this function with a
// token range restricted to a single line, this could be a one-pass
// algorithm, which would be simpler. I believe that it only needs
// two passes to support the recursive try-the-whole-function case.
// Rewrite this later, once there are more tests in place.
static TokenPosition ResolveBreakpointPos(const Function& func,
TokenPosition requested_token_pos,
TokenPosition last_token_pos,
intptr_t requested_column,
TokenPosition exact_token_pos) {
ASSERT(!func.HasOptimizedCode());
ASSERT(
(requested_column == -1 && exact_token_pos == TokenPosition::kNoSource) ||
requested_column > -1);
requested_token_pos =
TokenPosition::Max(requested_token_pos, func.token_pos());
last_token_pos = TokenPosition::Min(last_token_pos, func.end_token_pos());
Zone* zone = Thread::Current()->zone();
Script& script = Script::Handle(zone, func.script());
PcDescriptors& desc = PcDescriptors::Handle(zone);
#if defined(DART_DYNAMIC_MODULES)
auto& bytecode = Bytecode::Handle(zone);
#endif
if (func.HasBytecode()) {
#if defined(DART_DYNAMIC_MODULES)
bytecode = func.GetBytecode();
ASSERT(!bytecode.IsNull());
if (!bytecode.HasSourcePositions()) {
return TokenPosition::kNoSource;
}
#else
UNREACHABLE();
#endif
} else {
ASSERT(func.HasCode());
const auto& code = Code::Handle(zone, func.unoptimized_code());
ASSERT(!code.IsNull());
desc = code.pc_descriptors();
}
// First pass: find the safe point which is closest to the beginning
// of the given token range.
TokenPosition best_fit_pos = TokenPosition::kMaxSource;
intptr_t best_column = INT_MAX;
intptr_t best_line = INT_MAX;
if (func.HasBytecode()) {
#if defined(DART_DYNAMIC_MODULES)
// Only compiled code has synthetic token positions.
ASSERT(!requested_token_pos.IsSynthetic());
bytecode::BytecodeSourcePositionsIterator iter(zone, bytecode);
while (iter.MoveNext()) {
const TokenPosition& pos = iter.TokenPos();
if (!pos.IsWithin(requested_token_pos, last_token_pos)) {
// Token is not in the target range.
continue;
}
TokenPosition next_closest_token_position = TokenPosition::kMaxSource;
if (requested_column >= 0) {
// Find next closest emitted source position.
bytecode::BytecodeSourcePositionsIterator iter2(zone, bytecode);
while (iter2.MoveNext()) {
const TokenPosition& next = iter2.TokenPos();
if (!next.IsReal()) continue;
if ((pos < next) && (next < next_closest_token_position)) {
next_closest_token_position = next;
}
}
}
RefineBreakpointPos(script, pos, next_closest_token_position,
requested_token_pos, last_token_pos, requested_column,
exact_token_pos, &best_fit_pos, &best_column,
&best_line);
}
#endif
} else {
PcDescriptors::Iterator iter(desc, kSafepointKind);
while (iter.MoveNext()) {
const TokenPosition& pos = iter.TokenPos();
if (pos.IsSynthetic() && pos == requested_token_pos) {
// if there's a safepoint for a synthetic function start and the start
// was requested, we're done.
return pos;
}
if (!pos.IsWithin(requested_token_pos, last_token_pos)) {
// Token is not in the target range.
continue;
}
TokenPosition next_closest_token_position = TokenPosition::kMaxSource;
if (requested_column >= 0) {
// Find next closest safepoint
PcDescriptors::Iterator iter2(desc, kSafepointKind);
while (iter2.MoveNext()) {
const TokenPosition& next = iter2.TokenPos();
if (!next.IsReal()) continue;
if ((pos < next) && (next < next_closest_token_position)) {
next_closest_token_position = next;
}
}
}
RefineBreakpointPos(script, pos, next_closest_token_position,
requested_token_pos, last_token_pos, requested_column,
exact_token_pos, &best_fit_pos, &best_column,
&best_line);
}
}
// Second pass (if we found a safe point in the first pass). Find
// the token on the line which is at the best fit column (if column
// was specified) and has the lowest code address.
if (best_fit_pos != TokenPosition::kMaxSource) {
ASSERT(best_fit_pos.IsReal());
const Script& script = Script::Handle(zone, func.script());
const TokenPosition begin_pos = best_fit_pos;
TokenPosition end_of_line_pos = TokenPosition::kNoSource;
if (best_line < 0) {
script.GetTokenLocation(begin_pos, &best_line);
}
ASSERT(best_line > 0);
TokenPosition ignored = TokenPosition::kNoSource;
script.TokenRangeAtLine(best_line, &ignored, &end_of_line_pos);
end_of_line_pos = TokenPosition::Max(end_of_line_pos, begin_pos);
uword lowest_pc_offset = kUwordMax;
if (func.HasBytecode()) {
#if defined(DART_DYNAMIC_MODULES)
bytecode::BytecodeSourcePositionsIterator iter(zone, bytecode);
while (iter.MoveNext()) {
const TokenPosition& pos = iter.TokenPos();
uword pc_offset = iter.PcOffset();
if (requested_column >= 0) {
if (pos != best_fit_pos) {
// Not an match for the requested column.
continue;
}
} else if (!pos.IsWithin(begin_pos, end_of_line_pos)) {
// Token is not on same line as best fit.
continue;
}
// Prefer the lowest pc offset.
if (pc_offset < lowest_pc_offset) {
lowest_pc_offset = pc_offset;
best_fit_pos = pos;
}
}
#endif
} else {
PcDescriptors::Iterator iter(desc, kSafepointKind);
while (iter.MoveNext()) {
const TokenPosition& pos = iter.TokenPos();
if (requested_column >= 0) {
if (pos != best_fit_pos) {
// Not an match for the requested column.
continue;
}
} else if (!pos.IsWithin(begin_pos, end_of_line_pos)) {
// Token is not on same line as best fit.
continue;
}
// Prefer the lowest pc offset.
if (iter.PcOffset() < lowest_pc_offset) {
lowest_pc_offset = iter.PcOffset();
best_fit_pos = pos;
}
}
}
return best_fit_pos;
}
return TokenPosition::kNoSource;
}
bool BreakpointLocation::EnsureIsResolved(const Function& target_function,
TokenPosition exact_token_pos) {
if (IsResolved()) {
return true;
}
// Resolve source breakpoint in the newly compiled function.
TokenPosition resolved_pos =
ResolveBreakpointPos(target_function, token_pos(), end_token_pos(),
requested_column_number(), exact_token_pos);
if (!resolved_pos.IsDebugPause()) {
if (FLAG_verbose_debug) {
OS::PrintErr("Failed resolving breakpoint for function '%s'\n",
target_function.ToFullyQualifiedCString());
}
return false;
}
TokenPosition requested_pos = token_pos();
TokenPosition requested_end_pos = end_token_pos();
SetResolved(target_function, resolved_pos);
Breakpoint* breakpoint = breakpoints();
while (breakpoint != nullptr) {
if (FLAG_verbose_debug) {
OS::PrintErr("Resolved breakpoint %" Pd
" to pos %s, function '%s' (requested range %s-%s, "
"requested col %" Pd ")\n",
breakpoint->id(), token_pos().ToCString(),
target_function.ToFullyQualifiedCString(),
requested_pos.ToCString(), requested_end_pos.ToCString(),
requested_column_number());
}
debugger()->SendBreakpointEvent(ServiceEvent::kBreakpointResolved,
breakpoint);
breakpoint = breakpoint->next();
}
return true;
}
void GroupDebugger::MakeCodeBreakpointAtUnsafe(Thread* thread,
const Function& func,
BreakpointLocation* loc) {
DEBUG_ASSERT(thread->IsInStoppedMutatorsScope() ||
code_breakpoints_lock()->IsCurrentThreadWriter());
ASSERT(loc->token_pos().IsReal());
ASSERT((loc != nullptr) && loc->IsResolved());
auto* zone = thread->zone();
auto& code = Code::Handle(zone);
auto& bytecode = Bytecode::Handle(zone);
uword start = 0;
uword lowest_pc_offset = kUwordMax;
UntaggedPcDescriptors::Kind lowest_kind = UntaggedPcDescriptors::kAnyKind;
// Find the safe point with the lowest compiled code address
// that maps to the token position of the source breakpoint.
if (func.HasBytecode()) {
#if defined(DART_DYNAMIC_MODULES)
bytecode = func.GetBytecode();
ASSERT(!bytecode.IsNull());
if (!bytecode.HasSourcePositions()) {
return;
}
start = bytecode.PayloadStart();
bytecode::BytecodeSourcePositionsIterator iter(zone, bytecode);
while (iter.MoveNext()) {
if (iter.TokenPos() == loc->token_pos_) {
// Breakpoints are set and located using the address of the instruction
// following the breakpoint instruction, since frames contain
// return addresses.
const uword pc_offset =
KernelBytecode::Next(start + iter.PcOffset()) - start;
if (pc_offset < lowest_pc_offset) {
lowest_pc_offset = pc_offset;
}
}
}
#else
UNREACHABLE();
#endif
} else {
ASSERT(func.HasCode());
ASSERT(!func.HasOptimizedCode());
code = func.unoptimized_code();
ASSERT(!code.IsNull());
start = code.PayloadStart();
const auto& desc = PcDescriptors::Handle(zone, code.pc_descriptors());
PcDescriptors::Iterator iter(desc, kSafepointKind);
while (iter.MoveNext()) {
if (iter.TokenPos() == loc->token_pos_) {
if (iter.PcOffset() < lowest_pc_offset) {
lowest_pc_offset = iter.PcOffset();
lowest_kind = iter.Kind();
}
}
}
}
if (lowest_pc_offset == kUwordMax) {
return;
}
uword lowest_pc = start + lowest_pc_offset;
CodeBreakpoint* code_bpt = GetCodeBreakpoint(lowest_pc);
if (code_bpt == nullptr) {
// No code breakpoint for this code exists; create one.
if (!bytecode.IsNull()) {
code_bpt = new CodeBreakpoint(bytecode, loc, lowest_pc);
} else {
code_bpt = new CodeBreakpoint(code, loc, lowest_pc, lowest_kind);
}
if (FLAG_verbose_debug) {
OS::PrintErr("Setting code breakpoint at pos %s pc %#" Px " offset %#" Px
"\n",
loc->token_pos().ToCString(), lowest_pc, lowest_pc - start);
}
RegisterCodeBreakpoint(code_bpt);
} else {
if (FLAG_verbose_debug) {
OS::PrintErr(
"Adding location to existing code breakpoint at pos %s pc %#" Px
" offset %#" Px "\n",
loc->token_pos().ToCString(), lowest_pc, lowest_pc - start);
}
if (!code_bpt->HasBreakpointLocation(loc)) {
code_bpt->AddBreakpointLocation(loc);
}
}
if (loc->AnyEnabled()) {
code_bpt->Enable();
}
}
void GroupDebugger::MakeCodeBreakpointAt(const Function& func,
BreakpointLocation* loc) {
auto thread = Thread::Current();
if (thread->IsInStoppedMutatorsScope()) {
MakeCodeBreakpointAtUnsafe(thread, func, loc);
} else {
SafepointWriteRwLocker sl(thread, code_breakpoints_lock());
MakeCodeBreakpointAtUnsafe(thread, func, loc);
}
}
ErrorPtr Debugger::FindAndCompileMatchingFunctions(
const GrowableHandlePtrArray<const Script>& scripts,
TokenPosition start_pos,
TokenPosition end_pos,
GrowableObjectArray& code_function_list) const {
auto thread = Thread::Current();
auto zone = thread->zone();
Script& script = Script::Handle(zone);
Object& ensure_has_code_result = Object::Handle(zone);
for (intptr_t i = 0; i < scripts.length(); ++i) {
script = scripts.At(i).ptr();
ClosureFunctionsCache::ForAllClosureFunctions(
[&](const Function& function) {
ASSERT(!function.IsNull());
if ((function.token_pos() == start_pos) &&
(function.end_token_pos() == end_pos) &&
(function.script() == script.ptr()) && function.is_debuggable()) {
// If we've found a matching function, ensure it's compiled so
// the breakpoint currently being set can be resolved immediately.
ensure_has_code_result = function.EnsureHasCodeNoThrow();
if (ensure_has_code_result.IsError()) {
return false; // Stop iterating.
}
code_function_list.Add(function);
ASSERT(!function.HasImplicitClosureFunction());
}
return true; // Continue iterating.
});
if (ensure_has_code_result.IsError()) {
return Error::Cast(ensure_has_code_result).ptr();
}
Class& cls = Class::Handle(zone);
Array& functions = Array::Handle(zone);
Function& function = Function::Handle(zone);
Array& fields = Array::Handle(zone);
Field& field = Field::Handle(zone);
const ClassTable& class_table = *isolate_->group()->class_table();
const intptr_t num_classes = class_table.NumCids();
const intptr_t num_tlc_classes = class_table.NumTopLevelCids();
for (intptr_t i = 1; i < num_classes + num_tlc_classes; i++) {
const intptr_t cid =
i < num_classes ? i
: ClassTable::CidFromTopLevelIndex(i - num_classes);
if (class_table.HasValidClassAt(cid)) {
cls = class_table.At(cid);
// If the class is not finalized, e.g. if it hasn't been parsed
// yet entirely, we can ignore it. If it contains a function with
// an unresolved breakpoint, we will detect it if and when the
// function gets compiled.
if (!cls.is_finalized()) {
continue;
}
// Note: we need to check the functions of this class even if
// the class is defined in a different 'script'. There could
// be mixin functions from the given script in this class.
functions = cls.current_functions();
if (!functions.IsNull()) {
const intptr_t num_functions = functions.Length();
for (intptr_t pos = 0; pos < num_functions; pos++) {
function ^= functions.At(pos);
ASSERT(!function.IsNull());
bool function_added = false;
if (function.is_debuggable() && function.token_pos() == start_pos &&
function.end_token_pos() == end_pos &&
function.script() == script.ptr()) {
// If we've found a matching function, ensure it's compiled so
// the breakpoint currently being set can be resolved immediately.
ensure_has_code_result = function.EnsureHasCodeNoThrow();
if (ensure_has_code_result.IsError()) {
return Error::Cast(ensure_has_code_result).ptr();
}
code_function_list.Add(function);
function_added = true;
}
if (function_added && function.HasImplicitClosureFunction()) {
function = function.ImplicitClosureFunction();
if (function.is_debuggable()) {
// Ensure that the implicit closure function is compiled so the
// breakpoint currently being set can be resolved immediately.
ensure_has_code_result = function.EnsureHasCodeNoThrow();
if (ensure_has_code_result.IsError()) {
return Error::Cast(ensure_has_code_result).ptr();
}
code_function_list.Add(function);
}
}
}
}
fields = cls.fields();
if (!fields.IsNull()) {
const intptr_t num_fields = fields.Length();
for (intptr_t pos = 0; pos < num_fields; pos++) {
field ^= fields.At(pos);
ASSERT(!field.IsNull());
if (field.Script() != script.ptr()) {
continue;
}
if (!field.has_nontrivial_initializer()) {
continue;
}
function = field.EnsureInitializerFunction();
ASSERT(!function.IsNull());
if (function.is_debuggable() && function.HasCode() &&
function.token_pos() == start_pos &&
function.end_token_pos() == end_pos &&
function.script() == script.ptr()) {
ensure_has_code_result = function.EnsureHasCodeNoThrow();
if (ensure_has_code_result.IsError()) {
return Error::Cast(ensure_has_code_result).ptr();
}
code_function_list.Add(function);
}
}
}
}
}
}
return Error::null();
}
static void UpdateBestFit(Function* best_fit, const Function& func) {
if (best_fit->IsNull()) {
*best_fit = func.ptr();
} else if ((best_fit->token_pos().IsSynthetic() ||
func.token_pos().IsSynthetic() ||
(best_fit->token_pos() < func.token_pos())) &&
(func.end_token_pos() <= best_fit->end_token_pos())) {
*best_fit = func.ptr();
}
}
// If a best fit function is found, stores that function in |best_fit| and
// returns |true|.
// Note that in some cases, there may be a closure that is a better fit that the
// function returned in |best_fit|, because |FindBestFit| is only able to detect
// that closure if the function containing it has already been compiled.
bool Debugger::FindBestFit(const Script& script,
TokenPosition token_pos,
TokenPosition last_token_pos,
Function* best_fit) {
auto thread = Thread::Current();
auto isolate_group = thread->isolate_group();
Zone* zone = thread->zone();
Class& cls = Class::Handle(zone);
// A single script can belong to several libraries because of mixins.
// Go through all libraries and for each that contains the script, try to find
// a fit there.
// Return the first fit found, but if a library doesn't contain a fit,
// process the next one.
const GrowableObjectArray& libs = GrowableObjectArray::Handle(
zone, isolate_group->object_store()->libraries());
Library& lib = Library::Handle(zone);
for (int i = 0; i < libs.Length(); i++) {
lib ^= libs.At(i);
ASSERT(!lib.IsNull());
const Array& scripts = Array::Handle(zone, lib.LoadedScripts());
bool lib_has_script = false;
for (intptr_t j = 0; j < scripts.Length(); j++) {
if (scripts.At(j) == script.ptr()) {
lib_has_script = true;
break;
}
}
if (!lib_has_script) {
continue;
}
if (!lib.IsDebuggable()) {
if (FLAG_verbose_debug) {
OS::PrintErr("Library '%s' has been marked as non-debuggable\n",
lib.ToCString());
}
continue;
}
const String& script_url = String::Handle(zone, script.url());
ClosureFunctionsCache::ForAllClosureFunctions([&](const Function& fun) {
if (fun.script() == script.ptr() &&
FunctionOverlaps(fun, script_url, token_pos, last_token_pos)) {
// Select the inner most closure.
UpdateBestFit(best_fit, fun);
}
return true; // Continue iteration
});
if (!best_fit->IsNull()) {
// The inner most closure found will be the best fit. Going
// over class functions below will not help in any further
// narrowing.
return true;
}
Array& functions = Array::Handle(zone);
Function& function = Function::Handle(zone);
Array& fields = Array::Handle(zone);
Field& field = Field::Handle(zone);
Error& error = Error::Handle(zone);
const ClassTable& class_table = *isolate_->group()->class_table();
const intptr_t num_classes = class_table.NumCids();
const intptr_t num_tlc_classes = class_table.NumTopLevelCids();
for (intptr_t i = 1; i < num_classes + num_tlc_classes; i++) {
const intptr_t cid =
i < num_classes ? i
: ClassTable::CidFromTopLevelIndex(i - num_classes);
if (!class_table.HasValidClassAt(cid)) {
continue;
}
cls = class_table.At(cid);
// This class is relevant to us only if it belongs to the
// library to which |script| belongs.
if (cls.library() != lib.ptr()) {
continue;
}
// Parse class definition if not done yet.
error = cls.EnsureIsFinalized(Thread::Current());
if (!error.IsNull()) {
// Ignore functions in this class.
// TODO(hausner): Should we propagate this error? How?
// EnsureIsFinalized only returns an error object if there
// is no longjump base on the stack.
continue;
}
functions = cls.current_functions();
if (!functions.IsNull()) {
const intptr_t num_functions = functions.Length();
for (intptr_t pos = 0; pos < num_functions; pos++) {
function ^= functions.At(pos);
ASSERT(!function.IsNull());
if (IsImplicitFunction(function) || function.is_synthetic() ||
!function.token_pos().IsReal() ||
!function.end_token_pos().IsReal() || !function.is_debuggable()) {
// We skip implicit functions and synthetic functions because they
// do not have user specifiable source locations. We also skip
// functions marked as undebuggable.
continue;
}
if (FunctionOverlaps(function, script_url, token_pos,
last_token_pos)) {
// Closures and inner functions within a class method are not
// present in the functions of a class. Hence, we can return
// right away as looking through other functions of a class
// will not narrow down to any inner function/closure.
*best_fit = function.ptr();
return true;
}
}
}
// If none of the functions in the class contain token_pos, then we check
// if it falls within a function literal initializer of a field.
fields = cls.fields();
if (!fields.IsNull()) {
const intptr_t num_fields = fields.Length();
for (intptr_t pos = 0; pos < num_fields; pos++) {
TokenPosition start = TokenPosition::kNoSource;
TokenPosition end = TokenPosition::kNoSource;
field ^= fields.At(pos);
ASSERT(!field.IsNull());
if (field.Script() != script.ptr()) {
// The field should be defined in the script we want to set
// the breakpoint in.
continue;
}
if (!field.has_nontrivial_initializer()) {
continue;
}
start = field.token_pos();
end = field.end_token_pos();
if (token_pos.IsWithin(start, end) ||
start.IsWithin(token_pos, last_token_pos)) {
*best_fit = field.EnsureInitializerFunction();
return true;
}
}
}
}
}
return false;
}
// If |requested_column| is |-1|, then |exact_token_pos| must be
// |TokenPosition::kNoSource|.
BreakpointLocation* Debugger::SetCodeBreakpoints(
const GrowableHandlePtrArray<const Script>& scripts,
TokenPosition token_pos,
TokenPosition last_token_pos,
intptr_t requested_line,
intptr_t requested_column,
TokenPosition exact_token_pos,
const GrowableObjectArray& functions) {
ASSERT(
(requested_column == -1 && exact_token_pos == TokenPosition::kNoSource) ||
requested_column > -1);
Function& function = Function::Handle();
function ^= functions.At(0);
TokenPosition breakpoint_pos = ResolveBreakpointPos(
function, token_pos, last_token_pos, requested_column, exact_token_pos);
if (!breakpoint_pos.IsReal()) {
return nullptr;
}
const String& script_url = String::Handle(scripts.At(0).url());
BreakpointLocation* loc =
GetResolvedBreakpointLocation(script_url, breakpoint_pos);
if (loc == nullptr) {
// Find an existing unresolved breakpoint location.
loc = GetBreakpointLocation(script_url, token_pos, requested_line,
requested_column);
}
if (loc == nullptr) {
loc = new BreakpointLocation(this, scripts, breakpoint_pos, breakpoint_pos,
requested_line, requested_column);
RegisterBreakpointLocation(loc);
}
// A source breakpoint for this location may already exists, but it may
// not yet be resolved in code.
if (loc->IsResolved()) {
return loc;
}
loc->SetResolved(function, breakpoint_pos);
// Create code breakpoints for all compiled functions we found.
Function& func = Function::Handle();
const intptr_t num_functions = functions.Length();
for (intptr_t i = 0; i < num_functions; i++) {
func ^= functions.At(i);
ASSERT(func.HasCode());
group_debugger()->MakeCodeBreakpointAt(func, loc);
}
if (FLAG_verbose_debug) {
intptr_t line_number = -1;
intptr_t column_number = -1;
scripts.At(0).GetTokenLocation(breakpoint_pos, &line_number,
&column_number);
OS::PrintErr("Resolved code breakpoint for function '%s' at line %" Pd
" col %" Pd "\n",
func.ToFullyQualifiedCString(), line_number, column_number);
}
return loc;
}
#if !defined(DART_PRECOMPILED_RUNTIME)
static TokenPosition FindExactTokenPosition(const Script& script,
TokenPosition start_of_line,
intptr_t column_number);
#endif // !defined(DART_PRECOMPILED_RUNTIME)
ErrorPtr Debugger::SetBreakpoint(
const Script& script,
TokenPosition token_pos,
TokenPosition last_token_pos,
intptr_t requested_line,
intptr_t requested_column,
const Function& function,
BreakpointLocation** result_breakpoint_location) {
GrowableHandlePtrArray<const Script> scripts(Thread::Current()->zone(), 1);
scripts.Add(script);
return SetBreakpoint(scripts, token_pos, last_token_pos, requested_line,
requested_column, function, result_breakpoint_location);
}
ErrorPtr Debugger::SetBreakpoint(
const GrowableHandlePtrArray<const Script>& scripts,
TokenPosition token_pos,
TokenPosition last_token_pos,
intptr_t requested_line,
intptr_t requested_column,
const Function& function,
BreakpointLocation** result_breakpoint_location) {
ASSERT(scripts.length() > 0);
ASSERT(result_breakpoint_location != nullptr);
Function& func = Function::Handle();
const Script& script = scripts.At(0);
if (function.IsNull()) {
if (!FindBestFit(script, token_pos, last_token_pos, &func)) {
return Object::no_debuggable_code_error().ptr();
}
ASSERT(!func.IsNull());
// There may be closures that were not considered by the first call to
// |FindBestFit| because the functions containing them were not yet
// compiled. So, we must recursively compile functions until we converge
// at a true best fit function.
Function& tmp = Function::Handle();
Object& ensure_has_code_result = Object::Handle();
do {
ensure_has_code_result = func.EnsureHasCodeNoThrow();
if (ensure_has_code_result.IsError()) {
return Error::Cast(ensure_has_code_result).ptr();
}
tmp = func.ptr();
FindBestFit(script, token_pos, last_token_pos, &func);
ASSERT(!func.IsNull());
} while (tmp.ptr() != func.ptr());
} else {
func = function.ptr();
if (!func.token_pos().IsReal()) {
// Missing source positions?
return Object::no_debuggable_code_error().ptr();
}
}
if (!func.IsNull()) {
// There may be more than one function object for a given function in source
// code. There may be implicit closure functions, and there may be copies of
// mixin functions. Compile and collect all functions whose source code
// range matches exactly the best fit function we found. We compile all of
// theses functions eagerly so that the breakpoint currently being set can
// be resolved immediately.
GrowableObjectArray& code_functions =
GrowableObjectArray::Handle(GrowableObjectArray::New());
const Error& error = Error::Handle(FindAndCompileMatchingFunctions(
scripts, func.token_pos(), func.end_token_pos(), code_functions));
if (!error.IsNull()) {
return error.ptr();
}
if (code_functions.Length() > 0) {
// One or more function object containing this breakpoint location
// have already been compiled. We can resolve the breakpoint now.
// If requested_column is larger than zero, [token_pos, last_token_pos]
// governs one single line of code.
TokenPosition exact_token_pos = TokenPosition::kNoSource;
#if !defined(DART_PRECOMPILED_RUNTIME)
if (token_pos != last_token_pos && requested_column >= 0) {
exact_token_pos =
FindExactTokenPosition(script, token_pos, requested_column);
}
#endif // !defined(DART_PRECOMPILED_RUNTIME)
BreakpointLocation* loc = nullptr;
// Ensure that code stays deoptimized (and background compiler disabled)
// until we have installed the breakpoint (at which point the compiler
// will not try to optimize it anymore).
RunWithStoppedDeoptimizedWorld([&] {
loc = SetCodeBreakpoints(scripts, token_pos, last_token_pos,
requested_line, requested_column,
exact_token_pos, code_functions);
});
if (loc != nullptr) {
*result_breakpoint_location = loc;
return Error::null();
}
}
}
// There is no debuggable code at all at |token_pos|, so we leave
// |*result_breakpoint_location| as null.
return Error::null();
}
// Synchronize the enabled/disabled state of all code breakpoints
// associated with the breakpoint location loc.
void GroupDebugger::SyncBreakpointLocation(BreakpointLocation* loc) {
bool any_enabled = loc->AnyEnabled();
SafepointWriteRwLocker sl(Thread::Current(), code_breakpoints_lock());
CodeBreakpoint* cbpt = code_breakpoints_;
while (cbpt != nullptr) {
if (cbpt->HasBreakpointLocation(loc)) {
if (any_enabled) {
cbpt->Enable();
} else {
cbpt->Disable();
}
}
cbpt = cbpt->next();
}
}
ErrorPtr Debugger::SetBreakpointAtEntry(const Function& target_function,
bool single_shot,
Breakpoint** result_breakpoint) {
ASSERT(!target_function.IsNull());
ASSERT(result_breakpoint != nullptr);
if (!target_function.is_debuggable()) {
return Object::no_debuggable_code_error().ptr();
}
const Script& script = Script::Handle(target_function.script());
BreakpointLocation* bpt_location = nullptr;
const Error& error = Error::Handle(SetBreakpoint(
script, target_function.token_pos(), target_function.end_token_pos(), -1,
-1 /* no requested line/col */, target_function, &bpt_location));
if (!error.IsNull()) {
return error.ptr();
}
ASSERT(bpt_location != nullptr);
if (single_shot) {
*result_breakpoint = bpt_location->AddSingleShot(this);
} else {
*result_breakpoint = bpt_location->AddRepeated(this);
}
return Error::null();
}
ErrorPtr Debugger::SetBreakpointAtActivation(const Instance& closure,
bool single_shot,
Breakpoint** result_breakpoint) {
ASSERT(result_breakpoint != nullptr);
if (!closure.IsClosure()) {
return Object::no_debuggable_code_error().ptr();
}
const Function& func = Function::Handle(Closure::Cast(closure).function());
const Script& script = Script::Handle(func.script());
BreakpointLocation* bpt_location = nullptr;
const Error& error = Error::Handle(
SetBreakpoint(script, func.token_pos(), func.end_token_pos(), -1,
-1 /* no line/col */, func, &bpt_location));
if (!error.IsNull()) {
return error.ptr();
}
ASSERT(bpt_location != nullptr);
*result_breakpoint =
bpt_location->AddBreakpoint(this, Closure::Cast(closure), single_shot);
return Error::null();
}
Breakpoint* Debugger::BreakpointAtActivation(const Instance& closure) {
if (!closure.IsClosure()) {
return nullptr;
}
BreakpointLocation* loc = breakpoint_locations_;
while (loc != nullptr) {
Breakpoint* bpt = loc->breakpoints();
while (bpt != nullptr) {
if (closure.ptr() == bpt->closure()) {
return bpt;
}
bpt = bpt->next();
}
loc = loc->next();
}
return nullptr;
}
void Debugger::SetBreakpointAtResumption(const Object& function_data) {
ASSERT(!function_data.IsNull());
ASSERT(function_data.IsInstance());
breakpoints_at_resumption_.Add(function_data.ptr());
isolate_->set_has_resumption_breakpoints(true);
}
void Debugger::ResumptionBreakpoint() {
ASSERT(!breakpoints_at_resumption_.is_empty());
ASSERT(isolate_->has_resumption_breakpoints());
ActivationFrame* top_frame = TopDartFrame();
ASSERT(top_frame->function().IsSuspendableFunction());
const auto& function_data =
Object::Handle(top_frame->GetSuspendableFunctionData());
for (intptr_t i = 0, n = breakpoints_at_resumption_.length(); i < n; ++i) {
if (breakpoints_at_resumption_[i] == function_data.ptr()) {
breakpoints_at_resumption_.RemoveAt(i);
if (breakpoints_at_resumption_.is_empty()) {
isolate_->set_has_resumption_breakpoints(false);
}
if (FLAG_verbose_debug) {
OS::PrintErr(
"ResumptionBreakpoint - hit a breakpoint, continue single "
"stepping\n");
}
// Resumption breakpoints are for stepping out of await expressions,
// so record its information to avoid pausing within it.
SetLastSteppingInformation(top_frame);
EnterSingleStepMode();
return;
}
}
}
ErrorPtr Debugger::SetBreakpointAtLineCol(const String& script_url,
intptr_t line_number,
intptr_t column_number,
Breakpoint** result_breakpoint) {
// Prevent future tests from calling this function in the wrong
// execution state. If you hit this assert, consider using
// Dart_SetBreakpoint instead.
ASSERT(Thread::Current()->execution_state() == Thread::kThreadInVM);
ASSERT(result_breakpoint != nullptr);
BreakpointLocation* loc = nullptr;
const Error& error = Error::Handle(BreakpointLocationAtLineCol(
script_url, line_number, column_number, &loc));
if (!error.IsNull()) {
return error.ptr();
}
ASSERT(loc != nullptr);
*result_breakpoint = loc->AddRepeated(this);
return Error::null();
}
ErrorPtr Debugger::BreakpointLocationAtLineCol(
const String& script_url,
intptr_t line_number,
intptr_t column_number,
BreakpointLocation** result_breakpoint_location) {
ASSERT(result_breakpoint_location != nullptr);
Zone* zone = Thread::Current()->zone();
Library& lib = Library::Handle(zone);
GrowableHandlePtrArray<const Script> scripts(zone, 1);
const GrowableObjectArray& libs = GrowableObjectArray::Handle(
isolate_->group()->object_store()->libraries());
bool is_package = script_url.StartsWith(Symbols::PackageScheme());
bool is_dart_colon = script_url.StartsWith(Symbols::DartScheme());
Script& script_for_lib = Script::Handle(zone);
for (intptr_t i = 0; i < libs.Length(); i++) {
lib ^= libs.At(i);
// Ensure that all top-level members are loaded so their scripts
// are available for look up. When certain script only contains
// top level functions, scripts could still be loaded correctly.
lib.EnsureTopLevelClassIsFinalized();
bool useResolvedUri = !is_package && !is_dart_colon;
script_for_lib = lib.LookupScript(script_url, useResolvedUri);
if (!script_for_lib.IsNull()) {
scripts.Add(script_for_lib);
}
}
if (scripts.length() == 0) {
// No script found with given url. Create a latent breakpoint which
// will be set if the url is loaded later.
BreakpointLocation* latent_bpt =
GetLatentBreakpoint(script_url, line_number, column_number);
if (FLAG_verbose_debug) {
OS::PrintErr(
"Set latent breakpoint in url '%s' at "
"line %" Pd " col %" Pd "\n",
script_url.ToCString(), line_number, column_number);
}
*result_breakpoint_location = latent_bpt;
return Error::null();
}
TokenPosition first_token_idx = TokenPosition::kNoSource;
TokenPosition last_token_idx = TokenPosition::kNoSource;
// Assume all scripts with the same URL have the same token positions.
scripts.At(0).TokenRangeAtLine(line_number, &first_token_idx,
&last_token_idx);
if (!first_token_idx.IsReal()) {
// Script does not contain the given line number.
if (FLAG_verbose_debug) {
OS::PrintErr("Script '%s' does not contain line number %" Pd "\n",
script_url.ToCString(), line_number);
}
return Object::no_debuggable_code_error().ptr();
} else if (!last_token_idx.IsReal()) {
// Line does not contain any tokens.
if (FLAG_verbose_debug) {
OS::PrintErr("No executable code at line %" Pd " in '%s'\n", line_number,
script_url.ToCString());
}
return Object::no_debuggable_code_error().ptr();
}
ASSERT(first_token_idx <= last_token_idx);
Error& error = Error::Handle();
while ((*result_breakpoint_location == nullptr) &&
(first_token_idx <= last_token_idx)) {
error = SetBreakpoint(scripts, first_token_idx, last_token_idx, line_number,
column_number, Function::Handle(),
result_breakpoint_location);
if (!error.IsNull() &&
error.ptr() != Object::no_debuggable_code_error().ptr()) {
// We do not return an error immediately if |SetBreakpoint| returns
// |Object::no_debuggable_code_error().ptr()|, because there is a chance
// that |SetBreakpoint| will succeed if called again after
// |first_token_idx| is advanced.
return error.ptr();
}
first_token_idx = first_token_idx.Next();
}
if (*result_breakpoint_location == nullptr) {
if (FLAG_verbose_debug) {
OS::PrintErr("No executable code at line %" Pd " in '%s'\n", line_number,
script_url.ToCString());
}
return Object::no_debuggable_code_error().ptr();
}
return Error::null();
}
// Return innermost closure contained in 'function' that contains
// the given token position.
static FunctionPtr FindInnermostClosure(Zone* zone,
const Function& function,
TokenPosition token_pos) {
ASSERT(function.end_token_pos().IsReal());
const TokenPosition& func_start = function.token_pos();
const Script& outer_origin = Script::Handle(zone, function.script());
Function& best_fit = Function::Handle(zone);
ClosureFunctionsCache::ForAllClosureFunctions([&](const Function& closure) {
const TokenPosition& closure_start = closure.token_pos();
const TokenPosition& closure_end = closure.end_token_pos();
// We're only interested in closures that have real ending token positions.
// The starting token position can be synthetic.
if (closure_end.IsReal() && (function.end_token_pos() > closure_end) &&
(!closure_start.IsReal() || !func_start.IsReal() ||
(closure_start > func_start)) &&
token_pos.IsWithin(closure_start, closure_end) &&
(closure.script() == outer_origin.ptr())) {
UpdateBestFit(&best_fit, closure);
}
return true; // Continue iteration.
});
return best_fit.ptr();
}
bool GroupDebugger::EnsureLocationIsInFunction(Zone* zone,
const Function& function,
BreakpointLocation* location) {
const String& url = String::Handle(zone, location->url());
if (!FunctionOverlaps(function, url, location->token_pos(),
location->end_token_pos())) {
return false;
}
TokenPosition token_pos = location->token_pos();
#if !defined(DART_PRECOMPILED_RUNTIME)
TokenPosition end_token_pos = location->end_token_pos();
if (token_pos != end_token_pos && location->requested_column_number() >= 0) {
// Narrow down the token position range to a single value
// if requested column number is provided so that inner
// Closure won't be missed.
const Script& script = Script::Handle(location->script());
token_pos = FindExactTokenPosition(script, token_pos,
location->requested_column_number());
}
#endif // !defined(DART_PRECOMPILED_RUNTIME)
const Function& inner_function =
Function::Handle(zone, FindInnermostClosure(zone, function, token_pos));
if (!inner_function.IsNull()) {
if (FLAG_verbose_debug) {
OS::PrintErr(
"Pending breakpoint remains unresolved in "
"inner function '%s'\n",
inner_function.ToFullyQualifiedCString());
}
return false;
}
// There is no local function within function that contains the
// breakpoint token position.
return true;
}
void GroupDebugger::NotifyCompilation(const Function& function) {
if (!function.is_debuggable()) {
return;
}
Function& resolved_function = Function::Handle(function.ptr());
auto thread = Thread::Current();
auto zone = thread->zone();
// Going through BreakpointLocations of all isolates and debuggers looking
// for those that can be resolved and added code breakpoints at now.
//
// The check below is used instead of breakpoint_locations_lock acquisition.
// We don't need to acquire the lock if always run with stopped mutators.
// We can't acquire the lock if we run with stopped mutators as that could
// result in deadlock.
RELEASE_ASSERT(thread->IsInStoppedMutatorsScope());
for (intptr_t i = 0; i < breakpoint_locations_.length(); i++) {
BreakpointLocation* location = breakpoint_locations_.At(i);
if (EnsureLocationIsInFunction(zone, resolved_function, location)) {
// All mutators are stopped (see RELEASE_ASSERT above). We temporarily
// enter the isolate for which the breakpoint was registered.
// The code path below may issue service events which will use the active
// isolate's object-id ring for naming VM objects.
ActiveIsolateScope active_isolate(thread,
location->debugger()->isolate());
// Ensure the location is resolved for the original function.
TokenPosition exact_token_pos = TokenPosition::kNoSource;
#if !defined(DART_PRECOMPILED_RUNTIME)
if (location->token_pos() != location->end_token_pos() &&
location->requested_column_number() >= 0) {
exact_token_pos = FindExactTokenPosition(
Script::Handle(location->script()), location->token_pos(),
location->requested_column_number());
}
#endif // !defined(DART_PRECOMPILED_RUNTIME)
location->EnsureIsResolved(function, exact_token_pos);
if (FLAG_verbose_debug) {
Breakpoint* bpt = location->breakpoints();
while (bpt != nullptr) {
OS::PrintErr("Setting breakpoint %" Pd " for %s '%s'\n", bpt->id(),
function.IsClosureFunction() ? "closure" : "function",
function.ToFullyQualifiedCString());
bpt = bpt->next();
}
}
MakeCodeBreakpointAt(function, location);
}
}
}
void GroupDebugger::VisitObjectPointers(ObjectPointerVisitor* visitor) {
CodeBreakpoint* cbpt = code_breakpoints_;
while (cbpt != nullptr) {
cbpt->VisitObjectPointers(visitor);
cbpt = cbpt->next();
}
}
// static
void Debugger::VisitObjectPointers(ObjectPointerVisitor* visitor) {
ASSERT(visitor != nullptr);
BreakpointLocation* loc = breakpoint_locations_;
while (loc != nullptr) {
loc->VisitObjectPointers(visitor);
loc = loc->next();
}
loc = latent_locations_;
while (loc != nullptr) {
loc->VisitObjectPointers(visitor);
loc = loc->next();
}
for (intptr_t i = 0, n = breakpoints_at_resumption_.length(); i < n; ++i) {
visitor->VisitPointer(&breakpoints_at_resumption_[i]);
}
}
void Debugger::Pause(ServiceEvent* event) {
ASSERT(event->IsPause()); // Should call InvokeEventHandler instead.
ASSERT(!ignore_breakpoints_); // We shouldn't get here when ignoring bpts.
ASSERT(!IsPaused()); // No recursive pausing.
pause_event_ = event;
pause_event_->UpdateTimestamp();
// We are about to invoke the debugger's event handler. Disable
// interrupts for this thread while waiting for debug commands over
// the service protocol.
{
Thread* thread = Thread::Current();
DisableThreadInterruptsScope dtis(thread);
TIMELINE_DURATION(thread, Debugger, "Debugger Pause");
// Send the pause event.
Service::HandleEvent(event);
{
TransitionVMToNative transition(thread);
isolate_->PauseEventHandler();
}
// Notify the service that we have resumed.
const Error& error = Error::Handle(Thread::Current()->sticky_error());
ASSERT(error.IsNull() || error.IsUnwindError() ||
error.IsUnhandledException());
// Only send a resume event when the isolate is not unwinding.
if (!error.IsUnwindError()) {
ServiceEvent resume_event(event->isolate(), ServiceEvent::kResume);
resume_event.set_top_frame(event->top_frame());
Service::HandleEvent(&resume_event);
}
}
group_debugger()->Pause();
pause_event_ = nullptr;
}
void GroupDebugger::Pause() {
SafepointWriteRwLocker sl(Thread::Current(), code_breakpoints_lock());
if (needs_breakpoint_cleanup_) {
RemoveUnlinkedCodeBreakpoints();
}
}
void Debugger::EnterSingleStepMode() {
ResetSteppingFramePointer();
NotifySingleStepping(true);
}
void Debugger::ResetSteppingFramePointer() {
stepping_fp_ = 0;
#if defined(DART_DYNAMIC_MODULES)
stepping_fp_from_interpreted_frame_ = false;
#endif
}
void Debugger::ResetLastSteppingInformation() {
last_stepping_fp_ = 0;
last_stepping_pos_ = TokenPosition::kNoSource;
}
void Debugger::SetLastSteppingInformation(ActivationFrame* frame) {
last_stepping_fp_ = frame->fp();
last_stepping_pos_ = frame->TokenPos();
}
void Debugger::SetLastSteppingInformation(BreakpointLocation* bpt_location) {
// Continue stepping until we reach a token position that is not the one at
// which the breakpoint was set.
last_stepping_fp_ = 0;
last_stepping_pos_ = bpt_location->token_pos();
}
bool Debugger::MatchesLastSteppingInformation(ActivationFrame* frame) {
// No information for the last event emitted was recorded.
if (last_stepping_pos_ == TokenPosition::kNoSource) {
return false;
}
// If a specific FP was recorded, then it must match as well.
if (last_stepping_fp_ != 0 && last_stepping_fp_ != frame->fp()) {
return false;
}
return last_stepping_pos_ == frame->TokenPos();
}
void Debugger::SetSyncSteppingFramePointer(ActivationFrame* frame) {
stepping_fp_ = frame->fp();
#if defined(DART_DYNAMIC_MODULES)
stepping_fp_from_interpreted_frame_ = frame->IsInterpreted();
#endif
}
void Debugger::HandleSteppingRequest() {
ResetSteppingFramePointer();
if (resume_action_ == kStepInto) {
// When single stepping, we need to deoptimize because we might be
// stepping into optimized code. This happens in particular if
// the isolate has been interrupted, but can happen in other cases
// as well. We need to deoptimize the world in case we are about
// to call an optimized function.
NotifySingleStepping(true);
if (FLAG_verbose_debug) {
OS::PrintErr("HandleSteppingRequest - kStepInto\n");
}
} else if (resume_action_ == kStepOver) {
NotifySingleStepping(true);
if (stack_trace_->Length() > 0) {
SetSyncSteppingFramePointer(stack_trace_->FrameAt(0));
}
if (FLAG_verbose_debug) {
OS::PrintErr("HandleSteppingRequest - kStepOver stepping_fp=%" Px "\n",
stepping_fp_);
}
} else if (resume_action_ == kStepOut) {
auto* const trace = async_awaiter_stack_trace_ != nullptr
? async_awaiter_stack_trace_
: stack_trace_;
ActivationFrame* sync_caller = nullptr;
ActivationFrame* last_awaiter = nullptr;
for (intptr_t i = 1; i < trace->Length(); i++) {
auto* const frame = trace->FrameAt(i);
if (frame->kind() == ActivationFrame::kAsyncSuspensionMarker) {
last_awaiter = async_awaiter_stack_trace_->FrameAt(i + 1);
} else if (frame->IsDebuggable()) {
if (last_awaiter != nullptr) {
AsyncStepInto(last_awaiter->closure());
if (FLAG_verbose_debug) {
OS::PrintErr(
"HandleSteppingRequest - continue to async awaiter %s\n",
Function::Handle(last_awaiter->closure().function())
.ToFullyQualifiedCString());
}
return;
}
sync_caller = frame;
break;
}
}
// If there is no debuggable synchronous caller (i.e., an attempt to step
// out of the main function), just use the closest caller so the debugger
// doesn't emit another pause in the current function before exiting.
if (sync_caller == nullptr) {
ASSERT(trace->Length() > 1);
if (FLAG_verbose_debug) {
OS::PrintErr("HandleSteppingRequest - no debuggable callers\n");
}
sync_caller = trace->FrameAt(1);
}
NotifySingleStepping(true);
SetSyncSteppingFramePointer(sync_caller);
if (FLAG_verbose_debug) {
OS::PrintErr("HandleSteppingRequest- kStepOut %" Px "\n", stepping_fp_);
}
} else if (resume_action_ == kStepRewind) {
if (FLAG_trace_rewind) {
OS::PrintErr("Rewinding to frame %" Pd "\n", resume_frame_index_);
OS::PrintErr(
"-------------------------\n"
"All frames...\n\n");
StackFrameIterator iterator(ValidationPolicy::kDontValidateFrames,
Thread::Current(),
StackFrameIterator::kNoCrossThreadIteration);
StackFrame* frame = iterator.NextFrame();
intptr_t num = 0;
while ((frame != nullptr)) {
OS::PrintErr("#%04" Pd " %s\n", num++, frame->ToCString());
frame = iterator.NextFrame();
}
}
RewindToFrame(resume_frame_index_);
UNREACHABLE();
}
}
void Debugger::CacheStackTraces(DebuggerStackTrace* stack_trace,
DebuggerStackTrace* async_awaiter_stack_trace) {
ASSERT(stack_trace_ == nullptr);
stack_trace_ = stack_trace;
ASSERT(async_awaiter_stack_trace_ == nullptr);
async_awaiter_stack_trace_ = async_awaiter_stack_trace;
}
void Debugger::ClearCachedStackTraces() {
stack_trace_ = nullptr;
async_awaiter_stack_trace_ = nullptr;
}
static intptr_t FindNextRewindFrameIndex(DebuggerStackTrace* stack,
intptr_t frame_index) {
for (intptr_t i = frame_index + 1; i < stack->Length(); i++) {
ActivationFrame* frame = stack->FrameAt(i);
if (frame->IsRewindable()) {
return i;
}
}
return -1;
}
// Can we rewind to the indicated frame?
static bool CanRewindFrame(intptr_t frame_index, const char** error) {
// check rewind pc is found
DebuggerStackTrace* stack = Isolate::Current()->debugger()->StackTrace();
intptr_t num_frames = stack->Length();
if (frame_index < 1 || frame_index >= num_frames) {
if (error != nullptr) {
*error = Thread::Current()->zone()->PrintToString(
"Frame must be in bounds [1..%" Pd
"]: "
"saw %" Pd "",
num_frames - 1, frame_index);
}
return false;
}
ActivationFrame* frame = stack->FrameAt(frame_index);
if (!frame->IsRewindable()) {
intptr_t next_index = FindNextRewindFrameIndex(stack, frame_index);
if (next_index > 0) {
*error = Thread::Current()->zone()->PrintToString(
"Cannot rewind to frame %" Pd
" due to conflicting compiler "
"optimizations. "
"Run the vm with --no-prune-dead-locals to disallow these "
"optimizations. "
"Next valid rewind frame is %" Pd ".",
frame_index, next_index);
} else {
*error = Thread::Current()->zone()->PrintToString(
"Cannot rewind to frame %" Pd
" due to conflicting compiler "
"optimizations. "
"Run the vm with --no-prune-dead-locals to disallow these "
"optimizations.",
frame_index);
}
return false;
}
return true;
}
// Given a return address, find the "rewind" pc, which is the pc
// before the corresponding call.
static uword LookupRewindPc(const Code& code, uword return_address) {
ASSERT(!code.is_optimized());
ASSERT(code.ContainsInstructionAt(return_address));
uword pc_offset = return_address - code.PayloadStart();
const PcDescriptors& descriptors =
PcDescriptors::Handle(code.pc_descriptors());
PcDescriptors::Iterator iter(descriptors,
UntaggedPcDescriptors::kRewind |
UntaggedPcDescriptors::kIcCall |
UntaggedPcDescriptors::kUnoptStaticCall);
intptr_t rewind_deopt_id = -1;
uword rewind_pc = 0;
while (iter.MoveNext()) {
if (iter.Kind() == UntaggedPcDescriptors::kRewind) {
// Remember the last rewind so we don't need to iterator twice.
rewind_pc = code.PayloadStart() + iter.PcOffset();
rewind_deopt_id = iter.DeoptId();
}
if ((pc_offset == iter.PcOffset()) && (iter.DeoptId() == rewind_deopt_id)) {
return rewind_pc;
}
}
return 0;
}
void Debugger::RewindToFrame(intptr_t frame_index) {
Thread* thread = Thread::Current();
Zone* zone = thread->zone();
Code& code = Code::Handle(zone);
Function& function = Function::Handle(zone);
// Find the requested frame.
StackFrameIterator iterator(ValidationPolicy::kDontValidateFrames,
Thread::Current(),
StackFrameIterator::kNoCrossThreadIteration);
intptr_t current_frame = 0;
for (StackFrame* frame = iterator.NextFrame(); frame != nullptr;
frame = iterator.NextFrame()) {
ASSERT(frame->IsValid());
if (frame->IsDartFrame()) {
code = frame->LookupDartCode();
function = code.function();
if (!IsFunctionVisible(function)) {
continue;
}
if (code.is_optimized()) {
intptr_t sub_index = 0;
for (InlinedFunctionsIterator it(code, frame->pc()); !it.Done();
it.Advance()) {
if (current_frame == frame_index) {
RewindToOptimizedFrame(frame, code, sub_index);
UNREACHABLE();
}
current_frame++;
sub_index++;
}
} else {
if (current_frame == frame_index) {
// We are rewinding to an unoptimized frame.
RewindToUnoptimizedFrame(frame, code);
UNREACHABLE();
}
current_frame++;
}
}
}
UNIMPLEMENTED();
}
void Debugger::RewindToUnoptimizedFrame(StackFrame* frame, const Code& code) {
// We will be jumping out of the debugger rather than exiting this
// function, so prepare the debugger state.
ClearCachedStackTraces();
set_resume_action(kContinue);
resume_frame_index_ = -1;
EnterSingleStepMode();
uword rewind_pc = LookupRewindPc(code, frame->pc());
if (FLAG_trace_rewind && rewind_pc == 0) {
OS::PrintErr("Unable to find rewind pc for pc(%" Px ")\n", frame->pc());
}
ASSERT(rewind_pc != 0);
if (FLAG_trace_rewind) {
OS::PrintErr(
"===============================\n"
"Rewinding to unoptimized frame:\n"
" rewind_pc(0x%" Px " offset:0x%" Px ") sp(0x%" Px ") fp(0x%" Px
")\n"
"===============================\n",
rewind_pc, rewind_pc - code.PayloadStart(), frame->sp(), frame->fp());
}
Exceptions::JumpToFrame(Thread::Current(), rewind_pc, frame->sp(),
frame->fp(), true /* clear lazy deopt at target */);
UNREACHABLE();
}
void Debugger::RewindToOptimizedFrame(StackFrame* frame,
const Code& optimized_code,
intptr_t sub_index) {
post_deopt_frame_index_ = sub_index;
// We will be jumping out of the debugger rather than exiting this
// function, so prepare the debugger state.
ClearCachedStackTraces();
set_resume_action(kContinue);
resume_frame_index_ = -1;
EnterSingleStepMode();
if (FLAG_trace_rewind) {
OS::PrintErr(
"===============================\n"
"Deoptimizing frame for rewind:\n"
" deopt_pc(0x%" Px ") sp(0x%" Px ") fp(0x%" Px
")\n"
"===============================\n",
frame->pc(), frame->sp(), frame->fp());
}
Thread* thread = Thread::Current();
thread->set_resume_pc(frame->pc());
uword deopt_stub_pc = StubCode::DeoptForRewind().EntryPoint();
Exceptions::JumpToFrame(thread, deopt_stub_pc, frame->sp(), frame->fp(),
true /* clear lazy deopt at target */);
UNREACHABLE();
}
void Debugger::RewindPostDeopt() {
intptr_t rewind_frame = post_deopt_frame_index_;
post_deopt_frame_index_ = -1;
if (FLAG_trace_rewind) {
OS::PrintErr("Post deopt, jumping to frame %" Pd "\n", rewind_frame);
OS::PrintErr(
"-------------------------\n"
"All frames...\n\n");
StackFrameIterator iterator(ValidationPolicy::kDontValidateFrames,
Thread::Current(),
StackFrameIterator::kNoCrossThreadIteration);
StackFrame* frame = iterator.NextFrame();
intptr_t num = 0;
while ((frame != nullptr)) {
OS::PrintErr("#%04" Pd " %s\n", num++, frame->ToCString());
frame = iterator.NextFrame();
}
}
Thread* thread = Thread::Current();
Zone* zone = thread->zone();
Code& code = Code::Handle(zone);
StackFrameIterator iterator(ValidationPolicy::kDontValidateFrames,
Thread::Current(),
StackFrameIterator::kNoCrossThreadIteration);
intptr_t current_frame = 0;
for (StackFrame* frame = iterator.NextFrame(); frame != nullptr;
frame = iterator.NextFrame()) {
ASSERT(frame->IsValid());
if (frame->IsDartFrame()) {
code = frame->LookupDartCode();
ASSERT(!code.is_optimized());
if (current_frame == rewind_frame) {
RewindToUnoptimizedFrame(frame, code);
UNREACHABLE();
}
current_frame++;
}
}
}
// static
bool Debugger::IsDebuggable(const Function& func) {
if (!func.is_debuggable()) {
return false;
}
const Class& cls = Class::Handle(func.Owner());
const Library& lib = Library::Handle(cls.library());
return lib.IsDebuggable();
}
void GroupDebugger::RegisterSingleSteppingDebugger(Thread* thread,
const Debugger* debugger) {
WriteRwLocker sl(Thread::Current(), single_stepping_set_lock());
single_stepping_set_.Insert(debugger);
}
void GroupDebugger::UnregisterSingleSteppingDebugger(Thread* thread,
const Debugger* debugger) {
WriteRwLocker sl(Thread::Current(), single_stepping_set_lock());
single_stepping_set_.Remove(debugger);
}
bool GroupDebugger::HasBreakpointUnsafe(Thread* thread,
const Function& function) {
DEBUG_ASSERT(thread->IsInStoppedMutatorsScope() ||
breakpoint_locations_lock()->IsCurrentThreadReader());
// Check if function has any breakpoints.
String& url = String::Handle(thread->zone());
for (intptr_t i = 0; i < breakpoint_locations_.length(); i++) {
BreakpointLocation* location = breakpoint_locations_.At(i);
url = location->url();
if (FunctionOverlaps(function, url, location->token_pos(),
location->end_token_pos())) {
return true;
}
}
return false;
}
bool GroupDebugger::HasBreakpoint(Thread* thread, const Function& function) {
bool hasBreakpoint = false;
// Don't need to worry about the lock if mutators are stopped.
if (thread->IsInStoppedMutatorsScope()) {
hasBreakpoint = HasBreakpointUnsafe(thread, function);
} else {
SafepointReadRwLocker sl(thread, breakpoint_locations_lock());
hasBreakpoint = HasBreakpointUnsafe(thread, function);
}
if (hasBreakpoint) {
return true;
}
// TODO(aam): do we have to iterate over both code breakpoints and
// breakpoint locations? Wouldn't be sufficient to iterate over only
// one list? Could you have a CodeBreakpoint without corresponding
// BreakpointLocation?
if (HasCodeBreakpointInFunction(function)) {
return true;
}
return false;
}
bool GroupDebugger::IsDebugging(Thread* thread, const Function& function) {
{
ReadRwLocker ml(thread, single_stepping_set_lock());
if (!single_stepping_set_.IsEmpty()) {
return true;
}
}
return HasBreakpoint(thread, function);
}
void Debugger::set_resume_action(ResumeAction resume_action) {
auto thread = Thread::Current();
if (resume_action == kContinue) {
group_debugger()->UnregisterSingleSteppingDebugger(thread, this);
} else {
group_debugger()->RegisterSingleSteppingDebugger(thread, this);
}
resume_action_ = resume_action;
}
void Debugger::SignalPausedEvent(ActivationFrame* top_frame, Breakpoint* bpt) {
set_resume_action(kContinue);
ResetSteppingFramePointer();
NotifySingleStepping(false);
ASSERT(!IsPaused());
if ((bpt != nullptr) && bpt->is_single_shot()) {
RemoveBreakpoint(bpt->id());
bpt = nullptr;
}
ServiceEvent event(isolate_, ServiceEvent::kPauseBreakpoint);
event.set_top_frame(top_frame);
event.set_breakpoint(bpt);
event.set_at_async_jump(IsAtAsyncJump(top_frame));
Pause(&event);
}
static bool IsAtAsyncJump(ActivationFrame* top_frame) {
Thread* const thread = Thread::Current();
Zone* const zone = thread->zone();
if (!top_frame->function().IsAsyncFunction() &&
!top_frame->function().IsAsyncGenerator()) {
return false;
}
if (top_frame->IsInterpreted()) {
#if defined(DART_DYNAMIC_MODULES)
const auto& bytecode = top_frame->bytecode();
ASSERT(bytecode.HasSourcePositions());
const uword pc_offset = top_frame->pc() - bytecode.PayloadStart();
// pc_offset could equal to bytecode size if the last instruction is Throw.
ASSERT(pc_offset <= static_cast<uword>(bytecode.Size()));
bytecode::BytecodeSourcePositionsIterator iter(zone, bytecode);
bool is_yield_point = false;
while (iter.MoveNext()) {
if (pc_offset <= iter.PcOffset()) {
break;
}
is_yield_point = iter.IsYieldPoint();
}
return is_yield_point;
#else
UNREACHABLE();
#endif
} else {
const TokenPosition looking_for = top_frame->TokenPos();
const auto& pc_descriptors =
PcDescriptors::Handle(zone, top_frame->code().pc_descriptors());
if (!pc_descriptors.IsNull()) {
PcDescriptors::Iterator it(pc_descriptors, UntaggedPcDescriptors::kOther);
while (it.MoveNext()) {
if (it.TokenPos() == looking_for &&
it.YieldIndex() != UntaggedPcDescriptors::kInvalidYieldIndex) {
return true;
}
}
}
}
return false;
}
#if defined(DART_DYNAMIC_MODULES)
static ActivationFrame::Relation CompareTopDartFrameTo(uword other_fp,
bool is_interpreted) {
StackFrameIterator iterator(ValidationPolicy::kDontValidateFrames,
Thread::Current(),
StackFrameIterator::kNoCrossThreadIteration);
for (auto* frame = iterator.NextFrame(); frame != nullptr;
frame = iterator.NextFrame()) {
if (frame->IsDartFrame() && (frame->is_interpreted() == is_interpreted)) {
// The current frame's FP can be directly compared to the provided FP to
// provide an answer, since they're using the same stack.
//
// Since this function is only called if the top Dart frame is interpreted
// but the stepping frame is not or vice versa, the current frame is not
// the top Dart frame, so a result of kSelf means the top Dart frame
// is a callee.
return ActivationFrame::CompareTo(is_interpreted, frame->fp(),
other_fp) == ActivationFrame::kCaller
? ActivationFrame::kCaller
: ActivationFrame::kCallee;
}
}
// If there were no frames of the same type on the stack, this must have been
// a caller of the original jump into the interpreter.
ASSERT(is_interpreted);
return ActivationFrame::kCaller;
}
#endif
ErrorPtr Debugger::PauseStepping() {
ASSERT(Thread::Current()->single_step());
// Don't pause recursively.
if (IsPaused()) {
return Error::null();
}
ActivationFrame* frame = TopDartFrame();
ASSERT(frame != nullptr);
if (stepping_fp_ != 0) {
// Check whether we are in a Dart function that the user is
// interested in. If we saved the frame pointer of a stack frame
// the user is interested in, we ignore the single step if we are
// in a callee of that frame.
#if defined(DART_DYNAMIC_MODULES)
auto const relation =
stepping_fp_from_interpreted_frame_ == frame->IsInterpreted()
? frame->CompareTo(stepping_fp_)
// Since interpreted code and compiled code use different stacks,
// finding the relation to the top Dart frame requires some amount
// of stack frame iteration.
: CompareTopDartFrameTo(stepping_fp_,
stepping_fp_from_interpreted_frame_);
#else
// Note that we assume that the stack grows towards lower addresses.
auto const relation = frame->CompareTo(stepping_fp_);
#endif
// There is an "interesting frame" set. Only pause at appropriate
// locations in this frame.
if (relation == ActivationFrame::kCallee) {
// We are in a callee of the frame we're interested in.
// Ignore this stepping break.
return Error::null();
} else if (relation == ActivationFrame::kCaller) {
// We returned from the "interesting frame", there can be no more
// stepping breaks for it. Pause at the next appropriate location
// and let the user set the "interesting" frame again.
ResetSteppingFramePointer();
}
}
if (!frame->IsDebuggable()) {
return Error::null();
}
if (!frame->TokenPos().IsDebugPause()) {
return Error::null();
}
if (MatchesLastSteppingInformation(frame)) {
// Do not stop multiple times for the same token position.
// Several 'debug checked' opcodes may be issued in the same token range,
// a breakpoint could be set at a given pause point, or we could be in the
// bytecode interpreter which checks single stepping at each instruction.
return Error::null();
}
// If there is an active breakpoint at this pc, then the last stepping
// information should have been set appropriately to bail out above.
ASSERT(!group_debugger()->HasActiveBreakpoint(frame->pc()));
// TODO(dartbug.com/48378): Consider aligning async/async* functions
// with regular function wrt the first stop in the function prologue.
if ((frame->function().IsAsyncFunction() ||
frame->function().IsAsyncGenerator()) &&
frame->GetSuspendStateVar() == Object::null()) {
return Error::null();
}
if (FLAG_verbose_debug) {
OS::PrintErr(">>> single step break at %s:%" Pd ":%" Pd
" (func %s token %s address %#" Px " offset %#" Px ")\n",
String::Handle(frame->SourceUrl()).ToCString(),
frame->LineNumber(), frame->ColumnNumber(),
String::Handle(frame->QualifiedFunctionName()).ToCString(),
frame->TokenPos().ToCString(), frame->pc(),
frame->pc() - frame->PayloadStart());
}
SetLastSteppingInformation(frame);
CacheStackTraces(DebuggerStackTrace::Collect(),
DebuggerStackTrace::CollectAsyncAwaiters());
SignalPausedEvent(frame, nullptr);
HandleSteppingRequest();
ClearCachedStackTraces();
// If any error occurred while in the debug message loop, return it here.
return Thread::Current()->StealStickyError();
}
ErrorPtr Debugger::PauseBreakpoint() {
// We ignore this breakpoint when the VM is executing code invoked
// by the debugger to evaluate variables values, or when we see a nested
// breakpoint or exception event.
if (ignore_breakpoints_ || IsPaused()) {
return Error::null();
}
DebuggerStackTrace* stack_trace = DebuggerStackTrace::Collect();
ASSERT(stack_trace->Length() > 0);
ActivationFrame* top_frame = stack_trace->FrameAt(0);
ASSERT(top_frame != nullptr);
if (!Library::Handle(top_frame->Library()).IsDebuggable()) {
return Error::null();
}
BreakpointLocation* bpt_location = nullptr;
const char* cbpt_tostring = nullptr;
{
SafepointReadRwLocker cbl(Thread::Current(),
group_debugger()->code_breakpoints_lock());
CodeBreakpoint* cbpt = nullptr;
bpt_location = group_debugger()->GetBreakpointLocationFor(
this, top_frame->pc(), &cbpt);
if (bpt_location == nullptr) {
// There might be no breakpoint locations for this isolate/debugger.
return Error::null();
}
ASSERT(cbpt != nullptr);
if (FLAG_verbose_debug) {
cbpt_tostring = cbpt->ToCString();
}
}
Breakpoint* bpt_hit = bpt_location->FindHitBreakpoint(top_frame);
if (bpt_hit == nullptr) {
return Error::null();
}
if (FLAG_verbose_debug) {
OS::PrintErr(">>> hit %" Pd
" %s"
" (func %s token %s address %#" Px " offset %#" Px ")\n",
bpt_hit->id(), cbpt_tostring,
String::Handle(top_frame->QualifiedFunctionName()).ToCString(),
bpt_location->token_pos().ToCString(), top_frame->pc(),
top_frame->pc() - top_frame->PayloadStart());
}
// Set the last stepping information before handling the stepping request in
// case the breakpoint is disabled either because it is a one shot or because
// a service call disables it before the next stepping request.
SetLastSteppingInformation(bpt_location);
CacheStackTraces(stack_trace, DebuggerStackTrace::CollectAsyncAwaiters());
SignalPausedEvent(top_frame, bpt_hit);
HandleSteppingRequest();
ClearCachedStackTraces();
// If any error occurred while in the debug message loop, return it here.
return Thread::Current()->StealStickyError();
}
Breakpoint* BreakpointLocation::FindHitBreakpoint(ActivationFrame* top_frame) {
// There may be more than one applicable breakpoint at this location, but we
// will report only one as reached. If there is a single-shot breakpoint, we
// favor it; then a closure-specific breakpoint ; then an general breakpoint.
// First check for a single-shot breakpoint.
Breakpoint* bpt = breakpoints();
while (bpt != nullptr) {
if (bpt->is_single_shot() && bpt->closure() == Instance::null()) {
return bpt;
}
bpt = bpt->next();
}
// Now check for a closure-specific breakpoint.
bpt = breakpoints();
while (bpt != nullptr) {
if (bpt->closure() != Instance::null() &&
bpt->closure() == top_frame->GetClosure()) {
return bpt;
}
bpt = bpt->next();
}
// Finally, check for a general breakpoint.
bpt = breakpoints();
while (bpt != nullptr) {
if (!bpt->is_single_shot() && bpt->closure() == Instance::null()) {
return bpt;
}
bpt = bpt->next();
}
return nullptr;
}
void Debugger::PauseDeveloper(const String& msg) {
// We ignore this breakpoint when the VM is executing code invoked
// by the debugger to evaluate variables values, or when we see a nested
// breakpoint or exception event.
//
// Also ignore it if single stepping, as the debugger already generated
// an appropriate pause for the user call to the function before this point.
if (ignore_breakpoints_ || IsPaused() || IsSingleStepping()) {
return;
}
DebuggerStackTrace* stack_trace = DebuggerStackTrace::Collect();
ASSERT(stack_trace->Length() > 0);
CacheStackTraces(stack_trace, DebuggerStackTrace::CollectAsyncAwaiters());
// We are in the native call to Developer_debugger. the developer
// gets a better experience by not seeing this call. To accomplish
// this, we continue execution until the call exits (step out).
SetResumeAction(kStepOut);
// Reset the last stepping information to ensure we pause after stepping out.
ResetLastSteppingInformation();
HandleSteppingRequest();
ClearCachedStackTraces();
}
void Debugger::NotifyIsolateCreated() {
if (NeedsIsolateEvents()) {
ServiceEvent event(isolate_, ServiceEvent::kIsolateStart);
InvokeEventHandler(&event);
}
}
#if !defined(DART_PRECOMPILED_RUNTIME)
// |reference_pos_on_line| may specify the position of any column on a
// particular line. This function will return the position of column
// [column_number] on that same line.
static TokenPosition FindExactTokenPosition(const Script& script,
TokenPosition reference_pos_on_line,
intptr_t column_number) {
intptr_t line;
intptr_t col;
if (script.GetTokenLocation(reference_pos_on_line, &line, &col)) {
return TokenPosition::Deserialize(reference_pos_on_line.Pos() +
(column_number - col));
}
return TokenPosition::kNoSource;
}
#endif // !defined(DART_PRECOMPILED_RUNTIME)
void Debugger::NotifyDoneLoading() {
if (latent_locations_ == nullptr) {
// Common, fast path.
return;
}
auto thread = Thread::Current();
auto isolate_group = thread->isolate_group();
auto zone = thread->zone();
Library& lib = Library::Handle(zone);
Script& script = Script::Handle(zone);
String& url = String::Handle(zone);
BreakpointLocation* loc = latent_locations_;
BreakpointLocation* prev_loc = nullptr;
const GrowableObjectArray& libs =
GrowableObjectArray::Handle(isolate_group->object_store()->libraries());
GrowableHandlePtrArray<const Script> scripts(zone, 1);
while (loc != nullptr) {
url = loc->url();
bool found_match = false;
bool is_package = url.StartsWith(Symbols::PackageScheme());
for (intptr_t i = 0; i < libs.Length(); i++) {
lib ^= libs.At(i);
script = lib.LookupScript(url, !is_package);
if (!script.IsNull()) {
scripts.Add(script);
}
}
if (scripts.length() > 0) {
// Found a script with matching url for this latent breakpoint.
// Unlink the latent breakpoint from the list.
found_match = true;
BreakpointLocation* matched_loc = loc;
loc = loc->next();
if (prev_loc == nullptr) {
latent_locations_ = loc;
} else {
prev_loc->set_next(loc);
}
// Now find the token range at the requested line and make a
// new unresolved source breakpoint.
intptr_t line_number = matched_loc->requested_line_number();
intptr_t column_number = matched_loc->requested_column_number();
ASSERT(line_number >= 0);
TokenPosition first_token_pos = TokenPosition::kNoSource;
TokenPosition last_token_pos = TokenPosition::kNoSource;
scripts.At(0).TokenRangeAtLine(line_number, &first_token_pos,
&last_token_pos);
if (!first_token_pos.IsDebugPause() || !last_token_pos.IsDebugPause()) {
// Script does not contain the given line number or there are no
// tokens on the line. Drop the breakpoint silently.
Breakpoint* bpt = matched_loc->breakpoints();
while (bpt != nullptr) {
if (FLAG_verbose_debug) {
OS::PrintErr("No code found at line %" Pd
": "
"dropping latent breakpoint %" Pd " in '%s'\n",
line_number, bpt->id(), url.ToCString());
}
Breakpoint* prev = bpt;
bpt = bpt->next();
delete prev;
}
delete matched_loc;
} else {
// We don't expect to already have a breakpoint for this location.
// If there is one, assert in debug build but silently drop
// the latent breakpoint in release build.
BreakpointLocation* existing_loc =
GetBreakpointLocation(url, first_token_pos, -1, column_number);
ASSERT(existing_loc == nullptr);
if (existing_loc == nullptr) {
// Create and register a new source breakpoint for the
// latent breakpoint.
BreakpointLocation* unresolved_loc = new BreakpointLocation(
this, scripts, first_token_pos, last_token_pos, line_number,
column_number);
RegisterBreakpointLocation(unresolved_loc);
// Move breakpoints over.
Breakpoint* bpt = matched_loc->breakpoints();
unresolved_loc->set_breakpoints(bpt);
matched_loc->set_breakpoints(nullptr);
while (bpt != nullptr) {
bpt->set_bpt_location(unresolved_loc);
if (FLAG_verbose_debug) {
OS::PrintErr(
"Converted latent breakpoint "
"%" Pd " in '%s' at line %" Pd " col %" Pd "\n",
bpt->id(), url.ToCString(), line_number, column_number);
}
bpt = bpt->next();
}
group_debugger()->SyncBreakpointLocation(unresolved_loc);
}
delete matched_loc;
// Break out of the iteration over loaded libraries. If the
// same url has been loaded into more than one library, we
// only set a breakpoint in the first one.
// TODO(hausner): There is one possible pitfall here.
// If the user sets a latent breakpoint using a partial url that
// ends up matching more than one script, the breakpoint might
// get set in the wrong script.
// It would be better if we could warn the user if multiple
// scripts are matching.
break;
}
}
if (!found_match) {
// No matching url found in any of the libraries.
if (FLAG_verbose_debug) {
Breakpoint* bpt = loc->breakpoints();
while (bpt != nullptr) {
OS::PrintErr(
"No match found for latent breakpoint id "
"%" Pd " with url '%s'\n",
bpt->id(), url.ToCString());
bpt = bpt->next();
}
}
loc = loc->next();
}
}
}
// TODO(hausner): Could potentially make this faster by checking
// whether the call target at pc is a debugger stub.
bool GroupDebugger::HasActiveBreakpoint(uword pc) {
SafepointReadRwLocker sl(Thread::Current(), code_breakpoints_lock());
CodeBreakpoint* cbpt = GetCodeBreakpoint(pc);
return (cbpt != nullptr) && (cbpt->IsEnabled());
}
CodeBreakpoint* GroupDebugger::GetCodeBreakpoint(uword breakpoint_address) {
CodeBreakpoint* cbpt = code_breakpoints_;
while (cbpt != nullptr) {
if (cbpt->pc() == breakpoint_address) {
return cbpt;
}
cbpt = cbpt->next();
}
return nullptr;
}
BreakpointLocation* GroupDebugger::GetBreakpointLocationFor(
Debugger* debugger,
uword breakpoint_address,
CodeBreakpoint** pcbpt) {
ASSERT(pcbpt != nullptr);
SafepointReadRwLocker sl(Thread::Current(), code_breakpoints_lock());
*pcbpt = code_breakpoints_;
while (*pcbpt != nullptr) {
if ((*pcbpt)->pc() == breakpoint_address) {
return (*pcbpt)->FindBreakpointForDebugger(debugger);
}
*pcbpt = (*pcbpt)->next();
}
return nullptr;
}
void GroupDebugger::RegisterCodeBreakpoint(CodeBreakpoint* cbpt) {
ASSERT(cbpt->next() == nullptr);
DEBUG_ASSERT(code_breakpoints_lock()->IsCurrentThreadWriter() ||
Thread::Current()->IsInStoppedMutatorsScope());
cbpt->set_next(code_breakpoints_);
code_breakpoints_ = cbpt;
}
CodePtr GroupDebugger::GetPatchedStubAddress(uword breakpoint_address) {
SafepointReadRwLocker sl(Thread::Current(), code_breakpoints_lock());
CodeBreakpoint* cbpt = GetCodeBreakpoint(breakpoint_address);
if (cbpt != nullptr) {
return cbpt->OrigStubAddress();
}
UNREACHABLE();
return Code::null();
}
uint32_t GroupDebugger::GetPatchedOpcode(uword breakpoint_address) {
SafepointReadRwLocker sl(Thread::Current(), code_breakpoints_lock());
CodeBreakpoint* cbpt = GetCodeBreakpoint(breakpoint_address);
if (cbpt != nullptr) {
return cbpt->OrigOpcode();
}
UNREACHABLE();
return kMaxUint32;
}
bool Debugger::SetBreakpointState(Breakpoint* bpt, bool enable) {
SafepointWriteRwLocker sl(Thread::Current(),
group_debugger()->breakpoint_locations_lock());
if (bpt->is_enabled() != enable) {
if (FLAG_verbose_debug) {
OS::PrintErr("Setting breakpoint %" Pd " to state: %s\n", bpt->id(),
enable ? "enabled" : "disabled");
}
enable ? bpt->Enable() : bpt->Disable();
group_debugger()->SyncBreakpointLocation(bpt->bpt_location());
return true;
}
return false;
}
// Remove and delete the source breakpoint bpt and its associated
// code breakpoints.
void Debugger::RemoveBreakpoint(intptr_t bp_id) {
SafepointWriteRwLocker sl(Thread::Current(),
group_debugger()->breakpoint_locations_lock());
if (RemoveBreakpointFromTheList(bp_id, &breakpoint_locations_)) {
return;
}
RemoveBreakpointFromTheList(bp_id, &latent_locations_);
}
// Remove and delete the source breakpoint bpt and its associated
// code breakpoints. Returns true, if breakpoint was found and removed,
// returns false, if breakpoint was not found.
bool Debugger::RemoveBreakpointFromTheList(intptr_t bp_id,
BreakpointLocation** list) {
BreakpointLocation* prev_loc = nullptr;
BreakpointLocation* curr_loc = *list;
while (curr_loc != nullptr) {
Breakpoint* prev_bpt = nullptr;
Breakpoint* curr_bpt = curr_loc->breakpoints();
while (curr_bpt != nullptr) {
if (curr_bpt->id() == bp_id) {
if (prev_bpt == nullptr) {
curr_loc->set_breakpoints(curr_bpt->next());
} else {
prev_bpt->set_next(curr_bpt->next());
}
// Send event to client before the breakpoint's fields are
// poisoned and deleted.
SendBreakpointEvent(ServiceEvent::kBreakpointRemoved, curr_bpt);
curr_bpt->set_next(nullptr);
curr_bpt->set_bpt_location(nullptr);
// Remove possible references to the breakpoint.
if (pause_event_ != nullptr && pause_event_->breakpoint() == curr_bpt) {
pause_event_->set_breakpoint(nullptr);
}
delete curr_bpt;
curr_bpt = nullptr;
// Delete the breakpoint location object if there are no more
// breakpoints at that location.
if (curr_loc->breakpoints() == nullptr) {
if (prev_loc == nullptr) {
*list = curr_loc->next();
} else {
prev_loc->set_next(curr_loc->next());
}
if (!curr_loc->IsLatent()) {
// Remove references from code breakpoints to this breakpoint
// location and disable them.
// Latent breakpoint locations won't have code breakpoints.
group_debugger()->UnlinkCodeBreakpoints(curr_loc);
}
group_debugger()->UnregisterBreakpointLocation(curr_loc);
BreakpointLocation* next_loc = curr_loc->next();
delete curr_loc;
curr_loc = next_loc;
}
// The code breakpoints will be deleted when the VM resumes
// after the pause event.
return true;
}
prev_bpt = curr_bpt;
curr_bpt = curr_bpt->next();
}
prev_loc = curr_loc;
curr_loc = curr_loc->next();
}
// breakpoint with bp_id does not exist, nothing to do.
return false;
}
void GroupDebugger::RegisterBreakpointLocation(BreakpointLocation* location) {
DEBUG_ASSERT(breakpoint_locations_lock()->IsCurrentThreadWriter() ||
Thread::Current()->IsInStoppedMutatorsScope());
breakpoint_locations_.Add(location);
}
void GroupDebugger::UnregisterBreakpointLocation(BreakpointLocation* location) {
ASSERT(breakpoint_locations_lock()->IsCurrentThreadWriter());
for (intptr_t i = 0; i < breakpoint_locations_.length(); i++) {
if (breakpoint_locations_.At(i) == location) {
breakpoint_locations_.EraseAt(i);
return;
}
}
}
// Unlink code breakpoints from the given breakpoint location.
// They will later be deleted when control returns from the pause event
// callback. Also, disable the breakpoint so it no longer fires if it
// should be hit before it gets deleted.
void GroupDebugger::UnlinkCodeBreakpoints(BreakpointLocation* bpt_location) {
ASSERT(bpt_location != nullptr);
SafepointWriteRwLocker sl(Thread::Current(), code_breakpoints_lock());
CodeBreakpoint* curr_bpt = code_breakpoints_;
while (curr_bpt != nullptr) {
if (curr_bpt->FindAndDeleteBreakpointLocation(bpt_location)) {
curr_bpt->Disable();
needs_breakpoint_cleanup_ = true;
}
curr_bpt = curr_bpt->next();
}
}
// Remove and delete unlinked code breakpoints, i.e. breakpoints that
// are not associated with a breakpoint location.
void GroupDebugger::RemoveUnlinkedCodeBreakpoints() {
DEBUG_ASSERT(code_breakpoints_lock()->IsCurrentThreadWriter() ||
Thread::Current()->IsInStoppedMutatorsScope());
CodeBreakpoint* prev_bpt = nullptr;
CodeBreakpoint* curr_bpt = code_breakpoints_;
while (curr_bpt != nullptr) {
if (curr_bpt->HasNoBreakpointLocations()) {
if (prev_bpt == nullptr) {
code_breakpoints_ = code_breakpoints_->next();
} else {
prev_bpt->set_next(curr_bpt->next());
}
CodeBreakpoint* temp_bpt = curr_bpt;
curr_bpt = curr_bpt->next();
delete temp_bpt;
} else {
prev_bpt = curr_bpt;
curr_bpt = curr_bpt->next();
}
}
needs_breakpoint_cleanup_ = false;
}
BreakpointLocation* Debugger::GetResolvedBreakpointLocation(
const String& script_url,
TokenPosition code_token_pos) {
BreakpointLocation* loc = breakpoint_locations_;
String& loc_url = String::Handle();
while (loc != nullptr) {
loc_url = loc->url();
if (script_url.Equals(loc_url) && loc->code_token_pos_ == code_token_pos) {
return loc;
}
loc = loc->next();
}
return nullptr;
}
BreakpointLocation* Debugger::GetBreakpointLocation(
const String& script_url,
TokenPosition token_pos,
intptr_t requested_line,
intptr_t requested_column,
TokenPosition code_token_pos) {
BreakpointLocation* loc = breakpoint_locations_;
String& loc_url = String::Handle();
while (loc != nullptr) {
loc_url = loc->url();
if (script_url.Equals(loc_url) &&
(!token_pos.IsReal() || (loc->token_pos() == token_pos)) &&
((requested_line == -1) ||
(loc->requested_line_number_ == requested_line)) &&
((requested_column == -1) ||
(loc->requested_column_number_ == requested_column)) &&
(!code_token_pos.IsReal() ||
(loc->code_token_pos_ == code_token_pos))) {
return loc;
}
loc = loc->next();
}
return nullptr;
}
Breakpoint* Debugger::GetBreakpointById(intptr_t id) {
Breakpoint* bpt = GetBreakpointByIdInTheList(id, breakpoint_locations_);
if (bpt != nullptr) {
return bpt;
}
return GetBreakpointByIdInTheList(id, latent_locations_);
}
Breakpoint* Debugger::GetBreakpointByIdInTheList(intptr_t id,
BreakpointLocation* list) {
BreakpointLocation* loc = list;
while (loc != nullptr) {
Breakpoint* bpt = loc->breakpoints();
while (bpt != nullptr) {
if (bpt->id() == id) {
return bpt;
}
bpt = bpt->next();
}
loc = loc->next();
}
return nullptr;
}
void Debugger::AsyncStepInto(const Closure& awaiter) {
Zone* zone = Thread::Current()->zone();
auto& suspend_state = SuspendState::Handle(zone);
if (StackTraceUtils::GetSuspendState(awaiter, &suspend_state)) {
const auto& function_data =
Object::Handle(zone, suspend_state.function_data());
SetBreakpointAtResumption(function_data);
} else {
// We intentionally discard the value stored into the following variable.
Breakpoint* _ = nullptr;
const Error& error = Error::Handle(
SetBreakpointAtActivation(awaiter, /*single_shot=*/true, &_));
// This method cannot be called while responding to a Service RPC, so we
// know that the top error handler is an exit frame.
DEBUG_ASSERT(Thread::Current()->TopErrorHandlerIsExitFrame());
if (!error.IsNull()) {
if (error.ptr() == Object::out_of_memory_error().ptr()) {
Exceptions::ThrowOOM();
UNREACHABLE();
}
if (error.IsLanguageError()) {
Exceptions::ThrowCompileTimeError(LanguageError::Cast(error));
UNREACHABLE();
}
Exceptions::PropagateError(Error::Cast(error));
UNREACHABLE();
}
}
Continue();
}
void Debugger::Continue() {
SetResumeAction(kContinue);
ResetSteppingFramePointer();
NotifySingleStepping(false);
}
BreakpointLocation* Debugger::GetLatentBreakpoint(const String& url,
intptr_t line,
intptr_t column) {
BreakpointLocation* loc = latent_locations_;
String& bpt_url = String::Handle();
while (loc != nullptr) {
bpt_url = loc->url();
if (bpt_url.Equals(url) && (loc->requested_line_number() == line) &&
(loc->requested_column_number() == column)) {
return loc;
}
loc = loc->next();
}
// No breakpoint for this location requested. Allocate new one.
loc = new BreakpointLocation(this, url, line, column);
loc->set_next(latent_locations_);
latent_locations_ = loc;
return loc;
}
void Debugger::RegisterBreakpointLocationUnsafe(BreakpointLocation* loc) {
DEBUG_ASSERT(
group_debugger()->breakpoint_locations_lock()->IsCurrentThreadWriter() ||
Thread::Current()->IsInStoppedMutatorsScope());
ASSERT(loc->next() == nullptr);
loc->set_next(breakpoint_locations_);
breakpoint_locations_ = loc;
group_debugger()->RegisterBreakpointLocation(loc);
}
void Debugger::RegisterBreakpointLocation(BreakpointLocation* loc) {
auto thread = Thread::Current();
if (thread->IsInStoppedMutatorsScope()) {
RegisterBreakpointLocationUnsafe(loc);
} else {
SafepointWriteRwLocker sl(thread,
group_debugger()->breakpoint_locations_lock());
RegisterBreakpointLocationUnsafe(loc);
}
}
#endif // !PRODUCT
} // namespace dart