blob: 3f8fa540eb2385f4e20762b8e83236365fbaa732 [file]
//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
#include "RuntimeDebugPch.h"
#include "Language/InterpreterStackFrame.h"
#define InvalidScriptId 0xFFFFFFFF
namespace Js
{
InterpreterHaltState::InterpreterHaltState(StopType _stopType, const FunctionBody* _executingFunction, MutationBreakpoint* _activeMutationBP/*= nullptr*/) :
stopType(_stopType),
executingFunction(_executingFunction),
topFrame(nullptr),
framePointers(nullptr),
referencedDiagnosticArena(nullptr),
exceptionObject(nullptr),
stringBuilder(nullptr),
activeMutationBP(_activeMutationBP)
{
Assert(executingFunction || (stopType == STOP_EXCEPTIONTHROW || stopType == STOP_MUTATIONBREAKPOINT));
}
FunctionBody* InterpreterHaltState::GetFunction()
{
Assert(IsValid());
return this->topFrame->GetFunction();
}
int InterpreterHaltState::GetCurrentOffset()
{
Assert(IsValid());
return this->topFrame->GetByteCodeOffset();
}
void InterpreterHaltState::SetCurrentOffset(int offset)
{
Assert(IsValid());
if (this->topFrame->IsInterpreterFrame())
{
// For interpreter frames, actual scenarios we need changed offset are: set next in topmost frame, ignore exception.
// For throw exception we don't need it, but it doesn't hurt because interpreter will ignore the offset
// and rather just throw the exception.
this->topFrame->AsInterpreterFrame()->GetReader()->SetCurrentOffset(offset);
}
else
{
// For native frames, the only scenario we need to record changed offset is when we ignore exception.
if (this->exceptionObject && this->exceptionObject->IsDebuggerSkip())
{
this->exceptionObject->SetByteCodeOffsetAfterDebuggerSkip(offset);
}
}
}
bool InterpreterHaltState::IsValid() const
{
// "executingFunction == nullptr" when dispatching exception or mutation bp.
return topFrame && (topFrame->GetFunction() == executingFunction || executingFunction == nullptr);
}
StepController::StepController()
: stepType(STEP_NONE),
byteOffset(0),
statementMap(NULL),
frameCountWhenSet(0),
frameAddrWhenSet((size_t)-1),
stepCompleteOnInlineBreakpoint(false),
pActivatedContext(NULL),
scriptIdWhenSet(InvalidScriptId),
returnedValueRecordingDepth(0),
returnedValueList(nullptr)
{
}
bool StepController::IsActive()
{
return stepType != STEP_NONE;
}
void StepController::Activate(StepType stepType, InterpreterHaltState* haltState)
{
this->stepType = stepType;
this->byteOffset = haltState->GetCurrentOffset();
this->pActivatedContext = haltState->framePointers->Peek()->GetScriptContext();
Assert(this->pActivatedContext);
Js::FunctionBody* functionBody = haltState->GetFunction();
this->body.Root(functionBody, this->pActivatedContext->GetRecycler());
this->statementMap = body->GetMatchingStatementMapFromByteCode(byteOffset, false);
this->frameCountWhenSet = haltState->framePointers->Count();
if (stepType != STEP_DOCUMENT)
{
this->frameAddrWhenSet = (size_t)haltState->framePointers->Peek(0)->GetStackAddress();
}
else
{
// for doc mode, do not bail out automatically on frame changes
this->frameAddrWhenSet = (size_t)-1;
}
this->scriptIdWhenSet = GetScriptId(functionBody);
if (this->returnedValueList == nullptr)
{
this->returnedValueList = JsUtil::List<ReturnedValue*>::New(this->pActivatedContext->GetRecycler());
this->pActivatedContext->GetThreadContext()->SetReturnedValueList(this->returnedValueList);
}
}
void StepController::AddToReturnedValueContainer(Js::Var returnValue, Js::JavascriptFunction * function, bool isValueOfReturnStatement)
{
if (this->pActivatedContext != nullptr) // This will be null when we execute scripts when on break.
{
ReturnedValue *valuePair = RecyclerNew(pActivatedContext->GetRecycler(), ReturnedValue, returnValue, function, isValueOfReturnStatement);
this->returnedValueList->Add(valuePair);
}
}
void StepController::AddReturnToReturnedValueContainer()
{
AddToReturnedValueContainer(nullptr/*returnValue*/, nullptr/*function*/, true/*isValueOfReturnStatement*/);
}
void StepController::StartRecordingCall()
{
returnedValueRecordingDepth++;
}
void StepController::EndRecordingCall(Js::Var returnValue, Js::JavascriptFunction * function)
{
if (IsActive() && this->pActivatedContext != nullptr && returnValue != nullptr)
{
if (this->pActivatedContext->GetThreadContext()->GetDebugManager()->IsAtDispatchHalt())
{
// OS bug 3050302 - Keeping this FatalError for finding other issues where we can record when we are at break
Js::Throw::FatalInternalError();
}
bool isStepOut = stepType == STEP_OUT || stepType == STEP_DOCUMENT;
// Record when :
// If step-out/document : we need to record calls only which are already on the stack, that means the recording-depth is zero or negative.
// if not step-out (step-in and step-over). only for those, which are called from the current call-site or the ones as if we step-out
if ((!isStepOut && returnedValueRecordingDepth <= 1) || (isStepOut && returnedValueRecordingDepth <= 0))
{
// if we are step_document, we should be removing whatever we have collected so-far,
// since they belong to the current document which is a library code
if (stepType == STEP_DOCUMENT)
{
this->returnedValueList->ClearAndZero();
}
AddToReturnedValueContainer(returnValue, function, false/*isValueOfReturnStatement*/);
}
}
returnedValueRecordingDepth--;
}
void StepController::ResetReturnedValueList()
{
returnedValueRecordingDepth = 0;
if (this->returnedValueList != nullptr)
{
this->returnedValueList->ClearAndZero();
}
}
void StepController::HandleResumeAction(Js::InterpreterHaltState* haltState, BREAKRESUMEACTION resumeAction)
{
ResetReturnedValueList();
switch (resumeAction)
{
case BREAKRESUMEACTION_STEP_INTO:
Activate(Js::STEP_IN, haltState);
break;
case BREAKRESUMEACTION_STEP_OVER:
Activate(Js::STEP_OVER, haltState);
break;
case BREAKRESUMEACTION_STEP_OUT:
Activate(Js::STEP_OUT, haltState);
break;
case BREAKRESUMEACTION_STEP_DOCUMENT:
Activate(Js::STEP_DOCUMENT, haltState);
break;
}
}
void StepController::Deactivate(InterpreterHaltState* haltState /*=nullptr*/)
{
// If we are deactivating the step controller during ProbeContainer close or attach/detach we should clear return value list
// If we break other than step -> clear the list.
// If we step in and we land on different function (we are in recording phase the current function) -> clear the list
if ((haltState == nullptr) || (haltState->stopType != Js::STOP_STEPCOMPLETE || (this->stepType == STEP_IN && this->returnedValueRecordingDepth > 0)))
{
ResetReturnedValueList();
}
if (this->body)
{
Assert(this->pActivatedContext);
this->body.Unroot(this->pActivatedContext->GetRecycler());
}
this->pActivatedContext = NULL;
stepType = STEP_NONE;
byteOffset = Js::Constants::NoByteCodeOffset;
statementMap = NULL;
frameCountWhenSet = 0;
scriptIdWhenSet = InvalidScriptId;
frameAddrWhenSet = (size_t)-1;
}
bool StepController::IsStepComplete_AllowingFalsePositives(InterpreterStackFrame * stackFrame)
{
Assert(stackFrame);
if (stepType == STEP_IN)
{
return true;
}
else if (stepType == STEP_DOCUMENT)
{
Assert(stackFrame->GetFunctionBody());
return GetScriptId(stackFrame->GetFunctionBody()) != this->scriptIdWhenSet;
}
// A STEP_OUT or a STEP_OVER has not completed if we are currently deeper on the callstack.
return this->frameAddrWhenSet <= stackFrame->GetStackAddress();
}
bool StepController::IsStepComplete(InterpreterHaltState* haltState, HaltCallback * haltCallback, OpCode originalOpcode)
{
int currentFrameCount = haltState->framePointers->Count();
AssertMsg(currentFrameCount > 0, "In IsStepComplete we must have at least one frame.");
FunctionBody* body = haltState->framePointers->Peek()->GetJavascriptFunction()->GetFunctionBody();
bool canPossiblyHalt = haltCallback->CanHalt(haltState);
OUTPUT_TRACE(Js::DebuggerPhase, _u("StepController::IsStepComplete(): stepType = %d "), stepType);
uint scriptId = GetScriptId(body);
AssertMsg(scriptId != InvalidScriptId, "scriptId cannot be 'invalid-reserved'");
int byteOffset = haltState->GetCurrentOffset();
bool fCanHalt = false;
if (this->frameCountWhenSet > currentFrameCount && STEP_DOCUMENT != stepType)
{
// all steps match once the frame they started on has popped.
fCanHalt = canPossiblyHalt;
}
else if (STEP_DOCUMENT == stepType)
{
OUTPUT_TRACE(Js::DebuggerPhase, _u("StepController::IsStepComplete(): docId when set=%d, currentDocId = %d, can Halt = %d, will halt = %d "), this->scriptIdWhenSet, scriptId, canPossiblyHalt, fCanHalt);
fCanHalt = (scriptId != this->scriptIdWhenSet) && canPossiblyHalt;
}
else if (STEP_IN != stepType && this->frameCountWhenSet < currentFrameCount)
{
// Only step into allows the stack to be deeper
OUTPUT_TRACE(Js::DebuggerPhase, _u("StepController::IsStepComplete(stepType = %d) returning false "), stepType);
return false;
}
else if (STEP_OUT == stepType)
{
fCanHalt = this->frameCountWhenSet > currentFrameCount && canPossiblyHalt;
}
else if (nullptr != this->statementMap && this->statementMap->isSubexpression && STEP_IN != stepType)
{
// Only step into started from subexpression is allowed to stop on another subexpression
Js::FunctionBody* pCurrentFuncBody = haltState->GetFunction();
Js::FunctionBody::StatementMap* map = pCurrentFuncBody->GetMatchingStatementMapFromByteCode(byteOffset, false);
if (nullptr != map && map->isSubexpression) // Execute remaining Subexpressions
{
fCanHalt = false;
}
else
{
Js::FunctionBody::StatementMap* outerMap = pCurrentFuncBody->GetMatchingStatementMapFromByteCode(this->statementMap->byteCodeSpan.begin, true);
if (nullptr != outerMap && map == outerMap) // Execute the rest of current regular statement
{
fCanHalt = false;
}
else
{
fCanHalt = canPossiblyHalt;
}
}
}
else
{
// Match if we are no longer on the original statement. Stepping means move off current statement.
if (body != this->body || NULL == this->statementMap ||
!this->statementMap->byteCodeSpan.Includes(byteOffset))
{
fCanHalt = canPossiblyHalt;
}
}
// At this point we are verifying of global return opcode.
// The global returns are alway added as a zero range begin with zero.
if (fCanHalt && originalOpcode == OpCode::Ret)
{
Js::FunctionBody* pCurrentFuncBody = haltState->GetFunction();
Js::FunctionBody::StatementMap* map = pCurrentFuncBody->GetMatchingStatementMapFromByteCode(byteOffset, true);
fCanHalt = !FunctionBody::IsDummyGlobalRetStatement(&map->sourceSpan);
if (fCanHalt)
{
// We are breaking at last line of function, imagine '}'
AddReturnToReturnedValueContainer();
}
}
OUTPUT_TRACE(Js::DebuggerPhase, _u("StepController::IsStepComplete(stepType = %d) returning %d "), stepType, fCanHalt);
return fCanHalt;
}
bool StepController::ContinueFromInlineBreakpoint()
{
bool ret = stepCompleteOnInlineBreakpoint;
stepCompleteOnInlineBreakpoint = false;
return ret;
}
uint StepController::GetScriptId(_In_ FunctionBody* body)
{
// safe value
uint retValue = BuiltInFunctionsScriptId;
if (body != nullptr)
{
// FYI - Different script blocks within a HTML page will have different source Info ids even though they have the same backing file.
// It might imply we notify the debugger a bit more than needed - thus can be TODO for performance improvements of the Just-My-Code
// or step to next document boundary mode.
AssertMsg(body->GetUtf8SourceInfo() != nullptr, "body->GetUtf8SourceInfo() == nullptr");
retValue = body->GetUtf8SourceInfo()->GetSourceInfoId();
}
return retValue;
}
AsyncBreakController::AsyncBreakController()
: haltCallback(NULL)
{
}
void AsyncBreakController::Activate(HaltCallback* haltCallback)
{
InterlockedExchangePointer((PVOID*)&this->haltCallback, haltCallback);
}
void AsyncBreakController::Deactivate()
{
InterlockedExchangePointer((PVOID*)&this->haltCallback, NULL);
}
bool AsyncBreakController::IsBreak()
{
return haltCallback != NULL;
}
bool AsyncBreakController::IsAtStoppingLocation(InterpreterHaltState* haltState)
{
HaltCallback* callback = this->haltCallback;
if (callback)
{
return callback->CanHalt(haltState);
}
return false;
}
void AsyncBreakController::DispatchAndReset(InterpreterHaltState* haltState)
{
HaltCallback* callback = this->haltCallback;
Deactivate();
if (callback)
{
callback->DispatchHalt(haltState);
}
}
}