Commit 9329f558 authored by Alex Kodat's avatar Alex Kodat Committed by Commit Bot

[debug] Restore StepNext on correct frame for RestoreDebug

When an Isolate in a multi-threaded environment is being debugged
and a thread does a Step Over (StepNext internally) one-shot
breaks are created in the code at the stack frame where the
StepNext occurred. However, if the stepped-over statement had
a function call and the called function (or some function that
it called) unlocked the Isolate (via a C++ function call) and
another thread then locked the Isolate, an ArchiveDebug would
be done which would save the fact that a StepNext is active and
the call frame depth of the StepNext. The one-shot breaks would
then be cleared to avoid stopping the now running thread.

When the original thread that did the StepNext relocks the Isolate,
a RestoreDebug is done which, seeing that a StepNext was active
calls PrepareDebug which assumes that the StepNext must be for
the current JS frame which is usually correct, but not in this
case. This results in the StepNext break actually occurring in the
function that called the C++ function not in the function where
the StepNext was originally done. In addition, the function where
the break now happens must necessarily be deoptimized if
optimized, and debug code and a source map table created if one
doesn't already exists though this is largely invisible to the
user.

Occasionally, a crash/core dump also occurs because the stack
guard is restored after the debugging environment is restored in
the RestoreThread code which can prevent the compiler from being
called to generate the source map table (for the incorrect
function) since the stack guard is another thread's stack guard,
and so might appear that the stack guard has been gone past so
the compiler is not called, resulting in there being no source
map table. But PrepareStep ends up calling the BreakIterator
(via the DebugInfo constructor) which assumes there is a source
map table so we get a crash.

The fix is to have PrepareStep to skip to the frame where the
StepNext was done before doing its thing. Since the only
PrepareStepcaller that requires a frame other than the current
frame, is RestoreDebug, a target frame parameter was added to
PrepareStep that's set by RestoreDebug and defaults to -1
indicating to use the current frame for all other callers.

While this made the order of the debug environment and stack
guard no longer cause an obvious problem, it still felt wrong
to defer restoration of the stack guard until after something
as potentially complex as PrepareStep might be called, so the
order of RestoreDebug and RestoreStackGuard calls were reversed.

Bug: v8:10902
Change-Id: I174e254e72414c827e113aec142f1d329ebe73d8
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2405932
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Reviewed-by: 's avatarToon Verwaest <verwaest@chromium.org>
Reviewed-by: 's avatarYang Guo <yangguo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#70152}
parent 91619242
......@@ -372,6 +372,18 @@ char* Debug::RestoreDebug(char* storage) {
ClearOneShot();
if (thread_local_.last_step_action_ != StepNone) {
int current_frame_count = CurrentFrameCount();
int target_frame_count = thread_local_.target_frame_count_;
DCHECK(current_frame_count >= target_frame_count);
StackTraceFrameIterator frames_it(isolate_);
while (current_frame_count > target_frame_count) {
current_frame_count -= frames_it.FrameFunctionCount();
frames_it.Advance();
}
DCHECK(current_frame_count == target_frame_count);
// Set frame to what it was at Step break
thread_local_.break_frame_id_ = frames_it.frame()->id();
// Reset the previous step action for this thread.
PrepareStep(thread_local_.last_step_action_);
}
......@@ -2040,15 +2052,8 @@ int Debug::CurrentFrameCount() {
while (!it.done() && it.frame()->id() != break_frame_id()) it.Advance();
}
int counter = 0;
while (!it.done()) {
if (it.frame()->is_optimized()) {
std::vector<SharedFunctionInfo> infos;
OptimizedFrame::cast(it.frame())->GetFunctions(&infos);
counter += infos.size();
} else {
counter++;
}
it.Advance();
for (; !it.done(); it.Advance()) {
counter += it.FrameFunctionCount();
}
return counter;
}
......
......@@ -181,6 +181,14 @@ void StackTraceFrameIterator::Advance() {
} while (!done() && !IsValidFrame(iterator_.frame()));
}
int StackTraceFrameIterator::FrameFunctionCount() const {
DCHECK(!done());
if (!iterator_.frame()->is_optimized()) return 1;
std::vector<SharedFunctionInfo> infos;
OptimizedFrame::cast(iterator_.frame())->GetFunctions(&infos);
return static_cast<int>(infos.size());
}
bool StackTraceFrameIterator::IsValidFrame(StackFrame* frame) const {
if (frame->is_java_script()) {
JavaScriptFrame* js_frame = static_cast<JavaScriptFrame*>(frame);
......
......@@ -1234,6 +1234,7 @@ class V8_EXPORT_PRIVATE StackTraceFrameIterator {
bool done() const { return iterator_.done(); }
void Advance();
void AdvanceOneFrame() { iterator_.Advance(); }
int FrameFunctionCount() const;
inline StandardFrame* frame() const;
......
......@@ -128,8 +128,10 @@ bool ThreadManager::RestoreThread() {
from = isolate_->handle_scope_implementer()->RestoreThread(from);
from = isolate_->RestoreThread(from);
from = Relocatable::RestoreState(isolate_, from);
from = isolate_->debug()->RestoreDebug(from);
// Stack guard should be restored before Debug, etc. since Debug etc. might
// depend on a correct stack guard.
from = isolate_->stack_guard()->RestoreStackGuard(from);
from = isolate_->debug()->RestoreDebug(from);
from = isolate_->regexp_stack()->RestoreStack(from);
from = isolate_->bootstrapper()->RestoreState(from);
per_thread->set_thread_state(nullptr);
......@@ -262,8 +264,8 @@ void ThreadManager::EagerlyArchiveThread() {
to = isolate_->handle_scope_implementer()->ArchiveThread(to);
to = isolate_->ArchiveThread(to);
to = Relocatable::ArchiveState(isolate_, to);
to = isolate_->debug()->ArchiveDebug(to);
to = isolate_->stack_guard()->ArchiveStackGuard(to);
to = isolate_->debug()->ArchiveDebug(to);
to = isolate_->regexp_stack()->ArchiveStack(to);
to = isolate_->bootstrapper()->ArchiveState(to);
lazily_archived_thread_ = ThreadId::Invalid();
......
......@@ -3945,34 +3945,58 @@ class ArchiveRestoreThread : public v8::base::Thread,
break_count_(0) {}
void Run() override {
v8::Locker locker(isolate_);
isolate_->Enter();
v8::HandleScope scope(isolate_);
v8::Local<v8::Context> context = v8::Context::New(isolate_);
v8::Context::Scope context_scope(context);
v8::Local<v8::Function> test = CompileFunction(isolate_,
"function test(n) {\n"
" debugger;\n"
" return n + 1;\n"
"}\n",
"test");
debug_->SetDebugDelegate(this);
v8::internal::DisableBreak enable_break(debug_, false);
{
v8::Locker locker(isolate_);
v8::Isolate::Scope i_scope(isolate_);
v8::HandleScope scope(isolate_);
v8::Local<v8::Context> context = v8::Context::New(isolate_);
v8::Context::Scope context_scope(context);
auto callback = [](const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::Local<v8::Value> value = info.Data();
CHECK(value->IsExternal());
auto art = static_cast<ArchiveRestoreThread*>(
v8::Local<v8::External>::Cast(value)->Value());
art->MaybeSpawnChildThread();
};
v8::Local<v8::FunctionTemplate> fun = v8::FunctionTemplate::New(
isolate_, callback, v8::External::New(isolate_, this));
CHECK(context->Global()
->Set(context, v8_str("maybeSpawnChildThread"),
fun->GetFunction(context).ToLocalChecked())
.FromJust());
v8::Local<v8::Function> test =
CompileFunction(isolate_,
"function test(n) {\n"
" debugger;\n"
" nest();\n"
" middle();\n"
" return n + 1;\n"
" function middle() {\n"
" debugger;\n"
" nest();\n"
" Date.now();\n"
" }\n"
" function nest() {\n"
" maybeSpawnChildThread();\n"
" }\n"
"}\n",
"test");
v8::Local<v8::Value> args[1] = {v8::Integer::New(isolate_, spawn_count_)};
debug_->SetDebugDelegate(this);
v8::internal::DisableBreak enable_break(debug_, false);
int result = test->Call(context, context->Global(), 1, args)
.ToLocalChecked()
->Int32Value(context)
.FromJust();
v8::Local<v8::Value> args[1] = {v8::Integer::New(isolate_, spawn_count_)};
// Verify that test(spawn_count_) returned spawn_count_ + 1.
CHECK_EQ(spawn_count_ + 1, result);
int result = test->Call(context, context->Global(), 1, args)
.ToLocalChecked()
->Int32Value(context)
.FromJust();
isolate_->Exit();
// Verify that test(spawn_count_) returned spawn_count_ + 1.
CHECK_EQ(spawn_count_ + 1, result);
}
}
void BreakProgramRequested(
......@@ -3985,9 +4009,12 @@ class ArchiveRestoreThread : public v8::base::Thread,
i::PrintF("ArchiveRestoreThread #%d hit breakpoint at line %d\n",
spawn_count_, location.GetLineNumber());
switch (location.GetLineNumber()) {
case 1: // debugger;
CHECK_EQ(break_count_, 0);
const int expectedLineNumber[] = {1, 2, 3, 6, 4};
CHECK_EQ(expectedLineNumber[break_count_], location.GetLineNumber());
switch (break_count_) {
case 0: // debugger;
case 1: // nest();
case 2: // middle();
// Attempt to stop on the next line after the first debugger
// statement. If debug->{Archive,Restore}Debug() improperly reset
......@@ -3999,12 +4026,25 @@ class ArchiveRestoreThread : public v8::base::Thread,
// that the parent thread correctly archived and restored the
// state necessary to stop on the next line. If not, then control
// will simply continue past the `return n + 1` statement.
//
// A real world multi-threading app would probably never unlock the
// Isolate at a break point as that adds a thread switch point while
// debugging where none existed in the application and a
// multi-threaded should be able to count on not thread switching
// over a certain range of instructions.
MaybeSpawnChildThread();
break;
case 2: // return n + 1;
CHECK_EQ(break_count_, 1);
case 3: // debugger; in middle();
// Attempt to stop on the next line after the first debugger
// statement. If debug->{Archive,Restore}Debug() improperly reset
// thread-local debug information, the debugger will fail to stop
// before the test function returns.
debug_->PrepareStep(StepOut);
break;
case 4: // return n + 1;
break;
default:
......@@ -4033,8 +4073,8 @@ class ArchiveRestoreThread : public v8::base::Thread,
// This is the most important check in this test, since
// child.GetBreakCount() will return 1 if the debugger fails to stop
// on the `return n + 1` line after the grandchild thread returns.
CHECK_EQ(child.GetBreakCount(), 2);
// on the `next()` line after the grandchild thread returns.
CHECK_EQ(child.GetBreakCount(), 5);
}
}
......@@ -4048,18 +4088,14 @@ class ArchiveRestoreThread : public v8::base::Thread,
};
TEST(DebugArchiveRestore) {
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator = CcTest::array_buffer_allocator();
v8::Isolate* isolate = v8::Isolate::New(create_params);
v8::Isolate* isolate = CcTest::isolate();
ArchiveRestoreThread thread(isolate, 5);
ArchiveRestoreThread thread(isolate, 4);
// Instead of calling thread.Start() and thread.Join() here, we call
// thread.Run() directly, to make sure we exercise archive/restore
// logic on the *current* thread as well as other threads.
thread.Run();
CHECK_EQ(thread.GetBreakCount(), 2);
isolate->Dispose();
CHECK_EQ(thread.GetBreakCount(), 5);
}
class DebugEventExpectNoException : public v8::debug::DebugDelegate {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment