setjmp、longjump 和堆栈重建

Posted

技术标签:

【中文标题】setjmp、longjump 和堆栈重建【英文标题】:setjmp, longjump and stack reconstruction 【发布时间】:2016-10-28 17:03:52 【问题描述】:

通常 setjmp 和 longjmp 不关心调用堆栈 - 相反,函数只是保存和恢复寄存器。

我想使用 setjmp 和 longjmp 以便保留调用堆栈,然后在不同的执行上下文中恢复

EnableFeature( bool bEnable )

if( bEnable )

   if( setjmp( jmpBuf ) == 0 )
   
        backup call stack 
    else 
        return; //Playback backuped call stack + new call stack
   
 else 
   restore saved call stack on top of current call stack
   modify jmpBuf so we will jump to new stack ending
   longjmp( jmpBuf )

这种方法是否可行 - 有人可以为我编写一个示例代码吗?

为什么我自己相信它是可行的 - 因为我已经编码/原型化了类似的代码片段:

Communication protocol and local loopback using setjmp / longjmp

有两个调用堆栈同时运行 - 彼此独立。

但只是为了帮助您完成这项任务 - 我将为您提供获取本机和托管代码的调用堆栈的功能:

//
//  Originated from: https://sourceforge.net/projects/diagnostic/
//
//  Similar to windows API function, captures N frames of current call stack.
//  Unlike windows API function, works with managed and native functions.
//
int CaptureStackBackTrace2(
    int FramesToSkip,                   //[in] frames to skip, 0 - capture everything.
    int nFrames,                        //[in] frames to capture.
    PVOID* BackTrace                    //[out] filled callstack with total size nFrames - FramesToSkip
)

#ifdef _WIN64
    CONTEXT ContextRecord;
    RtlCaptureContext( &ContextRecord );

    UINT iFrame;
    for( iFrame = 0; iFrame < (UINT)nFrames; iFrame++ )
    
        DWORD64 ImageBase;
        PRUNTIME_FUNCTION pFunctionEntry = RtlLookupFunctionEntry( ContextRecord.Rip, &ImageBase, NULL );

        if( pFunctionEntry == NULL )
        
            if( iFrame != -1 )
                iFrame--;           // Eat last as it's not valid.
            break;
        

        PVOID HandlerData;
        DWORD64 EstablisherFrame;
        RtlVirtualUnwind( 0 /*UNW_FLAG_NHANDLER*/,
            ImageBase,
            ContextRecord.Rip,
            pFunctionEntry,
            &ContextRecord,
            &HandlerData,
            &EstablisherFrame,
            NULL );

        if( FramesToSkip > (int)iFrame )
            continue;

        BackTrace[iFrame - FramesToSkip] = (PVOID)ContextRecord.Rip;
    
#else
    //
    //  This approach was taken from StackInfoManager.cpp / FillStackInfo
    //  http://www.codeproject.com/Articles/11221/Easy-Detection-of-Memory-Leaks
    //  - slightly simplified the function itself.
    //
    int regEBP;
    __asm mov regEBP, ebp;

    long *pFrame = (long*)regEBP;               // pointer to current function frame
    void* pNextInstruction;
    int iFrame = 0;

    //
    // Using __try/_catch is faster than using ReadProcessMemory or VirtualProtect.
    // We return whatever frames we have collected so far after exception was encountered.
    //
    __try 
        for( ; iFrame < nFrames; iFrame++ )
        
            pNextInstruction = (void*)(*(pFrame + 1));

            if( !pNextInstruction )     // Last frame
                break;

            if( FramesToSkip > iFrame )
                continue;

            BackTrace[iFrame - FramesToSkip] = pNextInstruction;
            pFrame = (long*)(*pFrame);
        
    
    __except( EXCEPTION_EXECUTE_HANDLER )
    
    

#endif //_WIN64
    iFrame -= FramesToSkip;
    if( iFrame < 0 )
        iFrame = 0;

    return iFrame;
 //CaptureStackBackTrace2

我认为可以修改它以获得实际的堆栈指针(x64 - eSP 和 x32 - 已经有一个指针)。

【参考方案1】:

在法律上,setjmp/longjmp 只能用于在嵌套调用序列中“跳回”。这意味着它永远不需要真正“重建”任何东西——在你执行longjmp 的那一刻,一切都完好无损,就在堆栈中。您需要做的就是回滚在setjmplongjmp 之间积累的额外内容。

longjmp 自动为您执行“浅”回滚(即它只是将原始字节从堆栈顶部清除而不调用任何析构函数)。因此,如果您想进行适当的“深度”回滚(就像异常在调用层次结构中飞升时所做的那样),您必须在需要深度清理的每个级别上 setjmp,“拦截”跳转,执行手动清理,然后longjmp 进一步向上调用层次结构。

但这基本上是“穷人的异常处理”的手动实现。为什么要手动重新实现它?如果你想用 C 代码来做,我会理解的。但是为什么在 C++ 中呢?

附:是的,setjmp/longjmp 有时以非标准方式用于在 C 中实现协同例程,这确实涉及“跨越”跳转和堆栈恢复的原始形式。但这是非标准的。在一般情况下,由于我上面提到的相同原因,在 C++ 中实现会更加痛苦。

【讨论】:

在 C++ 中 setjmplongjmp 有很好的用例吗?异常处理的要点之一是确保析构函数正确执行,longjmp 完全绕过了这一点。 @JonathanLeffler:我可以看到在 C++ 中实现协程的可能性,您希望在某个范围内退出执行代码,但希望以可以重新定义的形式维护该范围-entered 以便稍后继续执行(即,当协程产生时,您确实想要为它或它的调用序列执行 dtors)。也就是说,我怀疑这可以通过(仅)便携式使用 setmmp/longjmp 来实现。 @JerryCoffin 是的,但即便如此,例如 mingws 和 MSVC 的 setjmp/longjmp 实现也存在问题,因此 boost 不会回复 setjmp/longjmp

以上是关于setjmp、longjump 和堆栈重建的主要内容,如果未能解决你的问题,请参考以下文章

c setjmp longjmp

C语言中利用setjmp和longjmp做异常处理

Linux信号详解:signal与sigaction函数

Linux信号详解:signal与sigaction函数

Linux信号详解:signal与sigaction函数

为啥调试器需要符号来重建堆栈?