| //------------------------------------------------------------------------------------------------------- |
| // 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); |
| } |
| } |
| } |