PA1
本文是在完成后才整理的文章,因而可能会有所遗漏部分修改内容,经过整理仍旧比较混乱,可阅读性很低,只作为学习记录参考
且本人能力有限,代码可能会出现一些未检查出来的bug
计算机可以没有寄存器吗?
理论可行,这里指的是ISA的通用寄存器,例如堆栈机等
NEMU阶段
配置系统kconfig
使用make menuconfig打开需要的即可。
makefile阅读
$@代指冒号前面
$<是冒号后面的
cc一般是编译器
cflags是编译参数
%:表示通配符,用于匹配任何字符,包括空字符。例如,%.c 匹配所有以 .c 结尾的文件名。
可以先查看make过程中都运行了哪些命令,然后反过来理解$(CFLAGS)等变量的值。 为此, 我们可以键入make -nB, 它会让make程序以”只输出命令但不执行”的方式强制构建目标. 运行后, 你可以看到很多形如
init_monitor()函数
初始化函数init_monitor(),将客户程序读入到客户计算机中。
`代码如下:
1 | void init_monitor(int argc, char *argv[]) { |
解析函数parse_args():
1 | static int parse_args(int argc, char *argv[]) { |
getopt_long()的功能和使用方法是:
函数原型:
1
2
3int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);argv:选项元素。以
-开头,然后紧跟一个选项字符。当重复调用函数getopt_long的时候,函数会连续返回选项元素对于里面的
option结构体的table,是选项表,其实现方法为1
2
3
4
5
6struct option {
const char *name;
int has_arg;
int *flag;
int val;
};name是选项的名称;
has_arg:
若为0或者
no_argment,则不需要参数。若为1或者
required_argument,则需要参数flag: 如果为 NULL,返回 val。否则,将 val 存储到 flag 指向的位置,并返回 0
val:如果 flag 为 NULL,则返回该值,否则存储在 flag 指向的变量中
剩余都是一些初始化的内容。
load_img() 函数
将客户程序镜像加载到模拟内存的起始位置(RESET_VECTOR)。
c
1 | static long load_img() { |
- 若未指定镜像文件,则使用内置镜像(大小为 4096 字节)。
- 使用
guest_to_host(RESET_VECTOR)将客户机物理地址转换为主机内存指针,然后直接读取文件内容至该区域。
这里就是Monitor的初始化工作,它的功能就是设置好ISA和默认程序,初始化内存和cpu状态。
strtok()
1 |
|
参数
- str: 要分割的字符串。在第一次调用时,传入要分割的字符串;后续调用时,传入
NULL,表示继续分割同一个字符串。 - delim: 分隔符字符串。
strtok()会根据这个字符串中的任意一个字符来分割str。
返回值
返回指向下一个标记的指针。如果没有更多的标记,则返回 NULL。
注意事项
修改原字符串: strtok() 会修改传入的字符串,将分隔符替换为 \0(空字符)。因此,原始字符串会被破坏。
不可重入: strtok() 使用静态缓冲区来保存状态,因此它不是线程安全的。如果在多线程环境中使用,可以考虑使用 strtok_r()(可重入版本)。
连续分隔符: 如果字符串中有连续的分隔符,strtok() 会忽略它们,并返回下一个有效的标记。
模拟cpu执行
1 | // 译码相关代码 |
关于优雅的的退出
是因为cmd_q返回值为-1所以调用了return is_exit_status_bad(),直接exit(0)就行了。
基础设施
单步执行
代码实现
1 |
|
这里我用了sscanf来弄这个,不过这里我没有做负数的检查什么的。
打印寄存器
代码实现
1 | static int cmd_info(char *args) { |
这里我是把两者都实现了关于w和r的命令,关于w要后面再说
1 | void isa_reg_display() { |
直接通过isa这个接口来进行输出所有寄存器就行了。
扫描内存
代码实现
1 |
|
设计思路:先用 expr() 求出起始地址,再循环调用 paddr_read() 读取物理内存,每次读 4 字节(一个 word)。
1 | word_t expr(char *e, bool *success) { |
设计主要解决两个问题:词法解析 和 区分乘号与解引用运算符。
- 词法分析:调用
make_token(e)将输入字符串e切分成一个 token 序列(存于全局数组tokens[]中),每个 token 包含类型(如数字、运算符、括号等)和值。如果词法分析失败(例如出现非法字符),直接返回 0 并标记success = false。 - 二义性消除:遍历所有 token,根据上下文将
'*'重新分类为 乘法(二元运算符)或 解引用(一元运算符)。 - 表达式求值:调用
eval(0, nr_token - 1, success)对 token 序列进行递归下降求值,得到最终结果。
区分*的两种作用
* 既可以表示乘法(如 a * b),也可以表示指针解引用(如 *p 或 *(p+1))。词法分析器无法单靠一个字符判断其含义,必须借助上下文。
如果 * 前面的 token 是运算符或左括号,那么它就是一个一元解引用运算符;否则它就是二元乘法运算符。
make_token
1 | static bool make_token(char *e) { |
通过正则表达式来记录token,这个是最终的版本,一开始肯定不是这样的,这是经过一次次完善出来的
函数依赖一个全局的规则表 rules[],每个规则包含:
- 正则表达式字符串(如
"[0-9]+"匹配十进制数) - 对应的 token 类型(如
TK_DEC) - 预编译的正则表达式
re[i]
1 | static struct rule { |
这些规则按照优先级顺序存储在数组中。匹配时按顺序尝试,第一个完整匹配(pmatch.rm_so == 0 表示从当前位置开始匹配)的规则胜出,从而自然处理了关键字的优先级(例如 "0x" 前缀应在普通数字之前匹配)。
eval
1 |
|
思路就是eval(p, q) 计算从 tokens[p] 到 tokens[q] 这一子表达式的值,采用分治法:
- 处理一元运算符(负号
-和 解引用TK_DEREF) - 处理括号(递归剥离最外层括号)
- 处理数字、寄存器
- 找到主运算符(最低优先级的二元运算符),递归计算左右子表达式,再应用运算
负号 '-' 在词法分析时被标记为普通运算符,需要根据上下文区分一元负号还是二元减法。
ps:此处负号的处理仍有问题,会把整个后面都取负号。
判断规则:
- 位于表达式开头(
p == 0)→ 一元负号 - 前一个 token 是左括号或运算符 → 一元负号
- 否则为二元减法(后面由主运算符逻辑处理)
解引用也是一元运算符,其操作数是后续的子表达式(应计算出地址),然后从客户机物理内存中读取 4 字节的值。
find_main_op
1 |
|
扫描 tokens[p..q] 区间,忽略括号内的内容,找出优先级最低(即 pri 数值最大)的二元运算符,并返回其下标。若找不到任何运算符则返回 -1。这个结果将被 eval 函数用于递归求值。
同优先级处理:当遇到优先级相同(pri == best_pri)的运算符时,选择最右边的那个(i > op_pos),这实现了左结合。例如 a - b - c 被解释为 (a - b) - c 而不是 a - (b - c)。
在 eval 函数中,当表达式不是一元运算符、括号或值时,会调用 find_main_op(p, q) 获得主运算符位置 op,然后递归计算 eval(p, op-1) 和 eval(op+1, q),最后根据 tokens[op].type 执行运算。
表达式求值
见上面
表达式生成器
1 | /*************************************************************************************** |
整体思路
- 随机生成表达式:递归构造一个合法的算术表达式,限制深度避免溢出。(虽然还是有溢出)
- 嵌入 C 程序:将表达式字符串填入一个 C 程序的
main函数模板中。
1 | static void gen_rand_expr_rec() { |
监视点
1 | wp_pool[32](静态数组) |
每个监视点存储:
NO:编号expr:表达式字符串(动态分配)old_val:上次求值结果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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
typedef struct watchpoint {
int NO;
struct watchpoint *next;
char *expr;
word_t old_val;
} WP;
static WP wp_pool[NR_WP] = {};
static WP *head = NULL, *free_ = NULL;
static void free_wp(WP *wp);
void init_wp_pool() {
int i;
for (i = 0; i < NR_WP; i ++) {
wp_pool[i].NO = i;
wp_pool[i].next = (i == NR_WP - 1 ? NULL : &wp_pool[i + 1]);
}
head = NULL;
free_ = wp_pool;
}
static WP* alloc_wp() {
if (free_ == NULL) {
printf("No free watchpoint slots.\n");
return NULL;
}
WP *wp = free_;
free_ = free_->next;
wp->next = NULL;
wp->expr = NULL;
return wp;
}
bool free_wp_by_no(int no) {
WP *prev = NULL;
WP *curr = head;
while (curr) {
if (curr->NO == no) {
// 从链表中移除
if (prev) {
prev->next = curr->next;
} else {
head = curr->next;
}
// 释放监视点
free_wp(curr);
printf("Watchpoint %d deleted\n", no);
return true;
}
prev = curr;
curr = curr->next;
}
return false;
}
bool check_watchpoints() {
WP *wp = head;
bool triggered = false;
while (wp) {
bool success;
word_t new_val = expr(wp->expr, &success);
if (success && new_val != wp->old_val) {
printf("\nWatchpoint %d: %s\n", wp->NO, wp->expr);
printf("Old value = %u (0x%08x)\n", wp->old_val, wp->old_val);
printf("New value = %u (0x%08x)\n", new_val, new_val);
wp->old_val = new_val;
triggered = true;
}
wp = wp->next;
}
return triggered;
}
void display_watchpoints() {
if (head == NULL) {
printf("No watchpoints.\n");
return;
}
printf("Num Expression\n");
WP *wp = head;
while (wp) {
printf("%d %s\n", wp->NO, wp->expr);
wp = wp->next;
}
}
static void free_wp(WP *wp) {
if (wp->expr) {
free(wp->expr);
wp->expr = NULL;
}
wp->next = free_;
free_ = wp;
}
WP* new_wp(char *expr_str) {
WP *wp = alloc_wp();
if (wp == NULL) return NULL;
bool success = false;
word_t val = expr(expr_str, &success);
if (!success) {
printf("Invalid expression: %s\n", expr_str);
free_wp(wp);
return NULL;
}
wp->expr = strdup(expr_str);
wp->old_val = val;
wp->next = head;
head = wp;
printf("Watchpoint %d: %s\n", wp->NO, wp->expr);
return wp;
}
WP* get_wp_head() {
return head;
}init_wp_pool()
初始化对象池,将
wp_pool中的节点串成一条空闲链表,活跃链表置空。c
1
2
3
4
5
6
7
8
9void init_wp_pool() {
int i;
for (i = 0; i < NR_WP; i ++) {
wp_pool[i].NO = i;
wp_pool[i].next = (i == NR_WP - 1 ? NULL : &wp_pool[i + 1]);
}
head = NULL;
free_ = wp_pool;
}- 每个节点的
NO赋值为其下标。 - 节点按数组顺序链接:
wp_pool[i]的next指向wp_pool[i+1],最后一个指向NULL。 free_指向第一个空闲节点(wp_pool[0])。
alloc_wp()
从空闲链表中分配一个监视点节点。
c
1
2
3
4
5
6
7
8
9
10
11static WP* alloc_wp() {
if (free_ == NULL) {
printf("No free watchpoint slots.\n");
return NULL;
}
WP *wp = free_;
free_ = free_->next;
wp->next = NULL;
wp->expr = NULL;
return wp;
}- 如果
free_为空(已用完所有监视点),报错返回NULL。 - 取下
free_指向的第一个节点,并更新free_指向下一个。 - 初始化新节点的
next = NULL,expr = NULL(调用者负责填充)。
free_wp()
将节点放回空闲链表(释放资源)。
c
1
2
3
4
5
6
7
8static void free_wp(WP *wp) {
if (wp->expr) {
free(wp->expr);
wp->expr = NULL;
}
wp->next = free_;
free_ = wp;
}- 先释放
wp->expr占用的动态内存(若存在),并置空。 - 将
wp插入空闲链表头部(头插法)。
free_wp_by_no()
根据监视点编号删除一个监视点。
c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20bool free_wp_by_no(int no) {
WP *prev = NULL;
WP *curr = head;
while (curr) {
if (curr->NO == no) {
// 从活跃链表中移除
if (prev) {
prev->next = curr->next;
} else {
head = curr->next;
}
free_wp(curr); // 放回空闲链表
printf("Watchpoint %d deleted\n", no);
return true;
}
prev = curr;
curr = curr->next;
}
return false;
}- 遍历活跃链表
head,找到编号为no的节点。 - 调整前驱节点的
next指针将其跳过。 - 调用
free_wp将节点放回空闲池并释放表达式字符串。 - 返回
true表示删除成功,否则false。
new_wp()
创建一个新的监视点。
c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20WP* new_wp(char *expr_str) {
WP *wp = alloc_wp();
if (wp == NULL) return NULL;
bool success = false;
word_t val = expr(expr_str, &success);
if (!success) {
printf("Invalid expression: %s\n", expr_str);
free_wp(wp);
return NULL;
}
wp->expr = strdup(expr_str);
wp->old_val = val;
wp->next = head;
head = wp;
printf("Watchpoint %d: %s\n", wp->NO, wp->expr);
return wp;
}- 每个节点的
步骤:
- 分配一个空闲节点。
- 调用
expr()对表达式字符串进行预求值,验证其合法性。- 若不合法,释放节点并报错返回。
- 复制表达式字符串(
strdup)到wp->expr。 - 保存当前求值结果到
old_val。 - 头插法将新节点插入活跃链表(
next = head; head = wp)。 - 打印成功信息并返回节点指针。
check_watchpoints()
遍历所有监视点,重新求值并与旧值比较,若改变则输出信息并返回 true。
c
1 | bool check_watchpoints() { |
- 依次对每个活跃监视点调用
expr(wp->expr, &success)求新值。 - 如果求值成功且新值与
old_val不同,则打印新旧值(十进制和十六进制),更新old_val,并设置triggered = true。 - 返回值表示是否有监视点被触发(模拟器可据此暂停执行)。
display_watchpoints()
打印所有已设置的监视点。
c
1 | void display_watchpoints() { |
- 简单遍历活跃链表,输出编号和表达式。
get_wp_head()
返回活跃链表头指针。
c
1 | WP* get_wp_head() { |
isa_reg_str2val()
1 | word_t isa_reg_str2val(const char *s, bool *success) { |
- 去除可选的前缀
$:如果字符串以$开头,则跳过该字符,得到纯寄存器名。这样用户既可以使用$pc也可以使用pc来访问程序计数器。 - 特殊处理程序计数器
pc:若字符串是"pc",则直接返回全局 CPU 状态中的cpu.pc,并设置成功标志为true。 - 匹配通用寄存器:遍历预定义的寄存器名数组
regs[32](该数组与cpu.gpr[32]的索引一一对应),找到匹配项后返回相应通用寄存器的值。 - 支持数字索引形式:如果字符串以
'x'开头,则尝试将后续字符解析为整数(例如x0、x31)。若解析得到的索引在 0~31 范围内,则直接返回cpu.gpr[idx]。 - 失败处理:如果以上均未匹配,则设置
*success = false,并返回 0。
execute()(在cpu-exec.c里面)
1 |
|
关于前面测试的事情
1 |
|
我修改了这个参数选取,增加了-t来测试,往初始化里面加了一个专门测试的入口
1 | void init_monitor(int argc, char *argv[]) { |
1 | void sdb_expr_test(const char *filename) { |
riscv32有哪几种指令格式?
R-type, I-type, S-type, U-type 四种核心格式,并在随后添加了基于立即数处理的变种 B-type 和 J-type。因此,总共有 6种 基本指令格式
LUI指令的行为是什么?
具体来说,该指令会把一个 20位的立即数 加载到目的寄存器 rd 的高20位,而寄存器的低12位则被填充为0
mstatus寄存器的结构是怎么样的?
mstatus 是一个 XLEN 位的 CSR (Control and Status Register)。它包含了许多控制处理器状态的字段,例如中断使能(MIE等)、异常处理前的特权级(MPP)、以及浮点单元状态(FS)等。具体位域布局可在手册“Machine Status Registers”章节的详细说明中找到
Shell命令
- 代码行数统计方法
- 统计所有 .c 和 .h 文件行数(含空行和注释):使用
find nemu/ -name "*.c" -or -name "*.h" | xargs cat | wc -l命令。 - 统计所有 .c 和 .h 文件代码行数(除去空行):使用
find nemu/ -name "*.c" -or -name "*.h" | xargs cat | grep -v '^[[:space:]]*$' | wc -l命令。 - 统计PA1阶段新增或修改的代码行数:使用
git diff pa0 --shortstat -- . ":(exclude)nemu/tools"命令。 - 将统计命令集成到Makefile中:可以在
nemu/Makefile文件中增加一个count目标,这样在终端中直接执行make count即可自动统计并输出结果。
- 统计所有 .c 和 .h 文件行数(含空行和注释):使用
编译选项解析
在 nemu/scripts/build.mk 文件中可以看到 -Wall 和 -Werror 这两个选项。它们的含义是:
-Wall:-W表示Warning,all表示“所有”。它用于开启GCC的大部分常见警告信息,帮助发现潜在的代码隐患。-Werror:-Werror的含义是将所有编译警告(Warning)当作错误(Error)处理。只要代码触发了任何警告,编译过程就会失败并停止,强迫开发者修复所有警告。