在开发中遇到这样一个问题,程序要跑很久才会出现一次崩溃,由于系统有硬件看门狗,因此程序在崩溃时coredump文件都没生成就已经重启了,这对定位程序bug代价严重。gdb可以对程序错误有很好的定位,因此为了模仿gdb这种从出错函数回溯的功能,于是想实现能否程序中也能实现这个。
一开始,想看gdb源码,但是真的太多太复杂了(不是一般人看的,神人看的)。后来就探究Linux内核是如何判断程序已经跑飞然后产生信号的,我能不能像gdb那样截取这个信号,并从拿到出错的函数的地址。因此经过一番栈帧的探究,最后发现程序中的函数关系是如下图示的。
上图所示是程序函数的调用关系(fp指针模式)下的示意图,在编译程序时我们一般用-O2进行编译,此时fp指针模式将不存在,因此我们需要手动添加编译、链接条件。
-rdynamic -funwind-tables -ffunction-sections -fno-omit-frame-pointer
如果不添加以上编译条件的话,将不能使用该fp模式进行栈回溯。
在ARM Linux中,当前函数的指令地址、函数返回地址、栈顶、FP指针,其分别由PC(R15)、LR(R14)、SP(R13)、FP(R11)寄存器分别保存,因此如果想要获取出错函数的地址,获取当前出错函数的R0-R15一组寄存器值便可得到对应的调用关系的中起始函数调用地址。其中当前函数栈的大小为:size = SP-FP;FP寄存器的值指向存放上一个函数PC(也即是上一个函数的入口),因此可通过这种关系我们就能一步一步的进行函数追溯了。
当前PC所指向的是当前函数入口地址,fp值所指向的是上一函数入口地址,fp值-4是上一个函数的fp,再由上一个fp往上一层追溯,直到超过线程栈最大8M的地址空间停止,这样就完成栈的追溯了。
现在问题来了,在程序发生段错误或其它信号时,该如何截获该信号?又如何获取上述一组寄存器信息呢?
回答:我的解决思路是,稍微修改内核,然后程序通过内核模块接口读出寄存器组信息。
首先在内核信号处理文件signal.c中的handle_signal函数内,在内核检测到程序段错误或其它错误时,在未切换寄存器组信息时,保存当前出错函数对应的PC、SP、FP、LR信息,如下示。
struct pt_regs gStackEnvs;
EXPORT_SYMBOL(gStackEnvs);
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
sigset_t *oldset = sigmask_to_save();
int ret;
/*过滤信号,只保存设定信号时的寄存器组信息*/
if(ksig->info.si_signo == SIGSEGV ||ksig->info.si_signo == SIGABRT ||
ksig->info.si_signo == SIGFPE ||ksig->info.si_signo == SIGILL)
{
/*
printk(KERN_EMERG "[kernel] ===> getSigno= %d\n",ksig->info.si_signo);
printk(KERN_EMERG "---------------> r0= 0x%x\n",regs->ARM_ORIG_r0);
printk(KERN_EMERG "---------------> cpsr= 0x%x\n",regs->ARM_cpsr);
printk(KERN_EMERG "---------------> pc= 0x%x\n",regs->ARM_pc);
printk(KERN_EMERG "---------------> lr= 0x%x\n",regs->ARM_lr);
printk(KERN_EMERG "---------------> sp= 0x%x\n",regs->ARM_sp);
printk(KERN_EMERG "---------------> ip= 0x%x\n",regs->ARM_ip);
printk(KERN_EMERG "---------------> fp= 0x%x\n",regs->ARM_fp);
*/
memcpy(&gStackEnvs,regs,sizeof(struct pt_regs));
gStackEnvs.uregs[15] = kernel_text_address(regs->ARM_pc)
? regs->ARM_pc : regs->ARM_lr;
}
/*
* Set up the stack frame
*/
if (ksig->ka.sa.sa_flags & SA_SIGINFO)
ret = setup_rt_frame(ksig, oldset, regs);
else
ret = setup_frame(ksig, oldset, regs);
/*
* Check that the resulting registers are actually sane.
*/
ret |= !valid_user_regs(regs);
signal_setup_done(ret, ksig, 0);
}
然后编写一个内核模块以读取该寄存器组信息。如下示:
extern struct pt_regs gStackEnvs; //函数堆栈信息
static int fp_frameInfoRead(struct file * file, const char __user * buffer, size_t count, loff_t * ppos)
{
size_t cnt = 0;
memcpy(&myStackEnvsInfo,&gStackEnvs,sizeof(gStackEnvs));
if(count > sizeof(myStackEnvsInfo))
cnt = sizeof(myStackEnvsInfo);
else
cnt = count;
if(!copy_to_user((char *)buffer, (char*)&myStackEnvsInfo, count))
return cnt;
else
return -1;
}
然后在应用层main函数中,注册对应的信号处理函数,当程序发生错误时,内核将执行用户注册的信号处理函数,我们可以在此获取寄存器组信息,然后完成栈回溯。
signal(SIGSEGV, sigroutine);//在main函数注册信号
void sigroutine(int dunno)
{
unsigned char ucLog[256] = {0};
/*进行栈回溯*/
fpDumpFrameInfo(ucLog,sizeof(ucLog));
//打印log或写log
}
以下为栈回溯实现函数,回溯结束约束条件为线程栈大小最大8M,回溯的地址超过8M就会结束回溯。
static int fpUnwindFrame(struct stackframe *frame)
{
unsigned long high, low;
unsigned long fp = frame->fp;
low = frame->sp;
high = T_ALIGN(low, 8192*1024); //Linux stack default size is 8M
if (fp < low + 4 || fp > high - 4)
return -EINVAL;
frame->pc = *(unsigned long *)(fp - 0);
frame->sp = frame->fp + 4;
frame->fp = *(unsigned long *)(fp - 4);
return 0;
}
static int fpDumpFrameInfo(char *pOutBuf,int inbufSize)
{
int urc = 0;
int ni = 0;
int fd = -1;
int nCnt = 8;
int nBufSize = 0;
unsigned long where = 0;
unsigned long tmpValue = 0;
unsigned long addr = 0;
unsigned char addrInfo[16] = {0};
unsigned long high, low;
struct stackframe frame;
if(NULL == pOutBuf)
return -1;
fd = open(DEVNAME, O_RDWR);
if(fd == -1)
{
printf("open file %s failed!\n", DEVNAME);
return-1;
}
ni = read(fd, uregs, sizeof(uregs));
if(ni < sizeof(uregs))
{
printf("get frametraceInfo failed\n");
return-1;
}
frame.fp = uregs[11];
frame.sp = uregs[13];
frame.lr = uregs[14];
frame.pc = uregs[15];
addr = uregs[11];
memset(pOutBuf,0,inbufSize);
memset(addrInfo,0,sizeof(addrInfo));
sprintf(addrInfo,"[PC=0x%x ",uregs[15]);
strcat(pOutBuf,addrInfo);
memset(addrInfo,0,sizeof(addrInfo));
sprintf(addrInfo,"LR=0x%x ",uregs[14]);
strcat(pOutBuf,addrInfo);
memset(addrInfo,0,sizeof(addrInfo));
sprintf(addrInfo,"SP=0x%x ",uregs[13]);
strcat(pOutBuf,addrInfo);
memset(addrInfo,0,sizeof(addrInfo));
sprintf(addrInfo,"FP=0x%x]",uregs[11]);
strcat(pOutBuf,addrInfo);
nBufSize = strlen(pOutBuf); //计算已写入长度
while (nCnt>0) {
nCnt--;
where = frame.pc;
low = frame.sp;
high = T_ALIGN(low, 8192*1024); //Linux stack default size is 8M
if (addr < low || addr > high -4)
break;
if(nBufSize > 225) //如果已写入长度大于225,不再追溯
break;
memcpy(&tmpValue,(char*)addr,sizeof(tmpValue));
memset(addrInfo,0,sizeof(addrInfo));
sprintf(addrInfo,"addr=0x%x ",tmpValue);
strcat(pOutBuf,addrInfo);
nBufSize += strlen(addrInfo); //更新已写入长度
addr-=4;
memcpy(&tmpValue,(char*)addr,sizeof(tmpValue));
addr = tmpValue;
urc = fpUnwindFrame(&frame);
if (urc < 0)
break;
}
return 0;
}