/* * Unwinding proceeds as follows: * * - Discard all evaluation stack temporaries (including pre-live * activation records). * * - Check if the faultOffset that raised the exception is inside a * protected region, if so, if it can handle the Fault resume the * VM at the handler. * * - Failing any of the above, pop the frame for the current * function. If the current function was the last frame in the * current VM nesting level, return UnwindAction::Propagate, * otherwise go to the first step and repeat this process in the * caller's frame. * * Note: it's important that the unwinder makes a copy of the Fault * it's currently operating on, as the underlying faults vector may * reallocate due to nested exception handling. */ UnwindAction unwind(ActRec*& fp, Stack& stack, PC& pc, Offset faultOffset, // distinct from pc after iopUnwind Fault fault) { FTRACE(1, "entering unwinder for fault: {}\n", describeFault(fault)); SCOPE_EXIT { FTRACE(1, "leaving unwinder for fault: {}\n", describeFault(fault)); }; for (;;) { FTRACE(1, "unwind: func {}, faultOffset {} fp {}\n", fp->m_func->name()->data(), faultOffset, implicit_cast<void*>(fp)); /* * If the handledCount is non-zero, we've already this fault once * while unwinding this frema, and popped all eval stack * temporaries the first time it was thrown (before entering a * fault funclet). When the Unwind instruction was executed in * the funclet, the eval stack must have been left empty again. * * (We have to skip discardStackTemps in this case because it will * look for FPI regions and assume the stack offsets correspond to * what the FPI table expects.) */ if (fault.m_handledCount == 0) { discardStackTemps(fp, stack, faultOffset); } if (const EHEnt* eh = fp->m_func->findEH(faultOffset)) { switch (checkHandlers(eh, fp, pc, fault)) { case UnwindAction::ResumeVM: // We've kept our own copy of the Fault, because m_faults may // change if we have a reentry during unwinding. When we're // ready to resume, we need to replace the fault to reflect // any state changes we've made (handledCount, etc). g_vmContext->m_faults.back() = fault; return UnwindAction::ResumeVM; case UnwindAction::Propagate: break; } } // We found no more handlers in this frame, so the nested fault // count starts over for the caller frame. fault.m_handledCount = 0; auto const lastFrameForNesting = fp == fp->arGetSfp(); tearDownFrame(fp, stack, pc, faultOffset); if (lastFrameForNesting) { FTRACE(1, "unwind: reached the end of this nesting's ActRec chain\n"); break; } } return UnwindAction::Propagate; }
void pushFault(Exception* e) { Fault f; f.m_faultType = Fault::Type::CppException; f.m_cppException = e; g_vmContext->m_faults.push_back(f); FTRACE(1, "pushing new fault: {}\n", describeFault(f)); }
void pushFault(const Object& o) { Fault f; f.m_faultType = Fault::Type::UserException; f.m_userException = o.get(); f.m_userException->incRefCount(); g_vmContext->m_faults.push_back(f); FTRACE(1, "pushing new fault: {}\n", describeFault(f)); }
/* * Unwinding proceeds as follows: * * - Discard all evaluation stack temporaries (including pre-live * activation records). * * - Check if the faultOffset that raised the exception is inside a * protected region, if so, if it can handle the Fault resume the * VM at the handler. * * - Check if we are handling user exception in an eagerly executed * async function. If so, pop its frame, wrap the exception into * failed StaticWaitHandle object, leave it on the stack as * a return value from the async function and resume VM. * * - Failing any of the above, pop the frame for the current * function. If the current function was the last frame in the * current VM nesting level, return UnwindAction::Propagate, * otherwise go to the first step and repeat this process in the * caller's frame. * * Note: it's important that the unwinder makes a copy of the Fault * it's currently operating on, as the underlying faults vector may * reallocate due to nested exception handling. */ UnwindAction unwind(ActRec*& fp, Stack& stack, PC& pc, Fault fault) { FTRACE(1, "entering unwinder for fault: {}\n", describeFault(fault)); SCOPE_EXIT { FTRACE(1, "leaving unwinder for fault: {}\n", describeFault(fault)); }; for (;;) { bool discard = false; if (fault.m_raiseOffset == kInvalidOffset) { /* * This block executes whenever we want to treat the fault as if * it was freshly thrown. Freshly thrown faults either were never * previosly seen by the unwinder OR were propagated from the * previous frame. In such a case, we fill in the fields with * the information from the current frame. */ always_assert(fault.m_raiseNesting == kInvalidNesting); // Nesting is set to the current VM nesting. fault.m_raiseNesting = g_context->m_nestedVMs.size(); // Raise frame is set to the current frame fault.m_raiseFrame = fp; // Raise offset is set to the offset of the current PC. fault.m_raiseOffset = fp->m_func->unit()->offsetOf(pc); // No handlers were yet examined for this fault. fault.m_handledCount = 0; // We will be also discarding stack temps. discard = true; } FTRACE(1, "unwind: func {}, raiseOffset {} fp {}\n", fp->m_func->name()->data(), fault.m_raiseOffset, implicit_cast<void*>(fp)); assert(fault.m_raiseNesting != kInvalidNesting); assert(fault.m_raiseFrame != nullptr); assert(fault.m_raiseOffset != kInvalidOffset); /* * If the handledCount is non-zero, we've already seen this fault once * while unwinding this frema, and popped all eval stack * temporaries the first time it was thrown (before entering a * fault funclet). When the Unwind instruction was executed in * the funclet, the eval stack must have been left empty again. * * (We have to skip discardStackTemps in this case because it will * look for FPI regions and assume the stack offsets correspond to * what the FPI table expects.) */ if (discard) { discardStackTemps(fp, stack, fault.m_raiseOffset); } do { const EHEnt* eh = fp->m_func->findEH(fault.m_raiseOffset); if (eh != nullptr) { switch (checkHandlers(eh, fp, pc, fault)) { case UnwindAction::ResumeVM: // We've kept our own copy of the Fault, because m_faults may // change if we have a reentry during unwinding. When we're // ready to resume, we need to replace the fault to reflect // any state changes we've made (handledCount, etc). g_context->m_faults.back() = fault; return UnwindAction::ResumeVM; case UnwindAction::Propagate: break; case UnwindAction::Return: not_reached(); } } // If we came here, it means that no further EHs were found for // the current fault offset and handledCount. This means we are // allowed to chain the current exception with the previous // one (if it exists). This is because the current exception // escapes the exception handler where it was thrown. } while (chainFaults(fault)); // If in an eagerly executed async function, wrap the user exception // into a failed StaticWaitHandle and return it to the caller. if (!fp->resumed() && fp->m_func->isAsyncFunction() && fault.m_faultType == Fault::Type::UserException) { tearDownEagerAsyncFrame(fp, stack, pc, fault.m_userException); g_context->m_faults.pop_back(); return pc ? UnwindAction::ResumeVM : UnwindAction::Return; } // We found no more handlers in this frame, so the nested fault // count starts over for the caller frame. auto const lastFrameForNesting = !fp->sfp(); tearDownFrame(fp, stack, pc); // Once we are done with EHs for the current frame we restore // default values for the fields inside Fault. This makes sure // that on another loop pass we will treat the fault just // as if it was freshly thrown. fault.m_raiseNesting = kInvalidNesting; fault.m_raiseFrame = nullptr; fault.m_raiseOffset = kInvalidOffset; fault.m_handledCount = 0; g_context->m_faults.back() = fault; if (lastFrameForNesting) { FTRACE(1, "unwind: reached the end of this nesting's ActRec chain\n"); break; } } return UnwindAction::Propagate; }