转自: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. C语言版本:单链表的实现

    slist.h #ifndef __SLIST_H__ #define __SLIST_H__ #include<cstdio> #include<malloc.h> #inc ...

  2. 作业6--四则运算APP之Sprint计划

    1.现状 小组成员各就各位,理顺计划思路,制定工作计划. 2.部分需求索引卡 第一个阶段没有具体功能的实现. 3.任务认领 产品负责人:王宏财 Master:陈思明 项目开发成员:许佳豪.吴旭涛 4. ...

  3. Daily Scrum - 11/16

    时间:午饭 今天小组例会主要是汇报了各自的进度.任烁那边主要为工程添加了单词的类(包含各个参数等成员变量),方便以后实现算法:拜重阳实现了一个简易的“点进-点出”UI,可谓迈出了艰难的第一步:章玮和罗 ...

  4. AIX上安装配置DB2

    在2台P550数据库主机上安装DB2 V8.2,两台数据库主机间进行数据库HA配置,实现数据库双机互备. 本文档编写以磁盘大小360G,数据库名CRAMS_JS为例. 设备准备 请系统管理员协助划分D ...

  5. [转帖]将改名贯彻到底,Xeon E3系列将改名为Xeon E

    将改名贯彻到底,Xeon E3系列将改名为Xeon E 导读:    E3 系列改名为 XEON E 系列entry 入门级的含义 E5 1XXX 序列改名为 XEON W 系列 workstatio ...

  6. leetcode Database4

    一.Department Top Three Salaries The Employee table holds all employees. Every employee has an Id, an ...

  7. Semantic Versioning Specification & 语义化版本

    Semantic Versioning Specification & 语义化版本 Semantic Versioning Specification http://semver.org 16 ...

  8. Delphi用户登录窗口框架

    经常看到一些新手在CSDN上问登录窗口如何写,也看到N多人form1.show/form1.create/…中做form2.show之类.实在看不下去了.这种写法实在不是很好,于是还是把自己理解的登录 ...

  9. BZOJ4830 [Hnoi2017]抛硬币 【扩展Lucas】

    题目链接 BZOJ4830 题解 当\(a = b\)时,我们把他们投掷硬币的结果表示成二进制,发现,当\(A\)输给\(B\)时,将二进制反转一下\(A\)就赢了\(B\) 还要除去平局的情况,最后 ...

  10. 【洛谷P2661】信息传递

    题目大意:给定一个有 N 个点,N 条边且每个点的出度均为 1 的有向图,求该有向图的一个最小环. 题解:由于每个点的出度均为 1,可知可能的情况只有以下几种:一个环或多个环,一个环+一条链.因此,可 ...