RISC-V MCU堆棧機制
1、什么是堆棧?
在嵌入式的世界里,堆棧通常指的是棧,嚴格來說,堆棧分為堆(Heap)和棧(Stack)。
- 棧(Stack): 一種順序數據結構,滿足后進先出(Last-In / First-Out)的原則,由編譯器自動分配和釋放。使用一級緩存,調用完立即釋放。
- 堆(Heap):類似於鏈表結構,可對任意位置進行操作,通常由程序員手動分配,使用完需及時釋放(free),不然容易造成內存泄漏。使用二級緩存。
2、堆棧的作用
- 函數調用時,如果函數參數和局部變量很多,寄存器放不下,需要開辟棧空間存儲。
- 中斷發生時,棧空間用於存放當前執行程序的現場數據(下一條指令地址、各種緩存數據),以便中斷結束后恢復現場。
3、堆棧大小定義
RISC-V MCU的堆棧大小通常在ld鏈接腳本中定義,關於ld鏈接腳本可查看該文:RISC-V MCU ld鏈接腳本說明。
ENTRY( _start ) /* 入口地址 */
__stack_size = 2048; /* 定義棧大小 */
PROVIDE( _stack_size = __stack_size );/* 定義_stack_size符號,類似於全局變量 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000 , LENGTH = 0x10000
RAM (xrw) : ORIGIN = 0x20000000 , LENGTH = 0x5000
}
/*
...
中間省略
...
*/
.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : /* 分配棧空間0x20004800 ~ 0x20005000,共2KB */
{
. = ALIGN(4);
PROVIDE(_susrstack = . );
. = . + __stack_size;
PROVIDE( _eusrstack = .);
} >RAM
以RISC-V MCU CH32V103為例,在其ld鏈接腳本中,定義了_stack_size符號,值為 2048 Byte,后面使用該值在.stack段中分配棧空間,可更改此值調整棧空間大小。
CH32V103 的RAM共20KB,除去程序用到的data、bss段,剩下空間即為動態數據段,供堆棧的動態使用。
ld鏈接腳本中,沒有明確定義heap堆的大小,按照其定義,動態數據段,除了stack占用的,剩下的都可用於heap,通過malloc進行動態管理。
4、壓棧出棧過程
以CH32V103 printf函數調用為例,其反匯編代碼如下:
000007a4 <iprintf>:
7a4: 7139 addi sp,sp,-64 # 調整堆棧指針sp,分配64字節的棧空間
7a6: da3e sw a5,52(sp) # 壓棧,保存a5寄存器的值
7a8: d22e sw a1,36(sp) # 壓棧,按需保存相應的寄存器
7aa: d432 sw a2,40(sp)
7ac: d636 sw a3,44(sp)
7ae: d83a sw a4,48(sp)
7b0: dc42 sw a6,56(sp)
7b2: de46 sw a7,60(sp)
7b4: 80818793 addi a5,gp,-2040 # 20000078 <_impure_ptr>
7b8: cc22 sw s0,24(sp) # 壓棧,保存幀指針fp(s0)
7ba: 4380 lw s0,0(a5)
7bc: ca26 sw s1,20(sp)
7be: ce06 sw ra,28(sp) # 壓棧,保存返回地址(ra寄存器)
7c0: 84aa mv s1,a0
7c2: c409 beqz s0,7cc <iprintf+0x28>
7c4: 4c1c lw a5,24(s0)
7c6: e399 bnez a5,7cc <iprintf+0x28>
7c8: 8522 mv a0,s0
7ca: 2315 jal cee <__sinit>
7cc: 440c lw a1,8(s0)
7ce: 1054 addi a3,sp,36
7d0: 8626 mv a2,s1
7d2: 8522 mv a0,s0
7d4: c636 sw a3,12(sp)
7d6: 167000ef jal ra,113c <_vfiprintf_r>
7da: 40f2 lw ra,28(sp) # 出棧,恢復返回地址(ra寄存器)
7dc: 4462 lw s0,24(sp) # 出棧,恢復幀指針fp(s0)
7de: 44d2 lw s1,20(sp) # 出棧,按需恢復相應的寄存器
7e0: 6121 addi sp,sp,64 # 釋放棧空間
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
void *_sbrk(ptrdiff_t incr) { extern char _end[]; extern char _heap_end[]; static char *curbrk = _end; if ((curbrk + incr < _end) || (curbrk + incr > _heap_end)) return NULL - 1; curbrk += incr; return curbrk - incr; }
-
修改ld鏈接腳本,定義heap大小
-
默認RAM中除去data、bss、stack等剩余的都為heap空間
增加PROVIDE( _heap_end = . ); 定義,位置如下:
PROVIDE( _end = _ebss); PROVIDE( end = . ); /* 定義heap起始位置 */ .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : { PROVIDE( _heap_end = . ); /* 定義heap結束位置,默認到棧底結束 */ . = ALIGN(4); PROVIDE(_susrstack = . ); /*ASSERT ((. > 0x20005000),"ERROR:No room left for the stack");*/ . = . + __stack_size; PROVIDE( _eusrstack = .); } >RAM
-
指定heap大小的修改方式如下:
增加 PROVIDE( _heap_end = . + 0x400); 定義,位置如下:
PROVIDE( _end = _ebss); PROVIDE( end = . ); /* 定義heap起始位置 */ PROVIDE( _heap_end = . + 0x400); /* 定義heap結束位置,長度為1KB */ .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : { . = ALIGN(4); PROVIDE(_susrstack = . ); /*ASSERT ((. > 0x20005000),"ERROR:No room left for the stack");*/ . = . + __stack_size; PROVIDE( _eusrstack = .); } >RAM
-
參考:
https://github.com/riscv/riscv-gnu-toolchain/issues/571
https://github.com/lowRISC/ibex/issues/1415