C语言陷阱与技巧第9节,在程序运行异常时,输出错误函数链路径
发表于: 2019-04-28 08:30:59 | 已被阅读: 24 | 分类于: C语言
上一节提到在C语言程序开发中,调用函数时需要根据判断其输出或者处理是否符合预期,才能做下一步的处理,否则很可能会引发灾难性的结果。例如下面程序员小明写的这段C语言代码:
double get_val()
{
...
return val;
}
int fun1()
{
...
double val = get_val();
res = log(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语言程序没有按照预期运行时,判断是否 get_val() 的返回值有异常。
现在问题又来了,如果在 fun2() 函数中也有类似的代码需要调用 get_val() 函数,并且也不期望 get_val() 函数输出负值。这种情况下,fun2() 函数要是也在异常代码处有 printf("we got a unexpected value\n"); 语句就不合适了,因为一旦 get_val() 出问题,很难断定是 fun1() 还是 fun2() 的调用出问题。
backtrace() 函数的使用
事实上,在C语言程序开发中,总会有几个非常底层的函数——其他函数都会调用它们,例如上面例子中的 get_val() 函数,以及上一节中的 cond() 函数。要是底层函数能在出错时,将“函数调用链”(例如上例中的 main->fun1->get_val)打印出来,那么调用它的函数就都省去了写输出信息的麻烦了。
backtrace() 函数的作用就在于此,它的C语言代码原型如下:
int backtrace(void **buffer, int size);
在 Linux 终端输入 man 命令,可以得到 backtrace() 函数的使用说明:
上一节我们在 fun1() 和 fun2() 函数中添加了不同的打印语句,用于在程序出现异常时区分错误信息。相关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
而且通过调用函数链推断程序的出错路径,看起来更加清晰。
封装 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;
}
...
./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 函数族的,这种情况下,可以尝试使用编译器函数