RISC-V架构异常处理与栈回溯(一)

目录

 RISC-V栈帧结构分析

开启编译优化后FP当成普通寄存器

实现自己的异常处理栈回溯函数

异常处理打印上下文和任务栈验证


        前面两篇文章分析了RISC-V上FreeRTOS的启动以及OS任务切换的处理流程,本文基于之前的分析,实现自己的栈回溯函数,程序出错陷入异常处理中,打印处函数调用关系及其上下文数据,方便以后问题调试。还是基于qemu模拟器,使用FreeRTOS官方标准发布包的FreeRTOS/Demo/RISC-V-Qemu-virt_GCC工程调试。

RISC-V栈帧结构分析

        写一个简单的test.c

long test_fun_b(long x, long y)
{
	printf("%s x:%ld y:%ld\r\n", __func__, x, y); 
	return x+y;
}

long test_fun_a(long m, long n, long x, long y)
{
	long b = 10;
	long c = 11;

	b = b + c + m;
	c = b + c + n;

	test_fun_b(b, c); 

	printf("%s b:%ld c:%ld\r\n", __func__, b, c);

	return b+c;
 
}

long test_fun_c(long x0, int x1, short x2, char x3, const char* x4, int x5, int x6, int x7, char x8)
{
	long b;
	long c;


	b = x0 + x1 + x2 + x3;
	c = x5 + x6 + x7 + x8;

	printf("%s b:%ld c:%ld %s x4:%d\r\n", __func__, b, c, x4, x8);

	b = test_fun_a(b, c, 0, 2); 

	return b+c;
}

int main(void)
{
        test_fun_c(1, 2, 3, 4, "test", 6, 7, 8, 9);

        return 0;
}

        使用RISC-V Linux编译器编译:riscv32-unknown-linux-gnu-gcc  test.c -o rv_test,

之后反汇编:riscv32-unknown-linux-gnu-objdump -S -d rv_test > rv_test.dsi

截取部分反汇编代码如下:

RISC-V架构异常处理与栈回溯(一)_第1张图片

从进入函数test_fun_a开始分析,如下图所示,假设此时sp处在1位置,

RISC-V架构异常处理与栈回溯(一)_第2张图片

执行完115行

   103f8: 7179                 addi sp,sp,-48

sp移动到了上图2位置,也就是在栈中留出了test_fun_a的栈帧,继续执完接下来两行代码,

   103fa: d606                 sw ra,44(sp)

   103fc: d422                 sw s0,40(sp)

则,ra和s0(s0也就是fp)都保存到当前函数栈帧中,如上图所示的位置。接下来执行  

   103fe:	1800                addi	s0,sp,48

s0的值更新了,指向了函数栈帧的开始位置,也就是上图sp位置1。继续执行,保存函数局部变量、参数a0,a1...等,直到141行调用函数test_fun_b时,test_fun_a的栈帧大致如下:

RISC-V架构异常处理与栈回溯(一)_第3张图片

继续执行,进入函数test_fun_b,执行完test_fun_b前面3行代码,

   103be:	1101                	   addi	sp,sp,-32
   103c0:	ce06                		sw	ra,28(sp)
   103c2:	cc22                		sw	s0,24(sp)

此时s0的值还未修改,sw s0,24(sp)压栈保存的是test_fun_a栈帧的起始位置,栈帧如下图所示。

RISC-V架构异常处理与栈回溯(一)_第4张图片

继续执行完

   103c4: 1000                 addi s0,sp,32

之后,s0又指向test_fun_b的栈帧起始位置,如下图所示:

RISC-V架构异常处理与栈回溯(一)_第5张图片

如此循环,相当于一个链表把该任务的栈帧给串起来。发生错误时,我们直接可以找到任务函数栈帧的起始位置s0和任务栈的栈顶指针sp,也可以找到该函数的返回地址ra,通过ra的值也就找到了该函数的调用者,就可以分析出函数调用关系。

下面是一张RV64和ARM64栈帧对比图:

RISC-V架构异常处理与栈回溯(一)_第6张图片

        以上分析用的是基于riscv32-unknown-linux-gnu-gcc编译器。FreeRTOS/Demo/RISC-V-Qemu-virt_GCC工程用的是riscv64-unknown-elf-gcc编译器,把上面的代码放入main_blinky.c中prvQueueSendTask中,

RISC-V架构异常处理与栈回溯(一)_第7张图片CFLAGS += -O0时,编译后反汇编,汇编代码如:

RISC-V架构异常处理与栈回溯(一)_第8张图片

可以看到栈帧保存方式也是一样的,也就是符合上面的规则栈帧保存规则。

开启编译优化后FP当成普通寄存器

后来发现,CFLAGS += -O2 开启O2优化编译后反汇编,s0/fp被作为普通寄存器使用了,汇编代码如下:

RISC-V架构异常处理与栈回溯(一)_第9张图片

RISC-V架构异常处理与栈回溯(一)_第10张图片

可以看到s0被当作普通寄存器使用了,也就是没有了fp指针,但是ra还是符合上面的规则,放在函数栈帧的起始位置,而且反汇编中找不到test_fun_a和test_fun_b,实际上这两个函数被展开到test_fun_c里面了。这样任务栈如下图所示(s0作为普通寄存器,没有了fp指针):

RISC-V架构异常处理与栈回溯(一)_第11张图片

         虽然riscv64-unknown-elf-gcc开启O2优化后没有fp指针,但是通过sp指针,结合反汇编代码还是可以找ra(反汇编代码函数入口第一行addi    sp,sp,-x,x即为函数栈帧的大小,sp还是指向栈顶),也就可以找到函数的Caller,可以手动完成栈回溯。

关于fp指针,可以参考文章[GCC入坑指南] -fomit-frame-pointer 和 -fno-omit-frame-pointer 编译标识 - 简书

RISC-V架构异常处理与栈回溯(一)_第12张图片

实现自己的异常处理栈回溯函数

        基于以上的分析,我们就是可以实现异常处理栈回溯函数。在前一篇文章RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_Dingjun798077632的博客-CSDN博客中分析了,同步异常处理的代码如下:

RISC-V架构异常处理与栈回溯(一)_第13张图片

synchronous_exception -> handle_exception,如果不是ecall触发的异常则调用application_exception_handler -> freertos_risc_v_application_exception_handler。

函数freertos_risc_v_application_exception_handler是一个.weak 函数,最终进入死循环。

RISC-V架构异常处理与栈回溯(一)_第14张图片

        所以,我们实现自己的freertos_risc_v_application_exception_handler函数,打印异常上下文信息与栈回溯。

        函数实现如下:参数mcause异常原因、mepc异常返回地址、mtval异常值寄存器(比如非法内存地址访问时,保存该内存地址值)、mstatus机器状态、pxTopOfStack异常发生时任务栈栈顶指针。

void freertos_risc_v_application_exception_handler(UBaseType_t mcause, UBaseType_t mepc, 
	UBaseType_t mtval, UBaseType_t mstatus, StackType_t * pxTopOfStack)
{
#if __riscv_xlen == 64
    #define portWORD_SIZE 8
    #define store_x sd
    #define load_x ld
#elif __riscv_xlen == 32
    #define store_x sw
    #define load_x lw
    #define portWORD_SIZE 4
#else
    #error Assembler did not define __riscv_xlen
#endif
    #define portCONTEXT_SIZE ( 31 * portWORD_SIZE )
    #define portCONTEXT_COUNT ( portCONTEXT_SIZE/portWORD_SIZE )

    int i;
    TaskStatus_t TaskStatus;
    UBaseType_t reg;
	StackType_t* pStack;
	UBaseType_t* fp;

    xprintf("\033[0m\033[1;31m%s\033[0m","Panic: Freertos risc-v application exception handler\n");
	//xprintf("Panic: Freertos risc-v application exception handler\n");
	xprintf("mcause:	0x%lx\nmepc:	0x%lx\nmtval:	0x%lx\nmstatus:0x%lx\npxTopOfStack:  %p\n", mcause, mepc - portWORD_SIZE, 
		mtval, mstatus, pxTopOfStack);

/* 
    参考portcontextRESTORE_CONTEXT中恢复context寄存器的顺序,打印异常发生前的各通用寄存器值
    load_x  x1, 1 * portWORD_SIZE( sp )
    load_x  x5, 2 * portWORD_SIZE( sp )
    load_x  x6, 3 * portWORD_SIZE( sp )
    load_x  x7, 4 * portWORD_SIZE( sp )
    load_x  x8, 5 * portWORD_SIZE( sp )
    load_x  x9, 6 * portWORD_SIZE( sp )
	... 
	...
*/
    xprintf("The exception context regs:\n");
    for (i = 1; i <= 28; i++){
        reg = *((UBaseType_t*)pxTopOfStack + i);
        if (i >= 2) {
            xprintf("x%d: 0x%lx\n", i+3, reg);
            if (i+3 == 8)
              fp = (UBaseType_t*)reg; //Risc-V寄存器x8即是s0/FP
        } else {
            xprintf("x%d: 0x%lx\n", i, reg);
        }
    }

    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED ) 
	{
         vTaskGetInfo(NULL, &TaskStatus, pdTRUE, eInvalid);

        xprintf("Ther current task is %s, StackBase:%p, StackEnd:%p, StackTop:%p, StackHighWaterMark:%d\n", 
		    TaskStatus.pcTaskName, TaskStatus.pxStackBase, TaskStatus.pxEndOfStack, pxTopOfStack, TaskStatus.usStackHighWaterMark);

        if (TaskStatus.pxEndOfStack < pxTopOfStack + portCONTEXT_COUNT) {
            xprintf("Error: he current task stack error pxEndOfStack:%p pxTopOfStack:%p\n", TaskStatus.pxEndOfStack, pxTopOfStack);
            goto loop;
        }

        i = 0;
		//循环打印出函数的fp和ra返回地址, TaskStatus.pxEndOfStack为task栈底(最高地址)
        /*
            这部分通过fp查找ra开启O1或者O2优化后有问题,可以去掉(刚开始写文章时没发现开启优化fp被作为普指针)
        */
		xprintf("Ther current task bactrace:\n");
        while (fp < TaskStatus.pxEndOfStack) 
        {
            xprintf("fun%d: FramePointer:0x%lx\tReturnAddr:0x%lx\n", i++, fp, *(fp-1));
			fp = (UBaseType_t*)*(fp-2);
        } 

		//打印出当前栈数据,任务栈贞起始位置为pxTopOfStack + portCONTEXT_COUNT
		xprintf("Ther current task Stack:");
		i = 0;
		for (pStack = pxTopOfStack + portCONTEXT_COUNT; pStack < TaskStatus.pxEndOfStack; pStack++)
		{
			reg = *(UBaseType_t*)pStack;
			if (i++ % 8 == 0) {
				xprintf("\n%p:  0x%lx  ", pStack, reg);
			} else{
				xprintf("0x%lx  ", reg);
			}
		}

		xprintf("\n");
		//任务栈打印完成后,pStack必然指向pxEndOfStack
		if (pStack != TaskStatus.pxEndOfStack)
			xprintf("Error: the pStack not pointer to the EndOfStack\n");
    } else {

    }

loop:
	xprintf("Freertos risc-v application exception handler end\n");
	while(1);
}

        为了显眼,把freertos_risc_v_application_exception_handler的第一个打印Pannic用红色显示xprintf("\033[0m\033[1;31m%s\033[0m","Panic: Freertos risc-v application exception handler\n");

        上面代码中xprintf是自己写的printf函数,在中断处理中用xprintf代替printf。上面的宏定义portCONTEXT_SIZE、 portWORD_SIZE是从FreeRTOS\Source\portable\GCC\RISC-V\portContext.h中copy过来的(和架构相关)。

portContext.h中的portCONTEXT_SIZE宏定义如下:

RISC-V架构异常处理与栈回溯(一)_第15张图片

        修改FreeRTOS\Source\portable\GCC\RISC-V\portASM.S的代码,在调用函数freertos_risc_v_application_exception_handler之前,通过a0-a4传入函数的参数(这里application_exception_handler之前a0已经保存为了mcause,a1保存的mepc + 4,之前的文章已经分析过了),portASM.S修改如下:

RISC-V架构异常处理与栈回溯(一)_第16张图片

        同时freertos_risc_v_application_exception_handler中打印了当前任务的状态信息,需要开启部分FreeRTOS的部分配置:

RISC-V架构异常处理与栈回溯(一)_第17张图片

在FreeRTOS\Source\include\task.h增加代码    

#elif ( configRECORD_STACK_HIGH_ADDRESS == 1 )

        StackType_t * pxEndOfStack;               /* Points to the end address of the task's stack area. */

修改后如下图(新增了57-58行):

RISC-V架构异常处理与栈回溯(一)_第18张图片

打印任务栈时,需要通过vTaskGetInfo获取任务栈的栈底(栈起始位置)。

该函数代码FreeRTOS\FreeRTOS\Source\tasks.c

RISC-V架构异常处理与栈回溯(一)_第19张图片

异常处理打印上下文和任务栈验证

        在之前测试函数test_fun_b中加入一条非法的指令,编译反汇编(这里需要在Makefile中将CFLAGS += -O2改为CFLAGS += -O0,关闭O2优化,否则test_fun_b和test_fun_a编译时会直接展开到test_fun_c,栈回溯没有了函数多重调用关系)。

        下面的测试还是基于CFLAGS += -O0编译的,一开始没发现CFLAGS += -O2会把fp当作普通寄存器使用,下面演示略过fp栈回溯部分,通过sp对照反汇编找到ra,手动完成栈回溯,开启O2依然起效,文章都写好了,就懒得改了。

RISC-V架构异常处理与栈回溯(一)_第20张图片

        程序编译运行后,进入异常处理freertos_risc_v_application_exception_handler函数中打印如下(对照反汇编代码):

RISC-V架构异常处理与栈回溯(一)_第21张图片

        对照反汇编代码,可以看到触发异常的pc地址为0x800008b8,该地址在函数test_fun_b中,而且刚好是增加的错误代码asm volatile(".word 0x1234567")。上面打印的最后一个函数func0的地址ReturnAddr为0x80000948,0x80000948对应的代码恰好是test_fun_a中调用test_fun_b函数的下一条指令的地址。

        对应的反汇编代码也可以看出,test_fun_b函数的栈帧大小为32byte, ra保存在sp偏移28的位置,从栈帧起始0x80082dd0 + 28byte的地方也是0x80000948。

        同样的方法也可以找到test_fun_a的ReturnAddr为0x80000a34,依此类推,最终找到任务的入口函数,如下图。

RISC-V架构异常处理与栈回溯(一)_第22张图片

RISC-V架构异常处理与栈回溯(一)_第23张图片

当我们修改test_fun_b函数,改为非法地址访问时

RISC-V架构异常处理与栈回溯(一)_第24张图片

重新编译运行,可以看到mcause为0x00000007,mtval为0x00000034即为上面非法访问的地址

RISC-V架构异常处理与栈回溯(一)_第25张图片

mcause 0x00000007对应Store sccess faut,之前mcause 0x00000002对应Illegal instruction。

        需要说明的是,一般Load和Store指令都是非精确异常,也就是mepc并不一定指向触发异常的指令地址,但mtval是准确的(指向访问出错的内存地址)。现代cpu为了提高效率,一般都支持超标量流水线,乱序执行,(比如当前乘法指令,下一条加法指令而且操作数不相干,乘法指令需要多个周期,加法指令就可能先执行完),加上Cach影响(比如当前指令Catch,下一条指令Catch命中)等...,cpu并不保证严格的遵守指令流水线。硬件外设驱动,有很多时候需要保证前一条指令先于后一条指令执行完,就需要用的Fence、Fence.i指令(对应arm也有dsb、dmb、isb指令),这里就不展开讨论了。

        下面时平头哥玄铁E907(RISC-V架构,arm等其它处理器也有类似的特性)用户手册对精确或者非精确异常的描述:

RISC-V架构异常处理与栈回溯(一)_第26张图片

你可能感兴趣的:(RISC-V,FreeRTOS,risc-v)