语言《RISCV指令集介绍与汇编语言:计基2》

和学校里学的x86架构不同,RISC-V指令格式的设计十分简洁、高效。为了在下一节课能够更好地理解如何搭建CPU,首先需要对RISC-V指令集有基本的了解。该文章大部分图片来自彭东老师的计算机基础实战

什么是指令集?

先来看一个问题,什么是指令集?或者说,什么是指令?

我们都知道,CPU是基于晶体管、电阻、电容等基本元器件所实现的集成电路,那么实际上它是如何工作的呢?

抽象成数字电路来看,当我们给CPU的一些指定端口传入**“有意义”的高低电平时,CPU内部的逻辑电路就会按照“事先设计的规则”进行运转,并在一些端口输出最终“计算的结果”**。用一幅图来描述这个过程:

img

上图“有意义的高低电平”就是所谓的指令,而右侧的端口输出就是执行这一指令所得到的结果。为便于理解,我们想象一个极其简易的CPU(说CPU都抬举它了···),它有三个输入端口,一个输出端口,其内部只有一个双路选择器,功能也很简单,根据指定的输入高低电平,选择某一路输入端口的数据作为输出,如下:

img

我们约定,其中一个输入端口In0用来控制双路选择器:如果In0端口是低电平0,则双路选择器接通In1,输出端口Out0的值就是In1,反之则反

例如,我们输入100(这是二进制表示,最后的0表示In0),则Out0输出0;如果输入101,则Out0输出1.

前面所说的100和101就是一个极其简易的指令。

相信到这,你应该知道指令到底是个什么东西了吧,它其实有两个鲜明的特征:事先约定、CPU实现

也就是说这些指令是在搭建CPU之前就已经约定好的,而后实现的CPU必须按照指令来实现相应的功能。

**那什么是指令集呢?**当CPU内部逻辑电路比上面所说的双路选择器复杂成千上万倍时,它可以完成各种各样的功能,自然也需要多种多样的指令,这些指令共同构建了一个指令集

现在较为常见的指令集主要有Intel的x86指令集(微机原理所学的8086微处理器正是基于此)、AMD64指令集、ARM、RISC-V等等。

另外,按照指令集的复杂与否,可以将指令集分为两类:CISC(Complex Instruction Set Computers )和RISC(Reduced Instruction Set Computer)。两者的区别主要在于,寻址方式是否复杂、指令编码是否统一等等,更详细的资料可以参考CISC与RISC

有意思的是,现在的RISC与CISC其实两者都是一种优势互补的姿态,比如CISC的代表x86体系其实早就开始“偷学”了:它表面的指令集并没有变,但是在CPU内部进行译码时,会将指令解析成多条内部微码,而这写内部微码与RISC有许多相似的地方。

RISC-V指令集

上面简单讲了指令集是什么与RISC、CISC的区别,下面我们专注于其中一个指令集RISC-V。

RISC最初起源于加州伯克利分校的一个4人团队,RISC-V是研究团队在2010年推出的第五代RISC体系,读作“risk-five”,由于它的免费、开源和高效,RISC-V很快便引来越来越多的科技巨头的加入。

基础指令与扩展指令

RISC-V的指令集由两部分构成:基础指令与扩展指令。如下图所示:

img

其中,根据寄存器位宽和地址空间不同,分为32、64、128位三种不同整数指令集(用I表示)。整数指令集包括算术、逻辑、分支、访存(访问内存)指令等,已经可以实现一个完整的软件栈。

如果一些CPU有更多的功能要求,可以在基础指令的基础上组装扩展指令,扩展指令主要有以下这些:

  • M:乘除法、取模求余指令
  • F:单精度浮点指令
  • D:双精度浮点指令
  • Q:四倍浮点指令
  • A:原子操作指令,例如常见的cas(compare and swap)指令
  • C:压缩指令,主要用于改善程序大小
  • G:= I+M+A+D+F,表示通用处理器所包含的指令集
  • 其他可参考:RISC-V官方手册
  • 通常使用RISC-V指令集的CPU通常采用这样的命名方式:RV【位宽】【支持的指令】,例如RV32I表示基于32位整数指令构建的;而RV64IMAC表示在64位整数指令集上增加了乘除法、原子、压缩指令集。

    寄存器

    按理来说接下来应该继续讲解RISC-V指令集的具体指令格式,但是指令格式和寄存器联系紧密,必须先对寄存器构成有基本的了解。

    RISC-V定义了32个通用寄存器和一个PC寄存器(看到这可以先想想8086的寄存器类型,区别很明显),寄存器的位宽和指令集位宽匹配。下图列出了32个寄存器的ABI名称和功能说明:

    img

    表中的 ABI 全称为 Application Binary Interface,即应用程序二进制接口,可以理解为寄存器别名,在高级语言在生成汇编语言的时候会用到它们。

    OK,继续讲指令格式!

    指令格式

    我们先挑选最基础的RV32I来看看它的指令命令方式,值得注意的是,RV32I包含的指令是固定、永远不会改变的,相应指令的取名方式如下图

    img

    可以看到,RV32I指令集中的指令命名方式有很明显的特点:由英文首字母拼成。如branch equal缩写位be,表示当条件相等时进行分支跳转。之后的章节中遇到不动的指令都可以跳转到这进行查看

    实际上,RISC-V指令根据格式特点可以分为六种类型(Type):

  • R Type:用于寄存器——寄存器之间的操作 (Register)
  • I Type:短立即数及内存访问操作(Immediate)
  • S Type:用于内存store操作 (Store)
  • B Type:用于条件跳转操作 (Branch)
  • U Type:用于长立即数操作
  • J Type:用于无条件跳转操作 (Jump)
  • 它们的指令格式如下图所示

    img

    上图opcode表示指令操作码,通过这7位就知道这是一个什么指令;rs1、rs2、rd分别表示源寄存器1、2以及目的寄存器;imm代表立即数;funct3、funct7代表指令对应的功能,这在之后会讲。

    仔细观察上图的指令格式可以发现:三个寄存器都固定在指令同样的位置,这为指令译码提供了便利。

    接下来我们就来看看RV32I中立即数的算术逻辑指令(I-Type)长什么样子,如下图

    img

    首先根据指令命名方式判断左侧的含义,从上到下依次为,立即数与源寄存器相加addi、寄存器小于立即数slti、寄存器小于立即数无符号版本sltiu、立即数与源寄存器异或xori、立即数与源寄存器按位或ori、立即数与源寄存器按位与andi、寄存器根据立即数逻辑右移slli、寄存器根据立即数逻辑右移srli、寄存器根据立即数算术右移srai,不清楚的朋友可以回到上文去查看英文全称。

    顺带一提,左移(不管是什么左移)会在末尾补0,算术右移在最高位补充符号位,而逻辑右移在最高位补充0。为什么要区分逻辑右移和算术右移呢?这是从计算的角度思考的,无论正数负数,右边加0都等同于乘以2;而负数进行逻辑右移的结果不等于除以2,需要用算术右移;但如果只有算术右移,无符号数的运算受到影响,因此需要逻辑右移。

    最右侧的“0010011”就是之前提到的指令操作码。

    寄存器与寄存器的操作的指令格式如下,区别仅在于立即数部分被源寄存器所替换:

    img

    有关内存如何访写(load and store)、分支跳转(有条件/条件)等强烈建议观看原文:计算机基础实战5

    汇编指令一览

    指令格式的讲解比较抽象,因为已经涉及计算机底层表达了,为了方便大家的理解,找到了一篇RISC-V指令汇编指令的总结,相信通过这部分内容会对前文的内容有更深刻的理解,同时,为接下来的手写CPU提供助力!

    原文链接:RISCV常见指令

    算术运算

  • add rd,rs1,rs2
    :将寄存器rs1与rs2的值相加并写入寄存器rd。
  • sub rd,rs1,rs2
    :将寄存器rs1与rs2的值相减并写入寄存器rd。
  • addi rd,rs1,imm
    :将寄存器rs1的值与立即数imm相加并存入寄存器rd。
  • mul rd,rs1,rs2
    :将寄存器rs1与rs2的值相乘并写入寄存器rd。
  • div rd,rs1,rs2
    :将寄存器rs1除以寄存器rs2的值,向零舍入并写入寄存器rd。
  • rem rd,rs1,rs2
    :将寄存器rs1模寄存器rs2的值并写入寄存器rd。
  • 逻辑运算

  • and rd,rs1,rs2
    :将寄存器rs1与rs2的值按位与并写入寄存器rd。
  • andi rd,rs1,imm
    :将寄存器rs1的值与立即数imm的值按位与并写入寄存器rd。
  • or rd,rs1,rs2
    :将寄存器rs1与rs2的值按位或并写入寄存器rd。
  • ori rd,rs1,imm
    :将寄存器rs1的值与立即数imm的值按位或并写入寄存器rd。
  • xor rd,rs1,rs2
    :将寄存器rs1与rs2的值按位异或并写入寄存器rd。
  • xori rd,rs1,imm
    :将寄存器rs1的值与立即数imm的值按位异或并写入寄存器rd。
  • 移位运算

  • sll rd,rs1,rs2
    :将寄存器rs1的值左移寄存器rs2的值这么多位,并写入寄存器rd。
  • slli rd,rs1,imm
    :将寄存器rs1的值左移立即数imm的值这么多位,并写入寄存器rd。
  • srl rd,rs1,rs2
    :将寄存器rs1的值逻辑右移寄存器rs2的值这么多位,并写入寄存器rd。
  • srli rd,rs1,imm
    :将寄存器rs1的值逻辑右移立即数imm的值这么多位,并写入寄存器rd。
  • sra rd,rs1,rs2
    :将寄存器rs1的值算数右移寄存器rs2的值这么多位,并写入寄存器rd。
  • srai rd,rs1,imm
    :将寄存器rs1的值算数右移立即数imm的值这么多位,并写入寄存器rd。
  • 内存访问与写入

  • lb rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读一个字节,符号扩展后存入rd
  • lh rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读半个字,符号扩展后存入rd
  • lw rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读一个字,符号扩展后存入rd
  • lbu rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读一个无符号的字节,零扩展后存入rd
  • lhu rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读半个无符号的字,零扩展后存入rd
  • lwu rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读一个无符号的字,零扩展后存入rd
  • sb rs1,offset(rs2)
    :把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的8位
  • sh rs1,offset(rs2)
    :把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的16位
  • sw rs1,offset(rs2)
    :把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的32位
  • 举个例子,有如下C语言片段:

    long long A[100];
    A[10] = A[3] + a;
    

    假设数组A首地址在寄存器x3内,a在x2内,则这段代码的汇编表达为:

    ld x10,24(x3)       # long long占64bits=8bytes,A[3]的地址为A[0]+3*8
    add x10,x2,x10
    sd x10,80(x3)
    

    比较指令

    有符号数:

  • slt rd,rs1,rs2
    :若rs1的值小于rs1的值,rd置为1,否则置为0
  • slti rd,rs1,imm
    :若rs1的值小于立即数imm,rd置为1,否则置为0
  • 无符号数:

  • sltu rd,rs1,rs2
    :若rs1的值小于rs1的值,rd置为1,否则置为0
  • sltiu rd,rs1,imm
    :若rs1的值小于立即数imm,rd置为1,否则置为0
  • 条件跳转

  • beq rs1,rs2,lable
    :若rs1的值等于rs2的值,程序跳转到lable处继续执行
  • bne rs1,rs2,lable
    :若rs1的值不等于rs2的值,程序跳转到lable处继续执行
  • blt rs1,rs2,lable
    :若rs1的值小于rs2的值,程序跳转到lable处继续执行
  • bge rs1,rs2,lable
    :若rs1的值大于等于rs2的值,程序跳转到lable处继续执行
  • 注意,在汇编中没有括号{}来控制代码作用区域,只能通过label标签来表示要跳转的指令行,类似于C语言中的goto

    无条件跳转

  • j label
    :程序直接跳转到lable处继续执行
  • jal rd,label
    :用于调用函数,把下一条指令的地址保存在rd中(通常用x1),然后跳转到label处继续执行
  • jalr rd,offset(rs)
    :可用于函数返回,把下一条指令的地址存到rd中,**然后跳转到rs+offset地址处的指令继续执行。**若rd=x0就是单纯的跳转(x0不能被修改)
  • 其他

    思考题

    1. 想想看,为什么要通过调整立即数的某些位,从 U-TYPE 指令得到 J-TYPE 指令格式呢?这样调整以后有什么好处?

    试图回答:直接在寄存器内部调整指令,减少了指令读取事件,大大加快了指令执行的整体效率。

    推荐阅读

    计算机基础实战

    RISC-V指令集

    RISC-V处理器与片上系统设计

    ,减少了指令读取事件,大大加快了指令执行的整体效率。

    推荐阅读

    计算机基础实战

    RISC-V指令集

    RISC-V处理器与片上系统设计

    RISC与CISC的思考

    物联沃分享整理
    物联沃-IOTWORD物联网 » 语言《RISCV指令集介绍与汇编语言:计基2》

    发表评论