1.应用场景
ARM开发过程中经常进程运行着出现段错误,这时候单纯靠加日志打log效率太低。使用gdb的话,由于APP进程太多,生成的core的文件特别大,而且gdb在arm板子也不好单步调试,不太友好还是pass掉。
目前使用段错误捕捉SIGSEGV信号,通过backtrace和backtrace_symbols函数进行堆栈信息定位,再使用addr2line工具将指令的地址和可执行映像转换成文件名、函数名和源代码行数。
root@chenwr-pc:/home/workspace/chenwr/study/test# ./out
./out
[2020/07/21 09:38:15.249517 INFO]: [libcwr.c:0193]:
=========>>>catch signal 11 <<<=========
[2020/07/21 09:38:15.265993 INFO]: [libcwr.c:0170]: dummy_function
/home/workspace/chenwr/study/test/test.c:35
[2020/07/21 09:38:15.270385 INFO]: [libcwr.c:0170]: main
/home/workspace/chenwr/study/test/test.c:44
Segmentation fault (core dumped)
2.实现代码
C代码接口。
/*******************************************************************
** 函数名: run_shell_cmd
** 函数描述: shell命令执行判断,保存执行结果。
** 参数: [in] cmd: shell命令;
** [out] result: shell命令执行结果;
** [in] len: result长度
** 返回: shell命令执行失败返回-1;执行成功返回shell命令结束码
** 说明:
** 如果/bin/sh不能执行,退出状态为127,比如执行命令rename,但系统没有安装rename该命令则返回值为127。
** 详细内容,man system进行查看。
********************************************************************/
INT32S run_shell_cmd(INT8S *cmd, INT8S *result, INT32S len)
{
INT32S rc = 0;
INT32S ret = -1;
INT32S offset = 0;
FILE *fd = NULL;
do {
fd = popen(cmd, "r");
if (NULL == fd) {
perror("popen error\n");
break;
}
while (NULL != fgets(result, len, fd)) {
offset = strlen(result);
result += offset;
}
rc = pclose(fd);
if (-1 == rc) {
perror("pclose error\n");
break;
}
if (!WIFEXITED(rc)) {
perror("Run command failed\n");
break;
} else {
ret = WEXITSTATUS(rc);
}
} while (0);
if (NULL == fd || -1 == rc) {
strncpy(result, strerror(errno), len);
//FK_TRACE_INFO("errno = %s\n", strerror(errno));
}
fd = NULL;
return ret;
}
/*******************************************************************
** 函数名: get_curprocess_name
** 函数描述: 获取当前进程名字
** 参数: [out] process_name: 进程名
** 返回: 成功返回0,失败返回-1。
********************************************************************/
INT32S get_curprocess_name(INT8S *process_name)
{
FILE *fd = NULL;
INT8S proc_pid_path[100] = {0};
INT8S buf[100] = {0};
//获取进程名字
snprintf(proc_pid_path, sizeof(proc_pid_path), "/proc/%d/status", getpid());
do {
fd = fopen(proc_pid_path, "r");
if (NULL == fd) {
FK_TRACE_ERROR("fopen %s fail\n", proc_pid_path);
break;
}
if (NULL == fgets(buf, sizeof(buf)-1, fd)) {
fclose(fd);
FK_TRACE_ERROR("get current process name fail\n");
break;
}
sscanf(buf, "%*s %s", process_name);
fclose(fd);
return EXIT_OK;
} while(0);
return EXIT_ERROR;
}
/*******************************************************************
** 函数名: print_coredumped_msg
** 函数描述: 输出段错误详细信息,具体到函数行数。
** 参数: [in] symbols: 符号信息 (backtrace_symbols的返回值);[in] catch_num: 捕获信息元素个数(backtrace的返回值)
** 返回: 成功返回0,失败返回-1。
** 说明:
** int backtrace(void **buffer, int size);
** 该函数获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针数组,参数size用来指定buffer中可以保存多少个void*元素。
** 函数的返回值是实际返回的void*元素个数。buffer中的void*元素实际是从堆栈中获取的返回地址。
**
** char **backtrace_symbols(void *const *buffer, int size);
** 该函数将backtrace函数获取的信息转化为一个字符串数组,参数buffer是backtrace获取的堆栈指针,size是backtrace返回值。
** 函数返回值是一个指向字符串数组的指针,它包含char*元素个数为size。
** 每个字符串包含了一个相对于buffer中对应元素的可打印信息,包括函数名、函数偏移地址和实际返回地址。
********************************************************************/
INT32S print_coredumped_msg(INT8S **symbols, INT32S catch_num)
{
INT8S tools[100] = "addr2line";
INT8S process_name[100] = {0};
INT8S cmd[100] = {0};
INT8S buf[100] = {0};
INT8S result[MAXBUF_SIZE] = {0};
INT8S *front_position = NULL;
INT8S *back_position = NULL;
INT8S *tmp = NULL;
INT32S ret, i;
//判断addr2line工具是否存在
ret = run_shell_cmd(tools, result, MAXBUF_SIZE);
if (127 == ret) {
FK_TRACE_ERROR("%s is not exist\n", tools);
return EXIT_ERROR;
}
//获取当前进程名
ret = get_curprocess_name(process_name);
if (ret) {
return EXIT_ERROR;
}
//打印段错误详细行数
for (i = 0; i < catch_num; i++) {
memset(buf, 0, sizeof(buf));
memset(cmd, 0, sizeof(cmd));
memset(result, 0, sizeof(result));
//处理字符串,找到地址
front_position = strstr(symbols[i], "[");
back_position = strstr(symbols[i], "]");
memcpy(&buf, front_position + 1, back_position - front_position - 1);
snprintf(cmd, 100, "%s -Cfe %s %s", tools, process_name, buf);
ret = run_shell_cmd(cmd, result, MAXBUF_SIZE);
if (-1 == ret) {
return EXIT_ERROR;
}
//结果包含??全部过滤掉
tmp = strstr(result, "??");
if (!tmp) {
FK_TRACE_INFO("%s\n", result);
}
}
front_position = NULL;
back_position = NULL;
tmp = NULL;
return EXIT_OK;
}
/*******************************************************************
** 函数名: catch_core_dump
** 函数描述: 捕捉段错误信息
** 参数: [in] signo: 信号
** 返回: 无
********************************************************************/
void catch_core_dump(INT32S signo)
{
INT32S catch_num;
void *buf[MAXBUF_SIZE] = {0};
INT8S **symbols = NULL;
FK_TRACE_INFO("\n=========>>>catch signal %d <<<=========\n", signo);
catch_num = backtrace(buf, MAXBUF_SIZE);
symbols = backtrace_symbols(buf, catch_num);
if (NULL == symbols) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
print_coredumped_msg(symbols, catch_num);
signal(signo, SIG_DFL); //恢复信号默认处理
raise(signo); //重新发送信号
free(symbols);
symbols = NULL;
}
/*******************************************************************
** 函数名: catch_core_dump_fd
** 函数描述: 捕捉段错误信息保存在文件中
** 参数: [in] signo: 信号
** 返回: 无
********************************************************************/
void catch_core_dump_fd(INT32S signo)
{
INT32S catch_num;
const INT8S file_path[] = "./core_dump.txt";
void *buf[MAXBUF_SIZE] = {0};
INT32S fd = 0;
fd = open(file_path, O_RDWR | O_CREAT, 777);
if (-1 == fd) {
FK_TRACE_ERROR("open %s fail\n", file_path);
}
FK_TRACE_INFO("\n=========>>>catch signal %d <<<=========\n", signo);
catch_num = backtrace(buf, MAXBUF_SIZE);
backtrace_symbols_fd(buf, catch_num, fd);
signal(signo, SIG_DFL); //恢复信号默认处理
raise(signo); //重新发送信号
close(fd);
}
makefile文件记得添加-g -rdynamic -funwind-tables -ffunction-sections -finstrument-functions这几个编译选项。
其次如果在ARM板子测试,需要交叉编译addr2line这个工具。
3.代码分析
3.1接口说明
头文件:
#include <execinfo.h>
函数原型:
int backtrace (void **buffer, int size);
char **backtrace_symbols (void *const *buffer, int size);
void backtrace_symbols_fd (void *const *buffer, int size, int fd);
函数说明:
backtrace()函数,backtrace 英文释义回溯,向后追踪。
获取函数调用堆栈帧数据,即回溯函数调用列表。数据将放在buffer中。我的理解是发生段错误时的函数调用堆栈信息存放在buffer中,size为这个buffer数组下标,表示最多能保存多少个函数信息,如果内存富裕的情况下,这个buffer和size最好还是搞大点。
backtrace_symbols()函数,symbols为符号表。参数buffer是从backtrace()函数获取的数组指针,size是该数组中的元素个数(backtrace()函数的返回值)。
该函数主要功能:
将从backtrace()函数获取的地址转为描述这些地址的字符串数组。每个地址的字符串信息包含对应函数的名字、在函数内的十六进制偏移地址、以及实际的返回地址(十六进制)。需注意的是,当前,只有使用elf二进制格式的程序才能获取函数名称和偏移地址,此外,为支持函数名功能,可能需要添加相应的编译链接选项如-rdynamic;否则,只有十六进制的返回地址能被获取。backtrace_symbols()函数返回值是一个字符串指针,是通过malloc函数申请的空间,使用完后,调用者必需把它释放掉(直接free二级指针)。注:如果不能为字符串获取足够的空间,该函数的返回值为NULL。
我的理解是backtrace_symbols将backtrace获取的字符串通过符号表转化成实际地址,程序猿能看懂的内容。
[2020/07/20 15:22:28.657537 INFO]: [test.c:0044]:
=========>>>catch signal 11 <<<=========
[cwr] buf = H▒▒A▒Ɖ▒▒▒▒▒E▒▒I▒▒▒▒
[cwr] symbols = ./out(catch_core_dump_t+0x54) [0x4012a4]
[cwr] buf = H▒▒
[cwr] symbols = /lib/x86_64-linux-gnu/libc.so.6(+0x36cb0) [0x7fd22965dcb0]
[cwr] buf = ▒%
[cwr] symbols = ./out(dummy_function+0x13) [0x401063]
[cwr] buf = H▒t▒
[cwr] symbols = ./out(main+0x4f) [0x400f4f]
[cwr] buf = ▒▒蔢
[cwr] symbols = /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7fd229648f45]
[cwr] buf = ▒▒ `
[cwr] symbols = ./out() [0x400f8e]
Segmentation fault (core dumped)
root@chenwr-pc:/home/workspace/chenwr/study/test# objdump -T out
out: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000400f00 g DF .text 0000000000000065 Base main
0000000000401050 g DF .text 000000000000002e Base dummy_function
0000000000400d38 g DF .init 0000000000000000 Base _init
[cwr] symbols = ./out(dummy_function+0x13) [0x401063] 这下就能够搞清楚段错误的地址,然后通过addr2line这个工具转换成实际的地址。
=========>>>catch signal 11 <<<=========
[2020/07/20 15:33:33.156955 INFO]: [libcwr.c:0169]: dummy_function
/home/workspace/chenwr/study/test/test.c:70
[2020/07/20 15:33:33.162320 INFO]: [libcwr.c:0169]: main
/home/workspace/chenwr/study/test/test.c:78
backtrace_symbols_fd()函数,与backtrace_symbols()函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行。它不会调用malloc函数,因此,它可以应用在函数调用可能失败的情况下。
cat core_dump.txt
libcwr.so(catch_core_dump_fd+0xdc)[0x7f11cc3ada0c]
/lib/x86_64-linux-gnu/libc.so.6(+0x36cb0)[0x7f11cc019cb0]
./out(dummy_function+0x13)[0x400d43]
./out(main+0x27)[0x400c27]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0x7f11cc004f45]
./out[0x400c66]
参考资料:
linux backtrace()详细使用说明
objdump 符号表输出格式
gcc选项-g与-rdynamic的异同
现在就是有个疑问添加-g -rdynamic这些编译选项后,整个进程空间占用变得很大。实际应用过程中,大家是否都是开发debug阶段的时候开启,正式release版本关闭呢?
3.2 signal
头文件 #include <signal.h>
signal(参数1,参数2);
参数1:要处理的信号
参数2:信号处理方式
分三种,系统默认(SIG_DFL)、忽略(SIG_ING)和捕获。
signal(SIGSEGV, catch_core_dump_fd);为SIGSEGV信号安装新的处理函数。
SIGSEGV信号表示非法内存访问
Signal Description
SIGABRT 由调用abort函数产生,进程非正常退出
SIGALRM 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
SIGBUS 某种特定的硬件异常,通常由内存访问引起
SIGCANCEL 由Solaris Thread Library内部使用,通常不会使用
SIGCHLD 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略
SIGCONT 当被stop的进程恢复运行的时候,自动发送
SIGEMT 和实现相关的硬件异常
SIGFPE 数学相关的异常,如被0除,浮点溢出,等等
SIGFREEZE Solaris专用,Hiberate或者Suspended时候发送
SIGHUP 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送
SIGILL 非法指令异常
SIGINFO BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程
SIGINT 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程
SIGIO 异步IO事件
SIGIOT 实现相关的硬件异常,一般对应SIGABRT
SIGKILL 无法处理和忽略。中止某个进程
SIGLWP 由Solaris Thread Libray内部使用
SIGPIPE 在reader中止之后写Pipe的时候发送
SIGPOLL 当某个事件发送给Pollable Device的时候发送
SIGPROF Setitimer指定的Profiling Interval Timer所产生
SIGPWR 和系统相关。和UPS相关。
SIGQUIT 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程
SIGSEGV 非法内存访问
SIGSTKFLT Linux专用,数学协处理器的栈异常
SIGSTOP 中止进程。无法处理和忽略。
SIGSYS 非法系统调用
SIGTERM 请求中止进程,kill命令缺省发送
SIGTHAW Solaris专用,从Suspend恢复时候发送
SIGTRAP 实现相关的硬件异常。一般是调试异常
SIGTSTP Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程
SIGTTIN 当Background Group的进程尝试读取Terminal的时候发送
SIGTTOU 当Background Group的进程尝试写Terminal的时候发送
SIGURG 当out-of-band data接收的时候可能发送
SIGUSR1 用户自定义signal 1
SIGUSR2 用户自定义signal 2
SIGVTALRM setitimer函数设置的Virtual Interval Timer超时的时候
SIGWAITING Solaris Thread Library内部实现专用
SIGWINCH 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程
SIGXCPU 当CPU时间限制超时的时候
SIGXFSZ 进程超过文件大小限制
SIGXRES Solaris专用,进程超过资源限制的时候发送
关于信号的内容,就不总结了,参考其他大神的博客吧。
signal() 函数详解
Linux 信号(signal)
linux SIGSEGV 信号捕捉,保证发生段错误后程序不崩溃
3.3 编译选项
在Ubuntu gcc编译中不添加-funwind-tables -ffunction-sections -finstrument-functions也能够正常打印地址,但是在ARM开发板环境下不添加这几个选项就无法打印。
[2020/07/13 13:31:08.116897 INFO]: [libcwr.c:0192]:
=========>>>catch signal 11 <<<=========
Segmentation fault
-funwind-tables
unwind table,这个表记录了与函数相关的信息,共三个字段:函数的起始地址,函数的结束地址,一个 info block 指针。
-ffunction-sections
将每个函数或符号创建为一个sections,其中每个sections名与function或data名保持一致。即使compiler为每个function和data item分配独立的section。
-finstrument-functions
在每个函数的入口和出口处会各增加一个额外的hook函数的调用,增加的这两个函数分别为:
void __cyg_profile_func_enter (void *this_fn, void *call_site);
void __cyg_profile_func_exit (void *this_fn, void *call_site);
其中第一个参数为当前函数的起始地址,第二个参数为返回地址,即caller函数中的地址。
GCC的几个重要选项解释
使用gcc的-finstrument-functions选项进行函数跟踪
3.4 其他
获取当前进程名的方式。
printf("%s\n", argv[0]);
结果为./out。(out为我创建的进程名)
必须int main(int argc, char **argv) 这种形式才能打印而且还得字符串处理掉./ 换种兼容性比较好的方式。
通过获取进程信息来获取进程名。
sscanf 中%*s 作用是忽略字符串,直至到空格作为结束字样。刚好Name: 后面有个空格。