PA2.1

写在前面的话

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

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

思考题

  1. 设某指令执行前 eip 值为 x1,该指令执行后 eip 值为 x2,那么 x2 - x1 的这个差值都包括了一条指令的哪些组成成分?

    opcode、操作数的地址、存储地址、指令地址,指令类型不同包括不同的成分。

  2. opcode_table 数组中存放了所有指令的信息,请问表中每个表项是什么类型?NEMU 又是如何通过这个表项得知操作数长度、应该使用哪个译码函数、哪个执行函数等信息的?

    opcode_entry类型,NEMU通过set_width知道操作数长度,通过make_DHelper函数知道要用哪个译码函数,通过make_EHelper函数知道要执行函数信息。

  3. 操作数结构体/共同体中都包括哪些成员,分别存储什么信息?他们是如何实现协同工作的?

    typedef struct {
      uint32_t type; //操作数类型
      int width; //操作数宽度
      union {
        uint32_t reg; //寄存器操作数
        rtlreg_t addr; //操作数的地址
        uint32_t imm; //立即操作数
        int32_t simm; //无符号立即操作数
      };
      rtlreg_t val;//RTL寄存器
      char str[OP_STR_SIZE];//操作数指令
    } Operand;
    
  4. 复现宏定义

    • make_EHelper(mov) //mov 指令的执行函数

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

      ->#define concat(x, y) concat_temp(x, y)

      ->#define concat_temp(x, y) x ## y

      把x和y这两个参数粘在一起变成xy,所以在宏定义中将exec_mov粘在一起,执行mov操作

    • make_EHelper(push) //push 指令的执行函数

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

      ->#define concat(x, y) concat_temp(x, y)

      ->#define concat_temp(x, y) x ## y

      把x和y这两个参数粘在一起变成xy,所以在宏定义中将exec_push粘在一起,执行push操作

    • make_DHelper(I2r) //I2r 类型操作数的译码函数

      #define make_DHelper(name) void concat(decode_, name) (vaddr_t *eip)

      与上一个一样,把decode_I2r连接起来,执行immediate to register译码操作

    • IDEX(I2a, cmp) //cmp 指令的 opcode_table 表项

      #define IDEXW(id, ex, w) {concat(decode_, id), concat(exec_, ex), w}

      ->id变量表示译码函数参数名称,传给译码函数,ex变量表示执行函数参数名称,传给执行函数,w表示width,处理的数据的宽度。

    • EX(nop) //nop 指令的 opcode_table 表项

      #define EX(ex) EXW(ex, 0)

      ->ex变量表示执行函数参数名称,传给执行函数,EX(nop)=EXW(nop, 0)

    • make_rtl_arith_logic(and) //and 运算的 RTL 指令

      #define make_rtl_arith_logic(name) \
        static inline void concat(rtl_, name) (rtlreg_t* dest, const rtlreg_t* src1, const rtlreg_t* src2) { \
          *dest = concat(c_, name) (*src1, *src2); \
        } \
        static inline void concat3(rtl_, name, i) (rtlreg_t* dest, const rtlreg_t* src1, int imm) { \
          *dest = concat(c_, name) (*src1, imm); \
        }
      

      这个我在实现RTL指令的时候详细说明了。

  5. 立即数背后的故事

    由于大端小端两种方式数据的字节保存的地址位置相反,需要注意大端小端两种保存数据模式的不同,先判断机子是大端还是小端存储方式,如果是大端,就传到一个将小端存储方式转换成大端存储方式的函数里面就行了。

  6. 神奇的 eflags

    当运算的结果超过字长所能表示的范围时,产生溢出,CF当然不能代替OF,对于运算的数来说,只要符合进位的情况,CF就置1,怎么可能所有的进位情况都会超过字长所表示的范围呢?在运算的过程中,当两个符号相同的数相加,结果的符号与之相反,则OF=1,否则OF=0. 或者当两个符号不同的数相减,结果的符号与减数相同,则OF=1,否则OF=0.

实验内容

实现所有 RTL 指令

一开始打开rtl.h文件发现里面这么多TODO()可给我吓坏了,之后好好看了PPT和讲义才找清楚他们都是干什么的,然后才好开始写,一些rtl指令很简单,完全就是跟着注释走,基本上注释已经把答案写出来了。

在开始写所有的指令之前,有一段代码引起了我的注意
在这里插入图片描述
这是干啥的啊,看起来定义了好多的运算,但是定义的又不是那么清楚直接,我想了半天才明白了,重点在他定义的make_rtl_arith_logic里面,这个函数有一个参数名字叫name,下面紧接着一个concat函数,作用应该是将rtl_name连接起来,就构成了rtl_name这样一个东西,然后呢,在这个函数里面就把这个东西当成c_name,找到上面的宏定义来实现相应的功能,第35到44行,一共定义了这么多运算,这可就省了好多事了,在接下来完善rtl指令的时候就可以直接调用这些定义好的运算了。于是就开始实现相关指令

rtl_mv函数,就是简单赋值:
rtl_not函数:
在这里插入图片描述
rtl_eq0函数:
rtl_eqi函数:
rtl_neq0函数:
rtl_msb函数:请注意,这个函数的写法是错误的!将在PA2.2中解决
在这里插入图片描述
然后这些最简单的操作写完了,就可以写其他的比较复杂的操作了。

rtl_push函数:
rtl_pop函数:
在这里插入图片描述
rtl_sext函数,这个函数写的时候是费了我好大的劲,想了很多种情况,然后问了助教之后,不管src是多少位,通通扩展到32位,这样以来就简单一些,可以通过与一个数相或或者左移右移的方法来实现符号扩展,这里我选择的是左移右移的方式。因为寄存器都是无符号整型,所以左移右移没办法移入1,所以要设置一个带符号整数,通过带符号整数来左移右移,最后赋给dest
在这里插入图片描述
make_rtl_setget_eflags,在写这里的时候遇到了一些困难,这个函数没有注释提示,然后我只知道和寄存器有关,然后就无从下手,把这个任务拖了拖,结果后面一看讲义,要我自己实现eflags结构体,然后就转去先做任务3再回来做这个,其实就是简单的赋值操作。
在这里插入图片描述

rtl_update_ZF函数,简单的判断之后就传到rtl_set_ZF函数里面设置给ZF相应的值。请注意,这个函数的写法是错误的!将在PA2.2中发现并解决

rtl_update_SF函数,找到符号位,传到rtl_set_SF函数里面设置给SF相应的值。请注意,这个函数的写法是错误的!将在PA2.2中发现并解决
在这里插入图片描述

实现 eflags 寄存器

查询了位域的相关概念,把讲义搬过来

 31                  23                  15               7             0
 +-------------------+-------------------+-------+-+-+-+-+-+-+-------+-+-+
 |                                               |O| |I| |S|Z|       | |C|
 |                       X                       | |X| |X| | |   X   |1| |
 |                                               |F| |F| |F|F|       | |F|
 +-------------------+-------------------+-------+-+-+-+-+-+-+-------+-+-+

类似于这样子(从百度百科上面抄了个例子)

struct bs {
unsigned a:4;
unsigned :0 ;/*空域*/
char b:4 ;/*从下一单元开始存放*/
unsigned c:4;
}data;
//a 为unsigned,空域为unsigned

那这样以来岂不是就很好实现,对照讲义可以看出来,这些符号位肯定都是无符号整型,这几个符号位各占一位,其他的都是空域。由于下面有初值问题,所以还要定义一个变量存放初值。由于struct里面是位域,所以只能在struct外面再搞一个struct,然后设置一个变量来存放值。

struct {
    struct {
      uint32_t CF:1;//CF占一位
      unsigned : 5;//之后是5位空域
      uint32_t ZF:1;//ZF占一位
      uint32_t SF:1;//SF占一位
      unsigned : 1;//1位空域
      uint32_t IF:1;//IF占一位
      unsigned : 1;//1位空域
      uint32_t OF:1;//OF占一位
      unsigned : 20;//20位空域
    };
    uint32_t value;//赋初值要用
  }eflags;

然后要设置eflags的初值,在手册第10章,一下子就找到了
在这里插入图片描述

要设置2为初值,在monitor.c中的restart函数里面添加
在这里插入图片描述

从任务6回来,发现自己的sub指令实现的有问题,找了半天的错误,发现逻辑上面没啥错误,然后想到sub指令跟寄存器结构体有关,不会是我结构体实现的有问题把…就反过来检查自己的结构体,思考了一下好像是有点问题的,寄存器的位域是结构体,但是eflags寄存器的初值应该是大家共用的而不是单单只有一个才对,并且最开始union申请空间直接要申请32位的初值空间,这样才可以在这个空间里面定义位域。所以把外面哪个struct改成了union
在这里插入图片描述

实现 6 条基本指令

先把call指令实现:

/* 0xe8 */ IDEXW(J,call,0), EMPTY, EMPTY, EMPTY,

然后找到decode.c文件中的make_DHelper(J)函数,根据i386手册里面第275页,找到call指令的相关伪代码并在make_EHelper(call)函数里面写出来相关操作:
在这里插入图片描述
实现完call指令以后兴致勃勃运行dummy单步执行一下,结果发现了这个情况
在这里插入图片描述
找到Assertion '0' failed这个问题所在,发现是make_DoHelper(SI)这个函数没有实现,然后顿时就懵了,不是说好填完表然后写好执行函数就行了嘛,怎么还要写这个译码函数??然后我在讲义里面找到了这样一句话:

make_Dophelper(name):名为 decode_op_name 的操作数译码函数的原型说明,那这个函数应该是和带符号立即数的进一步译码有关系,虽然还是不太懂其中的原理,但是根据注释还有前面的make_DoHelper(I)可以很快写出来相应的功能,照猫画虎嘛。这里实现的也有问题呀!!! 将在PA2.2中发现并解决
在这里插入图片描述
之后再尝试运行dummy,成功实现。
在这里插入图片描述
现在来实现push指令:

接着上一步继续si告诉我0x55没有实现,就查表找到0x55位置,然后查阅i386手册

在这里插入图片描述

从50到57都是跟push相关的,只要实现32位的就行了,找到make_DHelper(r),那么开始填表,虽然要运行我们这个程序的话填0x55就行了,但是为了以后着想,从0x500x57都要填。

  /* 0x50 */	IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4),
  /* 0x54 */	IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4),

找到data-mov.c文件中的make_EHelper(push)函数,开始实现。

make_EHelper(push) {
  rtl_push(&t0);//把寄存器压栈
  if (id_dest->type == OP_TYPE_REG) { rtl_sr(id_dest->reg, id_dest->width, &t0); }//如果目的操作数是寄存器操作数,写入寄存器
  else if (id_dest->type == OP_TYPE_MEM) { rtl_sm(&id_dest->addr, id_dest->width, &t0); }//如果是在内存里面,写入内存
  else { assert(0); } //不可能出现其他情况了。

  print_asm_template1(push);
}

从第6步回来,怎么想感觉自己都写的没错啊…问题出在哪儿了呢?于是用printf一行一行打印来确定问题所在之处,最后确定到rtl_sm这个函数,写入寄存器的操作出了问题,我不是push ebp么?为啥写入寄存器的值出现问题了啊!返回来从头查看程序代码,发先我的第一行rtl_push(&t0);//把寄存器压栈这是啥?我为什么要把一个啥也不是的temp压栈?我说为什么cpu.ebp一直是0,然后马上更改,给t0赋上了寄存器的值,然后再入栈,最后再写入操作数,就OK了。这里的push仍然有一步实现有问题!!!将在PA2.2中发现并解决
在这里插入图片描述
si测试实现push功能
在这里插入图片描述

但是由上图可知在0x83这个地址处他又不知道干啥了,于是查看反汇编发现是sub指令,接下来就实现sub指令

i386中找到0x83位置,发现要找grp1,在grp1中找到sub,在第6个位置
在这里插入图片描述
在这里插入图片描述

回到我们的表里,找到make_group(gp1,找到第六个EMPTY,改一下,无译码函数,直接执行,v表示字长不定,所以直接用EX
在这里插入图片描述
[外链图片转存失败(img-FPsxdwy0-1562674179640)(图片/033.png)]

arith.c里面找到执行函数,根据手册上面写的伪代码开始实现,一开始完全照着手册上面来实现,写个if写个else,本来觉得没啥问题,结果一跑就出错了,我想想这逻辑也没问题啊,怎么就跑不对了呢?然后就往下翻,还是在这个文件里面,我看到了有个sbb执行的执行函数,哎呦,这个跟sub好像哎,于是我就继续查手册,找到了这个指令的相关内容:

在这里插入图片描述

这跟sub不就差一个CF的问题么,有了sbb那么sub也好实现了,对照着他就可以开始写了:
在这里插入图片描述

写完sub指令发现之前自己的push第3、4、5行完全可以直接调用operand_write函数…我竟然没注意到这个函数!

成功实现

在这里插入图片描述

继续si直到报错

在这里插入图片描述

发现0x31这个地址处的指令没有实现,查看反汇编以后是xor指令,现在就去实现它:

查手册,从0x30开始到0x35都是要实现异或的,0x300x31E->G,反过来对应译码函数要找G2E,0x320x33正好相反要找E2G,0x34要找I2a,0x35要找I2r

在这里插入图片描述

返回到表中开始填写相关位置
在这里插入图片描述

logic.c文件中开始写执行函数

还是查手册获得xor指令怎么写,CF和OF位都是0,目标操作数等于srcdest的异或
在这里插入图片描述
在这里插入图片描述
成功实现

在这里插入图片描述

再运行一步又出现问题了!真是问题不断,这次查阅了反汇编,发现是pop指令没有实现

在这里插入图片描述

i386手册里面,pop就在push的旁边,0x580x5F

在这里插入图片描述
回到我们的opcode表,其实跟push一模一样,译码函数也是r
在这里插入图片描述
就连执行函数也和push的逻辑一模一样,就反过来,一个压栈,一个出栈就行了。

在这里插入图片描述

成功实现

在这里插入图片描述

继续si,这回是0xc3地址处的指令没有实现,还是老惯例,查看反汇编,发现是最后一个ret指令,现在开始实现吧:
老样子查i386手册
在这里插入图片描述

找到opcode0xc3的位置,因为是空白,所以没有译码函数,直接执行。

/* 0xc0 */ IDEXW(gp2_Ib2E, gp2, 1), IDEX(gp2_Ib2E, gp2), EMPTY, EX(ret),

找到之前写call指令的那个文件,开始写make_EHelper(ret)函数

回想理论课中ret指令做了些什么?将eip退栈就行了,并且参照call指令,还要设置跳转标志。
在这里插入图片描述
在这里插入图片描述
成功实现
在这里插入图片描述

对已实现指令增加标志寄存器行为

好像在实现第2个任务的时候,对照着i386手册就直接把这些指令的符号位设置实现了,当时没看到第四个任务…

callpushretpop指令不改变任何标志位

subxor指令改变除了IF的其他标志位

这些都在任务2实现了

运行第一个客户程序

由于上面几个任务的完成,指令的实现,现在可以成功运行这个程序

在这里插入图片描述
不要忘记在all-instr.h里面声明所有的指令执行函数!
在这里插入图片描述

实现 differential testing

common.h中把#define DIFF_TEST的注释取消,讲义用了很长的篇幅来讲解这个东西,但是实现的话却只要一个if语句
在这里插入图片描述
成功发现自己编写失败的指令
在这里插入图片描述

哭着回到第二步检查自己的sub指令的问题…

eflags寄存器回来,再次测试

在这里插入图片描述

这回sub没问题了,push又出现了问题,然后回到第二步,之后就可以成功运行了。

利用 differential testing 检查已实现指令

在经历了任务6以后,成功实现对了所有的指令
在这里插入图片描述
PA2.1的内容到此结束,感谢您的耐心阅读,文中加黑加粗的写错的地方将会在后来的PA完成过程中更改。

原创文章 7 获赞 37 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_41983842/article/details/95232055
2.1