本文借鉴https://blog.csdn.net/u014608280/article/details/82669330,在他的基础上进行了完善和介绍在实际工程中的使用方法。
在阅读不熟悉大型工程特别是c++工程时,程序的回调,类的继承关系复杂会增加阅读难度。
在使用gcc/g++编译工程时使用-finstrument-functions标志会函数进入和退出时调用钩子函数。
void __cyg_profile_func_enter (void *this_fn, void *call_site);
void __cyg_profile_func_exit (void *this_fn, void *call_site);
我们通过实现这两个钩子函数在编译时加入编译标志可以在打印出当前函数的内存地址,从而通过addr2line工具找到具体的调用位置,将所有的地址串起来就可以形成完整的执行流程。
两个钩子函数的入口参数是函数指针第一个参数指向将要执行的位置,第二个参数指向调用的位置。但是两个位置指针都是内存地址,而不是相对程序起始的偏移地址,这里使用backtrace_symbols将地址进行转换输出。
我们首先重写钩子函数,一个两个程序文件我们可以直接将钩子写到程序中,但如果是一个大型工程且这必然是不方便的而且需要处理依赖关系。我们就想到了可以使用头文件。
在重写钩子函数时使用的printf直接打印到终端,也可以直接将这部分写入文件,但是我个人建议不要把钩子函数和程序输出分离开,这样不便于调试。日志文件的保存可以看我另一篇文章https://blog.csdn.net/cooper1024/article/details/119873836
finstrument.h
#ifndef __FINSTRUMENT_H_
#define __FINSTRUMENT_H_
#ifdef __cplusplus
extern "C"{
#endif
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
#include <unistd.h>
#include <stdlib.h>
static void __attribute__((no_instrument_function))
__cyg_profile_func_enter(void *this_func, void *call_func)
{
void* buffer[2]={ this_func, call_func };
char **point = backtrace_symbols(buffer, 2);
char call_excl[256]={0};
char call_adder[64]={0};
char this_excl[256]={0};
char this_adder[64]={0};
sscanf(point[1] ,"%[^(]%*[^+]+%[^)]", call_excl, call_adder);
sscanf(point[0] ,"%[^(]%*[^+]+%[^)]", this_excl, this_adder);
printf("From %s %s entry function %s %s\n", call_excl, call_adder, this_excl, this_adder);
free(point);
}
static void __attribute__((no_instrument_function))
__cyg_profile_func_exit(void *this_func, void *call_func)
{
void* buffer[2]={ this_func, call_func };
char **point = backtrace_symbols(buffer, 2);
char call_excl[256]={0};
char call_adder[64]={0};
char this_excl[256]={0};
char this_adder[64]={0};
sscanf(point[1] ,"%[^(]%*[^+]+%[^)]", call_excl, call_adder);
sscanf(point[0] ,"%[^(]%*[^+]+%[^)]", this_excl, this_adder);
printf("Exit function %s %s to %s %s\n", this_excl, this_adder, call_excl, call_adder);
free(point);
}
#ifdef __cplusplus
};
#endif
#endif
将两个钩子函数通过静态声明在头文件中的方式定义,在需要使用的文件中引入头文件即可。这种方式有一个优点就是我只要在需要跟踪的文件中引用头文件可以输出在改文件中的执行流程,在没有引用的文件中由于找不到这两个钩子函数的定义而不会执行钩子函数;但是在方便的同时有引入了一个问题,在一个大型工程中一个文件一个文件的去加会很麻烦单独加又不知道加在什么位置。这时我们就引入一个新的编译特性-include,使用-include会在每个文件中自动引入设置的头文件。
当程序执行后输出了大量的地址我们使用shll脚本直接将地址进行转换。
下面是实例
main.c
#include <stdio.h>
extern int test();
#ifdef __cplusplus
class Box
{
public:
void print(void );
};
// 成员函数定义
void Box::print(void)
{
printf("Hello C++\n");
}
#endif
int func1()
{
printf("this func1\n");
return 0;
}
int func(int a, int b)
{
func1();
test();
return a + b;
}
static inline void print(int n)
{
printf("%d\n", n);
}
int main()
{
#ifdef __cplusplus
Box Box1;
#endif
func(3, 4);
print(func(3,4));
#ifdef __cplusplus
Box1.print();
#endif
return 0;
}
test.c
#include <stdio.h>
int test()
{
printf("Hello world\n");
return 0;
}
Makefile
all:
gcc -o test_c main.c test.c -g -finstrument-functions -include finstrument.h
g++ -o test_cxx main.c test.c -g -finstrument-functions -include finstrument.h
test:
./test_c > log_c.txt
./test_cxx > log_cxx.txt
./add2line.sh log_c.txt backtrace_c.txt
./add2line.sh log_cxx.txt backtrace_cxx.txt
addr2line.sh
#!/bin/sh
if [ $# != 2 ]; then
echo 'Usage: addr2line.sh addressfile functionfile'
exit
fi;
echo > $2
while read line
do
TYPE=`echo $line | awk '{print $1}'`;
case $TYPE in
From)
EXCL=`echo $line | awk '{print $2}'`
ADDER=`echo $line | awk '{print $3}'`
addr2line -e $EXCL -fp $ADDER -s | sed 's/$/;/' >> $2
echo "-----> call{" >> $2
EXCL=`echo $line | awk '{print $6}'`
ADDER=`echo $line | awk '{print $7}'`
addr2line -e $EXCL -fp $ADDER -s | sed 's/$/;\n/' >> $2
;;
Exit)
EXCL=`echo $line | awk '{print $3}'`
ADDER=`echo $line | awk '{print $4}'`
addr2line -e $EXCL -fp $ADDER -s | sed 's/$/;/' >> $2
echo "}<----- return" >> $2
EXCL=`echo $line | awk '{print $6}'`
ADDER=`echo $line | awk '{print $7}'`
addr2line -e $EXCL -fp $ADDER -s | sed 's/$/;\n/' >> $2
;;
*)
echo $line | sed 's/$/;\n/' >> $2
esac;
done < $1
使用make
编译生成可执行文件,使用make test
将进行测试。log_c.txt
和log_cxx.txt
存放程序输出,backtrace_c.txt
和backtrace_cxx.txt
分别存放对应转换处理后的文件。backtrace_c.txt
和backtrace_cxx.txt
通过格式化工具格式化之后看会更加明了。
log_c.txt
From /lib/x86_64-linux-gnu/libc.so.6 0xe7 entry function ./test_c 0xcc0
From ./test_c 0xceb entry function ./test_c 0xc1b
From ./test_c 0xc47 entry function ./test_c 0xbde
Exit function ./test_c 0xbde to ./test_c 0xc47
From ./test_c 0xc51 entry function ./test_c 0x1106
Hello world
Exit function ./test_c 0x1106 to ./test_c 0xc51
Exit function ./test_c 0xc1b to ./test_c 0xceb
From ./test_c 0xcfa entry function ./test_c 0xc1b
From ./test_c 0xc47 entry function ./test_c 0xbde
Exit function ./test_c 0xbde to ./test_c 0xc47
From ./test_c 0xc51 entry function ./test_c 0x1106
Hello world
Exit function ./test_c 0x1106 to ./test_c 0xc51
Exit function ./test_c 0xc1b to ./test_c 0xcfa
From ./test_c 0xd01 entry function ./test_c 0xc76
7
Exit function ./test_c 0xc76 to ./test_c 0xd01
Exit function ./test_c 0xcc0 to /lib/x86_64-linux-gnu/libc.so.6 0xe7
格式化后的backtrace_c.txt
?? ??:0;
-----> call{
main 于 main.c:36;
main 于 main.c:41;
-----> call{
func 于 main.c:24;
func 于 main.c:26;
-----> call{
func1 于 main.c:19;
func1 于 main.c:19;
}<----- return
func 于 main.c:26;
func 于 main.c:27;
-----> call{
test 于 test.c:4;
Hello world;
test 于 test.c:4;
}<----- return
func 于 main.c:27;
func 于 main.c:24;
}<----- return
main 于 main.c:41;
main 于 main.c:41;
-----> call{
func 于 main.c:24;
func 于 main.c:26;
-----> call{
func1 于 main.c:19;
func1 于 main.c:19;
}<----- return
func 于 main.c:26;
func 于 main.c:27;
-----> call{
test 于 test.c:4;
Hello world;
test 于 test.c:4;
}<----- return
func 于 main.c:27;
func 于 main.c:24;
}<----- return
main 于 main.c:41;
main 于 main.c:45;
-----> call{
print 于 main.c:31;
7;
print 于 main.c:31;
}<----- return
main 于 main.c:45;
main 于 main.c:36;
}<----- return
?? ??:0;