简介:
gcc 编译器可以使用 “-fno-omit-frame-pointer”(不忽略栈帧指针) 编译选项,编译器会把特定寄存器作为栈帧保存寄存器,gcc 手册中对该编译选项的描述如下:
开启该编译选项后,risc-v 架构的处理器是使用S0寄存器作为特定的栈帧寄存器,来保存每个函数的栈帧,使用该编译选项可以根据栈帧的情况实现函数调用的栈回溯,RISC-V 文档中对寄存器的使用规定中可以看出s0(x8)作为栈帧保存寄存器使用
从上述描述还可以看出当函数是leaf 函数时不需要保存栈帧,从计算机的角度来看,用保存栈帧与仓库存储的类比,可以更理论化地解析如下:
普通函数调用:仓库存储多个任务的中间状态
函数的局部变量(相当于任务使用的工具)。
函数的返回地址(相当于任务完成后回到正确的下一步)。
调用者的寄存器状态(相当于任务前的仓库初始状态)。
每次一个普通函数调用另一个函数(比如函数A调用函数B),函数A需要暂时停止当前任务,并记录自己执行到哪一步、使用了哪些资源等信息。这些信息被保存在栈帧中,就像在仓库中分配一个专属空间,存储当前任务的中间状态。
栈帧保存的信息包括:
当子函数(B)完成后,仓库通过读取之前保存的状态,帮助主函数(A)恢复继续工作。
叶子函数调用:直接处理而不需要保存状态
叶子函数是调用链的终点,不会再调用其他函数。因为它的逻辑简单、独立,不需要保存调用者的执行状态。执行叶子函数时,寄存器或其他资源可以直接被使用,完成任务后直接返回,不涉及栈帧的分配和回收。
类比到仓库:叶子函数的任务就像一个独立的简化流程,执行时只需要直接取用货物并返回,整个过程无需额外占用储物空间(栈帧)存储中间状态。
效率和资源管理:减少不必要的开销
栈帧的保存和恢复会引入额外的开销(比如保存栈、恢复栈帧),类似于在仓库中开辟和清理储物空间。而叶子函数由于不需要保存这些状态,可以直接运行完成,节省了内存和时间。
普通函数调用需要“仓库”来保存当前任务状态,以便任务切换后能够恢复。而叶子函数由于没有后续调用,任务独立、简单,直接完成返回,不需要保存状态,从而避免了额外的存储开销,执行更高效。
我们编写如下的测试代码验证栈帧调用的功能
void func_d(void) { printf("i am funcd\r\n"); vTaskDelay(pdMS_TO_TICKS(1000000)); } void func_c(void) { printf("i am funcc\r\n"); func_d(); printf("bye funcc\r\n"); } void func_b(void) { printf("i am funcb\r\n"); func_c(); printf("bye funcb\r\n"); } void func_a(void) { printf("i am funca\r\n"); func_b(); printf("bye funca\r\n"); }
-fno-omit-frame-pointer 编译选项后,编译器编译的代码会在函数掉用过程中加入sp,ra 的保存处理过程,对应过程如下。
编译器对每个函数入口都添加上述的栈保存处理,最终上述func_a->func_b->func_c->func_d函数调用的栈最终在栈中体现如下:
上述栈帧相当于链表结构,我们只要知道func_d 的s0 FP 地址信息就可以获取func_c 的信息从而完成函数栈的回溯。
添加如下代码即可实现函数调用的回溯
#if __riscv_xlen == 32 #define BACKTRACE_LEN 8 #endif #if __riscv_xlen == 64 #define BACKTRACE_LEN 16 #endif struct stackframe { uint32_t s_fp; // frame pointer uint32_t s_ra; // return address }; unsigned int backtrace(char argc,char ** argv) { /* Thread list */ rt_list_t * pos; tskTCBList * node; struct stackframe * fp; uint8_t num = 0; rt_list_for_each(pos,&tasklist) { num = 0; node = rt_list_entry(pos,tskTCBList,list); const register unsigned long current_sp __asm__("sp"); // get current stack pointer if(current_sp<= (uint32_t)node->tcb->pxEndOfStack && current_sp >= (uint32_t)node->tcb->pxStack) { fp = (struct stackframe *)(__builtin_frame_address(0) - BACKTRACE_LEN); printf("=%p\r\n",fp); } else { fp = (struct stackframe *)(node->tcb->pxTopOfStack[8] - BACKTRACE_LEN); printf("==%p\r\n",fp); } printf("task %s backtrace fp 0x%08p.\r\n",node->tcb->pcTaskName,fp); while(1) { if(fp->s_fp > (uint32_t)node->tcb->pxEndOfStack || fp->s_fp < (uint32_t)node->tcb->pxStack) { break; } printf("[%d] stack fp = 0x%08x ra = 0x%08x.\r\n",num++,fp->s_fp,fp->s_ra); fp = (struct stackframe *)(fp->s_fp - BACKTRACE_LEN); } } return 1; } LTSH_FUNCTION_EXPORT(backtrace,"show task backtrace");