/*** *trnsctrl.cxx - Routines for doing control transfers * * Copyright (c) 1993-2001, Microsoft Corporation. All rights reserved. * *Purpose: * Routines for doing control transfers; written using inline * assembly in naked functions. Contains the public routine * _CxxFrameHandler, the entry point for the frame handler * *Revision History: * 05-24-93 BES Module created * 01-13-95 JWM NLG notifications now called from _CallSettingFrame(). * 04-10-95 JWM _CallSettingFrame() moved to lowhelpr.asm * 10-22-99 PML Add EHTRACE support * 11-30-99 PML Compile /Wp64 clean. * 01-31-00 PML Disable new warning C4851 * 02-14-00 PML C4851 in VC6PP is C4731 in VC7 * 03-02-00 PML Preserve callee-save regs across RtlUnwind (VS7#83643). * 03-03-00 PML No more C4851, it's only C4731 * 09-18-01 GB Support for exception specification (Provided by Arturl). * 03-18-02 PML Add anti-hacker security measures * ****/ #include #include #include #include #include #include #include #pragma hdrstop #include #pragma warning(disable:4311 4312) // x86 specific, ignore /Wp64 warnings #pragma warning(disable:4731) // ignore EBP mod in inline-asm warning #pragma warning(disable:4733) // ignore unsafe FS:0 modifications #ifdef _MT #define pFrameInfoChain (*((FRAMEINFO **) &(_getptd()->_pFrameInfoChain))) #else static FRAMEINFO *pFrameInfoChain = NULL; // used to remember nested frames #endif // // We use the random /GS security cookie to protect our private exception // registration records, CatchGuardRN and TranslatorGuardRN. That guards // against hackers trying to use CatchGuardHandler and TranslatorGuardHandler // with a spoofed registration record created via a buffer overrun. // extern "C" DWORD_PTR __security_cookie; ///////////////////////////////////////////////////////////////////////////// // // _JumpToContinuation - sets up EBP and jumps to specified code address. // // Does not return. // // NT leaves a marker registration node at the head of the list, under the // assumption that RtlUnwind will remove it. As it happens, we need to keep // it in case of a rethrow (see below). We only remove the current head // (assuming it is NT's), because there may be other nodes that we still // need. // void __stdcall _JumpToContinuation( void *target, // The funclet to call EHRegistrationNode *pRN // Registration node, represents location of frame ) { EHTRACE_ENTER_FMT1("Transfer to 0x%p", target); EHTRACE_RESET; register long targetEBP; #if !CC_EXPLICITFRAME targetEBP = (long)pRN + FRAME_OFFSET; #else targetEBP = pRN->frame; #endif __asm { // // Unlink NT's marker node: // mov ebx, FS:[0] mov eax, [ebx] mov FS:[0], eax // // Transfer control to the continuation point // mov eax, target // Load target address mov ebx, pRN // Restore target esp mov esp, [ebx-4] mov ebp, targetEBP // Load target frame pointer jmp eax // Call the funclet } } ///////////////////////////////////////////////////////////////////////////// // // _CallMemberFunction0 - call a parameterless member function using __thiscall // calling convention, with 0 parameters. // __declspec(naked) void __stdcall _CallMemberFunction0( void *pthis, // Value for 'this' pointer void *pmfn // Pointer to the member function ) { __asm { pop eax // Save return address pop ecx // Get 'this' xchg [esp],eax // Get function address, stash return address jmp eax // jump to the function (function will return // to caller of this func) } } ///////////////////////////////////////////////////////////////////////////// // // _CallMemberFunction1 - call a member function using __thiscall // calling convention, with 1 parameter. // __declspec(naked) void __stdcall _CallMemberFunction1( void *pthis, // Value for 'this' pointer void *pmfn, // Pointer to the member function void *pthat // Value of 1st parameter (type assumes copy ctor) ) { __asm { pop eax // Save return address pop ecx // Get 'this' xchg [esp],eax // Get function address, stash return address jmp eax // jump to the function (function will return // to caller of this func) } } ///////////////////////////////////////////////////////////////////////////// // // _CallMemberFunction2 - call a member function using __thiscall // calling convention, with 2 parameter. // __declspec(naked) void __stdcall _CallMemberFunction2( void *pthis, // Value for 'this' pointer void *pmfn, // Pointer to the member function void *pthat, // Value of 1st parameter (type assumes copy ctor) int val2 // Value of 2nd parameter (type assumes copy ctor w/vb) ) { __asm { pop eax // Save return address pop ecx // Get 'this' xchg [esp],eax // Get function address, stash return address jmp eax // jump to the function (function will return // to caller of this func) } } ///////////////////////////////////////////////////////////////////////////// // // _UnwindNestedFrames - Call RtlUnwind, passing the address after the call // as the continuation address. // // Win32 assumes that after a frame has called RtlUnwind, it will never return // to the dispatcher. // // Let me explain: // When the dispatcher calls a frame handler while searching // for the appropriate handler, it pushes an extra guard registration node // onto the list. When the handler returns to the dispatcher, the dispatcher // assumes that its node is at the head of the list, restores esp from the // address of the head node, and then unlinks that node from the chain. // However, if RtlUnwind removes ALL nodes below the specified node, including // the dispatcher's node, so without intervention the result is that the // current subject node gets poped from the list, and the stack pointer gets // reset to somewhere within the frame of that node, which is totally bogus // (this last side effect is not a problem, because esp is then immediately // restored from the ebp chain, which is still valid). // // So: // To get arround this, WE ASSUME that the registration node at the head of // the list is the dispatcher's marker node (which it is in NT 1.0), and // we keep a handle to it when we call RtlUnwind, and then link it back in // after RtlUnwind has done its stuff. That way, the dispatcher restores // its stack exactly as it expected to, and leave our registration node alone. // // What happens if there is an exception during the unwind? // We can't put a registration node here, because it will be removed // immediately. // // RtlUnwind: // RtlUnwind is evil. It trashes all the registers except EBP and ESP. // Because of that, EBX, ESI, and EDI must be preserved by this function, // and the compiler may not assume that any callee-save register can be used // across the call to RtlUnwind. To accomplish the former, inline-asm code // here uses EBX, ESI, and EDI, so they will be saved in the prologue. For // the latter, optimizations are disabled for the duration of this function. // #pragma optimize("g", off) // WORKAROUND for DOLPH:3322 void __stdcall _UnwindNestedFrames( EHRegistrationNode *pRN, // Unwind up to (but not including) this frame EHExceptionRecord *pExcept // The exception that initiated this unwind ) { EHTRACE_ENTER; void* pReturnPoint; EHRegistrationNode *pDispatcherRN; // Magic! __asm { // // Save the dispatcher's marker node // // NOTE: RtlUnwind will trash the callee-save regs EBX, ESI, and EDI. // We explicitly use them here in the inline-asm so they get preserved // and restored by the function prologue/epilogue. // mov esi, dword ptr FS:[0] // use ESI mov pDispatcherRN, esi } __asm mov pReturnPoint, offset ReturnPoint RtlUnwind(pRN, pReturnPoint, (PEXCEPTION_RECORD)pExcept, NULL); ReturnPoint: PER_FLAGS(pExcept) &= ~EXCEPTION_UNWINDING; // Clear the 'Unwinding' flag // in case exception is rethrown __asm { // // Re-link the dispatcher's marker node // mov edi, dword ptr FS:[0] // Get the current head (use EDI) mov ebx, pDispatcherRN // Get the saved head (use EBX) mov [ebx], edi // Link saved head to current head mov dword ptr FS:[0], ebx // Make saved head current head } EHTRACE_EXIT; return; } #pragma optimize("", on) ///////////////////////////////////////////////////////////////////////////// // // __CxxFrameHandler - Real entry point to the runtime; this thunk fixes up // the parameters, and then calls the workhorse. // extern "C" EXCEPTION_DISPOSITION __cdecl __InternalCxxFrameHandler( EHExceptionRecord *pExcept, // Information for this exception EHRegistrationNode *pRN, // Dynamic information for this frame void *pContext, // Context info (we don't care what's in it) DispatcherContext *pDC, // More dynamic info for this frame (ignored on Intel) FuncInfo *pFuncInfo, // Static information for this frame int CatchDepth, // How deeply nested are we? EHRegistrationNode *pMarkerRN, // Marker node for when checking inside // catch block BOOL recursive); // True if this is a translation exception // // This is a backwards-compatibility entry point. All new code must go to __CxxFrameHandler2 // extern "C" _CRTIMP __declspec(naked) EXCEPTION_DISPOSITION __cdecl __CxxFrameHandler( /* EAX=FuncInfo *pFuncInfo, // Static information for this frame */ EHExceptionRecord *pExcept, // Information for this exception EHRegistrationNode *pRN, // Dynamic information for this frame void *pContext, // Context info (we don't care what's in it) DispatcherContext *pDC // More dynamic info for this frame (ignored on Intel) ) { FuncInfo *pFuncInfo; EXCEPTION_DISPOSITION result; __asm { // // Standard function prolog // push ebp mov ebp, esp sub esp, __LOCAL_SIZE push ebx push esi push edi cld // A bit of paranoia -- Our code-gen assumes this // // Save the extra parameter // mov pFuncInfo, eax } EHTRACE_ENTER_FMT1("pRN = 0x%p", pRN); result = __InternalCxxFrameHandler( pExcept, pRN, pContext, pDC, pFuncInfo, 0, NULL, FALSE ); EHTRACE_HANDLER_EXIT(result); __asm { pop edi pop esi pop ebx mov eax, result mov esp, ebp pop ebp ret 0 } } extern "C" _CRTIMP __declspec(naked) EXCEPTION_DISPOSITION __cdecl __CxxFrameHandler2( /* EAX=FuncInfo *pFuncInfo, // Static information for this frame */ EHExceptionRecord *pExcept, // Information for this exception EHRegistrationNode *pRN, // Dynamic information for this frame void *pContext, // Context info (we don't care what's in it) DispatcherContext *pDC // More dynamic info for this frame (ignored on Intel) ) { FuncInfo *pFuncInfo; EXCEPTION_DISPOSITION result; __asm { // // Standard function prolog // push ebp mov ebp, esp sub esp, __LOCAL_SIZE push ebx push esi push edi cld // A bit of paranoia -- Our code-gen assumes this // // Save the extra parameter // mov pFuncInfo, eax } EHTRACE_ENTER_FMT1("pRN = 0x%p", pRN); result = __InternalCxxFrameHandler( pExcept, pRN, pContext, pDC, pFuncInfo, 0, NULL, FALSE ); EHTRACE_HANDLER_EXIT(result); __asm { pop edi pop esi pop ebx mov eax, result mov esp, ebp pop ebp ret 0 } } ///////////////////////////////////////////////////////////////////////////// // // __CxxLongjmpUnwind - Entry point for local unwind required by longjmp // when setjmp used in same function as C++ EH. // extern "C" void __FrameUnwindToState( // in frame.cpp EHRegistrationNode *pRN, // Dynamic information for this frame DispatcherContext *pDC, // More dynamic info for this frame (ignored on Intel) FuncInfo *pFuncInfo, // Static information for this frame __ehstate_t targetState); // State to unwind to extern "C" void __stdcall __CxxLongjmpUnwind( _JUMP_BUFFER *jbuf ) { EHTRACE_ENTER; __FrameUnwindToState((EHRegistrationNode *)jbuf->Registration, (DispatcherContext*)NULL, (FuncInfo *)jbuf->UnwindData[0], (__ehstate_t)jbuf->TryLevel); EHTRACE_EXIT; } ///////////////////////////////////////////////////////////////////////////// // // _CallCatchBlock2 - The nitty-gritty details to get the catch called // correctly. // // We need to guard the call to the catch block with a special registration // node, so that if there is an exception which should be handled by a try // block within the catch, we handle it without unwinding the SEH node // in CallCatchBlock. // struct CatchGuardRN { EHRegistrationNode *pNext; // Frame link void *pFrameHandler; // Frame Handler DWORD_PTR RandomCookie; // copy of __security_cookie FuncInfo *pFuncInfo; // Static info for subject function EHRegistrationNode *pRN; // Dynamic info for subject function int CatchDepth; // How deeply nested are we? #if defined(ENABLE_EHTRACE) && (_MSC_VER >= 1300) int trace_level; // Trace level to restore in handler #endif }; static EXCEPTION_DISPOSITION __cdecl CatchGuardHandler( EHExceptionRecord*, CatchGuardRN *, void *, void * ); void *_CallCatchBlock2( EHRegistrationNode *pRN, // Dynamic info of function with catch FuncInfo *pFuncInfo, // Static info of function with catch void *handlerAddress, // Code address of handler int CatchDepth, // How deeply nested in catch blocks are we? unsigned long NLGCode ) { EHTRACE_ENTER; // // First, create and link in our special guard node: // CatchGuardRN CGRN = { NULL, (void*)CatchGuardHandler, __security_cookie, pFuncInfo, pRN, CatchDepth + 1 #if defined(ENABLE_EHTRACE) && (_MSC_VER >= 1300) , __ehtrace_level #endif }; __asm { mov eax, FS:[0] // Fetch frame list head mov CGRN.pNext, eax // Link this node in lea eax, CGRN // Put this node at the head mov FS:[0], eax } // // Call the catch // void *continuationAddress = _CallSettingFrame( handlerAddress, pRN, NLGCode ); // // Unlink our registration node // __asm { mov eax, CGRN.pNext // Get parent node mov FS:[0], eax // Put it at the head } EHTRACE_EXIT; return continuationAddress; } ///////////////////////////////////////////////////////////////////////////// // // CatchGuardHandler - frame handler for the catch guard node. // // This function will attempt to find a handler for the exception within // the current catch block (ie any nested try blocks). If none is found, // or the handler rethrows, returns ExceptionContinueSearch; otherwise does // not return. // // Does nothing on an unwind. // static EXCEPTION_DISPOSITION __cdecl CatchGuardHandler( EHExceptionRecord *pExcept, // Information for this exception CatchGuardRN *pRN, // The special marker frame void *pContext, // Context info (we don't care what's in it) void * // (ignored) ) { #if defined(ENABLE_EHTRACE) && (_MSC_VER >= 1300) EHTracePushLevel(pRN->trace_level); #endif EHTRACE_ENTER_FMT1("pRN = 0x%p", pRN); __asm cld; // Our code-gen assumes this // // Validate our registration record, to prevent against hacker attacks. // if (pRN->RandomCookie != __security_cookie) { PER_FLAGS(pExcept) |= EXCEPTION_STACK_INVALID; return ExceptionContinueSearch; } EXCEPTION_DISPOSITION result = __InternalCxxFrameHandler( pExcept, pRN->pRN, pContext, NULL, pRN->pFuncInfo, pRN->CatchDepth, (EHRegistrationNode*)pRN, FALSE ); EHTRACE_HANDLER_EXIT(result); EHTRACE_RESTORE_LEVEL(true); return result; } ///////////////////////////////////////////////////////////////////////////// // // CallSEHTranslator - calls the SEH translator, and handles the translation // exception. // // Assumes that a valid translator exists. // // Method: // Sets up a special guard node, whose handler handles the translation // exception, and remembers NT's marker node (See _UnwindNestedFrames above). // If the exception is not fully handled, the handler returns control to here, // so that this function can return to resume the normal search for a handler // for the original exception. // // Returns: TRUE if translator had a translation (handled or not) // FALSE if there was no translation // Does not return if translation was fully handled // // Note: // This is also called in a special mode from TranslatorGuardHandler // to return the address of the continuation point, ExceptionContinuation, // a label inside CallSEHTranslator. We used to keep this address in the // TranslatorGuardRN, but that opens a security hole by allowing a buffer // overrun exploit to overwrite an EH registration record and fill in the // continuation point to vector wherever desired. // // The special mode is detected by a first argument having the value 0x123. // That is never a legitimate pointer and unambiguously indicates a special // case call. The 2nd arg in this case is treated as a void** to be used to // return the continuation address. // struct TranslatorGuardRN /*: CatchGuardRN */ { EHRegistrationNode *pNext; // Frame link void *pFrameHandler; // Frame Handler DWORD_PTR RandomCookie; // copy of __security_cookie FuncInfo *pFuncInfo; // Static info for subject function EHRegistrationNode *pRN; // Dynamic info for subject function int CatchDepth; // How deeply nested are we? EHRegistrationNode *pMarkerRN; // Marker for parent context void *ESP; // ESP within CallSEHTranslator void *EBP; // EBP within CallSEHTranslator BOOL DidUnwind; // True if this frame was unwound #if defined(ENABLE_EHTRACE) && (_MSC_VER >= 1300) int trace_level; // Trace level to restore in handler #endif }; static EXCEPTION_DISPOSITION __cdecl TranslatorGuardHandler( EHExceptionRecord*, TranslatorGuardRN *, void *, void * ); #define CSET_SPECIAL ((EHExceptionRecord *)0x123) #pragma optimize("g", off) // WORKAROUND for DOLPH:3322 BOOL _CallSETranslator( EHExceptionRecord *pExcept, // The exception to be translated EHRegistrationNode *pRN, // Dynamic info of function with catch void *pContext, // Context info (we don't care what's in it) DispatcherContext *pDC, // More dynamic info of function with catch (ignored) FuncInfo *pFuncInfo, // Static info of function with catch int CatchDepth, // How deeply nested in catch blocks are we? EHRegistrationNode *pMarkerRN // Marker for parent context ) { // // Process special case calling request - return address of internal // continuation label through pRN (which is actually a void** in this case) // if (pExcept == CSET_SPECIAL) { __asm { mov eax, offset ExceptionContinuation mov ecx, pRN mov [ecx], eax } return TRUE; } EHTRACE_ENTER; // // Create and link in our special guard node: // TranslatorGuardRN TGRN = { NULL, // Frame link (void*)TranslatorGuardHandler, __security_cookie, pFuncInfo, pRN, CatchDepth, pMarkerRN, NULL, // ESP NULL, // EBP FALSE // DidUnwind #if defined(ENABLE_EHTRACE) && (_MSC_VER >= 1300) , __ehtrace_level #endif }; __asm { // // Fill in the blanks: // mov TGRN.ESP, esp mov TGRN.EBP, ebp // // Link this node in: // mov eax, FS:[0] // Fetch frame list head mov TGRN.pNext, eax // Link this node in lea eax, TGRN // Put this node at the head mov FS:[0], eax } // // Call the translator; assume it will give a translation. // BOOL DidTranslate = TRUE; _EXCEPTION_POINTERS pointers = { (PEXCEPTION_RECORD)pExcept, (PCONTEXT)pContext }; __pSETranslator(PER_CODE(pExcept), &pointers); // // If translator returned normally, that means it didn't translate the // exception. // DidTranslate = FALSE; // // Here's where we pick up if the translator threw something. // Note that ESP and EBP were restored by our frame handler. // ExceptionContinuation: if (TGRN.DidUnwind) { // // If the translated exception was partially handled (ie caught but // rethrown), then the frame list has the NT guard for the translation // exception context instead of the one for the original exception // context. Correct that sequencing problem. Note that our guard // node was unlinked by RtlUnwind. // __asm { mov ebx, FS:[0] // Get the node below the (bad) NT marker mov eax, [ebx] // (it was the target of the unwind) mov ebx, TGRN.pNext // Get the node we saved (the 'good' marker) mov [ebx], eax // Link the good node to the unwind target mov FS:[0], ebx // Put the good node at the head of the list } } else { // // Translator returned normally or translation wasn't handled. // unlink our registration node and exit // __asm { mov eax, TGRN.pNext // Get parent node mov FS:[0], eax // Put it at the head } } EHTRACE_EXIT; return DidTranslate; } #pragma optimize("g", on) ///////////////////////////////////////////////////////////////////////////// // // TranslatorGuardHandler - frame handler for the translator guard node. // // On search: // This frame handler will check if there is a catch at the current level // for the translated exception. If there is no handler or the handler // did a re-throw, control is transfered back into CallSEHTranslator, based // on the values saved in the registration node. // // Does not return. // // On unwind: // Sets the DidUnwind flag in the registration node, and returns. // static EXCEPTION_DISPOSITION __cdecl TranslatorGuardHandler( EHExceptionRecord *pExcept, // Information for this exception TranslatorGuardRN *pRN, // The translator guard frame void *pContext, // Context info (we don't care what's in it) void * // (ignored) ) { #if defined(ENABLE_EHTRACE) && (_MSC_VER >= 1300) EHTracePushLevel(pRN->trace_level); #endif EHTRACE_ENTER_FMT1("pRN = 0x%p", pRN); __asm cld; // Our code-gen assumes this // // Validate our registration record, to prevent against hacker attacks. // if (pRN->RandomCookie != __security_cookie) { PER_FLAGS(pExcept) |= EXCEPTION_STACK_INVALID; return ExceptionContinueSearch; } if (IS_UNWINDING(PER_FLAGS(pExcept))) { pRN->DidUnwind = TRUE; EHTRACE_HANDLER_EXIT(ExceptionContinueSearch); EHTRACE_RESTORE_LEVEL(true); return ExceptionContinueSearch; } else { // // Check for a handler: // __InternalCxxFrameHandler( pExcept, pRN->pRN, pContext, NULL, pRN->pFuncInfo, pRN->CatchDepth, pRN->pMarkerRN, TRUE ); if (!pRN->DidUnwind) { // // If no match was found, unwind the context of the translator // _UnwindNestedFrames( (EHRegistrationNode*)pRN, pExcept ); } // // Transfer control back to establisher: // void *pContinue; _CallSETranslator(CSET_SPECIAL, (EHRegistrationNode *)&pContinue, NULL, NULL, NULL, 0, NULL); EHTRACE_FMT1("Transfer to establisher @ 0x%p", pContinue); EHTRACE_RESTORE_LEVEL(false); EHTRACE_EXIT; __asm { mov eax, pContinue mov ebx, pRN // Get address of registration node mov esp, [ebx]TranslatorGuardRN.ESP mov ebp, [ebx]TranslatorGuardRN.EBP jmp eax } // Unreached. return ExceptionContinueSearch; } } ///////////////////////////////////////////////////////////////////////////// // // _GetRangeOfTrysToCheck - determine which try blocks are of interest, given // the current catch block nesting depth. We only check the trys at a single // depth. // // Returns: // Address of first try block of interest is returned // pStart and pEnd get the indices of the range in question // TryBlockMapEntry* _GetRangeOfTrysToCheck( FuncInfo *pFuncInfo, int CatchDepth, __ehstate_t curState, unsigned *pStart, unsigned *pEnd ) { TryBlockMapEntry *pEntry = FUNC_PTRYBLOCK(*pFuncInfo, 0); unsigned start = FUNC_NTRYBLOCKS(*pFuncInfo); unsigned end = start; unsigned end1 = end; while (CatchDepth >= 0) { DASSERT(start != -1); start--; if ( TBME_HIGH(pEntry[start]) < curState && curState <= TBME_CATCHHIGH(pEntry[start]) || (start == -1) ) { CatchDepth--; end = end1; end1 = start; } } *pStart = ++start; // We always overshoot by 1 (we may even wrap around) *pEnd = end; DASSERT( end <= FUNC_NTRYBLOCKS(*pFuncInfo) && start <= end ); return &(pEntry[start]); } ///////////////////////////////////////////////////////////////////////////// // // _CreateFrameInfo - Save the frame information for this scope just before // calling the catch block. Put it at the head of the linked list. For // x86, all we need to save is the pointer to the exception object, so we // can determine when that object is no longer used by any nested catch // handler and can thus be destroyed on exiting from a catch. // // Returns: // Pointer to the frame info (the first input argument). // FRAMEINFO * _CreateFrameInfo( FRAMEINFO * pFrameInfo, PVOID pExceptionObject ) { pFrameInfo->pExceptionObject = pExceptionObject; pFrameInfo->pNext = pFrameInfoChain; pFrameInfoChain = pFrameInfo; return pFrameInfo; } ///////////////////////////////////////////////////////////////////////////// // // IsExceptionObjectToBeDestroyed - Determine if an exception object is still // in use by a more deeply nested catch frame, or if it unused and should be // destroyed on exiting from the current catch block. // // Returns: // TRUE if exception object not found and should be destroyed. // BOOL IsExceptionObjectToBeDestroyed( PVOID pExceptionObject ) { FRAMEINFO * pFrameInfo; for (pFrameInfo = pFrameInfoChain; pFrameInfo != NULL; pFrameInfo = pFrameInfo->pNext ) { if( pFrameInfo->pExceptionObject == pExceptionObject ) { return FALSE; } } return TRUE; } ///////////////////////////////////////////////////////////////////////////// // // _FindAndUnlinkFrame - Remove the frame information for this scope that was // inserted by _CreateFrameInfo. This should be the first frame in the list // (Ideally), but fibers deviate from ideal situation. // void _FindAndUnlinkFrame( FRAMEINFO * pFrameInfo ) { if (pFrameInfo == pFrameInfoChain) { pFrameInfoChain = pFrameInfo->pNext; return; } else { for (FRAMEINFO *pCurFrameInfo = pFrameInfoChain; pCurFrameInfo->pNext != NULL; pCurFrameInfo = pCurFrameInfo->pNext) { if (pFrameInfo == pCurFrameInfo->pNext) { pCurFrameInfo->pNext = pFrameInfo->pNext; return; } } } // Should never be reached. DASSERT(0); }