最近在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问题折腾了我整整两天的时间,其中遇到了许多头疼的小问题,不过最终有了个比较满意的解决方案,而且长了不少知识,令人欣慰。