• 好好学习,天天向上~
  • 欢迎欢迎~

C语言陷阱与技巧第9节,在程序运行异常时,输出错误函数链路径

c语言 lcc 236℃ 0评论

上一节提到在C语言程序开发中,调用函数时需要根据判断其输出或者处理是否符合预期,才能做下一步的处理,否则很可能会引发灾难性的结果。例如下面程序员小明写的这段C语言代码:

double get_val()
{
    ...
    return val;
}
int fun1()
{
    ...
    double val = get_val();
    res = log(val);
    ...
}


小明定义了 get_val() 函数,按照他的设计,get_val() 函数应该返回一个正数。根据上面的C语言代码,在 fun1() 函数中,程序使用变量 val 接收了 get_val() 函数返回的值,然后计算了 val 的对数值。

不判断函数的返回值写程序,可能是有隐患的

虽然小明预计 get_val() 函数返回值应该是正数,但是只要 get_val() 函数有可能返回负数,fun1() 函数中的这种写法就不合适了,因为对负数计算对数值是没有意义的,遇到 get_val() 返回负值的情况时,程序就会崩溃。因此,在执行 log(val) 之前,应判断 val 是否为负数,按照上一节的分析,最好在异常处加上 printf() 打印语句:

int fun1()
{
    ...
    double val = get_val();
    if(val > 0)
        res = log(val);
    else{
        printf("we got a unexpected value\n");
        ...
    }
    ...
}


有些朋友不喜欢写类似于“printf("we got a unexpected value\n");”这样的提示信息,但是在嵌入式C语言程序开发中,调试的一个重要手段就是输出日志,如果没有错误日志输出,一旦程序没有按照预期运行,查找起问题代码就会比较痛苦。

所以就本例而言,非常建议加上 printf("we got a unexpected value\n"); 语句,这样就可以在C语言程序没有按照预期运行时,判断是否 get_val() 的返回值有异常。

现在问题又来了,如果在 fun2() 函数中也有类似的代码需要调用 get_val() 函数,并且也不期望 get_val() 函数输出负值。这种情况下,fun2() 函数要是也在异常代码处有 printf("we got a unexpected value\n"); 语句就不合适了,因为一旦 get_val() 出问题,很难断定是 fun1() 还是 fun2() 的调用出问题。

按照上一节介绍的技巧,在 fun1() 函数中的 printf() 加上 “fun1”,fun2() 函数中的 printf() 加上“fun2”,这样在C语言程序出问题时,就能知道出错路径了。不过这么做还是有些麻烦的,要是很多函数都用得到 get_val() 函数,并且都期望它输出正直,每个函数都写一遍 printf("funxx: we got a unexpected value\n"); 语句就太麻烦了,有没有更好的技巧呢?自然是有的,请继续往下看。

backtrace() 函数的使用

事实上,在C语言程序开发中,总会有几个非常底层的函数——其他函数都会调用它们,例如上面例子中的 get_val() 函数,以及上一节中的 cond() 函数。要是底层函数能在出错时,将“函数调用链”(例如上例中的 main->fun1->get_val)打印出来,那么调用它的函数就都省去了写输出信息的麻烦了。

backtrace() 函数的作用就在于此,它的C语言代码原型如下:

int backtrace(void **buffer, int size);

在 Linux 终端输入 man 命令,可以得到 backtrace() 函数的使用说明:

backtrace() 函数可以将“函数调用链”的信息保存在 buffer 里,size 参数则表示最长保存多长的调用链,函数返回调用链的实际长度。实际上,buffer 里保存的是调用链中各个函数栈帧的对应地址,不过可以使用 backtrace_symbols() 函数将相应的地址转换为函数名。

上一节我们在 fun1() 和 fun2() 函数中添加了不同的打印语句,用于在程序出现异常时区分错误信息。相关C语言代码如下,请看


编译并执行C语言程序,得到如下输出:

# gcc t.c 
# ./a.out 
fun2 get unexpected cond
cond is false
# ./a.out 
fun1 get unexpected cond
cond is false
# ./a.out 
cond is true

现在在 cond() 函数中使用 backtrace() 系列函数,相关C语言代码如下,请看:

上面的C语言代码为了演示清晰,没有做很多的错误判断。

编译修改后的C语言代码并执行,得到如下输出:

# gcc t.c -g -rdynamic
# ./a.out 

---------- backtrace ---------

./a.out(cond+0x90) [0x400afd]
./a.out(fun1+0x12) [0x400b89]
./a.out(main+0xe) [0x400c13]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f6e8110ef45]
./a.out() [0x4009a9]

fun1 get unexpected cond (t.c line:39)
cond is false


从输出信息可以看出,即使没有 fun1() 函数中的打印语句,在C语言程序出现异常时,也可以根据 cond() 函数输出的调用函数链,得到程序的出错路径。

而且通过调用函数链推断程序的出错路径,看起来更加清晰。

封装 dump 库

从上面的C语言代码可以看出,在程序运行异常时,使用 backtrace() 函数族可以很方便输出相关的函数调用链。不过在C语言程序开发中,常常不止一个底层函数,如果每个底层函数都像 cond() 函数那样写 backtrace 相关代码,就太麻烦了,而且重复的代码也容易出错。所以,我们完全可以将 backtrace 相关代码封装成一个库,相关C语言代码如下,请看:

void dump()
{
    void *buffer[30];
    int nptrs;
    int i;

    nptrs = backtrace(buffer, 10);
    char **strings = backtrace_symbols(buffer, nptrs);
    printf("\n---------- backtrace ---------\n\n");
    for(i=0; i<nptrs; i++)
        printf("%s\n", strings[i]);
    free(strings);
    printf("\n");
}

int cond()
{
    static int cnt = 0;
    srandom(time(NULL)+ cnt++);
    long val = random()%10;
    if(val < 5){
        dump();
        return -1;
    }
    return 0;
}


编译修改后的C语言代码并执行,得到如下输出:

...
./a.out(dump+0x1f) [0x400aec]
./a.out(cond+0x85) [0x400c00]
./a.out(fun1+0x12) [0x400c20]
./a.out(main+0xe) [0x400caa]
...

一切与预期一致,C语言程序运行异常时,程序打印出了出错的代码路径:

dump<-cond<-fun1<-main

多出的 dump 显然不会影响我们追踪问题代码,不过如果不希望有 dump 输出,可以将 dump 封装成宏,这个工作留给读者自己做了。

前面的章节介绍过,C语言中的宏运行时不会产生自己的栈帧,因此 backtrace() 函数不会将其当做一个函数。

小结

本节先是通过实例说明了嵌入式C语言程序开发中,调用函数时需要根据判断其输出或者处理是否符合预期,才能做下一步的处理,否则很可能会引发灾难性的结果。讨论了增加错误输出日志的重要性,然后介绍了 backtrace 函数族。

这里需要说明的是,backtrace 函数族在多线程的C语言程序中表现可能就没有这么好了,感兴趣的读者可以做实验试一试。而且,很多嵌入式系统为了节约资源,都是不支持 backtrace 函数族的,这种情况下,可以尝试使用编译器函数 builtin_return_address() 函数,限于篇幅,下一节再说了。


C语言陷阱与技巧第9节,在程序运行异常时,输出错误函数链路径 - 刘冲的博客(https://blog.popkx.com) 原创版权所有,转载请注明出处。
点赞 (0)分享 (0)
发表我的评论
取消评论
表情     7 + 3 = ? (必填)