ARM Linux 手写实现栈回溯

        在开发中遇到这样一个问题,程序要跑很久才会出现一次崩溃,由于系统有硬件看门狗,因此程序在崩溃时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;
}

 

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值