11

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

 3 years ago
source link: https://blog.popkx.com/c-language-traps-and-techniques-section-9-output-error-function-link-path-when-program-runs-abnormally/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

小明定义了 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");
        ...
    }
    ...
}
721f8a1e9987ec6ea71e0948293ca151.png

有些朋友不喜欢写类似于“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() 的调用出问题。

3dcdb3f8f5f0b900ac7be4991d65fe19.png

按照上一节介绍的技巧,在 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() 函数的使用说明:

4ba51b5dcf14327d5c4d854e41374182.png

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

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

dbbd30e4d8008b0a091d492749550677.png

51cf955934e2eb5261b775dcf3279a05.png
编译并执行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语言代码如下,请看:

fb9378520fd8885da3069feb4d87a1af.png

上面的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
236454c078c22d4cdadd289259db1f0e.png

从输出信息可以看出,即使没有 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;
}
17d8eadbc8a1001c62f2160174c7e774.png

编译修改后的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 封装成宏,这个工作留给读者自己做了。

ea3f82d6756071c35d35d9c26aa206b2.png

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK