RISC-V Bytes: Caller and Callee Saved Registers

news2024/9/29 7:22:17

原文链接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.coloradmin.cn/o/1379198.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ftrans飞驰云联荣获“CSA 2023安全创新奖”

2023年12月21日&#xff0c;第七届云安全联盟大中华区大会在深圳成功举办。会上&#xff0c;CSA大中华区发布了多个研究成果并进行 CSA 2023年度颁奖仪式&#xff0c;Ftrans飞驰云联以其突出的技术创新能力和广泛的市场应用前景&#xff0c;荣获备受瞩目的“CSA 2023安全创新奖…

redis夯实之路-哨兵(Sentinel)机制详解

Sentinel&#xff08;哨兵&#xff09;保证了redis的高可用性&#xff0c;一个Sentinel或多个Sentinel组成的系统监视多个主从服务器&#xff0c;当主服务器下线时&#xff0c;自动将一个从服务器升级为主服务器。 sentinel的主要功能 集群监控&#xff1a;负责监控redis mas…

imgaug库指南(20):从入门到精通的【图像增强】之旅

引言 在深度学习和计算机视觉的世界里&#xff0c;数据是模型训练的基石&#xff0c;其质量与数量直接影响着模型的性能。然而&#xff0c;获取大量高质量的标注数据往往需要耗费大量的时间和资源。正因如此&#xff0c;数据增强技术应运而生&#xff0c;成为了解决这一问题的…

【Python】使用Opencv裁剪指定区域,再重构大小和保存示例

在Python中&#xff0c;使用OpenCV库可以很方便地截取图像的某一区域&#xff0c;然后尺寸重构&#xff0c;最后保存为新的图像文件。以下是一个示例代码&#xff0c;演示如何实现这一操作&#xff1a; import cv2# 读取图像 img cv2.imread(img.jpg)# 定义截取的区域&#x…

Istio 实战:WasmPlugin(Proxy-Wasm 插件)开发(实现限流和修改请求和响应的 header)

更多 istio 文章&#xff1a;Istio 专栏目录 WasmPlugin 的典型应用 限流&#xff1a;当前 envoy 提供的限流能力虽然比较强大&#xff0c;但主要提供了一些 api&#xff0c;在使用上对用户不够友好&#xff0c;且全局限流对每个请求都调用一次限流服务&#xff0c;性能损耗较…

SSM-SpringMVC+Spring+Mybatis

创建项目 创建好 项目后, 项目目录分析 数据库设计 我们采用员工表 Employee 与 部门表 Department 部门表 表设计--- 员工表 --表设计 因为有文件上传操作,因此 建立 info表 (其中 员工只能隶属一个部门,因此 两张表之间 有外键关系) java 代码 设计 数据库建立完毕后,需要…

自定义数据实现SA3D

SA3D&#xff1a;Segment Anything in 3D with NeRFs 实现了3D目标分割 原理是利用SAM(segment anything) 模型和Nerf分割渲染3D目标&#xff0c; SAM只能分块&#xff0c;是没有语义标签的&#xff0c;如何做到语义连续&#xff1f; SA3D中用了self-prompt, 根据前一帧的mask…

C#编程-了解线程的优先级

了解线程的优先级 控制线程行为的一个属性是它的优先级。.NET运行时环境基于它们的优先级执行线程。CPU一次仅执行一个线程。因此,处于执行的可运行状态的线程,排队等待轮到被处理器执行。线程是固定优先级调度的。带有优先级的每个线程在处理器的线程队列中有自己的位置。 …

Java面试之虚拟机

1、前言 本篇的面试题基于网络整理&#xff0c;和自己编辑。在不断的完善补充哦。 2、什么是虚拟机&#xff1f; Java 虚拟机&#xff0c;是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件( .class )。 Java 被设计成允许应用程…

老胡的周刊(第124期)

老胡的信息周刊[1]&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。 &#x1f3af; 项目 QAnything[2] 开源的企业级本地知识库问答解…

Java入门IDEA基础语法

1&#xff1a;Java入门 1.1 Java简介 Java是什么&#xff1a; Java是一门非常优秀的计算机语言 语言&#xff1a;人与人交流沟通的表达方式 计算机语言&#xff1a;人与计算机之间进行信息交流沟通的一种特殊语言 Java之父&#xff1a;詹姆斯高斯林&#xff08;James Gosli…

深入理解计算机系统(2):信息的表示和处理

信息存储 大多数计算机使用 8 位的块&#xff0c;或者字节(byte)&#xff0c;作为最小的可寻址的内存单位&#xff0c;而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组&#xff0c;称为虚拟内存(virtual memory)。内存的每个字节都由一个唯一的数字来标识…

Elasticsearch:是时候离开了! - 在 Elasticsearch 文档上使用 TTL

作者&#xff1a;来自 Elastic David Pilato 想象一下&#xff0c;圣诞老人必须向世界上所有的孩子们分发礼物。 他有很多工作要做&#xff0c;他需要保持高效。 他有一份所有孩子的名单&#xff0c;并且知道他们住在哪里。 他很可能会将礼物按区域分组&#xff0c;然后再交付。…

2024上半年教资笔试报名详细教程1月12日开始报名啦

重点提醒&#xff1a; 1、注册开放时间&#xff1a;2024年1月10日开始。 &#xff08;参加过笔试的考生&#xff0c;需要重新注册&#xff0c; 不影响已获得的笔试成绩。名额少的考点建议提前注册抢名额&#xff09; 2、网上报名时间&#xff1a;2024年1月12日至15日。 千万不…

【IDEA--dubug相关】-- 1. 取消debug的所有断点 2. debug侧边栏消失问题

下面是一些经常在日常debug时用到的场景&#xff0c;方便查看、与君共勉&#xff01; 文章目录 1. 如何取消所有断点2. debug底部左边侧边栏消失 1. 如何取消所有断点 如图我们可能在项目中给很多代码行添加了断点&#xff0c;一个一个点取消麻烦 在debug运行中点击选中底部…

《C语言学习》---郝斌版---笔记

简介 学习计算机&#xff0c;离不开C语言的学习&#xff0c;而C语言学习过程中的视频课教程&#xff0c;目前来说&#xff0c;如果郝斌老师的C语言排第二&#xff0c;没有人敢排第一 郝斌老师的C语言教程&#xff0c;通俗易懂&#xff0c;引人发思&#xff0c;特别适合新手入门…

深度探析卷积神经网络(CNN)在图像视觉与自然语言处理领域的应用与优势

目录 前言1 CNN网络结构与工作原理1.1 输入层1.2 卷积层1.3 最大池化层1.4 全连接层 2 应用领域2.1 图像视觉领域中CNN的应用2.2 NLP领域中CNN的应用 3 CNN的限制与未来展望3.1 CNN的挑战3.2 CNN的展望 结语 前言 卷积神经网络&#xff08;CNN&#xff09;作为一种强大的深度学…

2023一带一路暨金砖国家技能发展与技术创新大赛“网络安全”赛项省选拔赛样题卷①

2023金砖国家职业技能竞赛"网络安全" 赛项省赛选拔赛样题 2023金砖国家职业技能竞赛 省赛选拔赛样题第一阶段&#xff1a;职业素养与理论技能项目1. 职业素养项目2. 网络安全项目3. 安全运营 第二阶段&#xff1a;安全运营项目1. 操作系统安全配置与加固任务一Linux …

DNS解析和主从复制

一、DNS名称解析协议 二、DNS正向解析 三、DNS主从复制 主服务器 从服务器

2024年湖北职称评审对论文的要求

1.期刊发表版面的时间节点2024年12月及之前 2.期刊是正规的期刊&#xff0c;有国内刊号 3.期刊能在国家出版社总署检索到 4.文章内容查重符合知网查重标准 5.论文方向和申报专业方向一致 6.必须要是第一作者或者独著 7.评正高的人才们要准备中文核心论文两篇或出版专业学术论著…