Linux Call Trace原理分析

来源:互联网 发布:注册淘宝网店铺多少钱 编辑:程序博客网 时间:2024/05/18 17:03
Linux Call Trace原理分析  
本文介绍了在Linux环境下根据EABI标准进行call trace调试的一般性原理。
本文所说的call trace是指程序出问题时能把当前的函数调用栈打印出来。
本文只介绍了得到函数调用栈的一般性原理,没有涉及Linux的core dump机制。
下面简单介绍powerpc环境中如何实现call trace。
内核态call trace
内核态有三种出错情况,分别是bug, oops和panic。
bug属于轻微错误,比如在spin_lock期间调用了sleep,导致潜在的死锁问题,等等。
oops代表某一用户进程出现错误,需要杀死用户进程。这时如果用户进程占用了某些信号锁,所以这些信号锁将永远不会得到释放,这会导致系统潜在的不稳定性。
panic是严重错误,代表整个系统崩溃。
 
OOPS
先介绍下oops情况的处理。Linux oops时,会进入traps.c中的die函数。
int die(const char *str, struct pt_regs *regs, long err)
       。。。
       show_regs(regs);
void show_regs(struct pt_regs * regs)函数中,会调用show_stack函数,这个函数会打印系统的内核态堆栈。
具体原理为:
       从寄存器里找到当前栈,在栈指针里会有上一级调用函数的栈指针,根据这个指针回溯到上一级的栈,依次类推。
       在powerpc的EABI标准中,当前栈的栈底(注意是栈底,不是栈顶,即Frame Header的地址)指针保存在寄存器GPR1中。在GPR1指向的栈空间,第一个DWORD为上一级调用函数的Frame Header指针(Back Chain Word),第二个DWORD是当前函数在上一级函数中的返回地址(LR Save Word)。通过此种方式一级级向上回溯,完成整个call dump。除了这种方法,内建函数__builtin_frame_address函数理论上也应该能用,虽然在内核中没有见到。(2.6.29的ftrace模块用到了__builtin_return_address函数)。
show_regs函数在call trace的时候,只是用printk打印了一下栈中的信息。如果当前系统没有终端,那就需要修改内核,把这些栈信息根据需求保存到其它地方。
例如,可以在系统的flash中开出一块空间专门用于打印信息的保存。然后,写一个内核模块,再在die函数中加一个回调函数。这样,每当回调函数被调用,就通知自定义的内核模块,在模块中可以把调用栈还有其它感兴趣的信息保存到那块专用flash空间中去。这里有一点需要注意的是,oops时内核可能不稳定,所以为了确保信息能被正确写入flash,在写flash的函数中尽量不要用中断,而用轮循的方式。另外信号量、sleep等可能导致阻塞的函数也不要使用。
此外,由于oops时系统还在运行,所以可以发一个消息(信号,netlink等)到用户空间,通知用户空间做一些信息收集工作。

Panic
Panic时,Linux处于更最严重的错误状态,标志着整个系统不可用,即中断、进程调度等都已经停止,但栈还没被破坏。所以,oops中的栈回溯理论上还是能用。printk函数中因为没有阻塞,也还是能够使用。
用户态call trace
用户程序可以在以下情形call trace,以方便调试:
l程序崩溃时,都会收到一个信号。Linux系统接收到某些信号时会自动打印call trace。
2在用户程序中添加检查点,类似于assert机制,如果检查点的条件不满足,就执行call trace。
用户态的call trace与内核态相同,同样满足EABI标准,原理如下:
在GNU标准中,有一个内建函数__builtin_frame_address。这个函数可以返回当前执行上下文的栈底(Frame Header)指针(同时也是指向Back Chain Word的指针),通过这个指针得到当前调用栈。而这个调用栈中,会有上一级调用函数的栈底指针,通过这个指针再回溯到上一级的调用栈。以此类推完成整个call dump过程。

得到函数的地址后,可以通过符号表得到函数名字。如果是动态库中定义的函数,还可以通过扩展函数dladdr得到这个函数的动态库信息。


dladdr是怎么做的
前一阵子项目改一个bug的时候,需要查看driver里某些变量的值。原来调试上层app都是在target端用gdbserver,然后在host端来remote debug。现在调试driver/kernel,只能用OS本身附带的一个kernel调试器来做了。这个调试器可以同时提供kernel和user的call stack,以供我们参考。不过呢,kernel的call stack确实可以直接在host端用objdump或者gdb快速定位source,但user的call stack就不行了,因为app本身exe进程里有一大堆dll和so……光知道一个进程内的地址根本没有用啊。
说起来,有一个办法倒是可以,就是推出那个kernel调试器,启动gdbserver,然后用gdb来查看,不过这样很麻烦,速度也慢,更要命的gdbserver依赖于网络通信,而那正好是我需要的调试的driver……怎么办呢?
有没有什么工具,直接运行在target端,就可以通过某pid的某address来给出这是哪个dll或者so里的哪个相对address呢?这项任务一听,就是glibc那个对POSIX做的漂亮的扩展dladdr函数的任务么。那么dladdr到底是怎么做的呢?
在linux里面,每当某进程中调用dlopen打开动态连接库的同时,都会相应的维护该进程的一个link,加入新载入的这个动态库的名称在进程内的地址空间等基本信息,然后更新symbol。因此,只要遍历这个link,得知查找的address是在哪一个item的地址范围内,就可以找到该动态库的名称,然后减去进程载入的起始地址就得到了在动态库内的相对地址了,这也就是dladdr的原理。
尽管工作中遇到的OS不是linux,但是glibc的实现也是类似的。而且由于是嵌入式上跑的,反而更加简单。link的首地址竟然是保存在ld.so的一个固定的变量里,而且这个link也是一个专门用于记录动态库信息的结构,也比linux下的大杂烩要简单很多:)所以对我的问题就更方便了,直接用ptrace去attach目标进程,peek到保存link首地址的数据,然后逐个查询~~~如此,不用再麻烦remote debug啦,找到相对地址后我直接gdb里用info line。