Table of contents
Open Table of contents
关于函数栈帧 StackFrame
在 rCore 文档中对函数栈帧的解读中有两处不好理解,ra
和 fp
.
首先可以先考虑一个进程的内存,应该有内核栈、用户栈、代码库、用户堆、数据和代码等等组成(参考《现代操作系统:原理与实现》)。如果问AI这个问题或者是网络上的blog,会说由代码段、数据段、BSS段、堆和栈组成。
内存区域 | 存储内容 | 权限 | 动态性 | |
---|---|---|---|---|
代码段 | 可执行指令(.text 段) | 只读+执行 | 静态 | |
数据段 | 已初始化的全局/静态变量(.data ) | 可读+写 | 静态 | |
BSS 段 | 未初始化的全局/静态变量(.bss ) | 可读+写 | 静态(清零) | |
堆(Heap) | 动态分配的内存(malloc 等) | 可读+写 | 动态增长 | |
栈(Stack) | 函数调用栈帧、局部变量 | 可读+写 | 动态增长 |
函数的栈帧保存在栈区域中,执行的代码保存在代码段中。
关于一个栈帧包含什么,可以见函数调用过程中的栈帧与内存管理一篇。一个栈从高地址向低地址延申,fp
表示栈底所在的位置(高地址,Frame Pointer),sp
表示栈顶(低地址,Stack Pointer)。在32位的x86架构上这两个值分别用 ebp
和 esp
表示,64位上是 rbp
和 rsp
.
不妨使用一个简单的代码来探究。根据函数调用的内存对齐规则,故有sub rsp, 8
一句。
操作 | 对 rsp 的影响 | 对齐状态 |
---|---|---|
call main | rsp -= 8 (压入返回地址) | 未对齐(rsp%16=8 ) |
sub rsp, 8 | rsp -= 8 (主动调整) | 对齐(rsp%16=0 ) |
call printf | rsp -= 8 (压入返回地址) | 未对齐(rsp%16=8 ) |
add rsp, 8 | rsp += 8 (恢复调整) | 对齐(rsp%16=0 ) |
li a1, -16
的作用是 将立即数 -16
加载到寄存器 a1
中,其他的指令可以参考运算与控制流指令和When to use j, jal, Jr, jalr?.
注意RISC-V是小端序, 高位是 %hi()
,低位 %lo()
.
说回 ra
和 fp
的作用。ra
字面意思是返回地址,指的是当前函数执行完毕后应返回的地址,在函数返回前要从栈中加载 ra
,然后通过 ret/jr
跳转回调用者。也就是说 ra
是之前被调用处的一个中断的地址位置。fp
(preview fp
) 是父函数栈帧的帧指针,也就是要恢复的父函数栈帧基址。
ra
主要指的是函数调用,fp
是栈帧。
假设函数 A
调用函数 B
:
-
进入
B
时:-
B
的栈帧保存A
的ra
(返回地址)和A
的fp
(父帧指针)。 -
B
的fp
被设置为当前栈帧的基址(如sp + offset
)。
-
-
退出
B
时:-
从栈中恢复
A
的ra
到ra
寄存器,恢复A
的fp
到fp
寄存器。 -
调整
sp
释放B
的栈空间,通过ret
跳转回A
继续执行。
-