当我们需要明确函数的调用关系时,可以在编译的时候使用gcc的编译选项-finstrument-functions来打印函数调用栈。
在编译时加上-finstrument-functions后,gcc会在函数内部增加两个hook函数的调用,具体就是在每个函数的入口处调用__cyg_profile_func_enter函数,在函数的出口处调用__cyg_profile_func_exit函数。两个函数的声明如下:
void __cyg_profile_func_enter (void *this_fn, void *call_site);
void __cyg_profile_func_exit (void *this_fn, void *call_site);
其中,参数this_fn为当前函数的起始地址,call_site为当前函数的返回地址,即caller函数中执行完当前函数的下一条指令的地址。这两个函数的具体实现,可以根据你的需求自行实现。
举个例子,我们有整体的测试源代码test.c如下:
#include <stdio.h>
#include <stdlib.h>
int add(int a, int b){
return a + b;
}
void print(int n){
printf("%d\n", n);
}
int main(){
print(add(1, 2));
return 0;
}
同时,为了自定义hook函数,我们还需要新增ftrace.c和ftrace.h,编译测试源文件test.c时需要将ftrace.c一起编译并链接成一个可执行文件。
#include <stdarg.h>
#include <stdlib.h>
#include "ftrace.h"
#define DEBUG_FILE_PATH "./ftrace.log"
void __attribute__((no_instrument_function))
debug_log(const char *format, ...)
{
FILE *fp;
va_list ap;
va_start(ap, format);
fp = fopen(DEBUG_FILE_PATH, "w");
if(NULL == fp)
{
printf("Can not open debug file.\n");
return;
}
vfprintf(fp, format, ap);
va_end(ap);
fflush(fp);
fclose(fp);
}
void __attribute__((no_instrument_function))
__cyg_profile_func_enter(void *this, void *call)
{
debug_log("Enter\n%p\n%p\n", this, call);
}
void __attribute__((no_instrument_function))
__cyg_profile_func_exit(void *this, void *call)
{
debug_log("Exit\n%p\n%p\n", this, call);
}
#ifndef __FTRACE_H__
#define __FTRACE_H__
void __attribute__((no_instrument_function)) debug_log(const char *format, ...);
void __attribute__((no_instrument_function)) __cyg_profile_func_enter(void*, void*);
void __attribute__((no_instrument_function)) __cyg_profile_func_enter(void*, void*);
#endif
注意,ftrace.h中声明的三个函数需要加上no_instrument_function的属性,不然自己追踪自己就会使程序陷入死循环,追踪函数中调用的函数也需要加上该属性。
1. 执行编译命令:
gcc test.c ftrace.c -g -finstrument-functions -o test
2. 运行可执行文件,生成trace的log用于函数调用栈的分析:
./test
补充ftrace.log的图片
3. 使用工具addr2line来转化log中的地址为具体的函数名和源文件行位置:
addr2line -e test -a 0x4007b8 -fp -s
编译一个shell脚本addr2line.sh批量转化ftrace.log中的地址:
#!/bin/sh
if [ $# != 3 ]; then
echo 'Usage: addr2line.sh executefile addressfile outputfile'
exit
fi;
rm -rf trans_ftrace.log
space_num=0
print_spaces() {
printf "%${1}s" ""
}
cat $2 | while read line
do
if [ "$line" = 'Enter' ]; then
read line1
read line2
print_spaces count
echo "-----> call" >> $3
print_spaces count
addr2line -e $1 -pf $line1 -s >> $3
((count++))
elif [ "$line" = 'Exit' ]; then
((count--))
read line1
read line2
print_spaces count
echo "<----- return" >> $3
print_spaces count
addr2line -e $1 -fp $line2 -s >> $3
fi;
done
./addr2line.sh test ftrace.log trans_ftrace.log