什么是标准C函数:以RISC-V架构下的C函数为例

目录

[0 相关内容](#0 相关内容)

[1 RISC-V架构下的标准C函数](#1 RISC-V架构下的标准C函数)

[2 标准C函数的函数头和函数尾](#2 标准C函数的函数头和函数尾)

1.为什么需要函数头和尾?

[2. 栈帧(Stack Frame):函数的私人工作区](#2. 栈帧(Stack Frame):函数的私人工作区)

[3. 函数头(Prologue):建立栈帧](#3. 函数头(Prologue):建立栈帧)

[4. RISC-V函数栈帧结构](#4. RISC-V函数栈帧结构)

[5. 函数尾(Epilogue):销毁栈帧](#5. 函数尾(Epilogue):销毁栈帧)

[6. 优化与 fp 的省略](#6. 优化与 fp 的省略)

总结


0 相关内容

【笔记】RISC-V架构:fp寄存器与backtrace栈回溯

【外部链接】RISC-V函数栈帧结构与backtrace

1 RISC-V架构下的标准C函数

在 RISC-V 架构中,一个"标准 C 函数"不仅仅是你用 C 语言写的代码块,它在被编译器翻译成汇编指令后,会严格遵循一套称为应用程序二进制接口(ABI,Application Binary Interface) 的规则。这套规则确保了不同函数之间可以正确、高效地协同工作。而函数头(Prologue)函数尾(Epilogue),就是编译器在每个函数的开头和结尾自动插入的、用于维护这些规则的指令序列。

2 标准C函数的函数头和函数尾

1.为什么需要函数头和尾?

当一个函数(称为被调用者,callee)被另一个函数(称为调用者,caller)调用时,会发生几件事情:

  • 控制权转移:CPU 需要跳转到被调用者的代码。

  • 数据共享:调用者可能需要传递参数给被调用者。

  • 资源使用:被调用者会使用寄存器来运算,但它不能随意破坏调用者正在使用的寄存器内容。

  • 正确返回:被调用者执行完毕后,必须把控制权准确地交还给调用者,并返回结果。

函数头和函数尾就是为了解决"资源使用"和"正确返回"这两个问题而生的。它们共同负责建立和销毁当前函数的栈帧(Stack Frame)

2. 栈帧(Stack Frame):函数的私人工作区

栈帧是当前函数在栈上独占的一块内存区域。它就像一个临时的"办公室",用来存放:

  • 返回地址:函数执行完后,应该回到哪里。

  • 调用者的帧指针:用于在函数返回后恢复调用者的栈帧。

  • 被调用者需要保存的寄存器 :当前函数打算使用的、但又不允许破坏的寄存器(在 RISC-V 中主要是 s0-s11spgptp 等)。

  • 局部变量:函数内部定义的变量。

  • 传递给其他函数的参数(当参数太多,寄存器放不下时)。

在 RISC-V 的 ABI 中,有两个寄存器对栈帧管理至关重要:

  • sp(栈指针) :始终指向栈帧的顶部(也就是当前可用栈空间的最低地址,因为栈是向下生长的)。它像一个移动的标记,随着函数内的压栈和出栈操作而变化。

  • fp(帧指针) :指向当前栈帧的底部 (也就是一个固定的参考点)。它通常指向刚进入函数时,保存了调用者 fp 的那个位置。有了 fp,即使在函数执行过程中 sp 上下移动,也可以通过 fp 加上固定的偏移量,稳定地访问局部变量和调用者的信息。

3. 函数头(Prologue):建立栈帧

当一个函数被调用时,它的第一条指令序列(函数头)就开始着手建立自己的"办公室"。通常包含以下步骤:

  1. 保存返回地址 :如果当前函数还需要调用其他函数,它必须把 ra(返回地址)保存到栈上,否则再次调用时 ra 会被覆盖。

  2. 保存调用者的帧指针 :将调用者的 fp 值压入栈中。这样,当本函数退出时,可以恢复调用者的 fp

  3. 设置自己的帧指针 :将当前的 sp 值复制到 fp。现在,fp 就指向了这个新栈帧的底部。

  4. 分配局部变量空间 :将 sp 向下移动(减去一个值),为局部变量和临时存储腾出空间。

一个 RISC-V 汇编的例子:

假设有一个简单的 C 函数:

复制代码
int add_and_double(int a, int b) {
    int c = a + b;
    return c * 2;
}

如果使用帧指针(编译选项 -fno-omit-frame-pointer),编译器可能生成类似如下的汇编,其中函数头就是标注的部分:

复制代码
add_and_double:
    # 函数头 (Prologue) 开始
    addi sp, sp, -16      # 1. 分配栈空间 (16字节)
    sw   ra, 12(sp)       # 2. 保存返回地址 (ra)
    sw   s0, 8(sp)        # 3. 保存调用者的帧指针 (s0)
    addi s0, sp, 16       # 4. 设置新的帧指针 (s0 指向旧栈顶)
    # 函数头结束

    # 函数体
    add  a0, a0, a1       # c = a + b (结果在 a0)
    addi t0, zero, 2      # t0 = 2
    mul  a0, a0, t0       # a0 = c * 2 (返回值)

    # 函数尾 (Epilogue) 开始
    lw   s0, 8(sp)        # 1. 恢复调用者的帧指针
    lw   ra, 12(sp)       # 2. 恢复返回地址
    addi sp, sp, 16       # 3. 释放栈空间
    ret                   # 4. 返回 (跳转到 ra)
    # 函数尾结束

4. RISC-V函数栈帧结构

地址从高到低扩展,fp 指向函数帧的首地址,sp 是栈指针,与函数帧无关。fp-8 指向返回地址(对应图中 Return Address ),fp-16 指向上一函数帧的首地址(对应图中 To Prev. Frame )

5. 函数尾(Epilogue):销毁栈帧

当函数执行完毕,准备返回时,函数尾负责清理现场,确保调用者能无缝衔接。它的操作与函数头完全对称:

  1. 释放局部变量空间 :将 sp 增加回刚进入函数时的值(通常使用 addi sp, sp, XX)。

  2. 恢复调用者的帧指针 :从栈上加载之前保存的调用者 fp 值到 fp 寄存器。

  3. 恢复返回地址 :从栈上加载之前保存的 ra 值到 ra 寄存器。

  4. 执行返回指令 :在 RISC-V 中通常是 ret(它是 jalr x0, x1, 0 的伪指令)。

6. 优化与 fp 的省略

你可能会想,既然有了 sp,为什么还需要 fp?因为 sp 在函数执行中会不断变化(比如在调用其他函数前要压栈参数),用它来访问局部变量会变得复杂和低效。fp 提供了一个稳定的锚点。

然而,在开启优化(如 -O1 或更高)时,编译器可以追踪 sp 的变化,仍然可以通过 sp 加上动态计算的偏移量来访问局部变量。如果同时使用 -fomit-frame-pointer 选项,编译器就会省略掉帧指针 。此时,函数头中就不再保存和设置 fp,而是直接使用 sp 来管理栈帧。这会节省一点栈空间(少保存一个寄存器)和一个寄存器(fp/s0 可以被用作普通变量寄存器),但会让调试时的栈回溯变得更复杂一些(因为没有了稳定的帧指针链)。

总结

  • 函数头(Prologue) :一个标准 C 函数的"开场白",由编译器自动生成,负责建立栈帧,保存返回地址和调用者寄存器,为局部变量腾出空间。

  • 函数尾(Epilogue) :这个函数的"结束语",同样由编译器生成,负责销毁栈帧,恢复之前保存的寄存器和返回地址,然后交还控制权。

  • 核心目的 :确保函数调用的嵌套递归可以正确进行,同时保证每个函数的局部数据不会互相干扰,并且调用者的状态在函数返回后能完美恢复。

正是因为有这样一套严谨的、由编译器和 ABI 共同保障的机制,我们才能用 C 语言安全地编写出无限嵌套的函数调用。而【【笔记】RISC-V架构:fp寄存器与backtrace栈回溯】中讨论的 FreeRTOS 异常入口(手写汇编)之所以不需要函数头和尾,是因为它不遵循标准的函数调用约定,它直接操作硬件状态,目标是保存整个任务上下文,而不是作为一个普通函数被调用。

相关推荐
三品吉他手会点灯3 小时前
C语言学习笔记 - 34.数据类型 - 编程规范与高效学习方法
c语言·开发语言·笔记·学习
Lucky_ldy3 小时前
C语言学习:动态内存管理(数据结构关键)
c语言·数据结构·学习
JackSparrow4144 小时前
彻底理解Java NIO(二)C语言实现 I/O多路复用+Reactor模式 服务器详解
java·linux·c语言·后端·nio·reactor模式
三品吉他手会点灯4 小时前
C语言学习笔记 - 37.数据类型 - scanf函数的基本用法
c语言·开发语言·笔记·学习
草莓熊Lotso4 小时前
【Linux系统加餐】从原理到实战:System V消息队列全解析 + 基于责任链模式的工业级封装
linux·运维·服务器·c语言·c++·人工智能·责任链模式
Aurorar0rua12 小时前
CS50 x 2024 Notes C -14
c语言·开发语言·学习方法
鱼很腾apoc15 小时前
【学习篇】第20期 超详解 C++ 多态:从语法规则到底层原理
java·c语言·开发语言·c++·学习·算法·青少年编程
不吃土豆的马铃薯16 小时前
4.SGI STL 二级空间配置器 allocate 与_S_refill 源码解析
c语言·开发语言·c++·dreamweaver·内存池
水饺编程19 小时前
第5章,[Win32 章节] :几种典型的颜色
c语言·c++·windows·visual studio