题 目: riscv-32模拟器设计
专 业: 计算机科学与技术
完成日期: 2021-01-16

1.课程设计概述

1.1 课设目的

​ 通过模拟一个简单完整的计算机系统, 最终在上面运行游戏“仙剑奇侠传”,来深入理解程序如何在计算机上运行

1.2 实验环境

​ OS:Manjaro 20.2 Nibia

​ Kernal:x86_64 Linux 5.9.11-3-MANJARO

​ gcc:10.2.0

2. 实验过程

2.1 PA0

平常使用linux较多,较为熟悉,未碰到环境配置方面的困难

  • 初始化相关环境变量

    根目录init.sh中注释掉第十行return

    后执行 bash init.sh ,使用的不是默认shell,需要更改20-21 行

  • 设置$ISA环境变量

  • nemu/makefile 中

    • 第33行:

      CFLAGS   += -O0 -g -MMD -Wall -ggdb3 -std=c99 $(INCLUDES) -D__ISA__=$(ISA) -fomit-frame-pointer # -mmanual-endbr -fcf-protection=none #-Werror 禁止编译器优化,调试时使用
      
    • 第71行:

      @$(LD) -O0 -rdynamic $(SO_LDLAGS) -o $@ $^ -lSDL2 -lreadline -ldl
      
    • 注释掉第25行关闭自动添加commit历史

    • ···

  • make 报错 找不到optarg

    src/monitor/monitor.c: In function ‘parse_args’:
    src/monitor/monitor.c:67:3: warning: implicit declaration of function ‘getopt’ [-Wimplicit-function-declaration]
       while ( (o = getopt(argc, argv, "-bl:d:a:")) != -1) {
       ^
    src/monitor/monitor.c:70:28: error: ‘optarg’ undeclared (first use in this function)
           case 'a': mainargs = optarg; break;
                                ^
    src/monitor/monitor.c:70:28: note: each undeclared identifier is reported only once for each function it appears in
    make: *** [build/obj-mips32/monitor/monitor.o] Error 1
    

    在include/common.h 中 #include<getopt.h> 解决

  • 调试环境

    gdb调试

2.2 PA1

选择ISA为:riscv-32

2.2.1 设计及实现

单步执行

解析出执行步数(无参数时默认为一),直接调用cpu_exec(num)即可

static int cmd_si(char *args) {
  char * arg = strtok(args, " ");
  if (arg == NULL) {
    cpu_exec(1);
    return 0;
  }
  int num = atoi(arg);
  cpu_exec(num);
  return 0;
};
打印寄存器

完善isa_reg_display()函数即可,riscv的寄存器为32个通用寄存器加上$PC

void isa_reg_display() {
    printf("General reg: ----------------------------------------------------------- \n");
    int i, j;
    for(i = 0 ; i < 32 ; i++) {
      printf("%-3s :0x%08x |  ", regsl[i], cpu.gpr[i]._32);
      if ((i+1)%4 == 0)
        printf("\n");
    }
    printf("Special reg: ----------------------------------------------------------- \n");
    printf("$pc :0x%08x\n\n", cpu.pc);
}
扫描内存

通过 strtok 分别获得字符串型的地址和扫描长度,调用expr(后续实现)求得表达式结果作为地址,调用 vaddr_read 函数扫描内存后按格式打印即可。

...
vaddr_t addr = expr(EXPR, &success);
...
for (int i = 0; i < n; i++) {
		uint32_t data = vaddr_read(addr + i * 4, 4);
		printf("0x%08x	", addr + i * 4);
		for (int j = 0; j < 4; j++) {
      printf("0x%02x	" , data & 0xff);
      data = data >> 8;
		}
		printf("\n");
	}
...
表达式求值
  • 首先需要完善正则匹配:

      {" +", TK_NOTYPE, 0},              // spaces
      {"0[xX][0-9a-fA-F]+", TK_HEX, 0},  // hex
      {"[0-9]+", TK_DEX, 0},             // dex
      {"(\\$[0a-zA-Z]+)|([xX][0-9]+)",TK_REG, 0},			 // register $ OR x0~x31
      {"\\|\\|", TK_OR ,1},              // or
      {"&&", TK_AND, 2},                 // and 
      {"==", TK_EQ, 3},                  // equal
      {"!=", TK_NEQ, 3},                 // not equal
      {"\\+", '+', 4},                   // plus
      {"-", '-', 4},                     // sub
      {"\\*", '*', 5},                   // mul
      {"/", '/', 5},                     // div
      {"!", '!', 6},                     // not
      {"\\(", '(', 7},                   // bra_l
      {"\\)", ')', 7},                   // bra_r
    

    在原来rule的基础上增加了优先级字段,用于后续在表达式递归下降的过程中求得主运算符。

    这里有几个需要注意的地方:

    ​ - 像是*或者 ( 这样的符号需要经过两次转义,一次是c语言字符串转义,一次是正则引擎需要的转义

    ​ - regex.h 不支持正则中类似于 <! 这样的前置匹配,所以无法直接配置十进制,这里采用优先匹配十六进制再匹配十进制的方式绕过去

  • 接着完善make_token()函数

    识别出表达式中的每一个token。在for循环中,用regexec()函数匹配目标文本串和前面定义的rules[i]中的正则表达式比较,pmatch.rm_so==0表示匹配串在目标串中的第一个位置,pmatch.rm_eo表示结束位置,position和substr_len表示读取完后的位置和读取长度。成功识别到对应规则后,进行type、pri、str的复制即可

  • 接着进入对与表达式的处理eval函数,首先是对于表达式是否合法进行判断,这里需要满足start<= end 的条件,当start == end 的时候表示进行到了递归结束点,进行相应位置表达式(HEX、DEX、REG)的求值即可。对于start < end 的情况,首先需要判断表达式括号是否匹配,这里增加了一个函数

    static check_bra check_parentheses(int start, int end),参数为表达式起始和结束位置,返回值为枚举类型check_bra

    typedef enum {
      BRA_SURRONDED, MATCH, DISMATCH 
    } check_bra;
    

    其中BRA_SURROUNDED代表表达式被()包裹且括号内为括号匹配的表达式,MATCH代表括号匹配但不满足前一种情况的表达式,DISMATCH代表不满足括号匹配的表达式。

    当遇到DISMATCH时停止解析返回即可;当遇到BRA_SURRONDED时直接解析内层表达式即可;当遇到MATCH时才是我们需要进行处理的部分

  • 在MATCH的情况下,为了进行表达式的递归下降,首先要找到表达式中最后进行运算的符号,即主运算符,以此讲表达式分成前后两部分后分别进行计算,由函数 static int dominant_operator(int start, int end) 实现。

    static int dominant_operator(int start, int end)
    {
    	int op = start, pri_min = 10;
    	for (int i = start; i <= end;i ++)
    	{
    		if (tokens[i].type == TK_HEX || tokens[i].type == TK_DEX || tokens[i].type == TK_REG)
    			continue;
    		int bra_count = 0;
    		bool flag = true;
    		for (int j = i - 1; j >= start; j--) { 
    			if (tokens[j].type == '(') {
            if (bra_count == 0) {
              flag = false;
              break;
            }
            bra_count--;
          }
    			if (tokens[j].type == ')')
            bra_count++; 
    		}
    		if (!flag)
          continue;
    		if (tokens[i].pri <= pri_min) {
          op = i;
          pri_min = tokens[op].pri;
        }
      }
    	return op;
    }
    

    此函数根据之前数据结构中存储的优先级结构,来确定最后运算的符号,为优先级数字最小且不在括号中的符号,返回其index

    再求得主运算符之后,左右分别递归后运算即可。

  • 后续eval增加了对于解引用运算(*)和负号(-)的处理,因为无法在正则匹配中区分出来,所以需要在匹配完成后在进行符号的判断和优先级的修正,将这两个符号设为一个较高的优先级,在后续求得主运算符后,判断为这两个运算符时直接进行求值即可

      for (int i = 0; i < nr_token; i ++) {
    		if (tokens[i].type == '*' && (i == 0 || (tokens[i - 1].type != TK_DEX && tokens[i - 1].type != TK_HEX && tokens[i - 1].type != TK_REG && tokens[i - 1].type !=')') )) {
    			tokens[i].type = TK_DEREF;
          tokens[i].pri = 6;
    		}
    		if (tokens[i].type == '-' && (i == 0 || (tokens[i - 1].type != TK_DEX && tokens[i - 1].type != TK_HEX && tokens[i - 1].type != TK_REG && tokens[i - 1].type !=')') )) {
    			tokens[i].type = TK_MINUS;
          tokens[i].pri = 6;
    		}
    	}
    

    碰到的问题:

    ​ - 在解析类似于表达式p (1+2)*(3+4) 的时候出错,实现括号匹配解析的时候,不能将其归为BRA_SURROUNDED类型,会把这个表达式识别为不合法的表达式,因此增加了必须左侧有两个连续左括号的条件,但因此又导致了对于简单的表达式p (1+2) 回被认为是MATCH类型,进而求主运算符,导致错误;最后将BRA_SURROUNDED的条件设置为上述两种情况的并集,成功解决问题。

监视点
  • 监视点管理操作

    WP* wp_new (); // 申请新监视点
    void wp_free(WP * wp); // 释放监视点
    void wp_delete(int num); // 删除对应序号的监视点
    void wp_display(); //打印所有的监视点
    bool wp_check(); //监测监视点的值是否变化
    

    wp_new 从free链表中取一个结点给head链表。

    wp_free 函数是遍历head链表直到找出对应NO的结点,从head中删除,添加到free链表中。

    wp_chcek 函数是便利head链表中的监视点,求出其值后与上一次的值进行比较,打印出变化的监视点并返回false

  • 实现监视点

    修改cpu_exec.c,每执行完一条命令,调用wp_check() 函数,进行相应监视点的值的判断

    if (!wp_check()) {
      nemu_state.state = NEMU_STOP;
    }
    

2.2.2 运行结果

​ 进行相关的测试

  • help c q

    PA实验记录
  • sisi 2info rq

    PA实验记录
  • w $pc == 0x80100000w $pcinfo wd 2si 2si 2q

    PA实验记录

    PA实验记录

  • p (1+2)*(4/3)

    PA实验记录
  • p (3/3)+)(123*4

    PA实验记录
  • p (1+(3*2) +(($pc-$pc) +(*$pc-1**$pc)+(0x5--5+ *0X80100005 - *0x80100005) )*4)

    PA实验记录

2.2.3 问题解答

  • 我选择ISA为:riscv-32

  • 理解基础设施:

    450 * 20 * 0.5 = 4500 (min) = 75 (h)

  • 查阅手册:

    • riscv32有哪几种指令格式?

      • 用于寄存器-寄存器操作的 R 类型指令

      • 用于短立即数和访存 load 操作的 I 型指令

      • 用于访存 store 操作的 S 型指令

      • 用于条件跳转操作的 B 类型指令

      • 用于长立即数的 U 型指令

      • 用于无条件跳转的 J 型指令。

    • LUI指令的行为是什么?

      ​ 将符号位扩展的 20 位立即数 immediate 左移 12 位,并将低 12 位置零,写入 x[rd]中

      ​ x[rd] = sext(immediate[31:12] << 12)

    • mstatus寄存器的结构是怎么样的?

      mstatus(Machine Status)它保存全局中断使能,以及许多其他的状态

      PA实验记录
  • 代码统计

    • nemu/目录下的所有.c和.h和文件总共有多少行代码?

      find . -name "*.c" -or -name "*.h" | xargs grep -Ev "^$" | wc -l
      #4432
      
    • 和框架代码相比, 你在PA1中编写了多少行代码?

      git checkout pa0
      find . -name "*.c" -or -name "*.h" | xargs grep -Ev "^$" | wc -l
      #4009
      

      增加了423行代码

    • 加入 make count 命令

      COUNT_L := $(shell  find . -name "*.h" -or -name "*.c" | xargs grep -Ev "^$$" | wc -l) 
      COUNT_ADD := $(shell expr $(COUNT_L) - 4009)
      ...
      count:
      	@echo $(COUNT_L) lines in nemu
      	@echo $(COUNT_ADD) lines added into the frame code     
      
  • 解释gcc中的-Wall-Werror有什么作用? 为什么要使用-Wall-Werror?

    -Wall,打开gcc的所有警告。
    -Werror,它要求gcc将所有的警告当成错误进行处理。

    便于发现代码中潜在的问题

2.3 PA2

2.3.1 设计及实现

实现相关指令

参考老师提供的riscv手册,首先实现了下图中的指令

PA实验记录PA实验记录

对于一条指令的实现来说,大致有以下几个步骤:

  • 确定指令opcode,添加到opcode_table中(利用好在include/cpu/exec.h 中定义的macros)

    #define IDEXW(id, ex, w)   {concat(decode_, id), concat(exec_, ex), w}
    #define IDEX(id, ex)       IDEXW(id, ex, 0)
    #define EXW(ex, w)         {NULL, concat(exec_, ex), w}
    #define EX(ex)             EXW(ex, 0)
    #define EMPTY              EX(inv)
    

    opcode_table中的每个instr都以struct OpcodeEntry的形式定义,其中包括2个函数指针(译码函数和执行函数)和宽度。exec_once调用时,首先获取指令地址—cpu.pc值,传递给decinfo.seq_pc,供后面译码用,然后将获取到的pc传递给isa_exec,执行该pc处的指令然后在完成后更新cpu.pc。

    对于具有相同opcode的指令来说,还需要设置二级table作为func的区分,例如一系列条件跳转指令,ld指令等

  • 在decode.c中编写译码函数,如lui的make_DHelper(U),展开则为decode_U.注意,添加了新的译码函数后,还需要再include/decode.h中添加声明

    实现六种指令类型的解码

  • 通过调用rtl和伪rtl_funcs实现执行部分,对于R-type,I-type,S-type, B-type的执行函数还需要根据funct3进一步译码,在执行这一系列类型的执行函数时需要进行进一步区分。

    nemu已经实现了许多rtl(寄存器传输)和isa相关的功能,通过调用它们可以方便实现大多数指令

  • 将已实现的exec-func添加到all-instr.h

实现相关库函数
  • 实现字符串处理函数

    通过阅读手册和c参考书可以轻松实现

  • 实现sprintf

    在vsprintf中打印实现%s %d %c %x 和位宽 并在其他stdio函数中调用它的功能即可

    int vsprintf(char *out, const char *fmt, va_list ap) {
    //实现%s 和 %d %c %x 
    char *str = out;
    while(*fmt){
      if(*fmt != '%'){
          *str = *fmt++;
          str++;
          continue;
      }
      fmt++;
    
      //先判断是不是数字
      char * format_begin = fmt;
      char * format_end = fmt; 
      while (is_num(*fmt)) {
        format_end ++;
        fmt ++;
      }
      int format_len = get_format_length(format_begin, format_end);
    
      switch (*fmt++) {
        case 's' : {
          char * t = va_arg(ap, char*);
          int len = strlen(t);
          for(int i = 0; i < len; i++) {
            *str++ = *t++;
          }
          break;
        }
        case 'd' : {
          long int n = va_arg(ap, int);
          if (n < 0) {
            n = - n;
            *str++ = '-';
          }
          uint32_t num = (uint32_t)n;
          char num_str[30];
          int i = num_2_10str(num_str, num);
          if ( i < format_len) {
            memset(str, ' ', format_len - i);
            str += format_len - i;
          }
          strcpy(str, num_str);
          str += i;
          break;
        }
        case 'u': {
          uint32_t num = va_arg(ap, int);
          char num_str[30];
          int i = num_2_10str(num_str, num);
          if ( i < format_len) {
            memset(str, ' ', format_len - i);
            str += format_len - i;
          }
          strcpy(str, num_str);
          str += i;
          break;
        }
        case 'c': {
          char c = (char)va_arg(ap, int);
          *str++ = c;
          break;
        }
        case 'p':
        case 'x': {
          uint32_t num = va_arg(ap, uint32_t);
          char num_str[30];
          int i = num_2_16str(num_str, num);
          if ( (i + 2) < format_len) {
            memset(str, ' ', format_len - i - 2 );
            str += format_len - i -2 ;
          }
          *str++ = '0';
          *str++ = 'x';
          strcpy(str, num_str);
          str += i;
          break;
        }
        default :
          // printf("%c\n", --fmt);
          assert(0);
          break;
      }
    }
    *str='\0';
    return 0;
    }
    
实现输入输出
  • 实现printf

    int printf(const char *fmt, ...) {
        va_list args;
        va_start(args, fmt);
        char buf[1024];
        int n = vsprintf(buf, fmt, args);
        va_end(args);
        for(int i = 0; buf[i] != '\0'; i++) {
          _putc(buf[i]);
        }
        return n;
    }
    

    将直接调用vsprintf后得到的结果逐字符通过_putc(char)进行串口输出即可

  • 实现时钟

    调用timer时,它使用inl instr调用nemu / device中的相关处理函数,该函数将返回时间。

    size_t __am_timer_read(uintptr_t reg, void *buf, size_t size) {
      switch (reg) {
        case _DEVREG_TIMER_UPTIME: { // PA2.3
          _DEV_TIMER_UPTIME_t *uptime = (_DEV_TIMER_UPTIME_t *)buf;
          uint32_t new_time = inl(RTC_ADDR);
          uptime->hi = 0;
          uptime->lo = new_time - init_time;
          return sizeof(_DEV_TIMER_UPTIME_t);
        }
        ...
        
    void __am_timer_init() {
      init_time = inl(RTC_ADDR);
    }
    

    这里要注意的是计时器应在初始化时存储时间以在后续提供合适的值,即init_time

    从自定义的RTC(Real Time Clock)中获取到当前时间后完成时间的更新

  • 实现键盘输入

    nemu/src/device/keyboard.c 模拟了i8042通用设备接口芯片的功能

    size_t __am_input_read(uintptr_t reg, void *buf, size_t size) {
      switch (reg) {
        case _DEVREG_INPUT_KBD: {
          _DEV_INPUT_KBD_t *kbd = (_DEV_INPUT_KBD_t *)buf;
          // pa 2.3
          kbd->keydown = 0;
          kbd->keycode = inl(KBD_ADDR);
          return sizeof(_DEV_INPUT_KBD_t);
        }
      }
      return 0;
    }
    

    从自定义的KDB_ADDR中读出键入值后赋值给keycode即可

  • 实现VGA

    VGA初始化时注册了从0xa0000000开始的一段用于映射到video memory的MMIO空间,代码只模拟了400x300x32的图形模式,需要实现的有两部分

    • 屏幕大小寄存器的软件部分
    //read
    size_t __am_video_read(uintptr_t reg, void *buf, size_t size) {
    	...
        uint32_t video_info = inl(SCREEN_ADDR);
        info->width = (video_info >> 16) & 0x0ffff;
        info->height = video_info & 0x0ffff;
        ...
    }
    
    
    //write
    size_t __am_video_write(uintptr_t reg, void *buf, size_t size) {
        ...
        // fb: the whole data that will be show
        uint32_t *fb = (uint32_t *)(uintptr_t)FB_ADDR;
    
        // /* pixels: the data that will be painted
        uint32_t *pixels = ctl->pixels;
    
        // screen size
        int W = screen_width();
        int H = screen_height();
    
        // copy size
        int size_pixels_copy = sizeof(uint32_t) * min(W - x, w);
    
        // copy data from pixiels to fb
        for (int i = 0; i < h && y + i < H; i++) {
        memcpy(&fb[(y + i) * W + x], pixels, size_pixels_copy);
        pixels += w;
        }
        ...
    }
    

    这里向屏幕(x, y)坐标处绘制w*h的矩形图像,图像像素按行优先方式存储在pixels,每个像素用32位整数以00RRGGBB的方式描述颜色.

    • 同步寄存器的硬件部分
    static void vga_io_handler(uint32_t offset, int len, bool is_write) {
      // TODO: call `update_screen()` when writing to the sync register
      if(is_write) {
        update_screen();
      }
    }
    

    当写入同步寄存器的时候更新屏幕即可

2.3.2 运行结果

  • 串口 在nexus-am/tests/amtest/目录下键入 make mainargs=h run

    PA实验记录
  • 时钟 在nexus-am/tests/amtest/目录下键入 make mainargs=t run

    PA实验记录
  • 键盘 在nexus-am/tests/amtest/目录下键入 make mainargs=k run

    PA实验记录
  • VGA 在nexus-am/tests/amtest/目录下键入 make mainargs=v run

    PA实验记录
  • 看看NEMU跑多快 nexus-am/apps/

    • dhrystone

      PA实验记录
    • coremark

      PA实验记录
    • microbench

      PA实验记录
  • nemu目录下,执行sh runall.sh ISA=riscv32,结果如下图所示

    PA实验记录

    通过所有测试用例

  • slider

    PA实验记录
  • typing

    PA实验记录
  • litenes

    PA实验记录

2.3.3 问题解答

  • 请整理一条指令在NEMU中的执行过程.

    • exec_once每次都会执行一条指令,其过程如下:

      • 获取指令地址cpu.pc值,传给decinfo.seq_pc,供译码使用

      • 将获取到的pc传递给isa_exec,执行该pc处的指令

        isa_exec的详细执行过程如下:

        • 根据pc的值去取指令放入到decinfo.isa.instr.val中
        • 对指令的opcode译码,并根据opcode_table[32]和idex定义并执行译码函数和执行函数
      • 更新pc值,update_pc

  • 编译与链接 在nemu/include/rtl/rtl.h中, 你会看到由static inline开头定义的各种RTL指令函数. 选择其中一个函数, 分别尝试去掉static, 去掉inline或去掉两者, 然后重新进行编译, 你可能会看到发生错误. 请分别解释为什么这些错误会发生/不发生? 你有办法证明你的想法吗?

    在c/c++中,inline 是表示内联函数,一般比较短的函数可以写成内联函数,在编译的时候直接在调用处展开。没有了函数的调用及堆栈转换开销,所以运行速度比较快。

    • 去掉static

      内联展开,无错误

    • 去掉inline

      多处报警告
      ./include/rtl/rtl.h:132:13: 警告:‘rtl_not’ defined but not used [-Wunused-function]
      132 | static void rtl_not(rtlreg_t *dest, const rtlreg_t* src1) {
      

      每个编译单元分别定义,但static将其限定为仅本编译单元可见,因此并不会报错,但所有包含此头文件但未使用 rtl_not 的函数都会出现 [-Wunused-function] 的警告

    • 去掉inline和static rtl_not多重定义

      报错,链接失败
      /usr/bin/ld: build/obj-riscv32/isa/riscv32/exec/compute.o: in function `rtl_not':
      /home/zhuzhicheng/project/PA/ics2019_1/nemu/./include/rtl/rtl.h:132: multiple definition of `rtl_not'; build/obj-riscv32/cpu/cpu.o:/home/zhuzhicheng/project/PA/ics2019_1/nemu/./include/rtl/rtl.h:132: first defined here
      /usr/bin/ld: build/obj-riscv32/isa/riscv32/exec/exec.o: in function `rtl_not':
      /home/zhuzhicheng/project/PA/ics2019_1/nemu/./include/rtl/rtl.h:132: multiple definition of `rtl_not'; build/obj-riscv32/cpu/cpu.o:/home/zhuzhicheng/project/PA/ics2019_1/nemu/./include/rtl/rtl.h:132: first defined here
      /usr/bin/ld: build/obj-riscv32/isa/riscv32/exec/special.o: in function `rtl_not':
      /home/zhuzhicheng/project/PA/ics2019_1/nemu/./include/rtl/rtl.h:132: multiple definition of `rtl_not'; build/obj-riscv32/cpu/cpu.o:/home/zhuzhicheng/project/PA/ics2019_1/nemu/./include/rtl/rtl.h:132: first defined here
      ...
      collect2: 错误:ld 返回 1
      make: *** [Makefile:72:build/riscv32-nemu] 错误 1
      

      每个包含该头文件的模块中都会定义一次rtl_not ,在链接时就会出现多重定义的错误;当用static进行限定时,对应的函数仅本编译单元中可见,不会出现定义的冲突。

  • 编译与链接

    1. nemu/include/common.h中添加一行volatile static int dummy; 然后重新编译NEMU. 请问重新编译后的NEMU含有多少个dummy变量的实体? 你是如何得到这个结果的?

      PA实验记录
    2. 添加上题中的代码后, 再在nemu/include/debug.h中添加一行volatile static int dummy; 然后重新编译NEMU. 请问此时的NEMU含有多少个dummy变量的实体? 与上题中dummy变量实体数目进行比较, 并解释本题的结果.

      PA实验记录

      <dummy>数目未变化,common.h 已经包含了 debug.h ,对于变量的重复声明仅作用一次

    3. 修改添加的代码, 为两处dummy变量进行初始化:volatile static int dummy = 0; 然后重新编译NEMU. 你发现了什么问题? 为什么之前没有出现这样的问题? (回答完本题后可以删除添加的代码.)

      PA实验记录

      出现重定义错误,因为common.h 包含了debug.h 导致对 dummy 进行了两次初始化

  • 了解Makefile

    请描述你在nemu/目录下敲入make后,make程序如何组织.c和.h文件, 最终生成可执行文件nemu/build/$ISA-nemu

    - 指定要编译的源文件
    SRCS = $(shell find src/ -name "*.c" | grep -v "isa")
    SRCS += $(shell find src/isa/$(ISA) -name "*.c")
    OBJS = $(SRCS:src/%.c=$(OBJ_DIR)/%.o)
    
    - 对于每个.c文件进行编译,gcc 的编译参数由 CFLAGS 指定
    $(OBJ_DIR)/%.o: src/%.c
    @echo + CC $<
    @mkdir -p $(dir $@)
    @$(CC) $(CFLAGS) $(SO_CFLAGS) -c -o $@ $<
    
    - 对于上述编译得到的 obj 文件进行链接,生成最终的可执行文件
    $(BINARY): $(OBJS)
    $(call git_commit, "compile")
    @echo + LD $@
    @$(LD) -O0 -rdynamic $(SO_LDLAGS) -o $@ $^ -lSDL2 -lreadline -ldl
    

2.4 PA3

2.4.1 运行结果

  • task1

    PA实验记录

2.4.2 问题解答

  • 你会在__am_irq_handle()中看到有一个上下文结构指针c, c指向的上下文结构究竟在哪里? 这个上下文结构又是怎么来的? 具体地, 这个上下文结构有很多成员, 每一个成员究竟在哪里赋值的? $ISA-nemu.h, trap.S, 上述讲义文字, 以及你刚刚在NEMU中实现的新指令, 这四部分内容又有什么联系?

    触发raise_init后,会有以下动作:

    • 将当前PC值保存到sepc寄存器
    • 在scause寄存器中设置异常号
    • 从stvec寄存器中取出异常入口地址
    • 跳转到异常入口地址

    跳转到异常入口地址之后,会先保留上下文到_Context

    	// __am_asm_trap
      MAP(REGS, PUSH)
    
      mv t0, sp
      addi t0, t0, CONTEXT_SIZE
      sw t0, OFFSET_SP(sp)
    
      csrr t0, scause
      csrr t1, sstatus
      csrr t2, sepc
    
      sw t0, OFFSET_CAUSE(sp)
      sw t1, OFFSET_STATUS(sp)
      sw t2, OFFSET_EPC(sp)
    

    根据offset可以确定_Context中字段的顺序

    // ref am/am/src/$ISA/nemu/trap.S
    #define OFFSET_SP     ( 2 * 4)
    #define OFFSET_CAUSE  (32 * 4)
    #define OFFSET_STATUS (33 * 4)
    #define OFFSET_EPC    (34 * 4
    

    因此_Context的结构为

    struct _Context {
      uintptr_t gpr[32], cause, status, epc;
      struct _AddressSpace *as;
    };
    

    其中包括32个通用寄存器,和3个csr寄存器

    顺序由trap.S中定义的offset所决定

  • 从Nanos-lite调用_yield()开始, 到从_yield()返回的期间, 这一趟旅程具体经历了什么? 软(AM, Nanos-lite)硬(NEMU)件是如何相互协助来完成这趟旅程的? 你需要解释这一过程中的每一处细节, 包括涉及的每一行汇编代码/C代码的行为, 尤其是一些比较关键的指令/变量. 事实上, 上文的必答题"理解上下文结构体的前世今生"已经涵盖了这趟旅程中的一部分, 你可以把它的回答包含进来.

    - nanos-lite/src/main.c     _yield()
    - nexus-am/am/src/riscv32/nemu/cte.c    _yield()
       - asm volatile("li a7, -1; ecall");  执行ecall指令
          - raise_intr(9, cpu.pc)          
              将当前PC值保存到sepc寄存器
              在scause寄存器中设置异常号
              从stvec寄存器中取出异常入口地址
              跳转到异常入口地址
              		- nexus-am/am/src/riscv32/nemu/trap.S   __am_asm_trap
              		 		保留_Contex上下文
              		 		跳转到异常处理程序  __am_irq_handle
              		 				- nexus-am/am/src/riscv32/nemu/cte.c 
              		 						根据c->cause 记录上下文切换原因后传递给上下文处理程序
              		 						根据时间类型调用对应的do_event函数 nanos-lite/src/irq.c
              		 		恢复上下文
    - 执行下一条指令
    

3. 设计总结与心得

3.1 课设总结

当前进度:

  • PA1 - 最简单的计算机
    • task PA1.1: 实现单步执行, 打印寄存器状态, 扫描内存
    • task PA1.2: 实现算术表达式求值(+ - * / 解引用 负数 and or == !=)
    • task PA1.3: 实现监视点
  • PA2 - 冯诺依曼计算机系统
    • task PA2.1: 实现riscv-32的相关指令,通过所有测试
    • task PA2.2: 实现相关klib库函数,通过string测试
    • task PA2.3: 实现基本的输入输出
  • PA3 - 批处理系统
    • task PA3.1: 实现_yield自陷操作
    • task PA3.2: 实现系统调用(未完成)

​ 由于课程实验与卓工班的生产实习冲突了,只能抽出周六周日的时间做PA相关实验,从十二月下旬开始到一月中大概三个周末的时间,目前只完成了PA1、PA2和PA3.1,没能完成后续的实验,有些可惜吧。

​ pa0的基础环境配置,基本所有的内容都属于linux的基本操作,基本没有碰到障碍,比较顺利

​ pa1的实验难度较小,主要是对基本数据结构的处理,基本没有涉及到实验代码框架以及和具体指令架构相关的内容,可以直接使用gdb调试较,完成pa2后对pa1进行测试的时候发现w指令与当时最后的实现效果不一致,最后发现是因为为了兼容在递归过程中对于表达式值不合规的判断,对于expr做了一些修改,在一上来会对success的值进行判断,w中传入expr的success未赋初值导致直接退出解析失败。碰到类似的问题比较好定位根源,完成度较好。

​ pa2的实验难度相比pa1有了很大的提升,一方面是实验的框架代码比较繁琐,一些起到帮助的宏定义会导致函数的定义不容易找到,刚开始的时候找相关定义有些许麻烦,在熟悉代码框架后很容易解决;另一方面pa2的调试难度较大,无法使用gdb直接对指令进行调试。我在刚开始实现相关的指令的时候,有些难指令错误不好定位,寻找漏掉的相关指令时碰到了很多的麻烦,后来在指令的实现中加入了对于func7的判断(发现只根据opcode并无法直接表达所有指令,有func和opcode相同的指令),对于没有匹配上的指令直接assert(0),这才发现了一些指令的缺失,纠正错误。另一方面,在指令的bug基本改完以后才看到有diff-test这个工具,长个教训,要提升大局观,应该通览全局后再实现细节才好。

​ pa3只完成了task1,task2在实现loader以后在跳转到sys_call的过程不符合预期,暂时未能定位到原因

​ 实验有一个比较恼人的地方,commit历史的自动记录导致git管理较为混乱,有几次无法定位自己做了哪处改动导致出错的,在追溯commit历史的时候,冗余的历史导致花费了很多时间。

3.2 课设心得

​ 总的来说,作为一个系统能力培养的实验,nemu还是给我带来一些收获的,包括但不限于对项目的组织方式,通用ISA接口的抽象,通用函数的定义,宏定义协助构造函数,makefile的编写,编译相关参数,代码模块化,git分支管理,查阅文档的能力等等,是实践相关理论知识的一个很好的方式。我在实习过程中学到的一些工作上的东西在完成实验的过程中有很多体现,对于自身能力的提升有一些帮助。

​ nemu的很多理念还是很好的,对于形成良好的代码习惯很有帮助,但是放到这个时间点来完成时间上有些仓促,而且效果可能没有那么好,如果能把这个实验提前一段时间,放到大三或者大二的话帮助可能会更大一些,可以起到很好的引导作用。

相关文章:

  • 2021-12-05
  • 2021-06-13
  • 2021-04-07
  • 2021-10-14
  • 2021-12-02
  • 2021-05-18
  • 2022-01-06
  • 2022-12-23
猜你喜欢
  • 2021-10-20
  • 2021-11-09
  • 2022-12-23
  • 2021-09-24
  • 2021-08-21
  • 2021-09-24
  • 2021-10-17
相关资源
相似解决方案