现在的位置: 首页 > 综合 > 正文

一个glibc中abort不能backtrace的问题

2016年10月18日 ⁄ 综合 ⁄ 共 3334字 ⁄ 字号 评论关闭

最近在arm linux平台上用gdb调试一个crash的问题,当问题复现后backtrace发现函数调用卡在了libc.so中的abort上,类似如下所示:

(gdb) bt
#0  0x40281ae8 in raise () from /lib/libc.so.6
#1  0x402830ec in abort () from /lib/libc.so.6
#2  0x402830ec in abort () from /lib/libc.so.6
#3  0x402830ec in abort () from /lib/libc.so.6
#4  0x402830ec in abort () from /lib/libc.so.6
#5  0x402830ec in abort () from /lib/libc.so.6
#6  0x402830ec in abort () from /lib/libc.so.6
#7  0x402830ec in abort () from /lib/libc.so.6
#8  0x402830ec in abort () from /lib/libc.so.6
#9  0x402830ec in abort () from /lib/libc.so.6

总之堆栈显示无数个abort(),很是奇怪。于是我对abort反汇编:

0002a7fc <abort>:
   2a7fc:       e59fa2a0        ldr     sl, [pc, #672]  ; 2aaa4 <abort+0x2a8>
   2a800:       e59fb2a0        ldr     fp, [pc, #672]  ; 2aaa8 <abort+0x2ac>
   2a804:       e08fa00a        add     sl, pc, sl
   2a808:       e08a200b        add     r2, sl, fp
   2a80c:       e5923008        ldr     r3, [r2, #8]
   2a810:       ebffa902        bl      14c20 <__aeabi_read_tp>
   2a814:       e2409e4a        sub     r9, r0, #1184   ; 0x4a0
   2a818:       e1590003        cmp     r9, r3
...

发现一个反常的现象:abort第一条指令不是push,也就是它没有把lr等寄存器保存起来,以便返回时恢复。这样一来,当abort调用bl(call一个函数)指令后,lr就会被覆盖成abort自己的地址(当时pc的值),难怪backtrace的打印会这样,就算是神仙也找不回abort的返回给哪个函数了。

进一步调研后我发现,abort是不会返回的! (man abort: The abort() function never returns)。于是觉得编译器这样优化abort也不无道理,但这对调试程序带来了很大的麻烦。片刻思索后,我决定重新编译glibc,去掉对abort的这个优化。相对改makefile来说,我更倾向于改源码,因为更简单便捷,但实际上我尝试了多次才搞好:

第一次尝试:

我发stdlib.h中abort的声明:extern void abort (void) __THROW __attribute__ ((__noreturn__));    这里abort被加上了noreturn的属性。于是乎把这个属性删掉就是最直接的改法。不仅如此,我还在abort的定义中加上了return语句确保万无一失。但是编译时出现了警告:‘noreturn’ function does return。貌似没生效?结果确实没生效。google了一番找到原因:gcc内置赋予了abort noreturn的属性,即使声明中没有,真是无语。。。

第二次尝试:

我尝试abort的优化关闭(-O0),毕竟只执行一次的函数慢点也无所谓了。在源码中可以通过#pragma GCC optimize ("O0") 或__attribute__((optimize("-O0")))做到。然而编译时又出现警告,似乎编译器不认识optimize属性。原来optimize在gcc 4.4以后才引入,而我用的是4.1版本,放弃。。。

第三次尝试:

我怒了,决定改的更大些。经过数次修改abort代码后我发现,如果在abort中直接raise (SIGABRT)然后返回,它的汇编实现就会出奇的简单:

0002a7fc <abort>:
   2a7fc:       e3a00006        mov     r0, #6  ; 0x6
   2a800:       eafffb3e        b       29500 <raise>

这里abort直接b raise,不会修改bl,使得raise直接返回给abort的调用函数,相当于abort被内联掉了。实际调试后发现这样确实生效了,能看到完整的函数调用。不过这样粗暴的把abort改成简单的raise (SIGABRT)毕竟不好,我又改了一次,让abort直接调用另一个函数__abort,__abort充当了实际的实现。abort.c片段如下:

static void __abort(void) __attribute__ ((noinline));

void abort(void)
{
    __abort();
}

/* Cause an abnormal program termination with core-dump.  */
static void
__abort (void)
{
  struct sigaction act;
  sigset_t sigs;

...

汇编结果:

0002a7fc <__abort>:
   2a7fc:       e92d4ff0        push    {r4, r5, r6, r7, r8, r9, sl, fp, lr}
   2a800:       e59fa2a0        ldr     sl, [pc, #672]  ; 2aaa8 <__abort+0x2ac>
   2a804:       e59fb2a0        ldr     fp, [pc, #672]  ; 2aaac <__abort+0x2b0>
   2a808:       e08fa00a        add     sl, pc, sl
   2a80c:       e08a200b        add     r2, sl, fp
   2a810:       ebffa902        bl      14c20 <__aeabi_read_tp>
   2a814:       e5923008        ldr     r3, [r2, #8]
   2a818:       e2409e4a        sub     r9, r0, #1184   ; 0x4a0
   2a81c:       e1590003        cmp     r9, r3
   2a820:       e24ddf45        sub     sp, sp, #276    ; 0x114
   2a824:       0a000005        beq     2a840 <__abort+0x44>

...

0002aab4 <abort>:
   2aab4:       eaffff50        b       2a7fc <__abort>

我们发现__abort第一行指令如实保存了所有必要的寄存器,包括lr。backtrace结果:

#0  0x40101540 in *__GI_raise (sig=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:67
#1  0x401028d0 in __abort () at abort.c:100
#2  0x0007a284 in CC_eventProc (event=52, lid=3, par=0, par2=0x408b974c) at src/callctrl.c:9334
#3  0x0004502c in SIP_lineTsEventProc (ts=<optimized out>, line=0x418001bc, event=<optimized out>, par=1094419944, par2=0x413bb8e8) at src/line.c:2866

...

我们发现除了abort被改名成__abort外(因为abort本身被内联了),其他一切正常。本人认为这样这个修改非常安全,于是收工大吉。

这个abort问题折腾了我整整两天的时间,其中遇到了许多头疼的小问题,不过最终有了个比较满意的解决方案,而且长了不少知识,令人欣慰。

抱歉!评论已关闭.