实验一
郭高旭 ggx21@mails.tsinghua.edu.cn 2021010803
实验报告
-
实验一
-
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15la a0, 0x80400000
li t0, 10
li t3, 1
li t2, 1
loop:
sw t3, 0(a0)
add t3, t1, t2
addi a0, a0, 4
addi t0, t0, -1
mv t1,t2
mv t2,t3
bnez t0, loop
li a7, 10
jr ra -
结果
-
-
实验二
-
代码与结果
-
-
实验三
-
代码与结果(高八位
0x00000168
低八位0x6c8312d0
)
-
代码分析:
term:
-
结构
- test_programs函数:该函数用于检查是否可以运行所需的程序,如汇编器、反汇编器和二进制拷贝工具。
- output_binary函数:用于将二进制数据写入标准输出,兼容Python 2和Python 3。
- int_to_byte_string和byte_string_to_int函数:用于将整数和字节字符串相互转换。
- multi_line_asm和single_line_disassmble函数:这两个函数用于将汇编代码转换为二进制指令和将二进制指令反汇编为汇编代码。它们使用了系统中安装的汇编器和反汇编器。
- run_T、run_A、run_F、run_R、run_D、run_U、run_G函数:这些函数是与RISC-V处理器进行交互的主要函数。例如,
run_T
用于打印页表信息,run_A
用于写入汇编指令,run_R
用于读取寄存器的值,run_D
用于显示内存内容,run_U
用于读取内存内容并反汇编,run_G
用于运行用户程序。 - MainLoop函数:这是程序的主循环,用于接收用户输入的命令并执行相应的操作。
- InitializeSerial和InitializeTCP函数:用于初始化串口或TCP连接,根据命令行参数选择不同的初始化方式。
- Main函数:程序的入口点,根据命令行参数选择串口或TCP连接,然后进入主循环。
-
交互:见思考题第五问
kernel:
-
结构
-
evec.s #监控程序的入口点,是最先执行的代码 init.S #初始化,主要工作是为kernel分配资源 kernel32.ld kernel64.ld shell.S #主要功能实现 test.S # trap.S #定义了一些报错的消息 utils.S #读写串口的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
- 交互:同样见思考题的4,5问
## 思考题
### 1.risc-v与x86寻址方式异同
#### 异
1. **复杂寻址模式:** RV32I省略了x86-32中存在的复杂寻址模式。x86-32提供了多种寻址模式,例如基址寻址、间接寻址等,而RV32I采用了更简单的模式,只支持基地址寄存器和立即数的组合。
2. **歧视数据类型:** RV32I的寻址模式不会歧视任何数据类型,可以用于加载和存储不同类型的数据,而x86-32的寻址模式可能需要根据数据类型进行调整。
3. **堆栈指令:** x86-32提供了特殊的堆栈指令,例如push和pop,用于堆栈操作。相比之下,RV32I没有专门的堆栈指令,而是借助通用寄存器来实现堆栈操作。
#### 同
加载和存储的支持寻址模式是符号扩展 12 位立即数到基地址寄存器(在risc-v中是唯一支持的寻址模式)
### 2.阅读监控程序,列出监控程序的 19 条指令,请根据自己的理解对用到的指令进行分类,并说明分类原因。
- 运算类
ADD: ADDI, AND, ANDI, OR, ORI, SLLI, SRLI, XOR,AUIPC
- 跳转控制类
BEQ: BNE,JAL,JALR
- save&load类
LB: LW,LUI,SB,SW
lui主要是为了构造一个足够大的立即数
- 分类原因
分类方法对应着冯诺依曼架构计算机**(运算器、控制器、存储器、输入设备、输出设备)**,不考虑输入输出设备,运算类对应运算器,跳转控制类对应控制器,sl类对应存储器
### 3.term 是如何实现用户程序计时的。
在term.py的run_G函数是在kernel中执行用户risc-v汇编程序的主要代码
```python
def run_G(addr):
# ...
time_start = timer()
while True:
ret = inp.read(1)
if ret == b'\x07':
break
elif ret == b'\x81':
print('killed timeout program.')
break
elif ret == b'\x80':
trap()
output_binary(ret)
print('') #just a new line
elapse = timer() - time_start
print('elapsed time: %.3fs' % (elapse))
-
在kernel/common.h处定义了如下几个信号
1 |
在kernel/shell.s中的.OP_G:部分定义了kernel如何判断程序是否超时
-
li a0, SIG_TIMERSET(0x06)
,写开始计时信号,告诉终端用户程序开始运行 -
首先,它获取当前
mtime
计时器的值保存t1
(RV32)中。 -
然后,它将一个固定值
10000000
添加到mtime
,以表示用户程序的最大允许运行时间。 -
接着,它将新的值写入
mtimecmp
计时器比较寄存器,以设置超时时间。 -
如果 RV32 架构,它还会检查进位并更新高 32 位的
mtimecmp
寄存器。 -
如果超时,跳转到trap.s的userret_timeout,发送超时信号(0x81)
4.说明 kernel 是如何使用串口的。
主要使用了utsil.s中WRITE_SERIAL
和read_serial
两个函数
-
WRITE_SERIAL
函数负责向串口发送数据。它会不断检查串口是否就绪,一旦就绪,就将数据写入串口的传输寄存器。 -
READ_SERIAL
函数用于从串口接收数据。它会不断检查串口是否有可读数据,一旦有数据可读,就将数据从串口的接收寄存器读取出来。
这两个函数都采用了忙等待的方式,也就是它们会一直等待串口准备好(写入或读取),然后再执行相应的操作。
5.请问 term 如何检查 kernel 已经正确连入,并分别指出检查代码在 term 与 kernel 源码中的位置。
-
判断正确连入
- 通过InitializeTCP函数与kernel建立tcp链接
- 在主函数中使用test_program检查Term 所需的三个外部程序是否可以正常使用
- 在main函数中尝试读取来自内核的前33个字节信息(欢迎消息)
- 在main函数中会使用
outp.write(b'W')
发送一个'W'
命令,以探测内核的 XLEN(字长)设置: - 进入主循环(读取用户终端输入)
-
检查代码在term中的位置
主要是test_program和main两个函数
-
在kernel中的位置
kernel不需要特地检查是否已经与term进行了tcp链接,kernel只是在开始或者每次成功处理指令后跳转到
read_serial
检测串口是否有输入