PA2.2&PA2.3

   日期:2020-05-16     浏览:1762    评论:0    
核心提示:PA2.2&PA2.3 计算机组成原理 实现更多地指令如sub,add使其可以运行c文件,并且运行时间测试,键盘测试和打字小游戏操作系统

写在前面的话

如果您对该系列感兴趣的话,推荐您先看一下南京大学的计算机组成原理实验(也就是PA)的讲义,然后再来看这篇文章可能有更多地收获。如果您是要完成该作业的学生,我推荐你先看讲义,或者好好听老师的讲课,然后自己独立完成这个作业,但是如果你没有听懂,或者你无论如何也无法理解讲义上面的字,又或者说对讲义上面的某点知识某个问题不了解而又觉得太简单不好意思问老师,那么您可能会从这篇文章里面获得一些你需要的信息。本篇文章将会包括笔者自己做PA的所有经过,希望你并不将该文章当成抄袭的根源,而是成为你思考的源泉。
现在已经到了PA2的阶段,这才真正开始了组成原理的道路,在PA0是搭建环境,PA1复习复习C语言,写写功能函数,而现在才开始真正去做计算机结构内部的东西,难度也上了一个档次。
PA2.2包含要实现的几十条机器指令,我将按照以运行不同文件的顺序来填补他们。由于此次任务较多,我也会写下我遇到的问题和大家共同交流。

PA系列传送门

PA0:https://blog.csdn.net/qq_41983842/article/details/88921427
PA1.1:https://blog.csdn.net/qq_41983842/article/details/88934779
PA1.2:https://blog.csdn.net/qq_41983842/article/details/89714479
PA1.3:https://blog.csdn.net/qq_41983842/article/details/89714689
PA2.1:https://blog.csdn.net/qq_41983842/article/details/95232055
PA2.2&2.3:https://blog.csdn.net/qq_41983842/article/details/101164495
PA3.1:https://blog.csdn.net/qq_41983842/article/details/103094859
PA3.2:https://blog.csdn.net/qq_41983842/article/details/103843093
PA4:https://blog.csdn.net/qq_41983842/article/details/104667951

目录

文章目录

    • 写在前面的话
    • PA系列传送门
    • 目录
    • 思考题
    • 实验内容
      • 实现更多的指令,启用已实现指令,逐个通过测试样例
        • 在add.c里面:
        • 在add-longlong.c里面
        • 在bubble-sort.c里面
        • 在fact.c里面
        • 在leap-year.c里面
        • 在load-store.c里面
        • 在matrix-mul.c里面
        • 在mov-c.c里面
        • 在mul-longlong.c里面
        • 在sub-longlong.c里面
        • 在bit.c里面
        • 在recursion.c里面
      • 一键回归测试
      • 加入IOE
      • 实现输入输出指令,运行Hello World
      • 实现 IOE 抽象
        • 实现_uptime()
        • 实现 _read_key()
        • 画矩形函数
      • 运行 timetest
      • 看看 NEMU 跑多快
      • 运行 keytest
      • 添加内存映射I/O
      • 运行 videotest
      • 运行打字小游戏
    • 遇到一些的问题以及解决方法

思考题

  1. 什么是 API?

    API是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

  2. AM 属于硬件还是软件?

    我认为AM属于软件,因为他并不是真实存在的,而是一个抽象的计算机模型,是理论化的,计算机都是通过这个模型来进行生产出来,变成真实的硬件。我认为他和操作系统比较相近,操作系统是运行在硬件上介于APP层的硬件管理程序,而AM也是运行在NEMU上的介于APP层之间的一个运行程序,相当于实现了和操作系统相同的功能。

  3. 为什么堆和栈的内容没有放入可执行文件里面

    因为堆和栈中数据的变化比较频繁,如果放进可执行文件中读取速度会变慢。所以当程序运行的时候,从Memory里面来申请栈和堆的使用。

  4. 从你输入完这条命令敲击回车,直到出现 NEMU 的提示符 (nemu) 之间,都做了哪些工作?

    • 敲回车
    • ARCH=x86-nemu:让AM上面的项目编译到 x86-nemu 的 AM 中
    • make ALL=dummy run:用dummy作为测试样例,调用 nexus-am/am/arch/x86-nemu/img/run 来启动 NEMU并载入 dummy 这个用户程序进入运行
    • 出现(NEMU)
  5. 神奇的eflags(2)

    +-----+-----+------------------------------------+
    | SF  |  OF |                实例                |
    +-----+-----+------------------------------------+
    |  0  |  0  |               2 - 1                |
    +-----+-----+------------------------------------+
    |  0  |  1  |         -1 - 0x80000000            |
    +-----+-----+------------------------------------+
    |  1  |  0  |               1 - 2                |
    +-----+-----+------------------------------------+
    |  1  |  1  |         0x7fffffff - -1            |
    +-----+-----+------------------------------------+
    
  6. 指令中的 above, below, greater, less 这些大小关系词之间有什么关联呢

    • op2>op1,且是无符号数,触发above,也就是ja指令跳转
    • op2<op1,且是无符号数,触发below,也就是jb指令跳转
    • op2>op1,且是有符号数,触发greater,也就是jg指令跳转
    • op2>op1,且是有符号数,触发less,也就是jl指令跳转
  7. NEMU的本质

    • label1:
      	x = x - 1;
      	y = y + 1; //x和y是两个加数,先通过一个数相减到零另外一个数相加的方式把x转移到y上面
      jne y, label1 //一直到y等于0为止
      
      label2:
      	y = y - 1;
      	a = a + 1; //现在y是x和y的和,只需要把x和y转移到a上面就行了。
      
    • NEMU还缺少友好的用户交互界面也就是输入输出、图形、视频等。

  8. 事实上, EMPTYEX(?)EXW(?, ?) 都是等价的

    EMPTYEX(inv)EXW(inv, 0) 都是等价的

  9. mov 指令后面的lbw 这些字符为什么会存在?仔细观察他们是在什么地方出现的,不加这些标识符会有什么后果?每个指令,准确地说,每个指令每次出现都需要用着几个字符标识吗?

    • 为了说明进行mov指令的操作数的宽度。

    • exec.h文件中有如下定义

      #define suffix_char(width) ((width) == 4 ? 'l' : ((width) == 1 ? 'b' : ((width) == 2 ? 'w' : '?')))
      

      不加这些标识符将会把进行mov指令操作数的宽度混淆,无法区别。

    • 都需要

  10. 如果让你来设计 CPU 和设备的通信,你会如何实现呢?

    从简单来讲,就是采用应答式的通信,CPU通过一系列的发送端口向相应的设备发出指令信号,设备接收之后执行自己的工作,执行结束或者遇到错误发送反馈信号给CPU的接收端口。

  11. CPU 需要知道设备是如何工作的吗

    不需要,CPU只需要将字节送到端口上,然后剩下的事情交给设备来干,如果有数据返回的话CPU需要知道这个反馈,我倒是认为操作系统需要知道设备是怎么工作的,这样设备出故障了以后才可以采取相关措施。在这期间,设备需要从CPU的输出端口上面读取相应的操作指令来完成它本身的工作。

  12. 什么是驱动

    驱动程序全称设备驱动程序,是添加到操作系统中的特殊程序,其中包含有关硬件设备的信息。此信息能够使计算机与相应的设备进行通信。驱动程序是硬件厂商根据操作系统编写的配置文件,可以说没有驱动程序,计算机中的硬件就无法工作。

    操作系统是用户和电脑之间的接口,而驱动程序是硬件可以运行的必要保障,打个比方,操作系统就像是人的生命一样,没有生命,电脑就是一个空的躯体,驱动,就是保证人在有生命的情况下,各个器官都可以正常工作。

  13. CPU 在执行这条指令的时候需要知道自己要访存的地址是真正处于内存中还是经过重定向的吗

    不需要,只需要把某段地址上的数据设定为指定的值就行了。

  14. 如果代码中的地址 0x8048000 最终被映射到一个设备寄存器, 去掉 volatile 可能会带来什么问题

    反汇编:

    去掉关键字后反汇编:

    其实我感觉好像没啥区别,百度了一下,发现volatile去掉以后后面的几步就会省略,然后如果进行这些代码优化的话就会损失数据。

  15. 这个 hello 程序和我们在程序设计课编程写的第一个 Hello World 程序一样吗?如果不一样, 它们分别运行在计算机哪个层次中?

    不一样。计算机中的Hello World 程序运行在硬件层,而我们这个hello程序运行在AM层

  16. 如何检测多个按键同时被按下

    每个键盘对应一个键盘码,当这个键盘被按下去的时候,构成一个通路,发生按键事件,因为每个键盘的按键码是不同的,所以他们之间相互独立,互不影响,当我按下多个键盘的时候,计算机只需要找到对应达成通路的键盘位置来执行相应的操作就行了。

  17. 分别解释为什么会发生这些错误? 你有办法证明你的想法吗

    去掉了很多static但是运行nemu的时候并没有报错…但是去掉inline以后就会报错。百度了一下inline关键字的作用,用来把一些频繁使用的函数放到栈里面提高运行效率。所以去掉这个关键字后这个函数就没有定义在栈区,由于其他函数调用这个rtl操作的时候仍然是在栈区寻找,并没有用到这个在栈区外面的函数,所以就会出现这种错误,显示这个函数定义了但是没有被使用。

    而如果把他们两个都去掉的话,就会出现这种错误,生成.o文件的时候出问题。也就是去掉了staitc关键字后就会出现多重定义的问题。

  18. 重新编译后的 NEMU 含有多少个 dummy 变量的实体

    有29个。可以通过grep -rn "dummy" |wc -l指令来查看

  19. 此时的 NEMU 含有多少个 dummy 变量的实体

    有58个,多了29个,加上了debug.h里面的。 同样通过上面那条指令查看。

  20. 发现了什么问题

    会有一个连接错误,他没初始化之前是若符号,所以不会报错,根据他之前定义的强符号处来处理,而定义了强符号也就是初始化之后呢就会出现两个强符号大家的情况,自然就是连接错误。

  21. 系统板保留 1K 个 I/O 端口, 那么系统 I/O 地址的范围是多少

    1K个接口=2^10,每个端口是8个地址,那么地址就是0000H->8000H。变成16地址编线以后变成8001H-FFFFH。

  22. 有什么来自 CPU 的信号参与了设备的选通或控制

    比如磁盘与内存间的信息交换,希望用硬件在外设与内存间直接进行数据交换,而不通过CPU,这样数据传送的速度的上限就取决于存储器的工作速度.但是,通常系统的地址和数据总线以及一些控制信号线(例如I/O等)是由CPU管理的,所以当CPU发出DMA响应信号之后,DMA控制器接管对总线的控制.在DMA方式时,CPU把这些总线让出来,由他接管,控制传送的字节数,判断DMA是否结束,以及发出DMA结束等信号.这些都是由硬件实现的.在DMA传送结束以后,他结束DMA请求信号,释放总线,使CPU恢复正常工作。

实验内容

实现更多的指令,启用已实现指令,逐个通过测试样例

跟PA2.1一样,还是实现指令,但是这回实现的指令就比之前的多多了,好在熟悉了它的过程,实现起来也没有那么无从下手了。

  • Data Movement Instructions: mov, push, pop, leave, cltd(在i386手册中为cdq), movsx, movzx
  • Binary Arithmetic Instructions: add, inc, sub, dec, cmp, neg, adc, sbb, mul, imul, div, idiv
  • Logical Instructions: not, and, or, xor, sal(shl), shr, sar, setcc, test
  • Control Transfer Instructions: jmp, jcc, call, ret
  • Miscellaneous Instructions: lea, nop

一共要实现这么多指令,看了看这么多测试用例,先从那个开始实现呢?

在add.c里面:

8d这个地址没有实现

查表发现是lea指令

由于这个函数已经写好了是M2G译码函数,我们只需要填表即可

成功实现

0x83处出了问题

和2.1实现的sub一样,要在grp1里面找,查看反汇编是and操作没有实现

第5个是and,填表

logic.c里面写执行函数

make_EHelper(and) {
  rtl_and(&t1, &id_dest->val, &id_src->val); //目的操作数与源操作数相与
  operand_write(id_dest, &t1); //写入目的操作数

  rtl_update_ZFSF(&t0, id_dest->width); //更新ZFSF位
  t0 = 0;
  rtl_set_OF(&t0); //设置OF位为0
  rtl_set_CF(&t0); //设置CF位为0
  
  print_asm_template2(and);
}

结果一运行发现and指令读取的立即数不对

最后定位到make_DopHelper(SI)函数符号扩展的问题在PA2.1没有实现,当初想的太简单了,直接赋值了,没有考虑数据长度的问题。现在修正过来:

之后修改一下执行函数:

成功运行

查看0xff处,在grp5里面找,由反汇编得知是push没有填表

填表

实现


查找0x66处,查看反汇编,xchg指令没有实现,查看手册发现是oprand_size操作

10006a: 66 90 xchg %ax,%ax

但是发现这个操作已经被填表、声明、实现了,那问题在哪里呢?我看到了66后面的90地址,发现我的90地址处没有填入任何东西,先把90地址处填好吧,nop指令,又是直接执行。

然后执行函数他也已经写好了,吃惊的是只有1个printf,我看里面也没有写todo,那就直接用把。

成功实现

查找0x03处,是add指令

可以很轻松的填表


操作很简单,但是符号位都要更新。模仿已经实现的adc指令在arith.c中实现add

成功实现


查看0x3b

填表,从0x380x3d都要实现


与减法的不同就在于他不改变结果,只改变eflags,少了操作数的写入操作,在arith.c里面开始实现

成功实现


查看0x0f处和反汇编发现是set操作

10008a: 0f 94 c0 sete %al

表中已经填好了。并且执行函数也在exec.c里面给出来了。现在在2-byte表里查看0x94地址处的相关操作

0x90开始到0x9f全部都是set相关操作,全部填表,也是填在里面,手册勘误中setcc的长度为1,也就是8位。

大家都一样,译码函数是E,宽度虽然为2,但是手册上面写到first byte is 0FH,所以我们操作的位数只有一位。

logic.c文件中找到了相应的执行函数

make_EHelper(setcc) {
  uint8_t subcode = decoding.opcode & 0xf;
  rtl_setcc(&t2, subcode);
  operand_write(id_dest, &t2);

  print_asm("set%s %s", get_cc_name(subcode), id_dest->str);
}

发现rtl_setcc函数没有被实现。

cc.c文件里面找到了rtl_setcc函数,从注释来看,让我查询每个eflag来看看是不是满足条件代码。枚举类型不赋初值,第一个默认为0,往后依次加一。这就是讲义中说的哪个需要更新eflags的操作。根据手册勘误来完成相关的操作!

void rtl_setcc(rtlreg_t* dest, uint8_t subcode) {
  bool invert = subcode & 0x1;
  enum {
    CC_O, CC_NO, CC_B,  CC_NB,  //值从0到3
    CC_E, CC_NE, CC_BE, CC_NBE, //值从4到7
    CC_S, CC_NS, CC_P,  CC_NP,  //值从8到11
    CC_L, CC_NL, CC_LE, CC_NLE  //值从12到15
  };//枚举的值和subcode的字节对应

  // TODO: Query EFLAGS to determine whether the condition code is satisfied.
  // dest <- ( cc is satisfied ? 1 : 0)
  switch (subcode & 0xe) {
    case CC_O://0
      rtl_get_OF(dest);
      break;
    case CC_B://2
      rtl_get_CF(dest); //小于,通过CF来判断不够减
      break;
    case CC_E://4
      rtl_get_ZF(dest);
      break;
    case CC_BE: { //6
      rtl_get_CF(&t0);
      rtl_get_ZF(&t1);
      rtl_or(dest, &t0, &t1); //小于等于,CF和ZF至少一个要等于1才行
    }break;
    case CC_S: //8
      rtl_get_SF(dest);
      break;
    case CC_L: { //12
      rtl_get_SF(&t0);
      rtl_get_OF(&t1);
      rtl_xor(dest, &t1, &t0); //带符号数的小于,SF不能等于OF
    }break;
    case CC_LE: { //14
      rtl_get_ZF(&t0);
      rtl_get_SF(&t1);
      rtl_get_OF(&t2);
      rtl_xor(&t3, &t1, &t2);
      rtl_or(dest, &t0, &t3); //带符号数的小于等于,ZF=1或者SF不等于OF
    }break;
    default: panic("should not reach here");
    case CC_P: panic("n86 does not have PF");
  }

  if (invert) {
    rtl_xori(dest, dest, 0x1);
  }
}


成功实现


查看2-byte表的0xb6处,movzx指令没有实现

0xb60xb7处填表,一个宽度为1,一个宽度为2

执行函数相当于是一个0扩展并且不影响符号位,已经写好了。声明一下就可以了。成功实现


查看0x85处,是test指令没有实现

填表,0x84是一个字节,0x85是变长字节,译码函数G2E


logic.c里面写执行函数

成功实现


查看0x74处,查看反汇编,是jcc指令没有实现

100034: 74 02 je 100038 <nemu_assert+0xc>

执行函数在control.c已经实现好了,直接声明+填表就行了。从0x70开始这一行都填好表,译码函数应该是J

成功实现


查看0x83处,再查看grp1,最后看反汇编是add操作,发现自己grp1里面的add没有填表

填表即可


make_group(gp1,
    EX(add), EMPTY, EMPTY, EMPTY,
    EX(and), EX(sub), EMPTY, EMPTY)

成功实现


还是0x83处,这回是grp1cmp指令,照样填表,成功实现

make_group(gp1,
    EX(add), EMPTY, EMPTY, EMPTY,
    EX(and), EX(sub), EMPTY, EX(cmp))



查看0x6a处,立即数的push操作


填表,译码函数是push_SI,执行函数还是push
运行之后会收到operand_write报错。检查自己的push函数,当初2.1写的其实是有问题的。push就应该是简简单单的压栈保存操作,没有写入操作数的过程,就像这里,如果写入操作数的话,函数会找不到操作数的种类而报错。其他的像pop、add指令才是有写入操作数的。将6a和68处全部写好,可以看到,68处是变长,6a处是1个字节

更改函数之后成功实现该指令

最后成功跑通这个文件

在add-longlong.c里面


查看手册,是adc指令没有实现。

0x100x15,根据长度来进行填表,adc执行函数写好了,直接声明就行了。

成功实现


查看0x09处,or指令没有实现

从08开始到0D都要填表,注意操作数长度,常规操作。


根据手册实现执行函数

成功实现,并且完成了add-longlong文件的运行

在bubble-sort.c里面

在这里就遇到了一个问题,也是一个常见问题,jcc指令运行的时候eip的值不对

通过PPT上面讲的各种情况依次判断,首先查看表有没有填错,对照反汇编代码进行比较看看有没有不同的地方,比较发现没有,然后看看其他的指令的eflags有没有写错,毕竟jcc是根据eflags的值来进行跳转的。经过比较发现,先查看jcc上面一条cmp指令的SF、ZF、OF位在执行这条指令的前后发生的变化

此时我们就可以判断SF位没有写对,因为我们传入了一个负数,SF位应该是1,但是SF位没变。然后检查rtl_SF函数,发现2.1写的这个函数有问题,取到的符号位是错的,更改这个函数,通过右移相应位数来找到符号位。

还有一个需要注意的问题就是,写这个rtl的时候,结果不要用一个变量来存,就比如说我传进rtl_update_SF这个函数的result参数是t0,而在这个函数里面我仍然用t0储存result的符号位,那么设置SF位的时候是没问题的,但是如果在cmp函数里面更新完SF后又要用这个t0来做其他的操作,那么就会出错了。之前我就犯了这个错误,后来把他改成了没人用的t4。ZF位也是同样的道理。说到ZF位,其实之前直接用那种[width * 8 - 1]的放来来取数组相应位置是非常不科学的,之前没出错可能是因为碰巧吧,根本就没有考虑长度的问题。现在修正一下,一起修正的还有rtl_msb函数,也是用的这种方式,不能直接对数组取这样的位,而是应该通过右移来解决。



解决了SF的问题,现在成功运行

在fact.c里面


查看0x43处,inc指令没有实现,填表,都是对寄存器的操作,所以长度为4
这是个巨坑!!!!!这里写的有问题!PA3.1会进行改正


查看操作非常简单,符号位更新ZF、SF和OF。


成功实现


查找0xebjmp指令没有实现,根据长度填表,

IDEX(J, call), IDEX(J,jmp), EMPTY, IDEXW(J,jmp,1),

指令已经实现,填表后成功运行


查看0x0f处,imul指令没有实现
1000b0: 0f af d0 imul %eax,%edx 在2-byte表里面相应位置填

EMPTY, EMPTY, EMPTY, IDEX(E2G, imul2),

查看执行函数可以知道,imul指令分好几个操作数有不同的执行函数,这里我们实现两个操作数的。声明以后就可以实现了。

查看0x48处,dec指令没有实现

对32位寄存器操作,宽度为4,开始填表

  IDEX(r,dec), IDEX(r,dec), IDEX(r,dec), IDEX(r,dec),
  IDEX(r,dec), IDEX(r,dec), IDEX(r,dec), IDEX(r,dec),


在我们这里没有AF,所以实现自减然后更新符号位就行了,模仿之前实现的inc

成功实现并且跑通这个函数

在leap-year.c里面


查找0xf6处,是grp3里面的test指令
之前已经实现了test,相应位置填表就行了。

make_group(gp3,
    IDEX(test_I,test), EMPTY, EMPTY, EMPTY,
    EMPTY, EMPTY, EMPTY, EMPTY)

成功实现


查找0x991000db: 99 cltdcltd指令没有实现,先填表

cwtlcltd都填了好了,他们两个都没有译码函数,直接执行

EX(cwtl), EX(cltd), EMPTY, EMPTY,

这个指令相对来说很简单,就是简单的将eax或者ax寄存器的值符号扩展32位到edx寄存器,也就是说,如果eaxax寄存器的二进制序列的最高位为0,则cltd指令就把edx或者dx置为0,相反.如果eax寄存器的二进制序列最高位为1,则cltd指令将会自从填充edx寄存器为32个1.判断decoding.is_operand_size_16的值,根据0和1这两种情况在data-mov.c里面开始实现

cwtl指令也是同样的操作,跟ctld差不多,都是一个if-else,还省去了右移的操作,直接读出来然后符号扩展就行了。只不过针对的是alaxeax这三个寄存器。

成功实现

查看0xf7处,1000dc: f7 fe idiv %esiidiv操作没有实现。这个也是直接填表然后声明就行了。为了方便,这次把所有grp3里面已经实现的执行函数的表全部填完

make_group(gp3,
    IDEX(test_I,test), EMPTY, EMPTY, EMPTY,
    EX(mul), EX(imul1),EX(div), EX(idiv)) //imul在这里操作数只有一个

成功实现

并且成功跑通这个函数

在load-store.c里面


查看反汇编和手册,100063: 0f bf 83 c0 01 10 00 movswl 0x1001c0(%ebx),%eaxmovsx指令没有实现译码函数是mov_E2G,执行函数就是已经写好的movsx0xbe0xbf这两个地方宽度不同,现在填表

成功实现


查看0xd31000ec: d3 e0 shl %cl,%eax左移操作没有实现,这次把左移右移和grp2中的带符号右移sar全部实现,方便以后。
grp2里面填表

这三个操作都不需要更新CF位和OF位,简直省了一半的力气,手册里面把这几个指令的操作写在一起了。而且几乎全部都是符号位的操作,所以我就根据之前已经实现的rtl指令来写,好在这三个指令的rtl操作都已经定义好,直接用就行了。写好一个函数之后,其他两个函数也是照葫芦画瓢。

成功实现

查表得知这次是not指令填表没有译码函数,直接执行。

make_group(gp3,
    IDEX(test_I,test), EMPTY, EX(not), EMPTY,
    EX(mul), EX(imul1),EX(div), EX(idiv)) //imul在这里操作数只有一个

这个操作很好实现,说白了就是按位取反,然后不影响任何的标志位。

成功实现并且完美运行load-store.c

在matrix-mul.c里面



长跳转指令没有实现,其实就是没有填表,之前已经实现了短跳转指令,长度为变长。

成功实现并且成功运行程序

在mov-c.c里面


实现指令,没有译码函数,直接执行

data-mov.c里面实现这个函数,leave指令是将栈指针指向帧指针,然后POP备份的原帧指针到%ebp,所以leave等价于

movl %ebp %esp
popl %ebp

这样就好实现了,利用rtl指令很快就可以写出来。

成功实现并且运行这个文件

在mul-longlong.c里面



sub指令没有填表,根据长度和译码函数来填表,从28到2d

成功实现并且运行

在sub-longlong.c里面



sbb指令没有填表,根据长度和译码函数填表

成功实现并运行

在bit.c里面


查看手册,是在grp4里面的incdec没有实现。直接填表就行。宽度都是1。


成功实现


查看手册,and没有填表

填表

成功运行

在recursion.c里面


grp5里面有一个call指令,没有填表

查看反汇编10011a: ff 15 f0 01 10 00 call *0x1001f0,这个call有点奇怪,要从内存里面读出来地址,然后再跳转。这是转移地址在内存中的call指令。在control.c文件里面找到了call_rm操作需要我写。

看到反汇编中还有100099: ff 25 e8 01 10 00 jmp *0x1001e8

1000c1: ff 05 fc 01 10 00 incl 0x1001fc等操作,为了方便就把incdec还有jmpjmp_rm也都填表。

汇编格式为call word/dword ptr 内存单元地址,其实现的过程大概可以分成这几条

push CS #将call指令的下一条指令的段地址
push IP #及偏移地址入栈
jmp ... #转到子程序去执行

对照着上面的jmp_rmcall,其实就是他们两个结合起来的操作。


成功实现并运行

all-instr.h中声明所有的函数

一键回归测试

经过上面指令的实现,现在可以通过一键回归测试了

加入IOE

打开#define HAS_IOE

出现窗口

实现输入输出指令,运行Hello World

先在all-instr.h中声明

在手册中找到inout的位置,从e4->e7,从ec->ef


根据指令宽度和译码函数填表,这两个指令有专门的译码函数,执行函数是inout。长度只有两种,变长和b

system.c里面开始写执行函数,out要调用 pio_write() 函数,in要调用 pio_read() 函数并且还要有写入操作数的行为。查看这两个调用的函数,发现 pio_read() 还要有一个data的返回值,所以需要通过这个返回值来写入操作数。

打开HAS_SERIAL

这时运行出现了问题,遇到了未知的操作

查看手册发现是test指令没有填表
填表

成功实现

实现 IOE 抽象

实现_uptime()

在上课的时候助教讲过,实现这个时间需要通过一个减法,通过0x48处的端口作为RTC寄存器获得的当前时间减去NEMU初始化时候的“当前”时间,就会获得这个时间差。

ioe.c里面的_uptime()函数里添加这样的代码:

实现 _read_key()

ioe.c里面找到相关代码,返现只有一个返回值是_KEY_NONE,全局搜索这个返回值,在SPEC.md中发现按键代码由_KEY_XXX指定,其中_KEY_NONE = 0。所以我要根据我按键的不同来返回不同的按键代码。讲义上面讲到初始化时会注册 0x60 处的端口作为数据寄存器, 注册 0x64 处的端口作为状态寄存器,那么我们按键的状态就在0x64这个端口的寄存器中储存,而通码的值为 断码 + 0x8000

画矩形函数

void _draw_rect(const uint32_t *pixels, int x, int y, int w, int h) {
  int len = sizeof(uint32_t) * ( (x + w >= _screen.width) ? _screen.width - x : w );
  uint32_t *p_fb = &fb[y * _screen.width + x];
  for (int j = 0; j < h; j ++) {
    if (y + j < _screen.height) {
      memcpy(p_fb, pixels, len);
    }
    else {
      break;
    }
    p_fb += _screen.width;
    pixels += w;
  }
}

感觉这个函数很复杂,我只能大概讲出来他干了些什么事情…首先要判断这个x+w是不是超出了屏幕的边界,然后呢通过一个大循环来把每个像素的值memcpy也就是复制到他对应的内存里面,然后显示出来,通过p_fbpixels的不断变化来实现这种动画效果。

运行 timetest

成功在NEMU中运行 timetest 程序,每秒钟输出一句话。

看看 NEMU 跑多快

注释掉DEBUGDIFF_TEST

  • dhrystone (大家的电脑跑多少分呢)

    x86-nemu

  • coremark

    x86-nemu

  • microbench

    大规模测试集ref

    小规模测试集testmake INPUT=TEST run

    x86-nemu的时候出了些问题,neg指令没有实现。求补运算,说实话这个命令挺少见到过的。从手册中可以得知,当操作数为0时,置CF位为0,当操作数不为0时,置CF位为1。现在我需要把目的操作数的所有数据位取反。注意这里的取反不是按位取反,而是x->-x,


    然后发现rol指令没有实现,在grp2里面,先填表,发现没有他的执行函数,然后现在来重新在logic.c里面写执行函数。

    左移一次,左移前的最高位送入最低位以及CF,也就是左边移出的位补到右边。与RCL不同之处在于他没有进位。所以我们需要左移和右移操作,并且最后要写入操作数。一开始我把所有的位数都先左移出来,然后想一下子赋值,结果发现并不能实现,所以就考虑for循环的情况,每次取出来最高位给一个变量赋值,然后这时候目的操作数左移一位,此时这个数的最后面一位变成了0,这个时候我赋值的这个变量的值只有两种情况,对应最高位的两种情况:0或者1,之后通过异或操作,把这个变量与目的操作数异或就达到了目的。

    最后跑分:

运行 keytest

运行 keytest 程序

添加内存映射I/O

讲义上面已经写的很详细了,就是加一个if-else语句就行。

mmio_id <- is_mmio(???);
if (mmio_id != -1) {
  调用 mmio_read/write(???);
  返回读到的映射内存数据(写映射内存则返回空即可);
}


由于memory.c里面只包含了nemu.h这个头文件,所以要新增加头文件mmio.h

运行 videotest

运行 videotest 程序

运行打字小游戏

typing目录下运行打字小游戏,如果是虚拟机可能没有那么流畅,如果小于3FPS就说明前面实现的可能有问题,效率比较低。

遇到一些的问题以及解决方法

  1. 遇到问题:实现and指令的时候nemu打出来的反汇编的立即数是0xf0而反汇编文件里面是0xfffffff0

    解决方案:在make_DoHelper函数里面没有实现符号扩展,由于默认是0扩展,所以实现失败。

  2. 遇到问题:实现push %0x1指令的时候operand_write函数报错assert(0)被触发。

    解决方案:push就应该是简简单单的压栈保存操作,没有写入操作数的过程,就像这里,如果写入操作数的话,函数会找不到操作数的种类而报错。其他的像pop、add指令才是有写入操作数的。去掉push函数里面的写入操作数操作。

  3. 遇到问题:diff-test报错,jcc指令的eip值不匹配。

    解决方案:通过PPT上面讲的各种情况依次判断,首先查看表有没有填错,对照反汇编代码进行比较看看有没有不同的地方,比较发现没有,然后看看其他的指令的eflags有没有写错,毕竟jcc是根据eflags的值来进行跳转的。经过比较发现,jcc上面一条cmp指令出现了问题,SF位没有写对,然后检查rtl_SF函数,发现2.1写的这个函数有问题,取到的符号位是错的,更改这个函数,通过右移相应位数来找到符号位。成功解决。

  4. 遇到问题:键盘测试总是无法识别相应的键盘,按下去键盘什么都不会输出

    解决方案:在keytest的程序里面通过printf来确定位置,最后发现程序并没有执行_read_key这个函数,打字小游戏都没问题,但是键盘测试一直都接收不到。最后找了很长的时间,疯狂排除错误仍然不知道问题在哪个地方,在可能出现问题的地方到处都调试了然后发现实现的没有问题,一度陷入了深深的悲伤之中。结果一天上课碰巧提起来了这个问题,一跟同学描述我错误的过程,然后同学告诉我应该在make run之后弹出来的窗口里面打字,而不是在终端里面的nemu里按键。一下子一语点醒梦中人!我之前所有的键盘的测试,全部都是在nemu里面输入,而这些输入的东西是被当做跟siinfo等一样的指令来执行的。而现在我应该在编写好的这个keytest里面按键来测试程序。回去一试,果然可以成功接收。

PA2.2和PA2.3的部分到这里就结束了,篇幅非常长,感谢您的耐心阅读。

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
更多>相关资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服