1、理解YEMU的执行过程
简单来说分为三步,取值译码执行。
取值:YEMU中定义了指令数组,每次取值都依次在指令数组中读取。

uint8_t M[NMEM] = {   // 内存, 其中包含一个计算z = x + y的程序
  0b11100110,  // load  6#     | R[0] <- M[y]
  0b00000100,  // mov   r1, r0 | R[1] <- R[0]
  0b11100101,  // load  5#     | R[0] <- M[x]
  0b00010001,  // add   r0, r1 | R[0] <- R[0] + R[1]
  0b11110111,  // store 7#     | M[z] <- R[0]
  0b00010000,  // x = 16
  0b00100001,  // y = 33
  0b00000000,  // z = 0
};

译码:根据取的指令进行译码,由于YEMU中规定的指令类型只有两种,所以YEMU的译码操作用case语句即可以完成。译码过程中用提前定义好的R type 和M type结构即可完成译码,得到后续执行需要用到的rd src1 src2等操作数。

#define DECODE_R(inst) uint8_t rt = (inst).rtype.rt, rs = (inst).rtype.rs

执行:在译码完成后,得到对应的操作,YEMU直接在case语句后完成执行,同时pc++,准备读取下一条指令。这里还需要判断是否处于结束状态,如此循环即可。

    case 0b0000: { DECODE_R(this); R[rt]   = R[rs];   break; }
    case 0b0001: { DECODE_R(this); R[rt]  += R[rs];   break; }
    case 0b1110: { DECODE_M(this); R[0]    = M[addr]; break; }
    case 0b1111: { DECODE_M(this); M[addr] = R[0];    break; }
    default:

2、整理一条指令在NEMU中执行的过程:
说轮廓一些也是三步(类似把大象关进冰箱里需要几步了hhh),取值译码执行。
但是这里的这三步和YEMU差距很大,也有很多细节。
在NEMU中取值,首先要看是否有bin文件,如果没有bin文件使用提前定义好的指令数组,通过peme_read来读取指令,所以取值就是一次读内存的操作。
译码操作相对复杂了许多,但某种程度上和YEMU相似,riscv32的指令类型有I U S J R B,所以对应的译码结构也有这几种。


#define src1R() do { *src1 = R(rs1); } while (0)
#define src2R() do { *src2 = R(rs2); } while (0)
#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)
#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)
#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)
#define immJ() do { *imm = SEXT(( BITS(i, 31, 31) << 19 | BITS(i, 30, 21) | BITS(i, 20, 20) << 10|\
                                              (BITS(i, 19, 12) << 11)) << 1, 21); } while(0)
#define immB() do {*imm = SEXT((BITS(i,31,31) << 11 | BITS(i,30,25) << 4 | BITS(i,11,8) | BITS(i,8,7) << 10) << 1,13);} while(0)

    case TYPE_I: src1R();          immI(); break;
    case TYPE_U:                   immU(); break;
    case TYPE_S: src1R(); src2R(); immS(); break;
    case TYPE_J:                   immJ(); break;
    case TYPE_R: src1R();  src2R();        break;
    case TYPE_B: src1R(); src2R(); immB(); break;

看吧~译码时候也是通过case语句来根据指令的类型来进行译码的,解析处对应的立即数,src1 rd src2。
解析后就是执行,执行通过NEMU给出的宏就可以了,其实本质上也是一个case语句,进行匹配。

  INSTPAT("??????? ????? ????? 000 ????? 00100 11", addi   , I, R(rd) = src1 + imm);  
  INSTPAT("??????? ????? ????? ??? ????? 01101 11", lui    , U, R(rd) = imm );
  INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc  , U, R(rd) = s->pc + imm);

当匹配成功后开始执行,执行后pc跳转到next_pc,判断是否结束,若没结束则继续此步骤。
3、理解打字小游戏如何运行
其实这个在讲义中写的也比较清晰了,本质上就是刷新屏幕,接受输入 ,刷新游戏 往复循环这样。
打字小游戏也不例外,main函数中首先初始化了ioe接口,这里是26个字母按键,而后又初始化了屏幕,把VGA的像素点填充完整,也就是初始化为全紫色。初始化结束后开始while循环。
循环的内容就是我们刚才说的三步,首先先把字母刷新出来,也就是在VGA屏幕中刷新出来的字母,同时更新现有的字符状态和位置(因为字符需要一直掉落),然后把到达屏幕底部的字符进行处理(计入miss)。
而后我们对键盘内容进行接受,我们用AM现成的KEYBRD接口进行接收,判断键盘的内容是否是掉落的字母,也就是check_hit()。
最后我们进行VGA屏幕更新,先读缓存池,而后在把当前的状态写到缓存池中供下次读取用。
在初始化屏幕时

4、在nemu/include/cpu/ifetch.h中, 你会看到由static inline开头定义的inst_fetch()函数. 分别尝试去掉static, 去掉inline或去掉两者, 然后重新进行编译, 你可能会看到发生错误. 请分别解释为什么这些错误会发生/不发生? 你有办法证明你的想法吗?
我在去掉static 或去掉inline时没有发生错误,而把二者都去掉后发生错误:

/usr/bin/ld: /home/white/ysyx-workbench/nemu/build/obj-riscv32-nemu-interpreter/src/engine/interpreter/hostcall.o: in function `inst_fetch':
hostcall.c:(.text+0x0): multiple definition of `inst_fetch'; /home/white/ysyx-workbench/nemu/build/obj-riscv32-nemu-interpreter/src/isa/riscv32/inst.o:inst.c:(.text+0xe00): first defined here
collect2: error: ld returned 1 exit status
make: *** [/home/white/ysyx-workbench/nemu/scripts/build.mk:54: /home/white/ysyx-workbench/nemu/build/riscv32-nemu-interpreter] Error 1

可以看到是inst_fetch重定义了。。。话说这不正是static的作用咩?那为什么单独删除他们没发生错误,反而两个一起删除就发生错误咯?
static:

  • 内部链接:将函数限制为文件内可见,即只能在定义它的源文件内调用。不会在其他文件中可见。
    避免命名冲突:多个文件可能会定义相同名字的函数。使用 static 可以避免这种命名冲突,因为它们的作用域仅限于各自的文件。
    形象来说就是项目类似一个大别墅,不同文件是一个小屋子,函数名是小屋子里的人,人可能会重名,在大别墅中进行点名(调用)时候会混乱,static就会限制这个张三 不可以出屋子。
  • 这是最主要的作用,当然还有保持变量内容持久,如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
  • 最后就是不太重要的:把定义的变量默认设为0.

inline
inline 关键字建议编译器将函数的代码直接复制到每个调用点,而不是通过常规的函数调用机制。这有几个潜在的优点:

  • 消除函数调用开销:内联函数的调用不会引入函数调用的开销,如参数压栈和返回地址保存等。
  • 代码优化:编译器可以对内联函数进行更好的优化,例如常量传播和死代码消除。

inline 仅仅是建议,编译器可以忽略这个建议。如果函数体太大或太复杂,编译器可能不会内联。
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收
获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

清楚了static和inline的作用后,那么static inline一起删除为什么会报错就清楚了。仅删除static,保留inline不报错的原因是:编译器可能会使用weak符号来实现:多个定义中只有一个会被最终链接。(原来是编译器的功劳?😃
删除inline 保留static:函数仍然具有内部链接属性,仍然只在定义它的源文件内可见,不会导出到其他文件,因此不会在链接阶段引发冲突。
所以删除static inline后,会引发报错咯~。