南大-ICS2021 PA1~PA2.2 学习笔记&记录

代码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 be
        if(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)

每个译码辅助函数负责进行一种类型的操作数译码, 把指令中的操作数信息分别记录在译码信息sdest成员, src1成员和src2成员中, 它们分别代表目的操作数和两个源操作数. nemu/include/cpu/decode.h中还定义了三个宏id_dest, id_src1id_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, s2t0(在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);

在这里插入图片描述

所以实现一个新指令的步骤如下:

  1. nemu/src/isa/riscv32/instr/decode.c中添加正确的模式匹配规则
  2. 用RTL实现正确的执行辅助函数, 在./src/isa/riscv32/instr/compute.h或者instr文件夹下其他的头文件
  3. nemu/src/isa/riscv32/include/isa-all-instr.h中把指令添加到INSTR_LIST
  4. 必要时在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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值