函数调用关系

如何获取函数之间的调用情况

gcc编译选项-finstrument-functions可以记录运行时的函数调用和退出信息. 使用此选项时, 编译器会

  • 在所生成的汇编代码的每个函数的入口处增加对函数__cyg_profile_func_enter()的调用;
  • 在每个函数退出时增加对__cyg_profile_func_exit()的调用。

man gcc获取这些函数的详细使用方式。

栈调用关系跟踪

  • 使用glibc提供的backtrace函数和backtrace_symbols
  • 使用libunwind库
  • 根据函数调用规范, 如果有N个参数,将N个参数压栈自右向左顺序压栈,然后是将返回地址压栈,最后是将ebp压栈保存起来。 示例代码
    #include <dlfcn.h>
    void **getEBP( int dummy )
    {
    /*原理:参数的地址下面是返回地址,返回地址的下面是被保存的ebp的地址*/
       void ** ebp = (void ** )&dummy -2 ;
       return  * ebp;
    }
    void print_walk_backtrace( void )
    {
       int dummy;
       int frame = 0;
       Dl_info dlip;
       void ** ebp = getEBP( dummy );
       void ** ret = NULL;
       printf( "Stack backtrace:\n" );
       while( ebp )
       {
           ret = ebp + 1;
           dladdr( * ret, &dlip );
           printf("Frame %d:[ebp=0x%08x][ret=0x%08x] %s\n",
                   frame++, * ebp, * ret, dlip.dli_sname );
           ebp = (void**)(* ebp);
           // get the next frame pointer
       }
       printf("-------------------------------------\n");
    }
    
  • __builtin_return_address (info gcc: C Extensions:Return Address)

另一种方式(摘自网络)

/*
如果源代码编译时使用了-O1或-O2优化选项
可执行代码会把ebp/rbp/rsp寄存器当作普通寄存器使用,导致backtrace失败。
为了防止这种情况发生,可以在编译时使用-O2  -fno-omit-frame-pointer  或-Og 来避免优化中使用上述寄存器。
 */
#define STACKCALL __attribute__((regparm(1),noinline))  
void ** STACKCALL getEBP(void){  
        void ** ebp=NULL;  
        __asm__ __volatile__ ("mov %%rbp, %0;\n\t"  
                    :"=m"(ebp)      /*输出*/  
                    :      /*输入*/  
                    :"memory");     /*不受影响的寄存器*/  
        return (void ** )(* ebp);  
}  
int my_backtrace(void ** buffer,int size){  

    int frame=0;  
    void ** ebp;  
    void ** ret=NULL;  
    unsigned long long func_frame_distance=0;  
    if(buffer!=NULL && size >0)  
    {  
        ebp=getEBP();  
        func_frame_distance=(unsigned long long)(* ebp) - (unsigned long long)ebp;  
        while(ebp&& frame<size  
            &&(func_frame_distance< (1ULL<<24)) /*assume function ebp more than 16M*/
            &&(func_frame_distance>0))  
        {  
            ret=ebp+1;  
            buffer[frame++]= * ret;  
            ebp=(void**)(* ebp);  
            func_frame_distance=(unsigned long long)(* ebp) - (unsigned long long)ebp;  
        }  
    }  
    return frame;  
}

关于调试与优化

gcc编译参数-fomit-frame-pointer在函数调用时可以减少几条切换栈基址的指令, 并且基址寄存器被用作通用寄存器,会有性能提升。 但是由于堆栈中没有存储rbp,那么从add函数并不能追溯整个函数的调用栈。 参数-fno-omit-frame-pointer禁用把EBP当作通用寄存器来使用。

可以不通过RBP,而是使用libunwind。

abort不能backtrace

有版本的abort不能backtrace,因为gcc内置赋予了abort noreturn的属性,编译器会对其优化, 函数第一条指令不是push返回地址,因此backtrace时不知道abort返回地址。

递归导致栈空间满不能backtrace

使用sigaltstack 设置signal stack context 该函数函数原型如下:

#include <signal.h>
int sigaltstack(const stack_t * ss, stack_t * oss);
typedef struct {
    void  * ss_sp;     // Base address of stack
    int    ss_flags;   // Flags
    size_t ss_size;    // Number of bytes in stack
} stack_t;

要想创建一个新的可替换信号栈,ss_flags必须设置为0,ss_sp和ss_size分别指明可替换信号栈的起始地址和栈大小。系统定义了一个常数SIGSTKSZ,该常数对极大多数可替换信号栈来说都可以满足需求,MINSIGSTKSZ规定了可替换信号栈的最小值。 如果想要禁用已存在的一个可替换信号栈,可将ss_flags设置为SS_DISABLE。 而sigaltstack第一个参数为创建的新的可替换信号栈,第二个参数可以设置为NULL,如果不为NULL的话,将会将旧的可替换信号栈的信息保存在里面。

addr2line

backtrace函数的输出: XXX.so(func+offset)

nm XXX.so | grep func
addr2line -e XXX.so func_addr+offset

一个检查错误所在代码行的脚本:

#/bin/sh
#脚本接受一个参数,格式为 /path/to/booking.so(initfunc+0xea)
loc=$1
#获取动态库的名称
lib=$(echo $loc | awk -F[+\(\)] '{print $1}')
#获取函数名
func=$(echo $loc | awk -F[+\(\)] '{print $2}')
#获取偏移地址
offset=$(echo $loc | awk -F[+\(\)] '{print $3}')
#获取函数的地址
base=$(nm $lib | grep -w $func | awk '{print $1}')
echo "lib=$lib"
echo "func=$func"
echo "offset=$offset"
echo "FIND FUNC $(nm $lib | grep -w $func)"
echo "base=$base"
#计算函数地址+偏移的16进制值(发生coredump的地方)
let b=0x$base
let o=offset
let x=b+o
addr=$(echo "obase=16;$x" | bc)
echo "addr=$addr"
#使用addr2line获取所在代码及其行号
addr2line -e $lib 0x$addr
#可以加上-f参数现在所在的函数名

results matching ""

    No results matching ""