【嵌入式】J-Link Commander + map文件 + asm文件 离线定位crash崩溃

前言

以此来刷新下年久失修的博客的活跃度。嵌入式开发中,最令人头疼的就是遇到程序崩溃的问题,对于稳定性要求极高的场合,这无疑是重大事故。导致程序崩溃的原因一般分为四种:HardFault、MemManage、BusFault、UsageFault。对于各种Fault的解释和引起原因,可以自行百度,本文主要讲解在各种Fault发生后,如何利用JLink Commander保存异常现场,并在此基础上通过map、asm文件寻找导致异常的真凶。
在程序开发阶段,如果发生可以稳定复现异常,一般可借助IDE调试的方式进行排查。然而,对于偶发的异常,使用IDE排查就显得力不从心了。另外,对于现场运行的设备(未使能看门狗),如果发生了异常,此时已经无法通过Jlink连接至IDE调试了。因此,如何借助JLink将珍贵的异常现场保存并根据现场进行分析就显得尤其重要。

一 崩溃环境制造

为展示抓取异常现场、分析异常元凶,模拟了一个比较隐晦的崩溃情况。阅读并考虑以下代码:

typedef struct{
	 uint8_t function_id;
	 void (*function)();
}T_function;

void function_instance()
{
		PRINT("Here is a function_instance\r\n");
}

uint8_t buf_temp[64] __attribute__((at(0x20004300))) = {1};

T_function func __attribute__((at(0x20004300 + 64)))= {
		1,
		function_instance
};

void crashcode()
{
		uint8_t i;
	
		PRINT("func.function addr before illegal operation:0x%08x\r\n",(uint32_t)func.function);
	
		func.function();
		
		for(i = 0; i < 128; i++)
		{
				buf_temp[i] = i;
		}
		
		PRINT("func.function addr after illegal operation:0x%08x\r\n",(uint32_t)func.function);
		
		func.function();
		
		PRINT("Have been crashed, will not be printed\r\n");
}

代码中首先定义了一个T_function结构体,其中有个成员为函数指针,func结构体初始化时将其赋值为function_instance函数。
然后,定义了一个64字节的buf_temp缓存。为了让buf_temp与func结构体在RAM中紧挨着,这里用到了__attribute__方法,将两个变量放在了连续的RAM空间中。如此一来,内存中0x20004300地址存放了64字节的buf_temp数组,紧跟着buf_temp数组的是func结构体。
最后,编写crashcode函数,顾名思义,这段代码就是为了让程序崩溃的。

二 崩溃代码分析

在看分析之前,读者可以先自己阅读并分析crashcode函数,看是否能分析出异常的根本原因。
可以把这个简单的函数拆分成6部分来看,依次为:
1. 打印非法操作之前,func结构体中function成员的地址。正常情况下,function将指向function_instance;
2. 执行func.function(),由于function指向了function_instance函数,因此就相当于运行了function_instance函数;
3. 对buf_temp数组进行循环赋值;
4. 打印非法操作之后,func结构体中function成员的地址;
5. 再次执行func.function();
6. 执行func.function()后,进行一句话的打印,此时由于程序已经崩溃,因此这个打印并不会执行成功,因为程序已经停在了HardFault_Handle中断的while(1)死循环里。
问题出在第3部分,对一个64字节的buf_temp数组进行了128字节的写入,数组越界导致冲掉了紧随其后的func结构体,使得其中的function成员指向了一个未知的地址,因此当再次执行func.function()时,系统抛出异常,程序崩溃。
这就是一段数组越界读写导致崩溃的典型代码。当内存中存放指针的变量被意外篡改后,下次使用这个指针时就有极大的概率会发生崩溃。

三 下载验证

在这一小节之后,我们假设我们完全不知道程序崩溃的原因。 将上述代码下载到MCU中,上电运行,果不其然,"Have been crashed, will not be printed\r\n"并没有得到打印。在非法操作前,func.function指向了0x000006eb地址,而后来指向了0x47464544地址。
在这里插入图片描述

四 异常现场抓取

这个时候,我们使用JLink Commander连接MCU,依次选择MCU信号或内核类型、JTag连接方式、JTag speed之后,Jlink Commander就成功的连上了MCU。
在这里插入图片描述
由于此时程序已经处于崩溃状态(在HardFault_Handle里执行while(1)),因此我们在Jlink Commander中键入halt停止MCU的,从而抓取此时的各个内核寄存器值,如下图所示。
在这里插入图片描述
此时,我们可以读出MCU中R0~R12寄存器、SP栈顶指针、MSP主栈顶指针、PSP任务栈顶指针、LR链接寄存器等丰富的信息。也可以看出IPSR目前的值为003,即发生了HardFault异常。要定位崩溃前执行的程序,就要知道MCU内核中异常时的压栈顺序,然后通过SP指针倒推出进入HardFault_Handle之前在运行那一句程序。
因此,我们键入mem 0x20007f58 128并敲回车,如下图所示。这个指令的意思为:从0x20007f58的内存地址读出0x128个字节的数据。为什么是从0x20007f58读呢?因为上图中可以看出,此时的栈顶的位置就是0x20007f58,我们读出0x20007f58之后的内存后,就可以通过异常压栈顺序进行倒推,从而定位引起崩溃的程序。
在这里插入图片描述

五 异常现场分析

当MCU发生异常中断时,其寄存器的压栈顺序与内核有关。对于Cortex-M系列内核,其压栈顺序为PC LR R12 R3 R2 R1 R0,SP栈顶目前指向的是R0,也就是说0x20007f58地址现在存放的是R0的内容。因此,我们可以逆推一下,通过上图我们不难推出此时PC LR R12 R3 R2 R1 R0的值分别为0x47464544、0x0000071f、0x00000000、0x0000071b、0x40003400、0x47464544。需要关注的是LR 寄存器内容,即0x0000071f,这个值保存的是进入异常中断前的PC指针的内容,即正在运行的程序的下一条指令所在的地址,
此时,我们通过asm汇编文件就可以查看0x0000071f地址存的是哪条汇编指令。另,asm汇编文件各个IDE都可以生成,不会的百度。
在asm文件中,我们定位到0x0000071f地址的汇编程序,可以清晰的发现这段代码处于crashcode函数中,并且就是执行func.function()前后的一段程序。
在这里插入图片描述

因此,我们就跑到crashcode里分析,func.function()附近发生了什么呢?范围一下子缩小了,就不难发现问题了。通过阅读代码不难发现,对64字节长度的buf_temp数组进行了128字节的写入,越界了。
那么为什么越界操作会把MCU干崩了呢?这就要看内存中buf_temp之后存的是什么了 。此时map文件派上用场了,不知道什么是map文件的再次自行百度。我们打开map文件,搜索buf_temp,如下所示。可以看出buf_temp存放在0x20004300的地址,并占用了64个字节。紧随其后的就是func、tick、devInfoCBs等等变量。由于我们对buf_temp进行了128字节的写入,因此,从func变量0x20004340开始,后边64字节内的内存区域全被篡改了。
在这里插入图片描述
此时,func结构体中的function指向了一个非法的地址,正如第一张图的串口打印内容所示,function指向了0x47464544。那么将function所谓一个void函数取值并运行时,程序崩溃。而function正常指向的是function_instance函数的地址,是0x000006eb,从map文件中也可以看出。在这里插入图片描述

六 总结

本文只是提供了一个比较典型的崩溃分析案例,讲述如何用JLink Commander帮助我们抓取崩溃现场并分析崩溃原因。本例子抛在cortex-m0内核的裸机系统中,对于跑了操作系统的程序而言,分析SP的时候就要去找PSP进行倒退。而对于非cortex-m内核的MCU,只要了解其异常时的压栈顺序,也可以借助此办法进行异常抓取和分析。

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜老越

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值