一、问题
有个打印log的函数,想知道该函数执行的时候,之前执行了哪些函数?
二、分析
在应用程序打印函数栈需要通过函数backtrace(),该函数对应头文件如下:
#include<execinfo.h> 1、三个与打印调用栈相关的函数
打印函数栈需要使用到以下3个函数
intbacktrace(void**buffer,intsize); 函数功能:用于获取当前线程的调用堆栈。参数:buffer:它是一个指针数组,函数获取的当前线程的调用堆栈将会被存放在buffer中。在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈 框架有一个返回地址。size:用来指定buffer中可以保存多少个void*元素。返回值:实际获取的指针个数,最大不超过size大小。
char** backtrace_symbols (void *const *buffer, int size);
函数功能:将从backtrace函数获取的信息转化为一个字符串数组。参数:buffer:从backtrace函数获取的数组指针。size:是该数组中的元素个数(backtrace函数的返回值)。返回值:是一个指向字符串数组的指针,它的大小同buffer相同。每个字符串包含了一个相对于buffer中对应元素的 可打印信息。它包括函数名,函数的偏移地址,和实际的返回地址。
注:
- 1、只有使用ELF二进制格式的程序才能获取函数名称和偏移地址。在其他系统,只有16进制的返回地址能被获取。另外,需要传递相应的标志给链接器,以能支持函数名功能即编译选项-rdynamic。
- 2、backtrace_symbols生成的字符串都是malloc出来的,最后需要free该块内存。
voidbacktrace_symbols_fd(void*const*buffer,intsize,intfd) 功能:backtrace_symbols_fd与backtrace_symbols函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行.它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况。参数:fd:通常填写STDOUT_FILENO
2. 链接库
在编译的时候需要加上**-rdynamic**选项。
-rdynamic Passtheflag-export-dynamictotheELFlinker,ontargetsthatsupportit.Thisinstructsthelinkertoaddallsymbols,notonlyusedones,tothedynamicsymboltable.Thisoptionisneededforsomeusesof"dlopen"ortoallowobtainingbacktracesfromwithinaprogram. 该选项让链接器将所有符号添加到动态符号表中,这样才能将函数地址翻译成函数名,否则打印的结果是不会打印函数名的。
另外,这个选项不会处理static函数,所以,static函数的符号无法得到。
3. 举例
#include<execinfo.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> voidfun1(); voidfun2(); voidfun3(); voidprint_stacktrace(); voidprint_stacktrace() { intsize=16; void*array[100]; intstack_num=backtrace(array,size); char**stacktrace=backtrace_symbols(array,stack_num); backtrace_symbols_fd(array,size,STDOUT_FILENO); #if0 char**stacktrace=backtrace_symbols(array,stack_num); for(inti=0;i<stack_num;++i) { printf("%s\n",stacktrace[i]); } free(stacktrace); #endif } voidfun1() { printf("stackstracebegin:\n"); print_stacktrace(); } voidfun2() { fun1(); } voidfun3() { fun2(); } intmain() { fun3(); } 编译运行gcc编译时加上-rdynamic参数,通知链接器支持函数名功能(不加-rdynamic参数则无函数名打印):
gcc123.c-orun-rdynamic-g 执行结果:
4. 补充 address2line
同一个函数可以在代码中多个地方调用,如果我们只是知道函数,要想知道在哪里调用了该函数,可以通过address2line命令来完成,我们用第2步中编译出来的test2来做实验(address2line的-f选项可以打出函数名, -C选项也可以demangle):
address2line
三、内核代码中如何打印函数栈?
在Linux内核中提供了一个可以打印出内核调用堆栈的函数 dump_stack()。
该函数在我们调试内核的过程中可以打印出函数调用关系,该函数可以帮助我们进行内核调试,以及让我们了解内核的调用关系。
1. 头文件
该函数头文件为:
#include<asm/ptrace.h> 使用方式:
直接在想要查看的函数中添加
dump_stack(); 2. 举例
测试代码如下:hello.c
1#include<linux/init.h> 2#include<linux/module.h> 3#include<asm/ptrace.h> 4 6MODULE_LICENSE("GPL"); 7MODULE_AUTHOR("PD"); 8voidaaa(inta); 9voidbbb(intb); 10voidccc(intc); 11 14voidccc(intc) 15{ 16printk(KERN_SOH"cccc\n"); 17dump_stack(); 18printk("cis%d\n",c); 19} 20voidbbb(intb) 21{ 22intc=b+10; 23printk(KERN_SOH"bbbb\n"); 24ccc(c); 25} 26voidaaa(inta) 27{ 28intb=a+10; 29printk(KERN_SOH"aaaa\n"); 30bbb(b); 31} 32 34staticinthello_init(void) 35{ 36inta=10; 37 38aaa(a); 39printk(KERN_SOH"hello_init\n"); 40 41return0; 42} 43staticvoidhello_exit(void) 44{ 45printk("hello_exit\n"); 46return; 47} 48 49module_init(hello_init);//insmod 50module_exit(hello_exit);//rmmod Makefile
ifneq($(KERNELRELEASE),) obj-m:=hello.o else KDIR:=/lib/modules/$(shelluname-r)/build PWD:=$(shellpwd) all: make-C$(KDIR)M=$(PWD)modules clean: rm-f*.ko*.o*.mod.o*.symvers*.cmd*.mod.c*.order endif 编译安装模块
dmesg-c make insmodhello.ko 【注意】 都在root权限下操作
结果
可以看到在函数ccc中使用dump_stack()打印出了ccc的函数调用栈。
在内核开发中,我们可以使用dump_stack()来打印相关信息,同时在内核源码学习中也可以用来了解函数调用关系。
原文地址:https://mp.weixin.qq.com/s/odyYVeDpO_1NcS8U_QE0AQ