以前在实验室调测RK3399模块时碰到的一个程序启动时小概率异常问题,当时使用gdb重现了该问题,使用“bt”命令查看该异常线程栈信息提示:”Backtrace stopped:previous frame identical to this frame (corrupt stack?)”,无法看到完整线程栈信息(已经被破坏了)。当时碰到这个问题的开发人员一筹莫展,找笔者协助定位,笔者也很费了一番功夫,前后折腾两天才最终找到问题原因,今天分享一下当时的问题定位过程。
如图1,使用gdb的“info threads”命令查看,主线程1被“lock_wait”挂死了,进一步使用bt命令查看主线程1,提示“Backtrace stopped:previous frame identical to this frame (corrupt stack?)”;除了最近层#0和#1的调用信息,其他栈信息均被破坏了。
图1 线程栈信息
图2 当前线程寄存器信息
x命令的基本格式为“x/[count][format][unit] address”,具体含义和使用方法这里略,具体可以参考文章《gdb堆栈被破坏时的定位方法》(或使用AI工具或自己网上搜索)。
针对这个问题,笔者尝试着通过x命令先后查看了当前线程栈的sp,x30~x22的众多寄存器附近地址段内存信息,有些寄存器如sp明显已经被破坏,有些寄存器附近内存信息参考意义不大,对定位起到关键作用的是x29寄存器附近内存信息,提供了大量的残存函数调用栈信息,具体摘录如下:
(gdb) x/1536ag $x29
(栈地址 :上级调用函数栈地址 函数名 )
0x7fe405ff90: 0x7fe405ffb0 0x55560b4140
0x7fe405ffa0: 0x1 0x55560cccb8
0x7fe405ffb0: 0x7fe405fff0 0x55560c7cd0
0x7fe405ffc0: 0x7fac01f050 0x55589172c0 <_ZL17g_SemIDMainModLog>
0x7fe405ffd0: 0x7fe4060018 0x6
0x7fe405ffe0: 0x0 0x1600000000
0x7fe405fff0: 0x7fe4060120 0x55560c419c
... ... ... ...(省略无关信息)
0x7fe4060120:0x7fe40613b0 0x7fb29776c0 <__kernel_rt_sigreturn>
0x7fe4060130:0x7fb271d000 0x400
... ... ... ...(省略无关信息)
0x7fe40613b0:0x7fe40613e0 0x7fb2611a74
0x7fe40613c0: 0x7fb271d000 0x7fe40619f8
0x7fe40613d0:0x7fe4061b20 0x55560cac00
0x7fe40613e0:0x7fe40619c0 0x7fb2618ad8
... ... ... ...(省略无关信息)
0x7fe40619c0: 0x7fe4061b10 0x55560c7df8
... ... ... ...(省略无关信息)
0x7fe4061b00:0x3031000000000005 0xa5450c5831bfea00
0x7fe4061b10:0x7fe4061c50 0x55560bde7c
... ... ... ...(省略无关信息)
0x7fe4061c50: 0x7fe4061d50 0x55560bc904
0x7fe4061c60: 0x55615e78c0 0x7fb28e2ad8
... ... ... ...(省略无关信息)
0x7fe4061d50:0x7fe4061d80 0x55560bb9b4
... ... ... ...(省略无关信息)
0x7fe4061d80:0x7fe4061ef0 0x55560c3608
... ... ... ...(省略无关信息)
0x7fe4061ef0: 0x7fe4061f10 0x55560c3d20
0x7fe4061f00: 0x10000000000 0x26100000000
0x7fe4061f10: 0x7fe40621c0 0x7fb25eb7a0 <__libc_start_main+224>
... ... ... ...(省略无关信息)
从x29寄存器地址段附近的内存信息,可以完整恢复该主线程1的函数调用栈过程如下:
__libc_start_main => main
=> RunItpMod
=> ITP_ServiceOperationProcess
=> ITP_Standard_Frame_Main
=>Frame_Main_GetClient
=>Kb_LogStr (此处第一次调用 MDSemClass::Lock 锁)
=>printf
=>vfprintf (此处内存溢出,导致系统异常,触发信号量)
=>__kernel_rt_sigreturn
=>ITP_Handle_Sig
=>Kb_LogStr (此处第二次调用 MDSemClass::Lock 锁)
=>MDSemClass::Lock(触发系统死锁)
根据从上述恢复后的主线程1调用栈信息,并结合原始代码分析,最后找到问题原因:
1)公共日志函数“Kb_LogStr”代码中对日志长度判断保护不足,特定情况下的长日志可能导致“vfprintf ”内存溢出,程序启动时主线程“main”对“Kb_LogStr”的日志操作调用有一定概率会导致该内存溢出异常:
2)公共日志函数“Kb_LogStr”中使用了“MDSemClass::Lock”互斥保护锁;主线程“main”和信号量处理回调函数“ITP_Handle_Sig”都调用了公共日志函数“Kb_LogStr”,当主线程在调用“Kb_LogStr”写入日志时(第一次调用“MDSemClass::Lock ”锁)产生概率性异常(参见描述1),此时系统触发了异常信号量处理回调,其二次调用的“Kb_LogStr”函数中的“MDSemClass::Lock ”与之前的调用形成死锁。
解决方案:
1)修订公共日志函数“Kb_LogStr”代码中对日志长度判断保护不足的bug,解决潜在内存溢出问题;
2)修改信号量处理回调函数“ITP_Handle_Sig”中对公共日志函数“Kb_LogStr”的调用为其他非相关函数,避免潜在的死锁逻辑问题。
本问题解决的关键点:使用gdb的“info reg”和“x”等命令完整恢复了失败线程的栈函数调用信息。