Lecture 10: RISC-V Procedures

RISC-V Procedures

C Functions

C 函数

在 C 语言中,函数是一种用于封装代码块的结构,可以通过函数名调用该代码块。在下面的示例中,main 函数调用了 mult 函数来计算两个整数的乘积。

main() {
    int i, j, k, m;
    ...
    i = mult(j, k); 
    m = mult(i, i); 
}

int mult (int mcand, int mlier) {
    int product = 0;
    while (mlier > 0) {
        product = product + mcand;
        mlier = mlier - 1;
    }
    return product;
}

在这个示例中,main 函数调用了 mult 函数。mult 函数将两个整数相乘,并返回结果。

Six Fundamental Steps in Calling a Function

调用函数的六个基本步骤

  1. 将参数放置在函数可以访问的位置。
    • 通常使用寄存器传递参数。
  2. 将控制权转移到函数。
    • 使用跳转指令进入函数。
  3. 获取函数所需的(本地)存储资源。
    • 为局部变量分配内存。
  4. 执行函数的预期任务。
    • 计算并处理数据。
  5. 将返回值放置在调用代码可以访问的位置,并恢复使用的任何寄存器;释放本地存储。
    • 通常使用寄存器返回值。
  6. 将控制权返回到起始点,因为一个函数可以在程序中的多个点被调用。
    • 使用返回指令跳转回调用点。

RISC-V Function Call Conventions

RISC-V 函数调用约定

在 RISC-V 中,函数调用和返回使用了一些特定的寄存器:

  • a0-a7 (x10-x17):八个参数寄存器,用于传递参数和返回值。
  • ra (x1):返回地址寄存器,用于存储返回地址。
  • s0-s11 (x8-x9, x18-x27):保存寄存器,用于保存函数调用前的状态。

这些寄存器帮助我们在函数调用过程中高效地传递参数和返回值,并确保返回到正确的调用点。

Instruction Support for Functions (1/4)

函数指令支持 (1/4)

在 RISC-V 中,每条指令都是 4 字节,并且像数据一样存储在内存中。以下示例展示了如何在内存中存储和调用函数。

C 代码:

... sum(a, b); ...
int sum(int x, int y) {
    return x + y;
}

在 RISC-V 中:

地址 (十进制表示) 指令
1000
1004
1008
1012
1016
2000
2004

Instruction Support for Functions (2/4)

函数指令支持 (2/4)

将上面的 C 代码转换为 RISC-V 指令:

地址 (十进制表示) 指令
1000 mv a0, s0 // x = a
1004 mv a1, s1 // y = b
1008 addi ra, zero, 1016 // ra = 1016
1012 j sum // 跳转到 sum 函数
1016
2000 sum: add a0, a0, a1
2004 jr ra // 返回地址

Instruction Support for Functions (3/4)

函数指令支持 (3/4)

为什么在这里使用 jr 而不是 j?

因为 sum 可能在很多地方被调用,所以我们不能返回到固定的位置。调用过程必须能够说“返回这里”。

地址 (十进制表示) 指令
2000 sum: add a0, a0, a1
2004 jr ra // 新指令:跳转到寄存器地址

Instruction Support for Functions (4/4)

函数指令支持 (4/4)

单条指令跳转并保存返回地址:跳转并链接 (jal)

  • 之前:

    地址 (十进制表示) 指令
    1008 addi ra, zero, 1016 // ra = 1016
    1012 j sum // 跳转到 sum
  • 之后:

    地址 (十进制表示) 指令
    1008 jal sum // ra = 1012, 跳转到 sum

为什么要有 jal?

  • 使常见情况更快:函数调用非常常见。
  • 减少程序大小。
  • 使用 jal 不需要知道代码在内存中的确切位置。

这样可以大大简化函数调用过程,并提高程序执行效率。

RISC-V Function Call Instructions

RISC-V 函数调用指令

调用函数:跳转并链接指令 (jal)

  • 实际上应该是 laj(“链接并跳转”)
  • “链接”意味着形成指向调用位置的地址或链接,以允许函数返回到正确的地址
  • 跳转到地址并同时将以下指令的地址保存到寄存器 ra
  • 例子:jal FunctionLabel

从函数返回:跳转寄存器指令 (jr)

  • 无条件跳转到寄存器中指定的地址:jr ra
  • 汇编器简写:ret = jr ra

指令支持摘要

实际上,只有两条指令:

  • jal rd, Label:跳转并链接
  • jalr rd, rs, imm:跳转并链接寄存器

标签可能没有足够的位数来跳到我们想要跳转的位置。使用 jalr 时,我们跳转到寄存器 rs + 立即数 imm 的内容(如同基地址指针和偏移量)并设置 rd 如同在 jal 中。

jjrret 是伪指令!

  • jjal x0, Label

补充*

在程序执行过程中,函数调用和返回是极其频繁的操作。不同的处理器架构对这些操作的实现方式有所不同。尤其在处理递归调用和嵌套调用时,不同架构的差异可能导致程序行为的显著不同。RISC-V 和 x86 就是两个典型的例子,其中 RISC-V 需要手动管理栈,而 x86 自动处理栈操作。

x86 架构中的 ret 指令

调用过程中的细节

在 x86 架构中,函数调用和返回通过 callret 指令自动处理栈操作:

  1. 序言(Prologue)
    • call 指令将当前指令指针(EIP 或 RIP)的值压入栈中,这是返回地址。
    • 栈指针(ESP 或 RSP)减小,指向新的栈顶。
    • call 指令跳转到子程序的入口地址。
  2. 子程序执行
    • 子程序开始执行,使用 pushpop 操作管理局部变量和寄存器保存。
  3. 尾声(Epilogue)
    • ret 指令从栈中弹出返回地址,并将其加载到指令指针(EIP 或 RIP)中。
    • 栈指针增加,恢复到调用 call 指令之前的状态。
    • 程序执行流返回到调用点的下一条指令。

由于 callret 指令自动处理栈操作,因此不会因为缺少手动管理而产生栈混乱或循环调用的问题。

section .text
global _start

_start:
    call my_function
    ; 返回后继续执行
    mov eax, 1
    int 0x80

my_function:
    ; 函数体
    ret

RISC-V 架构中的 ret 指令

调用过程中的细节

在 RISC-V 架构中,函数调用和返回依赖于 jaljalr 指令,并且需要手动管理栈操作:

  1. 序言(Prologue)
    • jal 指令将返回地址保存在链接寄存器(ra,即 x1)。
    • jal 指令跳转到子程序的入口地址。
  2. 子程序执行
    • 子程序开始执行。需要手动将返回地址(ra)和必要的寄存器压入栈中,以保存现场。
  3. 尾声(Epilogue)
    • 在子程序结束时,从栈中弹出返回地址到 ra 寄存器。
    • 使用 jalr 指令跳转到 ra 寄存器中保存的返回地址。

栈管理的挑战

由于 RISC-V 的栈管理需要手动操作,如果没有正确的序言和尾声,就会导致栈混乱。例如,未能保存和恢复 ra 寄存器的值,可能导致返回地址错误,从而导致循环或崩溃。

.section .text
.globl _start

_start:
    jal ra, my_function
    ; 返回后继续执行
    li a7, 93
    ecall

my_function:
    addi sp, sp, -16  # 手动调整栈指针
    sw ra, 12(sp)     # 保存返回地址
    ; 函数体
    lw ra, 12(sp)     # 恢复返回地址
    addi sp, sp, 16   # 恢复栈指针
    jalr x0, 0(ra)    # 返回调用点

关键差异

  1. 指令差异
    • x86 使用 callret 指令自动处理返回地址的压栈和出栈。
    • RISC-V 使用 jaljalr 指令,并且需要手动管理栈操作。
  2. 栈管理
    • 在 x86 中,callret 指令自动管理栈操作,不需要程序员手动干预,因此不容易出现栈混乱或返回地址错误的问题。
    • 在 RISC-V 中,程序员必须手动保存和恢复返回地址(ra),如果遗漏这些操作,可能导致返回地址错误、栈混乱,甚至程序崩溃。

理解 RISC-V 和 x86 在函数调用和返回机制上的差异,特别是栈管理的不同,有助于编写更加稳健和高效的代码。对于 RISC-V 架构,特别要注意在函数的序言和尾声中正确保存和恢复返回地址及必要的寄存器,以避免潜在的问题。而在 x86 中,由于指令自动处理了这些操作,相对更为简单和安全。

序言

在 x86 架构中,函数调用和返回的效率得益于特定指令的设计,这些指令能够同时处理栈操作和跳转。理解这些指令的工作原理,可以帮助我们更好地掌握函数调用机制和程序流控制。

x86 架构中的 callret 指令

call 指令

call 指令用于调用子程序,它能够完成以下两个任务:

  1. 将返回地址(即调用点的下一条指令的地址)压入栈中。
  2. 跳转到子程序的入口地址。

call 指令的操作可以分解为以下步骤:

  • 保存返回地址:首先,call 指令将当前指令指针(EIP 或 RIP)压入栈中。这是为了在子程序执行完毕后能够返回调用点。
  • 跳转:然后,call 指令将目标地址加载到指令指针中,使得程序执行流跳转到子程序的入口地址。

示例:

section .text
global _start

_start:
    call my_function
    ; 返回后继续执行
    mov eax, 1
    int 0x80

my_function:
    ; 子程序内容
    ret

在这个例子中,call my_function_start 之后的指令地址压入栈中,然后跳转到 my_function 执行。

ret 指令

ret 指令用于从子程序返回,它能够完成以下两个任务:

  1. 从栈中弹出返回地址。
  2. 跳转到返回地址,使程序继续执行调用点的下一条指令。

ret 指令的操作可以分解为以下步骤:

  • 恢复返回地址:首先,ret 指令从栈顶弹出一个地址值,并将其加载到指令指针中。
  • 跳转:然后,程序执行流跳转到这个地址,继续执行调用点之后的指令。

示例:

section .text
global _start

_start:
    call my_function
    ; 返回后继续执行
    mov eax, 1
    int 0x80

my_function:
    ; 子程序内容
    ret

在这个例子中,当 my_function 执行完毕后,ret 指令将返回地址从栈中弹出,并跳转回 _start 的下一条指令 mov eax, 1

栈的使用

在 x86 中,栈是从高地址向低地址增长的。栈指针(ESP 或 RSP)指向栈顶,栈的操作涉及指针的调整:

  • 压栈(Push):将数据压入栈时,栈指针减小。
  • 弹栈(Pop):从栈中弹出数据时,栈指针增大。

call 指令的栈操作

假设当前栈指针为 ESP,调用子程序前:

  • 栈内容(高地址): … 返回地址 (低地址)

执行 call 指令后:

  • 栈内容(高地址): … 返回地址 (低地址)
  • ESP 减小,指向新的栈顶(返回地址)

ret 指令的栈操作

假设当前栈指针为 ESP,返回子程序前:

  • 栈内容(高地址): … 返回地址 (低地址)

执行 ret 指令后:

  • 栈内容(高地址): …
  • ESP 增大,恢复到调用前的状态

关键点总结

  • callret 指令能够同时处理栈操作和跳转,这是 x86 架构中函数调用和返回的核心机制。
  • call 指令将返回地址压入栈中,然后跳转到子程序入口地址。
  • ret 指令从栈中弹出返回地址,然后跳转到这个地址。
  • 栈在 x86 中是从高地址向低地址增长的,栈指针(ESP 或 RSP)管理栈顶的位置。

x86 和 RISC-V 是两种不同的指令集架构,它们在设计上有显著的区别,这些区别源于各自的设计目标和历史背景。理解这些差异的原因以及各自的优缺点,有助于更好地选择和优化计算机系统。

设计区别的原因

x86 设计背景

x86 架构最初由 Intel 在1978年发布,其设计初衷是为了兼容早期的 8086 微处理器,因此包含了大量的历史包袱和复杂性。

  1. 向后兼容:为了保证与早期软件的兼容性,x86 保留了许多旧指令和模式。这使得指令集越来越复杂。
  2. 复杂指令集计算(CISC):x86 属于 CISC 架构,旨在通过复杂的指令来减少每个程序的指令数量。callret 等指令自动管理栈操作,简化了编程。
  3. 硬件自动化:为了简化编程和提高效率,x86 在硬件中实现了许多自动化功能,例如自动处理栈操作和指令指针管理。

RISC-V 设计背景

RISC-V 是由加州大学伯克利分校在2010年开始开发的,它是一种开放、简单且灵活的架构,设计目标是简洁和高效。

  1. 简化指令集计算(RISC):RISC-V 属于 RISC 架构,旨在通过简化的指令集来提高指令执行效率和减少硬件复杂性。
  2. 模块化设计:RISC-V 采用模块化设计,核心指令集很小,但可以通过添加扩展来支持更多功能。
  3. 开源与灵活性:RISC-V 是开源的,设计上非常灵活,允许不同的实现和优化。手动管理栈操作提供了更大的控制权,但也增加了编程复杂性。

优缺点分析

x86 的优缺点

优点

  1. 向后兼容性:x86 的最大优势是与早期软件的兼容性,能够运行几十年来开发的各种软件。
  2. 硬件自动化callret 指令自动管理栈操作和指令指针,简化了程序设计,减少了编程错误。
  3. 强大的生态系统:由于其历史悠久和广泛使用,x86 具有丰富的硬件和软件生态系统。

缺点

  1. 复杂性:为了向后兼容,x86 指令集非常复杂,导致硬件实现复杂且功耗较高。
  2. 效率问题:复杂的指令集和硬件逻辑可能导致指令执行效率降低,特别是在现代高性能计算需求下。
  3. 封闭性:x86 是专有架构,不同厂商之间缺乏灵活性和透明度。

RISC-V 的优缺点

优点

  1. 简洁性:RISC-V 指令集设计简洁,减少了硬件复杂性,提高了指令执行效率。
  2. 灵活性:模块化设计允许根据需求添加扩展,适应不同的应用场景。开源属性也促进了创新和优化。
  3. 高效性:由于指令集简洁,RISC-V 在性能和功耗上具有优势,特别适合嵌入式系统和高性能计算。

缺点

  1. 编程复杂性:手动管理栈操作增加了编程复杂性和潜在的编程错误风险。
  2. 生态系统不完善:虽然 RISC-V 发展迅速,但与 x86 相比,硬件和软件生态系统仍不够成熟。
  3. 缺乏向后兼容性:由于 RISC-V 是新兴架构,缺乏对旧软件的支持,可能需要更多的移植和优化工作。

Where Are Old Register Values Saved to Restore Them After Function Call?

旧寄存器值在函数调用后保存在哪里以便恢复?

需要一个地方保存调用函数前的旧值,返回时恢复并删除它们。理想的地方是栈:后进先出(LIFO)队列(例如,一叠盘子)。

  • 推(Push):将数据放入栈中
  • 弹(Pop):从栈中取出数据

栈在内存中,因此需要寄存器指向它。sp 是 RISC-V 中的栈指针(x2)。约定是栈从高地址向低地址增长:

  • 推(Push)递减 sp,弹(Pop)递增 sp

Stack

栈帧包括:

  • 返回的“指令”地址
  • 参数(arguments)
  • 其他局部变量的空间

栈帧是连续的内存块;栈指针告诉栈帧的底部在哪里。当过程结束时,栈帧被从栈中移除;释放内存供未来的栈帧使用。

RISC-V Function Call Example

RISC-V 函数调用示例

int Leaf (int g, int h, int i, int j) {
    int f;
    f = (g + h) - (i + j);
    return f;
}
  • 参数变量 g, h, ij 在参数寄存器 a0, a1, a2a3 中,变量 fs0 中。
  • 假设需要一个临时寄存器 s1

RISC-V Code for Leaf()

Leaf() 的 RISC-V 代码

Leaf:
    addi sp, sp, -8      # 调整栈以容纳 2 个条目
    sw s1, 4(sp)         # 保存 s1 供后续使用
    sw s0, 0(sp)         # 保存 s0 供后续使用
    
    add s0, a0, a1       # f = g + h
    add s1, a2, a3       # s1 = i + j
    sub a0, s0, s1       # 返回值 (g + h) - (i + j)
    
    lw s0, 0(sp)         # 恢复调用者的寄存器 s0
    lw s1, 4(sp)         # 恢复调用者的寄存器 s1
    addi sp, sp, 8       # 调整栈以删除 2 个条目
    jr ra                # 跳回调用程序

Stack Before, During, After Function

函数调用前、调用中和调用后的栈

  • 需要保存 s0s1 的旧值。

image-20240801104036868

Nested Calls and Register Conventions

嵌套调用和寄存器约定

What If a Function Calls a Function? Recursive Function Calls?

如果一个函数调用另一个函数?递归函数调用?

  • 调用另一个函数会覆盖 a0-a7ra 中的值。
  • 解决方案是使用栈来保存这些值。

Example: Nested Procedures

int sumSquare(int x, int y) {
    return mult(x, x) + y;
}
  • 调用 sumSquare 的某个函数正在调用 mult
  • ra 中的值需要保存以返回 sumSquare,但在调用 mult 时会被覆盖。
  • 在调用 mult 之前需要保存 sumSquare 的返回地址,可以使用栈来完成。

Register Conventions

寄存器约定

  • Caller: 调用函数
  • Callee: 被调用函数
  • 当被调用者从执行中返回时,调用者需要知道哪些寄存器可能已经更改,哪些寄存器保证不变。
  • 寄存器约定: 规定了在过程调用(jal)之后哪些寄存器将保持不变,哪些可能被更改。

为了减少因溢出和恢复寄存器而产生的昂贵的加载和存储,RISC-V 函数调用约定将寄存器分为两类:

  1. 在函数调用中保持不变
    • 调用者可以依赖这些值不变。
    • sp, gp, tp, “保存的寄存器” s0-s11s0 也是 fp
  2. 在函数调用中不保持不变
    • 调用者不能依赖这些值不变。
    • 参数/返回寄存器 a0-a7, ra
    • 临时寄存器 t0-t6

RISC-V Symbolic Register Names

RISC-V 符号寄存器名称

这张表格列出了RISC-V寄存器的名称、ABI名称、描述以及保存者。寄存器的编号是硬件可以理解的,而符号名称则是人类友好的。

Register ABI Name Description 中文描述 Saver
x0 zero Hard-wired zero 硬连线零值 -
x1 ra Return address 返回地址 Caller
x2 sp Stack pointer 栈指针 Callee
x3 gp Global pointer 全局指针 -
x4 tp Thread pointer 线程指针 -
x5 t0 Temporary/Alternate link register 临时/备用链接寄存器 Caller
x6-7 t1-2 Temporaries 临时寄存器 Caller
x8 s0/fp Saved register/Frame pointer 保存寄存器/帧指针 Callee
x9 s1 Saved register 保存寄存器 Callee
x10-11 a0-1 Function arguments/Return values 函数参数/返回值 Caller
x12-17 a2-7 Function arguments 函数参数 Caller
x18-27 s2-11 Saved registers 保存寄存器 Callee
x28-31 t3-6 Temporaries 临时寄存器 Caller

这些符号名称用于汇编代码中,使其更易读。

Register Usage Explained

x0 - zero

硬连线零值。该寄存器始终保存0值,不会被修改。用于需要零值的操作。

x1 - ra (Return address)

返回地址。调用函数时保存返回地址,函数执行完毕后跳回此地址。由调用者保存。

x2 - sp (Stack pointer)

栈指针。指向当前栈顶,用于管理函数调用时的栈帧。由被调用者保存。

x3 - gp (Global pointer)

全局指针。指向全局变量的基地址。

x4 - tp (Thread pointer)

线程指针。用于线程局部存储的基地址。

临时寄存器/备用链接寄存器。用于临时数据存储,不在函数调用间保持值。由调用者保存。

x6-x7 - t1-t2 (Temporaries)

临时寄存器。用于临时数据存储,不在函数调用间保持值。由调用者保存。

x8 - s0/fp (Saved register/Frame pointer)

保存寄存器/帧指针。用于保存数据,在函数调用间保持值。由被调用者保存。

x9 - s1 (Saved register)

保存寄存器。用于保存数据,在函数调用间保持值。由被调用者保存。

x10-x11 - a0-a1 (Function arguments/Return values)

函数参数/返回值。用于传递函数参数和返回值。由调用者保存。

x12-x17 - a2-a7 (Function arguments)

函数参数。用于传递函数参数。由调用者保存。

x18-x27 - s2-s11 (Saved registers)

保存寄存器。用于保存数据,在函数调用间保持值。由被调用者保存。

x28-x31 - t3-t6 (Temporaries)

临时寄存器。用于临时数据存储,不在函数调用间保持值。由调用者保存。

Usage Examples

使用示例

  • 调用函数
  • 在调用函数时,jal 指令将返回地址保存在 ra(x1)中。函数执行完毕后,jr ra 指令使用 ra 中的地址返回调用点。
  • 参数传递
  • 函数参数通过 a0-a7(x10-x17)寄存器传递。例如,第一个参数存储在 a0(x10)中,第二个参数存储在 a1(x11)中。
  • 保存寄存器
  • 被调用函数如果需要使用 s0-s11(x8-x27)寄存器,则需要在函数开始时将其保存到栈中,函数结束时再恢复。
  • 临时寄存器
  • 临时寄存器 t0-t6(x5-x7, x28-x31)用于存储临时数据,不需要在函数调用间保持值,因此不需要保存和恢复。

在 RISC-V 调用约定中,寄存器的使用遵循一套规则,这些规则决定了函数在调用和返回时如何处理寄存器的内容。这些规则包括哪些寄存器应该由调用者保存,哪些应该由被调用者保存。这种区分使得函数调用在使用寄存器时能够保持一致性,并避免意外覆盖重要的数据。

RISC-V 寄存器分类

RISC-V 的寄存器可以分为几类:

  1. 临时寄存器(t0-t6)
    • 这些寄存器不需要被调用者保存。调用者可以自由使用这些寄存器,并且不期望被调用者会在返回时保持它们的值。因此,如果被调用者在使用这些寄存器时覆盖了它们的值,调用者不能依赖这些寄存器在函数调用后仍然保持不变。
  2. 保存寄存器(s0-s11)
    • 这些寄存器需要被调用者保存。这意味着如果一个函数使用了这些寄存器,它必须在返回之前将这些寄存器恢复到调用时的状态。因此,被调用者在修改这些寄存器之前,通常会将它们的当前值保存到栈上,并在函数结束前将它们恢复。
  3. 返回地址寄存器(ra)
    • 用于存储返回地址,即调用函数后返回的位置。
  4. 参数寄存器(a0-a7)
    • 用于传递函数参数和返回值。调用者负责在调用前设置这些寄存器,被调用者可以自由使用这些寄存器。

如果Leaf 函数使用临时寄存器

判断使用哪种寄存器通常是编译器的任务,但在手动优化和特定场景下,程序员也需要做出相应的决策。如果程序员知道某个寄存器的值不会被函数调用破坏,可以使用临时寄存器。例如,在一个简单的计算过程中,没有调用其他函数,使用临时寄存器可以简化代码。

不过在被调用的函数(Callee)过程中,可能会再次调用其他函数,这会导致临时寄存器(t0-t6)被修改,从而破坏当前函数对这些寄存器值的依赖。因此,临时寄存器不适合在需要保持跨函数调用的值时使用。

1. 临时寄存器的特性

临时寄存器(t0-t6)在 RISC-V 的调用约定中属于“调用者保存”的寄存器。这意味着调用者在调用函数之前,如果需要保留这些寄存器中的值,就必须自己保存(通常是保存到栈上),然后在函数调用返回后恢复这些寄存器的值。被调用者(即被调用的函数)可以自由地使用这些寄存器,而不需要在返回之前恢复它们的值。

2. 函数的嵌套调用

在被调用的函数(如示例中的 Leaf 函数)中,可能会有多层函数调用。例如,Leaf 函数可能会再次调用其他函数:

Leaf:
    addi sp, sp, -8      # 调整栈以容纳 2 个条目
    sw s1, 4(sp)         # 保存 s1 供后续使用
    sw s0, 0(sp)         # 保存 s0 供后续使用

    add s0, a0, a1       # f = g + h
    add s1, a2, a3       # s1 = i + j
    call some_function   # 调用其他函数

    sub a0, s0, s1       # 返回值 (g + h) - (i + j)

    lw s0, 0(sp)         # 恢复调用者的寄存器 s0
    lw s1, 4(sp)         # 恢复调用者的寄存器 s1
    addi sp, sp, 8       # 调整栈以删除 2 个条目
    jr ra                # 跳回调用程序

在这个示例中,Leaf 函数中调用了 some_function。如果 some_function 使用了临时寄存器 t0-t6,那么在 some_function 返回时,t0-t6 的值可能已经被修改。

3. 临时寄存器的使用风险

如果 Leaf 函数使用了临时寄存器来存储重要的中间结果(如 s0, s1),并且 some_function 改变了这些寄存器的值,那么 Leaf 函数在 some_function 返回后,再使用这些寄存器时,就会得到错误的结果。这会导致程序行为不可预测,出现错误。

4. 保存寄存器的使用

保存寄存器(s0-s11)则不同。它们是“被调用者保存”的寄存器,即如果 Leaf 函数使用了这些寄存器,它有责任在使用这些寄存器之前保存它们的值,并在函数返回之前恢复它们的值。这样可以确保在 Leaf 函数调用 some_function 之后,s0s1 的值仍然是函数调用之前的状态,这样就可以安全地继续使用这些寄存器。

虽然在某些情况下,比如在最内层的被调用函数中,确实没有进一步调用其他函数的计划,因此在理论上使用临时寄存器可能不会立即引发问题。不过使用保存寄存器可以避免增加新的功能或调用其他函数后的潜在问题,使得代码更加稳定和易于维护。

程序员在手写汇编或特定优化场景中,可以自己决定使用哪种寄存器。程序员需要了解寄存器的用途和调用约定,以正确选择合适的寄存器。

And in Conclusion, the RV32 So Far…

总结,RV32 到目前为止…

  • Arithmetic/logic 算术/逻辑
    • add rd, rs1, rs2
    • sub rd, rs1, rs2
    • and rd, rs1, rs2
    • or rd, rs1, rs2
    • xor rd, rs1, rs2
    • sll rd, rs1, rs2
    • srl rd, rs1, rs2
    • sra rd, rs1, rs2
  • Immediate 立即数
    • addi rd, rs1, imm
    • andi rd, rs1, imm
    • ori rd, rs1, imm
    • xori rd, rs1, imm
    • slli rd, rs1, imm
    • srli rd, rs1, imm
    • srai rd, rs1, imm
  • Load/store 加载/存储
    • lw rd, rs1, imm
    • lb rd, rs1, imm
    • lbu rd, rs1, imm
    • sw rs1, rs2, imm
    • sb rs1, rs2, imm
  • Branching/jumps 分支/跳转
    • beq rs1, rs2, Label
    • bne rs1, rs2, Label
    • bge rs1, rs2, Label
    • blt rs1, rs2, Label
    • bgeu rs1, rs2, Label
    • bltu rs1, rs2, Label
    • jalr rd, rs, imm
    • jal rd, Label

© 2024 LzzsSite
该笔记由 LzzsG 基于 CS 61C / Garcia, Yan 的作品创作,采用的是 CC BY-NC-SA 许可。