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

转自: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 \r\n", 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: \r\n");
    for (i = 0; i < stack_depth; ++i) {
        printf(" [%d] %p \r\n", 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

时间: 2024-09-26 11:40:16

函数调用栈的获取原理分析【转】的相关文章

系统栈的工作原理(转)

  1.开篇 本篇文章着重写的是系统中栈的工作原理,以及函数调用过程中栈帧的产生与释放的过程,有可能名字过大,如果不合适我可以换一个名字,希望大家能够指正,小丁虚心求教!如果有哪里写的不清楚的或者错误的地方请及时更正,小丁再次谢过了.文章里面有错别字,也可能会有好友说寄存器的32.16位的区别其实我感觉这里主要讲的还是些原理性的东西,后续会将文章图片错别字进行调整.(图片里面的posh改为push) 2.内存的不同用途 根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行.但是不管什么样

[Android]任务列表中显示&quot;小程序&quot;的原理分析

今天被小程序刷屏了^^ 我也来凑凑热闹.谈谈微信是怎么实现在任务列表中显示"小程序"的. 效果 微信中打开了"滴滴(小程序)"后,可以看到,任务列表不仅显示了"微信", 还显示了"滴滴(小程序)"的人口.通过这个入口,就可以很方面的切换小程序了, 体验和原生程序也一样一样的. 分析 下面简单分析一下他的实现. 1.Android系统中,显示最近程序列表的View是 RecentsPanelView. 他通过refreshRec

ASP组件上传的三种机制和实现原理分析

上传 ASP 组件 FILE对象 当前,基于浏览器/服务器模式的应用比较流行.当用户需要将文件传输到服务器上时,常用方法之一是运行FTP服务器并将每个用户的FTP默认目录设为用户的Web主目录,这样用户就能运行FTP客户程序并上传文件到指定的 Web目录.这就要求用户必须懂得如何使用FTP客户程序.因此,这种解决方案仅对熟悉FTP且富有经验的用户来说是可行的. 如果我们能把文件上传功能与Web集成,使用户仅用Web浏览器就能完成上传任务,这对于他们来说将是非常方便的.但是,一直以来,由于File

IOS开发:Cocos2d触摸分发原理分析

  触摸是iOS程序的精髓所在,良好的触摸体验能让iOS程序得到非常好的效果,例如Clear.鉴于同学们只会用cocos2d的 CCTouchDispatcher 的 api 但并不知道工作原理,但了解触摸分发的过程是极为重要的.毕竟涉及到权限.两套协议等的各种分发. 本文以cocos2d-iphone源代码为讲解.cocos2d-x 于此类似,就不过多赘述了. 零.cocoaTouch的触摸 在讲解cocos2d触摸协议之前,我觉得我有必要提一下CocoaTouch那四个方法.毕竟cocos2

web上存漏洞及原理分析、防范方法(文件名检测漏洞)

我们通过前篇:<web上存漏洞及原理分析.防范方法(安全文件上存方法) >,已经知道后端获取服务器变量,很多来自客户端传入的.跟普通的get,post没有什么不同.下面我们看看,常见出现漏洞代码.1.检测文件类型,并且用用户上存文件名保存 复制代码 代码如下: if(isset($_FILES['img'])) { $file = save_file($_FILES['img']); if($file===false) exit('上存失败!'); echo "上存成功!"

ShellShock漏洞原理分析

9月24日,广泛存在于Linux的bash漏洞曝光.因为此漏洞可以执行远程命令,所以极为危.危害程度可能超过前段时间的心脏流血漏洞. 漏洞编号CVE-2014-6271,以及打了补丁之后被绕过的CVE-2014-7169,又称ShellShock漏洞. 漏洞起因: 要是用一句话概括这个漏洞,就是代码和数据没有正确区分. 此次漏洞很像SQL注入,通过特别设计的参数使得解析器错误地执行了参数中的命令.这其实是所有解析性语言都可能存在的问题. 1 env x='() { :;}; echo vulne

AbstractQueuedSynchronizer的介绍和原理分析(转)

  简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态.然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作: java.util.concurrent.locks.Abst

java中HashMap的原理分析_java

我们先来看这样的一道面试题: 在 HashMap 中存放的一系列键值对,其中键为某个我们自定义的类型.放入 HashMap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key 从 HashMap 里取出元素,这时候 HashMap 会返回什么? 文中已给出示例代码与答案,但关于HashMap的原理没有做出解释. 1. 特性 我们可以用任何类作为HashMap的key,但是对于这些类应该有什么限制条件呢?且看下面的代码: public class Person { priva

PHP中实现中文字符进制转换原理分析_php技巧

一,中文字符转十进制原理分析 GBK编码中一个汉字由二个字符组成,获取汉字字符串的方法如下 复制代码 代码如下: $string = "不要迷恋哥"; $length = strlen($string); for($i=0;$i<$length;$i++){ if(ord($string[$i])>127){ $result[] = ord($string[$i]).' '.ord($string[++$i]); } } var_dump($result); 由于一个汉字为