写一下关于函数调用栈的一些相关知识,对于在Linux下面进行c/c++开发,在问题定位时 查看调用栈信息是一个非常常用的定位方法,因为根据调用关系,可以知道程序的执行流程是什么样子。如果 不能查看调用栈,光知道程序在某个函数出错,还是比较难定位,假如这个函数在很多地方被调用,就很难知道是由于什么场景导致错误发生的。所以通过查看调用栈,就可以知道调用关系,当然就知道是什么场景导致问题发生。
在gdb里面常用的命令式:bt 或全称“backtrace”就可以打印出当前函数执行的调用栈。如下面程序
(gdb) bt
#0 0x080486da in func_3 ()
#1 0x08048766 in func_int ()
#2 0x080487ae in func_str ()
#3 0x080487ff in main ()
前面数字式层次关系,#0表示最上面,即当前函数。除了第0层前面的地址表示是当前pc值,其他地址信息都表示函数调用的返回地址,例如上面:func_int() -->func_3() ,func_3执行完成后,接着会执行0x08048766地址的指令。
上面简单介绍了一下Linux下面通过调用栈来定位问题,但调用栈的获取原理,以及如何获取,估计还是有些人会不知道的。之所以要介绍这个,因为对于一些大型系统,完善的日志功能是必不可少的,否则系统出了问题,没有相关日志,是非常痛苦的。尤其是在某些环境下,如电信领域,大多数是服务器或应用程序都是跑在单板上,出现问题了,不会像我们调试小程序那样直接用gdb进行调试。虽然某些情况下可以使用gdb attach上出问题的进程,但大多数服务器单板没有相关调试工具。所以要定位问题,基本上都是通过分析日志。还有一种情况,就是那种随机性问题,如果没有日志,那就更加痛苦了,就算你能够使用gdb也无能为力。所以日子功能是非常重要的。所以log非常重要,但是log中通常需要记录哪些信息呢?通常情况会保护函数调用出错时,把传入该函数的参数信息,或者一些关键全局变量信息,有些时候会记录日期,对于服务器程序,日期一般都会记录。另外还有一个也相对重要的就是调用栈信息。
所以下面来介绍一下获取调用栈的原理和方法:
在Linux+x86环境,c语言函数调用时,下面介绍一下c函数是怎么压栈的:栈是从高地址向下低地址移动。通常一个函数中会有参数,局部变量等相关信息,这些信息是通过下面原则分配栈的:
1、栈的信息排布为:先是局部变量存放,调用函数返回值存放,然后是调用其它函数参数函数,
<pre name="code" class="cpp"> 如下面程序:
int B(int c, int d)
{
return c+d;
}
int A(int a, int b)
{
int c = 0xff, d = 0xffff;
return B(c, d);
}
通过objdump -d 命令可以查看反汇编指令
反汇编出来后如下:
00000079 <B>:
79: 55 push %ebp
7a: 89 e5 mov %esp,%ebp
7c: 8b 45 0c mov 0xc(%ebp),%eax
7f: 03 45 08 add 0x8(%ebp),%eax
82: 5d pop %ebp
83: c3 ret
0000084 <A>:
84: 55 push %ebp
85: 89 e5 mov %esp,%ebp
87: 83 ec 18 sub $0x18,%esp
8a: c7 45 fc ff 00 00 00 movl $0xff,-0x4(%ebp)
91: c7 45 f8 ff ff 00 00 movl $0xffff,-0x8(%ebp)
98: 8b 45 f8 mov -0x8(%ebp),%eax
9b: 89 44 24 04 mov %eax,0x4(%esp)
9f: 8b 45 fc mov -0x4(%ebp),%eax
a2: 89 04 24 mov %eax,(%esp)
a5: e8 fc ff ff ff call a6 <A+0x22>
aa: c9 leave
ab: c3 ret
从上面反汇编可以看出,在A调用B时,A的调用栈布局信息如下,
地址: |---------|
| ebp |<--| push %ebp -------------A-----------------
|---------| |
| c | | movl $0xff,-0x4(%ebp) ;A函数局部变量 c
|---------| |
| d | | movl $0xffff,-0x8(%ebp) ;A函数局部变量 d
|---------| |
| | |
|---------| |
| | |
|---------| |
c+%ebp | d | | mov %eax,0x4(%esp) ;A调用B函数时,准备好参数d
|---------| |
8+%ebp | c | | mov %eax,(%esp) ;A调用B函数时,准备好参数c
|---------| |<----%esp -------------A----------------
4+%ebp | retaddr | | A 调用B的返回地址,在执行call指令时,指令自动把call指令下一条压入这个地方。
|---------| |
%ebp-> | ebp |--- 对应于执行B函数 :push %ebp时,把在A函数运行时的ebp保存到该位置中。
|---------|
低地址:
后面B在执行mov 0xc(%ebp),%eax时,
简单用语言描述一下函数调用过程,就那上A调用B来说,首先A函数准备好参数,即把局部变量c,d放到栈上,然后执行call B(call a6 <A+0x22>)指令,call指令执行时默认会把当前指令的下一条指令压入栈中,然后执行B函数第一条指令即(push %ebp),所以当执行到B函数push %ebp时,栈的信息就是上面那种样子了。
知道一般程序是怎么压栈的,并且A函数调用B函数会把A函数中调用B函数的那条call指令的下一条指令压栈栈中,通常情况一个函数第一条指令都是push %ebp, 功能是保存调用函数栈帧,第2条指令时mov %esp , %ebp,即把esp赋值给ebp,即初始化当前函数栈帧。
在执行过程中,函数调用首先指向call执行,然后执行被调用者第一条指令(push %ebp),c语言函数调用通常都是这样情况的,而call指令又一个隐藏动作就是把下一指令(返回地址)压栈。所以在栈里面排布就是
---------
| ret_addr|
|---------|
| ebp |
|---------|
我们再看一下第二条指令,mov %esp , %ebp , 初始化当前函数栈帧。最终结果如下
---------
| ret_addr| |
|---------| |
| ebp |---/
|---------|<--|
| ... | |
|---------| |
| ret_addr| |
|---------| |
| ebp |---/
|---------|<--|
| ... | |
|---------| |
| ret_addr| |
|---------| |
| ebp |---/
|---------|---|
所以我们只要知道当前%epb的值,就可以通过上面那种图示方法进行调用栈分析了。有人会问为什么libc有函数实现了,自己就没有必要了,但libc只提供获取当前线程的调用栈信息,有些时候需要获取其他线程的调用栈信息,这个时候就需要自己分析实现了,总体思路一样,只需要获取到其它线程的%ebp信息即可,但通常情况在用户态是不能够获取%ebp寄存器的,可以借助内存模块来实现。
下面写的一个小程序,一种方法使用libc库里面backtrace函数实现,还有一种就是自己通过分析调用栈信息来实现。
#include <stdio.h>
#include <string.h>
#include <execinfo.h>
/* 获取ebp寄存器值 */
void get_ebp(unsigned long *ebp)
{
__asm__ __volatile__("mov %%ebp, %0 \r\n"
:"=m"(*ebp)
::"memory");
}
int my_backtrace(void **stack, int size, unsigned long ebp)
{
int layer = 0;
while(layer < size && ebp != 0 && *(unsigned long*)ebp != 0 && *(unsigned long *)ebp != ebp)
{
stack[layer++] = *(unsigned long *)(ebp+4);
ebp = *(unsigned long*)ebp;
}
return layer;
}
int func_3(int a, int b, int c)
{
void *stack_addr[10];
int layer;
int i;
char **ppstack_funcs;
/* 通过调用libc函数实现 */
layer = backtrace(stack_addr, 10);
ppstack_funcs = backtrace_symbols(stack_addr, layer);
for(i = 0; i < layer; i++)
printf("\n%s:%p\n", ppstack_funcs[i], stack_addr[i]);
/* 自己实现 */
unsigned long ebp = 0;
get_ebp(&ebp);
memset(stack_addr, 0, sizeof(stack_addr));
layer = my_backtrace(stack_addr, 10, ebp);
for(i = 0; i < layer; i++)
printf("\nmy: %p\n", stack_addr[i]);
free(ppstack_funcs);
return 3;
}
int func_int(int a, int b, int c, int d)
{
int aa,bb,cc;
int ret= func_3(aa,bb,cc);
return (a+ b+ c+ d + ret);
}
int func_str()
{
int a = 1, b = 2;
int ret;
ret = func_int(a, a, b, b);
return ret;
}
int B(int c, int d)
{
return c+d;
}
int A(int a, int b)
{
int c = 0xff, d = 0xffff;
return B(c, d);
}
int main(int argc, char *argv[])
{
int ret = func_str();
return 0;
}
程序编译加上-rdynaminc
否则获取调用栈只有地址,没有函数名信息。
运行结果:
<pre name="code" class="cpp">./exe() [0x80484dd]:0x80484dd
./exe() [0x80485ea]:0x80485ea
./exe() [0x8048632]:0x8048632
./exe() [0x8048683]:0x8048683
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0xb7dd5bd6]:0xb7dd5bd6
./exe() [0x8048401]:0x8048401
my: 0x804858a
my: 0x80485ea
my: 0x8048632
my: 0x8048683
my: 0xb7dd5bd6
分享到:
相关推荐
Linux内核的栈使用,问题的定位
摘 要:基于对Linux 下蓝牙协议栈BlueZ 源代码的分析,给出BlueZ的组织结构和特点。分析蓝牙USB 传输驱动机制和数据处理过程, 给出实现蓝牙设备驱动的重要数据结构和流程,并总结Linux 下开发蓝牙USB 设备驱动的...
很清楚的分析了一个数据包如何通过查询路由进入内核ipsec协议栈的处理、Linux 内核ipsec协议栈详细的加解密流程以及加解密完后如何将数据包发送出去。 文档中前半部分主要介绍一些关键的数据结构,及其相互之间的...
该图描述了网络数据从linux驱动接收数据一直到上层应用socket调用的整个流程,为很多搞linux网络朋友提供帮助。
在linux编写应用程序时,程序崩溃,可以通过该代码回溯程序崩溃之前都调用了那些函数,方便bug定位
linux kernel module打印指定进程栈信息,简单说明: http://blog.csdn.net/qq123386926/article/details/50524901
网络协议栈的实现基本采用TCP/IP的四层架构(链路、网络、传输、应用)。不过在实际学习中通常讲到的是5层架构(物理、链路、网络、传输、应用)。 BSD风格 BSD风格就是通常说的 socket、bind、connect、listen、...
linux内核协议栈调用解析,数据包走向流程。 网络数据包走向
通过扩展Linux系统调用跟踪工具strace,实现了启发式跟踪工具heuristic-strace,其能够实时发现和自动跟踪应用程序中通过网络通信的进程,形成进程创建关系图、进程网络通信关系图,并结合系统调用的栈回溯信息,...
本篇文章主要介绍了Linux如何查看进程栈信息示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
(1)Socket应用层的各种网络应用程序基本上都是通过LinuxSocket编程接口来和内核空间的网络协议栈通信的。LinuxSocket是从BSDSocket发展而来的,它是Linux操作系统的重要组成部分之一,它是网络应用程序的基础。从...
下面小编就为大家带来一篇浅谈在linux kernel中打印函数调用的堆栈的方法。小编觉得挺不错的。现在就分享给大家。也给大家做个参考。一起跟随小编过来看看吧
XBuildStudio© 是运行在Windows平台上的,专为LINUX/UNIX平台软件开发的可视化工具,类似于Visual C++ Studio开发环境。该工具能有效提升您在LINUX环境的开发... 调用栈、线程查看。 函数导航,快速定位[正在进行]。
协议栈内核代码静态分析,对linux内核中以太网设备驱动的注册、802.3、网络层、TCP、UDP层双向调用回路有详细注释分析。
文章详细介绍了linux 内核中有关socket 相关的协议栈的初始化部分。文章中各个函数调用关系清晰,重要代码逻辑都有中文注释及中文旁白解释。是一篇很好的学习linux 内核网络子系统的文章。
9.1 linux系统调用及用户编程接口(api) 257 9.1.1 系统调用 257 9.1.2 用户编程接口(api) 257 9.1.3 系统命令 258 9.2 arm linux文件i/o系统概述 258 9.2.1 虚拟文件系统(vfs) 258 9.2.2 通用...
4.7.1 空闲进程的内核态栈 187 4.7.2 空闲进程的内存描述符 188 4.7.3 空闲进程的硬件上下文 190 4.7.4 空闲进程的任务状态段 190 第5章 中断和异常 192 5.1 基础知识 193 5.1.1 中断和异常的定义 ...
当一个任务(进程)执行系统调用而执行内核代码时,称进程处于内核内核态,此时处理器处于特权级最高的(0级)内核代码中执行,当进程处于内核态时,执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈...
XBuildStudio© 是运行在Windows平台上的,专为LINUX/UNIX平台软件开发的可视化工具,类似于Visual C++ Studio开发环境。该工具能有效提升您在LINUX环境的开发...调用栈、线程查看。 函数导航,快速定位[正在进行]。
获取当前的调用栈信息,结果存储在buffer中,返回值为栈的深度,参数size限制栈的最大深度,即最大取size步的栈信息。 char **backtrace_symbols(void *const *buffer, int size); 把backtrace获取的栈信息转化为...