沙特买f35:一种有趣的错误跟踪LOG

来源:百度文库 编辑:偶看新闻 时间:2024/04/29 12:45:42

一种有趣的错误跟踪LOG

我正式用C++做产品开发到现在大约有7、8年,从学C++算起的话有10年,经验应该算是比较丰富的。但是我很少写C++方面的技术文章,C++诞生到现在二十几年,经典书籍、技术文章数不胜数,能写的好像都被人写完了,找点值得一些的东西实在不容易。
在 前一家公司的时候,我负责做基础框架,用COM+实现业务逻辑层。COM+这种东西不像standalone的application,调试和错误定位 都很麻烦,而且它导出的接口是由外部模块执行,很多错误只能在后期才能发现。因此通常的排除故障的做法是在觉得可能出错的地方多输出一些LOG,通过 LOG信息逐步定位问题-这有点儿像嵌入式系统的调试。在复杂的场合下,知道在什么地方出错可能还不够,还得知道它是怎么执行到这一步的,换句话说,还得 记录代码执行的路径。但是从另一个方面来看,输出太多的信息不但会影响系统性能,而且无关的信息也会干扰故障的诊断。因此,显而易见的理想情况是,系统正 常时LOG越少越好,在出错的时候应该输出尽可能多的信息。我当时就想用C++做这么一个东西,可惜不久以后我就离开了那家公司,搞起了嵌入式,只留下一 个prototype。
如何才能做到仅在出错的时候输出错误信息?我的基本想法是,在那些比较重要的代码路径上记录一些信息,在函数退出的时候根据函数的返回值决定是否输出到 LOG文件,在出现异常的情况下,利用C++的栈对象会在异常展开时被析构这一事实,异常情况也能cover。看下面的例子:
bool bar()
{
    bool retval = false;
    APPLOG_FUNCTION_ENTRY2(retval, true);
    // do something
    return retval;
}
bool foo()
{
    APPLOG_FUNCTION_ENTRY;
    bar();
    throw 1; // throw an exception
    return true;
}
int main(int argc, char* argv[])
{
    try {
        foo();
    } catch(...){
    }
    return 0;
}
宏APPLOG_FUNCTION_ENTRY和APPLOG_FUNCTION_ENTRY2分别在栈上定义一个本地C++对象
,在函数foo()和bar()返回或者抛出异常时C++对象的析构函数会被调用,在析构函数里结合返回值和异常决定是否输出LOG。
在这里析构函数的实现是关键,它必须识别出正常路径的析构和异常展开的析构两种情况,这得用到一些汇编知识。正常路径下析构函数的调用汇编代码大致是这样:
call        AutoScopeTrace::~AutoScopeTrace这里的call在X86架构下是一个相对调用指令(0xE8)。异常展开路径下的析构函数的调用的汇编代码大致如下:
mov eax, AutoScopeTrace::~AutoScopeTrace
call eax
这个call是一个间接调用指令(0xD0)。这样根据call指令编码的不同,我们就可以区分正常路径析构和异常展开路径下的析构。然而又有一个问题, 如何在析构函数里获取调用它的代码地址呢?这个可以根据析构函数的返回地址(在栈上)来得到。然而在打开优化编译选项的情况,返回地址在栈上的位置并不确 定,如果析构函数可以带参数,我们可以用这种方法得到返回地址:
AutoScopeTrace::~AutoScopeTrace(int var)
{
    DWORD retaddr = (reinterpret_cast(&var))[-1];
}然而地球人都知道析构函数不能带参数。在VC中,一种解决办法是利用优化开关强制给这个析构函数生成frame pointer(ebp),然后用frame pointer定位返回地址:
#pragma optimize( "g" , off )
#pragma optimize( "y" , on )
AutoScopeTrace::~AutoScopeTrace()
{
// the first two instructions in the function are:
// push        ebp 
// mov         ebp,esp
}
#pragma optimize("", on)

这样[ebp+4]就是函数的返回地址,根据返回地址得到call指令编码,我们就能区分正常析构和异常析构。完整的析构函数的实现大致如下:

const BYTE INST_RELATIVE_CALL = 0xE8;
const BYTE INST_RELATIVE_CALL_LENGTH = 5;

inline static BOOL IsNormalCall(PBYTE callAddr, PBYTE entryAddr)
{
    if( callAddr[0] == INST_RELATIVE_CALL ) // verify if relative call
    {
        // verify call address
        // call/jump Addr + call/jump Offset + 5 = ret Addr + call/jump Offset = target function entry
        int callOffset = *(int*)(callAddr+1);
        PBYTE calcEntryAddr = callAddr+callOffset+INST_RELATIVE_CALL_LENGTH;
        return calcEntryAddr == entryAddr;
    }
    return FALSE;
}

// disable global optimization and setup frame pointer to get return address
#pragma optimize( "g" , off )
#pragma optimize( "y" , on )
AutoScopeTrace::~AutoScopeTrace()
{
    PBYTE callAddr, entryAddr;
    __asm {
        mov eax, [ebp+4];
        sub eax, 5;
        mov callAddr, eax;
        mov eax, AutoScopeTrace::~AutoScopeTrace;
        mov entryAddr, eax;
    }
    
    // being destructed under normal path?
    if( IsNormalCall(callAddr, entryAddr) )
    {
        if( resobj_ != NULL )
        {
            if( resobj_->result_ok() )
            {
                // succeeded, do nothing
            }
            else
            {
                // failed, log something
            }
        }
    }
    else
    {
        // exception occurred
        if( resobj_ == NULL )
        {
            // exception without return value
        }
        else
        {
            // exception with a return value
        }
    }
}
// use project default optimization settings
#pragma optimize("", on)

下面是前面的例子的LOG输出:
SCOPE TRACE started at Fri Jan 12 14:41:07 2007 in module: testscopetrace.exe(pid:2028, tid:3348)
testscopetrace.cpp(17): foo() Enter 
    testscopetrace.cpp(11): bar() Enter 
    testscopetrace.cpp(11): bar() Leave failed due to return value: bool=false 
testscopetrace.cpp(17): foo() Leave failed due to exception 

SCOPE TRACE stopped at Fri Jan 12 15:15:03 2007 in module: testscopetrace.exe(pid:2028, tid:3348)
如果bar()中retval为true,foo()中不抛出异常,LOG中不会记录信息。

这里可以下载一个比较完整的例子。

更新:
movieqiu网友回复说这个例子在VC6下编译通不过,这个我以前也发现了,但是没怎么管它。
支持VC6要解决两个问题。一是__FUNCTION__宏。VC6本身不支持__FUNCTION__,CodeProject的workaround是 在运行时通过Image Help API动态获取函数名,对于LOG来说性能代价太大,因此我不推荐这么做(实际上我给出的例子是不能用动态字串的)。最简单的workaround是把它 定义为空串,虽然可读性变差了点儿,但是因为源文件名和代码行都记录了,错误定位是没问题的:
#ifndef __FUNCTION__
#define __FUNCTION__ ""
#endif // __FUNCTION__

第二个问题是VC6不支持这样的用法:
mov eax, AutoScopeTrace::~AutoScopeTrace;实际上析构函数AutoScopeTrace::~AutoScopeTrace的地址并不是必须的。这里只是在正常析构时做一下冗余验证。但是如果想获取析构函数的地址的话,movieqiu网友给出的做法是错误的:
 PBYTE funcMask = this.~AutoScopeTrace;
上面这行在VS2005中是通不过编译的(this是指针,语法本身就不对)。在VC6中竟然真的可以通过,但显然是BUG,编译器编译出来的汇编代码如下:
mov         dword ptr [ebp-18h],0
[ebp-18]即变量funcMask(这句相当于funcMask=0;),显然不对。
要在VC6中获取析构函数的地址,work-around也是有的。我们知道正常路径下对析构函数的调用一定是相对调用(0xE8指令+偏移),因此可以通过析构函数被调用处的地址和偏移来计算析构函数的地址:
BOOL InitDtorAddr()
{
    APPLOG_FUNCTION_ENTRY;
    return TRUE;
}
static BOOL _Init = InitDtorAddr();
static PBYTE g_pDtorAddr = NULL;

AutoScopeTrace::~AutoScopeTrace()
{
    if (g_pDtorAddr)
        entryAddr = g_pDtorAddr;
    else
    {
        if( callAddr[0] == INST_RELATIVE_CALL )
        {
            int callOffset = *(int*)(callAddr+1);
            g_pDtorAddr = callAddr+callOffset+INST_RELATIVE_CALL_LENGTH;
        }
        return;
    }
}
具体做法可以看我重新上传的例子。分类: Windows编程, C/C++