前言
这是一篇关于ICS-PA的二周目学习记录. 作者在完成YSYX项目后编写此文, 因此内容可能存在遗漏或跳阶段的情况. 如有好的想法或疑问, 欢迎通过邮件与我交流.
目前YSYX项目使用的是riscv32e
架构. 经过考虑, 本文及后续分享将基于riscv32e
进行. 但在与riscv64
和riscv32
有特殊区别时, 会特别说明.
任务
PA1的任务并不复杂, 主要以思考题为主.
思考题: 计算机可以没有寄存器吗?
我认为是可行的, 但实现起来可能会有一定难度. 假设使用内存替代寄存器, 性能会显著下降, 且编程时需要频繁操作内存地址, 这无疑增加了编程的复杂性.
思考题: 尝试理解计算机如何计算.
基本流程如下:
PC
初值为0
, 取出地址0
内的指令mov r1, 0
, 执行后r1
寄存器储存数值为0
, 然后PC
更新为1
.PC
值为1
, 取出地址1
内的指令mov r2, 0
, 执行后r2
寄存器储存数值为0
, 然后PC
更新为2
.PC
值为2
, 取出地址2
内的指令addi r2, r2, 1
, 执行后r2
寄存器储存数值为当前r2
值加1
, 然后PC
更新为3
.PC
值为3
, 取出地址3
内的指令add r1, r1, r2
, 执行后r1
寄存器储存数值为当前r1
值加r2
值, 然后PC
更新为4
.PC
值为4
, 取出地址4
内的指令blt r2, 100, 2
, 执行后若r2
小于100
,PC
更新为2
.PC
值为5
, 取出地址5
内的指令jmp 5
, 执行后PC
更新为5
.
编程题: 从状态机视角理解程序运行.
( 0, x, x) -> ( 1, 0, x) -> ( 2, 0, 0) ->
( 3, 0, 1) -> ( 4, 1, 1) -> ( 5, 1, 2) ->
( 6, 3, 2) -> (..., ...., ...) -> (199, 4851, 99) ->
(200, 4950, 99) -> (201, 4950, 100) -> (202, 5050, 100)
思考题: kconfig生成的宏与条件编译.
MUXDEF
主要通过拼接__P_DEF_
和macro
后是否为__P_DEF_0
或__P_DEF_1
来实现. 若成立, 则展开为X,
, 在CHOOSE2nd
中起到占位作用, 从而保留前者;否则, 展开后保留后者. 需要注意的是, macro
必须是1
或0
, 其他内容无效.
思考题: 为什么全部都是函数?
将复杂或重复的功能进行分段封装, 有利于理清思路与后期维护.
思考题: 参数的处理过程.
操作系统提供的功能, 用于命令行参数的获取.
解决运行NEMU报错
[src/monitor/monitor.c:20 welcome] Exercise: Please remove me in the source code and compile NEMU again.
riscv32-nemu-interpreter: src/monitor/monitor.c:21: welcome: Assertion `0' failed.
根据报错信息, 可直接定位到src/monitor/monitor.c:20
处的assert
断言满足了条件. 找到对应代码后, 即可正常运行.
思考题: 究竟要执行多久?
cpu_exec
本身接受一个参数, 表示运行的步数, 类型为uint64_t
. 若传入-1
, 会自动转换为64位整数的最大值(2^64 - 1). 一般程序并不会运行这么多指令, 因此可视为一直运行直到结束.
思考题: 潜在的威胁(无符号溢出是否未定义).
根据RTFM的结果, 有符号溢出是未定义的, 但无符号溢出不是.
思考题: 谁来指示程序的结束?
实际上, 在main
函数前后都存在一些行为, 这些行为大多是操作系统为程序提供的支持, 例如参数的传入. 甚至可以通过一些系统调用, 在main
函数结束后继续运行某些函数.
思考题: 有始有终(程序的开始与结束).
在GNU/Linux
上, 程序在execve
系统调用开始时算作启动. 操作系统完成各项初始化后, 会进入main
函数运行程序内的内容. 而在main
函数结束并返回0
后, 会调用exit
系统调用, 操作系统开始回收资源, 结束整个进程.
编程题: 优美地退出.
判断nemu_state.halt_ret
的值后, 再将nemu_state.state
赋值为NEMU_QUIT
.
编程题: 简易调试器.
- 单步执行
- 函数
void cpu_exec(uint64_t n)
可以执行n
步.
- 函数
- 打印寄存器
- 宏定义
#define gpr(idx) (cpu.gpr[check_reg_idx(idx)])
可返回对应寄存器的值.
- 宏定义
- 扫描内存
- 函数
word_t vaddr_read(vaddr_t addr, int len)
有相关实现.
- 函数
- 表达式求值
- 实现稍微复杂一点, 但很适合锻炼编程. 下面给出大致思路:
- 完成各类符号的识别, 包括数字、运算符、括号、寄存器等. 若想功能更强, 可加入
pc
、csr
寄存器等. 特别注意类似* 乘号
与* 地址取值
,- 减号
与- 负号
同字符不同含义的情况. - 确定每一种运算的优先级, 大致排序为(具体情况请以手册为准): (括号)->(单目运算: 取反、取否、地址取值)->(乘除取余)->(加减)->(位移)->(比较)->(相等、不等)->(与)->(或)->(数字、寄存器、
pc
、csr
等) - 遍历整个表达式, 找到优先级最低的运算符处进行切分, 递归计算子表达式, 在递归最深处即为最高优先级子项, 递归返回结果即可. 单目三目运算与双目略有不同, 对应修改即可.
- 完成各类符号的识别, 包括数字、运算符、括号、寄存器等. 若想功能更强, 可加入
- 下面为一个简单伪代码例子, 仅计算加减乘除:
- 实现稍微复杂一点, 但很适合锻炼编程. 下面给出大致思路:
/*
1*2+3*8/2切分顺序
递归层数| 分割点
1 | +
2 | * *
3 | 1 2 3 /
4 | 8 2
*/
int expression_value(int start,int end) {
if (start > end) {
assert(0);
}
int cut_addr = cut_expression(start, end); // 找到最低优先级**运算符**
if (is_pare_parenthesis(start, end)) { // 括号配对, 拆括号
return expression_value(start + 1, end - 1);
}
else if (start == end) {
switch (tokens[start].type) {
case TK_NUM:
answer = atoi(tokens[start].str);
return answer;
default: assert(0);
}
}
else {
switch (tokens[cut_add].type) {
case TK_PLUS: return expression_value(start, cut_add - 1) + expression_value(cut_add + 1, end); break;
case TK_TIMES: return expression_value(start, cut_add - 1) * expression_value(cut_add + 1, end); break;
case ...;
}
}
}
- 监视点
- 监视点实现比较简单, 主要是操作链表, 这里就不展示了.
- 监视点自动停止运行只需要在
static void trace_and_difftest(Decode *_this, vaddr_t dnpc)
函数中添加判断函数并设置nemu状态即可:
if (is_change()) {
nemu_state.state = NEMU_STOP;
}
编程题: 表达式生成器.
人生苦短, 我用Python.
必答题
必答题就不全都写了, 只完成部分:
- riscv32有哪几种指令格式?
- 单讨论基础指令集的话, 有
R
、I
、S
、U
、B
、J
6种.
- riscv32的LUI指令的行为是什么?
- 将立即数左移20位存入寄存器.
- riscv32的mstatus寄存器的结构是怎么样的?
- 除去保留位, 每一位(两位)都表示某一个状态, 比较重要的有
MIE[3]
,MPP[12:11]
,MPIE[7]
等.
- 解释gcc中的-Wall和-Werror有什么作用?为什么要使用-Wall和-Werror?
-Wall
: 启用警告,-Werror
: 警告视为错误. 主要是为了提高代码质量, 减少潜在的错误.
小建议
后续任务100%会出现各类奇怪的bug, debug是必然的, 而且强度很高, 所以最好在PA1时就将sdb的功能做全一些.
除非你想当printf调试仙人.
下面给出作者为sdb写过的额外命令与扩展的功能:
- 功能
- 直接回车执行缓存指令, 默认为上一条.
- 设置变量.
- 记录最近的n条命令.
- 参数解析
- 输入字符串, 输出二维字符串数组指针.
- 某一项参数若被单引号包裹, 意为为表达式, 计算结果数值存入字符串的前4字节(类型强转, 64位的话就是8字节).
- 命令
si
指令变种sr
: 执行一步并打印寄存器.sm
: 执行一步, 若为访存, 则打印访存信息.- …
- 格式化输出, 增加颜色输出, 分割线等.
一行打印一个寄存器看起来不累吗 history
: 无参数时打印最近10条命令, 有参数则将缓存的指令替换为该参数指向的指令, 回车就能使用.
// 一个栗子
void isa_reg_display() {
for (int i = 0; i < MUXDEF(CONFIG_RVE, 16, 32) / 4; i++) {
for (int j = 0; j < 4; j++) {
printf(ANSI_FG_GREEN "%-3s" ANSI_NONE ": " FMT_WORD "\t", reg_name(i*4+j), gpr(i*4+j));
}
printf("\n");
}
printf(ANSI_FG_RED "%-7s" ANSI_NONE ": " FMT_WORD "\n", "pc", cpu.pc);
printf(ANSI_FG_RED "%-7s" ANSI_NONE ": " "%d" "\n", "mod", cpu.mode);
printf(ANSI_FG_RED "%-7s" ANSI_NONE ": " FMT_WORD "\n", "mepc", cpu.csr.mepc);
printf(ANSI_FG_RED "%-7s" ANSI_NONE ": " FMT_WORD "\n", "mcause", cpu.csr.mcause);
printf(ANSI_FG_RED "%-7s" ANSI_NONE ": " FMT_WORD "\n", "mstatus", cpu.csr.mstatus);
}