RISC-V MCU堆栈机制
1、什么是堆栈?
在嵌入式的世界里,堆栈通常指的是栈,严格来说,堆栈分为堆(Heap)和栈(Stack)。
- 栈(Stack): 一种顺序数据结构,满足后进先出(Last-In / First-Out)的原则,由编译器自动分配和释放。使用一级缓存,调用完立即释放。
- 堆(Heap):类似于链表结构,可对任意位置进行操作,通常由程序员手动分配,使用完需及时释放(free),不然容易造成内存泄漏。使用二级缓存。
2、堆栈的作用
- 函数调用时,如果函数参数和局部变量很多,寄存器放不下,需要开辟栈空间存储。
- 中断发生时,栈空间用于存放当前执行程序的现场数据(下一条指令地址、各种缓存数据),以便中断结束后恢复现场。
3、堆栈大小定义
RISC-V MCU的堆栈大小通常在ld链接脚本中定义,关于ld链接脚本可查看该文:RISC-V MCU ld链接脚本说明。
1 ENTRY( _start ) /* 入口地址 */ 2 3 __stack_size = 2048; /* 定义栈大小 */ 4 PROVIDE( _stack_size = __stack_size );/* 定义_stack_size符号,类似于全局变量 */ 5 6 MEMORY 7 { 8 FLASH (rx) : ORIGIN = 0x00000000 , LENGTH = 0x10000 9 RAM (xrw) : ORIGIN = 0x20000000 , LENGTH = 0x5000 10 } 11 /* 12 ... 13 中间省略 14 ... 15 */ 16 17 .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : /* 分配栈空间0x20004800 ~ 0x20005000,共2KB */ 18 { 19 . = ALIGN(4); 20 PROVIDE(_susrstack = . ); 21 . = . + __stack_size; 22 PROVIDE( _eusrstack = .); 23 } >RAM
以RISC-V MCU CH32V103为例,在其ld链接脚本中,定义了_stack_size符号,值为 2048 Byte,后面使用该值在.stack段中分配栈空间,可更改此值调整栈空间大小。
CH32V103 的RAM共20KB,除去程序用到的data、bss段,剩下空间即为动态数据段,供堆栈的动态使用。
ld链接脚本中,没有明确定义heap堆的大小,按照其定义,动态数据段,除了stack占用的,剩下的都可用于heap,通过malloc进行动态管理。
4、压栈出栈过程
以CH32V103 printf函数调用为例,其反汇编代码如下:
1 000007a4 <iprintf>: 2 7a4: 7139 addi sp,sp,-64 # 调整堆栈指针sp,分配64字节的栈空间 3 7a6: da3e sw a5,52(sp) # 压栈,保存a5寄存器的值 4 7a8: d22e sw a1,36(sp) # 压栈,按需保存相应的寄存器 5 7aa: d432 sw a2,40(sp) 6 7ac: d636 sw a3,44(sp) 7 7ae: d83a sw a4,48(sp) 8 7b0: dc42 sw a6,56(sp) 9 7b2: de46 sw a7,60(sp) 10 7b4: 80818793 addi a5,gp,-2040 # 20000078 <_impure_ptr> 11 7b8: cc22 sw s0,24(sp) # 压栈,保存帧指针fp(s0) 12 7ba: 4380 lw s0,0(a5) 13 7bc: ca26 sw s1,20(sp) 14 7be: ce06 sw ra,28(sp) # 压栈,保存返回地址(ra寄存器) 15 7c0: 84aa mv s1,a0 16 7c2: c409 beqz s0,7cc <iprintf+0x28> 17 7c4: 4c1c lw a5,24(s0) 18 7c6: e399 bnez a5,7cc <iprintf+0x28> 19 7c8: 8522 mv a0,s0 20 7ca: 2315 jal cee <__sinit> 21 7cc: 440c lw a1,8(s0) 22 7ce: 1054 addi a3,sp,36 23 7d0: 8626 mv a2,s1 24 7d2: 8522 mv a0,s0 25 7d4: c636 sw a3,12(sp) 26 7d6: 167000ef jal ra,113c <_vfiprintf_r> 27 7da: 40f2 lw ra,28(sp) # 出栈,恢复返回地址(ra寄存器) 28 7dc: 4462 lw s0,24(sp) # 出栈,恢复帧指针fp(s0) 29 7de: 44d2 lw s1,20(sp) # 出栈,按需恢复相应的寄存器 30 7e0: 6121 addi sp,sp,64 # 释放栈空间 31 7e2: 8082 ret # 函数返回,根据ra寄存器地址返回
5、malloc使用注意事项
CH32V103默认工程中,heap只有起始地址,没有结束地址约束,这样最终会导致malloc永远都不会返回NULL。
如果使用malloc时,需进行如下操作:
-
重写_sbrk函数,代码如下,放在工程任意位置,推荐放在debug.c 文件中。
_sbrk代码原型:https://github.com/riscv/riscv-newlib/blob/riscv-newlib-3.1.0/libgloss/libnosys/sbrk.c
1 void *_sbrk(ptrdiff_t incr) 2 { 3 extern char _end[]; 4 extern char _heap_end[]; 5 static char *curbrk = _end; 6 7 if ((curbrk + incr < _end) || (curbrk + incr > _heap_end)) 8 return NULL - 1; 9 10 curbrk += incr; 11 return curbrk - incr; 12 }
-
修改ld链接脚本,定义heap大小
-
默认RAM中除去data、bss、stack等剩余的都为heap空间
增加PROVIDE( _heap_end = . ); 定义,位置如下:
1 PROVIDE( _end = _ebss); 2 PROVIDE( end = . ); /* 定义heap起始位置 */ 3 4 .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : 5 { 6 PROVIDE( _heap_end = . ); /* 定义heap结束位置,默认到栈底结束 */ 7 8 . = ALIGN(4); 9 PROVIDE(_susrstack = . ); 10 /*ASSERT ((. > 0x20005000),"ERROR:No room left for the stack");*/ 11 . = . + __stack_size; 12 PROVIDE( _eusrstack = .); 13 } >RAM
-
指定heap大小的修改方式如下:
增加 PROVIDE( _heap_end = . + 0x400); 定义,位置如下:
1 PROVIDE( _end = _ebss); 2 PROVIDE( end = . ); /* 定义heap起始位置 */ 3 PROVIDE( _heap_end = . + 0x400); /* 定义heap结束位置,长度为1KB */ 4 5 .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : 6 { 7 . = ALIGN(4); 8 PROVIDE(_susrstack = . ); 9 /*ASSERT ((. > 0x20005000),"ERROR:No room left for the stack");*/ 10 . = . + __stack_size; 11 PROVIDE( _eusrstack = .); 12 } >RAM
-
参考:
https://github.com/riscv/riscv-gnu-toolchain/issues/571
https://github.com/lowRISC/ibex/issues/1415
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?