跳转至

RISC-V汇编&指令系统

基本概念

  • 指令系统(指令集,IS:Instruction Set)
  • 指令集系统架构(ISA)
    • 简称架构,也可以称为:处理器架构、指令集体系结构
    • 包含了程序员正确编写二进制机器语言程序所需的全部信息
    • 例如:如何使用硬件、指令格式、操作种类、操作数所能存放的寄存器组和结构,包括每个寄存器名称、编号、长度、用途
  • 系列机
    • 基本指令系统相同,基本系统结构相同的计算机
    • 实际是为了解决软件兼容的问题,给定一个ISA,可以有不同的实现方式;例如:AMD/Intel CPU都是X86-64指令集,ARM ISA也有不同的实现方式
    • IBM 360是第一个将ISA与其实现分离的系列机

指令集架构

  • ISA——抽象层,软件子系统与硬件子系统的桥梁和接口

img

\[ ISA功能\quad\begin{cases} 数据类型\\存储模型\\软件可见的处理器状态\\指令集\\系统模型\\外部接口\\\dots \end{cases} \quad ISA特性\quad\begin{cases} 成本和资源占用低\\简洁性:指令规整简洁\\架构和具体实现分离\\可扩展\\易于编程\\性能好\\\dots \end{cases} \]

ISA位宽

指的是通用寄存器的宽度,决定了寻址范围的大小,数据运算能力的强弱

ISA位宽和指令编码长度不一定相等,在64位架构中,也存在大量的16位编码

存储器寻址

指的是:处理器根据指令中给出的地址信息来寻找物理地址

  • 1980年以来几乎所有的机器的存储器都是按字节编址
  • 一个存储器地址可以访问
    • 1个字节,2个字节,4个字节……
  • 不同体系结构对字的定义是不同的
    • 16位字(Intel X86),32位字(MIPS,RISC-V)
  • 如何将字节地址映射到字地址(尾端问题)
  • 一个字是否可以存放在任何字节边界上(对齐问题)

尾端问题

指的是:在一个(双)字内部的字节顺序问题

例如:地址addr存储的字为0x89ABCDEFaddraddr+1addr+2addr+4四个字节分别存放的数据是什么?

addr+3 addr+2 addr+1 addr
小端 89 AB CD EF
大端 EF CD AB 89

对齐问题

  • 假设对s个字节长的对象访问地址为A,如果A mod s=0 ,称为边界对齐
  • 边界对齐的原因是存储器本身读写的要求,存储器本身读写通常就是边界对齐的,对于没有边界对齐的对象的访问,可能会导致两次访问或异常

寻址方式

指的是:通过指令中的操作数(不同方式)计算出地址

有效地址:由寻址方式说明的某一存储单元的实际存储器地址,有效地址

操作数类型、表示

面向应用、软件系统所处理的各种数据类型

类型由操作码决定或者数据附加硬件解释的标记(现已弃用)

  • 操作数在机器中的表示
    • 整型:原码、反码、补码、移码
    • 浮点:IEEE 754标准
    • 十进制:BCD码(二进制十进制表示)
    • ASCII character = 1 byte (64位寄存器能存8个ASCII字符)
    • ……

汇编语言(ASM)

  • 如RISC-V
  • 是一种低级编程语言
  • 不同的架构实际上具有一组支持的不同操作

主流架构

  1. Intel x86
  2. ARM
  3. RISC-V

  4. 早期趋势

    • 进行复杂的指令集计算
  5. 当前
    • 创建精简的指令集
  6. RISC-V介绍
    • 由UCB创建的第五代RISC的开源指令集规范
    • 适用于嵌入式的所有级别计算

指令语法

  • 包含一个操作码和三个操作数(op,dst,src1,src2)
    • op:操作助记符
    • dst:目标寄存器
  • 每一行仅允许执行一条指令
  • 每一条指令只有一个操作
  • “#”用于注释
  • C语言中的一条指令可能要拆分为多条汇编语言实现
# Fibonacci Sequence
main: add  t0, x0,  x0
        ↑   ↑   ↑    ↑
       op  dst src1 src2
# 将两个寄存器相加,将结果存在dst中

汇编指令操作对象

  • 寄存器
    • 32个通用寄存器:x0~x31(仅涉及RV321的通用寄存器组)
    • 在RISC-V中,算术逻辑运算所操作的数据必须直接来自寄存器
    • x0是一个特殊寄存器,只用于全零
    • 每个寄存器都有别名便于区别,实际硬件没有区别
    • RV321指令集通用寄存器是32位,RV641是64位
  • 内存
    • 可执行在寄存器和内存之间的读写
    • 读写操作使用字节为基本单位进行寻址
    • RV64可以访问最多\(2^{64}\)个字节的内存空间,即\(2^{61}\)个存储单元

汇编语言的变量

  1. 不能使用变量(如int a; float b;
  2. 汇编语言的操作对象以寄存器为主

img

运算

  1. 算术运算:add、sub、mul、div、addi
  2. 逻辑运算:and、or、xor、andi、ori、xori
  3. 移位操作:sll、srl、sra、slli、srli、srai
  4. 数据传输:ld、sd、lw、sw、lwu、lh、lhu、sh、lb、lbu、lbu、sb、lui
  5. 比较指令:slt、slti、sltu、sltiu
  6. 条件分支:beq、bne、blt、bge、bltu、bgeu

i表示其中有一个数是立即数 u表示为无符号数

整型加法

  • C: a=b+c;
  • RISC-V: add s1, s2, s3

溢出是因为计算机中表达数本身是有范围限制的,RISC-V 忽略溢出问题,高位被截断,低位写入目标寄存器

整型减法

  • C: a=b-c;
  • RISC-V: sub s1, s2, s3

整数乘法与除法

  • 乘法RISC-V:mul rd, rs1, rs2
    • 将计算结果写入寄存器rd,忽略算术溢出
    • 要得到高32位积,如果操作数都是有符号数,就用mulh指令
    • 如果一个有符号一个无符号,可以用mulhsu指令
  • 除法RISC-V:div rd, rs1, rs2
    • 将计算结果向零舍入,将这些数视为二进制补码,商写入寄存器rd

常数运算元

  • immediate number
  • 可以将常数存入寄存器中进行运算
  • 有一个特殊的寄存器x0,村入了一个特定常数0,且该寄存器的值不可修改

相关运算

  • \(eg:a=(b+c)-(d+e);\)
# 假设a->s0,b->s1,c->s2,d>s3,e->s4
add t1, s1, s2
add t2, s3, s4
sub s0, t1, t2

位操作

  • 按位逻辑运算
    • 寄存器操作数:and x5, x6, x7
    • 立即数操作:andi x5, x6, 3
  • RISC-V中没有NOT,按位取反
    • xori x5, x6, -1可以得到\(x5=\bar{x6}\)
  • 移位运算
    • 左移相当于乘以2
    • 逻辑右移,在最高位添加0
    • 算术右移:在最高位添加符号位
    • 移位的位数可以是立即数或者寄存器中的值
    • slli, srli, srai只需要最多移动63位???只会使用immediate低6位的值

注意区分andadd

比较

  • 有符号数的比较:Set Less Than(slt:通过比较两个操作数的大小来对目标寄存器进行设置)
    • slt dst, reg1, reg2,若reg1<reg2,返回1,否则返回0
  • 无符号数的比较
    • sltu dst, src1, src2,若src1<src2,返回1,否则返回0

数据传输(主存访问指令)

memop reg, offset(bAddr)

memop:操作运算符

reg:寄存器

bAddr: 基地址

offset:偏移值

  • 内存都是字节寻址
  • 1word=4bytes
  • 在c语言中我们可见的最小数据类型是char,为一个字节(8bit),所有的数据类型都是8bit的整数倍
  • 在字寻址每个地址的每个部分相隔4个字节
  • 指针算数在汇编中不会自动完成
  • 不同的体系结构对字(word)的定义不同(RISC-V中 1word = 4 bytes)
  • 内存是按照字节进行编址,不是按照字进行编址的
  • 字的地址为字内最低位字节的地址(小端模式)
  • 按字对齐,地址最后两位为0(4的倍数)

相关指令

  • load word(lw)
    • 获取某个寄存器中的数据或者基地址加上内存的偏移量处的数据
  • store word(sw)
    • 与lw相反,将某个值存储进某个寄存器或者基地址加上内存偏移量处的地址
  • eg:整形数组的地址是s3,值b存在s2中
  • C:
array[10] = array[3]+b;
  • Assembly:
lw t0, 12(s3) # t0=A[3]
lw t0, s2     # t0=A[3]+b
sw t0, 40(s3) # A[10]=t0=A[3]+b

传输一个字节的数据

使用专门的字节传输指令lb,sb

  • lb/sb使用的是低字节
  • 如果是sb指令,高56位(RVRV64)/24位(RVRV32)被忽略
  • 如果是lb指令,高56位(RVRV64)/24位(RVRV32)做符号扩展

如何在起始状态存储值到内存中

# 汇编器指令类型之一,制定内存的数据存储,也可以将其视作内存的静态位置
# 在数字后面可以添加多个数字,用逗号隔开,便可以得到一个数组
.data
source:
    .word 3
    .word 1
    .word 4
# 告诉汇编器将以下所有内容解释为代码
.text
main:
    la t1, source
    lw t2, 0(t1)
    lw t3, 4(t1)

字节序

  • 如何存储字符或者短整型?
  • 大字节序:最高字节位在地址最低位
  • 小字节序:最低字节位于内存最低位
  • eg: s0=0x 0000 0180
big endian 00 00 01 80
little endian 80 01 00 00

符号扩展

  • 指在保留数字符号(正负性)及其数值的情况下增加二进制数字位数的操作
  • 若为正数,如001010,则直接在最高位前添加0
  • 若为负数,如11 1111 0001(十进制的-15),则直接在最高位添加1

其他的存储与加载指令

Byte instruction
  • load byte(lb)
    • 高位的三个字节通过“符号扩展”填充
  • store byte(sb)
    • 仅读取一个字节(8bit),高位的三个字节均被忽略
  • eg: s0=0x00000180
lb s1,1(s0) # s1=0x00000001
# 将八位扩展成32位,因为s0最高位是1,所以前30位(2进制)都应该是1,所以用16进制表示为“F”(此时每一位代表4bit)
lb s2,0(s0) # s2=0xFFFFFF80
# 此时只看s2的最低1字节内容(16进制最低的两位)
sb s2,2(s0) # *(s0)=0x00800180

条件分支(跳转)语句

  • 条件为真则转到标签所指的语句执行,否则顺序执行
  • beq reg1,reg2,label #branch if equal
    • 如果reg1中的值=reg2中的值, 程序跳转到label处继续执行
  • bne reg1,reg2,label #branch if equal
    • 如果reg1中的值≠reg2中的值, 程序跳转到label处继续执行
  • blt reg1,reg2,label #branch if less than
    • 如果reg1 < reg2, 程序跳转到label处继续执行
  • bge reg1,reg2,label #branch if greater than or equal
    • 如果reg1 >= reg2, 程序跳转到label处继续执行

注意:没有依据标志位的跳转(与x86不同)

无条件跳转指令

  • jal rd, offset # (jump and link)
    • 将下一条指令的地址PC+4保存在寄存器rd(一般使用x1/ra
  • jalr rd, offset(rs1) #(jump and link register)
    • 把PC+4存到rd中
    • 类似jal,但是跳转到rsl+offset地址处的指令(更远)
    • 可以用于过程返回
    • 如果rd用x0,那么相当于只跳转不返回

RISC-V伪指令

  • 方便程序员编程
  • 通过汇编语言的变化或者组合来实现,不是硬件实现

例:

# 将src存入dst
mv dst, src

# 装入一个立即数
li dst, imm

实际硬件实现

addi dst, src, 0

addi dst, x0, imm

RV64I实现C语言(部分)

实现for循环

long long int A[20];
long long int sum = 0;
for (long long int i = 0; i < 20; i++){
    sum += A[i];
}
假设数组A的首地址保存在 x8,sum保存在x10中

    add x9, x8, x0 # x9=&A[0]
    add x10, x0, x0# sum=0
    add x11, x0, x0 # i=0
    addi x13, x0, 20 # x13=20
Loop:
    bge x11, x13, Done
    ld x12,0(x9) # x12=A[i]
    add x10, x10, x12
    addi x9, x9, 8 # &A[i+1]
    addi x11, x11, 1 # i++
    beq x0, x0, Loop
Done:

实现while循环

long long int save[100];
while(save[i]==k){
    i+=1;
}

假设i存储在x22中,k存储在x24中,save数组元素的地址保存在x25中

Loop:
    slli x10, x22, 3  # 8*i
    add x10, x10, x25 # save[]+8*i
    ld x9, 0(x10) # save[i]->x9
    bne x9, x24, Exit # save[i]!=k Exit
    addi x22, x22, 1 # i+=1
    beq x0, x0, Loop
Exit: ...

基本块

  • 基本块是这样的指令序列
  • 没有嵌入分支(除非在末尾)
  • 没有分支目标(除非在开头)
  • 编译器可以识别基本块以进行优化
  • 先进的处理器能够加速基本块的执行