南京大学 计算机系统基础 课程实验 2018(PA2)

其他部分

  1. PA0-1

1. 主要难点

1.1 NEMU是如何模拟指令执行的过程

这一个阶段就是阅读源代码,代码框架中间定义了大量的宏,比如

#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)

这些宏初次遇到的时候会让人感觉很抓狂,但是试验完成之后还是感觉这只是让人抓狂的冰山一角。

接下来分析一下一条指令是是如何执行完成的

  1. ui.c 经过一些列的初始化之后进入到ui_mainloop函数,is_batch_mode是命令行参数控制的内容,具体位置在nexus-am/am/arch/x86-nemu/img/run中间。
void ui_mainloop(int is_batch_mode) {
  if (is_batch_mode) {
    cmd_c(null);
    return;
  }
  // .....
  1. cpu-exec.c 中间的cpu_exec.c中间的函数主要用于处理IO辅助代码和gdb的检查点的分析
void cpu_exec(uint64_t n) {
  if (nemu_state == NEMU_END || nemu_state == NEMU_ABORT) {
    printf("Program execution has ended. To restart the program, exit NEMU and run again.\n");
    return;
  }
  // ....
  1. exec.c 终于到达真正的硬件层级,exec_wrapper主要处理内容是更新指令执行完成之后eip的更新和统计必要的执行信息。
void exec_wrapper(bool print_flag) {
  vaddr_t ori_eip = cpu.eip;
//....

现在分析一下更新eip的原理和错误的处理eip 会导致的问题:
第一个: 跳转指令(jmp, call , ...) 会导致exec_warp 中间的debug 语句异常

  int instr_len = decoding.seq_eip - ori_eip;
  sprintf(decoding.p, "%*.s", 50 - (12 + 3 * instr_len), "");

具体的表现为出现debug log不整齐的输出或者setment fault, 但是导致原因是很简单的,那就是上面的代码段中间的instr_len计算出来的长度错误,如果错误不是很离谱就出导致debug log 不整齐,当时如果比较离谱,长度过长,结果就是segment fault.

在求解instr_len之前执行的指令为

  vaddr_t ori_eip = cpu.eip; // 保存执行之前的cpu.eip
  decoding.seq_eip = ori_eip; // 为decoding.seq 赋值
  exec_real(&decoding.seq_eip); // 执行
  int instr_len = decoding.seq_eip - ori_eip; // 计算指令长度

  //  update_eip();  // 一下代码是此函数的展开
  if (decoding.is_jmp) { 
     decoding.is_jmp = 0; // 清空条状的标志位
  } else {
    cpu.eip = decoding.seq_eip; // 不是跳转需要借助decoding来更新eip
  }

似乎是不是有点一头雾水,不要着急,需要进一步的分析一下exec_real在做什么。

  1. exec.c 中间同样持有exec_real函数,实际上这一个文件将会是整个PA2 试验中间耗时最多的文件。exec_real 从函数名字可以知道,这才是开始指令执行的地方,
make_EHelper(real) {
  // fetch instrction move eip automatically
  uint32_t opcode = instr_fetch(eip, 1);
  decoding.opcode = opcode;
  set_width(opcode_table[opcode].width);
  idex(eip, &opcode_table[opcode]);
}

make_EHelper是一个宏,定义在exec.h中间

# define make_EHelper(name) void concat(exec_, name) (vaddr_t *eip)

你发现,任何函数没有直接修改cpu.eip 这一个变量的,而是修改了decoding中间变量,但是从更新update_eip()中间的逻辑可以发现,如果是非跳转指令,那么通过decoding的变量来修改cpu.eip,如果是跳转指令,那么cpu.eip就是被被rtl函数修改了

既然得到了这样的结论,那么来分析两个问题吧

call的函数中间不可以rtl_push(eip);不可以填写为rtl_push(&cpu.eip);, 因为eip是decoding.eip的指针,所有函数的参数传递都是使用这一个指针,修改了也就是decoding.eip的数值。你可能会问,非跳转指令为什么为什么需要修改decoding.eip, 又是在哪一个位置修改的,其实只有一个位置,instr_fetch(eip, 1) 指令。这一条指令在解码的过程中间,不断被调用,也就是decoding.eip 总是指向了CPU最近处理的
的字节,而cpu.eip则是指向在刚刚执行call指令的位置,如果这样填写,最后会导致发每次返回的时候都是在call指令的位置。

make_EHelper(call) {
  rtl_push(eip);
  rtl_j(decoding.jmp_eip);
  print_asm("call %x", decoding.jmp_eip);
}

如果对于eip和cpu.eip的区别理解不区分,如果将下面的函数中间前面两条语句替换为rtl_pop(eip)或者rtl_pop(&cpu.eip)同样导致错误,由于没有调用rtl_j(),导致在update_eip的时候,ret指令被识别为不同指令,cpu.eip的值会被decoding.eip重新刷新,

make_EHelper(ret) {
  rtl_pop(&t0);
  rtl_j(t0);
  print_asm("ret");
}

那么第一种写法看似没有问题, 因为decoding.eip中间的数值就是正确,通过刷新,cpu.eip的数值正好被修改。,但是如果开启了Debug, 使用cputest/dummy作为例子,其nemu-log.txt部分结果如下

  100000:   bd 00 00 00 00                        movl $0x0,%ebp
  100005:   bc 00 7c 00 00                        movl $0x7c00,%esp
  10000a:   e8 01 00 00 00                        call 100010
  100010:   55                                    pushl %ebp
  100011:   89 e5                                 movl %esp,%ebp
  100013:   83 ec 08                              subl $0x8,%esp
  100016:   e8 05 00 00 00                        call 100020
  100020:   31 c0                                 xorl %eax,%eax
  100022:   c3                                                            ret
  10001b:   d6                                    nemu trap

ret 指令不是对齐的,原因想必你是清楚的,decoding.eip的作用在于计算出来指令的长度,如果ret指令中间直接修改了decoding.eip, 那么计算出来的指令长度就是错误。

第二种写法,替换为rtl_pop(&cpu.eip)就更加不对了,update_eip会直接清除这里赋值。

既然关于eip讨论这么久,那就先剧透一下PA4中间可能遇到的bug, PA4中间一个地方要求实现时间片,也就是在exec_warpper()中间插入检查是否出现时钟中断,如果出现了, 就调用时钟中断,如果update_eip()的操作放置在raise_intr()的后面,结果就会出现各种奇怪的错误。

  if (cpu.INTR & cpu.IF) {
    cpu.INTR = false;
    update_eip(); // 需要收下更新eip
    raise_intr(IRQ_TIMER, cpu.eip);
  }

以上就是这一个试验可能遇到全部关于eip的坑,其实都是自己没有阅读源代码,然后自己坑自己,本实验没有处理synchronized exception, 那一种情况是需要执行exception handler之后需要重新执行指令,本实验模拟的CPU是非流水线的CPU, 导致eip的更新和异常的处理都变的很简单了。

  1. exec.c 中间持有一个最关键的表格, 也就是opcode_table。 opcode _table是一个opcode_entry类型结构体的数组。opcode_entry中间持有两个函数指针和设置操作数长度的变量,而函数指针就是处理一个opcode 所需要执行的解码逻辑和执行逻辑。

  2. exec.c中间idex函数会调用opcode_entry中间两个函数指针,来decode 和execute 指令,试验框架已经完成了所有mov指令的处理,我们剩下需要做的事情就是完成其他的指令的填写。

1.2 i386的编码和解码

理解i386的编码是试验的一大难点,毕竟汇编课程中间只是需要会写mov add 之类指令,没有人在乎二进制编码是什么样子的,组成原理试验中间使用的MIPS指令集,I J R 型号指令简单易懂。那么为什么i386 编码这么复杂,我认为主要原因是:
历史包袱
在指令集逐渐升级的过程中间,x86为了向下兼容,导致指令的数目越来越多,但是opecode 只有一个字节,怎么办,一共含有两个解决方法,理解这两个解决方法也就是后面试验的基础:

  1. 一种方法是使用转义码(escape code).x86中有一个2字节转义码0x0f, 当指令opcode的第一个字节是0x0f时, 表示需要再读入一个字节才能决定具体的指令形式(部分条件跳转指令就属于这种情况)
  2. 另一种方法是使用ModR/M字节中的扩展opcode域来对opcode的长度进行扩充

第一种方法解释opcode_table为什么划分为两个部分,第二个部分解释make_group,中间的细节就自己慢慢体会吧。

这一部分我有几个不成熟的小建议:

  1. 好好阅读文档
  2. 推荐一个网站, 实现具体指令的时候使用。
  3. 阅读i386手册的附录,理解关于E G I等等缩写的含义,理解解码函数的原理

1.3 Nemu如何模拟IO以及i386的实现IO的原理

IO的部分很简单,感觉随便填一下就完成,由于当时在赶进度,没有怎么思考其中的的原理,现在就好好回顾一下其中的内容。

1.4 Diff Test的实现原理

看了Diff Test的实现,我只好说,这一个框架的作者怎么这么猛啊。现在分析一下其中的源代码来,其中的收获也是不小的。

1.5 深入理解AM的含义

本项目的最上层的文件夹分别为Nemu Nemu-am nanos-lite navy-apps。其中nanos-lite是操作系统,Nemu是硬件,navy-apps是应用程序,难道这不就是一个计算机三个层次,为什么突然会多出来一个层次AM, 这一个用于做什么的。

文档的描述是为了封装硬件,实现统一的接口,具体的体现,在

2. 试验中间需要注意的问题

尽量早的完成diff test, 完成diff test的文档在完成指令填写之后,这导致很多人手动debug完成了大部分的指令填写之后才发现有这一个神器。指令填写过程中间一旦遇到bug,依赖于阅读汇编代码以及自己的gdb来debug, 需要话费及其长的时间,而且经过-02优化的代码难以阅读,遇到较大的项目,对应代码简直和天书一样。

不要遇到指令为实现的报错的时候才去完成对应的指令, i386的编码导致大量的类似的指令都是放在一起, 很多时候填写一条指令顺便查看一下附近编码的指令是不是也可以填写。比如如下是exec.c 中间定义的opcode_table的部分项目,全部都是前面两行全部都是add 指令,后面两行全部都是or 指令,仔细观察还会发现其中编码方式也是相同的

  /* 0x00 */    IDEXW(G2E, add, 1), IDEX(G2E, add), IDEXW(E2G, add, 1), IDEX(E2G, add),
  /* 0x04 */    IDEXW(I2a, add, 1), IDEX(I2a, add), EMPTY, EMPTY,
  /* 0x08 */    IDEXW(G2E, or, 1), IDEX(G2E, or), IDEXW(E2G, or, 1), IDEX(E2G, or),
  /* 0x0c */    IDEXW(I2a, or, 1), IDEX(I2a, or), EMPTY, EX(2byte_esc),

native 代码往往会泄露天机, 为了方便debug, 框架提供了native 代码,但是很多时候native代码和需要实现的代码大相径庭,只需要稍作修改就可以了,具体例子是VGA的实现以及PA3中间部分代码的实现。

`

你可能感兴趣的:(南京大学 计算机系统基础 课程实验 2018(PA2))