delphij's Chaos

选择chaos这个词是因为~~实在很难找到一个更合适的词来形容这儿了……

30 Nov 2005

如何从目标代码、DDB backtrace找到崩溃的原因

今天复习了一次……

第一,你需要源代码编译出来的包含调试符号的目标代码。
第二,backtrace中的内容要尽可能完整。

一般来说,DDB backtrace中会包含函数的名字,然而,它并不能提供更多的信息,例如变量名称,对应的源代码行等等。DDB被设计为尽可能简单,以避免在调试内核时引发崩溃,从而导致不得不丢开内核,开始调试调试器的尴尬局面。

另一个不能忽略的影响,便是C/C++编译器的优化功能可能引发的代码混淆。许多时候,编译器给出的代码可能并不那么直截了当,有几种比较常见的优化:

  • 循环展开。有时循环会被自动展开,以减少处理器的分支预测带来的消耗。
  • 内联展开。内联函数可能会在调用处展开。尽管函数调用很「便宜」,但有时编译器会通过将函数内联展开以期避免不必要得快取缓存刷洗效应。
  • 寄存器的使用。编译器会尽可能利用寄存器,以避免(昂贵的)内存操作。
    这些都会使汇编代码的样式与C元程序产生非常大的差异。

因此,在无法动态调试的时候,特别是当只能拿到DDB Backtrace的时候,需要一些方法来迅速完成问题的定位。下面我来介绍一些我本人的实际经验,希望能够抛砖引玉:

  1. 通过objdump拿到汇编代码。
    方法是objdump -D,这个不必多说。

  2. 找到代码的位置(DDB的backtrace会告诉你 函数名+偏移 的地址)。
    当使用objdump处理.o文件时,我们可以找到函数的入口地址,并计算出加上偏移量之后对应的地址。

  3. 寻找汇编代码中前后的特征值。
    如果你对编译器和代码都足够熟悉,以至于可以徒手把C程序编译成汇编代码的话,当然会不需要这样做。没有这种功力的话,我们可以用一些小技巧,例如寻找特征值。

什么是特征值呢?一般来说,内核中会有相当多的篇幅是在做判断或者循环,另一种可能是调用一些外部的函数(例如其他.o文件中的),还有一种可能就是显式地赋值,这些代码会有相当明显的特征,因此可以帮助我们迅速定位。

总体而言,内核中所发生的无头命案多以内存访问越界(Kernel Trap 12)告终。这类问题最常见的引发条件,则是触动无效指针(Invalid pointer defrencing)。例如下列代码:

%%%
  if (vp->vp_state & F_DONE) {
    /* do something */
  }

如果vp包含一无效值,则会引致崩溃。

现在我们有了两项特征:第一,我们通常可以在崩溃的附近找到->;第二,这类问题本身多出现于判断,或周围存在判断。上面的例子中,&会最终被翻译为一test[bl]指令。

发现崩溃果然是这样一个点,于是,查找代码中包含->和&的if语句(从objdump发现它下面有一je,因此一定是判断性的语句。

找到上述语句段后,应继续观察前后的语句。

确定位置之后,就比较容易人为制造或重现问题了。

接下来的问题就是KASSERT和printf。当然,想要更迅速准确地找到问题,还需要非常多的实践练习才可以。