随着服务器的内存逐渐增大,从16GB、32GB逐步拓展到64GB、128GB甚至256GB或更多。大型程序在遇到一些致命错误(比如SIGSEGV段错误、stack overflow)时,受限于磁盘空间大小、IO速度、恢复时间等限制,往往不具备条件把内存映像(coredump)转储到磁盘上,这为后续排查出错原因带来了很大困难。这时候就需要程序在遇到致命错误时,能够及时输出诊断信息。一些语言的运行时,比如java、golang等内建了错误堆栈输出的功能。那么c语言程序如何实现错误堆栈输出的功能呢?
请移步到小宇的博客获取更多技术文章
先睹为快
启动程序,然后发送SIGSEGV信号来模拟程序遇到非法内存访问错误的情形。
$ ./main &
[new process 2018]
$ kill -11 2018
#0 0x00007f30d7487680 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
#1 0x0000000000400b6b in system_ex ()
#2 0x0000000000400c87 in run ()
#3 0x0000000000400dcb in run_gdb ()
#4 0x0000000000400f37 in GetCrashInfo ()
#5 <signal handler called>
#6 0x0000000000400f9a in main ()
我们看到了函数的堆栈,进一步还可以打出诸如本地变量,局部变量,寄存器等等信息。
基本思路
运行中的程序输出堆栈的方法有2种。
第一种:借助backtrace函数。
第二种:借助gdb。
backtrace函数
backtrace用法比较简单,但是功能也比较有限,只能输出基本的堆栈。
比如:
$ cc -rdynamic prog.c -o prog
$ ./prog 3
backtrace() returned 8 addresses
./prog(myfunc3+0x5c) [0x80487f0]
./prog [0x8048871]
./prog(myfunc+0x21) [0x8048894]
./prog(myfunc+0x1a) [0x804888d]
./prog(myfunc+0x1a) [0x804888d]
./prog(main+0x65) [0x80488fb]
/lib/libc.so.6(__libc_start_main+0xdc) [0xb7e38f9c]
./prog [0x8048711]
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void
myfunc3(void)
{
int j, nptrs;
#define SIZE 100
void *buffer[100];
char **strings;
nptrs = backtrace(buffer, SIZE);
printf("backtrace() returned %d addresses\n", nptrs);
/* The call backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO)
would produce similar output to the following: */
strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (j = 0; j < nptrs; j++)
printf("%s\n", strings[j]);
free(strings);
}
static void /* "static" means don't export the symbol... */
myfunc2(void)
{
myfunc3();
}
void
myfunc(int ncalls)
{
if (ncalls > 1)
myfunc(ncalls - 1);
else
myfunc2();
}
int
main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "%s num-calls\n", argv[0]);
exit(EXIT_FAILURE);
}
myfunc(atoi(argv[1]));
exit(EXIT_SUCCESS);
}
借助gdb
gdb是专为调试诞生的工具,更为强大。但是如何在程序中调用gdb?系统已经提供了system调用,但是该调用不能把程序的信息输出给调用者。需要我们自己写一个system调用,能够把子进程输出的信息发送给主进程。
基本思路是fork一个进程,在子进程execv运行gdb,attach到主进程,运行一系列调试命令来输出调试信息。把子进程输出的信息发送给主进程的方法,涉及到了多线程通信的问题。一个比较简单的解决方法是在主进程和子进程之间建立管道,在子进程里面把标准输出重定向到管道的写入端,主进程从管道读取信息。为此专门写了一个system_ex
函数来完成这个工作。
/*
* system_ex -- system with something extra
*
* like original system but can save the
* cmd output value to buf. the memory
* used by buf is allocated by system_ex
* using malloc and realloc, make sure
* you free the buf memory after the
* function returns.
*
* yshen@2016
* Argument:
* cmd: the command as a null terminated
* string to execute
* buf: the buffer to save the stdout
* printed by the command
* Return:
* on sucess system_ex returns the
* command exit code
* on