1、异常现象

  • 程序出现了下列trap的打印(打印右边的注释,是根据程序编译的.map和.lst文件得到的内容)
ra         = 0x1ff04eb4  //_printf_i 函数内的一个代码地址
sp         = 0x1ff0e4c0  //栈指针,指向当前栈顶位置
gp         = 0x1ff0a7a0  //_sp地址(栈顶的地址)
tp         = 0xa5a5a5a5
t0         = 0x1ff02056
t1         = 0xfffffff0
t2         = 0xa5a5a5a5
fp         = 0x1ff0e610  //帧指针,指向当前栈帧的基址
s1         = 0x1ff09250
a0         = 0xfffe1265  //非法地址
a1         = 0x0
a2         = 0xfffe1264
a3         = 0x1ff0e748
a4         = 0x1ff0e60c
a5         = 0x1ff0e744
a6         = 0x0
a7         = 0x64
s2         = 0x1ff0e6a8
s3         = 0x1ff06634
s4         = 0x1ff09250
s5         = 0xfffe1265  //非法地址
s6         = 0xa
s7         = 0x25
s8         = 0x1
s9         = 0x7
s10        = 0xa5a5a5a5
s11        = 0xa5a5a5a5
t3         = 0xa5a5a5a5
t4         = 0xa5a5a5a5
t5         = 0xa5a5a5a5
t6         = 0xa5a5a5a5
mstatus    = 0x9880
mepc       = 0x1ff05e78  //memchr 函数内的一个代码地址
msubm      = 0x80
mcause     = 0x38000005  //这是一个异常
mdcause    = 0x2
mtval      = 0xfffe1265  //非法地址
stack trace:0x1ff067c8   //_svfiprintf_r 函数内的一个代码地址
stack trace:0x1ff01d3e
stack trace:0x1ff06994   //_svfiprintf_r 函数内的一个代码地址
stack trace:0x1ff04f8e   //_vsniprintf_r 函数内的一个代码地址
stack trace:0x1ff01064
stack trace:0x1ff0209a   //_puts 函数内的一个代码地址
stack trace:0x1ff020d4   //_printf 函数内的一个代码地址
stack trace:0x1ff03334   //tc_run_test_case 函数内的一个代码地址
stack trace:0x1ff038d6   //fpga_test_case_task 函数内的一个代码地址
stack trace:0x1ff01d3e
stack trace:0x1ff01422
stack trace:0x1ff01848
stack trace:0x1ff0104e   
stack trace:end

2、名词解释

2.1、Stack Trace(堆栈跟踪)
  • 是程序发生异常(或主动打印)时,输出的一系列方法调用链记录。它告诉你“程序在执行到哪一行代码时出了问题”,以及“这个方法是经由怎样的调用路径被触发的”。

  • 阅读顺序: 从下往上读,就是调用的历史;从上往下读,就是异常传播的路径。

  • 结合以上理论,可以知道异常的代码调用顺序如下:

fpga_test_case_task
tc_run_test_case
_printf
_puts
_vsniprintf_r
_svfiprintf_r
2.2、Exception Register(异常寄存器)
  • Exception Register 是当程序发生异常时,CPU或操作系统保存的异常现场信息,记录了异常发生瞬间的处理器状态。它与 Stack Trace 是互补的两种调试信息:

  • Stack Trace:程序的调用链路(逻辑层面的执行路径)

  • Exception Register:CPU的硬件状态(物理层面的执行现场)

(1)、mepc,记录异常发生时正在执行的指令地址
  • 最重要!直接告诉你代码执行到了哪个内存地址
(2)、sp,栈指针
  • 指向当前栈顶位置用于恢复函数调用栈、局部变量等
(3)、fp,帧指针
  • 指向当前栈帧的基址,用于回溯调用链(Stack Trace 的基础)
(4)、mstatus,(需要查RISCV的手册)
  • 状态寄存器 / 标志位
(5)、mcause,是 RISC-V 架构中的一个异常/中断原因寄存器
mcause     = 0x38000005
  • 最高位第31bit为0,表示这是一个异常
  • 第0bit为1,指令地址未对齐
  • 第2bit为1,非法指令
  • 第27bit为1,加载虚拟地址未对齐
  • 第28bit为1,加载虚拟访问故障
  • 第29bit为1,指令虚拟地址未对齐
(6)、mtval,异常相关的额外信息(如错误地址)
  • mcause 只告诉"发生了什么",mtval 告诉"具体是什么"(如错误地址、非法指令值)
mtval      = 0xfffe1265
  • 这个表示错误的地址,即程序运行过程中,发生了地址被篡改的行为

3、解决错误

  • 根据程序的打印,先到程序异常前的位置打个断点,可以观察到group指针,name指针,case_list指针的地址都是正常的sram地址。

在这里插入图片描述

  • 程序异常后,再观察,name地址、case_list等地址已经变成异常值,且恰好和mtval的值相等
    在这里插入图片描述

  • 可以观察到,group指针指向name指针,name指针指向"fpga_test_case_list"内容,

  • group指向的地址为0x1ff09afc,name指向的地址为0x1ff08d14

  • 即0x1ff09afc地址存放的是name指针变量,0x1ff08d14地址存放的是"fpga_test_case_list"内容

  • name指针变量的值被篡改了,可以用 *(int *)0x1ff09afc的方式来得到name指针变量的值

  • 在程序异常前,先打数据断点,再执行程序,调试时程序就会在异常的地方停下来,就能找到是哪段代码导致的异常。

-exec watch *(int *)0x1ff09afc

4、最终原因

  • 代码流程类似下面
volatile uint8_t flag = 0;
uint32_t value[10] = {0};

//中断复位函数
void isr()
{
    value[flag] = 10;
    flag++;
}

void main()
{
    while(flag != 10);
    flag = 0;
}
(1)、数据竞争(Data Race)
  • 主循环和ISR同时访问共享变量 flag,虽然使用了 volatile,但缺乏原子性保证:
// 主线程中
while(flag != 10);  // 读取flag
flag = 0;           // 写入flag

// ISR中
flag++;             // 读取-修改-写入操作,非原子
(2)、flag++ 不是原子操作
ldrb r0, [flag_addr]   ; 读取
add  r0, r0, #1        ; 修改
strb r0, [flag_addr]   ; 写入
  • 如果主循环在这三条指令之间读取flag,可能读到中间状态。

  • volatile 只能防止编译器优化,不能保证原子性,也不能防止CPU乱序执行。

  • 所以当主程序读到flag等于10的时候,中断可能还在写入11,就会发生数组越界的问题。

  • 可以修改中断服务函数如下

//中断复位函数
void isr()
{
		if(flag < 10)
		{
    		value[flag] = 10;
    		flag++;
    	}
}
  • 或者使用原子操作,临界区关中断,使用双缓冲/环形缓冲区等方式解决。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐