转自: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. 一个java实现的简单的4则运算器

    有些基础知识有欠缺,补一下,顺便练习一下java import com.sun.deploy.util.ArrayUtil; import java.util.*; public class Main ...

  2. Android : Error inflating class android.support.design.widget.NavigationView

      之前一直没搞定的,今天终于解决了. Android报错: android.view.InflateException: Error inflating class android.support. ...

  3. Beta冲刺——day5

    Beta冲刺--day5 作业链接 Beta冲刺随笔集 github地址 团队成员 031602636 许舒玲(队长) 031602237 吴杰婷 031602220 雷博浩 031602134 王龙 ...

  4. Beta 冲刺 四

    团队成员 051601135 岳冠宇 031602629 刘意晗 031602248 郑智文 031602330 苏芳锃 031602234 王淇 照片 项目进展 岳冠宇 昨天的困难 数据交换比较复杂 ...

  5. Linux命令(十四) 查看工作目录文件 ls

    目录 1.命令简介 2.常用参数介绍 3.实例 4.直达底部 命令简介 ls 命令是 Linux 下最常用的命令. ls 就是 list 的缩写.默认情况下 ls 命令用来打印出当前目录的清单, 如果 ...

  6. Oracle 标准版 企业版 个人版的区别 转帖

    转帖来源: https://blog.csdn.net/flg_inwind/article/details/2628133 同事方总:http://www.oracle.com/us/corpora ...

  7. Oracle 12c 之前的版本路线图

  8. Valid BFS? CodeForces - 1037D(思维 bfs)

    我真是一只菜狗......emm... 题意: 判断一个从1开始的队列是否可以按照bfs的顺序 进行遍历..必须从1开始...然后后边依次是bfs顺序 解析: 看代码能看懂吧...emm...就是把每 ...

  9. BZOJ 2865 字符串识别 | 后缀数组 线段树

    集训讲字符串的时候我唯一想出正解的题-- 链接 BZOJ 2865 题面 给出一个长度为n (n <= 5e5) 的字符串,对于每一位,求包含该位的.最短的.在原串中只出现过一次的子串. 题解 ...

  10. 洛谷 P4070 [SDOI2016]生成魔咒 解题报告

    P4070 [SDOI2016]生成魔咒 题目描述 魔咒串由许多魔咒字符组成,魔咒字符可以用数字表示.例如可以将魔咒字符 \(1\).\(2\) 拼凑起来形成一个魔咒串 \([1,2]\). 一个魔咒 ...