前言
这是一篇关于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、J6种.
- 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);
}