资源1: RISC-V China – RISC-V International 

资源2: RISC-V International – RISC-V: The Open Standard RISC Instruction Set Architecture

资源3: RV32I, RV64I Instructions — riscv-isa-pages documentation

1. 指令集架构的类型

        在讨论RISC-V或任何处理器架构时,区分 非特权指令集架构(Unprivileged ISA)特权指令集架构(Privileged ISA)是很重要的。这两种类型的指令集反映了不同的使用场景和权限级别,对于理解现代计算机系统的运作方式至关重要。

1.1 非特权指令集架构(Unprivileged ISA)

        非特权指令集架构包含了处理器在用户模式下可执行的指令集。这些指令对操作系统的内核模式和直接访问硬件资源的能力有限制非特权ISA设计用于执行应用程序代码,提供了如算术运算、数据传输、逻辑操作和控制流等基本操作的指令。

1.1.1 目的

        为应用程序提供执行其功能所需的基本指令集,同时限制对系统资源的直接访问,以保护系统的安全性和稳定性。

1.1.2 特点

        非特权指令通常包括操作数的算术和逻辑操作、访问内存的加载和存储指令、分支和跳转指令等。

1.1.3 安全性

        通过限制非特权指令不能直接执行某些影响系统整体行为的操作(如直接管理硬件资源),增加了操作系统的安全性

1.2 特权指令集架构(Privileged ISA)

        特权指令集架构包含了处理器在内核模式或其他特权模式下可执行的指令集。这些指令允许直接访问和控制硬件资源,如内存管理单元(MMU)、中断控制和设备I/O特权ISA对于操作系统的功能,如进程调度、内存管理和硬件抽象层的实现至关重要。

1.2.1 目的

        提供必要的指令和控制机制,以实现操作系统的核心功能,包括资源管理、安全隔离和硬件抽象。

1.2.2 特点

        特权指令集包括控制寄存器访问、设置和清除特权级别、管理中断和异常、控制内存访问权限等操作。

1.2.3 安全性

        由于特权指令允许执行对系统安全和稳定性有深远影响的操作,因此它们的使用严格限制于操作系统内核或其他信任的系统软件。

1.3 二者的联系与区别

1.3.1 联系

        非特权和特权指令集共同定义了处理器的功能和能力,为不同的软件层级提供了适当的执行权限。

1.3.2 区别

        主要在于执行权限和可访问的资源非特权ISA为用户级应用提供基本操作,而特权ISA为系统级软件提供对硬件和关键系统资源的全面控制。

        在RISC-V架构中,这种区分允许创建可扩展和安全的计算系统,能够适应从简单的嵌入式设备到复杂的多核服务器的广泛需求。通过这种方式,RISC-V既满足了对硬件直接控制的需要,又保障了系统的安全性和稳定性

2. 指令助记符

        RISC-V的指令助记符是设计用来帮助人们更容易理解和记忆各种指令的简短代号。这些助记符通常反映了指令的操作类型、操作数和操作模式。下面列出了一些常见的RISC-V指令助记符的类别和示例:

2.1 算术操作指令

        ADD: 加法指令,执行两个寄存器的内容相加
        SUB: 减法指令,从一个寄存器的内容中减去另一个寄存器的内容
        MUL: 乘法指令,执行两个寄存器内容的乘法操作

2.2 逻辑操作指令

        AND: 逻辑操作,对两个寄存器的位进行逻辑与操作
        OR: 逻辑操作,对两个寄存器的位进行逻辑或操作
        XOR: 逻辑异或操作,对两个寄存器的位进行逻辑异或操作
        NOT: 逻辑操作,对寄存器的每一位进行逻辑非操作

2.3 比较和分支指令

        BEQ: 分支如果相等,如果两个寄存器的内容相等,则跳转到指定的代码位置。
        BNE: 分支如果不等,如果两个寄存器的内容不相等,则跳转到指定的代码位置。
        BLT: 分支如果小于,如果第一个寄存器的内容小于第二个寄存器的内容,则跳转到指定的代码位置。
        BGE: 分支如果大于等于,如果第一个寄存器的内容大于等于第二个寄存器的内容,则跳转到指定的代码位置。

2.4 数据传输指令

        LW: 加载字,从内存中加载一个字到寄存器。
        SW: 存储字,将寄存器的内容存储到内存中的指定位置。
        LBU: 加载字节无符号,从内存中加载一个字节到寄存器,并将其视为无符号数。
        SB: 存储字节,将寄存器的最低字节存储到内存中的指定位置。

2.5 控制流指令

        JAL: 跳转并链接,跳转到指定的代码位置,并将返回地址保存到寄存器中。
        JALR: 跳转并链接寄存器,通过寄存器指定的地址跳转,并将返回地址保存到寄存器中。

2.6 特殊操作指令

        NOP: 无操作,这是一个不执行任何操作的指令,通常用于占位或延迟。
        FENCE: 内存屏障,用于指令和内存操作的排序保证。

        这些助记符是RISC-V指令集的一部分,它们代表了处理器可以执行的各种基本操作。了解这些基本的指令助记符有助于理解RISC-V程序的结构和功能。

2.7 举例说明

        在RISC-V指令集中,slt、slti、sltu 和 sltiu 是用于比较操作的指令,它们可以用于判断两个数的大小关系。下面是这四种指令的详细说明和示例:

2.7.1 slt(Set Less Than)

        指令格式: slt rd, rs1, rs2
        功能: 如果寄存器 rs1 中的值小于寄存器 rs2 中的值,则将寄存器 rd 设置为1,否则设置为0。这里的比较是有符号数的比较。
        示例: 假设寄存器 x5 的值为10,寄存器 x6 的值为20,则指令 slt x7, x5, x6 会将寄存器`x7`设置为1,因为10小于20。

2.7.2 slti(Set Less Than Immediate)

        指令格式:  slti rd, rs1, imm 
        功能: 如果寄存器 rs1 中的值小于立即数 imm ,则将寄存器 rd 设置为1,否则设置为0。比较也是有符号数的比较。
        示例: 假设寄存器 x5 的值为15,则指令 slti x7, x5, 20 会将寄存器 x7 设置为1,因为15小于20。

2.7.3 sltu(Set Less Than Unsigned)

        指令格式:  sltu rd, rs1, rs2
        功能: 如果寄存器 rs1 中的值(作为无符号数)小于寄存器 rs2 中的值(作为无符号数),则将寄存器 rd 设置为1,否则设置为0。
        示例: 假设寄存器 x5 的值为0xFFFFFFFF(4294967295,一个很大的无符号数),寄存器 x6 的值为1,指令 sltu x7, x5, x6 会将寄存器 x7 设置为0,因为从无符号数的角度来看,0xFFFFFFFF大于1。

2.7.4 sltiu(Set Less Than Immediate Unsigned)

        指令格式: sltiu rd, rs1, imm 
        功能: 如果寄存器 rs1 中的值(作为无符号数)小于立即数 imm (也被视为无符号数),则将寄存器 rd 设置为1,否则设置为0。
        示例: 假设寄存器 x5 的值为2,则指令 sltiu x7, x5, 3 会将寄存器 x7 设置为1,因为2小于3(作为无符号数比较)。

        这些指令在条件分支、循环控制和数值比较中非常有用,允许程序根据有符号或无符号数值的比较结果进行逻辑决策。

2.7 立即数的概念

        立即数(Immediate Value)是在汇编语言和计算机指令集中常见的概念,它指的是直接嵌入到指令中的常数值。与需要通过寄存器或内存地址访问的数据不同,立即数在指令执行时直接可用,无需进行额外的内存访问或寄存器查找。这种设计允许指令快速地使用固定的值,提高了指令执行的效率。

        立即数的用途包括但不限于:

        赋值操作:将一个固定的值直接赋给寄存器。
        算术操作:作为算术指令的一个操作数,与寄存器中的值进行加、减、乘、除等操作。
        条件分支:作为比较操作的参考值,或决定跳转指令的目的地址偏移量。
        地址计算:在加载(load)和存储(store)指令中,立即数常用来指定寄存器地址的偏移量。

        由于立即数是指令的一部分,其大小(位数)受到指令格式的限制。在不同的处理器架构中,立即数可以是有符号或无符号的数值,其范围和表示方式依赖于具体的指令集架构和指令的设计。

        例如,在RISC-V架构中,addi 指令允许将一个寄存器的内容与一个立即数相加,并将结果存回寄存器。如果指令是 addi x1, x2, 4 ,那么它的作用是将寄存器x2的值与立即数4相加,并将结果存储在寄存器x1中。这里的4就是一个立即数,它直接编码在指令中,无需从内存或其他寄存器中获取。

        立即数是汇编语言和低级编程中一个重要的概念,它使得指令能够更加紧凑和高效地执行常见的操作。

2.8 操作码(opcode)

        RISC-V 指令集架构(ISA)中的操作码(opcode)是用于指定要执行的操作的二进制序列。在 RISC-V 中,每条指令通常由一串二进制代码组成,操作码是这串代码中用于标识具体执行哪种操作的部分。

        操作码的具体作用包括:

        a.指定操作类型:操作码决定了指令要执行的基本操作,比如算术加法、数据加载、数据存储、条件分支等。

        b.指令格式识别:RISC-V 指令集支持多种指令格式,如R型(寄存器到寄存器操作)、I型(立即数操作)、S型(存储操作)、B型(分支操作)、J型(跳转操作)和U型(立即数加载操作)。操作码帮助识别这些不同的指令格式。

        RISC-V 操作码的结构设计遵循简洁高效的原则。以32位RISC-V指令为例,操作码通常位于指令的最低有效位(LSB)部分,长度可能为7位,用于标识指令的主要类型和操作格式。操作码的不同值对应不同的操作和指令格式,这些信息定义了如何解释指令的其余部分。

        例如,操作码可以指示一条指令是进行寄存器间的算术运算(R型),还是将一个立即数(即,指令中直接给出的数值)加载到寄存器中(I型)。在RISC-V中,具体的操作码值和相应的操作在RISC-V官方手册中有详细定义。

        简而言之,RISC-V中的操作码是指令的核心部分,用于标识执行何种操作,它直接影响处理器如何解释和执行一条指令。

3. 指令格式

        RISC-V架构提供了一套精简但功能强大的指令集,其指令格式设计以支持高效的硬件实现和简化的指令解码过程。RISC-V指令集主要分为几种基本格式,每种格式用于不同类型的操作,如数据处理、控制流改变、内存访问等。下面是RISC-V指令集中的主要指令格式:

RISC-V知识总结 —— 指令集-LMLPHP
3.1 R型(寄存器-寄存器操作)

        结构:[funct7][rs2][rs1][funct3][rd][opcode]

        R型(寄存器-寄存器操作)指令在RISC-V指令集架构中用于执行寄存器之间的算术和逻辑操作。这类指令直接操作处理器内部的寄存器,而不涉及内存访问,从而提供了高效的数据处理能力。

3.1.1 作用

        用于算术和逻辑操作,如加法(ADD)、减法(SUB)和与(AND)操作。

        R型指令的主要作用包括:

3.1.1.1 算术操作

        执行基本的算术运算,如加法(ADD)、减法(SUB)、乘法(MUL)和除法(DIV)

3.1.1.2 逻辑操作

        执行逻辑运算,如与(AND)、或(OR)、异或(XOR)和非(NOT)。

3.1.1.3 移位操作

        对寄存器值进行左移(SLL)和右移(SRL/SRA)操作。

3.1.1.4 比较操作

        比较寄存器中的值,如设置小于(SLT)操作。

3.1.2 格式和组成

        R型指令的格式包含三个寄存器操作数(源寄存器1(rs1)、源寄存器2(rs2)和目标寄存器(rd)),一个功能码(funct7)和一个操作码(opcode)

        R型指令的格式主要包括:

        操作码(opcode): 指示这是一条R型指令。
        功能码(funct3)和(funct7): 确定具体的操作类型,如加法或逻辑与。
        源寄存器1(rs1)和源寄存器2(rs2): 指定参与操作的两个寄存器。
        目标寄存器(rd): 存储操作结果的寄存器。

3.1.3 示例指令

        ADD: add rd, rs1, rs2 指令将寄存器rs1和rs2中的值相加,结果存储在rd寄存器中
        SUB: sub rd, rs1, rs2 指令从寄存器rs1的值中减去rs2的值,结果存储在rd寄存器中
        AND: and rd, rs1, rs2 指令执行寄存器rs1和rs2的位与操作,结果存储在rd中。
        SLT:   slt rd, rs1, rs2 指令比较寄存器rs1和rs2的值,如果rs1小于rs2,则rd被设置为1,否则设置为0。

3.1.4 作用总结

        R型指令通过提供一系列富有表现力的算术和逻辑操作,是RISC-V架构中实现高效数据处理的基础。这些指令的设计充分利用了处理器内部寄存器的高速访问能力,使得RISC-V处理器能够快速执行复杂的数值和逻辑计算,支撑起各种计算密集型应用的需求。

3.2 I型(立即数操作)

3.2.1 作用

        用于将立即数与寄存器中的值进行算术和逻辑操作,以及加载指令(如LW)和跳转指令(如JALR)。I型指令的主要作用包括:

3.2.1.1 算术操作

        执行寄存器与立即数之间的算术运算,如加法(ADDI)。

3.2.1.2 逻辑操作

        执行寄存器与立即数之间的逻辑运算,如与(ANDI)、或(ORI)、异或(XORI)。

3.2.1.3 加载操作

        从内存中加载数据到寄存器,如加载字(LW),其中立即数用作偏移量。

3.2.1.4 移位操作

        对寄存器值进行立即数指定的位移操作,如左移(SLLI)和右移(SRLI/SRAI)。

3.2.1.5 条件分支和跳转

        尽管分支(B型)和跳转(J型)指令通常不归类为I型,但某些形式的条件分支和跳转操作,如跳转并链接(JALR),使用立即数作为偏移量。

3.2.2 格式

        包含一个寄存器操作数(源寄存器1(rs1)),一个目标寄存器(rd),一个立即数(imm)和操作码(opcode)。
        结构:[imm][rs1][funct3][rd][opcode]

        I型指令的格式主要包括:

        操作码(opcode): 指示这是一条I型指令。
        功能码(funct3): 确定具体的操作类型,如加法或加载。
        源寄存器1(rs1): 指定参与操作的寄存器。
        目标寄存器(rd): 存储操作结果的寄存器。
        立即数(imm): 直接编码在指令中的数值,参与运算或指定偏移量。

        I型(Immediate类型)指令在RISC-V指令集架构中用于执行寄存器与立即数(即直接编码在指令中的数值)之间的操作。这类指令支持一系列算术、逻辑操作和内存访问操作,其中立即数直接参与运算或用于指定操作的参数,如偏移量。

3.2.3 示例指令

        ADDI: addi rd, rs1, imm指令将寄存器rs1的值与立即数imm相加,结果存储在rd寄存器中。
        LW: lw rd, imm(rs1) 指令从以寄存器rs1为基址、加上立即数偏移量imm的内存地址加载一个字到rd寄存器中。
        SLTI: slti rd, rs1, imm指令比较寄存器rs1的值与立即数imm,如果rs1小于imm,则将rd设置为1,否则设置为0。

3.2.4 作用总结

        I型指令通过结合寄存器操作和立即数操作,为RISC-V提供了灵活的数据处理能力。这包括直接的算术和逻辑操作,以及支持高效的内存访问和简单的条件处理。立即数的使用极大地增强了指令的表达能力,使得程序能够直接编码常数值和偏移量,从而简化代码并提高执行效率。

3.3 S型(存储指令)

        S型(Store类型)指令在RISC-V指令集架构中用于存储数据,即将数据从寄存器写入到内存中。S型指令的典型用途是将处理器寄存器中的数据保存到内存地址中,这个内存地址通常由基址寄存器(base register)和一个立即数偏移量(immediate offset)共同确定。

3.3.1 作用

        用于存储数据到内存,如存储字(SW)

        S型指令的主要作用包括:

3.3.1.1 数据存储

        将寄存器中的数据存储到内存中,支持不同的数据大小和类型,例如字节(SB)、半字(SH)和字(SW)等。

3.3.1.2 内存管理

        通过改变内存中的内容,S型指令对于实现各种内存管理策略,如缓冲管理、数据交换等,至关重要。

3.3.1.3 数据共享和通信

        在多任务、多线程环境中,S型指令可以用于实现进程或线程间的数据共享和通信。

3.3.2 格式和组成

        包含两个寄存器操作数(源寄存器1(rs1)和源寄存器2(rs2)),立即数(imm)和操作码(opcode)。立即数用于指定从基址寄存器(rs1)开始的偏移量

S型指令的格式由以下几部分组成:

        操作码(opcode): 指示这是一条存储类型的指令。
        功能码(funct3): 指定存储操作的具体类型(如SB、SH、SW等)。
        源寄存器1(rs1): 用作基址寄存器,与立即数偏移量一起确定数据存储的目标内存地址。
        源寄存器2(rs2): 存储将要写入内存的数据。
        立即数偏移量(imm): 与基址寄存器(rs1)的值相加,共同确定数据存储的具体内存地址。

        结构:[imm[11:5]][rs2][rs1][funct3][imm[4:0]][opcode]

3.3.3 示例

        考虑以下S型指令示例:

sw x9, 12(x2)

        这条指令的含义是将寄存器 x9 中的数据存储到以寄存器 x2 为基址、偏移量为12个字节的内存地址中。在这里,x2 通常指向一个有效的内存基址,而12是一个立即数偏移量,指示相对于基址的偏移位置。

        通过这种方式,S型指令在RISC-V架构中扮演着在内存和寄存器之间传输数据的关键角色,是内存访问和数据处理不可或缺的一部分。

3.3.4 S型(Store类型)指令中的立即数(imm)

        在RISC-V的S型(Store类型)指令中,立即数(imm)被分成两个部分并分散在指令的不同字段中。这种设计是为了优化指令格式,使其能够在固定的32位指令长度中高效地编码操作和操作数。具体到S型指令,立即数被用来指定存储操作的内存偏移量,而这个立即数被拆分为两部分:imm[11:5] 和 imm[4:0] 。

        imm[4:0]: 这部分包含了立即数的低5位,位于指令格式的最低位字段中。它和 imm[11:5] 一起组合起来形成了完整的12位立即数偏移量。

        imm[11:5]: 这部分包含了立即数的高7位,位于指令格式的最高位字段中。

        这种拆分立即数的方式允许指令同时携带两个寄存器操作数( rs1 和 rs2 )和一个12位的立即数,而不牺牲指令的紧凑性。在执行存储操作时,rs1 寄存器的值作为基址,立即数偏移量 imm加到这个基址上,形成最终的内存地址,然后 rs2 寄存器中的数据被存储到这个地址中。

        例如,一个存储字(SW)指令可能如下所示:

sw x9, 34(x2)

        这里,x9 是数据源寄存器( rs2 ), x2 是基址寄存器( rs1 ),而34是立即数偏移量,用于指定从 x2 指向的地址开始的偏移量,其中 34 的二进制表示中的低5位将位于 imm[4:0] ,高7位将位于 imm[11:5] 。这样,指令能够在不超过32位长度的限制下,有效地编码所有需要的信息。

3.4 B型(分支指令)

        B型(Branch类型)指令在RISC-V指令集架构中用于实现条件分支。这类指令允许程序根据寄存器中的值或比较结果改变执行流程,实现逻辑判断和条件执行的功能。B型指令通过比较两个寄存器的值来决定是否跳转到程序中的另一个位置。

3.4.1 作用

        用于条件分支,如分支如果等于(BEQ)。

        B型指令的主要作用包括:

3.4.1.1 控制流程

        根据条件(如两个数的相等性、大小等)改变程序的执行路径。这对于循环、条件语句(如if-else结构)等控制结构至关重要。

3.4.1.2 条件执行

        允许程序在满足特定条件时执行特定代码块,提高了程序的逻辑能力和灵活性。

3.4.1.3 减少不必要的执行

        通过避免执行不满足条件的代码块,可以提高程序的效率和性能。

3.4.2 格式和组成

        结构:[imm[12]][imm[10:5]][rs2][rs1][funct3][imm[4:1]][imm[11]][opcode]

        包含两个寄存器操作数(源寄存器1(rs1)和源寄存器2(rs2)),立即数(imm)表示跳转偏移量和操作码(opcode)。

B型指令的格式由以下几部分组成:

        操作码(opcode): 指示这是一条分支类型的指令。
        功能码(funct3) : 指定分支条件的具体类型(如等于、不等于、小于等)。
        源寄存器1(rs1)和源寄存器2(rs2): 用于比较的两个寄存器。根据这两个寄存器的比较结果,决定是否进行跳转。
        立即数偏移量(imm): 表示从当前指令地址开始的跳转偏移量,如果满足分支条件,则程序计数器(PC)会加上这个偏移量,跳转到新的指令地址执行。

3.4.3 示例

        考虑以下B型指令示例:

beq x1, x2, label

        这条指令的含义是比较寄存器 x1 和 x2 中的值,如果它们相等,则跳转到标签 label`指示的程序地址继续执行。label在汇编代码中通常是一个符号地址,编译器或汇编器会将其转换为具体的偏移量。

        B型指令使得RISC-V能够根据程序的逻辑条件动态地改变执行路径,是实现高效和灵活程序控制流的关键指令类型。

3.5 U型(长立即数操作)

        U型(Upper Immediate类型)指令在RISC-V指令集架构中用于处理立即数操作,特别是那些需要将较大的立即数直接加载到寄存器的场景。U型指令的设计目的是为了简化对大立即数的处理,这在设置大范围的基地址或进行大数值的初始化时特别有用。      
        格式:包含一个目标寄存器(rd),一个20位立即数(imm)和操作码(opcode)。
        结构:[imm][rd][opcode]

3.5.1 作用

        用途:用于加载一个20位的立即数到寄存器,如LUI(加载上部立即数到寄存器)。

        U型指令的主要作用包括:

3.5.1.1 加载大立即数

        直接将一个20位的立即数加载到寄存器的高位同时清除寄存器的低12位,这样可以快速初始化寄存器为一个较大的数值。

3.5.1.2 设置基地址

        在进行大范围内存访问或跳转时,可以使用U型指令来设置高位地址,与其他指令(如I型或S型)结合使用,完成对具体地址的操作。

3.5.2 格式和组成

        U型指令的格式主要包括:

        操作码(opcode): 指示这是一条U型指令。
        目标寄存器(rd): 需要加载立即数的目标寄存器。
        20位立即数(imm): 直接提供给寄存器的高20位值,而寄存器的低12位则被设置为0。

3.5.3 示例指令

        U型(Upper Immediate)指令在RISC-V指令集中主要有两种:LUI(Load Upper Immediate)和 AUIPC (Add Upper Immediate to PC)。这些指令用于处理需要大立即数的操作,特别是在设置大数值或进行跳转计算时。下面是这两种U型指令的用例:

3.5.3.1 LUI (Load Upper Immediate)

        设置大的常量值或初始化大的数据结构。

        将一个20位的立即数加载到寄存器的高20位,低12位清零。这用于构建大的常数值。 
        示例1:lui x1, 0x12345 将0x12345000加载到寄存器x1。

        示例2: 假设我们需要将一个32位的大数值 0x12345000 加载到寄存器x1中。可以使用 LUI 指令来实现这一点:

lui x1, 0x12345

        这条指令将立即数0x12345加载到x1寄存器的高20位,低12位被清零,结果是x1寄存器的内容变为0x12345000。

3.5.3.2 AUIPC (Add Upper Immediate to PC)
3.5.3.2.1 基本介绍

        将20位立即数左移12位后加到当前程序计数器(PC)的值上,结果存储到目标寄存器。这常用于实现基于当前位置的大范围跳转。
        示例:auipc x1, 0x12345 计算 PC + 0x12345000的结果,并存储到寄存器x1。

3.5.3.2.2 功能介绍

        AUIPC(Add Upper Immediate to PC)指令是RISC-V指令集中的一条指令,用于将一个20位的立即数左移12位后加到当前程序计数器(PC)的值上,计算的结果存储到目标寄存器中。这条指令非常有用,特别是在需要基于当前程序计数器位置进行大范围跳转的场景中。

3.5.3.2.3 指令格式

        AUIPC 指令属于U型格式,其结构如下:

        opcode(操作码): 指定这是一条 AUIPC 指令。
        rd(目标寄存器): 指令执行的结果将存储在这个寄存器中。
        imm[31:12](立即数): 指令的20位立即数部分,将被左移12位并加到PC上。

3.5.3.2.4 操作

        AUIPC 指令的操作可以表示为:

rd = PC + (imm[31:12] << 12)

        这里,imm[31:12]是指令中的20位立即数,将这个立即数左移12位可以确保加到PC上的是一个较大的值,从而实现跨越较大地址范围的跳转。

3.5.3.2.5 示例1

        在示例 auipc x1, 0x12345中:

        目标寄存器是 x1。
        立即数是 0x12345 ,但在 AUIPC 指令中只使用立即数的高20位,因此实际使用的立即数是 0x12345 左移12位后的值,即 0x12345000 。

        执行这条指令时,会将 0x12345000 加到当前的程序计数器(PC)值上,然后将结果存储到寄存器 x1 中。

        这种指令特别适用于实现基于当前位置的跳转,例如在进行函数调用或跳转到程序中的另一个段时。通过使用 AUIPC 指令,可以在不依赖于绝对地址的情况下,灵活地计算目标地址。

3.5.3.2.6 示例2

        用例: 实现基于当前程序计数器(PC)的相对地址计算,常用于大范围的跳转和数据访问。

        示例: 假设我们想要跳转到当前PC地址加上某个大偏移量的位置。如果偏移量是 0x12345000,我们可以使用 AUIPC 指令,然后通过一个相对较小的偏移量与之相加,来实现具体的跳转或数据访问。

auipc x1, 0x12345
addi x1, x1, 512 # 假设我们还需要加上512的偏移

        首先,AUIPC指令将当前PC的值加上 0x12345000 ,结果存入x1寄存器。然后,ADDI 指令将x1中的值再加上512,最终的结果是x1中存储了从当前PC开始的一个较大偏移后的地址。

3.5.3.2.6 示例3

        假设你正在编写一个RISC-V程序,需要从当前代码位置跳转到一个位于程序中相对较远位置的函数。这个函数的地址相对于当前指令的位置有一个已知的偏移量,但是由于指令需要使用相对地址计算,你不能直接使用一个绝对地址跳转。这里,AUIPC和JALR(Jump and Link Register)指令联合使用就非常有用。

3.5.3.2.6.1 示例场景

        当前PC值为 0x1000。
        你想要跳转到位于 0x12345678 的函数。
        函数的地址相对于当前指令的偏移量较大,不能直接通过一条简单的指令达到。

3.5.3.2.6.2 使用AUIPC和JALR实现跳转

        1. 计算偏移量
        假设目标函数的地址是 0x12345678 ,当前PC(即`AUIPC`指令的地址)是 0x1000。
        你首先需要计算从当前PC到目标地址的偏移量。为了简化这个例子,我们假设偏移量已知,且大于可以直接用一条指令编码的范围。

        2. 使用AUIPC来设置基地址
        由于直接偏移量太大,不能直接使用 JALR 跳转,我们使用 AUIPC 来获取一个接近目标地址的基地址。
        假设通过计算,我们知道 AUIPC 需要加上的立即数是 0x12345 (注意,实际操作时这个值需要根据目标地址和当前PC地址精确计算)。
        执行 AUIPC x1, 0x12345 。这会将 0x12345000 加到当前PC 0x1000 上,结果是 x1 = 0x12346000。

        3. 使用JALR进行最终跳转

        假设目标函数实际上在 0x12345678 ,我们已经有了基地址 0x12346000 在 x1 中。
        现在我们需要计算从 0x12346000`到`0x12345678`的偏移,这是一个负值,假设为`-0x988`(实际计算中需要精确计算)。
        执行 JALR x0, x1, -0x988 。 JALR`会将`x1 的值(基地址)加上`-0x988`(偏移),跳转到0x12345678 ,同时可以选择将下一条指令的地址保存到 x0(这里用`x0`,因为我们不需要保存返回地址)。

3.5.3.2.6.3 结果

        通过以上步骤,你就能够从当前位置 0x1000 跳转到 0x12345678 的目标函数,即使这个跳转超出了一条指令直接编码的偏移范围。这个方法利用了`AUIPC`来设置一个近似的基地址,然后通过`JALR`完成精确的跳转。这种技术在需要实现基于位置无关代码(PIC)的情况下特别有用,例如在动态链接库或操作系统内核中。

3.5.4 与其他指令的配合使用

        在RISC-V架构中,U型指令主要包括 LUI(Load Upper Immediate)和 AUIPC (Add Upper Immediate to PC),它们各自的作用及与其他指令的配合使用方式如下:

3.5.4.1 LUI (Load Upper Immediate)
3.5.4.1.1 作用

        LUI用于将一个20位的立即数加载到目标寄存器的高20位,同时将低12位清零。这允许程序直接设置寄存器中的大数值,常用于初始化大常量或设置大地址值。

3.5.4.1.2 配合使用

        与立即数操作指令配合: LUI 可以与ADDI(加上一个立即数)或其他立即数操作指令配合,用于生成完整的32位或更大的数值。
        示例:如果要在寄存器中设置一个具体的32位值`0x12345678`,可以先使用`LUI`设置高20位,然后用`ADDI`添加低12位。

  lui x1, 0x12345  # x1 = 0x12345000
  addi x1, x1, 0x678  # x1 = 0x12345678
3.5.4.2 AUIPC (Add Upper Immediate to PC)
3.5.4.2.1 作用

        AUIPC将一个20位立即数左移12位(形成一个大的偏移量),然后加到当前的程序计数器(PC)值上,结果存储在目标寄存器中。这主要用于实现基于当前地址的偏移计算,便于进行模块化编程和地址的动态计算。

3.5.4.2.2 配合使用

        与跳转指令配合:AUIPC常与JALR(Jump And Link Register)指令配合使用,实现跨模块或大范围的函数调用和跳转。
  
  示例:实现一个相对当前PC大偏移量的跳转。

  auipc x1, 0x12345  # x1 = 当前PC + 0x12345000
  jalr x0, x1, 0  # 跳转到x1指向的地址(假设这里没有额外偏移)

        与加载/存储指令配合:AUIPC 也可以与加载(如`LW`)或存储(如`SW`)指令配合使用,用于访问距离当前PC较远的数据。

  示例:访问一个大偏移量处的数据。

  auipc x1, 0x12345  # x1 = 当前PC + 0x12345000
  lw x2, 100(x1)  # 从x1 + 100的地址加载一个字到x2

3.5.5 总结

        U型指令通过设置大数值和基于PC的偏移地址计算,扩展了RISC-V的编程能力,特别是在处理大常量、实现跨模块调用和动态数据访问时。这些指令的设计体现了RISC-V架构对于灵活性和效率的重视,使得程序员可以更方便地实现复杂的编程模式和高效的内存管理策略。

        U型指令通过直接操作寄存器和PC的高位,为RISC-V提供了在不同编程场景下处理大数值和地址的能力。LUI 使得设置大的常量值成为可能,而`AUIPC`在实现程序的模块化和内存访问方面提供了极大的灵活性,特别是在处理跳转和地址计算时。这些指令是RISC-V指令集中处理大数值和实现高级控制流结构的关键工具。

        U型指令通过允许直接操作较大的立即数,极大地提高了RISC-V处理器设置大数值和地址计算的灵活性和效率,特别是在进行程序跳转、大数值初始化或操作系统级的内存管理时非常有用。

3.6 J型(跳转指令)

        这些指令格式的设计旨在优化指令的解码过程和提高执行效率,同时保持指令集的灵活性和可扩展性。

        J型(Jump类型)指令在RISC-V指令集架构中用于实现无条件跳转,允许程序执行流跳转到内存中的任意位置。这类指令对于控制程序流程、实现函数调用和返回等操作至关重要

3.6.1 作用

        用于无条件跳转,如JAL(跳转并链接),J型指令的主要作用包括:

3.6.1.1 实现无条件跳转

        允许程序无条件地跳转到指定的地址继续执行,这对于函数调用、循环跳出等场景非常重要。

3.6.1.2 函数调用和返回

        在一些RISC-V变体中,J型指令(如JAL)不仅用于跳转,还可以将返回地址保存到寄存器中,这样可以实现函数调用和返回机制。

3.6.1.3 程序流控制

        通过改变执行路径,J型指令可以实现复杂的控制流逻辑,如条件执行、循环和分支等。

3.6.2 格式和组成

        包含一个目标寄存器(rd),一个立即数(imm)表示跳转偏移量和操作码(opcode)。

        [imm[20]][imm[10:1]][imm[11]][imm[19:12]][rd][opcode]

        J型指令的格式主要包括:

        操作码(opcode): 指示这是一条J型指令。
        目标寄存器(rd): 用于保存返回地址的寄存器(对于JAL指令)。对于纯跳转操作(如J指令,如果存在),这个字段可能不被使用。
        立即数(imm): 表示从当前指令地址开始的跳转偏移量。这个偏移量是有符号的,允许向前或向后跳转。

3.6.3 示例指令

        J型指令的典型代表是JAL(Jump And Link):

3.6.3.1 作用

        JAL (Jump And Link): 执行无条件跳转到指定地址,并将下一条指令的地址(即当前PC值加上4)保存到指定的寄存器中,通常用于实现函数调用。跳转目标由当前PC值和立即数偏移量计算得出。这允许程序在执行完被调用的代码(如函数或过程)后返回到原始位置继续执行。

        示例:jal x1, offset 指令会计算 当前 PC + offset 作为跳转目标地址,并将跳转后的下一条指令地址保存到x1寄存器中。

3.6.3.2 具体用例
3.6.3.2.1 函数调用

        在调用一个函数时,可以使用 JAL 指令跳转到函数的起始地址,并将返回地址保存到寄存器中,通常是 ra (返回地址寄存器)。

 jal ra, function  # 跳转到function,保存返回地址到ra寄存器
3.6.3.2.2 循环或条件后的跳转

        虽然J型指令主要用于函数调用,但它也可以用于实现复杂的控制流,如在满足特定条件后跳转到循环的开始或结束。

jal x0, loop_start  # 无条件跳转到循环开始,不保存返回地址(使用x0)
3.6.3.2.3 程序内部大范围跳转

        当程序需要在内部进行大范围的跳转时(例如,从初始化代码跳转到主循环),JAL可以实现这一跳转,同时通过选择不将返回地址保存到任何寄存器(使用x0作为目标寄存器),避免了不必要的返回。

jal x0, main_loop  # 跳转到主循环,不需要返回,因此目标寄存器是x0

3.6.4 总结

        JAL 指令是RISC-V架构中实现函数调用和处理复杂控制流的关键。通过保存返回地址,它不仅支持了函数的调用与返回机制,而且提供了程序设计中所需的灵活性和功能性。这种方式使得程序可以在不同的代码段之间高效跳转,是构建模块化和可重用代码的基础。

        J型指令通过提供强大的无条件跳转功能,是实现程序控制流中关键结构(如函数调用、循环和条件分支)的基础。这些指令使得RISC-V架构能够支持复杂的程序逻辑和高效的函数调用机制,是现代编程模式在硬件层面的直接体现。

        J型(Jump类型)指令在RISC-V指令集中主要由`JAL`(Jump And Link)构成,它用于实现无条件跳转,同时能够将下一条指令的地址保存到寄存器中,这对于函数调用和返回特别有用。

02-23 12:59