这一篇博客中,我将完成PA1的收尾部分,我将完成完成对表达式求值的功能拓展,和程序监视点的实现,预计要花费较多的时间。可能很多内容也需要好好学习理解一下。
PA1 监视点
扩展表达式求值的功能
之前我们实现了算数表达式基本的求值(对整数的基本四则运算与结合律),但这些表达式都是常数组成的,他们的值不会发生变化,导致这样的表达式在监视点中没有任何意义,所以我们需要为我们的表达式拓展以下功能:
1 | <expr> ::= <decimal-number> |
那么首先我们就需要向框架中添加对应的解析,即更新程序的正则匹配逻辑:
1 | enum { |
这里需要注意两个问题:
一是 对于reg值得获取,框架代码在
/src/isa/$ISA/reg.c中提供了isa_reg_str2val()作为接口,它将返回名字为s的寄存器的值,且设置success用于指示是否匹配成功。二是 我们需要注意到对指针解引用的识别,对于符号
*它可能是乘号,也可能是对地址的解引用。实际上我们可以通过看其前一个标识的类型,我们就可以判断它的用法,所以可以给出框架代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13if (!make_token(e)) {
*success = false;
return 0;
}
/* TODO: Implement code to evaluate the expression. */
for (i = 0; i < nr_token; i ++) { // certain type 视具体情况而定
if (tokens[i].type == '*' && (i == 0 || tokens[i - 1].type == certain type) ) {
tokens[i].type = DEREF;
}
}
return eval(?, ?);
现在我们开始进一步的拓展我们的表达式的能力:
寄存器接口完善
我们需要根据$后的寄存器名,来获取对应的寄存器的值,并设置success:
1 | word_t isa_reg_str2val(const char *s, bool *success) { |
当发现寄存器返回值为-1时,则说明寄存器名称错误,我们要停止计算
表达式计算拓展
我们添加的功能有十六进制数的解析、寄存器的解析、还有部分简单逻辑运算的解析,我们依次分析,需要在哪些地方添加哪些功能:
首先是token的解析存储阶段:
1 | switch (rules[i].token_type) { |
我们添加了对十六进制标识符和寄存器的存储。
同时我们也要注意在eval(),由于我们引入了十六进制和寄存器进入表达式解析中,所以对于最小的子表达式
1 | <expr> ::= <decimal-number> |
我们的解析应该展开为:
1 | ... |
由于我们添加了逻辑表达式,所以我们还需要更新其对表达式的拆分和运算,首先是运算部分:
1 | ... |
与之而来的问题是,在引入逻辑表达式之后,我们该怎么拆分我们的表达式呢?我们知道对于目前已有的运算符,我们有* / > + - >逻辑运算的优先级关系,所以我们可以根据上一节中的规则来更新我们的主运算符号的选择:
1 | static int get_token(int start, int end){ |
至此,我们就完成了对于运算符的解析和拓展。
解引用分析
最后我们还需要解决的一个问题就是对于指针的解引用,我们根据框架代码,对负号和解引用的标识符进行替换:
1 | word_t expr(char *e, bool *success) { |
由于他们都是一元运算符,在计算的过程中拥有最高的优先级,所以我们需要更新搜索主运算符的函数:
1 | ... |
然后再最终的对表达式的整合中优先处理一元运算符号,再处理二元运算符:
1 | ... |
至此我们完成了对表达式运算符的拓展。
实现监视点
监视器允许我们设置监视点,当监视点中的表达式满足时,nemu就将nemu_state设置成NEMU_STOP,此时我们就可以对程序状态进行观察。
不过首先我们需要能够将所有监视点管理组织起来,在框架代码中已经定义好了监视点的结构和监视点池的结构:
1 | typedef struct watchpoint { |
我们需要管理两个链表head和free_。head用于存放使用中的监视点结构,free_用于组织空闲的监视点结构,我们可以使用init_wp_pool()函数,对两个链表进行初始化。
为了进一步的管理监视点池,我们需要两个函数:
WP* new_wp():用于从空闲的监视点池中选择一个监视点进行使用,同时也要对剩余监视点的数量进行判断void free_wp(WP* wp): 用于将正在使用中的监视点存回空闲链表中
实现了对监视点池的管理之后,我们就需要实现监视点的相关功能了(以下是需要完成的要求):
- 当用户给出一个待监视表达式时,
你需要通过
new_wp()申请一个空闲的监视点结构, 并将表达式记录下来. 然后在trace_and_difftest()函数(在nemu/src/cpu/cpu-exec.c中定义)的最后扫描所有的监视点, 每当cpu_exec()的循环执行完一条指令, 都会调用一次trace_and_difftest()函数. 在扫描监视点的过程中, 你需要对监视点的相应表达式进行求值(你之前已经实现表达式求值的功能了), 并比较它们的值有没有发生变化, 若发生了变化, 程序就因触发了监视点而暂停下来, 你需要将nemu_state.state变量设置为NEMU_STOP来达到暂停的效果. 最后输出一句话提示用户触发了监视点, 并返回到sdb_mainloop()循环中等待用户的命令. - 使用
info w命令来打印使用中的监视点信息, 至于要打印什么, 你可以参考GDB中info watchpoints的运行结果. - 使用
d命令来删除监视点, 你只需要释放相应的监视点结构即可.
监视点池的管理
这里我想监视点的结构中添加了一个used属性,用来判断当前监视点是否被启用,据此,我们就可以实现对监视点池中的监视点的使用和空闲的操作:
1 | WP* new_wp(){ |
现在我们需要进一步的完成监视点的功能,以及用于调用的接口
监视点功能实现
监视点的功能实际上就是在每一次执行之后,遍历一遍监视点链表,如果设置的表达式发生了改变,那么我们说监视点被触发了。所以我们要做的事情很简单,遍历计算表达式的值是否发生改变,如果是,则触发监视点:
1 | void wp_diff(){ |
由于NEMU在每次执行时都会在execute()中调用trace_and_difftest()我们在这个程序中加入每次对监视点的判断wp_diff()
现在我们只需要进一步完成监视点的接口,使得我们可以方便的创建销毁查看他们:
1 | P* new_wp(){ |
我们在终端进行调用,实现info w,d NO,w EXPR等功能:
1 | static int cmd_w(char* args){ |
至此我们就实现了监视点的功能。
对于断点我们可以通过w $pc=?的方式来让程序停止