第一章 引言
为什么要讲riscv?
riscv的特点:
-诞生于顶尖学术机构:诞生于加州大学伯克利分校的体系结构研究院。吸引了大批的顶尖企业参与(e.g. 谷歌、华为、高通、阿里巴巴为rsicv的发展提供了大量的资金支持和贡献了技术和人才)。
-精简指令集:指令相对精简,指令格式规整,易于实现和理解。只包含了最基本和常用的指令,避免了复杂和特殊用途的指令,使得处理器的硬件设计更加简洁高效。比如说长度固定(32位/64位)。
-模块化设计:可以根据不同的应用需求选择不同的模块进行组合。这使得riscv可以适应从嵌入式系统到高性能服务器等不同的应用场景,具有很高的灵活性。
-开源:例如有多个开源的riscv编译器(如gcc和llvm)可供选择。
-低功耗:指令集设计精简,处理器的硬件实现相对简单,有助于降低功耗。
命名规范
RV[###][abc......xyz]。
[###]表示的是寄存器的位宽,后面的字母标识处理器支持的指令集模块集合。
例如:RV32MA、RV64GC。
模块化
模块化ISA vs 增量式ISA
RISC-V ISA = 1个基本整数指令集 + 多个可选的扩展指令集。
基本整数指令集:唯一强制要求实现的基础指令集,其他指令集都是可选的扩展模块。
扩展模块指令集:RISC-V允许在视线中以可选的形式实现其他标准化和非标准化的指令集扩展。特定组合“IMAFD”被称为“通用”组合,用英文字母G表示。
扩展指令集 | 描述 |
M | 整式乘法与除法指令集 |
A | 存储器原子指令集 |
F | 单精度浮点指令集 |
D | 双精度浮点指令集 |
C | 压缩指令集(16位长度) |
...... | 其它标准化和非标准化指令集 |
HART
hart = hardware thread(个人理解为超线程)
一个hart就是一个虚拟的cpu。一个hart可以不受外界干扰的自主地去获取和执行risc-v指令。
特权级别
level | encoding | name | |
0 | 00 | user/application | U |
1 | 01 | supervisor | S |
2 | 10 | reserved | |
3 | 11 | machine | M |
当运行在用户态时,就是说cpu运行在user级别,进入到内核的时候就是supervisor级别。固件就是machine级别。运行在machine模式是不开虚拟地址的,全部运行在物理地址。
risc-v芯片一上电首先是进入到machine模式,再进入到supervisor模式,此时也叫保护模式,虚拟地址打开。
进程的实现依赖于虚拟地址,虚拟地址的实现需要MMU硬件支持。
Control and Status Register(CSR)
不同的特权级别下时分布对应各自的一套CSR,用于控制和获取响应level下的处理器工作状态。
高级别的特权可以访问低级别的CSR。比如说machine级别可以访问user级别的csr。
rsicv定义了专门用于操作csr的指令。
如果是用户程序,不太需要跟csr打交道。
异常和中断
异常:主动触发,cpu给你一次改过自新的机会,去执行一段挽救程序;当执行到非法指令的时候,cpu会停掉此指令流跳到一个特殊的地址去执行一段特殊的程序(自己写的程序,想对异常做的处理),执行完之后回到之前的指令再次运行;比如说除0异常;
中断:被动触发;cpu停掉当前程序,跳转到执行中断处理程序,执行完返回到下一条指令去执行;比如说外设通知你发生一件什么事情,跑去执行别的指令,执行完再回到下条指令,就像中断没有发生过一样。
ELF介绍
ELF(Executable Format)是一种unix-like系统上的二进制文件格式标准。
ELF标准中定义的采用ELF格式的文件分为4类:
ELF文件类型 | 说明 | 实例 |
可重定位文件(relocatable file) | 内容包含了代码和数据,可以被链接成可执行文件或共享目标文件 | linux上的.o文件 |
可执行文件 | 可以直接执行的文件 | linux上的a.out文件 |
共享目标文件 | 内容包含了代码和数据,可以作为链接器的输入,在链接阶段和其他的relacatable file或者shared object file一起链接成新的object file;或者在运行阶段,作为动态链接器的输入,和可执行文件结合,作为进程的一部分来运行。 | linux上的.so文件 |
核心转储文件(core dump file) | 进程意外终止时,系统可以将该进程的部分内容和终止时的其他状态信息保存到该文件中以供调试分析。 | linux上的core文件 |
ELF文件格式:
ELF Header | |
Program Header Table | 运行视图 |
.text | 程序指令 |
.init | 做初始化的一些指令 |
.data | 数据:全局变量等 |
.bss | |
...... | |
Section Header Table
| 从链接角度去描述了这个文件的内容 |
.text和.init等这些信息放置的时候都会section(节)对齐,但是在运行的时候会合在一起以节省空间,于是有了segment(段)概念,segment的信息存放于program header table中。segment fault就是因为出错的时候在内存中失败。
ELF文件处理相关工具:
Binutils官网地址:https://www.gnu.org/software/binutils/
ar:归档文件,将多个文件打包成一个大文件。
as:被gcc调用,输入汇编文件,输出目标文件工链接器ld连接。
ld:gnu链接器,被gcc调用,它把目标文件和各种库文件结合在一起,重定位数据,并链接符号引用。
objcopy:执行文件格式转换。
objdump:显示ELF文件的信息。
readelf:显示更多ELF格式文件的信息(包括DWARF调试信息)
示例1:
gcc -c hello.c
readelf -h hello.o // 查看头信息
readelf -S hello.o // 查看section信息
readelf -SW hello.o
实例2:
gcc -g -c hello.c
objdump -S hello.o // 显示汇编指令
嵌入式开发
嵌入式开发是一种比较综合性的技术,它不单指纯粹的软件开发技术,也不单是一种硬件配置技术;它是在特定的硬件环境下针对某款硬件进行开发,是一种系统级别的与硬件结合比较紧密的软件开发技术。程序并不是运行在本地,而是运行在特殊的硬件上。
参与编译和运行的机器根据其角色可以分成以下三类:
-build系统:生成编译器可执行程序的计算机。编译器在build系统上编译出来的。
-host系统:运行编译器可执行程序,编译链接应用程序的计算机系统。
-target系统:运行应用程序的计算机系统。
根据build/host/target的不同组合我们可以得到如下的编译方式分类:
-本地(native)编译:build==host==target
-交叉(cross)编译:build==host!=target
QEMU
QEMU是一套由(Fabrice Bellard)编写的以GPL许可证分发源码的计算机系统模拟软件,在GNU/Linux平台上使用广泛。
QEMU,支持多种体系架构。譬如:IA-32(x86),AMD 64, MIPS 32/64,RISC-V 32/64等等。
QEMU有两种主要运作模式:
-user mode: 直接运行应用程序 (比如说hello.o运行直接输出“hello”)
-system mode:模拟整个计算机系统,包括中央处理器及其他周边设备。
第二章 汇编语言编程
基本组成
汇编文件一般后缀为.S或.s,.S包含了预处理的语句,.s就是纯粹的汇编语句。
一个完整的RISC-V汇编程序有多条语句(statement)组成。一条典型的RISC-V汇编语言由3部分组成:
[label:] [operation] [comment]
打方括号表示可选。
-label表示一个标号,必须以":"结尾。label相当于一个地址,给这个地址起了个名字。是这条指令存放在内存的地址。
-operation可以由以下多种类型:
-instruction(指令):直接对应二进制机器指令的字符串
-preudo-instruction(伪指令):为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令(instruction)。(要在汇编器的手册里查看定义)
-directive(指示/伪操作):通过类似指令的形式(以“.”开头),通知汇编器如何控制代码的产生等,不对应具体的指令。属于汇编器自己定义的一些语法。(要在汇编器的手册里查看定义)
-macro:采用.macro/.endm自定义的宏
例子:
.macro do_nothing #directive
nop #preudo-instruction
nop #preudo-instruction
.endm
.text #directive 告诉汇编器生成的指令要放到text的section中
.global _start #directive _start是个全局变量,外部可见,有点像extern
_start: #label
li x6, 5 #preudo-instruction
li x7, 4 #preudo-instruction
add x5, x6, x7 #instruction
do_nothing #calling macro
.end #End of file
RISC-V汇编指令操作对象
寄存器:
-在RISC-V中,Hart在执行算术逻辑运算时所操作的数据必须直接来自寄存器。
-32个通用寄存器,x0~x31;
寄存器别名 | |
x0 | 读0寄存器,其值永远为0,写操作对其无效。 |
x1 | ra,用于存储函数调用后的返回地址。 |
x2 | sp,栈指针。 |
x3 | gp,全局指针。一半指向全局变量和静态变量。它可以帮助快速访问全局数据区域,提高对全局变量的访问效率。 |
x4 | tp,线程指针。主要用于多线程编程,指向当前线程的特定数据结构或信息。 |
x5-x7,x28-x31 | t0-t6,临时寄存器。通常用于临时存储或中间结果的保存。在程序执行过程中,可用于暂存数据、参与运算等。 |
x8-x9,x18-x27 | s0-s11,保存寄存器。用于在函数调用过程中保存寄存器的值,以便在函数返回时恢复这些值。 |
x10-x17 | a0-a7,参数寄存器。用于传递函数的参数。参数可以通过这些寄存器传递给被调用函数,方便函数内部对参数进行访问和处理。 |
RISC-V汇编指令类型
31-25 | 24-20 | 19-15 | 14-12 | 11-7 | 6-0 | |
funct7 | rs2 | rs1 | funct3 | rd | opcode | R-type |
imm[] | imm[] | rs1 | funct3 | rd | opcode | I-type |
imm[] | rs2 | rs1 | funct3 | imm[4:0] | opcode | S-type |
imm[] | rs2 | rs1 | funct3 | imm[4:1[11]] | opcode | B-type |
imm[] | imm[] | imm[] | imm[] | rd | opcode | U-type |
imm[] | imm[] | imm[] | imm[] | rd | opcode | J-type |
R-type:(register),每条指令中有三个fields,用于指定3个寄存器参数。
I-type:(Immediate),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为12bits)。
S-type: (Store),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为12bits,但fields的组织方式不同于I-type)。(用来访问内存的指令)
B-type: (Branch),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为12bits,但取值为2的倍数)。(跟分支跳转有关)
U-type:(Upper),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为20bits,用于表示一个立即数的高20位)。(auipc、lui)
J-type:(Jump),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为20bits)。
小端序的概念
riscv是小端序编指令。
算数指令
指令 | 语法 | 描述 | 例子 |
ADD | ADD RD, RS1, RS2 | RS1和RS2的值相加,结果保存到RD | add x5, x6, x7 |
SUB | SUB RD, RS1, RS2 | RS1的值减去RS2的值,结果保存到RD | sub x5, x6, x7 |
ADDI | ADDI RD, RS1, RS2 | RS1的值和IMM相加,结果保存到RD | addi x5, x6, x7 |
LUI | LUI RD, IMM | 构造一个32位的数,高的20位存放IMM,低12位清零,结果保存到RD | lui x5, 0x12345 |
AUIPC | AUIPC RD, IMM | 构造一个32位的数,高20位存放IMM,低12清零。结果和PC相加保存到RD。 | auipc x5, 0x12345 |
基于算术运算指令实现的其他伪指令
伪指令 | 语法 | 等价指令 | 指令描述 | 例子 |
LI | LI RD, IMM | LUI和ADDI的组合 | 将立即数加载到RD中 | li x5, 0x12345678 |
LA | LA RD, LABEL | AUIPC和ADDI组合 | 为RD加载一个地址值 | la x5,label |
NEG | NEG RD, RS | SUB RD, X0, RS | 对RS中的值取反并将结果存放在RD中 | neg x5, x6 |
MV | MV RD, RS | ADDI RD, RS, 0 | 将RS中的值拷贝到RD中 | mv x5, x6 |
NOP | NOP | ADDI x0, x0, 0 | 什么也不做 | nop |
LI(Load Immediate)
用算数指令构建一个大数
#例子:构建一个大数
# imm is in range of [-2048, +2047]
# 0x80000
lui x5, 0x80
addi x5, x0, 0x80
# imm is NOT in the range of [-2048, +2047]
# and the most-significant-bit of "lower-12" is 0
# 0x12345001
lui x6, 0x12345
addi x6, x6, 0x001
# imm is NOT in the range of [-2048, +2047]
# and the most-significant-bit of "lower-12" is 1
# 0x12345FFF
lui x7 0x12346
addi x7, x7, -1
用伪指令
li x7 xxxxxxxxx
LA(Load Address)
语法 | LA RD, LABEL | |
例子 | la x5, foo |
LA是一个伪指令
具体编程时给出需要加载的label,编译器会根据实际情况利用auipc和其他指令自动生成正确的指令序列。
常用语加载一个函数或者变量的地址。
例子
_start:
la x5, _start # x5 = _start
jr x5
反汇编出来很可能就是一条auipc指令。
逻辑运算指令
指令 | 格式 | 语法 | 描述 | 例子 |
AND | R-type | AND RD, RS1, RS2 | RD = RS1 & RS2 | and x5, x6, x7 |
OR | R-type | OR RD, RS1, RS2 | RD = RS1 | RS2 | or x5, x6, x7 |
XOR | R-type | XOR RD, RS1, RS2 | RD = RS1 ^ RS2 | xor x5, x6, x7 |
ANDI | I-type | ANDI RD, RS1, IMM | RD = RS1 & IMM | andi x5, x6, 20 |
ORI | I-type | ORI RD, RS1, IMM | RD = RS1 | IMM | or x5, x6, 20 |
XORI | I-type | XORI RD, RS1, IMM | RD = RS1 ^ IMM | xor x5, x6, 20 |
所有的逻辑指令都是按位操作。
逻辑移位运算指令
指令 | 格式 | 语法 | 描述 | 例子 |
SLL | R-type | SLL RD, RS1, RS2 | 逻辑左移 | sll x5, x6, x7 |
SRL | R-type | SRL RD, RS1, RS2 | 逻辑右移 | srl x5, x6, x7 |
SLLI | I-type | SLLI RD, RS1, IMM | 逻辑左移立即数 | slli x5, x6, 3 |
SRLI | I-type | SRLI RD, RS1, IMM | 逻辑右移立即数 | srli x5, x6, 3 |
无论是逻辑左移还是右移,补足的都是0。
算术移位运算指令
指令 | 格式 | 语法 | 描述 | 例子 |
SRA | R-type | SRA RD RS1, RS2 | 算术右移 | sra x5, x6, x7 |
SRAI | I-type | SRAI RD, RS1, IMM | 算术右移立即数 | srai x5, x6, 3 |
算术右移时按照符号位值补足。
内存读写指令
内存读
指令 | 格式 | 语法 | 描述 | 例子 |
LB | I-type | LB RD, IMM(RS1) | 读取一个8bits的数据到RD中,内存地址=RS1 + IMM,数据保存到RD之前执行sign-extended。 | lb x5, 40(x6) |
LBU | I-type | LBU RD, IMM(RS1) | 数据在保存到RD之间会执行zero-extended | lbu x5, 40(x6) |
LH | I-type | LH RD, IMM(RS1) | 读取一个16bit的数据。 | lh x5, 40(x6) |
LHU | I-type | LHU RD, IMM(RS1) | 读取伊恩16bit的数据。 | lhu x5, 40(x6) |
LW | I-type | LW RD, IMM(RS1) | 读取一个32bit的数据。 | lw x5, 40(x6) |
内存写
指令 | 格式 | 语法 | 描述 | 例子 |
SB | S-type | SB SR2, IMM(RS1) | 将RS2寄存器中低8bits的数据写出到内存中,内存地址=RS1 + IMM。 | sb x5, 40(x6) |
SH | S-type | SH SR2, IMM(RS1) | sh x5, 40(x6) | |
SW | S-type | SW SR2, IMM(RS1) | sw x5, 40(x6) |
条件分支指令
指令 | 格式 | 语法 | 描述 | 例子 |
BEQ | B-type | BEQ RS1, RS2, IMM | 如果相等则跳转 | beq x5, x6, 100 |
BNE | B-type | BNE RS1, RS2, IMM | 如果不相等则跳转 | bne x5, x6, 100 |
BLT | B-type | BLT RS1, RS2, IMM | 按照有符号数方式比较,如果小于则跳转 | blt x5, x6, 100 |
BLTU | B-type | BLTU RS1, RS2, IMM | 按照无符号数方式比较,如果小于则跳转 | bltu x5, x6, 100 |
BGE | B-type | BGE RS1, RS2, IMM | 按照有符号方式比较,大于等于就跳转 | bge x5, x6, 100 |
BGEU | B-type | BGEU RS1, RS2, IMM | 按照无符号方式比较,大小等于就跳转 | bgeu x5, x6, 100 |
伪指令 | 语法 | 等价指令 | 描述 |
BLE | BLE RS, RT, OFFSET | BGE RT, RS, OFFSET | 有符号方式比较,<= |
BLEU | BLEU RS, RT, OFFSET | BGEU RT, RS, OFFSET | 无符号方式比较,<= |
BGT | BGT RS, RT, OFFSET | BLT RT, RS, OFFSET | 有符号比较,> |
BGTU | BGTU RS, RT, OFFSET | BLTU RT, RS, OFFSET | 无符号比较,> |
BEQZ | BEQZ RS, RT, OFFSET | BNE RS, x0, OFFSET | ==0 |
BNEZ | BNEZ RS, RT, OFFSET | BLT RS, x0, OFFSET | !=0 |
BLTZ | BLTZ RS, RT, OFFSET | BGE x0, RS, OFFSET | < 0 |
BLEZ | BLEZ RS, RT, OFFSET | BLT RT, R0, OFFSET | <= 0 |
BGTZ | BGTZ RS, RT, OFFSET | BGE RT, RS, OFFSET | > 0 |
BGEZ | BGEZ RS, OFFSET | BGE RT, RS, OFFSET | >= 0 |
无条件跳转
JAL(Jump And Link)
语法 | JAL RD, LABEL |
例子 | jal x1, label |
jal指令用于调用子过程,跳转到指定位置,并且把下一条指令的地址写入RD,保存为返回地址。
JALR(Jump And Link Register)
语法 | JALR RD, IMM(RS1) |
例子 | jalr x0, 0(x5) |
jalr指令同样用于调用子过程,跳转到指定位置,并且把下一条指令的地址写入RD,保存为返回地址。
例子
int a = 1;
int b = 1;
void sum()
{
a = a + b;
return; // jalr x0, 0(x5) 不需要保存下一条指令的地址
}
void _start()
{
sum(); // jal x5, sum
other codes
...
}
思考:
如何解决更远距离的跳转?
AUIPC X6, IMM-20
JALR X1, X6, IMM-12
指令寻址模式总结
所谓寻址指的是指令中定位操作数或者地址的方式
寻址模式 | 解释 | 例子 |
立即数寻址 | 操作数是指令本身的一部分。 | addi x5,x6,20 |
寄存器寻址 | 操作数存放在寄存器中,指令中指定访问的寄存器从而获取该操作数。 | add x5, x6, x7 |
基址寻址 | 操作数在内存中,指令中通过指定寄存器(基址base)和立即数(偏移量offset),通过base+offset的方式获得操作数在内存中的地址从而获取该操作数。 | sw x5,40(x6) |
PC相对寻址 | 在指令中通过PC和指令中的立即数相加获得目的地址的值。 | beq x5,x6,100 |
第三章 函数的调用
函数调用过程概述
当caller和callee不是一个人写的,这个时候就需要制定一套规定。
函数调用的规则
寄存器名 | ABI名(编程用名) | 用途约定 | 谁负责在函数调用过程中维护这些寄存器 |
x0 | zero | 读取时总为0,写入时不起任何效果 | N/A |
x1 | ra | 存放函数返回地址 | Caller |
x2 | sp | 存放栈指针 | callee |
x5-x7,x28-x31 | t0-t2,t3-t6 | 临时寄存器,callee可能会使用这些寄存器,所以callee不保证这些寄存中的值在函数调用过程中保持不变。对于caller来说,如果需要的话应在调用前保存这些值。 | caller |
x8,x9,x18-x27 | s0,s1,s2-s11 | 保存寄存器。callee需要保证这些寄存器的值在函数返回后仍然维持函数之前的原值。 | callee |
x10,x11 | a0,a1 | 参数寄存器,用于在函数调用过程中保存第一个和第二参数。 | caller |
x12-x17 | a2,a7 | 参数寄存器。如果有更多的参数则要利用到栈。 | caller |
函数执行过程的三部曲:
-保存现场
-执行函数内容
-恢复现场
例子(尾调用)
例子(非尾调用)
riscv编程与c混合编程
汇编调用c语言
c语言调用汇编
int foo(int a, int b)
{
int c;
asm volatile(
"add %[sum], %[add1], %[add2]" // 汇编指令
:[sum] = r'(c) // 输出操作数列表(可选)
:[add1]"r"(a), [add2]"r"(b) // 输入操作数列表(可选)
);
return c;
}
第四章 UART串行接口调用
状态寄存器读写
语法 | CSRRW RD, CSR, RS1 | |
例子 | csrrw t6, mscratch, t6 | t6 = mscratch;mscratch = t6 |
语法 | CSRRS RD, CSR, RS1 | |
例子 | csrrs x5, mie, x6 | x5 = mie; mie |= x6 |
UART(统一异步接收传输协议)
通过异步收发传输器,是一种双全工、异步串行通信方式,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据,可以实现双全工传输和接收。
UART串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收。
UART在发送或接收过程中的一帧数据由4部分组成,起始位(低电平)、数据位、奇偶校验位和停止位(高电平)。
起始位标志着一帧数据的开始,停止位标志着一帧数据的结束,数据位是一帧数据中的有效数据。
信号与管脚定义
qemu 模拟了 ns16550a 芯片,通过查阅资料可知内存映射将对应的基地址映射到了 0x1000_0000 处。
platform.h编写
/* This machine puts UART register here in physical memory. */
#define UARTO 0x10000000
uart.c编写
#define UART_REG(reg) ((volatile uint8_t*)(UART0 + reg))
#define uart_read_reg(reg) (*(UART0 + reg))
#define uart_write_reg(reg, v) (*(UART0 + reg)) = (v)
NS16550a编程接口介绍
(进入程序)
代码链接
https://github.com/plctlab/riscv-operating-system-mooc
https://gitee.com/unicornx/riscv-operating-system-mooc