参考技术A
程序core掉,要去debug,但是函数堆栈乱掉了,很恶心.....经过Google/wiki一番,找到两种解决办法.
x86ManualBacktrace
This tutorial will show you how to manually rebuild a backtrace with GDB on x86 using the stack frame pointer and current instruction pointer.
Consider the following gdb backtrace:
It\'s pretty clear that this is corrupted, evidence the following field:
Get the register information for the process:
On x86:
Stack Frame Layout for x86:
Color Key:blue
Using the current stack frame address from %ebp dump the stack above it:
Using this stack dump we know that the stack frame address in the %ebp register is the stack frame for the current instruction in the %eip register (unless this was a leaf function which didn\'t stack a frame but that\'s irrelevant for this discussion). Using the %ebp and %eip registers as a starting point we can build the first line in our backtrace rebuild:
When program control branches to a new function a stack frame is stacked and the callee function\'s last address is stored at the new frame\'s address %ebp + 4 (i.e. the last memory address in the callee\'s stack frame which is +4 from the current stack frame at %ebp). In order to get the caller\'s stack frame and instruction pointer just look at %ebp and %ebp+4:
In the stack find the memory at the stack frame in %ebp and the one at %ebp+4 (which will hold the callee\'s instruction pointer).
So using the address at 0xbf9ef358 and 0xbf9ef35c which is 0xbf9ef388 and 0x00d94cf7 continue to build our list:
Continue by looking at the next stack frame address 0xbf9ef388:
Keep doing this until we have a full back trace. You\'ll know you\'ve reached the bottom of the stack when the previous stack frame pointer is 0x00000000.
Here\'s a complete manually rebuilt stack frame:
amd64下面,无非就是寄存器变成rbp,字长增加了一倍.当然这边选择了手动寻找函数返回地址,然后info symbol打印出函数名,其实还可以通过gdb格式化来直接打印函数名:
gdb>x/128ag rbp内的内容
所以手动还原的办法就变得很简单:
gdb>info reg rbp *x86换成info reg ebp
gdb>x/128ag rbp内的内容 *x86换成 x/128aw ebp的内容
这样就能看到函数栈.如果你想解析参数是啥,也是可以的,只是比较麻烦,苦力活儿....想解析参数,就要知道栈的布局,可以参考这篇文章: http://blog.csdn.net/liigo/archive/2006/12/23/1456938.aspx
昨天和 海洋 一块研究了下函数调用栈,顺便写两句。不足或错误之处请包涵!
理解调用栈最重要的两点是:栈的结构,EBP寄存器的作用。
首先要认识到这样两个事实:
1、一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作。
2、几乎所有本地编译器都会在每个函数体之前插入类似如下指令:PUSH EBP; MOV EBP ESP;
即,在程序执行到一个函数的真正函数体时,已经有以下数据顺序入栈:参数,返回地址,EBP。由此得到类似如下的栈结构(参数入栈顺序跟调用方式有关,这里以 C语言 默认的CDECL为例):
“PUSH EBP”“MOV EBP ESP”这两条指令实在大有深意:首先将EBP入栈,然后将栈顶指针ESP赋值给EBP。“MOV EBP ESP”这条指令表面上看是用ESP把EBP原来的值覆盖了,其实不然——因为给EBP赋值之前,原EBP值已经被压栈(位于栈顶),而新的EBP又恰恰指向栈顶。
此时EBP寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原EBP入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的EBP值!
一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层EBP值。
由于EBP中的地址处总是“上一层函数调用时的EBP值”,而在每一层函数调用中,都能通过当时的EBP值“向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值”。如此形成递归,直至到达栈底。这就是函数调用栈。
编译器对EBP的使用实在太精妙了。
从当前EBP出发,逐层向上找到所有的EBP是非常容易的:
这个办法比较简单,很容易实践,但是有一个前提,如果栈的内容被冲刷干净了,你连毛都看不到(事实就是这样).所以你需要开始栈保护...至少你还能找到栈顶的函数...
gcc有参数: -fstack-protector 和 -fstack-protector-all,强烈建议开启....
**********************************************************************/
用打印方法调试
在客户项目那里混了半年,发现Top的客户确实是比我们牛逼。先说说调试的方法。
客户那边不依赖于GDB调试,因为他们可能觉得GDB依赖于系统 实现,不利于移植吧,所以客户的程序完全是依赖于打印调试的。这点很佩服他们的软件规划能力和项目管理,实现能力。说老实话,如果换了一家中国公司,每人 一个调试方法,要follow 一个rule是很不容易的。
完善的调试菜单。调试菜单并不难实现,只是一个打印和字符接受的函数。在其中控制是开放某些打印信息。
在每个模块中加上仔细规划打印输出,根据需求分成不同的基本。最好情况是在最高打印级别中可以可以发现所有的问题。打印级别可以很方便的动态控制。
函数调用LOG
如果能定位发生问题的模块,可以在该模块的在每个函数的调用入口加上打印一个函数名字+Enter,在返回处加上一个函数名字+Exit。对于每个模块用一个打印开关控制是否打印Trace信息。在调试菜单中控制这个打印开关。
如果懒得加打印语句,可以利用gcc 的-finstrument-functions 选项来快速的加入调试信息。-finstrumnet-function会是的编译器在函数调用的开始和退出处调用
可以利用这两个函数来跟踪函数调用的过程。
在 实现这两个函数时要加入 attribute ((no_instrument_function));以避免编译器再调用这两个函数的时候也调用__cyg_profile_func_enter 和 __cyg_profile_func_exit 而造成循环调用。
可以用dladdr()来获得this_fn的文件和函数名。code如下:
// 由于dladdr是GNU扩展,不是dl的标准函数,因此在这句话必须加在文件的开始处
关于打印堆栈。可以用
该方法需要编译器支持。
但是需要在编译的时候加上-rdynamic 否则只能输出在内存中的绝对地址。
在没有-rdynamic的时候,关于如何找到动态库的运行时地址还需要研究。
可以在系统运行的时候发送SIGSEGV给应用程序,产生当前进程的Coredump来获取动态库中函数的运行是地址。
用GDB获取backtrace的方法(在有-g选项的时候可以看到,不需要-rdynamic):
list <*address>
在没有-g的时候,又该如何呢?
来源:Linux社区 作者:astrotycoon
一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的。
在glibc头文件"execinfo.h"中声明了三个函数用于获取当前线程的函数调用堆栈。
int backtrace(void **buffer,int size)
该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数 size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小
在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址
注意:某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会导致无法正确解析堆栈内容
char ** backtrace_symbols (void *const *buffer, int size)
backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组. 参数buffer应该是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值)
函数返回值是一个指向字符串数组的指针,它的大小同buffer相同.每个字符串包含了一个相对于buffer中对应元素的可打印信息.它包括函数名,函数的偏移地址,和实际的返回地址
现在,只有使用ELF二进制格式的程序才能获取函数名称和偏移地址.在其他系统,只有16进制的返回地址能被获取.另外,你可能需要传递相应的符号给链接器,以能支持函数名功能(比如,在使用GNU ld链接器的系统中,你需要传递(-rdynamic), -rdynamic可用来通知链接器将所有符号添加到动态符号表中,如果你的链接器支持-rdynamic的话,建议将其加上!)
该函数的返回值是通过malloc函数申请的空间,因此调用者必须使用free函数来释放指针.
注意:如果不能为字符串获取足够的空间函数的返回值将会为NULL
void backtrace_symbols_fd (void *const *buffer, int size, int fd)
backtrace_symbols_fd与backtrace_symbols 函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行.它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况
下面是glibc中的实例(稍有修改):
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
/* Obtain a backtrace and print it to @code{stdout}. */
void print_trace (void)
{
void *array[10];
size_t size;
char **strings;
size_t i;
size = backtrace (array, 10);
strings = backtrace_symbols (array, size);
if (NULL == strings)
{
perror("backtrace_synbols");
Exit(EXIT_FAILURE);
}
printf ("Obtained %zd stack frames.\n", size);
for (i = 0; i < size; i++)
printf ("%s\n", strings[i]);
free (strings);
strings = NULL;
}
/* A dummy function to make the backtrace more interesting. */
void dummy_function (void)
{
print_trace ();
}
int main (int argc, char *argv[])
{
dummy_function ();
return 0;
}
输出如下:
Obtained 4 stack frames.
./execinfo() [0x80484dd]
./execinfo() [0x8048549]
./execinfo() [0x8048556]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x70a113]
我们还可以利用这backtrace来定位段错误位置。