基于GD32 C10x MCU栈回溯调试原理实现

在嵌入式软件开发调试过程中,经常会出现程序在运行过程中莫名其妙崩溃进入HardFault异常中断。因为GD 或者ST 芯片对于hardfault异常中断的处理是跳进一个函数死循环,不利于程序进入异常问题的定位分析。平时我们通过调试器进入Debug模式,设置断点然后一步步定位发生异常的位置,这种方法只适用于代码量比较小的情况。当代码量比较大时,而且函数调用关系又复杂时,这种方法很费时间。本文基于GD32 C10x芯片实现一种栈会回溯调试方法,用于当程序因为数组越界、野指针、堆栈溢出或者非法地址访问时,快速定位发生异常的位置。

1.栈回溯调试原理
当MCU发生异常中断时,硬件会自动将xPSR、PC、LR、R12、R3~R0入栈,PC指针是程序发生崩溃的位置,LR是发生崩溃程序的下一条指令地址,R12、R3 ~R0是发生崩溃时内部寄存器的值。栈回溯调试原理就是程序发生崩溃后,在进入异常中断后,通过汇编程序,将R11 ~ R4 手动入栈,并将当前栈指针值保存在R0中,通过汇编调用C函数,C函数的入口参数即为栈的地址,在C函数中按照先进后出原则通过串口依次打印栈中信息。程序进入异常时,硬件自动保存栈内容如下:
进入异常栈内容
2. 栈回溯原理程序实现
本文通过定义一个指针,指向非法的sram地址,通过向非法地址赋值,从而进入hardfault异常中断。主函数代码如下:

#include <stdio.h>
#include "gd32c10x.h"
#include "main.h"
/* 异常中断栈内容结构体*/
typedef struct
{
	uint32_t LR_hardfault;
	uint32_t R4;
	uint32_t R5;
	uint32_t R6;
	uint32_t R7;
	uint32_t R8;
	uint32_t R9;
	uint32_t R10;
	uint32_t R11;
	uint32_t R0;
	uint32_t R1;
	uint32_t R2;
	uint32_t R3;
	uint32_t R12;
	uint32_t LR;
	uint32_t PC;
	uint32_t xPSR;
}Hard_Fault_Typedef;
uint8_t c[1024]={0};
uint32_t *p=(uint32_t*)0x20ffffff;

/*hardfault stack information output function*/
void hw_fault_printf(Hard_Fault_Typedef* hard_fault)
{
	printf("LR_hardfault: 0x%x\n",hard_fault->LR_hardfault);
	printf("R4: 0x%x\n",hard_fault->R4);
	printf("R5: 0x%x\n",hard_fault->R5);
	printf("R6: 0x%x\n",hard_fault->R6);
	printf("R7: 0x%x\n",hard_fault->R7);
	printf("R8: 0x%x\n",hard_fault->R8);
	printf("R9: 0x%x\n",hard_fault->R9);
	printf("R10: 0x%x\n",hard_fault->R10);
	printf("R11: 0x%x\n",hard_fault->R11);
	printf("R0: 0x%x\n",hard_fault->R0);
	printf("R1: 0x%x\n",hard_fault->R1);
	printf("R2: 0x%x\n",hard_fault->R2);
	printf("R3: 0x%x\n",hard_fault->R3);
	printf("R12: 0x%x\n",hard_fault->R12);
	printf("LR: 0x%x\n",hard_fault->LR);
	printf("PC: 0x%x\n",hard_fault->PC);
	printf("xPSR: 0x%x\n",hard_fault->xPSR);
}
/*usart configure function*/
void USART_init(void)
{
	rcu_periph_clock_enable(RCU_USART0);
  /*enable COM GPIO clock USART0*/
  rcu_periph_clock_enable(RCU_GPIOA);
  /* connect port to USARTx_Tx */
  gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_9);
  /* connect port to USARTx_Rx */
  gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ,GPIO_PIN_10); 
  usart_deinit(USART0);
  usart_baudrate_set(USART0, 115200U);
  usart_receive_config(USART0, USART_RECEIVE_ENABLE);
  usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
  usart_enable(USART0);
}

/*taskA function*/
void task_A(void)
{
	task_B();	
}

/*taskB function*/
void task_B(void)
{
	task_C(1);
}
uint8_t task_C(uint8_t data)
{
	*p=2;
	c[10]=data;
	return 100/data;
}

/*taskC function*/

int main(void)
{
  USART_init();
  task_A();	
	while(1)
  {
  }
}

int fputc(int ch, FILE *f)
{
    while (RESET == usart_flag_get(USART0, USART_FLAG_TBE));
    usart_data_transmit(USART0, (uint8_t) ch);
    return ch;
}

异常中断函数汇编代码如下:

HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler                 [WEAK]  
				IMPORT  hw_fault_printf
				mrs     r0, msp                           ;R0保存进入异常中断时,栈的地址
				stmdb   r0!, {r4-r11}                     ;依次将R11~R4寄存器值入栈
				stmdb   r0!, {lr}                         ;因为后面调用C函数可能会改变异常中断LR的地址,故将链接地址入栈
				msr     msp, r0                           ;将此时栈顶的实际地址赋值给msp主堆栈指针     
				push    {lr}                              ;将异常lr链接地址入栈
				bl      hw_fault_printf                   ;调用C函数打印异常中断栈内容信息
				pop     {lr}                              ;出栈恢复异常lr的值
		        bx      lr                               ;返回线程模式并使用主堆栈指针         
                B       .
                ENDP

串口助手打印信息如下:
打印异常栈信息
由上图我们可以看出进入异常时LR的地址为0xfffffff9,查阅内核手册可知此时SP指针使用的是主堆栈指针MSP,程序发生崩溃时PC指针的地址为0x8000b12,崩溃时下一条指令的地址为0x80008f7。知道以上信息后,我们可以在程序编译输出的反汇编代码中查找这两个地址信息,可以迅速定位程序崩溃地址。
反汇编代码查找程序崩溃地址
由上图可知可迅速定位出程序崩溃地址发生在C函数,我们只需要分析C函数中的代码,找出程序进入异常中断的原因。这里说一个大家容易产生疑惑的知识点哈,由上面内容可知崩溃时下一个指令地址为0x80008f7,我们会发现在反汇编代码中找不到这个地址,当时当我们将它二进制最低位清0,查找0x80008f6这个地址就可以找到。这是什么原因呢?因为ARM内核是Thumb 16bit和32bit 指令混用,此时是Thumb 16bit指令,所以地址一般是2 byte 对齐,所以需要将最低位清零,就和ARM PC指针地址每次都是加4原理一样。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值