[完结] 循序渐进,学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春
RISC-V 部分作业答案
RISC-V 念作 “risk-five”,代表着Berkeley所研发的第五代精简指令集。
该项目2010年始于加州大学伯克利(Berkeley)分校,希望选择一款 ISA 用于科研与教学。经过前期多年的的研究和选型,最终决定放弃使用现成的 X86 和 ARM 等 ISA,而是自己从头研发一款:
主要研发人员
一款高质量,免许可证,开放的RISC ISA。
一套由非盈利的RISC-V基金会维护的标准:https://riscv.org/ 。
适用于所有类型的计算机系统:从微控制器到超级计算机。
RISC-V不是一家公司,也不是一款 CPU 实现。
增量式 ISA :计算机体系结构的传统方法,同一个体系架构下的新一代处理器不仅实现了新的 ISA 扩展,还必须实现过去的所有扩展,目的是为了保持向后的二进制兼容性。典型的,以 80X86 为代表。
模块化 ISA:由 1 个基本整数指令集 + 多个可选的扩展指令集组成。基础指令集是固定的,永远不会改变。
RISC ISA = 1个基本整数指令集 + 多个可选的扩展指令集
基本整数(Integer)指令集
扩展模块指令集:
例子:
RISC-V 的 Privileged Specification 定义了三个特权级别(privilege level)
Machine 级别是最高的级别,所有的实现都需要支持。
可选的 Debug 级别(用于调试CPU)
物理内存保护(Physical Memory Protection,PMP)
虚拟内存(Virtual Memory)
GCC(GNU Compiler Collection)
gcc [options] [filenames]
常用选项 | 含义 |
---|---|
-E | 只做预处理 |
-c | 只编译不链接,生成目标文件“.o” |
-S | 生成汇编代码 |
-o file | 把输出生成到由 file 指定文件名的文件中 |
-g | 在输出的文件中加入支持调试的信息 |
-v | 显示输出详细的命令执行过程信息 |
针对多个源文件的处理
ELF(Executable Linkble Format)是一种Unix-like系统上的二进制文件格式标准。
ELF标准中定义的采用 ELF 格式的文件分为4类:
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件(Relocatable File) | 内容包含了代码和数据,可以被链接成可执行文件或共享目标文件。 | Linux上的 .o 文件 |
可执行文件(Executable File) | 可以直接执行的程序 | Linux上的 a.out |
共享目标文件(Shared Object File) | 内容包含了代码和数据,可以作为链接器的输入,在链接阶段和其他的 Relocatable File 或者 Shared Object File 一起链接成新的 Object File;或者在运行阶段,作为动态连接器的输入,和Executable File 结合,作为进程的一部分来运行。 | Linux上的 .so |
核心转储文件(Core Dump File) | 进程意外终止时,系统可以将该进程的部分内容和终止时的状态信息保存到该文件中以供调试分析。 | Linux 上的 core 文件 |
ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而执行一个ELF文件时,在加载器(Loader)看来它是由Program Header Table描述的一系列Segment的集合
左边是从汇编器和链接器的视角来看这个文件,开头的ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,Section Header Table中保存了所有Section的描述信息。右边是从加载器的视角来看这个文件,开头是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加载过程中没有用到,所以是可有可无的。注意Section Header Table和Program Header Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。
https://www.gnu.org/software/binutils/
嵌入式开发是一种比较综合性的技术,它不单指纯粹的软件开发技术,也不单是一种硬件配置技术;他是在特定的硬件环境下针对某款硬件进行开发,是一种系统级别的与硬件结合比较紧密的软件开发技术。
参与编译和运行的机器根据其角色可以分成以下三类:
根据 build/host/target 的不同组合我们可以得到如下的编译方式分类:
交叉(cross)编译:build == host != target
GNU 交叉编译工具链(Toolchain)
https://www.gnu.org/software/gdb/
https://www.qemu.org/
https://www.gnu.org/software/make/
MakeFile 由一条或者多条规则(rule)组成。
每条规则由三要素构成:
一个简单的 Makefile 规则如下:
Makfile中其他元素介绍
一个完整的RISC-V汇编程序有多条 语句(statement)组成
一个典型的RSIC-V汇编 语句 由3部分组成:
[label:] [operation] [comment]
label(标号):GNU汇编中,任何以冒号结尾的标识符都被认为是一个标号。
operation 可以有以下多种类型:
instruction(指令):直接 对应二进制机器指令的字符串。
pseudo-instruction(伪指令):为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令(instruction)。
directive(指令/伪操作):通过类似指令的形式(以 “ . ”开头),通知汇编器如何控制代码的产生等,不对应具体的指令。
macro:采用.macro/.endm自定义的宏。
.macro do_nothing # 宏的开头,定义了一个名为do_nothing的宏
nop # 宏的内容
.endm # 宏的结尾
comment(注释):常用方法,“#”开始到当前行结束。
指示符 | 作用 |
---|---|
.text | 代码段,之后跟的符号都在.text内 |
.data | 数据段,之后跟的符号都在.data内 |
.bss | 未初始化数据段,之后跟的符号都在.bss中 |
.section .foo | 自定义段,之后跟的符号都在.foo段中,.foo段名可以做修改 |
.align n | 按2的n次幂字节对齐 |
.balign n | 按n字节对齐 |
.globl sym | 声明sym未全局符号,其它文件可以访问 |
.string “str” | 将字符串str放入内存 |
.byte b1,…,bn | 在内存中连续存储n个单字节 |
.half w1,…,wn | 在内存中连续存储n个半字(2字节) |
.word w1,…,wn | 在内存中连续存储n个字(4字节) |
.dword w1,…,wn | 在内存中连续存储n个双字(8字节) |
.float f1,…,fn | 在内存中连续存储n个单精度浮点数 |
.double d1,…,dn | 在内存中连续存储n个双精度浮点数 |
.option rvc | 使用压缩指令(risc-v c) |
.option norvc | 不压缩指令 |
.option relax | 允许链接器松弛(linker relaxation,链接时多次扫描代码,尽可能将跳转两条指令替换为一条) |
.option norelax | 不允许链接松弛 |
.option pic | 与位置无关代码段 |
.option nopic | 与位置有关代码段 |
.option push | 将所有.option设置存入栈 |
.option pop | 从栈中弹出上次存入的.option设置 |
riscv-spec-20191213.ptf 第147页
小端序是相对大端序来的,他们共同的特征是:从内存的低地址依次向高地址读取和写入。
区别在于,对于0x12345678(下面是4字节对齐):
主机字节序(HBO - Host Byte Order)
一个多字节整数在计算机内存中存储的字节顺序称为主机字节序(HBO - Host Byte Order,或者叫本地字节序)
不同类型CPU的HBO不同,这与CPU的设计有关。分为大端序(Bit-Endian)和小端序(Little-Endian)。
主机字节序(大端序 vs 小端序)
riscv-spec-20191213.ptf 第157页
ADD
语法 | ADD RD, RS1, RS2 | |
---|---|---|
例子 | add x5, x6, x7 | x5 = x6 + x7 |
SUB(Substract)
语法 | SUB RD, RS1, RS2 | |
---|---|---|
例子 | sub x5, x6, x7 | x5 = x6 - x7 |
ADDI(ADD Immediate)
语法 | addi RD, RS1, IMM | |
---|---|---|
例子 | addi x5, x6, -2 | x5 = x6 + (-2) |
编码格式:I-type
注意:RISC-V ISA并没有提供 SUBI 指令
ADDI的局限性
给一个寄存器赋值的数值范围只有:[-2048, 2047)。如果要赋值一个大数(32位)怎么办?
√ 解决思路:自己构造一个。具体做法:
LUI(Load Upper Immediate)
语法 | LUI RD, IMM | |
---|---|---|
例子 | lui x5, 0x12345 | x5 = 0x12345 << 12 |
LUI 指令采用 U-type:
LUI 指令会构造一个 32bits 的立即数,这个立即数的高 20 位对应指令中的 imm ,低 12 位清零。这个立即数作为结果存放在 RD 中。
练习:
利用 LUI + ADDI 来为寄存器加载一个大数
0x12345678
lui x1, 0x12345 # x1 = 0x12345000
addi x1, x1, 0x678 # x1 = x1 + 0x678
0x12345fff
# 错误写法
# addi 中的 0xfff
# 会被符号扩展成一个32位的数字 0xffffffff
lui x1, 0x12345 # x1 = 0x12345000
addi x1, x1, 0xfff # x1 = x1 + 0xfff
# 正确写法
# 借位写法 给0xfff加上1给到x1,
# 再给x1减去1
# 相当于借的那一位1位 0x1000 - 1 = 0xfff
lui x1, 0x12346
addi x1, x1, -1
上面的写法虽然可以实现,但是还是过于麻烦,而且对于0x12345fff 可以用借位的写法,但是对于0xffff ffff就借不了位了,这个时候我们可以用 li 伪指令解决。
LI(Load Immediate)
语法 | LI RD, IMM | |
---|---|---|
例子 | li x5, 0x12345678 | x5 = 0x12345678 |
AUIPC
语法 | AUIPC RD, IMM | |
---|---|---|
例子 | auipc x5, 0x12345 | x5 = 0x12345 << 12 + pc |
LA(Load Address)
语法 | LA RD, LABEL | |
---|---|---|
例子 | la x5, foo |
指令 | 语法 | 描述 | 例子 |
---|---|---|---|
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, IMM | RS1的值和IMM相加,结果保存到RD | addi x5, x6, 100 |
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的组合 | 将立即数 IMM 加载到 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 |
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
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 |
伪指令 | 语法 | 等价指令 | 描述 | 例子 |
---|---|---|---|---|
NOT | NOT RD, RS | XORI RD, RS, -1 | 对 RS 的值按位取反,结果存放在 RD 中 | not x5, x6 |
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
SLL | R-type | SLL RD, RS1, RS2 | 逻辑左移(Shift Left Logical)RD = RS1 << RS2 | sll x5, x6, x7 |
SRL | R-type | SRL RD, RS1, RS2 | 逻辑右移(Shift Right Logical)RD = RS1 >> RS2 | srl x5, x6, x7 |
SLLI | I-type | SLLI RD, RS1, IMM | 逻辑左移立即数(Shift Left Logical Immediate)RD = RS1 << IMM | slli x5, x6, 3 |
SRLI | I-type | SRLI RD, RS1, IMM | 逻辑右移立即数(Shift Right Logical Immediate)RD = RS1 >> IMM | srli x5, x6, 3 |
无论是逻辑左移还是逻辑右移,补足的都是 0
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
SRA | R-type | SRA RD, RS1, RS2 | 算数右移(Shift Right Arithmetic) | sra x5, x6, x7 |
SRAI | I-type | SRAI RD, RS1, RS2 | 算数右移立即数(Shift Right Arithmetic Immediate) | srai x5, x6, 3 |
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
LB | I-type | LB RD, IMM(RS1) | Load Byte,从内存中读取一个8bits的数据到RD中,内存地址 = RS1 + IMM,数据在保存到RD之前会执行sign-extended。 | lb x5, 40(x6) |
LBU | I-type | LBU RD, IMM(RS1) | Load Byte Unsigned,从内存中读取一个8bits的数据到RD中,内存地址 = RS1 + IMM,数据在保存到RD之前会执行zero-extended。 | lbu x5, 40(x6) |
LH | I-type | LH RD, IMM(RS1) | Load Halfword,从内存中读取一个16bits的数据到RD中,内存地址 = RS1 + IMM,数据在保存到RD之前会执行sign-extended。 | lh x5, 40(x6) |
LHU | I-type | LHU RD, IMM(RS1) | Load Halfword Unsigned,从内存中读取一个16bits的数据到RD中,内存地址 = RS1 + IMM,数据在保存到RD之前会执行zero-extended。 | lhu x5, 40(x6) |
LW | I-type | LW RD, IMM(RS1) | Load Word,从内存中读取一个32bits的数据到RD中,内存地址 = RS1 + IMM。 | lw x5, 40(x6) |
注意:IMM给出的偏移量范围是[-2048, 2047]。
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
SB | S-type | SB RS2, IMM(RS1) | Store Byte,将RS2寄存器中低8bits的数据写出到内存,内存地址 = RS1 + IMM。 | sb x5, 40(x6) |
SH | S-type | SH RS2, IMM(RS1) | Store Halfword,将RS2寄存器中低16bits的数据写出到内存,内存地址 = RS1 + IMM。 | sh x5, 40(x6) |
SW | S-type | SW RS2, IMM(RS1) | Store Word,将RS2寄存器中32bits的数据写出到内存,内存地址 = RS1 + IMM。 | sw x5, 40(x6) |
注意:IMM给出的偏移量范围是[-2048, 2047]。
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
BEQ | B-type | BEQ RS1, RS2, IMM | Branch if EQual。比较RS1和RS2的值,如果相等,则执行路径跳转到一个新的地址。 | beq x5, x6, 100 |
BNE | B-type | BNE RS1, RS2, IMM | Branch if Not EQual。比较RS1和RS2的值,如果不相等,则执行路径跳转到一个新的地址。 | bne x5, x6, 100 |
BLT | B-type | BLT RS1, RS2, IMM | Branch if Less Than。按照有符号方式比较RS1和RS2的值,如果RS1 < RS2,则执行路径跳转到一个新的地址。 | blt x5, x6, 100 |
BLTU | B-type | BLTU RS1, RS2, IMM | Branch if Less Than(Unsigned)。按照无符号方式比较RS1和RS2的值,如果RS1 < RS2,则执行路径跳转到一个新的地址。 | bltu x5, x6, 100 |
BGE | B-type | BGE RS1, RS2, IMM | Branch if Greater Than。按照有符号方式比较RS1和RS2的值,如果RS1 >= RS2,则执行路径跳转到一个新的地址。 | bge x5, x6, 100 |
BGEU | B-type | BGEU RS1, RS2, IMM | Branch if Greater Than(Unsigned)。按照无符号方式比较RS1和RS2的值,如果RS1 >= RS2,则执行路径跳转到一个新的地址。 | bgeu x5, x6, 100 |
伪指令 | 语法 | 等价指令 | 描述 |
---|---|---|---|
BLE | BLE RS, RT, OFFSET | BGE RT, RS, OFFSET | Branch if Less & EQual,有符号方式比较,如果 RS <= RT,跳转到 OFFSTET |
BLEU | BLEU RS, RT, OFFSET | BGEU RT, RS, OFFSET | Branch if Less or EQual Unsigned,无符号方式比较,如果 RS <= RT,跳转到 OFFSTET |
BGT | BGT RS, RT, OFFSET | BLT RT, RS, OFFSET | Branch if Greater Than,有符号方式比较,如果 RS > RT,跳转到 OFFSTET |
BGTU | BGTU RS, RT, OFFSET | BLTU RT, RS, OFFSET | Branch if Greater Than Unsigned,无符号方式比较,如果 RS > RT,跳转到 OFFSTET |
BEQZ | BEQZ RS, OFFSET | BEQ RS, x0, OFFSET | Branch if EQual Zero,如果 RS == 0,跳转到OFFSET |
BNEZ | BNEZ RS, OFFSET | BNE RS, x0, OFFSET | Branch if Not EQual Zero,如果 RS != 0,跳转到OFFSET |
BLTZ | BLTZ RS, OFFSET | BLT RS, x0, OFFSET | Branch if Less Than Zero,如果 RS < 0,跳转到OFFSET |
BLEZ | BLEZ RS, OFFSET | BGE x0, RS, OFFSET | Branch if Less or EQual Zero,如果 RS <= 0,跳转到OFFSET |
BGTZ | BGTZ RS, OFFSET | BLT x0, RS, OFFSET | Branch if Greater Than Zero,如果 RS > 0,跳转到OFFSET |
BGEZ | BGEZ RS, OFFSET | BGE RS, x0, OFFSET | Branch if Greater or EQual Zero,如果 RS <= 0,跳转到OFFSET |
JAL(Jump And Link)
语法 | JAL RD, LABEL | |
---|---|---|
例子 | jal x1, label |
JAL指令使用J-type编码格式。
JAL指令用于调用子过程(subroutine/function)。
子过程的地址计算方式:首先对20bits宽的IMM x 2后进行sign-extended,然后将符号扩展后的值和PC的值相加。因此该函数跳转的范围是以PC为基准,上下~+/- 1MB。
JAL指令的下一条指令的地址写入RD,保存返回地址。
实际编程时,用label给出跳转的目标,具体IMM值由编译器和链接器最终负责生成。
该函数跳转的范围是以PC为基准,上下~+/- 1MB。
如何解决更远距离的跳转?
AUIPC x6, IMM-20
JALR x1, x6, IMM-12
JALR(Jump And Link Register)
语法 | JALR RD, IMM(RS1) | |
---|---|---|
例子 | jalr x0, 0(x5) |
如果跳转不需要返回,可以利用 x0 代题 JAL 和 JALR 中的 RD。
伪指令
伪指令 | 语法 | 等价指令 | 例子 |
---|---|---|---|
J | J OFFSET | JAL x0, OFFSET | j leap |
JR | JR RS | JARL x0, 0(RS) | jr x2 |
所谓寻址模式指的是指令中定位操作数(oprand)或者地址的方式。
寻址模式 | 解释 | 例子 |
---|---|---|
立即数寻址 | 操作数是指令本身的一部分。 | addi x5, x6, 20 |
寄存器寻址 | 操作数存放在寄存器中,指令中指定访问的寄存器从而获取该操作数。 | add x5, x6, x7 |
基址寻址 | 操作数在内存中,指令通过指定寄存器(基址base)和立即数(偏移offset),通过base+offset的方式获得操作数在内存中的地址从而获取该操作数。 | sw x5, 40(x6) |
PC 相对地址 | 在指令中通过PC和指令中的立即数相加获得目的地址的值。 | beq x5, x6, 100 |
寄存器 | ABI名(编程用名) | 用途约定 | 谁负责在函数调用过程中维护这些寄存器 |
---|---|---|---|
x0 | zero | 读取时总为0,写入时不起任何效果 | N/A |
x1 | ra | 存放函数返回地址的值(return address) | Caller |
x2 | sp | 存放栈指针(stack pointer) | Callee |
x5-x7, x28-x31 | t0-t2, t3-t6 | 临时(temporaries)寄存器,Callee可能会使用这些寄存器,所以Callee不保证这些寄存器中的值在函数调用过程中保持不变,这意味着对于Caller来说,如果需要的话,Caller需要自己在调用Callee之前保存临时寄存器中的值。 | Caller |
x8, x9, x18-x27 | s0, s1, s2-s11 | 保存(saved)寄存器,Callee需要保证这些寄存器的值在函数返回后仍然维持函数调用之前的原值,所以一旦Callee在自己的函数中会用到这些寄存器,则需要在栈中备份并在退出函数时进行恢复。 | Callee |
x10, x11 | a0, a1 | 参数(argument)寄存器,用于在函数调用过程中保存第一个和第二个参数,以及在函数返回时传递返回值。 | Caller |
x12-x17 | a2-a7 | 参数(argument)寄存器,如果函数调用时需要传递更多的参数,则可以用这些寄存器,但注意用于传递参数的寄存器最多只有8个(a0-a7),如果还有更多的参数则需要利用栈。 | Caller |
伪指令 | 等价指令 | 描述 | 例子 |
---|---|---|---|
jal offset | jal x1, offset | 跳转 offset 制定位置,返回地址保存在 x1(ra) | jal foo |
jalr rs | jalr x1, 0(rs) | 跳转到 rs 中值所指定的位置,返回地址保存在 x1(ra) | jarl s1 |
j offset | jal x0, offset | 跳转到 offset 指定位置,不保存返回地址。 | j loop |
jr rs | jalr x0, 0(rs) | 跳转到 rs 中值所指定的位置,不保存返回地址。 | jr s1 |
call offset | auipc x1, offset[31:12]+offset[11] jalr x1, oofset[11:0](x1) | 长跳转调用函数 | call foo |
tail offset | auipc x6, offset[31:12]+offset[11] jalr x0, offset[11:0](x6) | 长跳转尾调用 | tail foo |
ret | jalr x0, 0(x1) | 从 Callee 返回 | ret |
函数起始部分(Prologue) |
---|
减少 sp 的值,根据本函数中使用 saved 寄存器的情况以及 local 变量的多少开辟栈空间。 |
将 saved 寄存器的值保存到栈中。 |
如果函数中还会调用其他的函数,则将 ra 寄存器的值保存到栈中。 |
函数退出部分(Epilogue) |
---|
从栈中恢复 saved 寄存器 |
如果需要的话,从栈中恢复 ra 寄存器 |
增加 sp 的值,恢复到进入函数之前的状态 |
调用 ret 返回 |
asm [volatile](
“汇编指令”
: 输出操作数列表(可选)
: 输入操作数列表(可选)
: 可能影响的寄存器或者存储器(可选)
)
例子:
更多见【参考三】