http://blog.sina.com.cn/s/blog_a0aacb430101ptwk.html
1. gcc在编译时提供了对函数进入及返回时刻进行hook的选项:
gcc -finstrument-functions -c file.c
使用该选项后,gcc会在编译过程中对函数入口及退出的地方插入函数:
void func()
{
__cyg_profile_func_enter(void *this_func, void *call_site); <--gcc 插入
....
__cyg_profile_func_exit(void *this_func, void *call_site); <--gcc 插入
}
a. 这两个函数不用在头文件中声明,因为函数原型是gcc预定义好的
b. 不需要插入hook的函数需要添加属性描述符:
__attribute__((__no_instrument_function__))
对于:
__cyg_profile_func_enter
__cyg_profile_func_exit
以及这两个函数中调用的当前编译的函数需要加此属性,(不加就会一直循环调用,然后堆栈一直加加加加…最后就段错误了)
c. 这两个函数和其他的函数没有区别,this_func为调用hook函数的函数入口地址,call_site自然是调用被hook函数的函数的入口地址
d. 在这两个函数中使用的__func__宏会用这两个函数名称替换,当然是看不到被hook函数的名称的,因此只有函数地址可以用
2. 一个显示函数调用层次的例子:
static int depth = 0;
void __attribute__((__no_instrument_function__))
__cyg_profile_func_enter(void *this_func, void *call_site)
{
char buf[30];
sprintf(buf, "%%%ds--> 0x%%8x enter\n", depth);
printf(buf, " ", this_func);
depth++;
}
void __attribute__((__no_instrument_function__))
__cyg_profile_func_exit(void *this_func, void *call_site)
{
char buf[30];
if (deepth > 0) depth--;
sprintf(buf, "%%%ds<-- 0x%%8x exit\n", depth);
printf(buf, " ", this_func);
}
把该文件和其他文件一起编译:
gcc -finstrument-functions -gstabs -c main.c
gcc -finstrument-functions -gstabs -c debug.c
gcc -o p main.o debug.o
执行p,就可以看到输出了
类似于如下的输出(新浪博客吃空格,所以我用缩进替换了空格,不要在意这种细节):
--> 0x00400880 enter
--> 0x00400ec0 enter
<-- 0x00400ec0 exit
<-- 0x00400880 exit
3. 但是怎么把显示的函数地址换成函数的名字?
这个问题在google上可以找到答案:
http://bigwhite.blogbus.com/logs/146953475.html
该文使用在hook函数中调用addr2line命令来实现把函数地址换成成对应的符号名称。
但是显然,addr2line对于后期处理是合适的,对于具有大量调用行为的程序是不适用的,尤其是用到定时器的程序或者网络程序。
===============================================================
下面就讲讲如何不使用addr2line来找到地址对应的符号
如果需要在程序运行时进行转换至少有三种方法:
a. 维护一个函数地址及函数名称的列表
这个方法可以说是速度最快的,因为只要把需要跟踪的函数加入列表,减少了大量输出,同时列表的访问速度是非常快的,做成hash表,只会增加数十条指令。
b. 直接调用bfd库分析符号表
这个库就是objdump使用的库,但是显然,需要在程序运行时加载文件,因为符号表没有加载到内存,因此可能会需要一个初始化函数。
c. 修改elf的section
让符号表在程序开始运行时就加载到内存就可以在任何时候访问符号表,这时自然可以使用库或者自己编码分析符号表了。
a要对代码做出大量修改,b要读取文件,而且bfd库看的我眼花了,c要修改elf结构,相对来说c是比较简单易行的。
4. 这里只说说第三种的做法,前面两种就暂时忽略吧
要完成这个目标需要做这两件事:
a. 把符号表加载到程序的LOAD表里头,让进程初始化的时候就加载符号表到内存
这件事6.828的lab1已经给了答案,需要修改链接脚本,参阅lab/kern/kernel.ld,这里只列出了需要修改的节:
.stab : {
PROVIDE(__STAB_BEGIN__ = .);
*(.stab);
PROVIDE(__STAB_END__ = .);
BYTE(0)
}
.stabstr : {
PROVIDE(__STABSTR_BEGIN__ = .);
*(.stabstr);
PROVIDE(__STABSTR_END__ = .);
BYTE(0)
}
需要注意的是,这两个节可能需要调整对齐的大小,在树莓派的freebsd上需要放在.text后面,X64上就随意了。
b. 解析符号表
6.828同样在lab1给出了答案,同时也给出了分析符号表的源文件kdebug.c。
这个文件内容比较多,就不列出来了,里面的代码基本不太需要修改就可以用,需要注意的是stab的结构,对于X64来说,需要把stab改成4,1,1,2,4字节类型的结构,因为虽然X64上指针是64位,objdump显示stab中的地址字段也是64位,但是实际上stab中是用32位表示地址的,bfd中的代码还信誓旦旦的说X64的stab结构声明需要修改,害的老子调试了好久,简直坑爹。
对于arm平台的编译过程如下:
gcc -finstrument-functions -gstabs -c main.c
gcc -finstrument-functions -gstabs -c debug.c
gcc -Xlinker --script=armelf_fbsd.x -o p main.o debug.o
5.下面是输出函数名称的例子:
… 这里插入kdebug.c中的函数以及需要的头文件内容
static int depth = 0;
void __attribute__((__no_instrument_function__))
__cyg_profile_func_enter(void *this_func, void *call_site)
{
struct Eipdebuginfo E;
char buf[30];
debuginfo_eip((uintptr_t) this_func, &E);
sprintf(buf, "%%%ds--> %%.%ds_enter\n", depth, E.eip_fn_namelen);
printf(buf, " ", E.eip_fn_name);
depth++;
}
void __attribute__((__no_instrument_function__))
__cyg_profile_func_exit(void *this_func, void *call_site)
{
struct Eipdebuginfo E;
char buf[30];
if (depth > 0) depth--;
debuginfo_eip((uintptr_t) this_func, &E);
sprintf(buf, "%%%ds<-- %%.%ds_exit\n", depth, E.eip_fn_namelen);
printf(buf, " ", E.eip_fn_name);
}
下面是在树莓派上运行的0W-httpd函数调用跟踪,可以看到有很多繁杂的信息,其实只要去掉不需要插入hook函数的文件的对应的-finstrument-functions选项就可以消除大量的输出。
PS:添加这个文件后0W-httpd的一个http请求响应时间从20ms(-O3)增加到100ms(-O3)左右,相对于dtrace的pid还算可以接受。继续优化的话可以添加缓存以及合并输出,这点是dtrace所不能做到的。
0W-httpd中一次http请求的处理过程:
如何使用gcc跟踪函数调用