本帖最后由 我爱下载 于 2023-4-10 16:42 编辑
# 读第3章和第4章的实验
第3章主要讲解了RV64的基础指令集,并进行了简单的实验。讲解的主要内容包括加载与存储指令,PC相对寻址,移位操作,位操作指令,算术指令,比较指令,无条件跳转指令,条件跳转指令及CSR指令。下图为本章思维导图。
第4章是函数调用规范与栈,重点讲解在RISC-V体系结构中,堆栈的布局与入栈出栈操作方法等。下图为本章的思维导图。
# 实验和验证
## 第3章 实验部分验证
### 实验3-1:熟悉加载指令
汇编代码:
```
.global load_store_test
load_store_test:
li a0, 0x22000
li a1, 16
lw t0, (a0)
ld t1, 16(a0)
lui t0, 0x80200
lui t1, 0x400
ret
```
程序加载后,设定断点到我们测试的程序处。
通过s 命令单步运行后,观察寄存器中的内容可见
a0 = 0x22000
a1 = 0x10
说明立即数装载命令执行是正确。
执行 ld t1,16(a0)后,可以观察过到t1的数值内容。
执行
lui t0, 0x80200
lui t1, 0x400
语句后,由于装载过程需要将立即数左移12位,在进行符号扩展,所以第一条指令后
t0 = 0xFFFFFFFF80200000
t1 = 0x400000
实验验证语句执行是正确的。
### 实验3-2:PC相对地址寻址
汇编代码:
```
.align 3
#define MY_OFFSET -2048
.global pc_related_test
pc_related_test:
auipc t0, 1
addi t0, t0, MY_OFFSET
ld t1, MY_OFFSET(t0)
ret
```
装载程序,并执行。
执行了auipc t0, 1语句后根据书中的解释t0 = PC +(1 << 12)
从图中我们可以看到PC = 0x23110, 实际当前PC为执行了auipc t0,1后的地址,因此执行命令时的PC值等于当前PC值减4,所以t0 = 0x2310c+4096 = 0x2410C 。
图中的寄存器证明了当前执行命令后获取的数据是正确的。
继续执行addi t0,t0,MY_OFFSET后,由于我们设定MY_OFFSET=-2048,所以相当于t0寄存器值减去2048。
t0 = 0x2410c – 2048 = 0x2309C ,图中寄存器的内容证明语句执行是正确的。
### 实验3-3:memcpy()函数的实现
汇编代码:
```
.global my_memcpy_test
my_memcpy_test:
li t0, 0x20000
li t1, 0x21000
addi t2, t0, 32
.loop:
ld t3, (t0)
sd t3, (t1)
addi t0, t0, 8
addi t1, t1, 8
bgt t2, t0, .loop
ret
```
由于dongshanpi-d1s使用了内部的存储器,地址为0x20000,因此测试的时候把书中的地址更改了一下。
ld和sd指令每次复制8个字节数据,因此这个循环执行了4次,这里使用了条件跳转伪指令bgt 完成地址比较。
通过ld命令读取0x21000开始的8字节数据到t3中,t3=0x4e4f47650300006f,通过x命令查看目标地址中数据为(0x21000) = 0x0300006f,(0x21004)=0x4e4f4765,确定复制功能已生效。
继续执行循环复制命令后,通过x命令查看源地址0x20000,和目标地址0x21000地址内容,发现32字节数据完全相同,说明memcpy功能执行正确。
### 实验3-4:memset()函数的实现
汇编代码:
16位地址对齐方式的:
```
.global my_memset_test
/*
a0 = void *s
a1 = c
a2 = count
void *memset(void *s, int c, size_t count)
{
char *xs = s;
while(count--)
*xs++ = c;
return s;
}
*/
my_memset_test:
/* 获取c的8位数据存储到t3中*/
slliw t2, a1, 24
srliw t3, t2, 24
mv t2, t3
mv t4, t3
li t0, 7
.fc:
sllit2, t2, 8
or t3, t3, t2
addi t0, t0, -1
bnez t0, .fc
mv t1, a0
mv t0, a2
li t5, 16
.loop:
sd t3, (t1)
sd t3, 8(t1)
sub t0, t0, t5
bnez t0, .loop
ret
```
16位地址不对齐时,逐个设置。
```
.global my_memset1_test
/*
a0 = void *s
a1 = c
a2 = count
void *memset(void *s, int c, size_t count)
{
char *xs = s;
while(count--)
*xs++ = c;
return s;
}
*/
my_memset1_test:
/* 获取c的8位数据存储到t3中*/
slliw t2, a1, 24
srliw t3, t2, 24
mv t2, t3
mv t1, a0
mv t0, a2
li t5, 1
.loop1:
sb t2, (t1)
sub t0, t0, t5
addi t1, t1, 1
bnez t0, .loop1
ret
```
my_memset1_test((void *)0x20000, 0x55, 16);
函数调用地址和数量都是16位对齐的。通过移位的方式构建一个寄存器t3,让
t3的内容是输入内容重复了8次。然后利用sd命令一次对内存地址完成8字节置位。
当程序循环完成后,16字节都设置成0x55 。
![689370](/data/attachment/forum/202304/10/141702gm6nbqtcm1on41on.jpg.thumb.jpg?rand=583.4816585314239)
my_memset1_test((void *)0x20000, 0x05, 15);
如果地址或数量不是16位对齐,采用逐个字节复制的方式来完成,利用sb命令。
### 实验3-5:条件跳转指令1
汇编代码:
```
.global my_3_5_test
my_3_5_test:
mv t0, a0
bltu t0, a1, .L35_1
li a0, 0
j .L35_2
.L35_1:
li a0, -1
.L35_2:
ret
```
根据书中简介,当进行函数调用时,参数是通过a0 – a7 进行传递的,并且返回值也是通过a0 - a7传递的。
my_3_5_test(10, 20);
a0=10
a1=20
根据题目要求,如果a小于b,返回值为0xffffffffffffffff 。这个值就是有符号数 -1 。所以 a0 = -1,执行正确。
my_3_5_test(20, 10);
a0=20
a1=10
根据题目要求,如果a大于等于b,返回值为0 。所以a0 = 0,执行正确。
### 实验3-6:条件跳转指令2
汇编代码:
```
.global my_3_6_test
my_3_6_test:
mv t0, a0
beq t0, zero, .L36_1
addi a0, a1, 2
j .L36_2
.L36_1:
addi a0, a1, -1
.L36_2:
ret
```
my_3_6_test(0, 10);
当执行如上函数时,可知a0 = 0,满足因此返回值a0 = a1+2=10+2=0x0C,因此执行是正确的。
my_3_6_test(1, 10);
当执行如上函数时,可知a0 = 1,满足因此返回值a0 = a1-1=10-1=0x09,因此执行是正确的。
### 实验3-7:子函数跳转
汇编代码
```
.global my_3_6_test
my_3_6_test:
mv t0, a0
beq t0, zero, .L36_1
addi a0, a1, 2
j .L36_2
.L36_1:
addi a0, a1, -1
.L36_2:
ret
.global bl_test
bl_test:
li a0, 10
li a1, 20
call my_3_6_test
ret
```
编写bl_test,通过call调用my_3_6_test函数。
执行bl_test()函数后,正确的调用了汇编函数my_3_6_test子函数。
## 第4章 实验部分验证
### 实验4-1:观察栈布局
程序代码:
```
.global add_c
add_c:
/*栈往下扩展16字节*/
addi sp,sp,-16
/*保存调用函数的返回地址ra到栈里*/
sdra,8(sp)
add a0, a0, a1
/* 从栈中恢复ra返回地址*/
ldra,8(sp)
/* SP回到原点*/
addi sp,sp,16
ret
.global func1
func1:
/*栈往下扩展16字节*/
addi sp,sp,-16
/*保存调用函数的返回地址ra到栈里*/
sdra,8(sp)
li a0, 1
li a1, 2
call add_c
/* 从栈中恢复ra返回地址*/
ldra,8(sp)
/* SP回到原点*/
addi sp,sp,16
ret
.global k_main
k_main:
/*栈往下扩展16字节*/
addi sp,sp,-16
/*保存调用函数的返回地址ra到栈里*/
sdra,8(sp)
call func1
/* 从栈中恢复ra返回地址*/
ldra,8(sp)
/* SP回到原点*/
addi sp,sp,16
ret
```
堆栈分布图
![689432](/data/attachment/forum/202304/10/163753jbvjei0baijw1br4.jpg.thumb.jpg?rand=4602.651172539018)
### 实验4-2:观察栈回溯
程序代码:
```
#include "types.h"
extern int sys_uart_printf(const char * fmt, ...);
struct stackframe {
unsigned long s_fp;
unsigned long s_ra;
};
extern char _text[], _etext[];
static int kernel_text(unsigned long addr)
{
if (addr >= (unsigned long)_text &&
addr < (unsigned long)_etext)
return 1;
return 0;
}
static void walk_stackframe(void )
{
unsigned long sp, fp, pc;
struct stackframe *frame;
unsigned long low;
const register unsigned long current_sp __asm__ ("sp");
sp = current_sp;
pc = (unsigned long)walk_stackframe;
fp = (unsigned long)__builtin_frame_address(0);
while (1) {
if (!kernel_text(pc))
break;
/* 检查fp是否有效 */
low = sp + sizeof(struct stackframe);
if ((fp < low || fp & 0xf))
break;
/*
* fp 指向上一级函数的栈底
* 减去16个字节,正好是struct stackframe
*/
frame = (struct stackframe *)(fp - 16);
sp = fp;
fp = frame->s_fp;
pc = frame->s_ra - 4;
// if (kernel_text(pc))
// sys_uart_printf("[0x%x - 0x%x]pc 0x%x\r\n", sp, fp, pc);
}
}
void dump_stack(void)
{
// sys_uart_printf("Call Frame:\r\n");
walk_stackframe();
}
```