• 函数调用栈的获取原理分析【转】


    转自:http://hutaow.com/blog/2013/10/15/dump-stack/

    上一篇文章《在Linux程序中输出函数调用栈》,讲述了在Linux中如何利用backtrace获取调用栈,本篇文章主要介绍一下获取函数调用栈的原理,并给出相应的实现方式。

    要了解调用栈,首先需要了解函数的调用过程,下面用一段代码作为例子:

    #include <stdio.h>
    
    int add(int a, int b) {
        int result = 0;
    
        result = a + b;
    
        return result;
    }
    
    int main(int argc, char *argv[]) {
        int result = 0;
    
        result = add(1, 2);
    
        printf("result = %d 
    ", result);
    
        return 0;
    }
    

    使用gcc编译,然后gdb反汇编main函数,看看它是如何调用add函数的:

    (gdb) disassemble main 
    Dump of assembler code for function main:
       0x08048439 <+0>:     push   %ebp
       0x0804843a <+1>:     mov    %esp,%ebp
       0x0804843c <+3>:     and    $0xfffffff0,%esp
       0x0804843f <+6>:     sub    $0x20,%esp
       0x08048442 <+9>:     movl   $0x0,0x1c(%esp)  # 给result变量赋0值
       0x0804844a <+17>:    movl   $0x2,0x4(%esp)   # 将第2个参数压栈(该参数偏移为esp+0x04)
       0x08048452 <+25>:    movl   $0x1,(%esp)      # 将第1个参数压栈(该参数偏移为esp+0x00)
       0x08048459 <+32>:    call   0x804841c <add>  # 调用add函数
       0x0804845e <+37>:    mov    %eax,0x1c(%esp)  # 将add函数的返回值赋给result变量
       0x08048462 <+41>:    mov    0x1c(%esp),%eax
       0x08048466 <+45>:    mov    %eax,0x4(%esp)
       0x0804846a <+49>:    movl   $0x8048510,(%esp)
       0x08048471 <+56>:    call   0x80482f0 <printf@plt>
       0x08048476 <+61>:    mov    $0x0,%eax
       0x0804847b <+66>:    leave  
       0x0804847c <+67>:    ret    
    End of assembler dump.
    

    可以看到,参数是在add函数调用前压栈,换句话说,参数压栈由调用者进行,参数存储在调用者的栈空间中,下面再看一下进入add函数后都做了什么:

    (gdb) disassemble add
    Dump of assembler code for function add:
       0x0804841c <+0>:     push   %ebp             # 将ebp压栈(保存函数调用者的栈基址)
       0x0804841d <+1>:     mov    %esp,%ebp        # 将ebp指向栈顶esp(设置当前函数的栈基址)
       0x0804841f <+3>:     sub    $0x10,%esp       # 分配栈空间(栈向低地址方向生长)
       0x08048422 <+6>:     movl   $0x0,-0x4(%ebp)  # 给result变量赋0值(该变量偏移为ebp-0x04)
       0x08048429 <+13>:    mov    0xc(%ebp),%eax   # 将第2个参数的值赋给eax(准备运算)
       0x0804842c <+16>:    mov    0x8(%ebp),%edx   # 将第1个参数的值赋给edx(准备运算)
       0x0804842f <+19>:    add    %edx,%eax        # 加法运算(edx+eax)结果保存在eax中
       0x08048431 <+21>:    mov    %eax,-0x4(%ebp)  # 将运算结果eax赋给result变量
       0x08048434 <+24>:    mov    -0x4(%ebp),%eax  # 将result变量的值赋给eax(eax将作为函数返回值)
       0x08048437 <+27>:    leave                   # 恢复函数调用者的栈基址(pop %ebp)
       0x08048438 <+28>:    ret                     # 返回(准备执行下条指令)
    End of assembler dump.
    

    进入add函数后,首先进行的操作是将当前的栈基址ebp压栈(此栈基址是调用者main函数的),然后将ebp指向栈顶esp,接下来再进行函数内的处理流程。函数结束前,会将函数调用者的栈基址恢复,然后返回准备执行下一指令。这个过程中,栈上的空间会是下面的样子:

    函数调用过程中栈的情况

    可以发现,每调用一次函数,都会对调用者的栈基址(ebp)进行压栈操作,并且由于栈基址是由当时栈顶指针(esp)而来,会发现,各层函数的栈基址很巧妙的构成了一个链,即当前的栈基址指向下一层函数栈基址所在的位置,如下图所示:

    调用栈中各层函数栈基址间的关系

    了解了函数的调用过程,想要回溯调用栈也就很简单了,首先获取当前函数的栈基址(寄存器ebp)的值,然后获取该地址所指向的栈的值,该值也就是下层函数的栈基址,找到下层函数的栈基址后,重复刚才的动作,即可以将每一层函数的栈基址都找出来,这也就是我们所需要的调用栈了。

    下面是根据原理实现的一段获取函数调用栈的代码,供参考。

    #include <stdio.h>
    
    /* 打印调用栈的最大深度 */
    #define DUMP_STACK_DEPTH_MAX 16
    
    /* 获取寄存器ebp的值 */
    void get_ebp(unsigned long *ebp) {
        __asm__ __volatile__ (
            "mov %%ebp, %0"
            :"=m"(*ebp)
            ::"memory");
    }
    
    /* 获取调用栈 */
    int dump_stack(void **stack, int size) {
        unsigned long ebp = 0;
        int depth = 0;
    
        /* 1.得到首层函数的栈基址 */
        get_ebp(&ebp);
    
        /* 2.逐层回溯栈基址 */
        for (depth = 0; (depth < size) && (0 != ebp) && (0 != *(unsigned long *)ebp) && (ebp != *(unsigned long *)ebp); ++depth) {
            stack[depth] = (void *)(*(unsigned long *)(ebp + sizeof(unsigned long)));
            ebp = *(unsigned long *)ebp;
        }
    
        return depth;
    }
    
    /* 测试函数 2 */
    void test_meloner() {
        void *stack[DUMP_STACK_DEPTH_MAX] = {0};
        int stack_depth = 0;
        int i = 0;
    
        /* 获取调用栈 */
        stack_depth = dump_stack(stack, DUMP_STACK_DEPTH_MAX);
    
        /* 打印调用栈 */
        printf(" Stack Track: 
    ");
        for (i = 0; i < stack_depth; ++i) {
            printf(" [%d] %p 
    ", i, stack[i]);
        }
    
        return;
    }
    
    /* 测试函数 1 */
    void test_hutaow() {
        test_meloner();
        return;
    }
    
    /* 主函数 */
    int main(int argc, char *argv[]) {
        test_hutaow();
        return 0;
    }
    

    源文件下载:链接

    执行gcc dumpstack.c -o dumpstack编译并运行,执行结果如下:

     Stack Track: 
     [0] 0x8048475 
     [1] 0x8048508 
     [2] 0x804855c 
     [3] 0x804856a
    

    Comments

    comments powered by Disqus

  • 相关阅读:
    两个半成品的ORM
    Mayberry小镇的管理 | 三种截然不同的领导风格 3M
    敏捷的目的(方向)错了以后……
    Error:java: Compilation failed: internal java compiler error
    java: -source 1.5 中不支持 diamond 运算符 (请使用 -source 7 或更高版本以启用 diamond 运算符)
    看mybatis日志模块时涉及的动态代理
    看的顺眼的却Destination Unreachable
    如何下载钉钉回放视频
    不想学习时看一看会有帮助的,“但行好事,莫问前程”
    守护线程
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/7654975.html
Copyright © 2020-2023  润新知