77、【OS】【Nuttx】【启动】caller-saved 和 callee-saved 示例:栈指针和帧指针(上)
【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
接之前 blog
【OS】【Nuttx】【启动】caller-saved 和 callee-saved 示例
【OS】【Nuttx】【启动】caller-saved 和 callee-saved 示例:叶子函数
分析了示例中体现的 caller-saved 和 callee-saved 规则,以及里面的叶子函数概念,下面来看下栈空间操作的一些具体细节
栈空间操作
还是回到之前那个示例
// main.c
static int add_func(int a, int b) {return (a + b);
}int main(void) {int c = add_func(1, 2);return 0;
}
汇编代码如下
.cpu cortex-m4.arch armv7e-m.fpu softvfp.eabi_attribute 20, 1.eabi_attribute 21, 1.eabi_attribute 23, 3.eabi_attribute 24, 1.eabi_attribute 25, 1.eabi_attribute 26, 1.eabi_attribute 30, 6.eabi_attribute 34, 1.eabi_attribute 18, 4.file "main.c".text.align 1.syntax unified.thumb.thumb_func.type add_func, %function
add_func:@ args = 0, pretend = 0, frame = 8@ frame_needed = 1, uses_anonymous_args = 0@ link register save eliminated.push {r7}sub sp, sp, #12add r7, sp, #0str r0, [r7, #4]str r1, [r7]ldr r2, [r7, #4]ldr r3, [r7]add r3, r3, r2mov r0, r3adds r7, r7, #12mov sp, r7@ sp neededpop {r7}bx lr.size add_func, .-add_func.align 1.global main.syntax unified.thumb.thumb_func.type main, %function
main:@ args = 0, pretend = 0, frame = 8@ frame_needed = 1, uses_anonymous_args = 0push {r7, lr}sub sp, sp, #8add r7, sp, #0movs r1, #2movs r0, #1bl add_funcstr r0, [r7, #4]movs r3, #0mov r0, r3adds r7, r7, #8mov sp, r7@ sp neededpop {r7, pc}.size main, .-main.ident "GCC: (15:13.2.rel1-2) 13.2.1 20231009"
下面来看下栈空间操作的几个细节,首先是 main 函数这里的栈空间行为
LR 寄存器压栈
这个之前 blog 【OS】【Nuttx】【启动】caller-saved 和 callee-saved 示例:叶子函数 应该说得很详细了,因为调用了子函数,执行了跳转命令
...
bl add_func
str r0, [r7, #4]
...
- 这个指令会把返回地址(即 bl 下一条指令的地址,也就是 str r0, [r7, #4] 命令)写入 lr
- 如果不保存原来的 lr 值,那么之前在 lr 中保存的内容就会被覆盖。
- 为了能正确地从 main 返回到它的调用者(比如启动代码 _start 或操作系统),必须先把 lr 压栈保存
栈指针和帧指针
栈指针(Stack Pointer)和帧指针FP(Frame Pointer)是栈空间操作中两个非常重要的概念,它们各自承担不同的角色,
栈指针 SP
栈好理解,就是一个后进先出的数据结构,栈空间可以用来存储函数参数、返回地址、局部变量以及临时数据等,下面来看下官方文档 AAPCS 中,对栈的描述
这里有比较多的细节:
- 栈是一块连续的内存区域,可以存储函数中的局部变量
- 当调用函数所需的参数数量超过可用寄存器个数时,多出来的参数通过栈来传递(之前讲过的)
- full-descending:full 意味着 SP 指向最后一个有效的栈元素,即高地址栈顶,descending 表示栈向低地址方向增长,SP 会变小
- 栈在内存中有起始地址(高地址)和终止地址(低地址),但用户程序不需要知道这些地址是多少,这些地址信息一般由操作系统维护(比如 nuttx)
- 栈可以是固定的,也可以动态扩展
- 固定大小栈常见于嵌入式实时系统(比如 nuttx),预先分配好一定大小的空间
- 动态扩展栈常见于复杂操作系统(比如 linux,windows),当栈快用完时,系统可以自动扩大栈空间
帧指针 FP
帧指针是函数调用时分配的栈空间基址,为了方便访问局部变量和参数,编译器通常会将 sp 的当前值保存到一个通用寄存器中,比如 r7,然后使用 r7 作为栈空间基址来访问栈上的数据,下面来看下官方文档描述
这里也有比较多的细节:
- 调试器或操作系统在进行进行 backtrace(堆栈回溯)或 异常处理 时,需要知道每个函数调用是如何嵌套的,这就需要构建一条栈帧链,通俗点也叫调用链
- 每个函数调用生成的栈帧中,都会在栈上保留一个帧记录,这个记录有两个字段,第一个字段(低地址)指向前一个函数调用的帧指针,第二个字段(高地址)保存当前函数被调用时 LR 寄存器的原始值(即返回地址),其实就是之前 blog 看到的这个操作
- 当某个帧记录的前一个帧指针地址为 0,说明这已经是最外层的函数,后面没有调用者了,比如 main 或启动代码
- 编译器可以自由决定把帧记录放在栈帧的哪个位置(一般进入调用函数后先保存下帧记录)
- 调用函数时,在新的帧记录完全构造完成之前,不能更新帧指针 FP,确保在中断或异常发生时,堆栈状态是完整的,不会出现部分构造的帧记录,导致回溯失败
先分析到这里,下篇再分析