转自: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. Qrcode生成二维码的参数总结 及最小尺寸的测试

    Qrcode生成二维码,做过很多实验,探索最小规格的二维码到底是多少尺寸,和最高规格的二维码到底是多大尺寸.现在我总结总结: 有两种思路: 1.生成规格高的二维码,然后压缩到自己想要的尺寸的二维码.这 ...

  2. ElasticSearch 2 (23) - 语言处理系列之词根提取

    ElasticSearch 2 (23) - 语言处理系列之词根提取 摘要 世界上大多数语言都是屈折变化的,意思是词语可以通过变形来表达不同的含义: 数(Number): fox, foxes 时态( ...

  3. (转)linux 内存管理——内核的shmall 和shmmax 参数

    内核的 shmall 和 shmmax 参数 SHMMAX= 配置了最大的内存segment的大小 ------>这个设置的比SGA_MAX_SIZE大比较好. SHMMIN= 最小的内存seg ...

  4. 通过动态包含和Ajax机制抽取Web应用的公共页面

    在Java Web应用开发中,经常遇到的一种情况是,许多的页面中都包含着“公共页面”,这部分动态页面的特征是:访问量大,会带来较大的性能压力.功能设计上会动态地改变自身的元素.比如在登录前和登录后所展 ...

  5. BZOJ2655 calc(动态规划+拉格朗日插值法)

    考虑暴力dp:f[i][j]表示i个数值域1~j时的答案.考虑使其值域++,则有f[i][j]=f[i][j-1]+f[i-1][j-1]*i*j,边界f[i][i]=i!*i!. 注意到值域很大,考 ...

  6. 1019C Sergey's problem(思维)

    题意: 找出来一个点集S  使得S中的点不能互相通过一步到达 并且S中的点 可以在小于等于2的步数下到达所有的点 要父结点 不要子结点 这样就求出来一个点集S‘  而S'中可能存在 v -> u ...

  7. 安装windows 2003iso 步骤

    参考:http://www.2cto.com/os/201303/192509.html u盘winpe下安装windows server 2003   一.准备和资源需求 1.U盘,内置WinPE ...

  8. 学习Spring Boot:(十七)Spring Boot 中使用 Redis

    前言 Redis 1 是一个由Salvatore Sanfilippo写的key-value存储系统. edis是一个开源的使用ANSI C语言编写.遵守BSD协议.支持网络.可基于内存亦可持久化的日 ...

  9. 【BZOJ3489】A simple rmq problem(KD-Tree)

    [BZOJ3489]A simple rmq problem(KD-Tree) 题面 BZOJ 题解 直接做肯定不好做,首先我们知道我们是一个二维平面数点,但是限制区间只能出现一次很不好办,那么我们给 ...

  10. 【uoj228】 基础数据结构练习题

    http://uoj.ac/problem/228 (题目链接) 题意 给出一个序列,维护区间加法,区间开根,区间求和 Solution 线段树.考虑区间开根怎么做.当区间的最大值与最小值相等时,我们 ...