文章目录
- 代码github网址
- ICS2021其他博客
- 基础设施: 简易调试器
- 表达式求值
- 词法分析
- 递归求值
- 如何测试自己的代码
- 监视点的实现
- 扩展表达式求值的功能
- 实现监视点
- 阅读源码 2
- 译码
- 执行
- 用RTL表示指令行为
- 实现常用的库函数
- 实现常用的库函数
代码github网址
https://github.com/xiao-tai/ics2021
ICS2021其他博客
南大-ICS2021 PA2.3 学习笔记&记录
南大-ICS2021 PA3.1 学习笔记&记录
南大ICS2021–实现库函数vsnprintf
基础设施: 简易调试器
这些都还比较简单实现, 具体可以查看nemu/src/sdb/sdb.c
中的
-
cmd_c
: 启动执行 -
cmd_q
: 退出NEMU -
cmd_si
: 单步执行 -
cmd_x
: 打印内存 -
cmd_p
: 进行表达式求值, 具体实现看下节
表达式求值
主要文件为src/monitor/sdb/expr.c
,具体可以查看我的github
词法分析
对于一下表达式:
"0x80100000+ ($a0 +5)*4 - *( $t1 + 8) + number"
我们需要将各个单元(也就是token
)给识别出来, 使用正则表达式可以方便的匹配出一些复杂的pattern, 代码框架中的init_regex()
函数负责将定义的正则表达式进行验证, 函数在此处报错通常是因为使用了非法的正则表达式
本人所用到的正则表达式如下图
然后, 给定一个表达式, 通过make_token()
来识别token类型, 将识别结果保存在一个tokens[i]
中, 将对应的字符串保存在str[32]
属性中, 类型保存在type
属性中, 以上述表达式为例,
tokens[0].str = 0x80100000
, tokens[0].type = TK_HEX_NUM
, 以此类推, 这样一个表达式所有的taoken都识别完毕了.
递归求值
这里就是涉及到算法的知识了, 需要用到分治法, 这类方法在排序上也有一个经典的应用–快速排序算法. 表达式求值使用上, 我们需要先对短表达式求值, 再对长表达式求值, 所以要使用递归. 这里需要考虑括号的问题和运算符优先级的问题.
eval(p, q) {if (p > q) {/* Bad expression */}else if (p == q) {/* Single token.* For now this token should be a number.* Return the value of the number.*/}else if (check_parentheses(p, q) == true) {/* The expression is surrounded by a matched pair of parentheses.* If that is the case, just throw away the parentheses.*/return eval(p + 1, q - 1);}else {op = the position of 主运算符 in the token expression;val1 = eval(p, op - 1);val2 = eval(op + 1, q);switch (op_type) {case '+': return val1 + val2;case '-': /* ... */case '*': /* ... */case '/': /* ... */default: assert(0);}}
}
对于一个长表达式, 需要先找出主运算符, 本人计算主运算符逻辑是
-
遍历p和q之间所有的运算符
-
优先级高的运算符放到后面进行if判断, 因为主运算符是最后一步才进行运算的
// 获取主运算符的位置
int get_position(int p, int q) {int pos = 0;int num = 0;bool plus_or_sub = false;bool mul_or_div = false;bool minus = false;bool deref = false;for(int i = p; i <= q; i++) {if(tokens[i].type == '(')num++;if(tokens[i].type == ')')num--;if(num != 0)continue;if(tokens[i].type == TK_EQ) {return i;}// The lower the priority, the higher the judge will beif(tokens[i].type == '+' || tokens[i].type == '-') {pos = i;plus_or_sub = true;continue;} if((tokens[i].type == '*' || tokens[i].type == '/') && !plus_or_sub) {pos = i;mul_or_div = true;continue;}if(tokens[i].type == TK_MINUS && !plus_or_sub && !mul_or_div && !minus) {pos = i;minus = true;}if(tokens[i].type == TK_DEREF && !plus_or_sub && !mul_or_div && !deref) {pos = i;deref = true;}}return pos;
}
如何测试自己的代码
写一个随机生成表达式的程序, 要求合法, 其实就是将生成的表达式, 例如1 + 2 + 3
传入一个定义好的文件中
#include <stdio.h>int main() {unsigned result = 1 + 2 + 3;printf("%u\n", result);
}
监视点的实现
扩展表达式求值的功能
在递归求解表达式前, 要区分*
是乘法运算符, 还是解引用, 所以在进行递归前, 先做一次判断, 以区分负号和减号, 乘号和解引用运算符:
for (int i = 0; i < nr_token; i++) {if(tokens[i].type == '-') {if(i == 0 || (tokens[i-1].type != ')' && tokens[i-1].type != TK_NUM))tokens[i].type = TK_MINUS;}if(tokens[i].type == '*') {if(i == 0 || (tokens[i-1].type != ')' && tokens[i-1].type != TK_NUM))tokens[i].type = TK_DEREF;}
}
实现监视点
使用两个链表维护一个监视点池, head
负责维护使用中的监视点结构, free_
负责维护空闲的监视点结构. 监视点的机构体如下:
typedef struct watchpoint {int NO;struct watchpoint *next;// 额外添加两个成员, 用于后续的扫描char exp[32]; //表达式uint32_t res;
} WP;
这时还需要增加监视点和删除监视点这两个函数, 同时还需要有扫描所有监视点的函数:
-
增加监视点(new_wp): 申请一个空闲监视点, 并将表达式记录下来
-
删除监视点(free_wp(int no)): 通过监视点编号删除监视点, 将其表达式设置为’\0’, 即代表该监视点不参与监视
-
扫描监视点(check_all_wp()): 每当
cpu_exec()
执行一条指令时, 都会调用一次trace_and_difftest()
, 所以在其中扫描一次所有监视点, 对其表达式进行求值, 如果结果发生变化, 就打印出来, 并将nemu_state.state
变量设置为NEMU_STOP
具体见src/monitor/sdb/watchpoint.c
阅读源码 2
译码
译码上, 框架代码进行了多层抽象.
取指令的时候会把指令记录到s->isa.instr.val
, 首先匹配opecode
字段, 再匹配func3
字段, 再匹配func7
字段, 这样isa_fetch_decode()
会返回唯一的idx, 用于表示执行的指令.
但是我们还不知道操作对象(比如立即数是多少, 使用哪个寄存器), 使用译码辅助函数
// 宏定义解开的样子
def_DHelper(name) = void decode_name(Decode *s)
每个译码辅助函数负责进行一种类型的操作数译码, 把指令中的操作数信息分别记录在译码信息s
的dest
成员, src1
成员和src2
成员中, 它们分别代表目的操作数和两个源操作数. nemu/include/cpu/decode.h
中还定义了三个宏id_dest
, id_src1
和id_src2
, 用于方便地访问它们.
同时为了更好的实现操作数译码和指令译码的解耦, 使用译码操作数辅助函数
def_DopHelper(name) = void decode_op_name(Decode *s, Operand *op, word_t val, bool flag)
-
decode_op_i: 通过译码的指令获得立即数
-
decode_op_r: 通过译码的指令获得寄存器
执行
在fetch_decode()
中得到的执行辅助函数记录到s->EHelper
中
执行辅助函数统一通过宏def_EHelper
, 具体通过RTL指令来进行真正的执行
def_EHelper(name) = static inline void exec_name(Decode *s)
每个执行辅助函数都需要有一个标识该指令的ID以及一个表格辅助函数与之相对应, 这一点是通过一系列宏定义来实现的. 在nemu/src/isa/$ISA/include/isa-all-instr.h
中定义用于表示指令列表的宏INSTR_LIST
, 它定义了NEMU支持的所有指令.
用RTL表示指令行为
在NEMU中, RTL寄存器只有以下这些
- 不同ISA的通用寄存器(在
nemu/src/isa/$ISA/include/isa-def.h
中定义) - 临时寄存器
s0, s1, s2
和t0
(在nemu/include/rtl/rtl.h
中定义) - 零寄存器
rz
(在nemu/include/rtl/rtl.h
中定义), 它的值总是0
进入nemu/build/obj-riscv32-nemu-interpreter/src/cpu/cpu-exec.i
查看具体的执行情况, g_exec_table
如下:
static const void *g_exec_table[TOTAL_INSTR] = {[EXEC_ID_lui] = exec_lui,[EXEC_ID_lb] = exec_lb,[EXEC_ID_lbu] = exec_lbu,[EXEC_ID_lh] = exec_lh,[EXEC_ID_lhu] = exec_lhu,[EXEC_ID_lw] = exec_lw,[EXEC_ID_sw] = exec_sw,[EXEC_ID_sh] = exec_sh,[EXEC_ID_sb] = exec_sb,//.....
}
各个函数的具体实现位于src/isa/riscv32/instr/compute.h
中, 通过以下指令查找得到:
xiaoxTai:nemu$ grep -r "rtl_add" ../src/isa/riscv32/instr/compute.h: rtl_addi(s, ddest, dsrc1, id_src2->imm);
./src/isa/riscv32/instr/compute.h: rtl_add(s, ddest, dsrc1, dsrc2);
所以实现一个新指令的步骤如下:
- 在
nemu/src/isa/riscv32/instr/decode.c
中添加正确的模式匹配规则 - 用RTL实现正确的执行辅助函数, 在
./src/isa/riscv32/instr/compute.h
或者instr
文件夹下其他的头文件 - 在
nemu/src/isa/riscv32/include/isa-all-instr.h
中把指令添加到INSTR_LIST
中 - 必要时在
nemu/src/isa/riscv32/include/isa-exec.h
添加相应的头文件, 言外之意就是第二步写在哪个头文件下, 就把那个头文件, 加到这个isa-exec.h
中
接下来就是通过运行程序, 检查还有哪些指令没有实现, 安装好交叉编译环境, 通过执行
make ARCH=riscv32-nemu ALL=dummy run
运行结果打印的信息会提示你还有什么指令没有实现, 当然要通过在am-kernels/tests/cpu-tests/build/dummy-$ISA-nemu.txt
进行搜索
实现常用的库函数
相关库函数的具体实现, 也是面试中出现频率较高的问题了, 主要在abstract-machine/klib/src/
下
-
strcpy
,memset
等函数在string.c
下 -
sprintf()
在stdio.c
下, 这个函数实现的讲解见我的另一篇博客 -
atoi
,malloc
在stdlib.c下
果打印的信息会提示你还有什么指令没有实现, 当然要通过在am-kernels/tests/cpu-tests/build/dummy-$ISA-nemu.txt
进行搜索
实现常用的库函数
相关库函数的具体实现, 也是面试中出现频率较高的问题了, 主要在abstract-machine/klib/src/
下
-
strcpy
,memset
等函数在string.c
下 -
sprintf()
在stdio.c
下, 这个函数实现的讲解见我的另一篇博客南大ICS2021–实现库函数vsnprintf -
atoi
,malloc
在stdlib.c下