RISC-V汇编&指令系统
基本概念¶
- 指令系统(指令集,IS:Instruction Set)
- 指令集系统架构(ISA)
- 简称架构,也可以称为:处理器架构、指令集体系结构
- 包含了程序员正确编写二进制机器语言程序所需的全部信息
- 例如:如何使用硬件、指令格式、操作种类、操作数所能存放的寄存器组和结构,包括每个寄存器名称、编号、长度、用途
- 系列机
- 基本指令系统相同,基本系统结构相同的计算机
- 实际是为了解决软件兼容的问题,给定一个ISA,可以有不同的实现方式;例如:AMD/Intel CPU都是X86-64指令集,ARM ISA也有不同的实现方式
- IBM 360是第一个将ISA与其实现分离的系列机
指令集架构¶
- ISA——抽象层,软件子系统与硬件子系统的桥梁和接口
\[
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
存储的字为0x89ABCDEF
,addr
,addr+1
,addr+2
,addr+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
- 是一种低级编程语言
- 不同的架构实际上具有一组支持的不同操作
主流架构¶
- Intel x86
- ARM
-
RISC-V
-
早期趋势
- 进行复杂的指令集计算
- 当前
- 创建精简的指令集
- 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}\)个存储单元
汇编语言的变量¶
- 不能使用变量(如
int a; float b;
) - 汇编语言的操作对象以寄存器为主
运算¶
- 算术运算:add、sub、mul、div、addi
- 逻辑运算:and、or、xor、andi、ori、xori
- 移位操作:sll、srl、sra、slli、srli、srai
- 数据传输:ld、sd、lw、sw、lwu、lh、lhu、sh、lb、lbu、lbu、sb、lui
- 比较指令:slt、slti、sltu、sltiu
- 条件分支: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位的值
注意区分
and
和add
!
比较¶
- 有符号数的比较: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];
}
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: ...
基本块¶
- 基本块是这样的指令序列
- 没有嵌入分支(除非在末尾)
- 没有分支目标(除非在开头)
- 编译器可以识别基本块以进行优化
- 先进的处理器能够加速基本块的执行