当前位置: 首页 > news >正文

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!

相关文章:

  • SSH镜像、systemctl镜像、nginx镜像、tomcat镜像
  • C#编程-属性和反射
  • 从CISC到RISC-V:揭开指令集的面纱
  • 使用 PyQt 实现简单数据绑定和组件化
  • 文献阅读:Large Language Models as Optimizers
  • ZZULIOJ 1112: 进制转换(函数专题)
  • 【JaveWeb教程】(26) Mybatis基础操作(新增、修改、查询、删除) 详细代码示例讲解(最全面)
  • 解决方案类常用网址
  • linux如何创建文件教程分享
  • Ubuntu 22.04 Cron使用
  • 数据结构之Radix和Trie
  • 强化学习应用(四):基于Q-learning的物流配送路径规划研究(提供Python代码)
  • 【JavaWeb后端开发-第五章(1)】Mybatis入门基础
  • 常用Java代码-Java中的Optional类和null安全编程
  • VL53L4CD TOF开发(1)----驱动TOF进行测距
  • $translatePartialLoader加载失败及解决方式
  • 345-反转字符串中的元音字母
  • ABAP的include关键字,Java的import, C的include和C4C ABSL 的import比较
  • Essential Studio for ASP.NET Web Forms 2017 v2,新增自定义树形网格工具栏
  • jQuery(一)
  • mac修复ab及siege安装
  • python学习笔记 - ThreadLocal
  • Web标准制定过程
  • 闭包,sync使用细节
  • 不上全站https的网站你们就等着被恶心死吧
  • 从@property说起(二)当我们写下@property (nonatomic, weak) id obj时,我们究竟写了什么...
  • 给初学者:JavaScript 中数组操作注意点
  • 基于Volley网络库实现加载多种网络图片(包括GIF动态图片、圆形图片、普通图片)...
  • 前端路由实现-history
  • 前端每日实战 2018 年 7 月份项目汇总(共 29 个项目)
  • 如何解决微信端直接跳WAP端
  • 探索 JS 中的模块化
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 用quicker-worker.js轻松跑一个大数据遍历
  • RDS-Mysql 物理备份恢复到本地数据库上
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • # Java NIO(一)FileChannel
  • ###项目技术发展史
  • #我与Java虚拟机的故事#连载18:JAVA成长之路
  • (3)llvm ir转换过程
  • (day 2)JavaScript学习笔记(基础之变量、常量和注释)
  • (DFS + 剪枝)【洛谷P1731】 [NOI1999] 生日蛋糕
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (备忘)Java Map 遍历
  • (转) ns2/nam与nam实现相关的文件
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .Net CF下精确的计时器
  • .NET Micro Framework 4.2 beta 源码探析
  • .net使用excel的cells对象没有value方法——学习.net的Excel工作表问题
  • /ThinkPHP/Library/Think/Storage/Driver/File.class.php  LINE: 48
  • ??在JSP中,java和JavaScript如何交互?
  • @Controller和@RestController的区别?
  • @selector(..)警告提示
  • [2018][note]用于超快偏振开关和动态光束分裂的all-optical有源THz超表——