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继续执行。
-