本章介绍RV32I基本整数指令集。
RV32I旨在构成一个编译器目标并支持现代操作系统环境。ISA还旨在减少在最小实现中所需的硬件。RV32I包含40条独特的指令,然而一个简单的实现可能会使用一条总是产生陷阱的单个SYSTEM硬件指令来涵盖ECALL/EBREAK指令,同时也可能将FENCE指令实现为NOP,从而将基本指令数减少到38条。RV32I几乎可以模拟任何其他ISA扩展(A扩展除外,A扩展需要原子性的附加硬件支持)。
实际上,包含机器模式特权架构的硬件实现也需要6条CSR指令。
基本整数ISA的子集可能有助于教学目的,但是基本ISA已经定义地非常精简,除了省略对未对齐内存访问的支持并将所有SYSTEM指令视为单一陷阱之外,几乎没有动机将子集做为一个真实的硬件实现。
标准RISC-V汇编语言语法见汇编程序员手册[^1]
RV32I的大多数评注也适用于RV64I。
2.1 基本整数ISA的程序员模型
图2.1显示了基本整数ISA的非特权状态。对于RV32I,这32个x寄存器均为32位宽,即XLEN = 32。寄存器x0的所有位均等于0,为硬连线。通用寄存器x1-x31保存值,这些值被不同的指令解释为布尔值的集合或者是以二进制补码表示的有符号整数或无符号整数。
还有一个附加的非特权寄存器:程序计数器pc用来保存当前指令的地址。
图 2.1 RISC-V基本非特权整数寄存器状态
基本整数ISA中没有专用的堆栈指针或子例程返回地址链接寄存器。指令编码允许将任何x寄存器用于这些目的。但是,标准软件调用约定使用寄存器x1来保存调用的返回地址,而寄存器x5可用作备用链接寄存器。标准调用约定使用寄存器x2作为堆栈指针。
硬件可能会选择使用x1或x5来加速函数调用和返回。请参阅JAL和JALR指令的描述。
围绕x1是返回地址寄存器,x2是堆栈指针的假设,设计了可选的压缩16位指令格式。使用其他约定的软件可以正常运行,但可能具有更大的代码尺寸。
可用的架构寄存器的数量可能会对代码大小,性能和能耗产生很大影响。尽管对于运行编译后代码的整数ISA来说,可以说有16个寄存器是足够的,但在使用3地址格式的16位指令中无法将16个寄存器编码为完整的ISA。尽管2地址格式是可能的,但它会增加指令数量并降低效率。我们不希望使用中间指令大小(例如Xtensa的24位指令)来简化基本硬件实现,并且一旦采用32位指令大小,就可以直接支持32个整数寄存器。较大数量的整数寄存器还有助于提高高性能代码的性能,因为在这些代码中可以广泛使用循环展开,软件流水线和缓存切片。
由于这些原因,我们为基本ISA选择了常规大小的32个整数寄存器。动态寄存器的使用往往由一些经常访问的寄存器主导,并且可以优化寄存器文件的实现,以减少频繁访问的寄存器的访问能量[^22]。可选的压缩16位指令格式大多数情况下仅访问8个寄存器,因此可以提供密集的指令编码,而如果需要,其他指令集扩展可以支持更大的寄存器空间(平坦或分层)。
对于资源受限的嵌入式应用程序,我们定义了RV32E子集,该子集只有16个寄存器(第6章)。
2.2 基本指令格式
在基本RV32I ISA中,有四种核心指令格式(R/I/S/U),如图2.2所示。它们的长度均为固定的32位。基本ISA的IALIGN=32,意味着指令在内存中必须对齐到四字节边界上。如果目标地址不是IALIGN位对齐的,则在产生了分支或无条件跳转时生成指令地址未对齐异常。此异常在分支或跳转指令上报告,而不在目标指令上报告。对于未发生的条件分支,不会生成指令地址未对齐异常。
当添加具有16位长度或16位长度的其他奇数倍的指令扩展时(即IALIGN = 16),基本ISA指令的对齐约束将放宽到两个字节的边界。
在引起指令不对齐的分支或跳转上报告指令地址未对齐异常,有助于调试并简化IALIGN = 32的系统的硬件设计,在这些情况下,这是唯一可能发生不对齐的地方。
解码保留指令时的行为是未指定的。
某些平台可能要求保留供标准使用的操作码引发非法指令异常。其他平台可能允许保留的操作码空间用于非标准扩展。
RISC-V ISA将所有格式的源寄存器(rs1和rs2)和目标寄存器(rd)保持在同一位置,以简化解码。除了CSR指令中使用的5位立即数(第11章)外,立即数始终进行符号扩展,并且通常被打包到指令中最左边的可用位,并以降低硬件复杂性为原则进行分配。特别是,所有立即数的符号位始终位于指令的位31中,以加快符号扩展电路的速度。
图 2.2 RISC-V基本指令格式。每个立即数字段都用正在生成的立即数中位的位置(imm[x])进行标记,而不是像通常那样用指令的立即数字段中的位的位置来标记。
解码寄存器的指示符通常位于实现中的关键路径上,因此选择了这样的指令格式以将所有寄存器指示符在所有格式中都保持在相同位置,而不得不在格式之间移动立即数(这是与RISC-IV共享的属性,又名SPUR [11])。
实际上,大多数立即数或者很小或者需要所有XLEN位。我们选择了非对称的立即数拆分法(常规指令中为12位,再加上特殊的加载高20位立即数指令),以增加可用于常规指令的操作码空间。
立即数是符号扩展的,因为我们没有观察到像在MIPS ISA中那样对某些立即数使用零扩展的好处,而且希望使ISA尽可能简单。
2.3. 立即数编码变体
基于对立即数的处理,还有另外两种指令格式(B/J)变体,如图2.3所示。
S和B格式之间的唯一区别是,在B格式中12位立即数用于编码2的倍数的分支偏移。不像传统上那样在硬件中将指令编码中立即数的所有位左移一位,中间位(imm[10:1])和符号位保持在了固定位置,而S格式的最低位(inst[7])在B格式中编码为高阶位。
类似地,U和J格式之间的唯一区别是20位立即数向左移12位形成U立即数,向左移位1位形成J立即数。选择U和J格式立即数所在指令位的位置以最大程度地与其他格式以及彼此重叠。
图2.4显示了每种基本指令格式产生的立即数,并被标记为显示哪个指令位(inst[y])产生了立即数的每个位。
图 2.4 RISC-V指令产生的立即数类型。这些字段使用用于构造其值的指令位来标记。符号扩展位始终使用inst[31]。
符号扩展是对立即数最关键的操作之一(尤其是对于XLEN>32)。在RISC-V中,所有立即数的符号位始终保留在指令的第31位中,以允许符号扩展与指令解码并行处理。
尽管更复杂的实现可能为分支和跳转计算使用单独的加法器,因此不会因在指令类型之间保持立即数的位置恒定而受益,但我们希望降低最简单实现的硬件成本。通过旋转B和J立即数的指令编码中的位,而不是使用动态硬件多路复用器将立即数乘以2,我们将指令信号扇出和立即多路复用器成本降低了大约2倍。加扰的立即编码将使时间增加可忽略不计静态或提前编译。对于动态生成指令,会有一些小的额外开销,但是最常见的短前向分支具有直接的立即编码。
2.4. 整数计算指令
大多数整数计算指令对整数寄存器文件中保存的值的XLEN位进行运算。整数计算指令或者使用I型格式编码为寄存器-立即数操作,或者使用R型格式编码为寄存器-寄存器操作。寄存器-立即数指令和寄存器-寄存器指令的目的地均为寄存器rd。没有整数运算指令会导致算术异常。
我们没有在基本指令集中包含对整数算术运算的溢出检查的特殊指令集支持,因为很多溢出检查可以使用RISC-V分支廉价地实现。在无符号加法的溢出检查中,加法后只需要一个额外的分支指令即可:add t0, t1, t2; bltu t0, t1, overflow。
对于有符号加法,如果已知一个操作数的符号,则在加法之后仅需一个分支指令就可以进行溢出检查:addi t0,t1,+imm; blt t0,t1,overflow。这涵盖了使用立即数操作数加法的常见情况。
对于一般的有符号加法,在加法之后需要三条附加指令,以充分利用这样的观察:当且仅当另一个操作数为负时,总和应小于一个操作数。
add t0, t1, t2
slti t3, t2, 0
slt t4, t0, t1
bne t3, t4, overflow
在RV64I中,可以通过比较操作数上ADD和ADDW的结果来进一步优化32位带符号加法的检查。
整数寄存器-立即数指令
ADDI将以符号位扩展的12位立即数与寄存器rs1相加。算术溢出被忽略,结果只是加法结果的低XLEN位。ADDI rd,rs1,0用于实现MV rd,rs1汇编程序伪指令。
如果寄存器rs1小于以符号位扩展的立即数(两个都被视为带符号的数字),SLTI(置位如果小于立即数)就将寄存器rd置1,否则rd置0。SLTIU类似,但将值按无符号数比较(即立即数首先以符号位扩展为XLEN位,然后视为无符号数)。注意,如果rs1等于零,则SLTIU rd,rs1,1将rd设置为1,否则将rd设置为0(汇编程序伪指令SEQZ rd,rs)。
ANDI、ORI、XORI是对寄存器rs1和以符号位扩展的12位立即数执行按位与、或和异或的逻辑运算,并将结果写入rd。注意,XORI rd,rs1,-1 执行寄存器rs1的按位取反(汇编程序伪指令NOT rd,rs)。
常数移位被编码为特殊的I型格式。待移位的操作数在rs1中,而移位量被编码在I-立即数字段的低5位中。右移类型编码在第30位中。SLLI是逻辑左移(低位移入零)。SRLI是逻辑右移(零移入高位);SRAI是算术右移(原始符号位被复制到空出的高位)。
LUI(加载高位立即数)使用U型格式用于构建32位常量。LUI将U-立即数的值放在目标寄存器rd的高20位中,低12位填充零。
AUIPC(高位立即数与pc相加)使用U型格式用于构建pc相对地址。AUIPC从20位U-立即数构造32位的偏移量(低12位填充零),然后将此偏移量与AUIPC指令的地址相加,然后将结果写入寄存器rd。
AUIPC指令支持两种指令序列用来从当前PC地址访问任意偏移范围,以进行控制流传输和数据访问。一个AUIPC和一个JALR中12位立即数的组合可以将控制权转移到任何32位的相对于PC的地址,而一个AUIPC加上常规的加载或存储指令中的12位立即数可以访问任何32位的相对于PC的数据地址。
当前的PC地址可以通过将AUIPC指令中的U-立即数设置为0来获得。尽管JAL +4指令也可以用于获取本地PC(JAL之后的指令),但它可能会在比较简单的微架构中引起流水线中断或者在比较复杂的微架构中污染BTB结构。
整数寄存器-寄存器指令
RV32I定义了几种算术R型运算。所有操作都将读取rs1和rs2寄存器并作为源操作数,然后将结果写入寄存器rd。funct7和funct3字段选择操作的类型。
ADD将rs1和rs2相加。SUB从rs1中减去rs2。溢出被忽略,结果的低XLEN位写入rd。SLT和SLTU分别执行有符号和无符号比较,如果rs1<rs2,则将rd置1,否则置0。注意,如果rs2不等于零,则SLTU rd,x0,rs2将rd置1,否则将rd置零(汇编程序伪指令SNEZ rd,rs)。AND、OR和XOR执行按位逻辑运算。
SLL、SRL和SRA指令分别对寄存器rs1中的值执行逻辑左移、逻辑右移和算术右移,其移位量在寄存器rs2的低5位中。
NOP指令
NOP指令除了使pc向前推进并增加任何用到的性能计数器外不更改任何体系结构可见的状态。NOP编码为ADDI x0,x0,0。
NOP指令可用于将代码段与微架构上有意义的地址边界对齐,或为内联代码修改留出空间。尽管有许多可能的方法来编码NOP,但我们定义了标准的NOP编码以允许微架构的优化以及更有可读性的反汇编输出。其他的NOP编码可用于HINT指令(第2.9节)。
选择ADDI进行NOP编码是因为它最有可能在一系列系统中占用最少的资源来执行(如果未在解码中进行优化)。特别是,该指令仅读取一个寄存器。同样,ADDI功能单元更有可能在超标量设计中使用,因为加法是最常见的操作。特别是,地址生成功能单元可以使用和基址加偏移来进行地址计算所需的相同硬件来执行ADDI,而寄存器-寄存器类型的ADD或逻辑/移位操作则需要额外的硬件。
2.5. 控制转移指令
RV32I提供两种类型的控制转移指令:无条件跳转和条件分支。RV32I中的控制转移指令没有架构上可见的延迟槽。
无条件跳转
跳转并链接(JAL)指令使用J类型的格式,其中J-立即数编码一个2字节倍数的有符号的偏移量。偏移量以符号位扩展并与跳转指令的地址相加以形成跳转目标地址。因此,跳转指令可以指向±1 MiB的范围。JAL将跳转指令之后的指令地址(pc+4)存储到寄存器rd中。标准的软件调用约定使用x1作为返回地址寄存器,并使用x5作为备用链接寄存器。
备用链接寄存器支持调用millicode例程(例如,那些以压缩代码保存和恢复寄存器的例程),同时保留常规返回地址寄存器。寄存器x5被选为备用链接寄存器,因为它映射到标准调用约定中的临时寄存器,并且其编码与常规链接寄存器相比仅一位不同。
普通的无条件跳转(汇编程序伪指令J)被编码为rd=x0的JAL。
间接跳转指令JALR(跳转并链接寄存器)使用I型编码。将以符号位扩展的12位I-立即数与寄存器rs1相加,然后将结果的最低有效位置零,得到目标地址。紧跟在跳转指令之后的指令地址(pc+4)被写入寄存器rd。如果不需要结果,则可以将寄存器x0用作目的地。
无条件跳转指令均使用PC相对寻址来支持与位置无关的代码。JALR指令的定义可以使一个2指令序列在32位绝对地址范围内跳转到任意位置。首先LUI指令可以将目标地址的高20位加载到rs1,然后JALR可以加上地址的低位。同样,AUIPC和JALR可以跳转到32位的PC相对地址范围内的任意位置。
注意,与条件分支指令不同,JALR指令不会将12位立即数视为2字节的倍数。这避免了在硬件中实现另一种立即数格式。实际上,大多数JALR的用法要么立即数为零,要么与LUI或AUIPC配对使用,因此范围的略微减小并不显着。
在计算JALR目标地址时清除最低有效位不仅会略微简化硬件,而且允许将功能指针的低位用于存储辅助信息。尽管在这种情况下可能会略微丢失错误检查,但实际上跳到错误的指令地址通常会迅速引发异常(译者注:错过检查错误地址后下一个指令的地址也就错了并且会很快产生错误)。
当与基地址rs1=x0一起使用时,JALR指令可实现从地址空间中的任何地方进行单指令子例程调用最低2KiB或最高2KiB地址区域,这可用于实现对小型运行时库的快速调用。或者,ABI可以指定一个通用寄存器专门用于指向地址空间中其他位置的库。
如果目标地址未对齐IALIGN位的边界,则JAL和JALR指令将生成指令地址未对齐异常。
在IALIGN=16的机器(例如支持压缩指令集扩展C的机器)上不可能产生指令地址未对齐的异常。
返回地址预测堆栈是高性能指令预取单元的常见特性,但是需要准确检测用于过程调用和返回的指令才能有效。对于RISC-V,有关指令用法的提示通过使用的寄存器号进行隐式编码。仅当rd = x1/x5时,JAL指令才应将返回地址压入返回地址堆栈(RAS)。JALR指令应按表2.1所示压入/弹出RAS。
其他一些ISA在其间接跳转指令中添加了显式提示位,以指导返回地址堆栈操作。我们使用绑定到寄存器号的隐式提示和调用约定来减少用于这些提示的编码空间。
当两个不同的链接寄存器(x1和x5)指定为rs1和rd时,会同时弹出并压入RAS以支持协程。如果rs1和rd是相同的链接寄存器(x1或x5),则仅压入RAS以启用以下序列的宏运算融合:lui ra, imm20; jalr ra, imm12(ra) 和 auipc ra, imm20; jalr ra, imm12(ra)
条件分支
所有分支指令均使用B型指令格式。12位B-立即数将带符号的偏移量编码为2字节的倍数。偏移量以符号位扩展并与分支指令的地址相加得到目标地址。条件分支的范围为±4 KiB。
分支指令比较两个寄存器。BEQ和BNE产生分支的条件分别是寄存器rs1和rs2相等和不相等。BLT和BLTU产生分支的条件是rs1小于rs2(分别使用有符号和无符号进行比较)。BGE和BGEU产生分支的条件是rs1大于或等于rs2(分别使用有符号和无符号进行比较)。注意,BGT、BGTU、BLE和BLEU可以分别通过将BLT、BLTU、BGE和BGEU的操作数反转来合成。
可以用一条BLTU指令检查有符号数组的边界,因为任何负索引的比较结果都大于任何非负边界。(” />2.6. 加载和存储指令
RV32I是一种加载-存储体系结构,其中只有加载和存储指令才能访问内存,而算术指令只能在CPU寄存器上运行。RV32I提供了一个按字节寻址的32位地址空间。EEI将定义对于哪些指令来说哪些地址空间可以合法访问(例如,某些地址可能是只读的,或仅支持字访问)。目的地为x0的加载指令即使加载值会被丢弃,也仍然必须引发任何异常并引起其他任何副作用。
EEI将定义存储系统是小端还是大端。在RISC-V中,字节序是字节地址不变的。在字节地址不变的字节序系统中,以下属性成立:如果某个字节以某个字节序存储到某个地址的内存中,则在任何字节序中(译者注:即存储的字节序)从该地址进行的字节大小的加载动作将返回存储的值。
在小端字节序配置中,多字节存储将最低有效的寄存器字节写入最低的存储器字节地址,然后按其位置的升序依次写入其他寄存器字节。类似地加载动作将较低存储器字节地址的内容传输到较低的寄存器字节。
在大端字节序配置中,多字节存储将最高位置的寄存器字节写入最低的存储器字节地址,然后按位置的降序依次写入其他寄存器字节。类似地加载动作将较高的存储字节地址的内容传输到较低的寄存器字节。
加载和存储指令在寄存器和存储器之间传输值。加载指令以I型格式编码,存储指令为S型。最终的地址是通过将寄存器rs1与以符号位扩展的12位偏移量相加而获得的。加载指令从内存复制一个值到寄存器rd中。存储指令复制寄存器rs2中的值到内存。
LW指令从存储器加载一个32位值到rd。LH从内存中加载一个16位值,然后在存储到rd中之前将其以符号位扩展到32位。LHU从内存中加载一个16位的值,但是在存储到rd之前,以零扩展到32位。LB和LBU类似地定义为8位值。SW、SH和SB指令将寄存器rs2的低位存储到存储器中,分别存储32位、16位和8位值。
无论EEI如何,有效地址自然对齐的加载和存储操作都不会引发地址未对齐的异常。加载和存储有效地址未自然地与引用的数据类型对齐的地方(即有效地址不能被访问的字节大小所整除)的行为取决于EEI。
EEI可能保证完全支持未对齐的加载和存储操作,因此在执行环境中运行的软件将永远不会遇到包含或致命的地址未对齐陷阱。在这种情况下,未对齐的加载和存储可以用硬件处理,也可以通过一个不可见的陷阱进入到执行环境实现中,也可以根据地址将硬件和不可见的陷阱结合起来处理。
EEI可能无法保证未对齐的加载和存储会被无形地处理。在这种情况下,未自然对齐的加载和存储操作可能会成功执行或者引发异常。引发的异常可以是地址未对齐异常(address-misaligned exception)或者是访问错误异常(access-fault exception)。对于除了不对齐方式以外可以完成的内存访问,如果不应模拟未对齐的访问(例如,如果对内存区域的访问有副作用),则可以引发访问异常而不是地址未对齐的异常(?)。当EEI不能保证未对齐的加载和存储被不可见地处理时,则EEI必须定义地址不对齐引起的异常将导致包含陷阱(允许在执行环境中运行的软件处理该陷阱)还是致命陷阱(终止执行)。移植旧代码时,偶尔会需要不对齐的访问,并且当使用任何形式的packed-SIMD扩展或处理外部的压缩数据结构时,不对齐的访问都会帮助提高应用的性能。我们允许EEI通过常规的加载和存储指令选择支持未对齐的访问,目的是简化对未对齐硬件支持的添加。一种选择是禁止基本ISA中的未对齐访问,然后为未对齐访问提供一些单独的ISA支持,或者是帮助软件处理未对齐访问的特殊指令,或者是为未对齐访问提供一种新的硬件寻址模式。特殊指令很难使用,会使ISA复杂化,并且经常会添加新的处理器状态(例如SPARC VIS对齐地址偏移寄存器)或使现有处理器状态的访问复杂化(例如MIPS LWL/LWR部分寄存器写)。此外,对于面向循环的压缩SIMD代码,操作数未对齐时的额外开销促使软件根据操作数对齐情况提供多种形式的循环,这使代码生成变得复杂,并增加了循环启动开销。新的未对准硬件寻址模式在指令编码中占用相当大的空间,或者需要非常简化的寻址模式(例如,仅寄存器间接寻址)。
即使当未对齐的加载和存储能成功完成时,根据实现的不同,这些访问也可能运行得非常慢(例如,通过不可见的陷阱实现时)。此外,尽管自然对齐的加载和存储指令能够保证原子执行,但是未对齐的加载和存储可能无法保证,因此需要额外的同步来确保原子性。
我们不对未对齐的访问强制要求原子性,因此执行环境实现可以使用不可见的机器陷阱和软件处理程序来处理部分或所有未对齐的访问。如果提供了硬件未对齐的支持,则软件可以通过简单地使用常规加载和存储指令来利用它。然后,硬件可以根据运行时地址是否对齐来自动优化访问。
2.7. 内存排序指令
FENCE指令用于对由其他RISC-V harts和外部设备或协处理器看到的设备I/O和内存访问进行排序。设备输入(I)、设备输出(O)、存储器读(R)和存储器写(W)的任何组合可以相对于它们的任何组合来排序。非正式地说,其他RISC-V hart或外部设备在FENCE之前的前序集合中的任何操作之前都不能观察到FENCE之后的后序集合中的任何操作。第17章提供了对RISC-V存储器一致性模型的精确描述。
FENCE指令同样会对hart的存储器读写操作进行排序,就如观察到的外部设备发起的存储器读写操作一样(?)。但是,FENCE指令不会对由外部设备通过其它信号通知机制生成的事件进行排序。设备可能会通过一些外部通信机制(例如,某个存储器映射的控制寄存器可以驱动中断信号到中断控制器)观察到对存储器位置的访问。这种通信在FENCE排序机制的范围之外,因此FENCE指令不能保证中断信号的变化何时对中断控制器可见。特定设备可能提供额外的排序保证以减少软件开销,但这些超出了RISC-V存储器模型的范围。
EEI将定义可能的I/O操作,特别是,当通过加载和存储指令访问时,哪些内存地址将被作为设备输入和设备输出操作而不是内存读取和写入操作进行处理和排序。例如,通常使用不缓存的加载和存储来访问内存映射的I/O设备,这些加载和存储使用I和O位而不是R位和W位进行排序。指令集扩展可能还会描述新的I/O指令,这些指令也将使用FENCE中的I和O位进行排序。
屏障模式字段fm定义了FENCE的语义。fm=0000的FENCE指令保证其前序集合中的所有存储器操作在其后序集合中的所有存储器操作之前进行。
FENCE.TSO指令编码为一个FENCE指令,其中fm=1000,predecessor=RW,successor=RW。FENCE.TSO保证其前序集合中的所有加载操作在其后序集合中的所有存储器操作之前,并且保证前序集合中的所有存储操作在后序集合中的所有存储操作之前。这使得FENCE.TSO的前序集合中的non-AMO存储操作与其后序集合中的non-AMO加载操作不保证顺序。由于FENCE RW,RW的保序要求是FENCE.TSO保序要求的超集,因此忽略fm字段并将FENCE.TSO按照FENCE RW,RW来实现也是正确的。
FENCE指令中未使用的字段(rs1和rd)保留用于将来扩展中的细粒度屏障。为了向前兼容,基本的实现应忽略这些字段,而标准软件应将这些字段置零。同样,表2.2中的很多fm和predecessor/successor设置也保留供将来使用。基本的实现应将所有此类保留配置都视作fm=0000的普通屏障指令,而标准软件应仅使用非保留的配置。
*我们选择了一种宽松的内存模型,以通过简单的机器实现以及未来可能的协处理器或加速器扩展实现高性能。我们将I/O排序与内存R/W排序分开,以避免在设备驱动程序中进行不必要的序列化,并且还支持其他非内存路径来控制添加的协处理器或I/O设备。简单的实现可能会额外忽略前任和后继领域,并始终对所有操作执行保守的原则。
我们选择了一个宽松的内存模型,以便从简单的机器实现和未来可能的协处理器或加速器扩展中获得高性能。我们将I/O排序与内存R/W排序分开,以避免设备驱动程序hart中不必要的串行化,并支持替代的非内存路径来控制添加的协处理器或I/O设备。简单的实现可能会额外忽略predecessor和successor字段,并始终对所有操作执行保守的屏障。2.8. 环境调用和断点
SYSTEM指令用于访问可能需要特权访问的系统功能,并使用I型指令格式进行编码。它们可以分为两个主要类别:原子地读取、修改并写入控制和状态寄存器(CSR),以及所有其他潜在的特权指令。第11章介绍了CSR指令,后面的章节介绍了基本的非特权指令。
SYSTEM指令的定义允许较简单的实现始终将其捕获到单个软件陷阱处理程序中。更复杂的实现可能会在硬件中针对每个系统指令执行更多。
这两条指令引起对支持的执行环境的精确请求陷阱。
ECALL指令用于向执行环境发出服务请求。EEI将定义如何传递服务请求的参数,但通常将这些参数定义到整数寄存器文件中的指定位置。
EBREAK指令用于将控制权返回到调试环境。ECALL和EBREAK以前被称为SCALL和SBREAK。这些指令具有相同的功能和编码,但是经过重命名以反映它们可以比调用监督级操作系统或调试器更广泛地使用。
EBREAK主要设计用于调试器,以使执行停止并退回到调试器中。标准的gcc编译器还使用EBREAK标记不应执行的代码路径。
EBREAK的另一种用途是支持“半主机”,其中执行环境包括调试器,该调试器可以通过围绕EBREAK指令构建的备用系统调用接口提供服务。因为基于RISC-V的ISA不能提供多个EBREAK指令,所以RISC-V半主机使用特殊的指令序列来区分半主机EBREAK和调试器插入的EBREAK。
slli x0, x0, 0x1f # Entry NOP
ebreak # Break to debugger
srai x0, x0, 7 # NOP encoding the semihosting call number 7
请注意,这三个指令必须是32位宽的指令,也就是说,它们一定不能属于第18章所述的压缩16位指令。
移位NOP指令仍然被认为可以用作HINTs。
半主机是服务调用的一种形式,可以使用现有的ABI更自然地编码为ECALL,但这将要求调试器能够拦截ECALL,这是调试标准的新增功能。我们打算将ECALL与标准ABI结合使用,在这种情况下,半主机可以与现有标准共享服务ABI。
我们注意到,在较新的设计中,ARM处理器也已转向使用SVC而不是BKPT进行半主机调用。2.9. 提示指令
RV32I为HINT指令保留了很大的编码空间,这些指令通常用于将性能提示传达给微体系结构。和NOP指令一样,HINTs指令不会改变任何架构上可见的状态,只会增加指令pc以及任何应用的性能计数器。实现总是允许忽略编码的提示指令。
大多数RV32I HINTs被编码为rd=x0的整数计算指令。其它的RV32I HINTs编码为FENCE指令,其中前序集合或者后序集合为空且fm=0。选择这样的HINT编码是为了使简单的实现可以完全忽略HINTs,并且可以将HINT作为常规的指令执行,而者恰好不会改变架构状态。例如,如果目标寄存器为x0,则ADD为HINT;五位的rs1和rs2字段编码HINT参数。但是,一个简单的实现可以简单地将HINT作为写入x0的rs1和rs2的ADD来执行,这不会影响架构上可见的效果。
另外一个例子,一个pred字段为0且fm字段为0的FENCE指令是一个HINT;succ、rs1和rd字段编码HINT参数。一个简单的实现可以简单地将HINT作为FENCE来执行,而此FENCE对内存访问前序空集合以及succ字段编码的后序内存访问集合进行排序。由于前序集合和后续集合的交集为空,因此该指令没有强制内存排序,且不会有架构上可见的效果。表2.3列出了所有的RV32I HINT代码点。91%的HINT空间是为标准HINTs保留的。HINT空间的其余部分保留给自定义HINTs:在此子空间中不会定义任何标准HINTs。
我们预计标准提示最终将包括内存系统的空间和时间局部性提示、分支预测提示、线程调度提示、安全标签以及用于仿真/模拟的工具标记。