谁在call我-backtrace的实现原理【转】
转自: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的实现原理【转】的更多相关文章
- pstack使用和原理【转】
转自:http://www.cnblogs.com/mumuxinfei/p/4366708.html 前言: 最近小组在组织<<深入剖析Nginx>>的读书会, 里面作者提到 ...
- pstack使用和原理
前言: 最近小组在组织<<深入剖析Nginx>>的读书会, 里面作者提到了pstack这个工具. 之前写JAVA程序, 对jstack这个工具, 非常的喜欢, 觉得很有用. 于 ...
- 嵌入式 linux下利用backtrace追踪函数调用堆栈以及定位段错误
嵌入式 linux下利用backtrace追踪函数调用堆栈以及定位段错误 2015-05-27 14:19 184人阅读 评论(0) 收藏 举报 分类: 嵌入式(928) 一般察看函数运行时堆栈的 ...
- linux下利用backtrace追踪函数调用堆栈以及定位段错误
一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的. 在glibc ...
- 善用backtrace解决大问题【转】
转自:https://www.2cto.com/kf/201107/97270.html 一.用途: 主要用于程序异常退出时寻找错误原因 二.功能: 回溯堆栈,简单的说就是可以列出当前函数调用关系 三 ...
- Flutter的原理及美团的实践
导读 Flutter是Google开发的一套全新的跨平台.开源UI框架,支持iOS.Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件.自从2017年5月发布第一个版本以来,目前 ...
- Objective-C try/catch异常处理机制原理。
try-catch-finaly finally在任何情况下都会执行(不管有没有异常),属于整个体系的附属. 基本思想是跳到捕获锚点,重新执行. http://www.cnblogs.com/mark ...
- Native进程之Trace原理(转)——可直接输出某进程的栈帧——debuggerd
一. 概述 当发生ANR(Application Not Response,对于Java进程可通过kill -3向目标进程发送信号SIGNAL_QUIT, 输出相应的traces信息保存到目录/dat ...
- WatchDog工作原理
Android系统中,有硬件WatchDog用于定时检测关键硬件是否正常工作,类似地,在framework层有一个软件WatchDog用于定期检测关键系统服务是否发生死锁事件. watchdog的源码 ...
随机推荐
- 一个java实现的简单的4则运算器
有些基础知识有欠缺,补一下,顺便练习一下java import com.sun.deploy.util.ArrayUtil; import java.util.*; public class Main ...
- Android : Error inflating class android.support.design.widget.NavigationView
之前一直没搞定的,今天终于解决了. Android报错: android.view.InflateException: Error inflating class android.support. ...
- Beta冲刺——day5
Beta冲刺--day5 作业链接 Beta冲刺随笔集 github地址 团队成员 031602636 许舒玲(队长) 031602237 吴杰婷 031602220 雷博浩 031602134 王龙 ...
- Beta 冲刺 四
团队成员 051601135 岳冠宇 031602629 刘意晗 031602248 郑智文 031602330 苏芳锃 031602234 王淇 照片 项目进展 岳冠宇 昨天的困难 数据交换比较复杂 ...
- Linux命令(十四) 查看工作目录文件 ls
目录 1.命令简介 2.常用参数介绍 3.实例 4.直达底部 命令简介 ls 命令是 Linux 下最常用的命令. ls 就是 list 的缩写.默认情况下 ls 命令用来打印出当前目录的清单, 如果 ...
- Oracle 标准版 企业版 个人版的区别 转帖
转帖来源: https://blog.csdn.net/flg_inwind/article/details/2628133 同事方总:http://www.oracle.com/us/corpora ...
- Oracle 12c 之前的版本路线图
- Valid BFS? CodeForces - 1037D(思维 bfs)
我真是一只菜狗......emm... 题意: 判断一个从1开始的队列是否可以按照bfs的顺序 进行遍历..必须从1开始...然后后边依次是bfs顺序 解析: 看代码能看懂吧...emm...就是把每 ...
- BZOJ 2865 字符串识别 | 后缀数组 线段树
集训讲字符串的时候我唯一想出正解的题-- 链接 BZOJ 2865 题面 给出一个长度为n (n <= 5e5) 的字符串,对于每一位,求包含该位的.最短的.在原串中只出现过一次的子串. 题解 ...
- 洛谷 P4070 [SDOI2016]生成魔咒 解题报告
P4070 [SDOI2016]生成魔咒 题目描述 魔咒串由许多魔咒字符组成,魔咒字符可以用数字表示.例如可以将魔咒字符 \(1\).\(2\) 拼凑起来形成一个魔咒串 \([1,2]\). 一个魔咒 ...