转自:http://www.xuebuyuan.com/1504689.html

显示函数调用关系(backtrace/callstack)是调试器必备的功能之一,比如在gdb里,用bt命令就可以查看backtrace。在程序崩溃的时候,函数调用关系有助于快速定位问题的根源,了解它的实现原理,可以扩充自己的知识面,在没有调试器的情况下,也能实现自己backtrace。更重要的是,分析backtrace的实现原理很有意思。现在我们一起来研究一下:

glibc提供了一个backtrace函数,这个函数可以帮助我们获取当前函数的backtrace,先看看它的使用方法,后面我们再仿照它写一个。

#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h> #define MAX_LEVEL 4 static void test2()
{
int i = ;
void* buffer[MAX_LEVEL] = {}; int size = backtrace(buffer, MAX_LEVEL); for(i = ; i < size; i++)
{
printf("called by %p/n", buffer[i]);
} return;
} static void test1()
{
int a=0x11111111;
int b=0x11111112; test2();
a = b; return;
} static void test()
{
int a=0x10000000;
int b=0x10000002; test1();
a = b; return;
} int main(int argc, char* argv[])
{
test(); return ;
} 编译运行它:
gcc -g -Wall bt_std.c -o bt_std
./bt_std 屏幕打印:
called by ×
called by ×804848a
called by ×80484ab
called by ×80484c9 上面打印的是调用者的地址,对程序员来说不太直观,glibc还提供了另外一个函数backtrace_symbols,它可以把这些地址转换成源代码的位置(通常是函数名)。不过这个函数并不怎么好用,特别是在没有调试信息的情况下,几乎得不什么有用的信息。这里我们使用另外一个工具addr2line来实现地址到源代码位置的转换: 运行:
./bt_std |awk ‘{print “addr2line “$″ -e bt_std”}’>t.sh;. t.sh;rm -f t.sh 屏幕打印:
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c: backtrace是如何实现的呢? 在x86的机器上,函数调用时,栈中数据的结构如下: ---------------------------------------------
参数N
参数… 函数参数入栈的顺序与具体的调用方式有关
参数
参数
参数
---------------------------------------------
EIP 完成本次调用后,下一条指令的地址
EBP 保存调用者的EBP,然后EBP指向此时的栈顶。
----------------新的EBP指向这里---------------
临时变量1
临时变量2
临时变量3
临时变量…
临时变量5
--------------------------------------------- (说明:下面低是地址,上面是高地址,栈向下增长的) 调用时,先把被调函数的参数压入栈中,C语言的压栈方式是:先压入最后一个参数,再压入倒数第二参数,按此顺序入栈,最后才压入第一个参数。 然后压入EIP和EBP,此时EIP指向完成本次调用后下一条指令的地址 ,这个地址可以近似的认为是函数调用者的地址。EBP是调用者和被调函数之间的分界线,分界线之上是调用者的临时变量、被调函数的参数、函数返回地址(EIP),和上一层函数的EBP,分界线之下是被调函数的临时变量。 最后进入被调函数,并为它分配临时变量的空间。gcc不同版本的处理是不一样的,对于老版本的gcc(如gcc3.),第一个临时变量放在最高的地址,第二个其次,依次顺序分布。而对于新版本的gcc(如gcc4.),临时变量的位置是反的,即最后一个临时变量在最高的地址,倒数第二个其次,依次顺序分布。 为了实现backtrace,我们需要: .获取当前函数的EBP。
.通过EBP获得调用者的EIP。
.通过EBP获得上一级的EBP。
.重复这个过程,直到结束。 通过嵌入汇编代码,我们可以获得当前函数的EBP,不过这里我们不用汇编,而且通过临时变量的地址来获得当前函数的EBP。我们知道,对于gcc3.4生成的代码,当前函数的第一个临时变量的下一个位置就是EBP。而对于gcc4.3生成的代码,当前函数的最后一个临时变量的下一个位置就是EBP。 有了这些背景知识,我们来实现自己的backtrace: #ifdef NEW_GCC
#define OFFSET 4
#else
#define OFFSET 0
#endif/*NEW_GCC*/ int backtrace(void** buffer, int size)
{
int n = 0xfefefefe;
int* p = &n;
int i = ; int ebp = p[ + OFFSET];
int eip = p[ + OFFSET]; for(i = ; i < size; i++)
{
buffer[i] = (void*)eip;
p = (int*)ebp;
ebp = p[];
eip = p[];
} return size;
} 对于老版本的gcc,OFFSET定义为0,此时p+1就是EBP,而p[]就是上一级的EBP,p[]是调用者的EIP。本函数总共有5个int的临时变量,所以对于新版本gcc, OFFSET定义为5,此时p+5就是EBP。在一个循环中,重复取上一层的EBP和EIP,最终得到所有调用者的EIP,从而实现了backtrace。 现在我们用完整的程序来测试一下(bt.c): #include <stdio.h> #define MAX_LEVEL 4
#ifdef NEW_GCC
#define OFFSET 4
#else
#define OFFSET 0
#endif/*NEW_GCC*/ int backtrace(void** buffer, int size)
{
int n = 0xfefefefe;
int* p = &n;
int i = ; int ebp = p[ + OFFSET];
int eip = p[ + OFFSET]; for(i = ; i < size; i++)
{
buffer[i] = (void*)eip;
p = (int*)ebp;
ebp = p[];
eip = p[];
} return size;
} static void test2()
{
int i = ;
void* buffer[MAX_LEVEL] = {}; backtrace(buffer, MAX_LEVEL); for(i = ; i < MAX_LEVEL; i++)
{
printf("called by %p/n", buffer[i]);
} return;
} static void test1()
{
int a=0x11111111;
int b=0x11111112; test2();
a = b; return;
} static void test()
{
int a=0x10000000;
int b=0x10000002; test1();
a = b; return;
} int main(int argc, char* argv[])
{
test(); return ;
} 写个简单的Makefile: CFLAGS=-g -Wall
all:
gcc34 $(CFLAGS) bt.c -o bt34
gcc $(CFLAGS) -DNEW_GCC bt.c -o bt
gcc $(CFLAGS) bt_std.c -o bt_std clean:
rm -f bt bt34 bt_std 编译然后运行:
make
./bt|awk ‘{print “addr2line “$″ -e bt”}’>t.sh;. t.sh; 屏幕打印:
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c: 对于可执行文件,这种方法工作正常。对于共享库,addr2line无法根据这个地址找到对应的源代码位置了。原因是:addr2line只能通过地址偏移量来查找,而打印出的地址是绝对地址。由于共享库加载到内存的位置是不确定的,为了计算地址偏移量,我们还需要进程maps文件的帮助: 通过进程的maps文件(/proc/进程号/maps),我们可以找到共享库的加载位置,如:

00c5d000-00c5e000 r-xp : /home/work/mine/sysprog/think-in-compway/backtrace/libbt_so.so
00c5e000-00c5f000 rw-p : /home/work/mine/sysprog/think-in-compway/backtrace/libbt_so.so
… libbt_so.so的代码段加载到0×00c5d000-×00c5e000,而backtrace打印出的地址是:
called by 0xc5d4eb
called by 0xc5d535
called by 0xc5d556
called by ×80484ca 这里可以用打印出的地址减去加载的地址来计算偏移量。如,用 0xc5d4eb减去加载地址0×00c5d000,得到偏移量0×4eb,然后把0×4eb传给addr2line: addr2line ×4eb -f -s -e ./libbt_so.so 屏幕打印:
/home/work/mine/sysprog/think-in-compway/backtrace/bt_so.c: 栈里的数据很有意思,在上一节中,通过分析栈里的数据,我们了解了变参函数的实现原理。在这一节中,通过分析栈里的数据,我们又学到了backtrace的实现原理。

谁在call我-backtrace的实现原理【转】的更多相关文章

  1. pstack使用和原理【转】

    转自:http://www.cnblogs.com/mumuxinfei/p/4366708.html 前言: 最近小组在组织<<深入剖析Nginx>>的读书会, 里面作者提到 ...

  2. pstack使用和原理

    前言: 最近小组在组织<<深入剖析Nginx>>的读书会, 里面作者提到了pstack这个工具. 之前写JAVA程序, 对jstack这个工具, 非常的喜欢, 觉得很有用. 于 ...

  3. 嵌入式 linux下利用backtrace追踪函数调用堆栈以及定位段错误

    嵌入式 linux下利用backtrace追踪函数调用堆栈以及定位段错误 2015-05-27 14:19 184人阅读 评论(0) 收藏 举报  分类: 嵌入式(928)  一般察看函数运行时堆栈的 ...

  4. linux下利用backtrace追踪函数调用堆栈以及定位段错误

    一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的. 在glibc ...

  5. 善用backtrace解决大问题【转】

    转自:https://www.2cto.com/kf/201107/97270.html 一.用途: 主要用于程序异常退出时寻找错误原因 二.功能: 回溯堆栈,简单的说就是可以列出当前函数调用关系 三 ...

  6. Flutter的原理及美团的实践

    导读 Flutter是Google开发的一套全新的跨平台.开源UI框架,支持iOS.Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件.自从2017年5月发布第一个版本以来,目前 ...

  7. Objective-C try/catch异常处理机制原理。

    try-catch-finaly finally在任何情况下都会执行(不管有没有异常),属于整个体系的附属. 基本思想是跳到捕获锚点,重新执行. http://www.cnblogs.com/mark ...

  8. Native进程之Trace原理(转)——可直接输出某进程的栈帧——debuggerd

    一. 概述 当发生ANR(Application Not Response,对于Java进程可通过kill -3向目标进程发送信号SIGNAL_QUIT, 输出相应的traces信息保存到目录/dat ...

  9. WatchDog工作原理

    Android系统中,有硬件WatchDog用于定时检测关键硬件是否正常工作,类似地,在framework层有一个软件WatchDog用于定期检测关键系统服务是否发生死锁事件. watchdog的源码 ...

随机推荐

  1. 作业一_随笔1_初来乍到:学号&博客地址

    031302540——http://www.cnblogs.com/yyj031302540/ 计算机实验班叶艺洁

  2. 编码用命令行执行的C语言词语统计程序

    需求介绍 程序处理用户需求的模式为: wc.exe [parameter][filename] 在[parameter]中,用户通过输入参数与程序交互,需实现的功能如下: 1.基本功能 支持 -c   ...

  3. Software-Defined Networking:A Comprehensive Survey--Day4

    V. ONGOING RESEARCH EFFORTS AND CHALLENGES 这一节主要介绍了对SDN潜力的发挥有着重要推动作用的一些研究成果. A. Switch Designs 目前Ope ...

  4. Docker 下 mysql 简单的 主从复制实现

    1. 拉取镜像 docker pull mysql: 2. 运行这个镜像 docker run -d --name maser mysql: 3. 安装一些必要的软件 docker exec -it ...

  5. Alpha、伪Beta 发布后,夏一鸣的个人感想与体会

    伪Beta发布在4月15日拉开了帷幕,夏一鸣代表OneZero团队上台进行了Account的发布.产品发布成功,但依然存在问题和不足.以下就Alpha.伪Beta 发布谈一谈我自己(夏一鸣)的想法. ...

  6. 51Nod 1384 全排列

    给出一个字符串S(可能有重复的字符),按照字典序从小到大,输出S包括的字符组成的所有排列.例如:S = "1312", 输出为:   1123 1132 1213 1231 131 ...

  7. HTTP ERROR 400 Bad Request

    一springmvc项目中我新增记录完全ok,编辑就是不行,后台方法进不去.老是报错HTTP ERROR 400 Bad Request. 经过查询,说是400表示请求中的语法错误. 我把新增记录的请 ...

  8. CodeSmith自己动手写模板

    CodeSmith学习笔记------ 1.新建一个Code Smith Generator Template(C sharp) 2.一些常见标签的解释: ①外部变量: <%@ Property ...

  9. AdaBoost原理详解

    写一点自己理解的AdaBoost,然后再贴上面试过程中被问到的相关问题.按照以下目录展开. 当然,也可以去我的博客上看 Boosting提升算法 AdaBoost 原理理解 实例 算法流程 公式推导 ...

  10. final的用法---java基础知识

    Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类.非抽象类成员方法和变量. final类不能被继承,没有子类,final类中的方法默认是final的. final方法 ...