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,-320x000000000001015a <+2>:     sd         ra,24(sp)0x000000000001015c <+4>:     sd         s0,16(sp)0x000000000001015e <+6>:     addi       s0,sp,320x0000000000010160 <+8>:     li         a5,10x0000000000010162 <+10>:    sw         a5,-20(s0)0x0000000000010166 <+14>:    li         a5,20x0000000000010168 <+16>:    sw         a5,-24(s0)0x000000000001016c <+20>:    lw         a4,-20(s0)0x0000000000010170 <+24>:    lw         a5,-24(s0)0x0000000000010174 <+28>:    addw       a5,a5,a40x0000000000010176 <+30>:    sw         a5,-28(s0)0x000000000001017a <+34>:    lw         a5,-28(s0)0x000000000001017e <+38>:    mv         a1,a50x0000000000010180 <+40>:    lui        a5,0x1c0x0000000000010182 <+42>:    addi       a0,a5,176 # 0x1c0b00x0000000000010186 <+46>:    jal        ra,0x10332 <printf>0x000000000001018a <+50>:    li         a5,00x000000000001018c <+52>:    mv         a0,a50x000000000001018e <+54>:    ld         ra,24(sp)0x0000000000010190 <+56>:    ld         s0,16(sp)0x0000000000010192 <+58>:    addi       sp,sp,320x0000000000010194 <+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.rhkb.cn/news/238295.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL面试题 | 08.精选MySQL面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

AArch64 memory management学习(二)

提示 该博客主要为个人学习&#xff0c;通过阅读官网手册整理而来&#xff08;个人觉得阅读官网的英文文档非常有助于理解各个IP特性&#xff09;。若有不对之处请参考参考文档&#xff0c;以官网文档为准。AArch64 memory management学习一共分为两章&#xff0c;这是第二章。…

实战 | 某电商平台类目SKU数获取与可视化展示

一、项目背景 最近又及年底&#xff0c;各类分析与规划报告纷至沓来&#xff0c;于是接到了公司平台类目商品增长方向的分析需求&#xff0c;其中需要结合外部电商平台做对比。我选择了国内某电商平台作为比较对象&#xff0c;通过获取最细层级前台类目下的SKU数以及结构占比&…

Java网络爬虫--HttpClient

目录标题 技术介绍有什么优点&#xff1f;怎么在项目中引入&#xff1f; 请求URLEntityUtils 类GET请求带参数的GET请求POST请求 总结 技术介绍 HttpClient 是 Apache Jakarta Common 下的子项目&#xff0c;用来提供高效的、功能丰富的、支持 HTTP 协议的客户端编程工具包。相…

八分钟了解一致性算法 -- Raft算法

前言 #### 分布式一致性在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性。 #### 分布式一致性算法比较常见的一致性算法包括Paxos算法,Raft算法,ZAB算法等• Paxos是Leslie Lamport提出的一种基于消息传递的分布式一致性算法。很多分布式一致性算法都由…

机器学习根据金标准标记数据-九五小庞

根据金标准标记数据是一种在机器学习和数据科学中常见的操作&#xff0c;主要用于评估分类模型的性能。其基本步骤如下&#xff1a; 收集数据&#xff1a;首先需要收集相关领域的原始数据&#xff0c;这些数据通常来自不同的来源和渠道。数据清洗和预处理&#xff1a;在这一步…

什么是Modbus协议?

Modbus协议是一种在工业自动化领域广泛应用的通信协议&#xff0c;它允许不同设备之间进行可靠的数据交换和控制。该协议最初由Modicon公司于1979年创建&#xff0c;旨在提供一种简单而有效的方法&#xff0c;使PLC&#xff08;可编程逻辑控制器&#xff09;和其他自动化设备能…

四搭建dockerhub私有仓库

搭建dockerhub私有仓库 很多场景下&#xff0c;我们需使用私有仓库管理Docker镜像。相比Docker Hub&#xff0c;私有仓库有以下优势&#xff1a; 节省带宽&#xff0c;对于私有仓库中已有的镜像&#xff0c;无需从Docker Hub下载&#xff0c;只需从私有仓库中下载即可&#x…

Vue学习笔记五--路由

1、什么是路由 2、VueRouter 2、1VueRouter介绍 2、2使用步骤 2、3路由封装 3、router-link 3.1两个类名 3.2声明式导航传参 4、路由重定向、404 当找不到路由时&#xff0c;跳转配置到404页面 5、路由模式 6、通过代码跳转路由---编程式导航&传参 路由跳转时传参 跳转方式…

【漏洞复现】大华 DSS 数字监控系统 itcBulletin SQL 注入

漏洞描述 大华 DSS存在SQL注入漏洞,攻击者 pota/services/itcBuletin 路由发送特殊构造的数据包,利用报错注入获取数据库敏感信息。攻击者除了可以利用 SQL注入漏词获取数据库中的信息例如,管理员后台密码、站点的用户人人信息)之外,甚至在高权限的情况可向服务器中写入木…

【Python数据可视化】matplotlib之绘制高级图形:散点图、热力图、等值线图、极坐标图

文章传送门 Python 数据可视化matplotlib之绘制常用图形&#xff1a;折线图、柱状图&#xff08;条形图&#xff09;、饼图和直方图matplotlib之设置坐标&#xff1a;添加坐标轴名字、设置坐标范围、设置主次刻度、坐标轴文字旋转并标出坐标值matplotlib之增加图形内容&#x…

嘴尚绝卤味:健康卤味风潮来袭,引领卤味市场新变革!

随着生活水平的提高&#xff0c;人们对食品的需求已不再满足于基本的口感和饱腹&#xff0c;健康、营养成为越来越多人关注的焦点。在这种背景下&#xff0c;健康卤味理念应运而生&#xff0c;并迅速在卤味市场引发了一场深刻的变革。 健康卤味理念强调选用优质、健康的食材&am…

SpringBoot3 WebFlux 可观测最佳实践

前言 链路追踪是可观测性软件系统的一个非常好的工具。它使开发人员能够了解应用程序中和应用程序之间不同交互发生的时间、地点和方式。同时让观测复杂的软件系统变得更加容易。 从Spring Boot 3开始&#xff0c;Spring Boot 中用于链路追踪的旧 Spring Cloud Sleuth 解决方…

反射助你无痛使用Semantic Kernel接入离线大模型

本文主要介绍如何使用 llama 的 server 部署离线大模型&#xff0c;并通过反射技术修改 Semantic Kernel 的 OpenAIClient 类&#xff0c;从而实现指定端点的功能。最后也推荐了一些学习 Semantic Kernel 的资料&#xff0c;希望能对你有所帮助。 封面图片&#xff1a; Dalle3 …

docker/华为云cce 部署nacos 2.3.0 集群模式

镜像地址 https://hub.docker.com/r/nacos/nacos-server 版本 nacos/nacos-server:v2.3.0-slim 关键环境变量 使用mysql数据源 变量值备注MODEcluster启用集群模式MYSQL_SERVICE_DB_NAME数据库名MYSQL_SERVICE_USER数据库用户名MYSQL_SERVICE_PASSWORD数据库密码SPRING_D…

WPF 布局

了解 WPF中所有布局如下&#xff0c;我们一一尝试实现&#xff0c;本文档主要以图形化的形式展示每个布局的功能。 布局&#xff1a; Border、 BulletDecorator、 Canvas、 DockPanel、 Expander、 Grid、 GridView、 GridSplitter、 GroupBox、 Panel、 ResizeGrip、 Separat…

API设计:从基础到最佳实践

1*vWvkkgG6uvgmJT8GkId98A.png 在这次深入探讨中&#xff0c;我们将深入了解API设计&#xff0c;从基础知识开始&#xff0c;逐步进阶到定义出色API的最佳实践。 作为开发者&#xff0c;你可能对许多这些概念很熟悉&#xff0c;但我将提供详细的解释&#xff0c;以加深你的理解…

13、Redis高频面试题

1、项目中为什么用Redis 我们项目中之所以选择Redis&#xff0c;主要是因为Redis有下面这些优点&#xff1a; 操作速度快&#xff1a;Redis的数据都保存在内存中&#xff0c;相比于其它硬盘类的存储&#xff0c;速度要快很多数据类型丰富&#xff1a;Redis支持 string&#x…

【题解】—— 每日一道题目栏

2024.1 【题解】—— LeetCode一周小结1 1. 1599. 经营摩天轮的最大利润 2. 466. 统计重复个数 3. 2487. 从链表中移除节点 4. 2397. 被列覆盖的最多行数 5. 1944. 队列中可以看到的人数 6. 2807. 在链表中插入最大公约数 7. 383. 赎金信 【题解】—— LeetCode一周小…

易安联参与制定的《面向云计算的零信任体系》行业标准即将实施

中华人民共和国工业和信息化部公告2023年第38号文件正式发布行业标准&#xff1a;YD/T 4598.2-2023《面向云计算的零信任体系 第2部分&#xff1a;关键能力要求》及YD/T 4598.3-2023《面向云计算的零信任体系 第3部分&#xff1a;安全访问服务边缘能力要求》&#xff0c;并于20…