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!

  • 23
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 《计算机组成与设计:RISC-V版本,硬件与软件的互动》是一本关于计算机体系结构的教材。它涵盖了计算机硬件和软件之间的相互作用。 首先,这本教材深入介绍了计算机组成的基本概念和原理。它讲解了计算机硬件的各个组成部分,如中央处理器、存储器、输入输出设备等,并详细解释了它们之间的工作原理和互动方式。读者可以通过这些内容全面了解计算机硬件的工作方式。 此外,该教材还重点介绍了RISC-V指令集架构。RISC-V是一种现代的、开放的指令集架构,具有简洁、规范和可定制的特点。本书详细描述了RISC-V指令集的设计和实现,并解释了它与计算机硬件的紧密关系。读者可以通过学习RISC-V指令集,了解指令的执行过程,理解计算机在硬件层面上如何处理指令和数据。 在硬件和软件的交互方面,这本教材强调了它们之间的密切联系。它介绍了硬件和软件之间的界面和通信方式,如总线、中断和输入输出等。通过学习这些内容,读者将了解到计算机硬件和软件是如何相互配合工作的。它还讨论了如何进行硬件和软件的调试和优化,以提高计算机的性能和可靠性。 总的来说,《计算机组成与设计:RISC-V版本,硬件与软件的互动》这本书从计算机硬件和软件的角度全面介绍了计算机的组成和互动方式。通过学习这本教材,读者可以深入了解计算机系统的工作原理,并掌握如何设计和优化计算机系统的能力。 ### 回答2: 《计算机组织与设计:RISC-V版》是一本关于计算机硬件和软件互联的重要教材。这本书的主要内容包括计算机组织与结构、指令级并行、存储系统、互连技术、输入输出系统等。该书以RISC-V指令集架构为基础,详细介绍了计算机的硬件结构和设计原理,并与软件编程环境相结合。这种硬件软件相互补充的设计使得计算机能够高效、稳定地运行。 该书的特点之一是使用清晰的语言和具体的实例解释计算机硬件和软件之间的关系。通过逐步引入不同的主题和概念,读者可以深入了解计算机硬件组成的基本原理,并了解它们与软件编程之间的互动关系。此外,书中提供了大量的实践案例和练习题,使读者能够巩固所学的知识,并自主进行实践和思考。 在讲解硬件设计方面,该书详细讨论了计算机的基本组成部分,如处理器、存储器、输入输出设备以及互连技术等。它深入探讨了各个组件的工作原理和设计方法,包括流水线、缓存、并发控制等。此外,该书还介绍了指令级并行的相关技术,如流水线、超标量、动态调度等,使读者能够了解如何通过优化硬件设计来提高计算机的性能。 在软件编程方面,该书介绍了RISC-V指令集的特点和使用方法。它详细讲解了指令的结构和功能,以及如何使用汇编语言进行编程。此外,该书还介绍了操作系统、编译器和调试工具等软件开发环境的基本原理和使用方法,使读者能够理解软件和硬件之间的交互关系,以及如何进行有效的软件开发。 总之,《计算机组织与设计:RISC-V版》通过深入浅出的讲解和大量实例的引导,将计算机硬件和软件的复杂性简化为易于理解和学习的内容。它为读者提供了全面而深入的知识,使他们能够了解计算机系统的工作原理、优化硬件设计和进行高效软件编程。这本书是学习计算机组成与设计的重要参考资料,对于想要深入了解计算机硬件和软件的读者来说是一本不可或缺的教材。 ### 回答3: 《计算机组织与设计RISC-V版:硬件软件接口》介绍了计算机硬件和软件的互动关系。它涵盖了计算机系统中硬件和软件之间的接口,以及它们是如何相互作用的。 该书首先介绍了计算机体系结构的基本知识,包括指令集架构、计算机组成和设计原则等。接着,它深入探讨了RISC-V架构,该架构是一种现代的指令集架构,被广泛用于教育和学术研究。 书中还详细讨论了硬件和软件之间的接口,包括指令集、寄存器、内存和输入输出等。通过深入的解释和实例演示,读者可以了解硬件和软件之间的通信和协作方式。 此外,该书还介绍了一些高级主题,如流水线和并行处理。这些主题涉及到优化计算机性能的技术和策略,使读者能够更好地理解复杂的计算机系统结构。 《计算机组织与设计RISC-V版:硬件软件接口》适用于计算机科学、计算机工程和相关专业的学生。它是一本全面介绍计算机系统结构和设计原理的权威教材,旨在帮助读者深入理解计算机硬件和软件之间的互动关系。读者可以通过阅读本书,获得一种全面的计算机系统知识,为日后的学习和工作打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sunvally

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值