惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
T
Threatpost
WordPress大学
WordPress大学
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
PCI Perspectives
PCI Perspectives
T
The Exploit Database - CXSecurity.com
Y
Y Combinator Blog
雷峰网
雷峰网
爱范儿
爱范儿
The Hacker News
The Hacker News
Last Week in AI
Last Week in AI
Simon Willison's Weblog
Simon Willison's Weblog
T
Tor Project blog
S
Securelist
宝玉的分享
宝玉的分享
L
LangChain Blog
O
OpenAI News
AI
AI
P
Privacy International News Feed
L
LINUX DO - 最新话题
D
DataBreaches.Net
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Attack and Defense Labs
Attack and Defense Labs
罗磊的独立博客
M
MIT News - Artificial intelligence
Security Archives - TechRepublic
Security Archives - TechRepublic
月光博客
月光博客
博客园 - 【当耐特】
T
Tailwind CSS Blog
C
Cybersecurity and Infrastructure Security Agency CISA
H
Help Net Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园_首页
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Hacker News - Newest:
Hacker News - Newest: "LLM"
腾讯CDC
Jina AI
Jina AI
The Last Watchdog
The Last Watchdog
K
Kaspersky official blog
Webroot Blog
Webroot Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Blog — PlanetScale
Blog — PlanetScale
MyScale Blog
MyScale Blog
MongoDB | Blog
MongoDB | Blog
P
Proofpoint News Feed
Recorded Future
Recorded Future
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 三生石上(FineUI控件)
The Cloudflare Blog

Leo’s blog

[译] Understanding Incremental Decoding in fairseq 将化学分子的 ChEMBL ID 转化为 SMILES 的两种方法 通过 PDM 和 GitHub Actions 在 PyPI 上自动化发布你的 Python 包吧 华为云踩坑:由 URL 编码导致 yum 安装时的 No such file or directory 错误 为友治藏书印一方 忙忙碌碌中匆匆过眼我的 2023 年 Cloudflare + Backblaze 实现免费的博客图床方案 新服务器必做的基本设置——服务器迁移之记录 把博客站点交给了 Cloudflare 托管 RIME 脚本食用方法举隅:以输入苏州码为例 通过 SSH 在 Pycharm 上使用 Docker 容器中的 Python 解释器 如何在 X86 设备上使用 Docker 构建 ARM 镜像 文献总结|结构诱导的预训练 文献总结|MTGL-ADMET:一种通过地位理论与最大流增强并用于 ADMET 预测的多任务图学习框架 文献总结|探测图表示 四月十二奉新纸一试 文献总结|可以同时完成分子语言序列回归和生成的 Regression Transformer 旧书市场淘书记 文献总结|一种用于基于结构药物设计的 3D 生成模型
RISC-V 函数调用约定
Leo · 2024-12-01 · via Leo’s blog

调用函数前后存在旧数据与新数据两种状态,RISC-V 对使用哪些寄存器存储前后状态做了人为规定,这样的一系列规定称为调用约定(Calling Convention)。CS 61C 的补充资料在这方面描述得最为明了,本文主要根据这篇文章对 RISC-V 调用约定的要点做了总结。

基本定义

首先将发起调用的函数称为调用者(caller),将被调用的函数称为被调用者(callee)。注意,一个函数是调用者或被调用者是由其行为决定,当它被其他函数调用时是被调用者,当它调用其他函数时是调用者,两个身份可以先后存在。

其次 RISC-V 约定在一部分寄存器中的内容在调用函数后不会被改变,称为由被调用者保存的寄存器(callee-saved registers),包括 s0 - s11(保存寄存器,saved registers)和 sp。调用函数可能更改另一部分寄存器中的内容,这些寄存器称为由调用者保存的寄存器(caller-saved registers ),包括 a0 - a7(参数寄存器,argument registers)、t0 - t6(临时寄存器,temporary registers)和 ra(返回地址,return address)。

寄存器的功能和约定可以总结如下表:

编号 寄存器 ABI 名称 描述 保存方
0 x0 zero 常数 0 -
1 x1 ra 返回地址 caller
2 x2 sp 栈指针 callee
3 x3 gp 全局指针 -
4 x4 tp 线程指针 -
5 ~ 7 x6 ~ x7 t0 ~ t2 临时 caller
8 x8 s0 / fp 保存 / 帧指针 callee
9 x9 s1 保存 callee
10 ~ 11 x10 ~ x11 a0 ~ a1 函数参数 / 返回值 caller
12 ~ 17 x12 ~ x17 a2 ~ a7 函数参数 caller
18 ~ 27 x18 ~ x27 s2 ~ s11 保存 callee
28 ~ 31 x28 ~ x31 t3 ~ t6 临时 caller

实际案例

从抽象的概念来看比较难以理解,接下来将以上概念代入到几个具体的案例中理解 RISC-V 的调用约定。

调用者的视角

当我们调用一个函数时,被调用的函数对由其保存的寄存器负责,也就是说由被调用者保存的寄存器内容在该函数调用前后不变。但「不变」并不是指该函数无法使用这些寄存器,实际上函数可以使用任何一个寄存器,RISC-V 中的寄存器并没有「使用权限」的概念,只是在函数结束前必须将修改值恢复原状——这个过程我形象地称其为对寄存器中的值负责(preserve)。

将以上过程抽象为黑盒,从调用者的视角来看,当然可以认为被调用的函数不会修改由被调用者保存寄存器中的值。

上述过程可以通过以下代码理解:

addi s0, x0, 5     # 寄存器 s0 的值为 5
jal ra, func       # 调用 func
addi s0, s0, 0     # 不论 func 是什么,s0 的值还是 5

从反面来思考,就会意识到被调用的函数不对由调用者保存的寄存器负责,也就是说在该函数结束后,由调用者保存寄存器中的值是不可靠的垃圾值:

addi t0, x0, 5     # 寄存器 s0 的值为 5
jal ra, func       # 调用 func
addi t0, t0, 0     # t0 中的值是垃圾值!

 Warning 在调用函数后,不由被调用者负责寄存器中的值是否发生改变,实际取决于函数的实现,但作为负责编码的工程师,理应将这些寄存器中的值都视为垃圾,不应当依赖垃圾值执行程序。

规避垃圾值问题的技巧是,在寄存器中的值变得不可靠前,预先将其中值保存下来:

addi t0, x0, 5     # t0 中的值为 5
addi a0, t0, 10    # a0 中的值为 15,a0 是函数参数

# 调用函数前,调用者需要做的事
addi sp, sp, -8    # 栈指针向下移动
sw t0, 0(sp)       # 将 t0 的值压入栈帧
sw a0, 4(sp)       # 将 a0 的值压入栈帧

jal ra, func       # 调用函数 func
mv s0, a0          # 将函数返回值 a0 中的值存入 s0
mv s1, a1          # 将函数返回值 a1 中的值存入 s1

# 调用函数后,调用者需要做的事
lw t0, 0(sp)       # 从栈帧中弹出原先 t0 的值,并把值写入 t0
lw a0, 4(sp)       # 从栈帧中弹出原先 a0 的值,并把值写入 a0
addi sp, sp, 8     # 栈指针向上移动

# 目前 t0 与 a0 的值都是可靠的,因为它们的值是预先存入栈帧并从中还原的

从上面的代码中可以观察出 2 点,也是调用者视角下的调用约定:

  1. 调用者函数内,在调用函数前后,必须通过栈内存手动维护由调用者保存的寄存器前后一致(如 a0t0);
  2. 调用者函数内,可以任意修改由被调用者保存寄存器而不用担心副作用(如 s0s1)。

被调用者视角

理解调用者视角下的调用约定后,就不难理解被调用者视角下的操作了。直接观察下例:

# 函数正式操作前,被调用者需要做的事
addi sp, sp, -12   # 栈指针向下移动
sw ra, 0(sp)       # 将 ra 的值压入栈帧
sw s0, 4(sp)       # 将 s0 的值压入栈帧
sw s1, 8(sp)       # 将 s1 的值压入栈帧

# 函数正式操作

# 函数正式操作后,被调用者需要做的事
lw ra, 0(sp)       # 从栈帧中弹出原先 ra 的值,并把值写入 ra
lw s0, 4(sp)       # 从栈帧中弹出原先 s0 的值,并把值写入 s0
lw s1, 8(sp)       # 从栈帧中弹出原先 s1 的值,并把值写入 s1
addi sp, sp, 12    # 栈指针向上移动

ret                # 从函数中返回

可以得出类似的 2 点调用约定:

  1. 被调用者函数内,在函数正式操作前后,必须通过栈内存手动维护由被调用者保存的寄存器前后一致(如 ras0s1);
  2. 被调用者函数内,可以任意修改由调用者保存寄存器而不用担心副作用。

总而言之,RISC-V 的调用约定中规定了寄存器的保存方(saver),函数(caller / callee)对其相应寄存器中的内容负责,caller 维护 caller-saved 寄存器,callee 维护 callee-saved 寄存器。因为需要维护寄存器内容,在函数正式操作前都要把需要维护的内容存入栈内存,并在函数操作结束后从中还原。


References