《RISC-V 体系结构编程与实践(第2版)》——基础指令集
[复制链接]
本帖最后由 FuShenxiao 于 2025-1-8 10:49 编辑
在结束了考研/期末/毕设三者中的前两个之后,终于有时间来做测评了,这本书也是放了一个多月了才重新拿起来看。
在正式食用这本书之前,需要对系统进行配置,我采用的是利用VMware虚拟机在移动硬盘上安装Ubuntu20.04,这是由于去年8月微软的补丁,导致u盘启动可能会被认为是不安全的。我参考的是这篇CSDN上的文章:利用Vmware将Ubuntu系统安装到移动硬盘_如何做移动vmware的渗透镜像部署到移动硬盘-CSDN博客。如此制作好的移动硬盘就可以以双系统的方式启动。
随后安装QEMU实验平台以及其他相关依赖,即可在QEMU中进行RISC-V相关的实验。
基础指令集的知识似乎是通俗易懂的,各位通过其字面意思也能理解这些指令的含义,于是我打算直接借助实验和思考题对本章内容进行讲解。
实验3-1:熟悉加载指令集
(1)将0x80200000加载到a0寄存器,将立即数16加载到a1寄存器;(2)从0x80200000地址中读取4字节的数据;(3)从0x80200010地址中读取8字节的数据;(4)给出lui指令的直行结果
li rd, imm表示把imm(立即数)加载到寄存器中
lw rd, offset(rs1)表示以rs1寄存器的值为基地址,在偏移offset的地址处加载4字节数据,经过符号扩展之后写入目标寄存器rd中
ld rd, offset(rs1)表示以rs1寄存器的值为基地址,在偏移offset的地址处加载8字节数据,写入寄存器rd中
lui rd, imm表示先把imm(立即数)左移12位,然后进行符号扩展,最后把结果写入rd寄存器中
符号扩展指将得到的结果扩展为32位或64位,其高字节部分填充为1
同理,零扩展高字节部分填充为0
实现代码如下:
load_store_test:
li a0, 0x80200000
li a1, 16
li t0, 0x80200000
li t1, 0x80200010
lw a2, (t0)
ld a3, (t1)
lui t0, 0x8034f
lui t1, 0x400
ret
在GDB调试器中查看得到结果如下:
实验3-2:PC相对寻址
auipc rd, imm将imm(立即数)左移12位并带符号扩展到64位后,得到一个新的立即数,这个新的立即数是一个有符号的立即数,再加上当前PC值,然后存储到rd寄存器中
addi rd, rs1, imm将rs1寄存器的值和12位的立即数imm相加,并将结果存入rd寄存器中
auipc指令通常和addi指令联合使用来实现32位地址空间的PC相对寻址。其中auipc指令可以寻址与被访问地址按4KB对齐的地方,即被访问地址的高20位;addi指令可以在[-2048, 2047]范围内寻址,即被访问地址的低12位。
代码实现如下:
@define MY_OFFSET -2048
pc_related_test:
auipc t0, 1
addi t0, t0, MY_OFFSET
ld t1, MY_OFFSET(t0)
ret
得到结果如下:
实验3-3:memcpy()函数的实现
sd rs2, offset(rs1)将rs2寄存器的值存储到以rs1寄存器的值位基地址加上offset的地址处
blt rs1, rs2, label表示如果rs1寄存器的值小于rs2寄存器的值,则跳转到label处
代码实现如下:
my_memcpy_test:
li t0, 0x80200000
li t1, 0x80210000
addi t2, t0, 32
.loop:
ld t3, (t0)
sd t3, (t1)
addi t0, t0, 8
addi t1, t1, 8
blt t0, t2, .loop
ret
得到结果如下,可以看到地址0x80200000地址的内容已经复制到0x80210000地址处
实验3-4:memset()函数的实现
memset.c代码如下:
#include "memset.h"
extern void *__memset_16bytes_asm(void *s, unsigned long val, unsigned long count);
static void __memset_16bytes_inline_asm(void *p, unsigned long val,
int count)
{
int i = 0;
asm volatile (
"1: sd %[val], (%[p])\n"
"sd %[val], 8(%[p])\n"
"addi %[i], %[i], 16\n"
"blt %[i], %[count], 1b"
: [p] "+r" (p), [count]"+r" (count), [i]"+r" (i)
: [val]"r" (val)
: "memory"
);
}
static void *__memset_1bytes(void *s, int c, size_t count)
{
char *xs = s;
while (count--)
*xs++ = c;
return s;
}
static void *__memset(char *s, int c, size_t count)
{
char *p = s;
unsigned long align = 16;
size_t size, left = count;
int n, i;
unsigned long address = (unsigned long)p;
unsigned long data = 0ULL;
/* 这里c必须转换成unsigned long类型
* 否则 只能设置4字节,因为c变量是int类型
*/
for (i = 0; i < 8; i++)
data |= (((unsigned long)c) & 0xff) << (i * 8);
/*1. check start address is align with 16 bytes */
if (address & (align - 1)) {
//fixme: 这里应该是 对齐的后半段
size = address & (align - 1);
size = align - size;
__memset_1bytes(p, c, size);
p = p + size;
left = count - size;
}
/*align 16 bytes*/
if (left > align) {
n = left / align;
left = left % align;
#if 0
__memset_16bytes_asm(p, data, 16*n);
#else
__memset_16bytes_inline_asm(p, data, 16*n);
#endif
if (left)
__memset_1bytes(p + 16*n, c, left);
}
return s;
}
void *memset(void *s, int c, size_t count)
{
return __memset(s, c, count);
}
memset.S代码如下:
__memset_16bytes_asm:
li t0, 0
.loop:
sd a1, (a0)
sd a1, 8(a0)
addi t0, t0, 16
blt t0, a2, .loop
ret
得到结果如下:
实验3-5:条件跳转指令1
实现当a>=b时,返回值为0,否则返回值为0xffffffffffffffff
代码实现如下:
bltu与blt指令类似,只不过rs1寄存器的值和rs2寄存器的值位无符号数
compare_and_return:
bltu a0,a1,.L2
li a5,0
j .L3
.L2:
li a5,-1
.L3:
mv a0,a5
ret
在主程序中编写如下代码显示大小比较结果
val1 = compare_and_return(10, 9);
if (val1 == 0)
uart_send_string("compare_and_return ok\n");
else
uart_send_string("compare_and_return fail\n");
val2 = compare_and_return(9, 10);
if (val2 == 0xffffffffffffffff)
uart_send_string("compare_and_return ok\n");
else
uart_send_string("compare_and_return fail\n");
得到结果如下:
实验3-6:条件跳转指令2
当a=0时,返回b+2,否则返回b-1
代码实现如下:
sel_test:
beqz a0, .L4
addi a5, a1, -1
j .L5
.L4:
addi a5, a1, 2
.L5:
mv a0, a5
ret
在主程序中编写如下代码显示结果
val1 = sel_test(0, 9);
if (val1 == 11)
uart_send_string("sel test ok\n");
val2 = sel_test(5, 2);
if (val2 == 1)
uart_send_string("sel test ok\n");
得到结果如下:
实验3-7:子函数跳转
为了完成子函数跳转,首先将ra保存到栈,接着调用子函数,在完成子函数程序后从栈中恢复ra返回地址
具体代码如下:
branch_test:
/*把返回地址ra寄存器保存到栈里*/
addi sp,sp,-8
sd ra,(sp)
li a0, 1
li a1, 2
/* 调用add_test子函数 */
call add_test
nop
/* 从栈中恢复ra返回地址*/
ld ra,(sp)
addi sp,sp,8
ret
实验3-8:在汇编中实现串口输出功能
在boot.S中,程序的初始化中首先调用了__init_uart和print_asm两个子函数,随后才进入C语言的主程序
.globl _start
_start:
/* 关闭中断 */
csrw sie, zero
call __init_uart
call print_asm
/* 设置栈, 栈的大小为4KB */
la sp, stacks_start
li t0, 4096
add sp, sp, t0
/* 跳转到C语言 */
tail kernel_main
print_asm:
/*此时SP栈空间还没分配,把返回地址ra保存到临时寄存器中*/
mv s1, ra
la a0, boot_string
call put_string_uart
/*恢复返回地址ra*/
mv ra, s1
ret
.section .data
.align 12
.global stacks_start
stacks_start:
.skip 4096
.section .rodata
.align 3
.globl boot_string
boot_string:
.string "Booting at asm\n"
C语言中的main函数如下:
void kernel_main(void)
{
uart_init();
uart_send_string("Welcome RISC-V!\r\n");
while (1) {
;
}
}
可以看到最后首先输出"Booting at asm"接着输出"Welcome RISC-V!"
思考题1:RISC-V指令集有什么特点?
RISC-V采用模块化的设计方法,即设计一个最小的和最基础的指令集,这个最小的指令集可以完整地实现一个软件栈,其他特殊功能的指令集可以在最小指令集的基础上通过模块化的方式叠加实现,用于支持浮点数运算指令、乘法和除法指令等。
思考题2:RISC-V指令编码格式可以分成几类?
R类型:寄存器和寄存器算术指令
I类型:寄存器和立即数算术指令或者加载指令
S类型:存储指令
B类型:条件跳转指令
U类型:长立即数操作指令
J类型:无条件跳转指令
思考题3:什么是零扩展和符号扩展?
零扩展为计算机系统把小字节数据转换成大字节数据,将符号位扩展至所需要的位数,高位填充0
符号扩展与零扩展类似,高位填充1(0xFF)
思考题4:什么是PC相对寻址?
PC加上指令中给出的偏移量,得到操作数的实际地址。
思考题5:假设当前PC值位0x80200000,分别执行如下指令,a5和a6寄存器的值分别是多少?
auipc a5, 0x2
lui a6, 0x2
a5寄存器的值为 PC+sign_extend(0x2 << 12) = 0x80200000 + 0x2000 = 0x80202000
a6寄存器的值为 0x2 << 12 = 0x2000
思考题6:在下面的指令中,a1和t1寄存器的值分别是多少?
li t0, 0x8000008a00000000
srai a1, t0, 1
srli t1, t0, 1
srai为算数右移,高位需要进行符号扩展,a1寄存器的值为0xC000 0045 0000 0000
srli为立即数逻辑右移,高位需要进行零扩展,t1寄存器的值为0x4000 0045 0000 0000
思考题7:假设执行如下各条指令时当前的PC值位0x80200000,则下面那些指令是非法指令?
jal a0, 0x800fffff
jal a0, 0x80300000
两条指令都会出错,因为两条指令都超过了jal指令的跳转范围,jal指令的跳转范围为[0x8010 0000, 0x802F FFFE]
思考题8:请解析下面这条指令的含义
csrrw tp, sscratch, tp
先读取sscratch寄存器的旧值并写入tp寄存器,再将tp寄存器的旧值写入sscratch寄存器
用C语言伪代码实现如下:
tp = sscratch;
sscratch = tp;
思考题9:在RISC-V指令集中,如何实现大范围和小范围内跳转?
使用auipc与jalr指令实现基于当前PC偏移量±2GB范围的跳转
使用jal指令实现基于当前PC偏移量±1MB范围的跳转
|