计基2—riscv指令集介绍与汇编(代码片段)

苍山有雪,剑有霜 苍山有雪,剑有霜     2022-12-07     465

关键词:

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

什么是指令集?

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

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

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

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

我们约定,其中一个输入端口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的指令集由两部分构成:基础指令与扩展指令。如下图所示:

其中,根据寄存器位宽和地址空间不同,分为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名称和功能说明:

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

OK,继续讲指令格式!

指令格式

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

可以看到,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)

它们的指令格式如下图所示

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

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

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

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

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

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

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

有关内存如何访写(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 long64bits=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的思考

arm体系结构和汇编指令(代码片段)

...节奏运行2.CPU可以通过总线读取外部存储设备中的二进制指令集,然后解码执行3.这些可以被CPU解码执行的二进制指令集是CPU设计的时候确定的,是CPU的设计者(ARM公司)定义的,本质上是一串由1和0组成的数字。这就是CPU的汇编指... 查看详情

深入研究clang(十九)clang的riscv支持2(代码片段)

前文Clang的RISCV支持1介绍了Clang中有关RISCV的代码主要集中在三个地方:Driver部分、Basic部分和CodeGen部分,并且对Basic部分和CodeGen部分的内容和关系进行了介绍,只有Driver部分因为涉及到了ToolChain、Tool和Command(job)等概... 查看详情

arm汇编简单介绍(代码片段)

...*/  ;单行注释@ 2. 符号说明:  1)汇编指令,一条指令对应一个机器码,完成一定的功能  2)伪指令,一条指令对应多条机器码,完成一个稍微复杂的功能  3)伪操作,不会生成机器码,为了协助编译器... 查看详情

汇编程序基本指令集(代码片段)

指令概述指令指令是CPU操作的基本单位,每条指令执行一个特定的操作。可以理解为:指令通知CPU执行某种操作的“命令”。CPU全部指令的集合,称为指令集指令分类机器指令:二进制格式编码的序列(一串0,1代码书写)。注意:硬件只... 查看详情

mips汇编指令集(代码片段)

MIPS汇编MIPS指令集MIPS指令集属于精简指令集MIPS的所有指令都是32位,指令格式简单,而X86的指令长度不是固定的。简单的指令和格式易于译码和流水线操作,但是代码密度不高,导致二进制文件大MIPS有32个通用寄... 查看详情

arm汇编基础上(代码片段)

ARM是一个精简指令集处理器,其指令集的设计是定长的,也就是其汇编对应的机器码是定长的(2字节或者4字节)。那么对于定长而言,其优点就是更快的被执行,因为这样CPU取指令译码的速度相对x86的CPU... 查看详情

lc-3汇编语言指令集(代码片段)

LC-3汇编语言指令集LC-3汇编语言运算类指令ADD(addition)AND(Bit-wiselogicalAND)NOT(Bit-wisecomplement)LD(load)ST(store)LDI(loadindirect)STI(storeindirect)LDR(loadbase+offset)STR(storebase+offset)LEA(loadeffectiveaddress)BR(conditionalbranch)JMP(jump)RET(return)JSR(jumptos... 查看详情

计算机系统5-;计组与体系结构2|mips指令集(上)|指令系统

介绍指令集的指令格式、寻址方式、指令类型,介绍了一些著名的指令集。介绍指令集的指令格式、寻址方式、指令类型,介绍了一些著名的指令集。 查看详情

微架构指令集架构与汇编语言的关系

...(大部分内容来自维基百科,如有错误谢指正!)微架构、指令集架构和汇编语言这三者的关系大概是这样的,我们分别来介绍下指令集指令就是要计算机执行某种操作的命令。从计算机组成的层次结构来说 指令分为微指令(微... 查看详情

深入研究clang(十九)clang的riscv支持2(代码片段)

前文Clang的RISCV支持1介绍了Clang中有关RISCV的代码主要集中在三个地方:Driver部分、Basic部分和CodeGen部分,并且对Basic部分和CodeGen部分的内容和关系进行了介绍,只有Driver部分因为涉及到了ToolChain、Tool和Command(job)等概... 查看详情

二进制分析实战:x86汇编快速入门(代码片段)

因为汇编语言是二进制文件中机器指令的标准表示形式,许多二进制分析都基于反汇编,所以读者必须熟悉x86汇编语言的基础知识,才能从本书中获得最大收获。本附录将为你介绍汇编语言的基础知识。本附录的目的... 查看详情

计算机组成原理与接口技术笔记(代码片段)

...机中的信息存储第二章MIPS汇编语言1.计算机语言2.计算机指令架构CISC(复杂指令集计算机)RISC(精简指令集计算机)两种架构的对比3.MIPS汇编指令概述MIPS指令结构MIPS操作数类型MIPS指令类型4.MIPS指令操作数寄存器... 查看详情

精简指令集和复杂指令集及指令格式(代码片段)

5.1介绍CISC:复杂指令集RISC:精简指令集5.1.1CPU模型复杂指令集和精简指令集取决于CPU中的控制器的NN=111(8051)复杂指令集N=34(ARM)精简指令集SWAP(1)<--->MOV(3)2/8定律5.1.2编程语言编程语言分为编译型和解释行编译型:... 查看详情

thumb指令集程序示例(代码片段)

...ARM状态,一种Thumb状态。 本节课主要介绍Thumb状态及Thumb指令集。在012_relocate的程序基础上修改,创建013_thumb_014_003程序,并打开start.S和Makefile代码。1.对Makefile文件进行如下修改。 1all:led_on2.ouart.oinit.omain.ostart.o23#arm 查看详情

jvm学习笔记内存与垃圾回收篇(代码片段)

...半解释的。有些需要反复用到的字节码是直接编译成机器指令来执行的。就好比域名的解析,常用访问的域名解析是直接通过本地域名服务器,不常用的才会自顶向下层层解析。例如for循环中的代码是要重复利用的,... 查看详情

jvm学习笔记内存与垃圾回收篇(代码片段)

...半解释的。有些需要反复用到的字节码是直接编译成机器指令来执行的。就好比域名的解析,常用访问的域名解析是直接通过本地域名服务器,不常用的才会自顶向下层层解析。例如for循环中的代码是要重复利用的,... 查看详情

idaproarm指令集和thumb指令集的切换(代码片段)

... 类似下面这样: B6FC7DD0明显反汇编错了,成SVCMI指令了,解决方法也很简单,这里记录一下!在IDA中可以Edit->segments->ch 查看详情

riscv指令

...0,其它的31个寄存器为普通的通用整数寄存器。RiscV汇编指令:RiscV指令主要有以下几种类型:rd:是目的寄存器,registerdestination,rs是源寄存器,registersource。7-11,共5bits,可以表示32个x寄存 查看详情