`
rappy
  • 浏览: 43740 次
  • 性别: Icon_minigender_1
  • 来自: 天涯海角
文章分类
社区版块
存档分类
最新评论

[转]Linux调用栈

阅读更多
写一下关于函数调用栈的一些相关知识,对于在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网络栈剖析 #### 一、概述 Linux操作系统因其强大的网络功能而备受青睐,其网络栈基于BSD网络栈的设计并进行了改进优化。本篇文章旨在深入解析Linux网络栈的架构和实现机制,从socket到设备驱动程序,为...

    Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

    Linux操作系统中的栈是一种重要的数据结构,尤其在内存管理、函数调用以及多任务处理中扮演着至关重要的角色。在Linux系统中,栈可以分为进程栈、线程栈、内核栈以及中断栈。 首先,我们需要了解栈(Stack)的基本...

    Linux内核的栈使用,问题的定位

    Linux 内核栈使用与问题定位 Linux 内核栈使用是 Linux 内核或驱动开发人员常见的功能之一。栈回溯功能可以快速定位到在内核哪个函数崩溃,大概在函数什么位置,大大简化了问题排查过程。基于 MIPS、ARM 架构 Linux...

    Linux协议栈的实现

    ### Linux协议栈的实现 #### 一、概述 在深入探讨Linux协议栈的实现细节之前,我们先简要介绍Linux协议栈的基本概念及其在操作系统中的重要性。Linux协议栈是Linux内核的一部分,负责处理网络通信相关的任务。它...

    Linux协议栈初步分析之路由子系统

    Linux 协议栈路由子系统的初始化流程是一个复杂的过程,涉及到多个组件和函数的调用。通过对网络协议栈初始化流程的分析,我们可以更好地理解 Linux 协议栈路由子系统的工作机理,并为后续的研究和开发提供一个坚实...

    linux程序栈回溯

    这个库提供了C接口,可以在运行时动态获取调用栈信息,即使在没有GDB的情况下也能实现栈回溯。使用`libunwind`时,需要在代码中添加适当的钩子来捕获异常或信号,并调用库提供的API进行栈回溯。 在C++中,可以利用`...

    linux内核协议栈分析

    而数据包的发送流程则包括应用层发包和协议栈发包,应用程序通过系统调用将数据发送到网络层,然后经过协议栈的处理发送到网卡,并最终通过物理媒介传送到目的地。 本文档通过描述Linux内核协议栈的分层结构、关键...

    《LINUX内核网络栈源代码情景分析》笔记

    ### Linux内核网络栈源代码情景分析 #### 第1章:网络协议头文件分析 本章节主要关注Linux内核中的网络协议头文件及其相关内容。这些文件对于理解Linux网络栈的工作原理至关重要。 ##### include/linux/...

    深入分析linux tcp/ip协议栈

    在Linux中,这些协议通常由用户空间的库实现,如libcurl、libssl等,但内核也提供了诸如socket接口供应用程序调用。 在Linux中,网络数据包从上到下通过协议栈时,会经过socket接口、协议处理、设备驱动、物理媒介...

    linux-网络协议栈流程.pdf

    系统调用最终会转到内核空间的处理函数,开始网络协议栈的处理。 内核的网络协议栈处理涉及多个层面。在传输层,UDP和TCP协议具有各自的数据结构和操作集。例如,UDP的协议操作定义在`udp_prot`结构中,包括了诸如`...

    Linux下协议栈源码分析

    ### Linux 下协议栈源码分析 #### 一、概述 《Linux2.6协议栈源代码分析》是一本深入解析Linux内核网络协议栈的专著,主要关注于Linux 2.6版本的协议栈实现细节。该书不仅对网络协议栈进行了全面的介绍,还详细...

    linux-1.2.13 网络协议栈源码

    Linux内核并不直接处理这些协议,而是通过系统调用接口,如socket API,供用户空间的应用程序调用。这部分的源码主要分布在`net/socket`和`net/socketapi`目录。 学习这个源码时,可以关注以下几个关键点: - ...

    linux网络协议栈(UDP收发)

    Linux网络协议栈是操作系统核心的重要组成部分,它负责处理网络数据的传输、接收和协议转换。在本篇讨论中,我们将聚焦于Linux环境下的UDP(User Datagram Protocol)数据包的接收和发送过程,以及涉及到的关键技术...

    《linux内核网络栈源代码情景分析》.(曹桂平).[PDF].&ckook;.pdf.zip

    《Linux内核网络栈源代码情景分析》是曹桂平撰写的一本深入解析Linux内核网络处理机制的著作。这本书详细介绍了Linux操作系统如何处理网络数据包,从硬件接口到高层协议栈的每一个环节,帮助读者理解Linux网络内核的...

    linux协议栈深度解析

    7. **网络配置与管理**:解释如何通过系统调用或命令行工具来配置和监控协议栈的行为。 通过深入学习这份文档,读者不仅可以理解Linux协议栈的运作机制,还能具备分析和改进网络性能的能力,对于从事系统开发、网络...

    Linux 内核IPSec(xfrm)协议栈源码分析

    《Linux 内核 IPSec 协议栈源码分析》 1. 前言 在Linux内核中,IPSec(Internet Protocol Security)是一种网络安全协议,用于在IP层提供安全服务,包括加密和完整性保护。它基于XFRM(eXtensible Framework for ...

    linux网络协议栈源码实现

    Linux网络协议栈是操作系统核心部分,负责处理网络通信的关键组件。源码实现提供了深入理解其工作原理的机会。本文将详细探讨Linux中的`ifconfig`、`route`、`arp`和`rarp`命令,以及它们在网络协议栈中的作用。 `...

Global site tag (gtag.js) - Google Analytics