RISC-V Bytes: Caller and Callee Saved Registers

原文链接1:https://danielmangum.com/posts/risc-v-bytes-caller-callee-registers/
原文链接2:https://zhuanlan.zhihu.com/p/77663680 //主要讲栈帧
原文链接3:https://www.jianshu.com/p/b666213cdd8a //主要讲栈帧

This is part of a new series I am starting on the blog where we’ll explore RISC-V by breaking down real programs and explaining how they work. You can view all posts in this series on the RISC-V Bytes page.

When looking at the generated assembly for a function, you may have noticed that the first few instructions involve moving values from registers to the stack, then loading those values back into the same registers before returning. In this post we’ll explore why this is happening, why certain registers are used, and how behavior guarantees make life easier for compiler authors and enable software portability.

Defining Terms

Before we dive into what is happening there, let’s define some terms and take a look at the 32 general purpose registers supported in the RISC-V instruction set.

  • Caller: a procedure that calls one or more more subsequent procedure(s).
  • Callee: a procedure that is called by another.
  • Application Binary Interface (ABI): a standard for register usage and memory layout that allows for programs that are not compiled together to interact effectively.
  • Calling Conventions: a subset of an ABI specifically focused on how data is passed from one procedure to another.

Importantly, a procedure may be both a caller and a callee.

Now let’s take a look at the RISC-V registers:
在这里插入图片描述
You’ll notice the second column refers to the Application Binary Interface (ABI) and the third refers to the Calling Convention, both of which we defined earlier. This likely makes intuitive sense: if we all agree to use certain registers for specific purposes, we can expect data to be there without having to explicitly say that it is.

The fourth column may be a bit more opaque. While this table uses Preserved across calls? as a designation, you will frequently see all of the registers with Yes in the column referred to as callee-saved and those with No as caller-saved. This once again is related to how procedures communicate. It is great to agree on the purpose of our registers, but we also need to define what responsibilites a procedure has when interacting with them. In order for a register to be preserved across calls, the callee must make sure its value is the same when it returns to the caller as it was when the callee was, well, called!

An Example

The simplest example is the main function. You may be tempted to think that main would be an example of a procedure that is only a caller. In reality, it is called after some initial setup, which can very greatly depending on the language and the compiler. Almost every procedure is a callee, and only leaf procedures are not callers.

We’ll be using our program from last post to show how registers are preserved. In this case, main is being called by _start and it calls printf.

(gdb) disass main
Dump of assembler code for function main:
   0x0000000000010158 <+0>:     addi       sp,sp,-32
   0x000000000001015a <+2>:     sd         ra,24(sp)
   0x000000000001015c <+4>:     sd         s0,16(sp)
   0x000000000001015e <+6>:     addi       s0,sp,32
   0x0000000000010160 <+8>:     li         a5,1
   0x0000000000010162 <+10>:    sw         a5,-20(s0)
   0x0000000000010166 <+14>:    li         a5,2
   0x0000000000010168 <+16>:    sw         a5,-24(s0)
   0x000000000001016c <+20>:    lw         a4,-20(s0)
   0x0000000000010170 <+24>:    lw         a5,-24(s0)
   0x0000000000010174 <+28>:    addw       a5,a5,a4
   0x0000000000010176 <+30>:    sw         a5,-28(s0)
   0x000000000001017a <+34>:    lw         a5,-28(s0)
   0x000000000001017e <+38>:    mv         a1,a5
   0x0000000000010180 <+40>:    lui        a5,0x1c
   0x0000000000010182 <+42>:    addi       a0,a5,176 # 0x1c0b0
   0x0000000000010186 <+46>:    jal        ra,0x10332 <printf>
   0x000000000001018a <+50>:    li         a5,0
   0x000000000001018c <+52>:    mv         a0,a5
   0x000000000001018e <+54>:    ld         ra,24(sp)
   0x0000000000010190 <+56>:    ld         s0,16(sp)
   0x0000000000010192 <+58>:    addi       sp,sp,32
   0x0000000000010194 <+60>:    ret
End of assembler dump.

You may be thinking to yourself: why do we need so many instructions that just store a register into memory, then immediately load it back? Good question! We don’t! For simplicity here, we are compiling using gcc without any optimization. This essentially means that each source line is assembled in a vacuum without much consideration of the surrounding context. While this is inefficient and leads to a much larger program size, it can be useful for learning. Take a look at this program on Compiler Explorer and hover over the output to see which instructions map to each source line. We’ll explore how different optimization levels change code generation in a future post.

Let’s start from the top. The first thing you’ll notice is that we are decreasing the value in sp, our stack pointer register. Our first four instructions here are commonly referred to as the function prologue. For today’s post we are going to be primarily focusing on it and the function epilogue because these sections are where we perform the bookkeeping operations that are necessary to conform to calling conventions.

When we move the stack pointer, we are essentially incrementing or decrementing the size of our stack. In RISC-V, the stack grows downwards, so addi sp, sp, -32 is increasing the size of our downward growing stack by changing the stack pointer to contain an address 32 bytes lower.

A Caller-Saved Register

Next we want to store the contents of the saved registers onto the stack. Let’s pause for a moment and think about why we need to do this. If the registers are designated as “saved”, can we not just leave them untouched throughout the body of our procedure, keeping them intact when we return to the procedure that called us?

This is true if we are not going to re-use those registers at any point in our procedure we need to make sure we preserve their contents. For instance, take a look at <+42> where we call printf. Here we are specifying that we want to jump to the location of the printf procedure and set the contents of register ra to the address of the program counter plus four (ra <- PC + 4). This will inform printf to return to the address of the next instruction in our main body (<+50>). However, when printf does return, we need to know how to return to the procedure that called us (_start).

If we hadn’t saved the contents of ra in the prologue (<+2>), we would have lost that address, but because we stored it on the stack, we can load it back into ra in the epilogue (<+54>) and return to _start. Meanwhile, in the rest of the procedure body, we are free to use the register as needed. If we look at our table of general purpose registers above, we’ll notice that ra is designated as caller-saved (i.e. it is not preserved across calls). This aligns with the behavior we see as main, as the caller, saves ra before calling printf and updating ra with the address of the next instruction.

A Callee-Saved Register

You’ll also notice that we are storing s0 on the stack in the prologue (<+4>). Besides being designated as a callee-saved register, s0 is used as the frame pointer if one exists. The stack frame is the area of the stack reserved for the current procedure and it stretches from the frame pointer to the stack pointer. Procedures may use the frame pointer with an offset to store values on the stack, such as a variable that is only in-scope for that procedure (e.g. <+10>). In this way, the frame pointer is a boundary, marking the beginning of the region of the stack available for the procedure.

It is imperative that the frame pointer, or any other callee-saved register that is modified in the procedure, is restored prior to returning to the caller. Since _start is expecting its frame pointer to be unmodified after calling main, we must:

  1. Store it in the stack frame for main (<+4>).
  2. Set the new frame pointer for main (<+6>). You’ll notice the frame pointer now contains the address the stack pointer contained when our procedure began.
  3. Restore it before returning (<+56>).

You’ll notice that we will also restore the stack pointer (<+58>), as it is a callee-saved register as well. However, unlike ra, we don’t have to worry about storing the contents of s0 or sp on the stack prior to calling printf because it will adhere to the same conventions as a callee that main does for _start, ensuring that all of our callee-saved registers are unmodified when it returns.

Concluding Thoughts

While we have only scratched the surface of the benefits of ABI-compatibility in this post, we can already begin to see its value. In future posts, we’ll take a look at how a standardized ABI is even more important when depending on shared libraries, as well as examine some more complex examples of passing data between procedures. As always, these post are meant to serve as a useful resource for folks who are interested in learning more about RISC-V and low-level software in general. If I can do a better job of reaching that goal, or you have any questions or comments, please feel free to send me a message @hasheddan on Twitter!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/309953.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024年中级工程师职称业绩报告该怎么写呢?

1、在写报告时一定要注意时间问题&#xff0c;需要与项目实际时间一致&#xff0c;要把自己的工作经历写清楚&#xff0c;在项目里主要负责什么内容&#xff0c;担任什么职务。 2、可以写发现了什么问题&#xff0c;并如何去解决的&#xff0c;或者因为你发现和创新给项目带来的…

Mermaid 教程

Mermaid 教程 Mermaid 介绍 Mermaid 是一个用于生成流程图、时序图、甘特图等图表的 JavaScript 库。它使用类似于 Markdown 的文本语法&#xff0c;使得创建图表变得简单直观。以下是一个简单的 Mermaid 教程&#xff0c;介绍如何使用 Mermaid 创建流程图、时序图和甘特图。…

docker启动mongo

用户名&#xff1a;root 密码&#xff1a;123456 version: 3.1 services:mongo:image: mongo:7container_name: mongorestart: alwaysports:- 27017:27017volumes:- /opt/data/mongo:/data/dbenvironment:TZ: Asia/ShanghaiMONGO_INITDB_ROOT_USERNAME: rootMONGO_INITDB_ROO…

数字孪生+可视化技术 构建智慧新能源汽车充电站监管平台

前言 充电基础设施为电动汽车提供充换电服务&#xff0c;是重要的交通能源融合类基础设施。近年来&#xff0c;随着新能源汽车产业快速发展&#xff0c;我国充电基础设施持续增长&#xff0c;已建成世界上数量最多、服务范围最广、品种类型最全的充电基础设施体系。着眼未来新…

《C++ Primer》第14章 重载运算与类型转换(二)

参考资料&#xff1a; 《C Primer》第5版《C Primer 习题集》第5版 14.8 函数调用运算符&#xff08;P506&#xff09; 如果类重载了函数调用运算符&#xff0c;则我们可以像使用函数一样使用该类的对象。这样的类同时也能存储状态&#xff0c;所以它们比普通函数更加灵活。…

Android可换行的RadioGroup

Android可换行的RadioGroup,有时候需要换行显示的单选列表&#xff0c;当然可以有多种实现方式&#xff0c;比如recycleview或者listview实现&#xff0c;本文采用的是RadioGrouprediobutton方式实现。 一、首先自定义view public class WrapRadioGroup extends RadioGroup {pr…

【XR806开发板试用】+ FreeRtos开发环境搭建

获取SDK SDK可以通过官网直接下载。 下载完成之后&#xff0c;通过gzip命令解压文件 gzip -d xr806_sdk.tar.gz 获取编译链工具 还是按照官网操作指南&#xff0c;下载 gcc-arm-none-eabi-8-2019-q3-update 下载之后进行解压&#xff0c;同理。 注意修改GCC路径&#xff0c…

三、C语言分支与循环知识点补充——随机数生成

本章分支结构的学习内容如下&#xff1a; 三、C语言中的分支与循环—if语句 (1) 三、C语言中的分支与循环—关系操作符 (2) 三、C语言中的分支与循环—条件操作符 与逻辑操作符(3) 三、C语言中的分支与循环—switch语句&#xff08;4&#xff09;分支结构 完 本章循环结构的…

直播预告丨看零售场,如何玩转 MaaS

今年&#xff0c;有一个被频繁提及的词是MaaS 这类工具正在帮助千行百业实现大模型落地产业 在零售场&#xff0c;特别是像京东这样拥有超高并发、超复杂协同的电商场内 也沉淀出了一套通用的AI基础设施——九数算法中台 从提升客户服务体验、平台效率出发&#xff0c;训练各…

AtCoder ABC194

这期比193稍微简单一点 C - Squared Error 手玩一下&#xff1a; N 3 N3 N3时 展开得 a 2 b 2 − 2 a b b 2 − c 2 − 2 b c a 2 c 2 − 2 a c a^2b^2-2abb^2-c^2-2bca^2c^2-2ac a2b2−2abb2−c2−2bca2c2−2ac 每个数平方项都要计算 n − 1 n-1 n−1次 减的那一份可…

MYSQL篇--事务机制高频面试题

事务 1 什么是数据库事务&#xff1f; 事务是一个不可分割的数据库操作序列&#xff0c;也是数据库并发控制的基本单位&#xff0c;其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作&#xff0c;要么都执行&#xff0c;要么都不执行。…

图纸版本管理混乱怎么办?彩虹PDM系统帮你搞定!

在现代制造业和工程领域&#xff0c;图纸版本管理的混乱常常是一个棘手的问题。不同版本的图纸可能导致严重的错误和生产问题&#xff0c;影响了产品质量和交付时间。然而&#xff0c;有一个强大的工具可以帮助企业解决这个问题&#xff0c;那就是PDM产品数据管理系统。彩虹PDM…

云流量回溯的工作原理及关键功能

云计算和网络技术的快速发展为企业提供了更灵活、高效的业务运营环境&#xff0c;同时也引发了一系列网络安全挑战。在这个背景下&#xff0c;云流量回溯成为网络安全领域的一个关键技术&#xff0c;为企业提供了对网络活动的深入洞察和实时响应的能力。 一、 云流量回溯的基本…

pkuseg按照用户自定义词典分词错误修正

import pkusegc pkuseg.pkuseg(user_dict"./data/dict.txt") sentence 数字传播实验班 print(c.cut(sentence))字典中包含“”数字传媒与人文学院"&#xff0c;添加自定义词典后&#xff0c;文本被错误分成““数字传 播 实验班” &#xff0c;debug发现solve…

OpenShift 4 - 在 OpenShift 上运行物体检测 AI 应用

《OpenShift / RHEL / DevSecOps 汇总目录》 说明&#xff1a;本文已经在 OpenShift 4.14 RHODS 2.5.0 的环境中验证 说明&#xff1a;请先根据《OpenShift 4 - 部署 OpenShift AI 环境&#xff0c;运行 AI/ML 应用&#xff08;视频&#xff09;》一文完成 OpenShift AI 环境…

python爬虫实战(8)--获取虎pu热榜

1. 需要的类库 import requests from bs4 import BeautifulSoup import pandas as pd2. 请求地址 def fetch_data():url "https://bbs.xxx.com/" # Replace with the actual base URLresponse requests.get(url)if response.status_code 200:return response.c…

2024年最火爆的本地生活服务商平台推荐!

随着互联网的发展&#xff0c;本地生活团购服务市场逐渐成为各大平台争夺的焦点。视频号、DY等短视频平台纷纷入局&#xff0c;希望通过本地生活团购的形式吸引更多的用户&#xff0c;提高平台的活跃度和黏性。对于想要成为本地生活团购服务商的创业者来说&#xff0c;都不想放…

web期末作业网页设计——JavaScript

目录 一.作品简介 二.网页效果 首页 花语 登录界面 注册界面 三.网页代码 首页 登录界面 注册界面 视频界面 一.作品简介 网站系统文件种类包含&#xff1a;html网页结构文件、css网页样式文件、js网页特效文件、images网页图片文件。 网页作品代码简单&#xff…

2024 年 10 款最佳 Android 手机数据恢复软件榜单

当您因某些不合时宜的事故而丢失 Android 设备上的重要数据时&#xff0c;这真是一场灾难。如果您遇到这样的情况&#xff0c;请不要担心。我们列出了一些最好的 Android 数据恢复软件&#xff0c;可以帮助您使用 PC 检索手机丢失的数据。 在用于存储重要数据的各种存储设备中…

你真的掌握了“C语言分支循环”吗

目录 前言 1. if语句 1.1 if 1.2 else 1.3 分支中包含多条语句 1.4 嵌套if 1.5 悬空else问题 2. 关系操作符 3. 条件操作符 4. 逻辑操作符&#xff1a;&& , || , &#xff01; 4.1 逻辑取反运算符 4.2 与运算符 4.3 或运算符 4.4 练习&#xff1a;闰年的判…
最新文章