一般情况下,函数运行时查看堆栈的方式是使用外部调试器,如GDB(bt命令)。但是,有时候为了分析程序的bug,(主要是长时间运行的程序分析),在程序出错时打印出函数的调用栈是非常有用的。1获取堆栈信息在glibc头文件execinfo.h中,声明了三个函数来获取当前线程的函数调用堆栈。#includeintbacktrace(void**buffer,intsize);char**backtrace_symbols(void*const*buffer,intsize);voidbacktrace_symbols_fd(void*const*buffer,intsize,intfd);使用时有几点需要注意:backtrace的实现依赖于栈指针(fp寄存器),gcc编译时或添加栈指针优化参数-fomit-frame后的任意非零优化级别(-On参数)-pointer不会正确获取程序堆栈信息;backtrace_symbols的实现需要符号表的支持,在gcc编译过程中需要加入-rdynamic参数;内联函数没有栈帧,在编译过程中在调用位置展开;尾调用Tail-callOptimization会复用当前的函数栈,而不是生成新的函数栈,这样会导致栈信息获取错误。各功能的介绍和说明如下1.1。backtraceint回溯(void**buffer,intsize);该函数用于获取当前线程的调用栈,获取到的信息会存放在buffer中,buffer是一个指针列表,参数size用于描述buffer数组的长度。返回值是实际得到的指针最大个数不超过size。缓冲区中的指针实际上是从栈中获取的返回地址,每个栈帧都有一个返回地址。一些编译器优化选项会干扰获取正确的调用堆栈。此外,内联函数没有栈帧;删除帧指针也会导致栈内容无法正确解析。1.2.backtrace_symbolschar**backtrace_symbols(void*const*buffer,intsize);backtrace_symbols将从backtrace函数中获取的信息转换为字符串数组。参数:buffer是从backtrace函数中得到的指针数组;size是数组中元素的数量(回溯的返回值)。返回值是指向字符串数组的指针,每个字符串包含与缓冲区中相应元素相关的可打印消息。它包括函数名、函数的偏移地址和实际返回地址。只有使用ELF二进制格式的程序才能获取函数名和偏移地址。可能需要传递相应的链接参数来支持函数名功能。在使用GNUld链接器的系统中,需要传递-rdynamic链接参数,-rdynamic可以用来通知链接器将所有符号添加到动态符号表中。该函数的返回值是malloc函数申请的空间,所以调用者必须使用free函数来释放指针。如果没有足够的内存可以申请,backtrace_symbols将返回NULL。示例1:/*gccbacktrace_symbols.c-obacktrace_symbols-rdynamic*//**#include**intbacktrace(void**buffer,intsize);**char**backtrace_symbols(void*const*buffer,intsize);**voidbacktrace_symbols_fd(void*const*buffer,intsize,intfd);*/#include#include#include/*获取回溯并将其打印到@code{stdout}。*/voidprint_trace(void){void*array[10];size_t尺寸;字符**字符串;大小我;大小=回溯(数组,10);strings=backtrace_symbols(数组,大小);if(NULL==strings){perror("backtrace_symbols");退出(退出失败);}printf("获得%zd个栈帧。\n",size);for(i=0;i#includevoiddemo_fn3(void){intnptrs;#defineSIZE100void*buffer[SIZE];nptrs=回溯(缓冲区,SIZE);printf("backtrace()返回了%d个地址\n",nptrs);backtrace_symbols_fd(缓冲区、nptrs、STDOUT_FILENO);}静态voiddemo_fn2(void){demo_fn3();}voiddemo_fn1(intncalls){if(ncalls>1)demo_fn1(ncalls-1);elsedemo_fn2();}intmain(void){demo_fn1(3);return0;}执行如下:$./backtrace_symbols_fdbacktrace()returned8addresses./backtrace_symbols_fd(demo_fn3+0x2e)[0x4008c5]./backtrace_symbols_fd[0x40091e]./backtrace_symbols_fd(demo_fn1+0x25)[0x4000946].0x1e)[0x40093f]./backtrace_symbols_fd(demo_fn1+0x1e)[0x40093f]./backtrace_symbols_fd(main+0xe)[0x400957]/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f4cmboldsbacks40(+0x29)[0x4007e9]2。发生段错误时会自动触发调用跟踪。当然这个backtrace也可以用来定位segmenterror的位置。通常,当程序出现段错误时,系统会向程序发送SIGSEGV信号,默认处理是退出函数。我们可以使用信号(SIGSEGV,&your_function);函数接管SIGSEGV信号的处理。发生段错误后,程序会自动调用我们准备好的函数,获取该函数中当前的函数调用栈。/*gccdump_stack.c-odump_stack-rdynamic-Wall-g*/#include#include#include#include#include#defineARRAY_SIZE(x)(sizeof(x)/sizeof(x[0]))voiddump_stack(void){void*array[30]={0};size_tsize=backtrace(array,ARRAY_SIZE(array));backtrace_symbols_fd(array,size,STDOUT_FILENO);}voidsig_handler(intsig){psignal(sig,"handler");转储堆栈();信号(信号,SIG_DFL);提高(信号);}voiddemo_fn3(void){*((volatileint*)0x0)=0;/*错误,SIGSEGV*/}voiddemo_fn2(void){demo_fn3();}voiddemo_fn1(void){demo_fn2();}intmain(intargc,constchar*argv[]){if(signal(SIGSEGV,sig_handler)==SIG_ERR)perror("无法捕获SIGSEGV");demo_fn1();return0;}执行如下:$./dump_stackhandler:分段错误./dump_stack(dump_stack+0x45)[0x400a5c]./dump_stack(sig_handler+0x1f)[0x400aba]/lib/x86_64-linux-gnu/libc.so.6(+0x354b0)[0x7f3440b2a4b0]./dump_(demo_fn3+0x9)[0x400adf]./dump_stack(demo_fn2+0xe)[0x400af6]./dump_stack(demo_fn1+0xe)[0x400b07]./dump_stack(main+0x38)[0x400b42]/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f3440b15830]./dump_stack(_start+0x29)[0x400969]Segmentationfault(coredumped)可以看出真正异常函数的位置在./dump_stack(demo_fn3+0x9)[0x400adf]可以用addr2line看这个位置在哪一行代码:$addr2line-C-f-e./dump_stack0x400adfdemo_fn3backtrace/dump_stack.c:28用objdump转储函数的反汇编信息。并用grep显示地址0x400adf前后9行的信息$objdump-DS./dump_stack|grep"400adf"400adf:c70000000000movl$0x0,(%rax)backtrace$objdump-DS./dump_stack|grep-9"400adf"0000000000400ad6:voiddemo_fn3(void){400ad6:55push%rbp400ad7:4889e5mov%rsp,%rbp*((volatileint*)0x0)=0;/*错误,SIGSEGV*/400ada:b800000000mov$0x0,%eax400adf:c70000000000movl$0x0,(%rax)}400ae5:90nop400ae6:5dpop%rbp400ae7:c3retq000000000040voiddemo_fn2(void){-D参数表示显示所有汇编代码-S表示显示对应源码同上,也可以看到错误行信息。3如果使用glibc2.1或更高版本,底层函数只能使用backtrace函数,因此GCC提供了两个内置函数来获取运行时函数调用栈中的返回地址和帧地址。void*__builtin_return_address(intlevel);获取当前函数级别level的返回地址,即该函数被其他函数调用,然后该函数执行完毕后返回。所谓返回地址就是调用时的地址(其实就是调用位置下一条指令的地址)。void*__builtin_frame_address(unsignedintlevel);获取当前函数的栈帧地址。/*gccbuiltin_address.c-obuiltin_address*/#includevoidshow_backtrace(void){void*ret=__builtin_return_address(1);void*caller=__builtin_frame_address(0);printf("retaddress[%p],calladdress[%p]\n",ret,caller);}voiddemo_fn2(void){show_backtrace();}voiddemo_fn1(void){demo_fn2();}intmain(void){demo_fn1();return0;}执行如下:$./builtin_addressretaddress[0x400551],calladdress[0x7ffed99b01c0]这两个宏有两个致命的问题:参数不能用变量;无法知道调用堆栈何时结束libunwind.c-olibunwind-lunwind-Wall-g*/#include//printf#include#defineUNW_LOCAL_ONLY//我们只需要本地展开。#includevoidshow_backt种族(无效){unw_cursor_t游标;unw_context_tuc;//字符缓冲区[4096];unw_getcontext(&uc);//存储寄存器unw_init_local(&cursor,&uc);//使用上下文初始化while(unw_step(&cursor)>0){//展开到旧的栈帧charbuf[4096];unw_word_t偏移量;unw_word_tip,sp;//读取寄存器,ripunw_get_reg(&cursor,UNW_REG_IP,&ip);//读取寄存器,rbpunw_get_reg(&cursor,UNW_REG_SP,&sp);//获取名称和偏移量unw_get_proc_name(&cursor,buf,sizeof(buf),&offset);//x86_64,unw_word_t==uint64_tprintf("0x%016lx<%s+0x%lx>\n",ip,buf,offset);}}voidsig_handler(intsig){psignal(sig,"handler");显示回溯();信号(信号,SIG_DFL);raise(sig);}voiddemo_fn3(void){*((volatileint*)0x0)=0;/*错误,SIGSEGV*/}voiddemo_fn2(void){demo_fn3();}voiddemo_fn1(void){demo_fn2();}intmain(void){if(signal(SIGSEGV,sig_handler)==SIG_ERR)perror("无法捕获SIGSEGV");demo_fn1();return0;}执行如下:$./libunwindhandler:Segmentationfault0x00005644ef805b8a0x00007f68ed646f200x00005644ef805baf0x00005644ef805bc10x00005644ef805bcd0x00005644ef805bfc0x00007f68ed629b97<__libc_start_main+0xe7>0x00005644ef80596a<_start+0x2a>Segmentationfault使用游标每次回溯一帧,直到没有可用的父栈帧使用addr2line查看错误行如下:$addr2line-C-f-e./libunwind0x55d61dfaebaf?????:0$addr2line-C-f-e./libunwind0xbafdemo_fn3/home/rlk/codes/libunwind.c:47如上,由于偏移地址是一个比较小的值,栈中的那个比较大,因此可以适当截断高地址。然后使用objdump试试结果:$objdump-DS./libunwind|grep-6"baf"voiddemo_fn3(void){ba6:55push%rbpba7:4889e5mov%rsp,%rbp*((volatileint*)0x0)=0;/*错误,SIGSEGV*/baa:b800000000mov$0x0,%eaxbaf:c70000000000movl$0x0,(%rax)}bb5:90nopbb6:5dpop%rbpbb7:c3retq0000000000000bb8:同上,可以正确找到错误位置,但是偏移地址也要尝试低地址。值得一提的是,代码中通过函数地址获取函数名是比较耗时的,所以每次采样都做这个操作会严重影响程序的执行效率。因此,使用这种方法进行性能分析是比较耗时的。邮箱:MingruiZhou@outlook.com